Repository navigation
feat(download-guard): refuse disk-writing shell downloads when a forwarder is configured - #511
Open
BeiWay1145 wants to merge 1 commit into
Open
BeiWay1145 wants to merge 1 commit into
BeiWay1145 wants to merge 1 commit into
Conversation
…arder is configured Closes T-Auto#506. The model reaches for `curl -o` / `wget -O` / `Invoke-WebRequest -OutFile` because those commands work. They also write one connection at a time: on the reporting machine the same ISO moved at 0.13 MB/s that way and 11.78 MB/s through a downloader built for the job, roughly 90x. The rule that says "use the downloader" has only ever existed as prose, and prose did not stop it. This row moves the rule into the runtime, with one deliberate difference from the version that motivated the issue: it never names a downloader. The operator supplies a command template, and every part of the refusal is derived from it. An unconfigured install gets no listener at all. WHY THE DECISION IS SPLIT FROM THE DETECTION `src/download-guard/detect.ts` is a pure function over a command string, with no host dependency, so the rules can be tested exhaustively without mounting anything. `src/download-guard/policy.ts` turns a hit plus the operator's config into a decision. Keeping the refusal out of the detector is what makes "does this look like a download" a question that can be answered by evidence rather than by policy taste. FALSE NEGATIVES ARE THE SAFER ERROR Missing a download leaves the status quo. Refusing an ordinary command breaks a working session. So every rule requires a WRITE signal, and the suite of must-allow cases is deliberately larger than the must-refuse one. It includes the shapes that a naive rule gets wrong: `curl URL` printing to stdout, `-o -` and `wget -qO-` streaming into a pipe, `node download.cjs --help` (the word "download" proves nothing about where bytes go), redirecting *from* a file, and `Invoke-RestMethod` returning an object instead of writing one. SCOPE AND ITS LIMIT Shell tools only, and `script_path` is skipped outright: the file's contents are not in the call, and guessing at them would be the false positive this row is trying to avoid. WHAT IS VERIFIED node test/run.mjs download-guard download-guard-detect 56 checks download-guard-policy 15 checks download-guard-row 7 checks (real cordis waterfall) download-guard-mount 5 checks (through apply(), the composition root) The mount suite is the one that matters for integration: it proves the row is reached from `src/index.ts`, that the `download-guard` config section resolves, and that `enabled: false` is honoured even when a template is present. `node scripts/validate-manifest.mjs` - 84 checks. `node test/run.mjs manifest config host-metadata` - 8 suites. `node scripts/check-file-size.mjs` - largest new file 242 lines, total implementation 428. NOT VERIFIED, AND STATED SO RATHER THAN IMPLIED The full suite has 13 failing suites on this machine, none of them related to this change: they need a Rust worker (`npm run build:ptc`), a bash-ipc provenance receipt, or npm's default cache directory, which this machine denies (EPERM). None touches the download guard.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #506.
What this does
Adds a
download-guardrow that refuses a shell call which writes a download todisk, and names the faster downloader the operator configured instead.
The trigger for the issue was a measurement: the same ISO moved at 0.13 MB/s
through a model-invoked
curl -oand 11.78 MB/s through a downloader builtfor the job — about 90x. The rule "use the downloader" has only ever existed as
prose, and prose did not stop it.
This does not hardcode any downloader. The operator supplies a command
template containing
{url}(and optionally{file}); everything in the refusalis derived from it. With no template the row registers no listener at all.
Structure
src/download-guard/detect.tssrc/download-guard/policy.tssrc/components/download-guard/index.tstools/pre-executelistener.Splitting detection from the decision is what lets the rules be tested
exhaustively without mounting anything, and keeps "does this look like a
download" answerable by evidence rather than by policy taste.
False negatives are the safer error
Missing a download leaves the status quo; refusing an ordinary command breaks a
working session. So every rule requires a write signal, and the must-allow
suite is larger than the must-refuse one. It covers the shapes a naive rule gets
wrong:
curl URL→ stdout, not a filecurl -o -andwget -qO-→ streams into a pipenode download.cjs --help→ the word "download" proves nothingsort < input.txt→ redirecting from a fileInvoke-RestMethodwithout-OutFile→ returns an object, writes nothingscript_pathcalls → the file's contents are not in the call, so it is skippedrather than guessed at
Verification
download-guard-mountgoes throughapply()— the real composition root — so itfails if the row stops being reached from
src/index.ts, if thedownload-guardconfig section stops resolving, or ifenabled: falsestopsbeing honoured when a template is present.
Red/green: removing the listener registration fails
rowandmount; revertingdetect.tsto a "mentions a downloader" rule fails 5 must-allow cases; dropping{url}validation fails the config assertion.Size
Implementation 428 lines across 3 files (largest 242), tests 324, docs 2 lines
per language,
package.jsonexports 4 lines. Well under both the 1000-line filecap and the ~1000 implementation-line PR budget.
Note on the full suite
On this machine 13 suites fail, none related to this change: they require a
Rust worker (
npm run build:ptc), a bash-ipc provenance receipt, or npm'sdefault cache directory which this machine denies (EPERM). I did not run
npm run verify-gateto completion for that reason, and I am stating it ratherthan implying a green gate.
Follow-up, not in this PR
Issue #507 (blind
sleep→job_output(wait)) is a separate change and is notpart of this slice. A design for its prompt-injection layer is being prepared
separately, as the maintainer asked.