@hachej/boring-agent 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/file-guard.js +7 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
## One file tool set: the guard
|
|
10
10
|
|
|
11
|
-
The agent edits files with Pi's own `read`, `write`, `edit` and `bash` over the workspace's `ExecutionEnv`; there are no document or artifact tools of Boring's. `createFileGuard({ workspace, resolveAccess?, name? })` from `@hachej/boring-agent/file-guard` returns a native extension (named `name`, default `boring.files.guard`: two agents with different workspaces in one registry need two names, or the last installed guard wraps both) that wraps those three tools with the public `wrapTool` (select it after the extension that registers them). Every wrapped call runs inside the workspace provider's mutation queue (`@hachej/boring-files/workspace`), so a viewer save cannot land between `edit`'s internal read and its write. `workspace` is the call's workspace resolved like Pi's environment, or one binding `{ files, root, access? }` (see [Workspace per call](#workspace-per-call)); `resolveAccess` defaults to the binding's `access`. A genuine `read` call records the file's revision as the conversation's last-read revision of that workspace and path (`baselineKey`) in a native conversation document (`lastReadRevisions`, kept across restarts; a fork starts empty); `edit`'s internal read records nothing. `write` and `edit` of a file that exists are refused unless the conversation has read it and its revision still equals the recorded one, and the refusal tells the model to read the file again. Creating a file is allowed only while it is absent. The guard fails closed: an unresolvable workspace, a refused provider read, a path outside the root, a link that leads outside it and a path that does not exist (so Pi's read never falls back to another spelling of it, such as an NFD name) (resolved through the env's `canonicalPath`, so also a new file under a link directory) are refused for `read` as well as `write` and `edit`; nothing falls through to Pi's native tool. After a successful `write` or `edit` the baseline is the revision just written. Pi's `edit` tries an exact match first and falls back to a fuzzy one (Unicode normalisation, trailing whitespace, smart quotes, dashes and special spaces); the guard does not change that. A `bash` write is not intercepted, but it changes the revision, so the next `write` or `edit` of that file is refused until it is read. The guard adds no strictness option and no file API of its own.
|
|
11
|
+
The agent edits files with Pi's own `read`, `write`, `edit` and `bash` over the workspace's `ExecutionEnv`; there are no document or artifact tools of Boring's. `createFileGuard({ workspace, resolveAccess?, name? })` from `@hachej/boring-agent/file-guard` returns a native extension (named `name`, default `boring.files.guard`: two agents with different workspaces in one registry need two names, or the last installed guard wraps both) that wraps those three tools with the public `wrapTool` (select it after the extension that registers them). Every wrapped call runs inside the workspace provider's mutation queue (`@hachej/boring-files/workspace`), so a viewer save cannot land between `edit`'s internal read and its write. `workspace` is the call's workspace resolved like Pi's environment, or one binding `{ files, root, access? }` (see [Workspace per call](#workspace-per-call)); `resolveAccess` defaults to the binding's `access`. A genuine `read` call records the file's revision as the conversation's last-read revision of that workspace and path (`baselineKey`) in a native conversation document (`lastReadRevisions`, kept across restarts; a fork starts empty); `edit`'s internal read records nothing. `write` and `edit` of a file that exists are refused unless the conversation has read it and its revision still equals the recorded one, and the refusal tells the model to read the file again. Creating a file is allowed only while it is absent. The guard fails closed: an unresolvable workspace, a refused provider read, a path outside the root, a link that leads outside it and a path that does not exist (so Pi's read never falls back to another spelling of it, such as an NFD name) (resolved through the env's `canonicalPath`, so also a new file under a link directory) are refused for `read` as well as `write` and `edit`; nothing falls through to Pi's native tool. After a successful `write` or `edit` the baseline is the revision just written, and the guard calls the provider's `record` (owner ruling 2: observed, never receipted): the file's history gains the written revision with source `agent` (and the bytes it replaced, read before the tool ran when that revision is not already retained), and the workspace's change feed an `agent` event with the conversation. Nothing enters the publication journal. Pi's `edit` tries an exact match first and falls back to a fuzzy one (Unicode normalisation, trailing whitespace, smart quotes, dashes and special spaces); the guard does not change that. A `bash` write is not intercepted, but it changes the revision, so the next `write` or `edit` of that file is refused until it is read; it reaches the change feed (no history) through the file system's watcher or the host's `poll()` at turn end. The guard adds no strictness option and no file API of its own.
|
|
12
12
|
|
|
13
13
|
## Workspace per call
|
|
14
14
|
|
package/dist/file-guard.js
CHANGED
|
@@ -149,11 +149,17 @@ export function createFileGuard(options = {}) {
|
|
|
149
149
|
if (known !== before.revision)
|
|
150
150
|
return refusal(`Refused: ${path} changed since you last read it (the person or another process saved it). Read it again with the read tool, then redo your change on top of what is there now.`);
|
|
151
151
|
}
|
|
152
|
+
// Ruling 2: an agent write is observed (history, change event), never receipted. History keeps the replaced bytes too, so
|
|
153
|
+
// they are read now, before the tool overwrites them, unless that revision is already retained.
|
|
154
|
+
const preimage = before.kind === 'file' && !files.history(path).includes(before.revision) ? await files.read({ target: { resource: { providerId: files.providerId, path }, view: { kind: 'published' } }, revision: { kind: 'exact', value: before.revision } }, access) : undefined;
|
|
152
155
|
const result = await run();
|
|
153
156
|
if (!result.isError) {
|
|
154
157
|
const after = await observe(path);
|
|
155
|
-
if (after.kind === 'file')
|
|
158
|
+
if (after.kind === 'file') {
|
|
156
159
|
await recordRevision(api, key, after.revision, context);
|
|
160
|
+
// Not through `files.queue`: this already runs inside it. A failed history write never turns the done write into an error.
|
|
161
|
+
await files.record({ path, before: preimage?.kind === 'available' ? { revision: preimage.snapshot.ref.revision, bytes: preimage.snapshot.bytes } : null, after: after.revision, source: 'agent', conversationId: String(api.conversationId) }, access).catch(() => undefined);
|
|
162
|
+
}
|
|
157
163
|
}
|
|
158
164
|
return result;
|
|
159
165
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hachej/boring-agent",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
@@ -131,8 +131,8 @@
|
|
|
131
131
|
"@earendil-works/pi-durable": "1.1.0",
|
|
132
132
|
"@earendil-works/pi-ai": "1.1.0",
|
|
133
133
|
"@earendil-works/chord": "1.1.0",
|
|
134
|
-
"@hachej/boring-files": "0.2.
|
|
135
|
-
"@hachej/boring-ui-kit": "0.2.
|
|
134
|
+
"@hachej/boring-files": "0.2.1",
|
|
135
|
+
"@hachej/boring-ui-kit": "0.2.1"
|
|
136
136
|
},
|
|
137
137
|
"peerDependenciesMeta": {
|
|
138
138
|
"@hachej/boring-files": {
|