Repository navigation
feat: 1.3.4 — 文件工具读后写保护(内容哈希校验) - #37
Conversation
The built-in file tools let the model replace or edit any file in the workspace, including one it had never read, so a stale plan could silently discard content the user or another Agent had written in the meantime. file_read now records the SHA-256 of the whole file -- streamed in 64KB reads, so a large file is never loaded just to identify it -- in an in-memory, mutex-guarded FileObservationStore keyed by the resolved absolute path. file_write and file_edit consult it before changing a file that already exists: no record means the file was never read, and a record that no longer matches the bytes on disk means it changed after the read. Both cases refuse the mutation and return a localized instruction to read the file again, so the next step is a read of that revision instead of a retry of the same blind overwrite. Creating a file needs no read, a successful write or edit refreshes the record so a run of edits does not re-read between steps, and a file that disappeared after its read counts as a creation again. The record is not persisted and is capped, so a long-lived process cannot grow without bound, and dropping it only ever costs another read. Verification: ctest (122/122, including the new file_observation_tests with published SHA-256 vectors and the read-before-write cases in file_tool_tests), :app:assembleDebug, and tools/gen_tool_text_catalog.py --check.
versionCode 38 and versionName 1.3.4, with update.md carrying the release notes for the file tools reading a target before they may overwrite or edit it.
审查者指南本 PR 在 application 层引入可注入、线程安全的文件版本观察机制和流式 SHA-256 校验,并将其挂接到 file_read、file_write 与 file_edit 生命周期中,使已存在文件必须基于当前已读版本才能修改;同时补充双语错误提示、全面测试覆盖并发布 Android 1.3.4。 读取后写入文件保护时序图sequenceDiagram
actor AI
participant Registry as FileToolRegistry
participant Store as FileObservationStore
participant Files as ToolFileAccess
AI->>Registry: file_read(path)
Registry->>Files: ReadBytes(path, offset, 65536)
Files-->>Registry: file content chunks
Registry->>Files: ReadBytes(path, offset, 65536)
Files-->>Registry: end of file
Registry->>Store: Record(absolute_path, SHA-256)
Registry-->>AI: file contents
AI->>Registry: file_write(path, content)
Registry->>Store: ObservedHash(absolute_path)
Store-->>Registry: recorded hash
Registry->>Files: ReadBytes(path, offset, 65536)
Files-->>Registry: current file chunks
Registry->>Registry: compare current hash with recorded hash
alt hash matches
Registry->>Files: write file
Files-->>Registry: success
Registry->>Store: Record(absolute_path, SHA-256)
Registry-->>AI: success
else file not read or hash changed
Registry-->>AI: ToolFailure with file_read retry message
end
文件变更保护结果流程图flowchart TD
Start["file_write or file_edit"] --> Exists{Existing file?}
Exists -->|No| Mutate["Create or mutate file"]
Exists -->|Yes| Observed{ObservedHash exists?}
Observed -->|No| RefuseRead["Refuse mutation: use file_read first"]
Observed -->|Yes| Hash["HashFile in 64 KB chunks"]
Hash --> Match{Hash matches recorded revision?}
Match -->|No| RefuseStale["Refuse mutation: read file again"]
Match -->|Yes| Mutate
Mutate --> Refresh["RememberObserved after successful write/edit"]
文件级变更
提示与命令与 Sourcery 交互
自定义使用体验访问你的控制面板以:
获取帮助Original review guide in EnglishReviewer's Guide本 PR 在 application 层引入可注入、线程安全的文件版本观察机制和流式 SHA-256 校验,并将其挂接到 file_read、file_write 与 file_edit 生命周期中,使已存在文件必须基于当前已读版本才能修改;同时补充双语错误提示、全面测试覆盖并发布 Android 1.3.4。 Sequence diagram for read-before-write file protectionsequenceDiagram
actor AI
participant Registry as FileToolRegistry
participant Store as FileObservationStore
participant Files as ToolFileAccess
AI->>Registry: file_read(path)
Registry->>Files: ReadBytes(path, offset, 65536)
Files-->>Registry: file content chunks
Registry->>Files: ReadBytes(path, offset, 65536)
Files-->>Registry: end of file
Registry->>Store: Record(absolute_path, SHA-256)
Registry-->>AI: file contents
AI->>Registry: file_write(path, content)
Registry->>Store: ObservedHash(absolute_path)
Store-->>Registry: recorded hash
Registry->>Files: ReadBytes(path, offset, 65536)
Files-->>Registry: current file chunks
Registry->>Registry: compare current hash with recorded hash
alt hash matches
Registry->>Files: write file
Files-->>Registry: success
Registry->>Store: Record(absolute_path, SHA-256)
Registry-->>AI: success
else file not read or hash changed
Registry-->>AI: ToolFailure with file_read retry message
end
Flow diagram for file mutation guard outcomesflowchart TD
Start["file_write or file_edit"] --> Exists{Existing file?}
Exists -->|No| Mutate["Create or mutate file"]
Exists -->|Yes| Observed{ObservedHash exists?}
Observed -->|No| RefuseRead["Refuse mutation: use file_read first"]
Observed -->|Yes| Hash["HashFile in 64 KB chunks"]
Hash --> Match{Hash matches recorded revision?}
Match -->|No| RefuseStale["Refuse mutation: read file again"]
Match -->|Yes| Mutate
Mutate --> Refresh["RememberObserved after successful write/edit"]
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
嘿——我发现了 7 个问题
面向 AI Agent 的提示
请处理此次代码审查中的评论:
## 个别评论
### 评论 1
<location path="src/application/file_tool_registry.cpp" line_range="771" />
<code_context>
+ // cannot discard anything.
+ if (info.has_value()) {
+ const auto guard =
+ co_await CheckObservedRevision(context, target->absolute);
+ if (guard != MutationGuard::allowed)
+ co_return MutationRefused(context, guard, input_path);
</code_context>
<issue_to_address>
**并发更改会覆盖已验证的内容**
当其他写入者在版本检查之后、变更操作之前修改文件时,`CheckObservedRevision` 会在 `WriteText` 或 `file_edit` 的读取并重写序列之前完成,因此变更操作不会验证它实际修改的版本。并发写入也可能基于同一个摘要同时通过检查,后执行的写入会覆盖先执行的更改,而两者都报告成功。
请将版本检查与对应的写入或编辑设为原子操作,以便在文件于验证后发生更改时使变更失败。
另见 `src/application/file_tool_registry.cpp:772`、`src/application/file_tool_registry.cpp:855-857`。
</issue_to_address>
### 评论 2
<location path="src/application/file_tool_registry.cpp" line_range="769" />
<code_context>
+ // Read before write: an existing file may only be overwritten after a read
+ // that still matches what is on disk. A missing file is a creation, which
+ // cannot discard anything.
+ if (info.has_value()) {
+ const auto guard =
+ co_await CheckObservedRevision(context, target->absolute);
</code_context>
<issue_to_address>
**现有文件绕过了读取保护**
当对一个现有目标执行 `Stat` 失败,且错误不是 `not_found`,但后续写入成功时,`info.has_value()` 为 false,因此 `ExecuteFileWrite` 会将目标视为新文件并跳过 `CheckObservedRevision`。随后 `WriteText` 可能在没有记录读取的情况下覆盖现有文件。
请区分 `not_found` 与其他 `Stat` 错误;发生错误时应失败,而不是将目标当作不存在。
</issue_to_address>
### 评论 3
<location path="src/application/file_tool_registry.cpp" line_range="640" />
<code_context>
}
+ // The read succeeded, so the model has seen this revision: record it before
+ // returning, which is what authorizes a later write of the same file.
+ co_await RememberObserved(context, target->absolute);
co_return ToolSuccess(TruncateContent(AddLineNumbers(*content, 1)));
}
</code_context>
<issue_to_address>
**未被看见的文件内容可能被覆盖**
当范围读取只返回文件的一部分,或整文件结果被截断,而模型随后替换或编辑文件时,`RememberObserved` 会记录整个文件的摘要,尽管模型只收到一个窗口或截断后的输出。`file_write` 或 `file_edit` 会接受该摘要作为模型已看到完整版本的证明,从而可能丢失未被看见的内容。
不要根据部分读取或截断读取授权整文件变更;只有在模型收到完整文件时才记录观察结果。
另见 `src/application/file_tool_registry.cpp:715-716`。
</issue_to_address>
### 评论 4
<location path="src/application/file_tool_registry.cpp" line_range="640" />
<code_context>
}
+ // The read succeeded, so the model has seen this revision: record it before
+ // returning, which is what authorizes a later write of the same file.
+ co_await RememberObserved(context, target->absolute);
co_return ToolSuccess(TruncateContent(AddLineNumbers(*content, 1)));
}
</code_context>
<issue_to_address>
**未被看见的更改会被授权**
当文件在读取内容之后、`RememberObserved` 完成哈希之前发生更改时,`RememberObserved` 会独立于内容读取再次对路径进行哈希,因此可能记录模型并未收到的新版本。后续变更会通过保护检查,并可能覆盖这些未被看见的内容。
请将记录的摘要绑定到读取返回的确切字节,而不是之后再对路径进行哈希。
另见 `src/application/file_tool_registry.cpp:716`、`src/application/file_tool_registry.cpp:801`、`src/application/file_tool_registry.cpp:882`。
</issue_to_address>
### 评论 5
<location path="src/application/file_tool_registry.cpp" line_range="640" />
<code_context>
}
+ // The read succeeded, so the model has seen this revision: record it before
+ // returning, which is what authorizes a later write of the same file.
+ co_await RememberObserved(context, target->absolute);
co_return ToolSuccess(TruncateContent(AddLineNumbers(*content, 1)));
}
</code_context>
<issue_to_address>
**并发编辑绕过版本保护**
当其他写入者在读取内容与 `RememberObserved` 之间、保护检查的哈希与变更之间,或写入与写入后的哈希之间修改文件时,`RememberObserved` 可能授权模型未收到的字节,而 `CheckObservedRevision` 可能验证一个在写入前已经改变的版本。工具随后会在未检测到更改的情况下覆盖未被看见的内容。
请将观察结果绑定到读取返回的确切字节,并将每个受保护的变更与其版本检查设为原子操作。
另见 `src/application/file_tool_registry.cpp:715`、`src/application/file_tool_registry.cpp:771-773`、`src/application/file_tool_registry.cpp:801`、`src/application/file_tool_registry.cpp:855-857`、`src/application/file_tool_registry.cpp:882`。
</issue_to_address>
### 评论 6
<location path="src/application/file_tool_registry.cpp" line_range="467" />
<code_context>
+ Sha256Digest digest;
+ std::uint64_t offset = 0;
+ while (true) {
+ auto chunk = co_await files.ReadBytes(path, offset, kHashChunkBytes);
+ if (!chunk)
+ co_return std::unexpected(chunk.error());
</code_context>
<issue_to_address>
**大文件哈希会重复读取前缀**
当文件需要通过 `HuxToolFileAccess` 分多个哈希块处理时,`HashFile` 会以递增的偏移量调用 `ReadBytes`,而 `HuxToolFileAccess::ReadBytes` 每次调用都会打开新的流并消耗被跳过的前缀。哈希过程会反复重新读取之前的字节,使大文件读取和受保护的变更极其缓慢,甚至容易超时。
请通过单个顺序流进行哈希,或以其他方式确保每个字节只读取一次。
</issue_to_address>
### 评论 7
<location path="tests/file_tool_tests.cpp" line_range="1336-1342" />
<code_context>
+ invoked = co_await scenario->tools->Invoke("file_read",
+ R"({"file_path":"edit.txt"})");
+ EXPECT_EXPRESSION(invoked && !invoked->error);
invoked = co_await scenario->tools->Invoke(
"file_edit",
R"({"file_path":"edit.txt","old_string":"world","new_string":"there"})");
</code_context>
<issue_to_address>
**短范围读取未被隔离**
当测试在此前成功读取 `large.txt` 的范围之后运行时,即使回归问题导致这次短范围读取不再被记录,测试仍会通过:此前第 836–839 行对 `large.txt` 的 `file_read` 使用同一个注册表,可能为后续的 `file_write` 留下有效观察结果。第 1341–1342 行的断言随后会使用此前的记录通过,而没有验证本次范围读取是否授权了写入。
请在短范围读取用例之前使用新的路径,或清空观察存储,以确保只有这次读取能够授权写入。
</issue_to_address>Sourcery 评估
需要人工审查。 请先处理 7 个发现;新的基于哈希的保护机制控制现有文件是否可以被覆盖或编辑。如果它错误地接受了过期的观察结果,未被看见的工作区内容可能遭到破坏,并且即使回滚代码,这种破坏也会持续存在。回滚会移除保护机制,但不会恢复已经被更改的文件内容。
阻塞性发现:src/application/file_tool_registry.cpp:771、src/application/file_tool_registry.cpp:769、src/application/file_tool_registry.cpp:640、src/application/file_tool_registry.cpp:640、src/application/file_tool_registry.cpp:640,以及另外 2 个
Original comment in English
Hey - I've found 7 issues
Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments
### Comment 1
<location path="src/application/file_tool_registry.cpp" line_range="771" />
<code_context>
+ // cannot discard anything.
+ if (info.has_value()) {
+ const auto guard =
+ co_await CheckObservedRevision(context, target->absolute);
+ if (guard != MutationGuard::allowed)
+ co_return MutationRefused(context, guard, input_path);
</code_context>
<issue_to_address>
**Concurrent changes overwrite validated content**
When another writer changes the file after the revision check and before the mutation, `CheckObservedRevision` finishes before `WriteText` or `file_edit`'s read-and-rewrite sequence, so the mutation does not verify the revision it changes. Concurrent writes can also both pass against the same digest, with the later one overwriting the earlier change while both report success.
Make the revision check and the corresponding write or edit atomic, so a mutation fails if the file changed after validation.
Also at `src/application/file_tool_registry.cpp:772`, `src/application/file_tool_registry.cpp:855-857`.
</issue_to_address>
### Comment 2
<location path="src/application/file_tool_registry.cpp" line_range="769" />
<code_context>
+ // Read before write: an existing file may only be overwritten after a read
+ // that still matches what is on disk. A missing file is a creation, which
+ // cannot discard anything.
+ if (info.has_value()) {
+ const auto guard =
+ co_await CheckObservedRevision(context, target->absolute);
</code_context>
<issue_to_address>
**Existing files bypass the read guard**
When `Stat` fails for an existing target with an error other than `not_found`, and the subsequent write succeeds, `info.has_value()` is false, so `ExecuteFileWrite` treats the target as new and skips `CheckObservedRevision`. `WriteText` can then overwrite an existing file without a recorded read.
Distinguish `not_found` from other `Stat` errors and fail on errors instead of treating them as a missing target.
</issue_to_address>
### Comment 3
<location path="src/application/file_tool_registry.cpp" line_range="640" />
<code_context>
}
+ // The read succeeded, so the model has seen this revision: record it before
+ // returning, which is what authorizes a later write of the same file.
+ co_await RememberObserved(context, target->absolute);
co_return ToolSuccess(TruncateContent(AddLineNumbers(*content, 1)));
}
</code_context>
<issue_to_address>
**Unseen file content can be overwritten**
When a range read returns only part of a file, or a whole-file result is truncated, and the model then replaces or edits the file, `RememberObserved` records the digest of the whole file even though the model received only a window or truncated output. `file_write` or `file_edit` accepts that digest as proof the model saw the full revision, allowing unseen content to be lost.
Do not authorize a full-file mutation from a partial or truncated read; record an observation only when the model receives the complete file.
Also at `src/application/file_tool_registry.cpp:715-716`.
</issue_to_address>
### Comment 4
<location path="src/application/file_tool_registry.cpp" line_range="640" />
<code_context>
}
+ // The read succeeded, so the model has seen this revision: record it before
+ // returning, which is what authorizes a later write of the same file.
+ co_await RememberObserved(context, target->absolute);
co_return ToolSuccess(TruncateContent(AddLineNumbers(*content, 1)));
}
</code_context>
<issue_to_address>
**Unseen changes become authorized**
When the file changes after its content is read but before `RememberObserved` finishes hashing it, `RememberObserved` hashes the path separately from the content read, so it can record a newer revision the model did not receive. A later mutation passes the guard and can overwrite that unseen content.
Bind the recorded digest to the exact bytes returned by the read rather than hashing the path afterward.
Also at `src/application/file_tool_registry.cpp:716`, `src/application/file_tool_registry.cpp:801`, `src/application/file_tool_registry.cpp:882`.
</issue_to_address>
### Comment 5
<location path="src/application/file_tool_registry.cpp" line_range="640" />
<code_context>
}
+ // The read succeeded, so the model has seen this revision: record it before
+ // returning, which is what authorizes a later write of the same file.
+ co_await RememberObserved(context, target->absolute);
co_return ToolSuccess(TruncateContent(AddLineNumbers(*content, 1)));
}
</code_context>
<issue_to_address>
**Concurrent edits bypass revision protection**
When another writer changes the file between the content read and `RememberObserved`, between the guard hash and the mutation, or between a write and its post-write hash, `RememberObserved` can authorize bytes the model did not receive, and `CheckObservedRevision` can validate a revision that changes before the write. The tool then overwrites unseen content without detecting the change.
Bind observations to the exact bytes returned and make each guarded mutation atomic with its revision check.
Also at `src/application/file_tool_registry.cpp:715`, `src/application/file_tool_registry.cpp:771-773`, `src/application/file_tool_registry.cpp:801`, `src/application/file_tool_registry.cpp:855-857`, `src/application/file_tool_registry.cpp:882`.
</issue_to_address>
### Comment 6
<location path="src/application/file_tool_registry.cpp" line_range="467" />
<code_context>
+ Sha256Digest digest;
+ std::uint64_t offset = 0;
+ while (true) {
+ auto chunk = co_await files.ReadBytes(path, offset, kHashChunkBytes);
+ if (!chunk)
+ co_return std::unexpected(chunk.error());
</code_context>
<issue_to_address>
**Large-file hashing rereads prefixes**
When a file requires multiple hash chunks through `HuxToolFileAccess`, `HashFile` calls `ReadBytes` at increasing offsets, and `HuxToolFileAccess::ReadBytes` opens a new stream and consumes the skipped prefix on each call. Hashing rereads earlier bytes repeatedly, making large-file reads and guarded mutations extremely slow or prone to timing out.
Hash through one sequential stream, or otherwise read each byte only once.
</issue_to_address>
### Comment 7
<location path="tests/file_tool_tests.cpp" line_range="1336-1342" />
<code_context>
+ invoked = co_await scenario->tools->Invoke("file_read",
+ R"({"file_path":"edit.txt"})");
+ EXPECT_EXPRESSION(invoked && !invoked->error);
invoked = co_await scenario->tools->Invoke(
"file_edit",
R"({"file_path":"edit.txt","old_string":"world","new_string":"there"})");
</code_context>
<issue_to_address>
**Short range read is not isolated**
When the test runs after the earlier successful range read of `large.txt`, a regression that stops recording this short range read still passes: the earlier `file_read` of `large.txt` at lines 836–839 uses the same registry and can leave a valid observation for the later `file_write`. The assertions at lines 1341–1342 then pass using that earlier record, without verifying that this range read authorized the write.
Use a fresh path or clear the observation store before the short range-read case so only that read can authorize the write.
</issue_to_address>Sourcery assessment
Needs a human reviewer. 7 findings to address first, and the new hash-based guard controls whether existing files may be overwritten or edited; if it incorrectly accepts a stale observation, unseen workspace content can be corrupted and that damage persists after reverting the code. Reverting removes the guard, but it does not restore any file contents already changed.
Blocking findings: src/application/file_tool_registry.cpp:771, src/application/file_tool_registry.cpp:769, src/application/file_tool_registry.cpp:640, src/application/file_tool_registry.cpp:640, src/application/file_tool_registry.cpp:640, and 2 more
| // cannot discard anything. | ||
| if (info.has_value()) { | ||
| const auto guard = | ||
| co_await CheckObservedRevision(context, target->absolute); |
There was a problem hiding this comment.
🟠 高 · 并发更改会覆盖已验证的内容
当其他写入者在版本检查之后、变更操作之前修改文件时,CheckObservedRevision 会在 WriteText 或 file_edit 的读取并重写序列之前完成,因此变更操作不会验证它实际修改的版本。并发写入也可能基于同一个摘要同时通过检查,后执行的写入会覆盖先执行的更改,而两者都报告成功。
请将版本检查与对应的写入或编辑设为原子操作,以便在文件于验证后发生更改时使变更失败。
另见 src/application/file_tool_registry.cpp:772、src/application/file_tool_registry.cpp:855-857。
面向 AI Agent 的提示
在 `src/application/file_tool_registry.cpp` 第 771 行:
**并发更改会覆盖已验证的内容**
当其他写入者在版本检查之后、变更操作之前修改文件时,`CheckObservedRevision` 会在 `WriteText` 或 `file_edit` 的读取并重写序列之前完成,因此变更操作不会验证它实际修改的版本。并发写入也可能基于同一个摘要同时通过检查,后执行的写入会覆盖先执行的更改,而两者都报告成功。
请将版本检查与对应的写入或编辑设为原子操作,以便在文件于验证后发生更改时使变更失败。
另见 `src/application/file_tool_registry.cpp:772`、`src/application/file_tool_registry.cpp:855-857`。Original comment in English
🟠 High · Concurrent changes overwrite validated content
When another writer changes the file after the revision check and before the mutation, CheckObservedRevision finishes before WriteText or file_edit's read-and-rewrite sequence, so the mutation does not verify the revision it changes. Concurrent writes can also both pass against the same digest, with the later one overwriting the earlier change while both report success.
Make the revision check and the corresponding write or edit atomic, so a mutation fails if the file changed after validation.
Also at src/application/file_tool_registry.cpp:772, src/application/file_tool_registry.cpp:855-857.
Prompt for AI agents
In `src/application/file_tool_registry.cpp` at line 771:
**Concurrent changes overwrite validated content**
When another writer changes the file after the revision check and before the mutation, `CheckObservedRevision` finishes before `WriteText` or `file_edit`'s read-and-rewrite sequence, so the mutation does not verify the revision it changes. Concurrent writes can also both pass against the same digest, with the later one overwriting the earlier change while both report success.
Make the revision check and the corresponding write or edit atomic, so a mutation fails if the file changed after validation.
Also at `src/application/file_tool_registry.cpp:772`, `src/application/file_tool_registry.cpp:855-857`.There was a problem hiding this comment.
已修复(提交 9d13fa2)。
- 进程内:新增按路径的变更占用标记(
FileObservationStore::Claim/Release+ RAII 的MutationClaim),覆盖「读取 → 校验 → 写入 → 记录」整段。并发中的第二个工具调用会得到Cannot modify …: another tool call is already changing this file. Try again.,而不是两次都通过校验、后者覆盖前者却都报成功。 - 校验与变更用同一份字节:
CheckObservedRevision现在比较的是本次变更自己已经读到的那份内容(也是file_edit用来替换、file_write用来生成 diff 的同一份),校验与写入之间只剩WriteText这一个co_await。 - 残留窗口:
ToolFileAccess端口没有 compare-and-swap,进程之外的写入者在读取与写入之间改动文件无法被拦截。这一点已写在MutationClaim的注释和update.md里;要彻底消除需要给端口增加原子替换原语(replaceIfVersion之类),本次没有改动端口契约。
Address the review of the read-before-write guard. The shape of the protection stays the same; what changes is what authorizes a mutation and what happens while one is in flight. - Record the digest of the exact text the read returned instead of hashing the path again afterwards, so a record can never describe a revision that appeared between the read and the record. The guard likewise compares against the bytes the mutation already read for its own work, which removes a second look at the file and a window between the check and the write. - Track whether the model received the whole revision. file_write replaces the target outright, so it now requires a complete view: a windowed `start_kb`/`end_kb` read, or a whole-file read whose rendered output was truncated, records a partial observation, and a replacement is refused in favour of file_edit. file_edit keeps accepting a partial view because it carries over every byte it does not replace -- without that, files above the 50KB single-read limit could never be edited at all. - Hold a per-path claim across read, check, write and record. Two tool calls that both passed the freshness check used to both write, and the later one silently dropped the earlier one's change while both reported success; the second call is now refused with a retry hint. A writer outside the process cannot be excluded, because the file capability offers no compare-and-swap; that remaining window is documented at the claim and in update.md. - Treat a Stat failure that is not `not_found` as an error instead of a missing target, so a permission or I/O failure can no longer turn an existing file into a "creation" that skips the guard. The streaming hash helper is gone with the second look at the path, which also removes its repeated prefix reads. Tests: the range-read case now clears the record and reads a fresh path, so only that read can authorize what follows. New coverage for partial versus complete views, the truncated view, the exact digest recorded, a held claim and its release, and Stat failures on both write and edit. Verification: ctest 122/122, :app:lintDebug 0 errors, tools/gen_tool_text_catalog.py --check (80 keys), :app:assembleDebug, and the fake AI server tests 29/29.
Summary / 概述
新增文件工具读后写保护并发布 v1.3.4:AI 覆盖或编辑一个已存在的文件之前,必须先读取它;读取成功时把整份文件内容的 SHA-256 记在内存里,写入前重新计算并比对,不一致就拒绝写入并要求 AI 重新读取,避免无声覆盖它从未看过的内容。
master包含 2 个提交(64bfb31、7bcac76)、15 个文件,+874 / -8。versionCode38、versionName1.3.4;update.md已按既有版式补上## v1.3.4段落,发布工作流(master 上platform/android/app/build.gradle变更触发)可直接切出。Closes。User-visible behavior / 用户可见行为
对模型(工具调用)可见的行为变化:
file_write覆盖已存在、从未读取的文件Cannot modify <path>: the file already exists but has not been read. Read it with file_read first, then try again.file_edit编辑从未读取的文件old_string替换Cannot modify <path>: the file changed after it was read. Read it again with file_read, then try again.file_write新建文件file_delete两条拒绝文案都有中英双语文案,通过既有的
ToolLanguage与工具文案目录输出。对用户可见:拒绝时工具卡片显示既有的错误态与上述可读文案;没有新增界面。
实现要点 / Design
src/application/file_content_hash.{h,cpp}:无第三方依赖的流式 SHA-256(Sha256Digest::Update/Hex、一次性Sha256Hex),摘要与分块方式无关。src/application/file_observation_store.{h,cpp}:线程安全的内存「绝对路径 → 内容摘要」表;容量上限 4096,超出后整表作废(记录只是许可提示,丢弃只会多读一次,绝不会放宽限制);不落盘,重启后从零开始,只会更严格。HashFile():以 64KB 分块读取计算整份文件摘要,大文件不会因识别版本而整份载入内存。RememberObserved():记录工具读/写完之后的版本;无法计算摘要时改为遗忘,该文件按「未读」处理。CheckObservedRevision():返回allowed / not_read / changed;changed同时覆盖「读过之后变了」和「无法读回校验」。MutationRefused():统一产出本地化拒绝文案,始终带上补救办法。file_read成功(整份读或start_kb窗口读)后记录;file_write命中已存在文件、file_edit进入替换之前检查;两者写入成功后刷新记录。FileToolRegistry构造新增可选的FileObservationStore(默认自建),FileToolContext增加observations指针;需要时多个注册表可共享同一份「已读」记录(测试覆盖了共享注入)。default.properties/zh.properties各新增tool_file_write_needs_read、tool_file_write_stale_read;src/application/tool_text_catalog.{h,cpp}由tools/gen_tool_text_catalog.py重新生成(76 → 78 键,kToolTextKeyCount同步)。getDescription()逐字一致(读后写要求放在拒绝文案里,不写进 schema 描述);file_delete不在本次范围,它已经有删除理由 + 用户确认两道闸。erase,FileObservationStore::Forget()改为find+ 按迭代器删除——这个问题正是:app:assembleDebug跑出来并当场修掉的。Validation / 测试与验证
-j8—— 122/122(新增file_observation_tests目标后由 121 增至 122):app:lintDebug0 errors, 17 warnings(17 条均为既有项,与本改动无关):app:assembleDebugBUILD SUCCESSFUL,且BuildConfig与 APK 元数据均为VERSION_CODE=38、VERSION_NAME="1.3.4"adb devices为空(无设备/模拟器);本次没有 UI 改动,用户可见部分只是既有工具卡片的错误态文案,因此未做视觉复核Commands and environment used:
新增/扩写的覆盖:
tests/file_observation_tests.cpp(新增目标):SHA-256 标准向量(空串、abc、56 字节跨两块、恰好一块、多块、含0x00–0xFF的二进制串)与「任意分块结果一致」;观察表的记录 / 覆盖 / 遗忘 / 清空 / 容量上限。tests/file_tool_tests.cpp第 13 节:未读拒绝、读取后放行、工具自身写入刷新记录、外部改动拒绝且文件内容未被改写、内容相同不算改动、窗口读取(start_kb)授权整份覆写、读取失败不授权、写入失败后仍可重试、读取后被外部删除可重建、file_edit的未读与过期两条路径、中英文文案、共享记录注入到第二个注册表。file_write/file_edit用例按新规则先读取目标,语义不变。Design and safety checklist
ToolFileAccess端口未改,只用既有ReadBytes)ifchain.(观察表独立成类且可注入;工具仍由既有描述符表驱动,没有新增按名字分支)Screenshots or recordings
本次没有界面改动:改动全部发生在工具执行路径,用户能看到的是既有工具卡片在拒绝时的错误态与文案(中英文各一条)。本机也没有可用设备或模拟器,未附截图;如需实际界面复核,我可以在设备上补拍并更新本段。
CLA — required
CLA Signature: @LangLang03
Sourcery 摘要
通过要求当前的先前读取来保护现有文件变更,并将该变更作为 Android 1.3.4 版本发布。
新功能:
file_write覆盖现有文件或file_edit修改现有文件之前先读取文件;如果文件自记录读取后发生变化,则拒绝变更。错误修复:
增强功能:
构建:
文档:
测试:
Original summary in English
Sourcery 摘要
通过要求当前的先前读取来保护现有文件的修改,并拒绝可能覆盖未查看或已更改内容的写入操作。
新功能:
file_write替换或file_edit修改前读取现有文件;如果文件自记录读取后发生更改,则拒绝修改。错误修复:
增强功能:
构建:
文档:
测试:
Original summary in English
Summary by Sourcery
Protect existing file mutations by requiring a current prior read and rejecting writes that could overwrite unseen or changed content.
New Features:
file_writereplacement orfile_editmodification, and reject changes when the file has changed since the recorded read.Bug Fixes:
Enhancements:
Build:
Documentation:
Tests: