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
67
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.
Open
Changes from all commits
Commits
Show all changes
67 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 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
| 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.