Skip to content

D: make the runner's commit from audited task changes (#66, part 2) - #90

Merged
mchwang merged 2 commits into
mainfrom
feat/d-task-commit
Oct 1, 2026
Merged

mchwang merged 2 commits into
mainfrom
feat/d-task-commit

Conversation

@mchwang

@mchwang mchwang commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Part 2 of #66. It adds commitTaskChanges, the runner's commit of what the audit approved. Part 1 (#85) added the inspection it builds on.

Decision change

Decision 1 on #66 planned a separate exportTaskCommit. With the user's approval (2026-10-01), one operation now does both:

  • It runs one container, with both task volumes read-only.
  • It builds the commit there and returns it as a bounded git bundle.
  • The metadata volume is never writable after seeding, so the metadata baseline never moves.

Why: the manifest digest is computed in TypeScript (linkTargetChanges comes from the snapshot). So a container could not check the digest before writing to the metadata volume. Here, nothing is kept until Node has checked it.

What it does

commitTaskChanges(storage, { base, linkSnapshot, digest, message, trailers, author, committer, imageId, maxBundleBytes })

  1. Re-inspects in its own container, with the same Perl code as inspectTaskChanges. It prints the inspection's JSON before building anything, so the build cannot change what is checked.
  2. Builds the commit from the same reads.
    • The tree is base plus exactly the manifest's changes, each file stored as the inspection hashed it.
    • New content is hashed again, this time writing, into /tmp/objects, with the metadata's objects as alternates. Each blob must come out as the inspection's ID.
    • Hooks and signing are off, and the commit encoding is UTF-8.
    • Identities and dates come only from F.
  3. Returns a bundle. A scratch bare repo in /tmp holds one ref, refs/heads/codeboost (TASK_COMMIT_REF). Its bundle is streamed as base64 and capped at 64 MiB.
  4. Checks in Node. It keeps the bundle only if:
    • the manifest digest equals the approved digest;
    • nothing the runner never commits is present;
    • the commit ID equals the ID that tree, base, identities and message describe (recomputed in Node).

