You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
D: gitlink mounts, pinned gitlink parents and a pre-launch tree check (#81, #99) - #100
Closes#81. Closes#99 (folded in from #104 to answer Copilot's round-2 comment; see below).
What this does
Before an execute or fix agent starts, D now checks the task tree and protects every gitlink (submodule) path.
checkTaskTree(storage, { base, operations, imageId }) (agents/container/changes.ts) runs D's no-follow inspection with no declared links, in a new prelaunch mode of the tree script. It refuses with TaskTreeRefused unless:
the work tree is exactly the tree of base: no untracked or ignored entry, no changed content, mode or type, nothing in a gitlink directory, HEAD at base, and metadata as seeded;
each declared operation fits that tree: an add or rename destination is free, a source is a file or symlink, and no declared path lies beneath a file, symlink or gitlink;
every gitlink can be mounted: at most 256, and no , or " in a name.
A tree the script cannot read whole (exits 6, 8 or 9) is refused too.
Gitlink mounts. The check result is a one-shot capability. An execute or fix profile requires a check made for its own storage and head; a read-only phase refuses one. The profile adds --mount type=tmpfs,target=/work/<path>,readonly,tmpfs-mode=0555,tmpfs-size=4096 at every gitlink, and validateContainer allows exactly those mounts, read-only, with that size and mode.
F2b.executionDeps.prepare runs the check last, after the link snapshot, and passes the result to the launcher. A TaskTreeRefused is a safety finding (the task goes to needs human). Any other error fails the attempt.
Decisions (made before coding)
The projected state is the tree of the recorded head plus the item's file operations. F2a's declared-only projection was not used, because plan-format.md says a continuation uses the audited checkpoint.
Mount type: a read-only tmpfs. No host bind.
Execute and fix only. Read-only phases already mount /work read-only.
After review: renaming a parent of a nested gitlink moves its mount, so an agent can fill the old path during the run. The post-run inspection catches this as nested gitlink content. plan-format.md now records this, and D: stop renaming a parent of a nested gitlink during the run #99 tracks enforcing it during the run.
Review rounds
Round 1: independent reviewer in a fresh context. It found no blocking defect.
Fixed:
Validator claims had no failing test. Added tests for a dropped mount, a changed size or mode, a check made at another head, a name with ", and more than 256 gitlinks.
One test only exercised the test helper. Removed it.
Refusals that are not tree differences were worded as "the tree disagrees". The message is now "The pre-launch tree check refused".
Script exits 6, 8 and 9 surfaced as retryable failures. They are now refusals.
Gitlink names from the script were checked only to be strings. They are now validated as paths.
The validator required TmpfsOptions.Options to be absent. It now also accepts null or empty, so a newer daemon does not fail closed.
Corrected comments and doc wording: index vs. HEAD, the tree-script header, and check attempts.
Declared paths go to the script as arguments rather than on stdin. This follows the existing snapshot call; they are bounded at 200 × 1 KiB by assertArguments.
Perl's older Unicode tables refuse some names Node accepts. This was already true for the snapshot.
The adapter forwards treeCheck through a spread. Losing it would fail closed.
A check container still runs after a store-level cancel during the snapshot. That costs one container run and is not a correctness issue.
Lesson audit. Every finding is covered by existing AGENTS.md rules: "Treat every behavioural claim … give it a test that fails if the claim is false" (validator and doc claims), and "A bounded safety scan must fail closed" (script exits). No new rule is needed.
Folded in: #99, pinning the directories above nested gitlinks
Copilot's comment on the parent-rename test was right: the gap needed a test inside a real execute profile, and the fix belonged here. Commit 3d51d18, reviewed on #104, is now part of this PR:
Every directory above a nested gitlink is mounted again from the work volume at its own path (volume-subpath). A mountpoint cannot be renamed, so the gitlink's read-only mount cannot be moved aside and its path refilled.
checkTaskTree refuses more than 256 pinned directories, or more than 128 KiB of mount paths.
The validator derives the pins from the gitlinks and allows exactly those mounts: the task's own work volume, writable, at that subpath.
The gitlink-write probe runs inside a real execute container. For every directory above every nested gitlink, it checks that the directory is a mountpoint and rw,nosuid,nodev, that renaming it fails, that the gitlink is still in place, and that a file can still be written inside. With the pins removed, the test fails.
plan-format.md no longer has the exception.
The independent review of this part is recorded in #104.
Review round 2 (Copilot)
Comment
Outcome
The parent-rename test runs without the mounts
Fixed by folding in #99: the in-profile probe now tries the rename at every directory level, and the rename fails.
Exits 6 and 9 are not covered
Fixed in 1dd1182: an unreadable-directory case and a more-than-10,000-entries case. Dropping either exit makes the test fail.
Lesson audit: both comments fall under the existing AGENTS.md rule "give it a test that fails if the claim is false". No new rule is needed.
Evidence
The gitlink test was checked by mutation. With the mounts removed it fails (the validator refuses). With both the mounts and the validator check removed it also fails (the probe exits 1).
Before the round-1 fixes, the 7 #81 real-Docker tests passed locally. After the fixes, the local Docker daemon stopped responding (other sessions' suites were running on it), so the updated real-Docker tests and the full suite run in this PR's real-docker job.
npm run typecheck: clean.
runner-execution: 104/104.
The non-Docker suite was green, except two unrelated timeouts under load; both pass when run alone.
#81)
checkTaskTree runs the no-follow inspection with no declared links just
before an execute or fix launch. It refuses (TaskTreeRefused) unless the
work tree is exactly the tree of the recorded head, and each declared
operation fits that tree: add and rename destinations free, sources a
file or symlink, no declared path beneath a file, symlink or gitlink.
It also refuses a gitlink it cannot mount (over 256, or a name with a
comma or quote), and a tree it cannot read whole.
The result is a one-shot capability: an execute or fix profile requires
a check made for its own storage and head, mounts an empty read-only
tmpfs at every gitlink, and the validator allows exactly those mounts.
F2b's prepare runs the check last; a refusal is a safety finding.
A parent of a nested gitlink can still be renamed during the run; the
post-run inspection catches it. plan-format.md records this, and #99
tracks enforcing it during the run.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
String(index + 1).repeat(40) made an 80-character ID from the tenth
gitlink on, which git update-index --cacheinfo refuses; only the
257-gitlink case reached it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…99)
The read-only tmpfs at a nested gitlink (#81) moved with its parent
when the agent renamed that parent, so the old path could be refilled
during the run. Each directory above a nested gitlink is now mounted
again from the task's work volume at its own path (volume-subpath). A
mountpoint cannot be renamed, so the gitlink's mount stays put; what is
inside stays writable, nosuid and nodev, on the same volume and limits.
checkTaskTree refuses more than 256 pinned directories or more than
128 KiB of mount paths. The validator derives the pins from the
gitlinks and allows exactly those: the task's own work volume,
writable, at that subpath. The probe checks each pin and that renaming
it fails. plan-format.md no longer lists the exception.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mchwang
changed the title
D: empty read-only mounts at gitlink paths and a pre-launch tree check (#81)
D: gitlink mounts, pinned gitlink parents and a pre-launch tree check (#81, #99)
Oct 2, 2026
Gitlink rejection and masking are missing from read-only phases
docs/plan-format.md:131
This still requires gitlink rejection and masking before each agent or check invocation, but the implementation and agent-isolation.md:390-393 explicitly apply checkTaskTree and the per-gitlink mounts only to execute/fix. Read-only phases prevent writes, but they neither reject a populated gitlink nor hide its contents, so these two normative documents describe different guarantees. Please either narrow this requirement to writable invocations or implement the stated behavior for planning/questions/review as well.
plan-format.md asked for it before every invocation; #81 decided on
execute and fix (and future cmd runs) only. Read-only invocations get
a fresh non-recursive clone mounted read-only, so a gitlink there stays
empty.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot round 3 ("Gitlink rejection and masking are missing from read-only phases"): narrowed plan-format.md in 86bccd0 to match the decision recorded for #81. The rejection and the empty read-only mounts apply before every invocation that can write the task filesystem (execute, fix, and a check that runs an item's cmd there). Planning, questions, review, and checks that run as review get a fresh non-recursive clone mounted read-only, so every gitlink stays empty and nothing can fill one. This now agrees with agent-isolation.md.
Resolves the conflicts with #100 (gitlink mounts and the pre-launch tree
check) and passes the tree check through claudeLauncher: D requires it
for an execute start, so without it every production launch would fail.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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
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 #81. Closes #99 (folded in from #104 to answer Copilot's round-2 comment; see below).
What this does
Before an execute or fix agent starts, D now checks the task tree and protects every gitlink (submodule) path.
checkTaskTree(storage, { base, operations, imageId })(agents/container/changes.ts) runs D's no-follow inspection with no declared links, in a newprelaunchmode of the tree script. It refuses withTaskTreeRefusedunless:base: no untracked or ignored entry, no changed content, mode or type, nothing in a gitlink directory,HEADatbase, and metadata as seeded;,or"in a name.A tree the script cannot read whole (exits 6, 8 or 9) is refused too.
Gitlink mounts. The check result is a one-shot capability. An execute or fix profile requires a check made for its own storage and head; a read-only phase refuses one. The profile adds
--mount type=tmpfs,target=/work/<path>,readonly,tmpfs-mode=0555,tmpfs-size=4096at every gitlink, andvalidateContainerallows exactly those mounts, read-only, with that size and mode.F2b.
executionDeps.prepareruns the check last, after the link snapshot, and passes the result to the launcher. ATaskTreeRefusedis a safety finding (the task goes to needs human). Any other error fails the attempt.Decisions (made before coding)
/workread-only.Review rounds
Round 1: independent reviewer in a fresh context. It found no blocking defect.
Fixed:
", and more than 256 gitlinks.TmpfsOptions.Optionsto be absent. It now also accepts null or empty, so a newer daemon does not fail closed.checkattempts.Declined:
snapshotcall; they are bounded at 200 × 1 KiB byassertArguments.treeCheckthrough a spread. Losing it would fail closed.Lesson audit. Every finding is covered by existing AGENTS.md rules: "Treat every behavioural claim … give it a test that fails if the claim is false" (validator and doc claims), and "A bounded safety scan must fail closed" (script exits). No new rule is needed.
Folded in: #99, pinning the directories above nested gitlinks
Copilot's comment on the parent-rename test was right: the gap needed a test inside a real execute profile, and the fix belonged here. Commit
3d51d18, reviewed on #104, is now part of this PR:volume-subpath). A mountpoint cannot be renamed, so the gitlink's read-only mount cannot be moved aside and its path refilled.checkTaskTreerefuses more than 256 pinned directories, or more than 128 KiB of mount paths.gitlink-writeprobe runs inside a real execute container. For every directory above every nested gitlink, it checks that the directory is a mountpoint andrw,nosuid,nodev, that renaming it fails, that the gitlink is still in place, and that a file can still be written inside. With the pins removed, the test fails.The independent review of this part is recorded in #104.
Review round 2 (Copilot)
1dd1182: an unreadable-directory case and a more-than-10,000-entries case. Dropping either exit makes the test fail.Lesson audit: both comments fall under the existing AGENTS.md rule "give it a test that fails if the claim is false". No new rule is needed.
Evidence
The gitlink test was checked by mutation. With the mounts removed it fails (the validator refuses). With both the mounts and the validator check removed it also fails (the probe exits 1).
Before the round-1 fixes, the 7 #81 real-Docker tests passed locally. After the fixes, the local Docker daemon stopped responding (other sessions' suites were running on it), so the updated real-Docker tests and the full suite run in this PR's
real-dockerjob.npm run typecheck: clean.runner-execution: 104/104.🤖 Generated with Claude Code