Skip to content

D: pin every directory above a nested gitlink during execute and fix (#99) - #104

Merged
mchwang merged 1 commit into
feat/d-gitlink-mounts-81from
feat/d-gitlink-parents-99
Oct 2, 2026
Merged

mchwang merged 1 commit into
feat/d-gitlink-mounts-81from
feat/d-gitlink-parents-99

Conversation

@mchwang

@mchwang mchwang commented Oct 2, 2026

Copy link
Copy Markdown
Contributor

Closes #99. Stacked on #100; base is feat/d-gitlink-mounts-81. Retarget to main after #100 merges.

What this does

#100 puts an empty read-only tmpfs at every gitlink for execute and fix. An agent could rename a parent of a nested gitlink: the mount moved with it, and the agent could then refill the old path during the run. Only the post-run inspection caught that.

Now every directory above a nested gitlink is pinned. The profile mounts the task's work volume again at that path:

--mount type=volume,source=<work volume>,target=/work/<dir>,volume-subpath=<dir>

A mountpoint cannot be renamed (EBUSY), so the gitlink's mount stays where it is. Inside a pinned directory, everything stays writable, nosuid and nodev, on the same volume and limits.

  • checkTaskTree refuses more than 256 pinned directories, or more than 128 KiB of paths in the gitlink mounts. The second limit keeps the docker create arguments bounded.
  • validateContainer derives the pins from the gitlinks. It allows exactly those mounts: the task's own work volume, writable, at that subpath, with no volume-nocopy. It also refuses a subpath on /work itself.
  • The gitlink-write probe checks each pin from inside a real execute container: the pin is a mountpoint, it is rw,nosuid,nodev, renaming it fails, the gitlink is still there, and a file can be written inside the pin.
  • Docs: plan-format.md and agent-isolation.md no longer list the exception. The side effects are documented:
    • rename(2) and hard links across a pin fail with EXDEV (mv copies instead);
    • Docker 26 or later is needed for volume-subpath;
    • nothing may write the work volume between the check and the start.

The spike results are in the #99 comment.

Review

Independent reviewer (fresh context). It found no way to move a gitlink mount or refill its path. It tested in real Docker with the production flags: renames of every parent level, renameat2(RENAME_EXCHANGE), umount, user-namespace tricks (blocked by seccomp=builtin), and subpath escapes (refused by the daemon). It confirmed that Docker mounts parents before children whatever the argument order.

Fixed:

  • Two probe checks could not fail: touch && rm under set -e, and rmdir, which a parent always refuses. The probe now writes, checks and removes as separate commands. The "removing fails" claim is gone from the docs. The probe also checks nosuid and nodev on each pin.
  • Validator conditions had no test. Added cases for a pin from another volume (metadata), a volume-nocopy pin, and a tmpfs in place of a pin, in addition to the missing, read-only and wrong-subpath cases.
  • Added the argument-size bound, with a test.
  • The validator now derives the pins instead of trusting profile.gitlinkParents.
  • Documented the EXDEV effect on rename(2), and the assumption that nothing writes between the check and the start.
  • Fixed a JSDoc slip.

Declined:

  • DriverConfig and Labels on pins are not checked: the volume's own options are already checked through docker volume inspect.
  • There is no runtime Docker version check: an older daemon would not apply the subpath, and the validator would refuse it.
  • The probe does not write at the old path after a refused rename: the rename failing is the protection.

Lesson audit. These are covered by existing AGENTS.md rules: "Treat every behavioural claim … give it a test that fails if the claim is false" (probe and validator gaps), and "Pass text whose size follows user or agent input … the OS limits" (argument bound). No new rule is needed.

Evidence

  • Mutation: with the pin mounts and their validator check removed, the gitlink test fails (the probe exits 1).
  • Local real Docker: the gitlink block passes 12/12. One earlier run failed when the shared daemon dropped its connection mid-run; a rerun was clean.
  • npm run typecheck is clean.

🤖 Generated with Claude Code

…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
mchwang merged commit 3d51d18 into feat/d-gitlink-mounts-81 Oct 2, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant