@sema-agent/core 5.27.0 → 5.29.0
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/CHANGELOG.md +97 -0
- package/dist/agents/cascade.d.ts +1 -1
- package/dist/agents/observer.d.ts +3 -3
- package/dist/agents/peer-admission.d.ts +1 -1
- package/dist/agents/teacher.d.ts +1 -1
- package/dist/agents/verify.d.ts +6 -6
- package/dist/bench/metrics.d.ts +1 -1
- package/dist/brain/retry.d.ts +1 -1
- package/dist/core/arg-summary.d.ts +1 -1
- package/dist/core/ask-class.d.ts +2 -2
- package/dist/core/ask-question.d.ts +1 -1
- package/dist/core/ask-question.js +14 -1
- package/dist/core/background-shell.d.ts +5 -5
- package/dist/core/checkpoint-store.d.ts +14 -14
- package/dist/core/file-snapshot-store.d.ts +1 -1
- package/dist/core/hooks.d.ts +25 -3
- package/dist/core/hooks.js +22 -3
- package/dist/core/human-input-projection.d.ts +2 -2
- package/dist/core/memory-admission.d.ts +2 -2
- package/dist/core/memory-engine/engine.d.ts +1 -1
- package/dist/core/memory-engine/engine.js +2 -4
- package/dist/core/memory-engine/file-backend.d.ts +68 -9
- package/dist/core/memory-engine/file-backend.js +69 -27
- package/dist/core/memory-engine/header-hints.d.ts +1 -1
- package/dist/core/memory-engine/layout.d.ts +32 -3
- package/dist/core/memory-engine/layout.js +132 -8
- package/dist/core/memory-engine/types.d.ts +6 -5
- package/dist/core/memory-recall.d.ts +1 -1
- package/dist/core/memory.d.ts +2 -2
- package/dist/core/oracle-isolation.d.ts +2 -2
- package/dist/core/permission-rule-consent.d.ts +83 -9
- package/dist/core/permission-rule-consent.js +92 -1
- package/dist/core/permission-rule-model.d.ts +18 -2
- package/dist/core/permission-rule-model.js +21 -0
- package/dist/core/permission-rule-org.d.ts +23 -4
- package/dist/core/permission-rule-org.js +67 -20
- package/dist/core/permission-rule-store.d.ts +1 -1
- package/dist/core/permission-rule-store.js +2 -2
- package/dist/core/permission-rule-sync.d.ts +15 -1
- package/dist/core/permission-rule-sync.js +89 -47
- package/dist/core/runner/active-skill-scope.d.ts +1 -1
- package/dist/core/runner/memory-consolidation.d.ts +1 -1
- package/dist/core/runner/prepare-task.d.ts +8 -3
- package/dist/core/runner/prepare-task.js +22 -7
- package/dist/core/runner/runtask.d.ts +1 -1
- package/dist/core/runner/runtask.js +13 -6
- package/dist/core/runner/session-rule-policy.d.ts +2 -2
- package/dist/core/runner/teardown-bounded.d.ts +1 -1
- package/dist/core/runner/tool-disclosure.d.ts +1 -1
- package/dist/core/scheduler.d.ts +4 -4
- package/dist/core/session-policy-store.d.ts +3 -3
- package/dist/core/shared-memory/normalize.d.ts +1 -1
- package/dist/core/skill-tool-specifier.d.ts +1 -1
- package/dist/core/task-notification.d.ts +2 -2
- package/dist/core/task-registry-agent.d.ts +18 -9
- package/dist/core/task-registry-agent.js +51 -21
- package/dist/core/task-registry-monitor.js +1 -1
- package/dist/core/task-registry-shared.d.ts +13 -4
- package/dist/core/tool-errors.d.ts +2 -2
- package/dist/core/tool-policy.d.ts +78 -12
- package/dist/core/tool-policy.js +74 -7
- package/dist/core/tool-result-store.d.ts +109 -8
- package/dist/core/tool-result-store.js +95 -15
- package/dist/core/trace.d.ts +1 -1
- package/dist/core/types.d.ts +92 -22
- package/dist/core/types.js +30 -1
- package/dist/core/untrusted-text.d.ts +6 -0
- package/dist/core/untrusted-text.js +1 -0
- package/dist/engine/compaction/compaction.d.ts +1 -1
- package/dist/engine/harness/messages.d.ts +1 -1
- package/dist/engine/harness/types.d.ts +2 -2
- package/dist/engine/llm/types.d.ts +1 -1
- package/dist/engine/loop/types.d.ts +2 -2
- package/dist/engine/session/import-validate.d.ts +1 -1
- package/dist/engine/session/log-digest.d.ts +1 -1
- package/dist/fixtures/index.d.ts +18 -2
- package/dist/fixtures/index.js +11 -0
- package/dist/index.d.ts +2 -2
- package/dist/orchestration/goal.d.ts +1 -1
- package/dist/orchestration/workflow-types.d.ts +5 -4
- package/dist/orchestration/workflow.js +11 -3
- package/dist/prompt-assembly/artifact.d.ts +1 -1
- package/dist/prompts/default.d.ts +1 -1
- package/dist/prompts/default.js +1 -1
- package/dist/scenarios/scenario-registry.d.ts +3 -3
- package/dist/stores/file/tool-result-store.d.ts +43 -3
- package/dist/stores/file/tool-result-store.js +107 -19
- package/dist/tools/fs/bash-readonly-classifier.d.ts +2 -2
- package/dist/tools/fs/fs-bash.d.ts +8 -1
- package/dist/tools/fs/fs-search-tools.d.ts +1 -1
- package/dist/tools/fs/fs-shared.d.ts +6 -1
- package/dist/tools/fs/fs-shared.js +11 -7
- package/dist/tools/fs/index.d.ts +6 -0
- package/dist/tools/fs/index.js +2 -0
- package/dist/tools/fs/safety.d.ts +1 -1
- package/dist/tools/scheduler-tools.d.ts +1 -1
- package/dist/tools/task-list.d.ts +1 -1
- package/dist/tools/web.d.ts +3 -3
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ToolResultProvenance, ToolResultSlice, ToolResultStore } from "../../core/tool-result-store.js";
|
|
1
|
+
import type { ToolResultDeletionReport, ToolResultProvenance, ToolResultSlice, ToolResultStore } from "../../core/tool-result-store.js";
|
|
2
2
|
/**
|
|
3
3
|
* design/80 §2.5 — file {@link ToolResultStore}: write-once-idempotent, ONE file per ref.
|
|
4
4
|
*
|
|
@@ -16,7 +16,7 @@ import type { ToolResultProvenance, ToolResultSlice, ToolResultStore } from "../
|
|
|
16
16
|
* char slice identical to every other backend's and what keeps files written by older versions readable
|
|
17
17
|
* with no format migration.
|
|
18
18
|
*
|
|
19
|
-
* That is TWO published objects, so the window is stated rather than hidden (
|
|
19
|
+
* That is TWO published objects, so the window is stated rather than hidden (falsification review): the
|
|
20
20
|
* content link is the write-once election and the sidecar follows it, so a crash in between leaves an
|
|
21
21
|
* entry content-complete and momentarily UNOWNED. Publishing the sidecar FIRST would close that window
|
|
22
22
|
* and open a worse one — an ownerless writer's row could then be claimed by whoever raced it — so the
|
|
@@ -45,7 +45,7 @@ export declare class FileToolResultStore implements ToolResultStore {
|
|
|
45
45
|
* already applies to ref safety: a READ (`ownerOf`, an authorization question) degrades an
|
|
46
46
|
* unreadable or malformed sidecar to "unowned" so a corrupt file cannot crash a page-back, while a
|
|
47
47
|
* WRITE refuses to proceed over one it cannot read — silently treating damage as "no owner" is how a
|
|
48
|
-
* write reports success and leaves the entry unclaimable forever (
|
|
48
|
+
* write reports success and leaves the entry unclaimable forever (falsification review round 4).
|
|
49
49
|
* A MISSING sidecar is not damage in either face: it is the honest "unowned".
|
|
50
50
|
*/
|
|
51
51
|
private readOwnerFile;
|
|
@@ -55,4 +55,44 @@ export declare class FileToolResultStore implements ToolResultStore {
|
|
|
55
55
|
offset?: number;
|
|
56
56
|
limit?: number;
|
|
57
57
|
}): ToolResultSlice | undefined;
|
|
58
|
+
/**
|
|
59
|
+
* Erase every entry owned by `sessionId` (interface: {@link ToolResultStore.deleteBySession}). Without
|
|
60
|
+
* it a session deletion leaves this store's files on disk for the lifetime of the data root.
|
|
61
|
+
*
|
|
62
|
+
* Selection matches on the SIDECAR's `sessionId`, never on the filename. That is not a preference, it
|
|
63
|
+
* is the only thing available: the mint separates segments with `~`, which is outside this backend's
|
|
64
|
+
* filename charset, so every engine-minted ref folds to `<base>-<sha256 of the raw ref>` and the
|
|
65
|
+
* session segment is gone from the name (and the readable base is a lossy, many-to-one prefix, so
|
|
66
|
+
* matching on it would reach a neighbouring session's files). `taskId`, where an older write site
|
|
67
|
+
* stamped one, is ignored — it narrows the same session.
|
|
68
|
+
*
|
|
69
|
+
* Both namespaces are swept: identity-named files in the store dir (refs a caller minted inside the
|
|
70
|
+
* filename charset, and everything written before the injective mint) and {@link FOLDED_SUBDIR}.
|
|
71
|
+
*
|
|
72
|
+
* Removal order is CONTENT then SIDECAR, and it is load-bearing. Interrupted after the content unlink,
|
|
73
|
+
* the next sweep still finds the sidecar, matches the same session and converges; interrupted after a
|
|
74
|
+
* sidecar-first unlink, the content file would be left owner-less — readable, attributable to nobody,
|
|
75
|
+
* and beyond the reach of every future session sweep. Same reasoning as `put`'s publication order, in
|
|
76
|
+
* the opposite direction. The pair is then re-examined ONCE after both unlinks, which is what keeps
|
|
77
|
+
* a `put` racing the two from leaving bytes whose owner this sweep removed — and what keeps that
|
|
78
|
+
* re-examination from deleting a row a DIFFERENT session claimed in the same window (see the body).
|
|
79
|
+
*
|
|
80
|
+
* What the re-examination is NOT is atomic: it reads the owner and then acts, so a writer landing
|
|
81
|
+
* inside THAT gap is narrowed, not excluded. Stated rather than locked away, because the locking that
|
|
82
|
+
* would close it is ruled out one layer down — `fs-atomic.ts` §2.4 admits exactly one file lock, the
|
|
83
|
+
* boot lock, on the premise that a data root has a single writing process (`FileStorageBackend`
|
|
84
|
+
* acquires it before constructing this store; cross-process correctness is a database backend's job,
|
|
85
|
+
* as everywhere else in this family). Under that premise there is no gap at all: both `put` and this
|
|
86
|
+
* sweep are wholly synchronous, so nothing can interleave with either. The narrowing above is what
|
|
87
|
+
* remains worth doing for a root someone shares anyway, and it costs one `readFileSync`.
|
|
88
|
+
*
|
|
89
|
+
* Failure posture: an entry that vanished between `readdir` and the unlink is honest absence (ENOENT
|
|
90
|
+
* ok, the concurrent sweeper's row). Any OTHER unlink error throws — a deletion face that cannot
|
|
91
|
+
* delete has to be loud — and because the sweep is idempotent, the retry after the operator clears
|
|
92
|
+
* the cause resumes where it stopped. A sidecar that cannot be READ (EACCES, torn JSON, a shape that
|
|
93
|
+
* is not the whole record) is counted `unattributable` and its content is left alone: a file we could
|
|
94
|
+
* not read carries no evidence about whose it is, and this store's rows stay readable through `get`
|
|
95
|
+
* even with a damaged owner record, so removing one on a guess would destroy another session's output.
|
|
96
|
+
*/
|
|
97
|
+
deleteBySession(sessionId: string): ToolResultDeletionReport;
|
|
58
98
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { readFileSync } from "node:fs";
|
|
2
|
+
import { readFileSync, readdirSync, unlinkSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
import { assertSafeToolResultRef, assertToolResultProvenanceMatch, normalizeToolResultProvenance, } from "../../core/tool-result-store.js";
|
|
4
|
+
import { assertSafeToolResultRef, assertToolResultProvenanceMatch, normalizeToolResultProvenance, ToolResultRefConflictError, } from "../../core/tool-result-store.js";
|
|
5
5
|
import { ensureDir, sanitizePathComponent, writeThenLink } from "./fs-atomic.js";
|
|
6
6
|
import { assertAdoptionBootGate } from "./adoption/marker.js";
|
|
7
7
|
export class FileToolResultStore {
|
|
@@ -21,11 +21,11 @@ export class FileToolResultStore {
|
|
|
21
21
|
}
|
|
22
22
|
pathFor(ref) {
|
|
23
23
|
const { dir, stem } = this.locate(ref);
|
|
24
|
-
return join(dir, `${stem}
|
|
24
|
+
return join(dir, `${stem}${CONTENT_SUFFIX}`);
|
|
25
25
|
}
|
|
26
26
|
ownerPathFor(ref) {
|
|
27
27
|
const { dir, stem } = this.locate(ref);
|
|
28
|
-
return join(dir, `${stem}
|
|
28
|
+
return join(dir, `${stem}${OWNER_SUFFIX}`);
|
|
29
29
|
}
|
|
30
30
|
put(ref, content, provenance) {
|
|
31
31
|
assertSafeToolResultRef(ref);
|
|
@@ -46,7 +46,14 @@ export class FileToolResultStore {
|
|
|
46
46
|
}
|
|
47
47
|
if (provenance === undefined)
|
|
48
48
|
return;
|
|
49
|
-
|
|
49
|
+
try {
|
|
50
|
+
this.writeOwnerFile(ref, provenance);
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
if (err instanceof ToolResultRefConflictError)
|
|
54
|
+
unlinkIfAbsentOk(this.pathFor(ref));
|
|
55
|
+
throw err;
|
|
56
|
+
}
|
|
50
57
|
}
|
|
51
58
|
ownerOf(ref) {
|
|
52
59
|
return this.readOwnerFile(ref, { loud: false });
|
|
@@ -63,20 +70,9 @@ export class FileToolResultStore {
|
|
|
63
70
|
throw err;
|
|
64
71
|
return undefined;
|
|
65
72
|
}
|
|
66
|
-
const parsed = (
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
}
|
|
70
|
-
catch {
|
|
71
|
-
return undefined;
|
|
72
|
-
}
|
|
73
|
-
})();
|
|
74
|
-
if (typeof parsed === "object" && parsed !== null) {
|
|
75
|
-
const { sessionId, taskId } = parsed;
|
|
76
|
-
if (typeof sessionId === "string" && (taskId === undefined || typeof taskId === "string")) {
|
|
77
|
-
return taskId === undefined ? { sessionId } : { sessionId, taskId };
|
|
78
|
-
}
|
|
79
|
-
}
|
|
73
|
+
const parsed = parseOwnerRecord(raw);
|
|
74
|
+
if (parsed !== undefined)
|
|
75
|
+
return parsed;
|
|
80
76
|
if (opts.loud) {
|
|
81
77
|
throw new Error(`tool-result store: owner record for ref ${JSON.stringify(ref)} is present but unreadable — refusing to write over it`);
|
|
82
78
|
}
|
|
@@ -111,6 +107,98 @@ export class FileToolResultStore {
|
|
|
111
107
|
const content = limit != null ? full.slice(offset, offset + limit) : full.slice(offset);
|
|
112
108
|
return { content, offset, totalChars: full.length };
|
|
113
109
|
}
|
|
110
|
+
deleteBySession(sessionId) {
|
|
111
|
+
let deleted = 0;
|
|
112
|
+
let unattributable = 0;
|
|
113
|
+
for (const dir of [this.dir, join(this.dir, FOLDED_SUBDIR)]) {
|
|
114
|
+
let names;
|
|
115
|
+
try {
|
|
116
|
+
names = readdirSync(dir);
|
|
117
|
+
}
|
|
118
|
+
catch (err) {
|
|
119
|
+
if (err.code === "ENOENT")
|
|
120
|
+
continue;
|
|
121
|
+
throw err;
|
|
122
|
+
}
|
|
123
|
+
const listed = new Set(names);
|
|
124
|
+
for (const name of names) {
|
|
125
|
+
if (!name.endsWith(OWNER_SUFFIX))
|
|
126
|
+
continue;
|
|
127
|
+
const owner = readOwnerAt(join(dir, name));
|
|
128
|
+
if (owner === "gone")
|
|
129
|
+
continue;
|
|
130
|
+
if (owner === "damaged") {
|
|
131
|
+
unattributable++;
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (owner.sessionId !== sessionId)
|
|
135
|
+
continue;
|
|
136
|
+
const stem = name.slice(0, -OWNER_SUFFIX.length);
|
|
137
|
+
const sidecarPath = join(dir, name);
|
|
138
|
+
const contentPath = join(dir, `${stem}${CONTENT_SUFFIX}`);
|
|
139
|
+
unlinkIfAbsentOk(contentPath);
|
|
140
|
+
unlinkIfAbsentOk(sidecarPath);
|
|
141
|
+
const republished = readOwnerAt(sidecarPath);
|
|
142
|
+
if (republished === "gone") {
|
|
143
|
+
unlinkIfAbsentOk(contentPath);
|
|
144
|
+
}
|
|
145
|
+
else if (republished === "damaged") {
|
|
146
|
+
unattributable++;
|
|
147
|
+
}
|
|
148
|
+
else if (republished.sessionId === sessionId) {
|
|
149
|
+
unlinkIfAbsentOk(contentPath);
|
|
150
|
+
unlinkIfAbsentOk(sidecarPath);
|
|
151
|
+
}
|
|
152
|
+
deleted++;
|
|
153
|
+
}
|
|
154
|
+
for (const name of names) {
|
|
155
|
+
if (!name.endsWith(CONTENT_SUFFIX))
|
|
156
|
+
continue;
|
|
157
|
+
if (listed.has(`${name.slice(0, -CONTENT_SUFFIX.length)}${OWNER_SUFFIX}`))
|
|
158
|
+
continue;
|
|
159
|
+
unattributable++;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return { deleted, unattributable };
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
const CONTENT_SUFFIX = ".txt";
|
|
166
|
+
const OWNER_SUFFIX = ".owner.json";
|
|
167
|
+
function parseOwnerRecord(raw) {
|
|
168
|
+
const parsed = (() => {
|
|
169
|
+
try {
|
|
170
|
+
return JSON.parse(raw);
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
return undefined;
|
|
174
|
+
}
|
|
175
|
+
})();
|
|
176
|
+
if (typeof parsed === "object" && parsed !== null) {
|
|
177
|
+
const { sessionId, taskId } = parsed;
|
|
178
|
+
if (typeof sessionId === "string" && (taskId === undefined || typeof taskId === "string")) {
|
|
179
|
+
return taskId === undefined ? { sessionId } : { sessionId, taskId };
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return undefined;
|
|
183
|
+
}
|
|
184
|
+
function readOwnerAt(path) {
|
|
185
|
+
let raw;
|
|
186
|
+
try {
|
|
187
|
+
raw = readFileSync(path, "utf8");
|
|
188
|
+
}
|
|
189
|
+
catch (err) {
|
|
190
|
+
return err.code === "ENOENT" ? "gone" : "damaged";
|
|
191
|
+
}
|
|
192
|
+
return parseOwnerRecord(raw) ?? "damaged";
|
|
193
|
+
}
|
|
194
|
+
function unlinkIfAbsentOk(path) {
|
|
195
|
+
try {
|
|
196
|
+
unlinkSync(path);
|
|
197
|
+
}
|
|
198
|
+
catch (err) {
|
|
199
|
+
if (err.code !== "ENOENT")
|
|
200
|
+
throw err;
|
|
201
|
+
}
|
|
114
202
|
}
|
|
115
203
|
const NATIVE_FILENAME_CHARSET = /^[A-Za-z0-9_.-]+$/;
|
|
116
204
|
const FOLDED_SUBDIR = "folded-v2";
|
|
@@ -52,7 +52,7 @@ export interface LeadingCommandNameOptions {
|
|
|
52
52
|
* Returns `{ name }` for a parseable single bare command, or `{ reject }` with a human reason otherwise. It
|
|
53
53
|
* does NOT inspect ARGUMENTS for write flags or consult any allowlist — that is the caller's job (the
|
|
54
54
|
* allowlist for `bash_readonly`, the allow/deny lists for the coarse policy). Keeping ONE parser is the whole
|
|
55
|
-
* point: a second argv[0] parser would drift from this one and silently open a
|
|
55
|
+
* point: a second argv[0] parser would drift from this one and silently open a circumvention.
|
|
56
56
|
*/
|
|
57
57
|
export declare function parseLeadingCommandName(command: string, options?: LeadingCommandNameOptions): {
|
|
58
58
|
name: string;
|
|
@@ -289,7 +289,7 @@ export declare const POLL_LOOP_MAX_SLEEP_SECONDS = 600;
|
|
|
289
289
|
* no-egress envelope — every beat still runs allowlisted readers + literal `sleep`), and the
|
|
290
290
|
* walltime axis cascades a hard kill regardless. A deployment wanting NO command-substitution at
|
|
291
291
|
* all can prefer `{<lo>..<hi>}` or a literal list, which bash expands internally with no command
|
|
292
|
-
* run. (Codex
|
|
292
|
+
* run. (Codex falsification review flagged the substitution as [high] and recommended dropping it;
|
|
293
293
|
* kept per the ruling that names `$(seq <int> <int>)` as the one exemption — recorded for a
|
|
294
294
|
* tightening decision, since the brace form is a drop-in equivalent.)
|
|
295
295
|
* · STDIN-BLOCKING BODY (an availability residual, not a safety one). CLOSED for the shape this note
|
|
@@ -110,6 +110,10 @@ export declare function createBashTool(env: ExecutionEnv, rootCanonical: string,
|
|
|
110
110
|
/** RB-370 ② (cli [2088]): override of the `timeout` parameter's engine CEILING, in ms — only-widen
|
|
111
111
|
* vs the resolved default; see {@link import("./index.js").HandsToolkitOptions.bashMaxTimeoutMs}. */
|
|
112
112
|
bashMaxTimeoutMs?: number;
|
|
113
|
+
/** Structured sink for this leg's timeout-cap discard announcements — consumed by
|
|
114
|
+
* {@link import("./fs-shared.js").resolveBashTimeoutCaps} at mount (see
|
|
115
|
+
* {@link import("./index.js").HandsToolkitOptions.onNotice} for the replace-vs-warn contract). */
|
|
116
|
+
onNotice?: (n: import("../../core/types.js").EngineNotice) => void;
|
|
113
117
|
/**
|
|
114
118
|
* #181-F6 — whether the Monitor tool is on THIS run's roster (the Runner's own mount predicate is
|
|
115
119
|
* `backgroundTaskToolsActive`; it, not this tool, mounts Monitor). Read by the SR-2 gh rate-limit
|
|
@@ -122,7 +126,7 @@ export declare function createBashTool(env: ExecutionEnv, rootCanonical: string,
|
|
|
122
126
|
/**
|
|
123
127
|
* `bash_readonly` (effect:read) — a restricted shell for the verifier read-only boundary (design/44 M2):
|
|
124
128
|
* a single allowlisted, bare command with no shell operators. effect:read lets it survive the verifier's
|
|
125
|
-
* read-only filter, but it is NOT a policy
|
|
129
|
+
* read-only filter, but it is NOT a policy circumvention — it still goes through the design/37 gate (council #7:
|
|
126
130
|
* a read can still be a side channel, e.g. dumping a secret file). The allowlist is a coarse pre-filter;
|
|
127
131
|
* the policy gate is the authoritative control.
|
|
128
132
|
*
|
|
@@ -142,6 +146,9 @@ export declare function createBashReadonlyTool(env: ExecutionEnv, rootCanonical:
|
|
|
142
146
|
* legs share runShell's clamp, so both must share the configurable resolution). */
|
|
143
147
|
bashDefaultTimeoutMs?: number;
|
|
144
148
|
bashMaxTimeoutMs?: number;
|
|
149
|
+
/** Structured sink for the timeout-cap discard announcements — same seat the full `bash` leg
|
|
150
|
+
* takes (both foreground legs share the resolver, so both must share the sink). */
|
|
151
|
+
onNotice?: (n: import("../../core/types.js").EngineNotice) => void;
|
|
145
152
|
/** RB-413: the extra containment roots the structured file tools got (design/119 `--add-dir`,
|
|
146
153
|
* canonical). A read this deployment already sanctions for Read/Grep/Glob is equally sanctioned
|
|
147
154
|
* here — the two faces must not disagree about which directories exist for this session. */
|
|
@@ -6,5 +6,5 @@ export declare function createGlobTool(env: ExecutionEnv, rootCanonical: string,
|
|
|
6
6
|
* wake/resume reconciliation and (b) drive the verifier read-only boundary. Every mutating hand tool is
|
|
7
7
|
* `write` (RB-264 ⑥W1 folded `Write` back in — see below); `bash` is `write` (a command can do anything);
|
|
8
8
|
* `bash_readonly` is `read` (so it survives the verifier boundary — but still goes through the policy
|
|
9
|
-
* gate, council #7: effect:read is a redo-safety class, never a policy
|
|
9
|
+
* gate, council #7: effect:read is a redo-safety class, never a policy circumvention). */
|
|
10
10
|
export declare const HAND_TOOL_EFFECTS: Readonly<Record<string, ToolEffect>>;
|
|
@@ -3,6 +3,7 @@ import type { ExecutionEnv } from "../../internal/harness-types.js";
|
|
|
3
3
|
import { type FsViolation, type ReadFileState } from "./safety.js";
|
|
4
4
|
import { type DecodedTextFile } from "./encoding.js";
|
|
5
5
|
import { type ImageDownsampler } from "../../core/mcp.js";
|
|
6
|
+
import { type EngineNotice } from "../../core/types.js";
|
|
6
7
|
/**
|
|
7
8
|
* The "hand" tool band (design/44 §3) — built per task over an injected {@link ExecutionEnv} and a
|
|
8
9
|
* per-task {@link ReadFileState}, closure-captured (design/44 §11 ruling A: the codebase tool idiom; no
|
|
@@ -223,6 +224,10 @@ export declare function bashTimeoutArgRefusal(timeoutMs: number | undefined, cap
|
|
|
223
224
|
export declare function resolveBashTimeoutCaps(opts?: {
|
|
224
225
|
bashDefaultTimeoutMs?: number;
|
|
225
226
|
bashMaxTimeoutMs?: number;
|
|
227
|
+
/** Structured seat for the discard announcements below — wired, it REPLACES their `console.warn`
|
|
228
|
+
* (same ledger, same lines as `message`); absent, the console line prints verbatim. Threaded from
|
|
229
|
+
* the toolkit assembly (ultimately `RunnerDeps.onNotice`); never a global. */
|
|
230
|
+
onNotice?: (n: EngineNotice) => void;
|
|
226
231
|
}): {
|
|
227
232
|
defaultMs: number;
|
|
228
233
|
maxMs: number;
|
|
@@ -440,7 +445,7 @@ export declare function enoentMessage(env: ExecutionEnv, key: string, cwd: strin
|
|
|
440
445
|
/** Per-task mutable working directory shared by the shell and the path-taking fs tools (design/64 §16.3).
|
|
441
446
|
* Holds the RAW path (never canonicalized): bash `cd` updates `current`, and the fs tools resolve relative
|
|
442
447
|
* paths against it. Containment is still enforced per-op by resolveKey (canonicalize + within), so a `cd`
|
|
443
|
-
* through a symlink out of root cannot
|
|
448
|
+
* through a symlink out of root cannot carry across a relative fs path outside. */
|
|
444
449
|
export interface CwdRef {
|
|
445
450
|
current: string;
|
|
446
451
|
}
|
|
@@ -5,6 +5,7 @@ import { decodeTextBytes, normalizeFileText } from "./encoding.js";
|
|
|
5
5
|
import { shellQuote } from "./search.js";
|
|
6
6
|
import { isNotebookPath } from "./notebook.js";
|
|
7
7
|
import { sharpImageDownsampler } from "../../core/mcp.js";
|
|
8
|
+
import { deliverEngineNotice } from "../../core/types.js";
|
|
8
9
|
export const MAX_READ_BYTES = 256 * 1024;
|
|
9
10
|
export const SLICED_READ_MAX_BYTES = 64 * 1024 * 1024;
|
|
10
11
|
export const MAX_EDIT_BYTES = 1024 * 1024 * 1024;
|
|
@@ -88,7 +89,10 @@ export function envErrorDetail(message) {
|
|
|
88
89
|
return trimmed === "" ? "the execution environment reported no reason" : trimmed;
|
|
89
90
|
}
|
|
90
91
|
const announcedTimeoutConfig = new Set();
|
|
91
|
-
function
|
|
92
|
+
function emitTimeoutDiscardNotice(message, detail, onNotice) {
|
|
93
|
+
deliverEngineNotice(onNotice, { code: "config.env_timeout_discarded", message, detail });
|
|
94
|
+
}
|
|
95
|
+
function announceDiscardedTimeout(knob, raw, usedMs, onNotice) {
|
|
92
96
|
if (typeof raw === "string" && raw === "")
|
|
93
97
|
return;
|
|
94
98
|
const n = typeof raw === "number" ? raw : Number(raw);
|
|
@@ -105,7 +109,7 @@ function announceDiscardedTimeout(knob, raw, usedMs) {
|
|
|
105
109
|
if (announcedTimeoutConfig.has(line))
|
|
106
110
|
return;
|
|
107
111
|
announcedTimeoutConfig.add(line);
|
|
108
|
-
|
|
112
|
+
emitTimeoutDiscardNotice(line, { knob, raw, usedMs }, onNotice);
|
|
109
113
|
}
|
|
110
114
|
export function bashTimeoutParamDescription(caps, withDefault) {
|
|
111
115
|
const bounds = `${withDefault ? `default ${caps.defaultMs}, ` : ""}min ${MIN_BASH_TIMEOUT_MS}, max ${caps.maxMs}`;
|
|
@@ -131,13 +135,13 @@ export function resolveBashTimeoutCaps(opts) {
|
|
|
131
135
|
BASH_DEFAULT_TIMEOUT_MS;
|
|
132
136
|
const maxMs = Math.max(validTimeoutMs(opts?.bashMaxTimeoutMs) ?? validTimeoutMs(Number(process.env.BASH_MAX_TIMEOUT_MS)) ?? BASH_MAX_TIMEOUT_MS, defaultMs);
|
|
133
137
|
if (opts?.bashDefaultTimeoutMs !== undefined)
|
|
134
|
-
announceDiscardedTimeout("bashDefaultTimeoutMs", opts.bashDefaultTimeoutMs, defaultMs);
|
|
138
|
+
announceDiscardedTimeout("bashDefaultTimeoutMs", opts.bashDefaultTimeoutMs, defaultMs, opts?.onNotice);
|
|
135
139
|
if (process.env.BASH_DEFAULT_TIMEOUT_MS !== undefined)
|
|
136
|
-
announceDiscardedTimeout("BASH_DEFAULT_TIMEOUT_MS", process.env.BASH_DEFAULT_TIMEOUT_MS, defaultMs);
|
|
140
|
+
announceDiscardedTimeout("BASH_DEFAULT_TIMEOUT_MS", process.env.BASH_DEFAULT_TIMEOUT_MS, defaultMs, opts?.onNotice);
|
|
137
141
|
if (opts?.bashMaxTimeoutMs !== undefined)
|
|
138
|
-
announceDiscardedTimeout("bashMaxTimeoutMs", opts.bashMaxTimeoutMs, maxMs);
|
|
142
|
+
announceDiscardedTimeout("bashMaxTimeoutMs", opts.bashMaxTimeoutMs, maxMs, opts?.onNotice);
|
|
139
143
|
if (process.env.BASH_MAX_TIMEOUT_MS !== undefined)
|
|
140
|
-
announceDiscardedTimeout("BASH_MAX_TIMEOUT_MS", process.env.BASH_MAX_TIMEOUT_MS, maxMs);
|
|
144
|
+
announceDiscardedTimeout("BASH_MAX_TIMEOUT_MS", process.env.BASH_MAX_TIMEOUT_MS, maxMs, opts?.onNotice);
|
|
141
145
|
const optsCap = validTimeoutMs(opts?.bashMaxTimeoutMs);
|
|
142
146
|
const requestedCap = optsCap ?? validTimeoutMs(Number(process.env.BASH_MAX_TIMEOUT_MS));
|
|
143
147
|
if (requestedCap !== undefined && requestedCap < defaultMs) {
|
|
@@ -145,7 +149,7 @@ export function resolveBashTimeoutCaps(opts) {
|
|
|
145
149
|
const line = `${knob}=${requestedCap} is below the resolved default budget (${defaultMs}ms) — the ceiling was raised to ${maxMs}ms (the default always fits under the cap).`;
|
|
146
150
|
if (!announcedTimeoutConfig.has(line)) {
|
|
147
151
|
announcedTimeoutConfig.add(line);
|
|
148
|
-
|
|
152
|
+
emitTimeoutDiscardNotice(line, { knob, raw: requestedCap, usedMs: maxMs }, opts?.onNotice);
|
|
149
153
|
}
|
|
150
154
|
}
|
|
151
155
|
return { defaultMs, maxMs };
|
package/dist/tools/fs/index.d.ts
CHANGED
|
@@ -74,6 +74,12 @@ export interface HandsToolkitOptions {
|
|
|
74
74
|
* only-widen semantic: a ceiling below the default is a narrowing intent = invalid, ignored in
|
|
75
75
|
* favor of the default. Additive: absent ⇒ byte-compat 600s. */
|
|
76
76
|
bashMaxTimeoutMs?: number;
|
|
77
|
+
/** Structured sink for the timeout-cap discard announcements both shell legs emit at mount (see
|
|
78
|
+
* {@link import("../../core/types.js").EngineNotice} / `RunnerDeps.onNotice`). Wired, it REPLACES
|
|
79
|
+
* their `console.warn` line (a forwarding host must not show every fact twice); absent, the
|
|
80
|
+
* console line prints verbatim (byte-compat loudness). Threaded per toolkit build — never a
|
|
81
|
+
* global. */
|
|
82
|
+
onNotice?: (n: import("../../core/types.js").EngineNotice) => void;
|
|
77
83
|
/** RB-220 — see createBashTool's taskOpts.oneShot / createTaskOutputTool's TaskToolOptions.oneShot for
|
|
78
84
|
* the full contract: this run has no later turn for an async background notification to land in.
|
|
79
85
|
* Threaded to both the Bash background-launch receipt and (when this band mounts TaskOutput itself,
|
package/dist/tools/fs/index.js
CHANGED
|
@@ -41,6 +41,7 @@ export function createHandsToolkit(env, readFileState, rootCanonical, opts = {})
|
|
|
41
41
|
? createBashReadonlyTool(env, rootCanonical, new Set(bashReadonlyAllow ?? BASH_READONLY_DEFAULT_ALLOW), {
|
|
42
42
|
...(opts.bashDefaultTimeoutMs !== undefined ? { bashDefaultTimeoutMs: opts.bashDefaultTimeoutMs } : {}),
|
|
43
43
|
...(opts.bashMaxTimeoutMs !== undefined ? { bashMaxTimeoutMs: opts.bashMaxTimeoutMs } : {}),
|
|
44
|
+
...(opts.onNotice !== undefined ? { onNotice: opts.onNotice } : {}),
|
|
44
45
|
...(readFaceRoots !== undefined ? { additionalRoots: readFaceRoots } : {}),
|
|
45
46
|
})
|
|
46
47
|
: createBashTool(env, rootCanonical, commitCoAuthor, cwdRef, {
|
|
@@ -55,6 +56,7 @@ export function createHandsToolkit(env, readFileState, rootCanonical, opts = {})
|
|
|
55
56
|
...(additionalRoots !== undefined ? { additionalRoots } : {}),
|
|
56
57
|
...(opts.bashDefaultTimeoutMs !== undefined ? { bashDefaultTimeoutMs: opts.bashDefaultTimeoutMs } : {}),
|
|
57
58
|
...(opts.bashMaxTimeoutMs !== undefined ? { bashMaxTimeoutMs: opts.bashMaxTimeoutMs } : {}),
|
|
59
|
+
...(opts.onNotice !== undefined ? { onNotice: opts.onNotice } : {}),
|
|
58
60
|
...(opts.monitorToolActive !== undefined ? { monitorToolActive: opts.monitorToolActive } : {}),
|
|
59
61
|
}));
|
|
60
62
|
if (!readOnly && mountBackgroundTaskTools && hasBackgroundShell(env)) {
|
|
@@ -3,7 +3,7 @@ import type { ExecutionEnv } from "../../internal/harness-types.js";
|
|
|
3
3
|
* batch-B (CC 2.1.187 parity): Read/Edit/Write take the file path as `file_path`. Read the target the same way
|
|
4
4
|
* EVERYWHERE the path is consumed from a tool-call's args — not just the tool body but ALSO every gate that
|
|
5
5
|
* extracts the write target to confine it (sensitive-path, skill allowPaths, session allowDirs). A gate that only
|
|
6
|
-
* read `args.path` would see `undefined` for a `file_path` call and either fail-OPEN (
|
|
6
|
+
* read `args.path` would see `undefined` for a `file_path` call and either fail-OPEN (circumvent the guard — a real
|
|
7
7
|
* hole) or fail-closed (deny a legitimate write). Single source.
|
|
8
8
|
*
|
|
9
9
|
* The `path` arm is deliberately BROADER than what Read/Edit/Write accept (ruled 2026-08-03): their schema requires
|
|
@@ -27,7 +27,7 @@ export declare const AUTONOMOUS_LOOP_DYNAMIC_SENTINEL = "<<autonomous-loop-dynam
|
|
|
27
27
|
/** Runner-held 上下文 + run-stop 出口,工具工厂闭包捕获(模型触不到)。 */
|
|
28
28
|
export interface SchedulerToolContext {
|
|
29
29
|
/**
|
|
30
|
-
*
|
|
30
|
+
* **会话腿**的越界隔离键(Runner 填 sessionId ?? principal ?? taskId)。durable 腿不用这条链——它的键由
|
|
31
31
|
* {@link durableScopeOf} 从 principal 独立铸(ruled 2026-08-03,见该函数注释)。
|
|
32
32
|
*/
|
|
33
33
|
scope: string;
|
|
@@ -74,7 +74,7 @@ export interface TaskListStore {
|
|
|
74
74
|
*
|
|
75
75
|
* The in-memory store rejected such a row incidentally: its snapshot helper spreads `t.blocks`, so a
|
|
76
76
|
* missing array threw "blocks is not iterable" at write. The file store had no such accident and wrote the
|
|
77
|
-
* row to disk, where it survived until `TaskUpdate` reached `t.blocks.includes(...)` — a
|
|
77
|
+
* row to disk, where it survived until `TaskUpdate` reached `t.blocks.includes(...)` — a contaminated row that
|
|
78
78
|
* passes the write and detonates at an unrelated later call, in a different session, with no trace of where
|
|
79
79
|
* it came from. Two backends disagreeing about what a valid row is means a test written against one has
|
|
80
80
|
* different semantics on the other.
|
package/dist/tools/web.d.ts
CHANGED
|
@@ -21,7 +21,7 @@ export interface WebFetchConfig {
|
|
|
21
21
|
*
|
|
22
22
|
* Pre-2.13 review F1 (additive): the object return form lets a summarizer report that its own OUTPUT
|
|
23
23
|
* was cut (`truncated: true` — e.g. the sub-model hit max_tokens) as structured fact. The tool then
|
|
24
|
-
* renders the disclosure OUTSIDE the untrusted fence (a page must not be able to
|
|
24
|
+
* renders the disclosure OUTSIDE the untrusted fence (a page must not be able to self-declare it) and stamps
|
|
25
25
|
* `details.truncated`. A plain-string return still works and claims a complete summary.
|
|
26
26
|
*
|
|
27
27
|
* ruled 2026-08-05 (grounding contract): the object form additionally carries `inputTruncated` — the
|
|
@@ -126,7 +126,7 @@ export declare const WEBFETCH_SUMMARY_INPUT_HEADROOM = 0.8;
|
|
|
126
126
|
* reservation still gets a small excerpt rather than a computation that lands at or below zero and feeds
|
|
127
127
|
* the summarizer an empty page — a call that cannot possibly answer is strictly worse than one that might.
|
|
128
128
|
*
|
|
129
|
-
* Honest scope (
|
|
129
|
+
* Honest scope (falsification review round 1): this is a FLOOR, not a proof of fit. With the output
|
|
130
130
|
* reservation bounded at window/4 (see `resolveSummaryInputChars`), the floor now fires only on genuinely
|
|
131
131
|
* tiny windows (roughly under 3k tokens at the default chars-per-token) — a model whose declared
|
|
132
132
|
* dimensions leave no room gets a small excerpt whose attempt often succeeds anyway, and a failure is
|
|
@@ -139,7 +139,7 @@ export declare const WEBFETCH_SUMMARY_MIN_CONTENT = 4000;
|
|
|
139
139
|
* Resolve the page-content budget for {@link createWebFetchSummarizer} against `model`'s window.
|
|
140
140
|
* An explicit `override` wins outright (a deployment that measured its own model beats an estimate).
|
|
141
141
|
*
|
|
142
|
-
* Fail-loud on every non-finite input, same posture as {@link resolveWebMaxBytes} (
|
|
142
|
+
* Fail-loud on every non-finite input, same posture as {@link resolveWebMaxBytes} (falsification review
|
|
143
143
|
* round 1): a `NaN` anywhere in the model's declared dimensions propagates through the arithmetic and out
|
|
144
144
|
* through `Math.min`/`Math.max` unchanged, and a `NaN` budget makes `content.length > budget` false — so
|
|
145
145
|
* the clip silently stops happening and the whole oversize page is fed to the model, which is the failure
|