Skip to content

feat(agent_tool): revalidate file identity before remove deletes - #486

Merged
bobzhang merged 1 commit into
mainfrom
file-state-identity
Jul 13, 2026
Merged

feat(agent_tool): revalidate file identity before remove deletes#486
bobzhang merged 1 commit into
mainfrom
file-state-identity

Conversation

@bobzhang

@bobzhang bobzhang commented Jul 12, 2026

Copy link
Copy Markdown
Collaborator

PR 1 of 2 of the unified FileState work (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 in remove.

Problem

remove's gate was Created provenance 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.mbt restoring a tracked file the agent recreated, or a mv. remove would then delete the wrong file.

Fix — content revalidation

remove gates on created_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. Only remove reads the file, at delete time. This is simpler than mtime here, not heavier.

  • FileStateMap reshape (public enum kept, richer record private): FileState { Created | Modified } stays as the projection returned by get; a private Entry stores 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 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 (the serious hole the first review found).
  • Tool wiring: write records the content it wrote; edit records (seen=old, result=new); multi_edit records each file's provenance + final on-disk content after the auto-revert guard, keyed for continuity on the pre-batch content; remove reads + hashes at delete, distinguishing "never created" from "content changed since".

Verification

moon check --deny-warn --target native clean; moon test 199/199 in the touched set (new FileStateMap continuity unit tests, an edit rebound-downgrade test, a remove rebind-refusal test, README doc-tests); moon info mbti additive; moon fmt clean.

Next: PR 2 — read tracking + the unread-modify policy (block unread write / flag unread edit), superseding #241.

🤖 Generated with Claude Code

@bobzhang
bobzhang force-pushed the file-state-identity branch from 3ab77e3 to 1022a1b Compare July 12, 2026 08:03
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
bobzhang force-pushed the file-state-identity branch from 1022a1b to 819feab Compare July 12, 2026 08:19
@bobzhang
bobzhang merged commit 49d6c1c into main Jul 13, 2026
2 of 8 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