Refusals (TaskCommitRefused):

  • the work tree changed after the audit;
  • an agent commit;
  • a metadata change;
  • a declared link target change other than retargeted (the item's own edit of the link, which the audit judges);
  • gitlink content;
  • a directory or special file, a path under .git, or a symlink named .gitmodules.

An empty change set returns base with no commit. The same inputs always give the same head.

Also in this PR:

  • input (stdin) for runInProcessGroup and storage scripts (docker run -i). The message and identities go in that way, per AGENTS.md.
  • Recovery removes leftover codeboost-commit-* containers.
  • F's runner commit message neutralises issue references and @-mentions (runner/execution.ts).
  • Fake-Docker tests now install the SIGTERM handler before writing their "began" marker. Before, an abort under load could land between the two.
  • Docs: agent-isolation.md, runner-lifecycle.md, pull-request-opening.md.

Left for F (#87)

  • TaskWorkspace.commit needs the identities. F must fetch the bundle into its runner-owned repository and check TASK_COMMIT_REF == head, instead of calling exportTaskCommit.
  • core/run-audit.ts requires linkTargetTraversesLink === false for any symlink change. D never reports that field, so every symlink change is a violation today.
  • Both the audit and the commit refuse directory entries. So a file replaced by a package directory, and new ignored directories such as __pycache__, go to needs human.
  • Files that base's own ignore rules ignore, written into existing directories, are committed. This follows the part 1 contract; the audit marks them out of scope and pauses the task.

Validation (local; first pushed head c4ddc26, see round 6 for the current head)

  • npm run typecheck: passes.
  • Regular test job (CI's excludes): 1147 of 1147 pass.
  • Real-Docker runner commit (#66) tests: 7 of 7 pass. They cover:
    • every change kind, type changes, gitlinks, and names like -opt, :(glob)x and "quoted";
    • line endings and attributes, an 8 MiB incompressible file, and a SHA-256 repository;
    • an empty change set;
    • drift after the audit, the bundle limit, and each refusal.
  • Full real-Docker agent-container.test.ts (on a mid-review head): 127 pass, 1 skipped (Linux-only). One unrelated test, "releases a killed create once its settle window passes", failed under load and passed on rerun.
  • Fake-Docker storage, recovery and process-group suites pass.

Review rounds

Round Reviewer Fixed Declined
1 Independent fresh-context agent Base64 check regex overflowed the stack on bundles over about 4 MB. Noncharacters changed the commit ID. A -0000 offset was accepted. Pack threads could exceed --pids-limit. Memory: the inspection is now printed before the build. stdin destroyed on settle, with an abort-while-writing test. .gitmodules symlink. Test gaps. Doc wording. None. The two "stored differently" checks have no test: no way was found to make Git store a file differently on the second hash.
2 /code-review high retargeted was refused. Issue references in the commit message. The ref name was duplicated. An unbuilt commit gave a plain error. Ignored files committed: part 1 contract, listed above. A bundle is built on a refusal path only Node can detect: refusal path only.
3 /code-review high Two doc-wording fixes (both later reverted with the rule in round 6). None.
4 /code-review high Doc: a refused publish's commit is in F's repository, not discarded with the storage. None.
5 /code-review high Nothing new. None.
6 CI real-docker on c4ddc26 Reverted the refusal of a .gitattributes over 100 MB, added for round 1's order-dependence concern. That concern does not hold: Git 2.39.1+ ignores such a file read from the index too, and every .gitattributes leaves the scratch index. Part 1's test "applies no rules from a .gitattributes Git will not read" pins this, and it failed under the refusal. None.

Review lessons (merge gate)

  • Base64 regex overflow: one-off (a regex engine limit). The fix checks the base64 by re-encoding instead of with a regex.
  • Noncharacters and -0000: new rule in AGENTS.md, "Agent-controlled content": refuse inputs the tool would normalize when predicting its output.
  • Autovivification changed the reported manifest (found while writing the code): new rule in AGENTS.md: serialize a report before acting on the same data.
  • Pack threads over the pids limit: one-off (Git's default thread count).
  • stdin abort race: covered by "Required race regressions".
  • Issue references in the commit message: covered by "Neutralise issue references ... commit message".
  • Undocumented or untested refusal: covered by "Review readiness" (every behavioural claim needs a test).
  • A review-driven refusal that broke a pinned behaviour (round 6): covered by "Review readiness" (run final validation against the exact pushed head). The full real-Docker suite had last run before that fix.
  • retargeted refused, and stale lifecycle doc: one-off (contract alignment within this PR).
  • Fake-Docker SIGTERM race: one-off (a test fixture).

🤖 Generated with Claude Code

commitTaskChanges runs the change inspection again in one container with
both task volumes read-only, builds the commit from the same reads (base
plus exactly the manifest's changes, each file stored as hashed) into a
scratch object store in /tmp, and returns it as a bounded git bundle.
Node keeps the bundle only when the manifest digest matches the approved
one, refuses agent commits, metadata and link target changes, gitlink
content and changes a tree cannot hold, and checks the commit ID against
the one its inputs describe. Replaces the planned exportTaskCommit.

Also: stdin input for process groups and storage scripts, recovery of
leftover commit containers, neutralised issue references in F's runner
commit message, and a fix for a SIGTERM race in the fake-Docker tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…dex too

The refusal added in review broke part 1's pinned behaviour. Every
.gitattributes leaves the scratch index, and Git 2.39.1+ ignores one this
large read from the index as well, so hashing does not depend on order.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@mchwang
mchwang merged commit f4b8336 into main Oct 1, 2026
2 checks passed
@mchwang
mchwang deleted the feat/d-task-commit branch October 20, 2026 19:19
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