fix(sandbox): classify git push as network access - #726
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughGit network detection now handles value-taking global options, remote archives, wrappers, and platform-specific Git executables. Parsed and unparseable network commands receive network classification, with regression coverage for approved ChangesSandbox network classification
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant Agent
participant SandboxClassifier
participant GitAnalyzer
participant GitProcess
Agent->>SandboxClassifier: request git push
SandboxClassifier->>GitAnalyzer: classify Git subcommand
GitAnalyzer-->>SandboxClassifier: critical network risk
SandboxClassifier-->>Agent: request network approval
Agent->>GitProcess: execute approved git push
GitProcess-->>Agent: return command output
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Pull request overview
This PR updates the sandbox command analyzer/risk classifier to treat git push as network-sensitive, with added tests to ensure the AST-based analyzer flags it even when the command doesn’t contain an obvious URL.
Changes:
- Extend
commandUsesNetworkto classifygit pushas network access. - Add analyzer coverage for
git push(and a non-networkgit commit) inAnalyzeCommandtests. - Add a risk-classifier hardening test asserting
git pushis flagged as critical+network when regex-based detection would miss it.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 5 comments.
| File | Description |
|---|---|
| internal/sandbox/analyzer.go | Expands git subcommand network detection to include push. |
| internal/sandbox/analyzer_test.go | Adds AnalyzeCommand test cases for git push and a local-only git commit. |
| internal/sandbox/risk_hardening_test.go | Adds a hardening test to ensure AST-based classification flags git push as network. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
🧹 Nitpick comments (1)
internal/sandbox/risk_hardening_test.go (1)
297-309: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valuePrefer
t.Errorfovert.Fatalfin loops.Using
t.Fatalfinside a loop will immediately abort the test on the first failure, which prevents the remaining test cases from executing. Replacing it witht.Errorfallows all cases to be evaluated even if one fails.♻️ Proposed refactor
for _, command := range []string{ `curl https://example.com && "unterminated`, `git fetch origin && "unterminated`, `git pull origin main && "unterminated`, `git push gitlawb://example.com/repo.git main && "unterminated`, } { risk := classifyCommand(command) if !HasRiskCategory(risk, "unparseable_command") { - t.Fatalf("Classify(%q) = categories %v; want unparseable_command", command, risk.Categories) + t.Errorf("Classify(%q) = categories %v; want unparseable_command", command, risk.Categories) } if risk.Level != RiskCritical || !HasRiskCategory(risk, "network") { - t.Fatalf("Classify(%q) = level %s, categories %v; want critical network", command, risk.Level, risk.Categories) + t.Errorf("Classify(%q) = level %s, categories %v; want critical network", command, risk.Level, risk.Categories) } }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@internal/sandbox/risk_hardening_test.go` around lines 297 - 309, In the table-driven loop testing classifyCommand, replace both t.Fatalf calls with t.Errorf so each command case is evaluated even when an earlier assertion fails.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@internal/sandbox/risk_hardening_test.go`:
- Around line 297-309: In the table-driven loop testing classifyCommand, replace
both t.Fatalf calls with t.Errorf so each command case is evaluated even when an
earlier assertion fails.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 45f38035-3958-4d77-b84f-f163855e8c49
📒 Files selected for processing (5)
internal/agent/loop_test.gointernal/sandbox/analyzer.gointernal/sandbox/analyzer_test.gointernal/sandbox/risk.gointernal/sandbox/risk_hardening_test.go
Vasanthdev2004
left a comment
There was a problem hiding this comment.
Thanks for this, and the direction is right (git push should be network-gated). But the AST classifier still misses the most common git form, so I would like a fix before it lands.
The git branch of commandUsesNetwork calls firstSubcommand, which skips only dash-prefixed and numeric tokens. git's value-consuming global options put their value in the NEXT token, so firstSubcommand returns that value as the "subcommand." I ran the classifier against the current head:
git push origin main Network=true Risk=critical network <- correct
git -C repo push origin main Network=false Risk=high (none) <- missed
git -c http.sslVerify=false push Network=false Risk=high (none) <- missed
git --git-dir /x/.git push Network=false Risk=high (none) <- missed
git.exe push origin main Network=false Risk=high (none) <- missed
These all parse cleanly, so TooComplex stays false and the unparseable-pattern fallback never runs. So git -C <dir> push (the canonical form for operating on a repo without cd) classifies as plain shell, not network, and its risk drops from Critical to High.
To be fair on severity: this is not an always-open egress hole. When the sandbox backend is provisioned, the runtime deny-by-default still blocks the socket and raises the network prompt via ReasonNetworkBlocked, so the classifier is defense-in-depth there. But it becomes a real unprompted-egress path when the backend is unavailable or degraded, and the Critical-to-High mis-level can flip auto-allow in the more permissive autonomy modes regardless. Since the whole point of the PR is to classify these, I would rather close the gap than ship a gate that misses the most common invocation.
The fix looks small:
- In the git case, skip the values of git's space-separated value-consuming globals (-C, -c, --git-dir, --work-tree, --namespace, --exec-path, --super-prefix) before taking the subcommand. The joined --git-dir=/x form is already fine since it is one dash-prefixed token.
- Normalize a .exe program token so git.exe is treated as git.
- Add a PARSEABLE regression test: classifyCommand("git -C repo push origin main") should be RiskCritical with the network category. Right now the only -C test is the one with the trailing
&& "unterminated, which forces the unparseable path and masks this AST gap.
Otherwise the wiring is fine, and build/vet/gofmt are clean locally. Happy to re-review quickly once the AST path handles the option forms.
gnanam1990
left a comment
There was a problem hiding this comment.
APPROVE — the core fix is correct and well-covered: git clone|fetch|pull|push now classify as network access on the primary AST path, verified end-to-end (git push origin main → Network=true, git commit -m x stays Network=false), and the hardened regex fallback fails closed on unparseable variants. The one remaining gap is a minor consistency issue, not merge-blocking.
Nice work: the AST change (analyzer.go:153) and the fallback hardening (risk.go:36, now git(?:\s+[^\s;&|]+){0,8}\s+(clone|fetch|pull|push)) are both landed, and the new tests are real — TestAnalyzeCommand (git fetch/pull/push-custom-transport → network), TestClassifyASTCatchesNetworkProgramsRegexMisses, and TestClassifyUnparseableNetworkCommandFailsClosed including the git -C repo push … && "unterminated fail-closed case. All PR-relevant classification tests pass locally.
[Minor] AST path doesn't skip git global options, so git -C <dir> push isn't classified as network — inconsistent with the fallback you just hardened
internal/sandbox/analyzer.go:153 (root cause: firstSubcommand, analyzer.go:243) — pre-existing blind spot, PR-introduced inconsistency
The three reported findings all collapse to this single root cause. firstSubcommand skips dash-prefixed tokens but treats the next bare token as the subcommand, so for git -C /repo push origin main (words [-C, /repo, push, …]) it returns /repo, not push. The git case then returns Network=false. Runtime probe on the PR HEAD:
git push origin main Network=true TooComplex=false
git -C /repo push origin main Network=false TooComplex=false <- gap
git -c http.proxy=x push origin main Network=false TooComplex=false <- gap
git -C /repo fetch origin Network=false TooComplex=false <- gap
git -C /repo pull origin main Network=false TooComplex=false <- gap
git --git-dir=/repo/.git push origin main Network=true TooComplex=false (caught: --foo starts with '-')
The gap is specifically the space-separated value-taking global options (-C <path>, -c <name=value>, --git-dir <path>, --work-tree <path>, --namespace <ns>, --exec-path <path>). Because these commands parse cleanly (TooComplex=false), the hardened unparseableNetworkPattern at risk.go:36 is never consulted — that branch is gated on analysis.TooComplex at risk.go:132. So the fallback now tolerates git -C repo push but the primary AST path does not: the two paths disagree, and the network category the PR exists to add is silently omitted for the very common git -C <dir> push/fetch/pull form.
Impact is bounded — this is not a network-exfiltration bypass. Network enforcement mode is derived from policy.Network via NormalizeNetworkMode at profile.go:109, decoupled from the analyzer, and the auto-allow branch at engine.go:410 is gated on shellSandboxActive → NativeIsolation. So a misclassified git -C . push still runs wrapped by the platform sandbox with NetworkDeny enforced at the syscall level (the connect() is blocked and the agent reactively prompts), and where no native sandbox is active it prompts anyway via the general path rather than auto-allowing. The only real-world effect is degrading a proactive ReasonNetworkBlocked prompt (engine.go:355/357) into a reactive/generic one, plus the AST↔regex inconsistency.
Provenance: the underlying firstSubcommand blind spot is pre-existing — base analyzer.go:153 was firstSubcommand(words, nil) == "clone" and had the same hole for git -C <dir> clone. What this PR introduces is the inconsistency: it extended classification to push/fetch/pull and explicitly closed the -C gap in the regex fallback (and tests it), but left the primary AST path unfixed.
Suggested fix: give the git case a dedicated subcommand resolver that consumes git's global value-taking options before reading the subcommand (-C <path>, -c <name=value>, --git-dir, --work-tree, --namespace, --exec-path in their separate-token form), mirroring the tolerance already in unparseableNetworkPattern, then test the resolved token against {clone,fetch,pull,push}. Add git -C repo push origin main (plus -c / fetch / pull variants) to TestClassifyASTCatchesNetworkProgramsRegexMisses and TestAnalyzeCommand — those assertions fail today and would pin the fix.
Tests: go build ./..., go vet on the touched packages, and gofmt -l are clean; all PR-relevant classification/agent tests pass. The failing tests in internal/sandbox and internal/agent (path/symlink/out-of-workspace under /private/tmp) are pre-existing environmental failures that reproduce identically on base — not PR-attributable.
Merge is kevin's call per the program gate.
jatmn
left a comment
There was a problem hiding this comment.
I found issues that need to be addressed before this is ready.
Findings
-
Resolve the merge conflict with
main
GitHub currently reports this PR asCONFLICTING/DIRTY, so it cannot be merged or tested in its target-branch composition. Rebase or merge the current target branch and resolve the conflict before requesting another review. -
[P2] Classify Git commands after consuming global-option values
internal/sandbox/analyzer.go:153
The genericfirstSubcommandskips-C/-c/--git-dirthemselves but treats their following values as the subcommand. Consequently ordinary, parseable commands such asgit -C repo push origin main,git -c http.proxy=x fetch origin, andgit --git-dir /repo/.git pullproduce nonetworkrisk. Because they parse successfully, the new regex fallback is not consulted, and the engine skips its required proactive network-approval path. Use a Git-aware resolver that consumes value-taking global options (asinternal/agent/command_prefix.goalready does) and add parseable regression coverage. -
[P2] Recognize the Windows
git.execommand spelling
internal/sandbox/analyzer.go:152
effectiveProgramnormalizesgit.exetogit.exe, notgit, sogit.exe push origin mainnever reaches this new Git network classifier. It is parseable, so the fallback cannot repair the miss and the command does not receive the intended proactive network prompt. Normalize executable suffixes (or explicitly handlegit.exe) and cover that spelling in the analyzer and risk tests.
|
Merged current Resolve the merge conflict with [P2] Classify Git commands after consuming global-option values (@jatmn, @gnanam1990, @Vasanthdev2004 — all three findings share this root cause) — fixed with a git-aware
[P2] Recognize the Windows Parseable regression coverage — the key point from @Vasanthdev2004's review was that the only @copilot: "the approved-prompt behavior may need an execution-path change, not just classification" — checked, and no execution change is needed. The turn network grant already applies on approval; the missing piece was purely classification, so the command never reached that path. @copilot: use a Validation (Windows host, Go 1.26.5): @jatmn @Vasanthdev2004 @gnanam1990 — ready for another look; I can't use the reviewer-request button on this repo, hence the mention. @coderabbitai review |
|
✅ Action performedReview finished.
|
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
internal/sandbox/risk_hardening_test.go (1)
337-342: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick winCover
git.exein the unparseable network fallback.
unparseableNetworkPatternmatchesgitonly, so an unparseablegit.exe push ...misses the criticalnetworkcategory even though parseablegit.exeis classified correctly. Accept an optional.exesuffix and add that regression case.Proposed fix
-|\bgit(?:\s+[^\s;&|]+){0,8}\s+(clone|fetch|pull|push)\b +|\bgit(?:\.exe)?(?:\s+[^\s;&|]+){0,8}\s+(clone|fetch|pull|push)\b+ `git.exe push gitlawb://example.com/repo.git main && "unterminated`,As per coding guidelines,
**/*_test.gorequires regression tests for behavior changes.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@internal/sandbox/risk_hardening_test.go` around lines 337 - 342, Update the unparseableNetworkPattern to match both git and git.exe command names while preserving the existing network-command requirements. In the risk-hardening regression table in the relevant test, add an unparseable git.exe push case so it is classified under the network category.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@internal/sandbox/risk_hardening_test.go`:
- Around line 337-342: Update the unparseableNetworkPattern to match both git
and git.exe command names while preserving the existing network-command
requirements. In the risk-hardening regression table in the relevant test, add
an unparseable git.exe push case so it is classified under the network category.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 3b9a21d6-40d7-4443-869c-0714562d2150
📒 Files selected for processing (5)
internal/agent/loop_test.gointernal/sandbox/analyzer.gointernal/sandbox/analyzer_test.gointernal/sandbox/risk.gointernal/sandbox/risk_hardening_test.go
🚧 Files skipped from review as they are similar to previous changes (2)
- internal/sandbox/risk.go
- internal/agent/loop_test.go
jatmn
left a comment
There was a problem hiding this comment.
I found issues that need to be addressed before this is ready.
Findings
-
[P1] Recognize
--attr-sourceas a value-taking Git global option
internal/sandbox/analyzer.go:308
Git accepts--attr-source <tree-ish>before the subcommand (for example,git --attr-source HEAD push origin main), but it is absent from this new skip list.gitSubcommandtherefore returnsheadinstead ofpush; because this command parses successfully, the fallback is not consulted and the shell call never receives the criticalnetworkclassification or its network-enabled approval profile. Add this global option and a regression case; update the paired command-prefix parser too if the documented mirroring is intentional. -
[P2] Make the unparseable Git fallback cover the supported Windows form and option count
internal/sandbox/risk.go:36
The new AST path normalizesgit.exe, but parser-failing Windows commands never reach that code. For example,git.exe push origin main & rem 'runs undercmd.exebut is rejected by the POSIX parser; this pattern requires whitespace immediately aftergit, so classification adds onlyunparseable_command, notnetwork, and skips the network approval/turn-grant path. The{0,8}cap has the same failure once a Git invocation has more than four value-taking global options. Match an optional.exesuffix and scan Git tokens up to a command separator without the arbitrary cap, with regressions for both forms.
…allback gitSubcommand (and its mirror in internal/agent/command_prefix.go) was missing --attr-source from git's value-taking global options, so `git --attr-source HEAD push origin main` resolved to the wrong subcommand and never got classified as network access. The unparseable-command regex fallback used when the shell parser fails now also matches an optional .exe suffix, so a Windows form like `git.exe push origin main & rem '` — valid under cmd.exe but rejected by the POSIX parser AnalyzeCommand uses — still classifies as network. The generic-token scan before the subcommand no longer caps at 8 tokens; Go's regexp package is RE2-backed (linear time, no backtracking blowup), so the cap only served to silently drop coverage once a git invocation had more than a handful of value-taking global options. Addresses review feedback from jatmn on PR Gitlawb#726. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@internal/sandbox/risk.go`:
- Around line 36-44: The unparseable Git fallback in unparseableNetworkPattern
must not treat arbitrary tokens before push, fetch, or pull as global options.
Restrict matching to recognized Git global options and their values, or reuse
the shared token-aware resolver from analyzer.go, while preserving support for
git.exe and complex valid invocations. Add a regression test covering a local
command such as git status push so it is not classified as network.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 59e3c8be-fb6a-46bd-8c23-15d475e08ebd
📒 Files selected for processing (5)
internal/agent/command_prefix.gointernal/sandbox/analyzer.gointernal/sandbox/analyzer_test.gointernal/sandbox/risk.gointernal/sandbox/risk_hardening_test.go
🚧 Files skipped from review as they are similar to previous changes (2)
- internal/sandbox/risk_hardening_test.go
- internal/sandbox/analyzer.go
jatmn
left a comment
There was a problem hiding this comment.
I found issues that need to be addressed before this is ready.
Findings
- [P2] Do not treat arbitrary Git arguments as global options in the fallback
internal/sandbox/risk.go:44
The unbounded(?:\s+[^\s;&|]+)*portion consumes any token before a network verb. For example,git status push & rem 'is validcmd.exeinput but fails the POSIX parser;pushis only agit statuspathspec, yet the regex marks the command as critical network. That produces a network prompt and, on approval, a network-enabled turn profile for a local command. Restrict this span to the supported Git global options and their values, with a negative regression test.
|
Pushed 708c2c2 to address the remaining outstanding finding (from @coderabbitai and @jatmn): the unparseable-command regex fallback in Fix: the skipped span before the subcommand verb is now restricted to git's actual global options, mirroring Added Validation:
All previously reported findings (the |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- changed files in 708c2c2 ---'
git show --stat --oneline --find-renames 708c2c2
printf '%s\n' '--- Go declarations in affected files ---'
ast-grep outline internal/sandbox/risk.go --items all --type function --match 'Classify|Network|Git'
ast-grep outline internal/sandbox/analyzer.go --items all --type function --match 'git|Git|commandUsesNetwork'
ast-grep outline internal/sandbox/risk_hardening_test.go --items all --type function --match 'TestClassifyUnparseable'
printf '%s\n' '--- exact patch ---'
git show --format=fuller --find-renames --unified=80 708c2c2 -- \
internal/sandbox/risk.go \
internal/sandbox/risk_hardening_test.go \
internal/sandbox/analyzer.go
printf '%s\n' '--- related test cases and option helpers ---'
rg -n -C 12 \
'TestClassifyUnparseableNonGitOptionTokenStaysNonNetwork|TestClassifyUnparseableNetworkCommandFailsClosed|unparseableNetworkPattern|gitGlobalOptionConsumesValue|gitSubcommand' \
internal/sandbox/risk.go \
internal/sandbox/risk_hardening_test.go \
internal/sandbox/analyzer.goLength of output: 33304
The new regression test correctly asserts If you found this review helpful, would you consider giving us a shout-out on X? Thank you for using CodeRabbit! |
e887d2b to
10cbacd
Compare
|
Rebased this branch onto current Addressed the outstanding review blockers:
Validation completed:
|
jatmn
left a comment
There was a problem hiding this comment.
I found issues that need to be addressed before this is ready.
Findings
-
[P1] Fail closed for dynamic
env -Ssource
internal/sandbox/analyzer.go:168
The new split-string path is attempted only when every shell word is literal. ConsequentlyPAYLOAD='curl https://...'; env -S "$PAYLOAD"falls through to ordinary wrapper handling:effectiveProgramconsumes-Sand its operand, then reports no executable. GNUenv -Sreceives the shell-expanded value and executes its argv, so a network command reachesEngine.Evaluatewithout thenetworkcategory or the turn-scoped network grant. The fallback has the same failure mode: an opaque split string is returned as recognized-but-clean.The root cause is using literal reconstruction as both an optimization and a proof of safety. Keep an explicit
opaque/environment-dependent result for the split-string operand and propagate it through both AST and fallback paths. If the resulting executable or any command-defining token cannot be resolved statically, classify it as network-sensitive; add engine-level coverage for a shell-expanded-Soperand and for a child environment expansion such asCMD=curl env -S '${CMD} https://...'. -
[P1] Treat opaque PowerShell command source as network-sensitive
internal/sandbox/analyzer.go:201
fallbackPowerShellPayloadmarks-EncodedCommandas opaque, and this branch deliberately ignores opaque payloads. A valid UTF-16LE/Base64 payload forInvoke-WebRequest https://...therefore parses successfully but gets nonetworkrisk or approval prompt. The same loss occurs forpowershell -Command "$PAYLOAD":wordTextsdrops the expansion and the payload becomes empty. Neither case reaches the unparseable fallback because the outer POSIX command parses successfully. This defeats the Windows classifier-based network boundary.Do not use “cannot inspect” as “does not use network” for an interpreter argument that is executable source. Model PowerShell payload extraction as
(payload, opaque), inspect bounded valid encoded input if practical, and otherwise addnetworkwhenopaqueis true or the selected Command/CommandWithArgs operand is nonliteral. Cover valid encoded network source and shell-expanded Command source throughEngine.Evaluate, not only classifier helpers. -
[P1] Continue parsing START switches after its title
internal/sandbox/risk.go:383
CMD permits switches after the optional title, e.g.start "" /b curl https://....cmdStartPayloadTokenInfofirst callscmdStartOperandsTokenInfo, which stops at the quoted title; it then drops that title and returns/b curl .... The CMD resolver treats/bas the executable, while the POSIX resolver sees onlystart, so neither contributes a network classification. Adding& rem 'makes this valid CMD form use the intended unparseable fallback and it receives no prompt underNetworkDeny.Parse START's grammar as one invocation rather than as a one-pass prefix strip: consume its supported switches, consume an optional quoted title, then consume supported switches again before selecting the command. Preserve value-taking switch handling (
/d,/node,/affinity) in both passes and add engine-level regressions for title-before-/b, title-before-/wait, and title-before-/d <path>. -
[P2] Do not consume an argument after bare
--exec-path
internal/sandbox/analyzer.go:465
Git's bare--exec-pathis terminal (--exec-path[=<path>]):git --exec-path /tmp pushprints the local exec path and exits; it neither treats/tmpas an option value nor invokespush. The shared helper instead says bare--exec-pathconsumes the following token, soparseGitInvocationskips/tmp, reachespush, and marks the local informational command as network-sensitive. This is a regression from the previous first-subcommand scan and causes an unnecessary prompt and temporary network grant.Separate Git globals into terminal options, exact separated-value options, and inline-value-only options. Put bare
--exec-pathin the terminal class and retain--exec-path=<path>as a nonterminal inline form. Exercise both AST and fallback classification withgit --exec-path,git --exec-path /tmp push, andgit --exec-path=/tmp pushso the shared parser stays aligned with Git's actual option grammar.
10cbacd to
ba5771d
Compare
|
Addressed all four findings from the review on Findings[P1] Fail closed for dynamic An unresolvable split string was returned as recognized-but-clean. [P1] Treat opaque PowerShell command source as network-sensitive
[P1] Continue parsing START switches after its title
[P2] Do not consume an argument after bare Moved to Two test expectations changed
New regressions run through Validation
Open itemThe |
jatmn
left a comment
There was a problem hiding this comment.
I found issues that need to be addressed before this is ready.
Findings
-
[P1] Recognize abbreviated GNU
env --split-stringoptions
internal/sandbox/safe_command.go:522
GNUenvaccepts unambiguous long-option abbreviations:env --split 'curl https://evil.test'executescurljust asenv --split-string …does.envSplitOptionrecognizes only the exact long spelling (and its=form), soenvSplitCommandFieldsreturns unrecognized for this valid invocation. The normal AST path then skips--splitas an ordinary wrapper option and treats the whole quoted payload (curl https://evil.test) as one executable name; it never matchescurl, adds nonetworkcategory, and the engine therefore does not apply theNetworkDenyprompt.The root problem is modelling only selected spellings instead of the runtime option grammar. Centralize recognition of the supported GNU
env-Saliases—including accepted unambiguous long prefixes and joined values—or conservatively mark an unresolved split-string form as network-sensitive. Please add parsed-command engine regressions for--split <payload>and--split=<payload>, plus negative cases that prove non-split options and literal payload text do not receive a network grant. -
[P1] Fail closed when BusyBox or strace receives a dynamic child executable
internal/sandbox/analyzer.go:292
The new wrapper handling callswordTexts(args)before passing arguments tobusyboxCommandArgsandstraceCommandArgs;wordTextdeliberately removes shell expansions. AfterAPPLET=curl; busybox "$APPLET" https://evil.test, the second command parses successfully, but the wrapper resolver receives an empty child-program token and returns no network use. The command therefore never reaches the unparseable fallback, despite BusyBox executingcurl.strace "$APPLET" https://evil.testhas the same failure mode. An approved shell command can consequently egress without the network prompt or temporary network overlay.The root cause is losing the distinction between a literal child argv and an unknown one before interpreting a launcher. Preserve literalness alongside token text for delegated BusyBox/strace argv, and fail closed when the executable position or option parsing depends on an expansion rather than treating it as a clean unknown program. Please add engine-level regressions for dynamic BusyBox and strace child executables, together with literal non-network controls, so both the AST and fallback paths retain the same network-consent contract.
ba5771d to
58f6fcd
Compare
|
Pushed Fixed
Also found and fixed
Validation
Re-verified everything from earlier rounds (function-definition regression, |
jatmn
left a comment
There was a problem hiding this comment.
I found issues that need to be addressed before this is ready.
Findings
-
[P1] Classify CMD launcher payloads on the normal parseable path
internal/sandbox/analyzer.go:191
This change adds CMD-specific network classification, but the CMD resolver is only reached frommatchesUnparseableNetwork, whichclassifyWithScopecalls only after the POSIX parser reportsTooComplex. That leaves the normal AST path with no equivalent branch forcmd,call, orstart.On Windows, Zero passes bash-tool command text verbatim to
cmd.exe /d /c. An ordinarycmd /c git push origin main(and likewisecmd /c curl ...) is valid POSIX syntax, soAnalyzeCommandsuccessfully produces a call whose executable is onlycmd; it neither inspects/c's payload nor setsAnalysisResult.Network. Since the fallback is skipped, the engine can accept the command underNetworkDenyor an already-approved shell permission without the network prompt this PR is intended to provide.Please make command-text interpreter handling a shared classification step rather than a fallback-only feature: extract the CMD payload from the AST arguments, recurse into it with the same bounded/fail-closed rules used for shell and PowerShell source, and reuse that extraction in the fallback. Add engine-level regressions for normal parseable
cmd /c git push,cmd /c curl,call git push, andstartforms, not only examples made unparseable withrem '. -
[P1] Fail closed for CMD-expanded command names in fallback resolution
internal/sandbox/risk.go:154
The new fallback resolver resolves the command name literally, while CMD expands it before executing it. For example,set "N=curl" & call %N% https://evil.test & rem 'reaches the fallback because of the CMDremtail; CMD expands%N%tocurl, butfallbackBodyUsesNetworksees%N%as an unknown executable and emits nonetworkcategory. The same bypass works withN=gitfollowed by%N% push ...; delayed expansion (!N!) creates the equivalent problem when enabled.The root cause is that dynamic-token detection is both incomplete (
fallbackTokenLooksDynamiconly recognizes$and backticks) and applied only after BusyBox/strace have selected a child command. CMD resolution can produce a dynamic executable directly throughcall,cmd /c, orstart, so it reachesfallbackBodyUsesNetworkbefore that special-case check.Please centralize executable-token uncertainty handling at the point
fallbackBodyUsesNetworkselects its program. Recognize CMD%VAR%,%VAR:...%, and delayed!VAR!forms as unresolved executable text, and conservatively classify them as network-sensitive before testing program names. Apply the same rule after CMD'scall,/c, andstartpayload extraction. Add positive tests for each launcher plus negative tests proving literal percent/bang characters that cannot be expansions are not unnecessarily gated. -
[P1] Keep the fallback aligned with the AST check for dynamic
sh -cpayloads
internal/sandbox/risk.go:212
The new fallback recursion is missing the AST path's existing unresolved-payload check. Forsh -c "$PAYLOAD" & rem ', CMD executes the first segment while the unmatched quote routes classification through the fallback.shexpandsPAYLOADto a command such ascurl ...orgit push ..., but the fallback callsmatchesUnparseableNetworkAton the literal$PAYLOAD, which has no recognized network program, and returns no network risk.This is a parity bug introduced by adding the new fallback shell-payload recursion without carrying over
analyzeInto's!isLiteralWord(...) => Networkrule. The fallback cannot evaluate shell expansions, so recursively treating their spelling as shell source is not a safe substitute for recognizing that the executed command is unknown.Please use one common “command source is statically inspectable” check for AST and fallback interpreter payloads. When a selected
-cpayload contains an unresolved variable, command substitution, or equivalent dynamic source, classify it as network-sensitive before recursion; only recursively parse known literal payloads. Include fallback regressions for$VAR,${VAR}, command substitution, and a local literalsh -c 'printf ok'control. -
[P1] Preserve or fail closed on
env -Svariables used by a command-bearing argument
internal/sandbox/safe_command.go:686
splitEnvStringexpands${NAME}using Zero's own process environment and only marks the invocation unsafe when the final executable token is dependent. It does not receive shell assignment prefixes (CallExpr.Assigns) and therefore cannot model the environment that GNUenvactually receives. It also drops dependency state once the resolved executable is known, even when a later argument becomes interpreter source.For example,
PAYLOAD='curl https://evil.test' env -S 'sh -c "${PAYLOAD}"'exportsPAYLOADto GNUenv; GNUenv -Sexpands it, then invokessh -c 'curl https://evil.test'. The classifier instead reads Zero's ambientPAYLOAD(typically empty), resolvessh -c "", and skips the network prompt. The sameenvSplitCommandpath is used by the newly expanded fallback handling.Please avoid evaluating target-command variables from the reviewer/agent process environment. Carry the command's assignment environment and an explicit taint/unknown bit through split-string parsing, including nested
env -Srewrites. Any dynamic or assignment-derived field that controls an executable or interpreter payload should fail closed; a dependency in an ordinary inert argument can remain precise. Add AST and unparseable-fallback regressions for prefix assignments feeding direct executables andsh -cpayloads, plus controls for known local literals.
|
Addressed jatmn's latest review in
Regression coverage includes AST and fallback parity plus Validation:
Environment limitations: |
jatmn
left a comment
There was a problem hiding this comment.
I found issues that need to be addressed before this is ready.
Findings
-
[P1] Rebase this branch onto the declared base before merging
internal/providers/oauth.go:1
2d2450e(the currentmainbase) is not an ancestor off6435a6; their merge base iscabfeef. The submitted base-to-head diff therefore includes apparent reversions of #890's provider, model-discovery, agent, and TUI work across 37 unrelated files, including the OAuth helper and/fast/service-tier support, even though the two branch commits only concern sandbox classification. The repository explicitly treats this stale-base state as a hard blocker; whether GitHub's three-way merge retains a particular base-side hunk is not a substitute for reviewing an up-to-date resolved diff. Rebase onto2d2450eand preserve the base-side behavior during conflict resolution, rather than carrying these unrelated paths forward from the old merge base. Then request review of the resolved complete diff, because the classifier changes overlap a security-sensitive area and cannot be safely reviewed as two old commits in isolation. -
[P1] Classify quoted CMD command source before resolving its executable
internal/sandbox/analyzer.go:690
A parseablecmd /c "git push origin main"executesgit push, butcmdLauncherUsesNetworkpasses the entire quoted/cpayload tofallbackBodyUsesNetworkas one token.fallbackBodyUsesNetworkconsequently treatsgit push origin mainas the executable name rather than parsing it as command source, so it never reachesgitUsesNetwork; the request has only the normalshellrisk and is allowed underNetworkDeny(or an already-approved shell permission) without a network prompt or temporary grant. The unquoted AST test does not cover this because its arguments are already separate words.The root cause is using an argv-oriented CMD resolver for a field that CMD treats as interpreter source. Make command-source extraction a shared operation: once
/cor/kselects a payload, reclassify the payload as bounded command text (and fail closed if it cannot be read), rather than passing a quoted payload through the executable-token path. Reuse that operation in both the normal AST and unparseable fallback paths so their handling cannot diverge. AddEngine.Evaluateregressions for quotedcmd /c "git push …", quoted remotegit archive, and a non-network quoted local command. -
[P1] Keep a literal
env -Spayload classified when an unrelated trailing argument is dynamic
internal/sandbox/analyzer.go:168
env -S 'git push origin main' "$EXTRA"is parseable and GNUenvexecutes the static split-string command while appending the expanded trailing argument. BecauseliteralCallFieldsrequires every call argument to be literal, the unrelated trailing expansion bypassesenvSplitCommandFields;envSplitSourceDynamiconly considers whether the split-string operand itself is dynamic, andeffectiveProgramthen consumes-Sand finds no program. The result has no network category, so the push bypasses the approval boundary. The same shape hidescurland other static split-string commands.The root cause is conflating literalness of the whole shell call with inspectability of the command-defining
-Sfield. Preserve literalness/taint per argument while locating and expandingenv -S: resolve a literal split-string payload even when later inert argv is dynamic, and fail closed only when the executable position or an interpreter-source position depends on an unresolved value. Keep the AST and fallback paths on the sameenv -Sresolver, then add engine tests for static Git/curl payloads plus dynamic trailing argv, dynamic executable/source fields, and local controls whose dynamic arguments are not command-defining. -
[P1] Fail closed for dynamic executable positions inside PowerShell command source
internal/sandbox/analyzer.go:216
The new outer-source check only rejects a nonliteral-Commandargument. A literal PowerShell program such aspowershell -Command '& $env:APP https://evil.test'executescurlwhenAPP=curl, buttextualPayloadUsesNetworkparses the PowerShell text as POSIX shell. That parser treats$env:APPas a dynamic command word andeffectiveProgramsilently drops it, leaving neitherNetworknorTooComplexset; the outer command is therefore allowed without the network prompt. Assigning$APP='curl'inside the PowerShell source and invoking& $APP …has the same problem.The root cause is treating a successfully parsed-but-not-semantically-understood PowerShell payload as evidence that it is local. Model PowerShell command source separately from POSIX shell source: either parse enough PowerShell invocation forms to identify literal command positions, or conservatively mark an unresolved invocation/operator position as network-sensitive. Do not use the POSIX parser's clean result to clear a PowerShell payload it cannot interpret. Add
Engine.Evaluatecoverage for& $env:APP, a variable assigned in the source, valid literal network commands, and literal local commands so the fail-closed rule remains narrowly scoped.
f6435a6 to
8a7a7dd
Compare
jatmn
left a comment
There was a problem hiding this comment.
I found issues that need to be addressed before this is ready.
This PR has been through many review rounds because each pass fixes a specific spelling (git -C repo push, git.exe, caret escapes, abbreviated --split) while the underlying classifier architecture still treats parseable and unparseable input as two separate worlds, and treats interpreter source (CMD /c, eval, PowerShell -Command) as argv tokens. The sections below name the three merge blockers, then explain why the drip pattern keeps happening and what to change so the next round is not another spelling hunt.
Why this keeps producing follow-up findings
1. Two classification paths that do not share outcomes
Production classification flows through Classify → AnalyzeCommand (AST/analyzeInto) plus, only when TooComplex, matchesUnparseableNetwork (risk.go:1226–1227). The PR invested heavily in fallbackBodyUsesNetwork, CMD resolvers, and git token parsers on the fallback side, but Classify never consults fallback when the POSIX parser succeeds. That means any launcher the AST path does not recurse into (eval, quoted CMD payloads) stays clean even when matchesUnparseableNetwork would return true. Fallback-only tests (&& "unterminated", & rem ') pass; parseable bypasses remain.
Root-cause fix: Pick one authoritative classifier for network (and destructive) on shell commands. Practical options:
- Fold fallback into
Classify: e.g.network := analysis.Network || matchesUnparseableNetwork(command)with fallback tuned to avoid the old whole-string false positives, or - Make the AST path complete: every launcher that runs command text (
eval,cmd/call/start, PowerShell-Command,env -S, shell-c/--command) must recurse into payload as bounded command text, not pass payload throughfallbackBodyUsesNetworkasbody[0].
Do not add another parallel skip list without updating both paths and Classify.
2. Interpreter source vs executable argv
CMD /c "git push origin main" is not “run the program named git push origin main”. It is “run the command line git push origin main”. The new cmdLauncherUsesNetwork (analyzer.go:673–695) extracts the /c operand and passes it to fallbackBodyUsesNetwork, which calls executableTokenBase(body[0]) (risk.go:155) — basename only, no whitespace split, no re-tokenize. The same mistake class hits quoted call "…" payloads.
Root-cause fix: Introduce a small shared operation, e.g. classifyCommandText(payload string, depth int) bool, used by:
- AST launcher branches after extracting
/c,/k,CALL,eval, shell-c, PowerShell-Command - Fallback CMD/PowerShell payload extraction (
fallbackCommandInterpreterPayload, etc.)
That function should tokenize payload as command text (with CMD/POSIX rules as appropriate), recurse with depth limits, and fail closed when text cannot be read. Do not route interpreter payloads through executableTokenBase on the first token.
3. “Literal path succeeded” mistaken for “invocation is safe”
envSplitSourceDynamic (analyzer.go:574–577) returns false after a literal -S operand (“the ordinary literal path already reads it”), without scanning remaining argv. GNU env still appends trailing arguments (env -S 'printf ok' $PAYLOAD). The same pattern appears elsewhere: proving the split-string operand is literal is not proof the whole env invocation is statically known.
Root-cause fix: After recognizing -S/--split-string, scan the rest of the env argv. Any nonliteral token after the split → fail closed (Network=true). Apply the same rule on the fallback env split path. Negative tests: trailing literals only stay non-network.
4. Duplicated git grammar
parseGitInvocation, matchesUnparseableGitNetwork, gitTargetsRemoteArchive, and command_prefix.gitSubcommand each carry slices of git’s option grammar. This PR unified GitGlobalOptionConsumesValue but not terminal globals (--help, bare --exec-path, etc.), so sandbox and prefix parsers still disagree. Each round fixes one option (--attr-source, --remote after --) without a single git module.
Root-cause fix: One shared git scanner (value-taking globals, terminal globals, subcommand position, archive operand layout) feeding both gitUsesNetwork and matchesUnparseableGitNetwork, and export terminal-global behavior for command_prefix.go if prefix safety must stay aligned.
5. Test strategy that hides parseable gaps
Many new tests only exercise the unparseable suffix (& rem ', && "unterminated") or AnalyzeCommand helpers, not Engine.Evaluate with NetworkDeny + PermissionGranted: true. The integration test uses BackendUnavailable and never asserts NetworkAllow on a backend stub. So each round proves fallback spellings while parseable Windows forms (cmd /c "git push …") and grant application stay unverified.
Root-cause fix: For every launcher you add or touch, require a parseable Engine.Evaluate case: NetworkDeny, shell already granted, expect ActionPrompt + ReasonNetworkBlocked. For grant claims, assert plan policy NetworkAllow like TestGrantRequestPermissionsNetworkOverlaysPolicyForTurn. Use a parity table: old regex / known bypass → must be covered on parseable path, not only behind rem '.
6. Stale branch noise
Fork point dc15e822 vs main 0eab63c makes GitHub’s two-dot diff show ~50 files of apparent reverts. That wastes review cycles on credstore/dictation/daemon paths that never land on merge. Rebase first; review the three-dot diff only.
Findings (must fix before merge)
-
[P1] Rebase this branch onto current
mainbefore merge
Where: branch topology (dc15e822merge base vs0eab63cdeclared base)
What happens: GitHub’s two-dot diff (main..head) spans 50 files and shows apparent reversions of path-jail (#891), credential-store locking (#891), daemon cleanup (#774), dictation fixes (#852), worktree git hardening (#891), and diff-viewer work (#902). The three-dot merge surface is ten sandbox/agent files. Those “reverts” are diff artifacts — e.g.worktrees.godropshardenWorktreeGitin the two-dot view but not in a merge onto0eab63c. CI is green only on stale head8a7a7ddf; there is no check run on a rebased artifact.
Author guidance: Rebase onto0eab63c, resolve conflicts preservingmainbehavior, push, and request review of the three-dot diff. Do not interpret two-dot “reverts” as work this PR needs to undo. After rebase, re-run the validation block from the PR description on the new head. -
[P1] Classify quoted CMD and
CALLpayloads as command source (PR regression)
Where:internal/sandbox/analyzer.go:673(cmdLauncherUsesNetwork);internal/sandbox/risk.go:155(executableTokenBaseinfallbackBodyUsesNetwork)
Repro (verified on head):cmd /c "git push origin main",cmd /c "curl https://evil.test",call "git push origin main"→AnalyzeCommand:Network=false,TooComplex=false.Engine.EvaluatewithNetworkDeny+PermissionGranted: true→ActionAllow, reasontool requires approval before execution, risk categories["shell"]only — no network prompt, no turn overlay. Unquotedcmd /c git push origin maincorrectly getsnetwork.
Why this PR:cmdLauncherUsesNetworkdoes not exist onmain. It was added to cover CMD on the AST path but passes quoted/coperands as a single token tofallbackBodyUsesNetwork, which treatsgit push origin mainas the program name.
Failure impact: On Windows, Zero forwards bash-tool text tocmd.exe /d /c. Quoted one-liners are common. After one bash approval, push/curl egress without network prompt — the approval model Windows unelevated relies on when WFP is unavailable.
Root-cause fix: After/c,/k, orCALLselects a payload, call sharedclassifyCommandText(payload)(re-tokenize as command source; fail closed if unreadable). Do not pass quoted payload throughexecutableTokenBase(body[0]). Reuse the same helper in fallback CMD extraction so paths cannot diverge.
Tests:Engine.Evaluate— quotedcmd /c "git push …"(network prompt), quotedcall "curl …"(network prompt), quotedcmd /c "git status"(no network). -
[P1] Keep
env -Sclassified when trailing argv is dynamic (PR regression)
Where:internal/sandbox/analyzer.go:168–175(analyzeIntoenv split branch);envSplitSourceDynamic(analyzer.go:544–577)
Repro (verified on head):env -S 'git push origin main' "$EXTRA",env -S 'printf ok' $PAYLOAD,env -S 'printf ok' "${PAYLOAD}" https://evil.test→Network=false, nonetworkcategory. Contrast:env -S 'printf ok' curl "$URL"flags network (literalcurlin split string). Dynamic inside split string (env -S "$PAYLOAD") is covered; trailing argv after a literal split is not.
Why this PR:envSplitSourceDynamicis new. At lines 574–577, after a literal-Soperand it returnsfalseimmediately without scanning further tokens.literalCallFieldsfails on trailing$PAYLOAD, so the fast path at 168–175 never runs; analysis falls through toeffectiveProgram("env")with no network flag.
Failure impact: Attacker or agent can hide network binary in trailing expansion after a benign-looking split string once shell is approved.
Root-cause fix: InenvSplitSourceDynamicand the literalenvSplitCommandFieldssuccess path: after consuming-Soperand, continue scanning remaining argv; any!isLiteralWord→ treat as network-dependent / fail closed. Do not use “literal split operand” as early exit for the whole invocation. Mirror on fallbackenvsplit handling.
Tests:Engine.Evaluatefor trailing"$EXTRA",$PAYLOAD, and negative control with trailing literals only.
Follow-up (optional for #703; pre-existing or adjacent)
I would not block merge on these alone after the three items above, but they explain remaining review noise if left unaddressed.
-
[P2] Parseable
evalbypass (analyzeInto+classifyWithScope:1226–1227)
Repro:eval git push origin main→matchesUnparseableNetwork=truebutClassifyhas nonetwork(TooComplex gate exists onmain). PR addedevalinfallbackBodyUsesNetworkbut not ASTanalyzeInto.
Guidance: Addevallauncher branch callingclassifyCommandText(strings.Join(args," ")), or fold fallback intoClassifyas described above. -
[P2] Double-quoted nested
sh -c(analyzeInto:194–203)
Depth 3+ withfmt.Sprintf("sh -c %q", …)loses network without fail-closed; single-quote nesting is covered. Fail closed when launcher present but payload not resolved. -
[P2] Parseable
@curl(commandWordsUseNetworkAt+trimCMDEchoPrefixonly on fallback)
@curl https://evil.testclean on AST; unparseable@curl … & rem 'flagged. Apply echo-prefix normalization before program match on AST path. -
[P2]
gitSubcommandvsgitTerminalGlobalOptions(command_prefix.go:395vsanalyzer.go:449)
Sandbox stops at terminal globals; prefix parser does not. Share terminal-global set or export from shared git module. -
[P3]
git send-packnot in whitelist (gitUsesNetwork:363) — pre-existing onmain. -
[P3] Integration test (
loop_test.go:2444) usesBackendUnavailable, does not assertNetworkAllowon backend stub — tighten test or PR description.
Suggested author checklist before next review request
- Rebase onto
0eab63c; confirm three-dot diff is sandbox/agent only. - Implement shared command-text classification for interpreter payloads (CMD/call/eval/shell/PowerShell); stop routing payloads through
executableTokenBase. - Fix env trailing argv scan; add trailing-dynamic
Engine.Evaluatetests. - Decide Classify vs AST completeness for fallback parity (
eval,@curl); do not add fallback-only fixes without parseable coverage. - Run parseable
Engine.Evaluatematrix for every launcher touched; not only& rem 'cases. - (Optional) Single git grammar module + prefix alignment; optional follow-up PR if scope is tight.
This should collapse the next review to “did you rebase and fix the two PR regressions plus the architectural split?” rather than another round of individual spellings.
Classify `git push` and remote `git archive` as network-sensitive through one shared, positional Git option parser, and fail closed on the launcher forms whose command source cannot be resolved statically: shell functions and subshells, CMD (CALL/IF/FOR /F/START/caret and nested launchers), PowerShell and pwsh, GNU `env -S`, shell `-c` clusters, BusyBox, and strace. Source that exists but cannot be read is not evidence that a command is local: an undecodable `-EncodedCommand` payload, a PowerShell Command operand built from an expansion, and an `env -S` split string the shell expands all now classify as network rather than parsing cleanly to nothing. Bounded, valid encoded payloads are decoded and read instead of guessed at. START keeps taking switches after its optional window title, and git's bare `--exec-path` is terminal (`--exec-path[=<path>]`) so it no longer consumes the following token and mislabels a local informational command as network. An integration regression proves an approved `git push` receives the existing turn-scoped network overlay, including the `gitlawb://` transport. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Addresses the three merge blockers plus the architectural split behind them: the classifier treated interpreter SOURCE as argv tokens, and the parseable and unparseable paths reached different conclusions about the same launcher. One shared operation for command text classifyCommandText is now the single entry point for a string another program will run as a command line. It re-tokenizes the payload instead of taking its basename, runs both the AST scan and the unparseable matcher (a CMD one-liner is legitimately not POSIX, so the parser rejecting it is expected rather than proof of safety), and fails closed when the text cannot be read. CMD /c and /k, CALL, start, eval, shell -c, and PowerShell -Command all route through it on both paths. Quoted CMD and CALL payloads (P1) cmdBodyUsesNetwork reads a single quoted token BOTH ways, because the ambiguity is real: `cmd /c "git push origin main"` is a command line and `cmd /c "C:\Program Files\curl\curl.exe"` is one program path. CMD resolves it by trying both, so this does too and fails closed if either reading reaches the network. Shared with the fallback so the paths cannot diverge. env -S trailing argv (P1) A literal -S operand proves something about the split string, not about the invocation: GNU env appends the remaining argv to the argv the split string produced, so `env -S 'sh -c' "$PAYLOAD"` runs unreadable text. The scan now continues past the operand and fails closed on any dynamic token after it. Trailing literal argv stays local. Nested shell payloads wordText returned raw source for double-quoted parts, because the parser leaves escape removal to expansion time. For an argv token that is harmless; for a -c operand the text IS the next command, so one level of `sh -c "sh -c \"curl …\""` handed the recursion the fragment `\"sh` and lost everything after it. unescapeDoubleQuoted applies POSIX escape removal for the five characters where a backslash is special, leaving quoted Windows paths intact. Parseable-path parity eval and CMD's echo-suppression prefix were handled in the fallback but not in the AST, so identical text was network only when something else defeated the parser. Both now classify on the parseable path. git send-pack joins the network subcommand list, which both paths read. GitTerminalGlobalOption is exported so command_prefix stops where the sandbox stops — `git --help status` no longer resolves to the auto-approved prefix `git status`. Tests TestEvaluatePromptsForParseableNetworkLaunchers is the parity matrix the unparseable table lacked: every launcher, parseable (asserted), with the shell permission already granted, expecting ActionPrompt/ReasonNetworkBlocked — plus a negative table so failing closed does not mean flagging everything. The turn-grant integration test now asserts the plan's policy is NetworkAllow while the approved call runs and is not after the turn, rather than inferring the grant from the command having executed. Refs Gitlawb#703. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
8a7a7dd to
f0bca25
Compare
|
Rebased onto The split, closedThe root cause you named — interpreter source treated as executable argv, and two classification paths that don't share outcomes — is addressed with one shared operation.
Running both scans is deliberate: a CMD one-liner is legitimately not POSIX, so the parser rejecting it is expected, not evidence of safety. That is the "fold fallback in" option applied where it actually matters (payloads) without the whole-string false positives the old regex had. Findings[P1] Rebase — done, onto [P1] Quoted CMD and [P1] Also fixed (your P2/P3 list)Parseable Nested double-quoted Parseable
Integration test — tightened rather than re-scoped. It now asserts Test strategy
Validation
Two I could not run locally and am not claiming: |
jatmn
left a comment
There was a problem hiding this comment.
I found issues that need to be addressed before this is ready.
Findings
-
[P2] Do not turn an over-depth shell launcher into a clean command
internal/sandbox/safe_command.go:154
detectInteractiveCommandAtnow returns an emptyInteractiveCommandResultoncedepth > maxAnalyzerDepth. Its callers interpret that result as a confirmed non-interactive payload, so five nestedsh -c/bash -claunchers aroundvim filebypass the no-TTY preflight and run until the agent times out. The new test deliberately pins that false negative, but the detector's contract is to prevent terminal-bound commands from hanging the agent; an inspection budget must not turn “could not inspect” into “safe.” Preserve the recursion bound, but give the depth-exhausted case a conservative result (for example, a dedicated unsupported-nested-launcher reason marked interactive) and add an execution-path regression proving an over-depth wrapper chain is blocked rather than launched. -
[P2] Remove unrelated test seams from the issue-scoped PR
internal/providers/providerio/export_test.go:1
This PR adds four unreferenced test-only exports acrossproviderio,agenteval,tools, andtui(ScanSSEData,TraceEventKeys,ValidateToolCapabilities, and the TUI PR helpers). None is used by the sandbox/network implementation or its regression tests, and none belongs to approved issue #703. This is scope drift rather than support for the Git network grant: it adds test-visible API surface in unrelated packages, makes the security fix harder to audit, and leaves future maintainers to preserve helpers with no stated owner or use case. Remove these seams from this PR; if an independent test suite genuinely needs one, submit it separately with the consuming test and the relevant approved issue.
Guidance for the next revision
The recurring review churn is coming from a structural problem, not from any one command spelling. This PR now contains several partially overlapping readers for command execution semantics: the AST walker, the unparseable-command fallback, command-text recursion for launchers, env -S rewriting, CMD/PowerShell helpers, and separate interactive-command inspection. A narrow fix in one reader can leave another reader with a different answer, especially when a launcher changes whether an argument is argv or executable source. The depth-limit regression is an example of the same pattern: bounding a recursive reader was treated as an implementation detail, but its caller interprets the resulting empty value as a positive safety conclusion.
Before asking for another review, please make the safety contract explicit and drive every reader from it:
- Define three distinct outcomes for command inspection: known local, known network/interactive, and unresolved. Do not represent unresolved input as the same zero value as known-local input. At a recursion, parsing, decoding, or token-budget limit, the caller must choose a conservative prompt/block path rather than silently continuing.
- Keep one authoritative command-text classifier for launchers. Shell
-c,eval, CMD/c/CALL, PowerShell command operands, and any wrapper that executes source should pass their payload through that classifier with a shared depth budget. Keep argv parsing separate from source parsing; a quoted command line is not a single executable filename. - For wrappers that rewrite argv, such as
env -S, model the complete resulting argv—including appended trailing arguments and option terminators—before deciding whether the executable or a command-source operand is statically known. Use the same resolved representation for both the AST and fallback paths where possible. - Make the focused tests prove the enforcement boundary, not just helper return values. For every launcher family touched, include parseable
Engine.Evaluatecoverage underNetworkDenythat asserts a network command prompts and a local command does not. Include the over-depth case and verify that it is blocked/prompted rather than merely observing the detector helper. - Keep the final diff limited to #703. Move unrelated test seams and any follow-on parser generalization that cannot be covered by the Git network-grant behavior into focused, independently approved work.
That approach should make the next review about whether the single enforcement contract is satisfied, rather than about another parser-specific edge case.
Amp-Thread-ID: https://ampcode.com/threads/T-01a01695-4d5a-75a9-b8b5-7ce3cbb57943 Co-authored-by: Pierre Bruno <pierrebruno@hotmail.ch>
|
PierrunoYT updated the branch through
|
Summary
git pushand remotegit archiveinvocations as network-sensitive with shared, positional Git option parsingenv -S, shell-launcher, BusyBox, and strace forms while avoiding false prompts for non-executing text and local operandsgit pushreceives the temporary network overlay, including thegitlawb://transportFixes #703.
Validation
make fmt-checkgo vet ./...go test ./...go test -race ./internal/sandbox ./internal/agentgo test ./internal/agent -run '^TestRunApprovedGitPushPromptAppliesTurnNetworkGrant$' -count=1go run ./cmd/zero-release buildgo run ./cmd/zero-release smokemake lint-static(0 issues)make vulncheck(No vulnerabilities found.)git diff HEAD --check