Skip to content

D: gitlink mounts, pinned gitlink parents and a pre-launch tree check (#81, #99) - #100

Merged
mchwang merged 5 commits into
mainfrom
feat/d-gitlink-mounts-81
Oct 3, 2026
Merged

mchwang merged 5 commits into
mainfrom
feat/d-gitlink-mounts-81

Conversation

@mchwang

@mchwang mchwang commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

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)

  1. 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.
  2. Mount type: a read-only tmpfs. No host bind.
  3. Execute and fix only. Read-only phases already mount /work read-only.
  4. 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.
  • The nested-gitlink gap: plan-format.md amended, follow-up D: stop renaming a parent of a nested gitlink during the run #99 filed.

Declined:

  • 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.

🤖 Generated with Claude Code

mchwang and others added 2 commits October 1, 2026 17:47
#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>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Safety-critical exit handling and the nested-gitlink runtime behavior lack complete regression coverage.

Review effort: Balanced
Findings: 2 Medium severity

Open (2)
What changed in this PR

Adds pre-launch tree validation and read-only gitlink mounts for writable agent runs.

Changes:

  • Validates task trees and declared operations before launch.
  • Mounts and verifies empty, read-only tmpfs filesystems over gitlinks.
  • Adds lifecycle documentation and Docker integration tests.
File Description
agents/​adapters/​types.ts Carries tree-check capabilities through adapters.
agents/​container/​changes.ts Implements tree checks and one-shot capabilities.
agents/​container/​profile.ts Adds gitlink tmpfs mounts.
agents/​container/​run.ts Validates resolved gitlink mounts.
agents/​container/​tree-script.ts Adds pre-launch inspection mode.
agents/​policy.ts Adds the gitlink isolation probe.
runner/​execution.ts Runs and forwards pre-launch checks.
runner/​workspace.ts Connects workspaces to tree inspection.
test/​agent-container.test.ts Tests Docker isolation and refusals.
test/​runner-execution.test.ts Tests execution lifecycle integration.
docs/​plan-format.md Documents gitlink behavior and limitation.
docs/​implementation/​agent-isolation.md Documents isolation enforcement.
docs/​implementation/​runner-lifecycle.md Documents launch-check sequencing.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread test/agent-container.test.ts Outdated
Comment thread test/agent-container.test.ts
…es (review round 2)

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 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
@mchwang
mchwang requested a balanced review from Copilot October 2, 2026 23:54

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

The plan-format requirement contradicts the implementation’s execute/fix-only scope.

Review effort: Balanced
Findings: None

Resolved since last review (2)
Previously missed (1)

In code that hasn't changed since last review

Medium severity 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>
@mchwang

mchwang commented Oct 3, 2026

Copy link
Copy Markdown
Contributor Author

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.

@mchwang
mchwang merged commit f61b8ed into main Oct 3, 2026
2 checks passed
mchwang added a commit that referenced this pull request Oct 3, 2026
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>
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.

D: stop renaming a parent of a nested gitlink during the run D: empty read-only mounts at gitlink paths and a pre-launch tree check

2 participants