feat(agent_tool): revalidate file identity before remove deletes - #486
Merged
Conversation
bobzhang
force-pushed
the
file-state-identity
branch
from
July 12, 2026 08:03
3ab77e3 to
1022a1b
Compare
Closes the "path rebound to a different file" residual in remove: Created
provenance alone proved only that the agent once created *a* file at a
path, not that the file there now is still that file (a git checkout
restoring a tracked file, a mv onto the path). remove now gates on
`created_and_unchanged`, which pairs the provenance with a content check.
Identity is a content digest, not mtime. The file-writing tools already
hold the content in memory, so the digest (hex SHA-256) is computed
synchronously — no stat, no cancellation window — and, being a content
version, cannot collide the way coarse-resolution or mv-preserved mtimes
can (both concerns the plan review raised for a destructive gate). Only
remove reads the file, at delete time.
FileStateMap reshape (public enum kept, richer record private):
- Public `FileState { Created | Modified }` stays as the provenance
projection returned by `get`; a private `Entry` also stores the digest.
- `record_created`/`record_modified` (wholesale write) take the new
content's digest. `record_edited` (targeted edit) also takes the digest
of the content the tool READ, and keeps `Created` only if that still
matches what the agent last wrote — so an edit of a rebound file is
downgraded and cannot launder a rebind into a deletable Created.
- New `created_and_unchanged(path, digest)` and a `content_digest(content)`
helper.
Tool wiring: write records the content it wrote; edit records
(seen=old content, result=new content); multi_edit records each file's
provenance + final on-disk content AFTER the auto-revert guard, keyed for
continuity on the pre-batch content — but only when that final content is
one the tool itself left (the applied new content or the reverted
original), so an external write during the guard's check window is not
blessed as the agent's. remove checks the cheap Created provenance FIRST
and only then reads + hashes, distinguishing "never created" from
"content changed since".
Suited to the cooperative environment: the concern is accidental rebinds,
which change content. PR 1 of 2 (read-tracking + unread-modify policy
follows).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
bobzhang
force-pushed
the
file-state-identity
branch
from
July 12, 2026 08:19
1022a1b to
819feab
Compare
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
PR 1 of 2 of the unified
FileStatework (design note reviewed by the Codex CLI, then this implementation per-commit reviewed). Closes the "path rebound to a different file" residual that earlier reviews flagged inremove.Problem
remove's gate wasCreatedprovenance keyed by path — which proves only that the agent once created a file at a path, not that the file there now is still that file. A permitted operation can rebind the path:git checkout -- x.mbtrestoring a tracked file the agent recreated, or amv.removewould then delete the wrong file.Fix — content revalidation
removegates oncreated_and_unchanged, pairing the provenance with a content digest (hex SHA-256) of what the agent last wrote, and re-checks it by reading + hashing the file at delete time.Why a content digest, not mtime (the plan review + the first per-commit review pushed on this): the file-writing tools already hold the content in memory, so the digest is computed synchronously — no stat, no cancellation window — and, being a content version, cannot collide the way coarse-resolution or
mv-preserved mtimes can. A byte-identical different file passes, which is harmless. Onlyremovereads the file, at delete time. This is simpler than mtime here, not heavier.FileStateMapreshape (public enum kept, richer record private):FileState { Created | Modified }stays as the projection returned byget; a privateEntrystores the digest — no public break.record_created/record_modified(wholesale write) take the new content's digest;record_edited(targeted edit) also takes the digest of the content the tool read and keepsCreatedonly if that still matches what the agent last wrote — so an edit of a rebound file is downgraded and cannot launder a rebind into a deletableCreated(the serious hole the first review found).writerecords the content it wrote;editrecords(seen=old, result=new);multi_editrecords each file's provenance + final on-disk content after the auto-revert guard, keyed for continuity on the pre-batch content;removereads + hashes at delete, distinguishing "never created" from "content changed since".Verification
moon check --deny-warn --target nativeclean;moon test199/199 in the touched set (newFileStateMapcontinuity unit tests, aneditrebound-downgrade test, aremoverebind-refusal test, README doc-tests);moon infombti additive;moon fmtclean.Next: PR 2 — read tracking + the unread-modify policy (block unread
write/ flag unreadedit), superseding #241.🤖 Generated with Claude Code