Repository navigation
feat(tools): route the remaining write tools through the guard (U7, #1375) #1918
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
easonLiangWorldedtech
wants to merge
119
commits into
Zoo-Code-Org:main
Choose a base branch
from
easonLiangWorldedtech:fws/u7-tool-wiring
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+16,663
−371
Open
Changes from all commits
Commits
Show all changes
119 commits
Select commit
Hold shift + click to select a range
426f809
split unit U1 of PR 1833 (issue 1375)
4528075
fix(file-safety): close the pre-merge findings on the publish primiti…
9207cee
fix(file-safety): propagate a non-ENOENT lstat failure in resolvePubl…
5d02948
fix(file-safety): keep the rollback pair typed and the mock stand-ins…
1e7c390
rebuild unit u2 on the fixed chain
86ae014
chore(lint): prune the safeWriteJson suppression this unit earns
cbe99cc
rebuild unit u3 on the fixed chain
7559543
fix(task): declare the observation registry on Task in this unit
0fcf305
rebuild unit u4 on the fixed chain
7e0533e
rebuild unit u5 on the fixed chain
2f42d5c
rebuild unit U8 on U5 (corrected merge order)
47dc492
rebuild unit u6 in the corrected order
b5a213a
fix(apply-patch): a full hunk read is a complete observation
cf751fb
rebuild unit u7 on the corrected u6
38d99ed
rebuild unit U7 on the corrected U6
6a46b6b
fix(task): keep main's abort re-check in ask
9adf66f
fix(file-safety): inherit unit 1 committed guard
8647b7d
fix(file-safety): keep the publish error message in RollbackFailureError
3ba741a
test(file-safety): assert the exact staging directory removed after a…
7ad01ae
test: re-run mocked e2e and the ubuntu lane - no source change since …
14ec1fb
fix(file-safety): give the Windows DACL dump a per-write name
b27f4c8
fix(tools): carry U1's hunk-read completeness rule into this unit
46c1cfb
fix(tools): bring U7 onto the current file-safety core
ba10fd4
chore: trigger a fresh review pass at this head
1b2f187
fix(integrations): preview observation must not authorize a save; asy…
f4f917f
fix(tools): ApplyDiffTool must observe the version its diff was built on
fd1c719
test(tools): type the apply_diff observation doubles for check-types
d6446b5
fix(integrations): never adopt an autosaved match for an unauthorized…
d2d9e9d
fix(tools): never refresh a model observation the tool read on a diff…
160ddb5
fix(safety): confine before the lock, keep the post-commit backup
0f8c6c0
fix(safety): confine before mkdir, report the retained backup, stop p…
483ed65
fix(tools): honor an approval for a write outside the task root
8fc2c5b
fix(u7): no containment decision without a canonical workspace, and n…
183de84
fix(file-safety): one lock key whether the parent directory exists yet
e06bed1
test(file-safety): a backup copy that fails part-way must not survive…
3187e58
test(tools): pin which bracketing stat fails in the diff-read observa…
3183810
fix(diff-view): put the save's post-publish cleanup inside the same t…
c8caff0
fix(diff-view): let only the teardown that started a pass finalize th…
c434b0b
fix(diff-view): restore preview tabs for the pass that owns a rejecte…
9e59f01
fix(tools): pass the outside-workspace approval flag in its own position
cd5bfdc
fix(guardedWrite): one unresolvable workspace folder must not veto ev…
6b68a57
fix(guardedWrite): bind an approved outside-workspace write to the id…
510effe
fix(diff-view): keep the teardown guard held through the finalization…
376a7b9
fix(write-to-file): resolve the approved identity inside the tool's e…
d63ba73
test(tools): keep the outside-workspace flag reset on the failure path
800916e
fix(diff-view): finalize the session when a cancellation lands during…
13fa14b
fix(diff-view): one caller owns the session's finalization, by claim …
0b0c15b
fix(test): clear the merge artifacts on the U9 branch
da2e0ef
test(safety,tools) and refactor(apply-patch): make four tests prove w…
6d8f077
style: format the fifteen files the formatting gate flags on this PR
bc43841
fix(u7): align the unit with main's lint re-enable and format gate; d…
easonLiangWorldedtech 41c0295
fix(editor): withdraw the preview observation on every exit path
easonLiangWorldedtech a00c7e2
fix(editor): narrow the preview task before the withdrawal forget
easonLiangWorldedtech 14f8634
fix(editor): withdraw the preview entry by ownership, not content
easonLiangWorldedtech cf07183
fix(guarded-write): bind the publish to the authorized canonical target
easonLiangWorldedtech f756ee3
test(tools): pass toolCallId in guarded-write spec callbacks
easonLiangWorldedtech 6d2bf3e
fix(guarded-write): refuse a dangling leaf link before publishing
easonLiangWorldedtech f5fddf6
fix(diff-view): reconcile an already-open document after the guarded …
easonLiangWorldedtech c198c2e
fix(diff-view): fail saveDirectly when the reconcile leaves the targe…
easonLiangWorldedtech e349528
fix(diff-view): keep a dirty open buffer that differs from the guarde…
easonLiangWorldedtech 2f237b8
chore(knip): mark the guarded-write chain reset hook @internal
easonLiangWorldedtech f7aae8b
fix(editor): report the post-publish buffer conflict as a committed w…
easonLiangWorldedtech 8466a37
style: format the committed-write conflict assertions
easonLiangWorldedtech 44cf49b
Merge remote-tracking branch 'upstream/main' into fws/u7-tool-wiring
easonLiangWorldedtech 9edc71a
test(apply-diff): mock the stat/observation doubles the guarded read …
easonLiangWorldedtech ae4b018
fix(apply-diff): drop only the partial observation this call created …
easonLiangWorldedtech 2264dff
test(apply-diff): complete the denial fixture and assert no tool error
easonLiangWorldedtech d16fbcf
Merge upstream/main into fws/u7-tool-wiring
cbb6a16
fix(merge): restore the upstream hunks dropped by the main merge
easonLiangWorldedtech 6e8d494
fix(fws-u7): scope the clipped-read notice and re-check the lock unde…
easonliang28 1046c83
fix(fws-u7): re-verify the staging directory before the staged write
easonliang28 504af84
fix(utils): canonicalize both sides of the staging-directory drift check
easonLiangWorldedtech 40ac71f
test(utils): type the hoisted fs actuals by the callback return type
easonLiangWorldedtech 64dc74c
fix(utils): treat an unanswered drift probe as no evidence of drift
easonLiangWorldedtech 002b34f
Merge upstream/main (e943530c8, #2223) into fws/u7-tool-wiring for th…
easonLiangWorldedtech 52628eb
fix(#1918 r112): narrow four over-broad behaviours flagged in review
easonLiangWorldedtech 7f90fc2
fix(#1918 r112 follow-up): withdraw the source only if its unlink rem…
easonLiangWorldedtech 1956703
chore(file-safety): merge main 5319fe91f into fws/u7-tool-wiring
a020885
test(tools): reset the write_to_file doubles in a finally block
easonLiangWorldedtech 6222e24
test(tools): pin the flag-off apply_patch move as the unguarded raw w…
easonLiangWorldedtech aef08ed
test(utils): let the staging-directory leftover assertions see a stag…
easonLiangWorldedtech ceef734
test(tools): exercise observation-registry isolation through ReadFile…
easonLiangWorldedtech f06c6d9
fix(file-safety): compare staging containment by directory identity, …
easonLiangWorldedtech e5b4795
fix(utils): create the JSON staging file with the publish target's mode
easonLiangWorldedtech 273dd7b
test(file-safety,utils): keep the new staging rows typeclean
easonLiangWorldedtech 2b38c33
fix(safeWriteJson): treat an empty confineTo as a declared scope (#19…
easonLiangWorldedtech a39df1d
test(safeWriteJson): cover the in-scope dangling link to an outside r…
easonLiangWorldedtech 06dbe77
fix(readFileTool): stop the clipped-line notice indenting the first n…
easonLiangWorldedtech 7e00c5d
fix(diffView): reconcile the file document when a virtual document sh…
easonLiangWorldedtech 2304635
test(guardedWrite): show that a blocked publish does not hold other p…
easonLiangWorldedtech 30e2ef8
test(guardedWrite): cover resetChain while a link is still publishing…
easonLiangWorldedtech 278f428
test(safeWriteJson): show nothing is staged before the lock is held (…
easonLiangWorldedtech 0f60393
test(writeToFileTool): assert the canonical target reaches both save …
easonLiangWorldedtech c9bdb01
fix(safe-write): answer the lock-key drift guard with lock identity, …
easonLiangWorldedtech 6d85182
docs(safe-write): record what the confineTo presence test decides, an…
easonLiangWorldedtech 84b8b3a
test(file-safety): compare the aliased directory by identity, not by …
easonLiangWorldedtech 8b394b3
fix(safe-write): keep the validated staging file protected through th…
easonLiangWorldedtech 599a10b
fix(diff-view): close the tab the opener recognized, using the same c…
easonLiangWorldedtech 714988a
fix(apply_patch): refuse a file whose bytes cannot be decoded instead…
easonLiangWorldedtech dfa3032
fix(diff-view): carry a failed teardown to the cancellation that join…
easonLiangWorldedtech 9334a30
S4b (#1375): a committed publish is not undone by a cancellation
easonLiangWorldedtech 26afbd9
S4b (#1375): a committed write that is not durable keeps its authoriz…
easonLiangWorldedtech d921308
S4b (#1375): name the rejection the committed-publish rows were asser…
easonLiangWorldedtech 43d859d
S4b (#1375): create the directory the lock needs before taking the lock
easonLiangWorldedtech 3dc5140
test(S4b #1375): pin the publish-side claims the review rows named
easonLiangWorldedtech d75ed9b
test(S4b #1375): make the two lane-red rows ask the same question on …
easonLiangWorldedtech a15991d
test(S4b #1375): put the lock-key rows on the question the guard actu…
easonLiangWorldedtech 2efa9bd
test(S4b #1375): sort the acceptance row's four-name list and make it…
easonLiangWorldedtech a6a1156
test(utils): pin withFileLock's release-on-rejection contract, includ…
easonLiangWorldedtech bee03bb
test(utils): make the artefact-stat propagation row assert that its o…
easonLiangWorldedtech 0a76a7a
fix(utils): take the delete lock on the key the writer publishes under
easonLiangWorldedtech 7ae59a8
fix(utils): apply the publish target's mode to the staging fd, past t…
easonLiangWorldedtech 304db07
test(utils): gate the artefact-stat propagation row on a measured vol…
easonLiangWorldedtech c72de0c
fix(file-safety): resolveLockKey stops handing callers a key it could…
easonLiangWorldedtech f653f86
fix(apply-patch): refuse a move whose destination names the file it m…
easonLiangWorldedtech 42077a2
fix(diff-view): the committed-revert teardown says which state it end…
easonLiangWorldedtech 3159d0a
test(apply-patch): cover the move refusal under both preventFocusDisr…
easonLiangWorldedtech 15d9f8e
test(task-history): the delete rows compare files, not path spellings
easonLiangWorldedtech dc4c848
Stop the placeholder identity coming from the path it stats (upstream…
easonLiangWorldedtech File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,108 @@ | ||
| import { describe, it, expect, vi } from "vitest" | ||
|
|
||
| import { ObservationRegistry } from "../observationRegistry" | ||
|
|
||
| describe("ObservationRegistry", () => { | ||
| it("observe → get returns the recorded version and observedAt", () => { | ||
| const reg = new ObservationRegistry() | ||
| reg.observe("/a/b/c.ts", "1:2:300:4000000000:5000000000") | ||
|
|
||
| const obs = reg.get("/a/b/c.ts") | ||
| expect(obs).toBeDefined() | ||
| expect(obs!.version).toBe("1:2:300:4000000000:5000000000") | ||
| expect(typeof obs!.observedAt).toBe("number") | ||
| }) | ||
|
|
||
| it("re-observe replaces the entry with a fresh observedAt", () => { | ||
| vi.useFakeTimers() | ||
| const reg = new ObservationRegistry() | ||
| reg.observe("/a/b/c.ts", "v1") | ||
| const first = reg.get("/a/b/c.ts")! | ||
| expect(first.version).toBe("v1") | ||
|
|
||
| vi.advanceTimersByTime(50) | ||
| reg.observe("/a/b/c.ts", "v2") | ||
| const second = reg.get("/a/b/c.ts")! | ||
| expect(second.version).toBe("v2") | ||
| expect(second.observedAt).toBeGreaterThan(first.observedAt) | ||
|
|
||
| vi.useRealTimers() | ||
| }) | ||
|
|
||
| it("has returns true for observed paths, false otherwise", () => { | ||
| const reg = new ObservationRegistry() | ||
| reg.observe("/x.ts", "t1") | ||
| expect(reg.has("/x.ts")).toBe(true) | ||
| expect(reg.has("/y.ts")).toBe(false) | ||
| }) | ||
|
|
||
| it("size reflects the number of observed entries", () => { | ||
| const reg = new ObservationRegistry() | ||
| expect(reg.size).toBe(0) | ||
| reg.observe("/a.ts", "t1") | ||
| reg.observe("/b.ts", "t2") | ||
| expect(reg.size).toBe(2) | ||
| }) | ||
|
|
||
| it("clear removes all entries and resets size to 0", () => { | ||
| const reg = new ObservationRegistry() | ||
| reg.observe("/a.ts", "t1") | ||
| reg.observe("/b.ts", "t2") | ||
| reg.clear() | ||
| expect(reg.size).toBe(0) | ||
| expect(reg.get("/a.ts")).toBeUndefined() | ||
| expect(reg.has("/b.ts")).toBe(false) | ||
| }) | ||
|
|
||
| it("get on empty registry returns undefined", () => { | ||
| const reg = new ObservationRegistry() | ||
| expect(reg.get("/any.ts")).toBeUndefined() | ||
| }) | ||
|
|
||
| it("separate instances are independent — observing in one does not appear in the other", () => { | ||
| const regA = new ObservationRegistry() | ||
| const regB = new ObservationRegistry() | ||
| regA.observe("/shared.ts", "v1") | ||
| expect(regA.get("/shared.ts")).toBeDefined() | ||
| expect(regB.get("/shared.ts")).toBeUndefined() | ||
| regB.observe("/shared.ts", "v2") | ||
| expect(regA.get("/shared.ts")!.version).toBe("v1") | ||
| expect(regB.get("/shared.ts")!.version).toBe("v2") | ||
| }) | ||
|
|
||
| describe("completeness scope (S4b follow-up #46)", () => { | ||
| it("defaults to a complete observation when the read scope is not given", () => { | ||
| const reg = new ObservationRegistry() | ||
| reg.observe("/a/b/c.ts", "v1") | ||
|
|
||
| expect(reg.get("/a/b/c.ts")!.complete).toBe(true) | ||
| }) | ||
|
|
||
| it("records a partial observation when the read only returned a view of the file", () => { | ||
| const reg = new ObservationRegistry() | ||
| reg.observe("/a/b/c.ts", "v1", false) | ||
|
|
||
| expect(reg.get("/a/b/c.ts")!.complete).toBe(false) | ||
| }) | ||
|
|
||
| it("re-observing replaces the entry's completeness with the new read's scope", () => { | ||
| const reg = new ObservationRegistry() | ||
| reg.observe("/a/b/c.ts", "v1", false) | ||
| reg.observe("/a/b/c.ts", "v2") | ||
|
|
||
| const obs = reg.get("/a/b/c.ts")! | ||
| expect(obs.version).toBe("v2") | ||
| expect(obs.complete).toBe(true) | ||
| }) | ||
|
|
||
| it("re-observing with a partial scope downgrades a previously complete entry", () => { | ||
| const reg = new ObservationRegistry() | ||
| reg.observe("/a/b/c.ts", "v1") | ||
| reg.observe("/a/b/c.ts", "v2", false) | ||
|
|
||
| const obs = reg.get("/a/b/c.ts")! | ||
| expect(obs.version).toBe("v2") | ||
| expect(obs.complete).toBe(false) | ||
| }) | ||
| }) | ||
| }) |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| /** | ||
| * Per-task file observation registry (upstream epic #1375, phase A2). | ||
| * | ||
| * Each Task owns its own instance so parent and subtask observations are | ||
| * independent. The S4 guarded-write will compare these versions against the | ||
| * token recomputed pre-write to detect stale reads or file replacement. | ||
| * | ||
| * Pure in-memory — zero I/O, no dependencies. The S4 guarded-write consults | ||
| * these observations for the version check and for the completeness check that | ||
| * gates a full-file replacement. | ||
| */ | ||
|
|
||
| export interface FileObservation { | ||
| /** Version token derived from on-disk fs.stat (bigint mode). */ | ||
| version: string | ||
| /** Millisecond timestamp when the observation was recorded. */ | ||
| observedAt: number | ||
| /** | ||
| * Whether the read that produced this observation returned the complete | ||
| * file. A slice, line-range, truncated, or indentation-block read returns | ||
| * only a view of the file; such an observation authorizes targeted edits | ||
| * on the view the model saw, but never a full-file replacement. | ||
| */ | ||
| complete: boolean | ||
| } | ||
|
|
||
| export class ObservationRegistry { | ||
| private readonly entries = new Map<string, FileObservation>() | ||
|
|
||
| /** | ||
| * Record an observation for a file at its absolute path. | ||
| * | ||
| * Re-observing replaces the entry with a fresh observedAt timestamp, the | ||
| * new version token, and the read's completeness. `complete` defaults to | ||
| * true for callers that read the whole file themselves (spec doubles, | ||
| * WriteToFileTool). A caller whose read is internal to a targeted edit must | ||
| * carry the model's prior completeness instead, so the tool's own read cannot | ||
| * upgrade a partial read into authority for a full-file replacement. | ||
| */ | ||
| observe(absolutePath: string, version: string, complete: boolean = true): void { | ||
| this.entries.set(absolutePath, { version, observedAt: Date.now(), complete }) | ||
| } | ||
|
|
||
| get(absolutePath: string): FileObservation | undefined { | ||
| return this.entries.get(absolutePath) | ||
| } | ||
|
|
||
| has(absolutePath: string): boolean { | ||
| return this.entries.has(absolutePath) | ||
| } | ||
|
|
||
| /** | ||
| * Drop the observation for one path. A caller that must revoke an | ||
| * authorization it did not earn - a preview that observed a version the model | ||
| * never read - needs this instead of clear(), which would also discard the | ||
| * observations other reads of the same task still rely on. | ||
| */ | ||
| forget(absolutePath: string): boolean { | ||
| return this.entries.delete(absolutePath) | ||
| } | ||
|
|
||
| clear(): void { | ||
| this.entries.clear() | ||
| } | ||
|
|
||
| get size(): number { | ||
| return this.entries.size | ||
| } | ||
| } |
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
Oops, something went wrong.
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Script executed:
Repository: Zoo-Code-Org/Zoo-Code
Length of output: 3966
Script executed:
Repository: Zoo-Code-Org/Zoo-Code
Length of output: 47074
Script executed:
Repository: Zoo-Code-Org/Zoo-Code
Length of output: 42352
Script executed:
Repository: Zoo-Code-Org/Zoo-Code
Length of output: 14086
Script executed:
Repository: Zoo-Code-Org/Zoo-Code
Length of output: 43483
Script executed:
Repository: Zoo-Code-Org/Zoo-Code
Length of output: 23898
Script executed:
Repository: Zoo-Code-Org/Zoo-Code
Length of output: 8655
Revoke complete observations when condensation hides their reads.
If condensation omits file content, the old complete observation still authorizes
write_to_file. An approved replacement built from the summary can therefore delete omitted content. Revoke that authority when the corresponding read leaves the model’s context.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The authority outlives the read, measurably.
src/core/task/Task.ts:302isreadonly observationRegistry = new ObservationRegistry()— one registry for the life of the task, not for the life of the context — and the condensation path never touches it:condenseContextlives at:2682, and a search forobservationRegistryacrosssrc/core/condensereturns nothing at all. So when condensation replaces a full read with a summary, the complete observation for that file is still in the map withcomplete: true(src/core/task/observationRegistry.ts:24, set byobserveat:40-:42), andwrite_to_filestill treats the file as read.That makes your consequence real: an approved replacement generated from the summary can drop content the model can no longer see, and nothing in the registry says the read that authorized it has left the conversation.
Revoking on condensation is not a lookup either — it needs condensation to report which file reads it omitted, which is a fact about the condenser's output rather than about the registry. Major and heavy-lift, in a main-owned file, on a line this branch added: surfaced for you, and I have not guessed at the mechanism.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@easonLiangWorldedtech Agreed. The task-owned registry is not updated when condensation changes the model’s context. A complete observation can therefore continue to authorize a full replacement after the read is hidden.
The finding remains open. A regression test should verify that an omitted complete read no longer authorizes replacement, while a complete read still in context retains its authority. It should also cover failed or cancelled condensation.
You are interacting with an AI system.