Repository navigation
feat(daemon): guard cross-worktree Git mutations #8687
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 6 commits
Commits
Show all changes
51 commits
Select commit
Hold shift + click to select a range
2480163
feat(daemon): guard cross-worktree Git mutations
wenshao 2b0e8cd
fix(daemon): keep Git guard off serve fast path
wenshao 107b5f6
Merge branch 'main' into feat/daemon-git-worktree-guard
wenshao b175cd7
fix(serve): close daemon Git guard parser bypasses
wenshao 2af7419
Merge branch 'main' into feat/daemon-git-worktree-guard
qwen-code-dev-bot b1b7606
fix(serve): keep daemon Git guard out of serve fast-path closure
wenshao 83428be
fix(serve): close re-reviewed daemon Git guard bypasses and subagent …
wenshao 18e997a
fix(serve): restore lazy daemon Git guard import
wenshao ab1e3e2
fix(serve): close daemon Git guard shell front-end bypasses
wenshao 45753e0
fix(serve): restore the shell-wrapper analysis return shape
wenshao c14ffc4
fix(serve): close the remaining daemon Git guard front-end gaps
wenshao 7f66be6
fix(serve): resolve the repository Git discovers, not just the directory
wenshao b11585b
fix(serve): match chdir, brace-expansion and here-string semantics
wenshao dfeb461
fix(serve): treat relocated git describe as a target-repo write
wenshao af36eca
docs(serve): state what git describe actually rewrites
wenshao 8112fae
test(serve): cover the provider-attached marker with a real handshake
wenshao 338e0c7
fix(serve): close the round-3 shell and repository-discovery gaps
wenshao d9a3911
fix(serve): scope the relink invalidation to path-resolving Git runs
wenshao 012d252
fix(serve): rebuild the relink defense and close the round-4 gaps
wenshao 2bf64cd
fix(serve): carry relink and shell state across nested scopes
wenshao 0a58fe7
fix(daemon): evaluate the guard against the directory the tool runs in
wenshao cbcde28
Merge branch 'main' into feat/daemon-git-worktree-guard
wenshao 3a247f7
fix(daemon): reach the real guard path and close the round-6 escapes
wenshao a1652cc
fix(daemon): contain a sub-agent to an in-project agent worktree
wenshao 581d2bb
test(acp): assert the session identity and cwd the guard now receives
wenshao 6b755d1
fix(daemon): record ordinary targets a dynamic relinker re-points
wenshao 8baf7a7
Merge branch 'main' into feat/daemon-git-worktree-guard
qwen-code-dev-bot 0f970d4
fix(daemon): close the round-7 escapes, verified with the reported pa…
wenshao 8e46dba
fix(daemon): repair the round-7 patch and bound what this guard promises
wenshao 089c3e3
fix(daemon): close the round-9 critical forms an agent may actually emit
wenshao a2736b8
Merge remote-tracking branch 'origin/main' into work8687
wenshao cf2ea23
fix(daemon): replay an alias with the args its invocation appends
wenshao 7f4746f
fix(daemon): carry a function/alias body's cwd and exports to the caller
wenshao 6273464
fix(daemon): inherit the caller's allexport into a same-shell body
wenshao 01587ab
fix(daemon): complete the same-shell state model for bodies and subst…
wenshao 2f0b99f
fix(daemon): resolve shadowing and exported functions; isolate pipe s…
wenshao a2ae8bb
fix(daemon): close the interlocking gaps in my function-model work
wenshao 6deb8bd
fix(daemon): deny relocations disguised by a redirection
wenshao a43faa8
fix(daemon): deny relocation hidden by a leading redirect or a backgr…
wenshao 1203072
Merge remote-tracking branch 'origin/main' into work8687
wenshao e595f7b
fix(daemon): don't let a harmless or removed shadow mask a relocation
wenshao 70f5162
fix(daemon): drop exported functions when env clears the child enviro…
wenshao 68eb534
test(daemon): pin the sh-wrapper fail-closed contract and document it
wenshao 3333331
fix(daemon): model shell-definition removal the way the real shell does
wenshao 636602c
fix(daemon): bare unset keeps the function and env -u strips exported…
wenshao 5fc219d
fix(daemon): fail closed when a removal builtin could retract a track…
wenshao 6128e69
fix(daemon): skip leading redirections before the removal-builtin pre…
wenshao d95d87c
fix(daemon): replay a shadowed removal builtin and drop unset variables
wenshao a5f8099
fix(daemon): honor shadowed command/builtin prefixes and PATH-based r…
wenshao c7b6aa4
fix(daemon): catch delimiter-glued relocations and redirect-decoy pre…
wenshao a0427ae
fix(daemon): deny trailer/man/sendemail command-executing config keys
wenshao File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,122 @@ | ||
| # Daemon Git worktree guard | ||
|
|
||
| ## Context | ||
|
|
||
| A daemon ACP session is owned by one bound workspace. The model shell tool | ||
| already rejects an explicit `directory` outside its effective workspace, but a | ||
| Git command can relocate itself with `-C`, `--work-tree`, or `--git-dir` while | ||
| the shell process still starts inside the workspace. This can let a daemon | ||
| agent mutate another checkout or worktree after the direct directory form was | ||
| rejected. | ||
|
|
||
| ## Scope | ||
|
|
||
| The guard applies only to model tool execution through the managed daemon ACP | ||
| path. It does not change CLI or TUI shell validation, Git safety classification, | ||
| permission rules, confirmation behavior, or direct user shell execution. | ||
|
|
||
| The daemon enables its managed tool guard for every ACP child. The host owns the | ||
| bound workspace and adds it to the validated guard request before applying the | ||
| built-in policy. An optional external tool guard remains an additional policy | ||
| and receives the same request only after the built-in policy allows it. | ||
|
|
||
| ## Policy | ||
|
|
||
| The built-in guard inspects `run_shell_command` calls only. Command splitting | ||
| reuses core `splitCommands`; containment reuses core `realpathNearestExisting` | ||
| and `isWithinRoot`. It recognizes Git invocations whose repository location is | ||
| changed by literal forms of: | ||
|
|
||
| - `git -C <path>` and `git -C<path>` | ||
| - `git --work-tree <path>` and `git --work-tree=<path>` | ||
| - `git --git-dir <path>` and `git --git-dir=<path>` | ||
| - leading `GIT_DIR`, `GIT_WORK_TREE`, `GIT_COMMON_DIR`, or `GIT_INDEX_FILE` | ||
| assignments | ||
| - directory-shifting wrapper flags `env -C`/`--chdir` and `sudo -D`/`--chdir` | ||
| - `cd`, `pushd`, or `popd` builtins earlier in the same command chain, whose | ||
| targets become the containment basis for later Git invocations in that chain | ||
|
|
||
| Wrapper prefixes are unwrapped before Git detection: leading env assignments, | ||
| `command`, `env` (with its value-taking flags), `sudo` (with its value-taking | ||
| flags), `nohup`, `exec`, `timeout <duration>`, `sh|bash|dash|zsh|ksh -c` | ||
| payloads (analyzed recursively), `eval` payloads (analyzed recursively, with | ||
| cwd changes propagated because `eval` runs in the current shell), | ||
| path-qualified Git binaries by basename, and leading `{`/`!` shell syntax. | ||
| A segment whose program token cannot be classified (shell expansions) fails | ||
| closed when the segment also carries a Git relocation marker or a recorded | ||
| relocation. | ||
|
|
||
| Relative targets resolve from the command's effective starting directory: | ||
| `arguments.directory` when present, otherwise the session's current effective | ||
| working directory. The bridge supplies both that current directory and the | ||
| immutable bound workspace from trusted session state. The current effective | ||
| working directory is the allowed execution boundary so a session moved through | ||
| the controlled daemon `/cd` flow can operate in its selected worktree without | ||
| being mistaken for an escape from the original storage owner. Git applies `-C` | ||
| during option parsing and resolves relative `--git-dir`/`--work-tree` against | ||
| the post-`-C` cwd, so relative targets resolve against the final cwd of the | ||
| `-C` chain regardless of argv order. | ||
|
|
||
| A statically resolved Git relocation is denied when both of the following | ||
| hold: | ||
|
|
||
| 1. its target is outside the session's effective working directory after | ||
| canonical path resolution; | ||
| 2. its Git subcommand is mutating or cannot be classified as read-only. | ||
|
|
||
| Relocated commands whose subcommand is in a small verified read-only set | ||
| (`status`, `rev-parse`, `ls-files`, `grep`, `describe`, `cat-file`) remain | ||
| allowed. `diff`, `log`, `show`, and `blame` are excluded from that set: | ||
| `--output` writes files, and textconv-style drivers execute programs | ||
| configured by the target repository. Any `--output` flag demotes an | ||
| invocation. Commands with no recognized relocation retain existing behavior. | ||
| Dynamic relocation targets (`$` expansions, backticks, leading `~`, globs) | ||
| and command-executing `-c`/`--config-env` assignments (`alias.*`, | ||
| `core.editor`, `core.pager`, `credential.helper`, `filter.*`, `difftool.*`, | ||
| `mergetool.*`, `core.fsmonitor`, or values starting with `!`) are denied for | ||
| mutating or unknown subcommands because the daemon cannot prove that the | ||
| target remains inside the effective working directory. | ||
|
|
||
| `--git-dir` is evaluated by the repository git operates on, with | ||
| canonicalization before basename handling: a target whose canonical form ends | ||
| in `.git` uses its parent; a `.git` gitfile is followed through its `gitdir:` | ||
| redirect; a per-worktree administrative directory | ||
| (`<repo>/.git/worktrees/<name>`) is resolved through its `gitdir` file to the | ||
| linked worktree checkout. Unresolvable indirections fail closed. | ||
|
|
||
| ## Failure semantics | ||
|
|
||
| Malformed managed guard requests, stale session or prompt ownership, missing | ||
| trusted workspace context, policy exceptions, and malformed external-provider | ||
| responses fail closed before execution. Unparseable commands, dangling | ||
| relocation options, relocation targets that do not fully exist at decision | ||
| time (a missing target can still become an outward symlink before git runs), | ||
| and unreadable Git indirections are denied for mutating or unclassifiable | ||
| subcommands. A built-in denial is final and is not sent to the optional | ||
| provider. Denial reasons are length-clamped and control-character-stripped so | ||
| they always satisfy the guard result validation. | ||
|
|
||
| The managed guard plumbing is active for every daemon ACP child because the | ||
| built-in policy needs it. The child-side v1 restrictions (`/fork` and | ||
| agent-backed workspace memory remember/dream) key on the external provider | ||
| being attached, not on the plumbing's mere presence: under the built-in guard | ||
| alone, hidden-agent tool calls traverse the same managed guard and are | ||
| inspected by the same daemon-side policy. Without a provider the child also | ||
| resolves every non-shell tool call locally (the built-in policy allows them | ||
| structurally) instead of paying a child-daemon-child round trip per call; | ||
| `run_shell_command` always makes the round trip. With a provider attached | ||
| every call still makes it. | ||
|
|
||
| ## Non-goals | ||
|
|
||
| - No changes to core `ShellTool`, `ShellToolInvocation`, shell AST parsing, | ||
| `PermissionManager`, `evaluatePermissionFlow`, or `CoreToolScheduler`. | ||
|
wenshao marked this conversation as resolved.
Resolved
wenshao marked this conversation as resolved.
Resolved
wenshao marked this conversation as resolved.
Resolved
wenshao marked this conversation as resolved.
Resolved
wenshao marked this conversation as resolved.
Resolved
wenshao marked this conversation as resolved.
Resolved
wenshao marked this conversation as resolved.
Resolved
|
||
| - No new confirmation flow or linked-worktree exception. | ||
| - No restriction on direct user-entered daemon shell commands. | ||
| - No general shell interpreter or environment-variable analysis: script files | ||
| run by `bash script.sh` or `source` are not read, and variable values are | ||
| not tracked across commands. | ||
| - No heredoc body analysis: Git-shaped text inside a heredoc is scanned as | ||
| executable lines and can be denied even though the shell never executes it | ||
| (a fail-closed false positive, not a bypass). | ||
|
wenshao marked this conversation as resolved.
Resolved
|
||
| - No attempt to correlate a denial with a previous tool call. | ||
Oops, something went wrong.
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.