@sema-agent/core 5.17.0 → 5.18.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/CHANGELOG.md +121 -0
- package/dist/agents/subagent.js +24 -0
- package/dist/core/auto-compaction.d.ts +6 -0
- package/dist/core/auto-compaction.js +15 -1
- package/dist/core/checkpoint-store.d.ts +4 -2
- package/dist/core/governance-codes.js +1 -0
- package/dist/core/hooks.d.ts +9 -0
- package/dist/core/hooks.js +21 -0
- package/dist/core/mcp.js +3 -0
- package/dist/core/memory-engine/content-origin.d.ts +27 -0
- package/dist/core/memory-engine/content-origin.js +38 -0
- package/dist/core/memory-engine/engine.d.ts +12 -2
- package/dist/core/memory-engine/engine.js +172 -12
- package/dist/core/memory-engine/file-backend.d.ts +4 -0
- package/dist/core/memory-engine/file-backend.js +25 -3
- package/dist/core/memory-engine/index.d.ts +2 -1
- package/dist/core/memory-engine/index.js +2 -1
- package/dist/core/memory-engine/layout.d.ts +16 -0
- package/dist/core/memory-engine/layout.js +90 -2
- package/dist/core/memory-engine/sync-client.d.ts +1 -0
- package/dist/core/memory-engine/sync-client.js +23 -5
- package/dist/core/memory-engine/tools.d.ts +55 -0
- package/dist/core/memory-engine/tools.js +307 -0
- package/dist/core/memory-engine/types.d.ts +1 -1
- package/dist/core/memory.d.ts +4 -0
- package/dist/core/memory.js +15 -2
- package/dist/core/permission-rule-consent.d.ts +138 -0
- package/dist/core/permission-rule-consent.js +318 -0
- package/dist/core/permission-rule-model.d.ts +66 -0
- package/dist/core/permission-rule-model.js +135 -0
- package/dist/core/permission-rule-store.d.ts +89 -0
- package/dist/core/permission-rule-store.js +145 -0
- package/dist/core/permission-rules.d.ts +3 -2
- package/dist/core/permission-rules.js +9 -4
- package/dist/core/runner/prepare-memory.d.ts +3 -1
- package/dist/core/runner/prepare-memory.js +54 -14
- package/dist/core/runner/prepare-task.d.ts +12 -0
- package/dist/core/runner/prepare-task.js +206 -12
- package/dist/core/runner/runtask.d.ts +3 -1
- package/dist/core/runner/runtask.js +47 -5
- package/dist/core/runner/tool-output-projection.js +1 -1
- package/dist/core/tool-policy.d.ts +13 -1
- package/dist/core/tool-policy.js +93 -12
- package/dist/core/tools.js +1 -0
- package/dist/core/trace.d.ts +20 -0
- package/dist/core/types.d.ts +15 -0
- package/dist/core/wiring-manifest.d.ts +5 -1
- package/dist/core/wiring-manifest.js +2 -0
- package/dist/index.d.ts +7 -3
- package/dist/index.js +6 -2
- package/dist/stores/file/permission-rule-store.d.ts +32 -0
- package/dist/stores/file/permission-rule-store.js +213 -0
- package/dist/tools/fs/fs-bash.js +12 -5
- package/dist/tools/fs/fs-shared.d.ts +12 -0
- package/dist/tools/fs/fs-shared.js +65 -1
- package/dist/tools/web.js +2 -0
- package/package.json +1 -1
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { closeSync, constants as FS, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
4
|
+
import { PERMISSION_RULE_WRITER, applyTombstones, assertDeleteDeltaCarriesNoAdd, foldDelta } from "../../core/permission-rule-store.js";
|
|
5
|
+
import { canonicalize } from "../../core/canonical-json.js";
|
|
6
|
+
import { BootLock } from "./fs-atomic.js";
|
|
7
|
+
const EMPTY_READ = { rules: [], tombstones: [], rev: 0 };
|
|
8
|
+
function checksumOf(body) {
|
|
9
|
+
return `sha256:${createHash("sha256").update(canonicalize(body), "utf8").digest("hex")}`;
|
|
10
|
+
}
|
|
11
|
+
function assertSafeDir(dir) {
|
|
12
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
13
|
+
const st = lstatSync(dir);
|
|
14
|
+
if (st.isSymbolicLink() || !st.isDirectory()) {
|
|
15
|
+
throw new Error(`permission-rule store directory ${dir} is a symlink or not a directory; refusing to use it`);
|
|
16
|
+
}
|
|
17
|
+
if (typeof process.getuid === "function" && st.uid !== process.getuid()) {
|
|
18
|
+
throw new Error(`permission-rule store directory ${dir} is owned by another user; refusing to use it`);
|
|
19
|
+
}
|
|
20
|
+
if ((st.mode & 0o022) !== 0) {
|
|
21
|
+
throw new Error(`permission-rule store directory ${dir} is writable by group or others (mode ${(st.mode & 0o777).toString(8)}); ` +
|
|
22
|
+
`refusing to use it — anyone who can write here can grant themselves permissions`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
class FilePermissionRuleStore {
|
|
26
|
+
dir;
|
|
27
|
+
file;
|
|
28
|
+
acquireWriteLock;
|
|
29
|
+
onError;
|
|
30
|
+
durability = "durable";
|
|
31
|
+
fidelity = "json";
|
|
32
|
+
constructor(dir, file, acquireWriteLock, onError) {
|
|
33
|
+
this.dir = dir;
|
|
34
|
+
this.file = file;
|
|
35
|
+
this.acquireWriteLock = acquireWriteLock;
|
|
36
|
+
this.onError = onError;
|
|
37
|
+
}
|
|
38
|
+
read() {
|
|
39
|
+
let raw;
|
|
40
|
+
try {
|
|
41
|
+
assertSafeDir(this.dir);
|
|
42
|
+
const st = lstatSync(this.file);
|
|
43
|
+
if (st.isSymbolicLink() || !st.isFile()) {
|
|
44
|
+
const why = `${this.file} is a symlink or not a regular file; refusing to load any rule from it`;
|
|
45
|
+
this.disclose(why);
|
|
46
|
+
return { unreadable: why };
|
|
47
|
+
}
|
|
48
|
+
raw = readFileSync(this.file, "utf8");
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
const e = err;
|
|
52
|
+
if (e?.code === "ENOENT")
|
|
53
|
+
return { absent: true };
|
|
54
|
+
const why = `could not read ${this.file}: ${e?.message ?? String(err)}; loading zero rules`;
|
|
55
|
+
this.disclose(why);
|
|
56
|
+
return { unreadable: why };
|
|
57
|
+
}
|
|
58
|
+
let parsed;
|
|
59
|
+
try {
|
|
60
|
+
parsed = JSON.parse(raw);
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
const why = `${this.file} is not valid JSON (${err.message}); refusing the whole file and loading zero rules`;
|
|
64
|
+
this.disclose(why);
|
|
65
|
+
return { unreadable: why };
|
|
66
|
+
}
|
|
67
|
+
if (parsed?.schemaVersion !== 1 || !Array.isArray(parsed.rules) || !Array.isArray(parsed.tombstones) || typeof parsed.rev !== "number") {
|
|
68
|
+
const why = `${this.file} does not carry a readable rule-file shape; refusing the whole file and loading zero rules`;
|
|
69
|
+
this.disclose(why);
|
|
70
|
+
return { unreadable: why };
|
|
71
|
+
}
|
|
72
|
+
const { checksum, ...body } = parsed;
|
|
73
|
+
if (checksum !== checksumOf(body)) {
|
|
74
|
+
const why = `${this.file} failed its integrity check; refusing the whole file and loading zero rules`;
|
|
75
|
+
this.disclose(why);
|
|
76
|
+
return { unreadable: why };
|
|
77
|
+
}
|
|
78
|
+
return { file: parsed };
|
|
79
|
+
}
|
|
80
|
+
disclose(message) {
|
|
81
|
+
try {
|
|
82
|
+
this.onError?.(message);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
write(next) {
|
|
88
|
+
assertSafeDir(this.dir);
|
|
89
|
+
const payload = JSON.stringify({ ...next, checksum: checksumOf(next) }, null, 2);
|
|
90
|
+
const tmp = join(this.dir, `.${randomBytes(8).toString("hex")}.tmp`);
|
|
91
|
+
let fd;
|
|
92
|
+
try {
|
|
93
|
+
fd = openSync(tmp, FS.O_WRONLY | FS.O_CREAT | FS.O_EXCL | (FS.O_NOFOLLOW ?? 0), 0o600);
|
|
94
|
+
writeFileSync(fd, payload);
|
|
95
|
+
fsyncSync(fd);
|
|
96
|
+
closeSync(fd);
|
|
97
|
+
fd = undefined;
|
|
98
|
+
renameSync(tmp, this.file);
|
|
99
|
+
let dfd;
|
|
100
|
+
try {
|
|
101
|
+
dfd = openSync(this.dir, "r");
|
|
102
|
+
fsyncSync(dfd);
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
}
|
|
106
|
+
finally {
|
|
107
|
+
if (dfd !== undefined)
|
|
108
|
+
closeSync(dfd);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
catch (err) {
|
|
112
|
+
if (fd !== undefined) {
|
|
113
|
+
try {
|
|
114
|
+
closeSync(fd);
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
unlinkSync(tmp);
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
}
|
|
124
|
+
throw err;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
async list() {
|
|
128
|
+
const r = this.read();
|
|
129
|
+
if (!("file" in r))
|
|
130
|
+
return { ...EMPTY_READ };
|
|
131
|
+
return {
|
|
132
|
+
rules: applyTombstones(r.file.rules, r.file.tombstones),
|
|
133
|
+
tombstones: r.file.tombstones,
|
|
134
|
+
rev: r.file.rev,
|
|
135
|
+
checksum: r.file.checksum,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
current() {
|
|
139
|
+
const r = this.read();
|
|
140
|
+
if ("unreadable" in r) {
|
|
141
|
+
throw new Error(`refusing to write the permission-rule store while it cannot be read: ${r.unreadable}`);
|
|
142
|
+
}
|
|
143
|
+
return "absent" in r
|
|
144
|
+
? { schemaVersion: 1, actor: `file-${randomBytes(6).toString("hex")}`, counter: 0, rev: 0, rules: [], tombstones: [] }
|
|
145
|
+
: { schemaVersion: 1, actor: r.file.actor, counter: r.file.counter, rev: r.file.rev, rules: r.file.rules, tombstones: r.file.tombstones };
|
|
146
|
+
}
|
|
147
|
+
writeAndVerify(next) {
|
|
148
|
+
const expected = checksumOf(next);
|
|
149
|
+
this.write(next);
|
|
150
|
+
const after = this.read();
|
|
151
|
+
return "file" in after && after.file.checksum === expected;
|
|
152
|
+
}
|
|
153
|
+
writeChain = Promise.resolve();
|
|
154
|
+
serialize(fn) {
|
|
155
|
+
const run = this.writeChain.then(fn, fn);
|
|
156
|
+
this.writeChain = run.then(() => undefined, () => undefined);
|
|
157
|
+
return run;
|
|
158
|
+
}
|
|
159
|
+
get [PERMISSION_RULE_WRITER]() {
|
|
160
|
+
this.acquireWriteLock();
|
|
161
|
+
return this.writer;
|
|
162
|
+
}
|
|
163
|
+
writer = {
|
|
164
|
+
nextDot: async () => this.serialize(() => {
|
|
165
|
+
const cur = this.current();
|
|
166
|
+
const dot = { actor: cur.actor, counter: cur.counter + 1 };
|
|
167
|
+
if (!this.writeAndVerify({ ...cur, counter: dot.counter })) {
|
|
168
|
+
throw new Error("the permission-rule store did not survive its own write; refusing to hand out an identity");
|
|
169
|
+
}
|
|
170
|
+
return dot;
|
|
171
|
+
}),
|
|
172
|
+
apply: async (delta, opts) => this.serialize(() => {
|
|
173
|
+
const cur = this.current();
|
|
174
|
+
if (cur.rev !== opts.expectedRev)
|
|
175
|
+
return { conflict: true, rev: cur.rev };
|
|
176
|
+
const next = delta.kind === "redemption-add"
|
|
177
|
+
? { ...cur, rev: cur.rev + 1, rules: foldDelta(cur.rules, delta) }
|
|
178
|
+
: (assertDeleteDeltaCarriesNoAdd(delta), { ...cur, rev: cur.rev + 1, tombstones: [...cur.tombstones, delta.tombstone] });
|
|
179
|
+
if (!this.writeAndVerify(next)) {
|
|
180
|
+
throw new Error("the permission-rule store did not survive its own write; the change was not committed");
|
|
181
|
+
}
|
|
182
|
+
return { rev: cur.rev + 1 };
|
|
183
|
+
}),
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
export class FilePermissionRuleStoreProvider {
|
|
187
|
+
dir;
|
|
188
|
+
onError;
|
|
189
|
+
lock;
|
|
190
|
+
constructor(dir, onError) {
|
|
191
|
+
this.dir = dir;
|
|
192
|
+
this.onError = onError;
|
|
193
|
+
}
|
|
194
|
+
acquireWriteLock = () => {
|
|
195
|
+
if (this.lock !== undefined)
|
|
196
|
+
return;
|
|
197
|
+
assertSafeDir(this.dir);
|
|
198
|
+
const lock = new BootLock(join(this.dir, "PERMISSION-RULES-WRITER-LOCK"));
|
|
199
|
+
lock.acquire();
|
|
200
|
+
this.lock = lock;
|
|
201
|
+
};
|
|
202
|
+
forPrincipal(principal) {
|
|
203
|
+
if (typeof principal !== "string" || principal === "") {
|
|
204
|
+
return { list: async () => ({ ...EMPTY_READ }), durability: "process-local" };
|
|
205
|
+
}
|
|
206
|
+
const name = `${createHash("sha256").update(principal, "utf8").digest("hex")}.json`;
|
|
207
|
+
return new FilePermissionRuleStore(this.dir, join(this.dir, name), this.acquireWriteLock, this.onError);
|
|
208
|
+
}
|
|
209
|
+
dispose() {
|
|
210
|
+
this.lock?.release();
|
|
211
|
+
this.lock = undefined;
|
|
212
|
+
}
|
|
213
|
+
}
|
package/dist/tools/fs/fs-bash.js
CHANGED
|
@@ -9,7 +9,7 @@ import { MCP_IMAGE_MAX_BASE64 } from "../../core/mcp.js";
|
|
|
9
9
|
import { imageMagicMatches, withinAnyRoot } from "./safety.js";
|
|
10
10
|
import { isRemoteExecutionEnv, hasDestroy, isIsolated } from "../../core/remote-env.js";
|
|
11
11
|
import { ghRateLimitHint } from "./gh-rate-limit.js";
|
|
12
|
-
import { resolveBashTimeoutCaps, bashTimeoutCapsSec, bashMaxOutputChars, clipShellOutput, writeShellOverflowFile, createShellOverflowSpoolFence, shellRecoveryHint, CWD_SENTINEL, BASH_READONLY_CONFINEMENT_NOTE, } from "./fs-shared.js";
|
|
12
|
+
import { resolveBashTimeoutCaps, bashTimeoutCapsSec, bashTimeoutArgRefusal, bashTimeoutParamDescription, envErrorDetail, bashMaxOutputChars, clipShellOutput, writeShellOverflowFile, createShellOverflowSpoolFence, shellRecoveryHint, CWD_SENTINEL, BASH_READONLY_CONFINEMENT_NOTE, } from "./fs-shared.js";
|
|
13
13
|
import { BASH_READONLY_DEFAULT_ALLOW, coarseReadonlyCheck, classifyBoundedReadonlyPollLoop, classifyCompoundReadonly, classifySimpleCommandReadBoundary, NOT_AUTO_ALLOWED, } from "./bash-readonly-classifier.js";
|
|
14
14
|
export function bashReversibilityProbe(allow, boundary) {
|
|
15
15
|
const allowSet = new Set(allow ?? BASH_READONLY_DEFAULT_ALLOW);
|
|
@@ -498,7 +498,7 @@ export function createBashTool(env, rootCanonical, coAuthor = false, cwdRef = {
|
|
|
498
498
|
description: bashDescription(coAuthor, timeoutCaps, bgNotifies, bgRetained, bgSessionScoped, bgEnvIsolatedOwned),
|
|
499
499
|
parameters: Type.Object({
|
|
500
500
|
command: Type.String({ description: "The command to execute" }),
|
|
501
|
-
timeout: Type.Optional(Type.Number({ description:
|
|
501
|
+
timeout: Type.Optional(Type.Number({ description: bashTimeoutParamDescription(timeoutCaps, false) })),
|
|
502
502
|
description: Type.Optional(Type.String({
|
|
503
503
|
description: 'Clear, concise description of what this command does in active voice. Never use words like "complex" or "risk" in the description - just describe what it does.\n' +
|
|
504
504
|
"\n" +
|
|
@@ -517,6 +517,7 @@ export function createBashTool(env, rootCanonical, coAuthor = false, cwdRef = {
|
|
|
517
517
|
})),
|
|
518
518
|
}),
|
|
519
519
|
effect: "write",
|
|
520
|
+
contentOrigin: "execution",
|
|
520
521
|
isConcurrencySafe: (args) => {
|
|
521
522
|
const cmd = args?.command;
|
|
522
523
|
if (typeof cmd !== "string")
|
|
@@ -528,6 +529,9 @@ export function createBashTool(env, rootCanonical, coAuthor = false, cwdRef = {
|
|
|
528
529
|
},
|
|
529
530
|
execute: async (args, ctx) => {
|
|
530
531
|
const { command, timeout, run_in_background, description } = args;
|
|
532
|
+
const timeoutRefusal = bashTimeoutArgRefusal(timeout, timeoutCaps);
|
|
533
|
+
if (timeoutRefusal !== undefined)
|
|
534
|
+
return errorResult(timeoutRefusal, { type: "bash_invalid_timeout", code: "bash_invalid_timeout" });
|
|
531
535
|
if (run_in_background) {
|
|
532
536
|
if (!hasBackgroundShell(env)) {
|
|
533
537
|
return errorResult("Error (Bash): this environment does not support background processes (run_in_background).");
|
|
@@ -543,7 +547,7 @@ export function createBashTool(env, rootCanonical, coAuthor = false, cwdRef = {
|
|
|
543
547
|
...(requestedTimeoutSec !== undefined ? { timeout: requestedTimeoutSec } : {}),
|
|
544
548
|
});
|
|
545
549
|
if (!r.ok)
|
|
546
|
-
return errorResult(`Error (Bash): ${r.error.message}`);
|
|
550
|
+
return errorResult(`Error (Bash): the background process could not be started (${r.error.code}): ${envErrorDetail(r.error.message)}`);
|
|
547
551
|
const bgCaps = env.backgroundCapabilities;
|
|
548
552
|
const appliedTimeoutSec = typeof bgCaps.defaultBgTimeoutSec === "number" && typeof bgCaps.maxBgTimeoutSec === "number"
|
|
549
553
|
? Math.min(requestedTimeoutSec ?? bgCaps.defaultBgTimeoutSec, bgCaps.maxBgTimeoutSec)
|
|
@@ -953,11 +957,14 @@ export function createBashReadonlyTool(env, rootCanonical, allow, opts) {
|
|
|
953
957
|
"Still subject to the deployment's approval policy.",
|
|
954
958
|
parameters: Type.Object({
|
|
955
959
|
command: Type.String({ description: "A single allowlisted read-only command (no shell operators)." }),
|
|
956
|
-
timeout: Type.Optional(Type.Number({ description:
|
|
960
|
+
timeout: Type.Optional(Type.Number({ description: bashTimeoutParamDescription(timeoutCaps, true) })),
|
|
957
961
|
}),
|
|
958
962
|
effect: "read",
|
|
959
963
|
execute: async (args, ctx) => {
|
|
960
964
|
const { command, timeout } = args;
|
|
965
|
+
const timeoutRefusal = bashTimeoutArgRefusal(timeout, timeoutCaps);
|
|
966
|
+
if (timeoutRefusal !== undefined)
|
|
967
|
+
return errorResult(timeoutRefusal, { type: "bash_invalid_timeout", code: "bash_invalid_timeout" });
|
|
961
968
|
const reason = coarseReadonlyCheck(command, allow);
|
|
962
969
|
if (reason)
|
|
963
970
|
return errorResult(`Error (Bash): ${reason}`);
|
|
@@ -1008,7 +1015,7 @@ export function createEnvTaskOutputTool(env) {
|
|
|
1008
1015
|
}
|
|
1009
1016
|
const r = await env.pollBackground(id);
|
|
1010
1017
|
if (!r.ok)
|
|
1011
|
-
return errorResult(`Error (TaskOutput): ${r.error.message}`);
|
|
1018
|
+
return errorResult(`Error (TaskOutput): could not read ${id} (${r.error.code}): ${envErrorDetail(r.error.message)}`);
|
|
1012
1019
|
const shape = (text) => clipShellOutput(rx ? text.split(/\r?\n/).filter((l) => rx.test(l)).join("\n") : text);
|
|
1013
1020
|
const status = r.value.status === "exited" ? `exited(code ${r.value.exitCode})` : r.value.status;
|
|
1014
1021
|
const dropAcct = {};
|
|
@@ -30,6 +30,17 @@ export declare const BASH_DEFAULT_TIMEOUT_SEC = 120;
|
|
|
30
30
|
export declare const BASH_MAX_TIMEOUT_SEC = 600;
|
|
31
31
|
export declare const BASH_DEFAULT_TIMEOUT_MS: number;
|
|
32
32
|
export declare const BASH_MAX_TIMEOUT_MS: number;
|
|
33
|
+
export declare const MIN_BASH_TIMEOUT_MS = 1000;
|
|
34
|
+
export declare const BASH_TIMEOUT_PLATEAU_END_MS = 1500;
|
|
35
|
+
export declare function envErrorDetail(message: string | undefined): string;
|
|
36
|
+
export declare function bashTimeoutParamDescription(caps: {
|
|
37
|
+
defaultMs: number;
|
|
38
|
+
maxMs: number;
|
|
39
|
+
}, withDefault: boolean): string;
|
|
40
|
+
export declare function bashTimeoutArgRefusal(timeoutMs: number | undefined, caps: {
|
|
41
|
+
defaultMs: number;
|
|
42
|
+
maxMs: number;
|
|
43
|
+
}): string | undefined;
|
|
33
44
|
export declare function resolveBashTimeoutCaps(opts?: {
|
|
34
45
|
bashDefaultTimeoutMs?: number;
|
|
35
46
|
bashMaxTimeoutMs?: number;
|
|
@@ -37,6 +48,7 @@ export declare function resolveBashTimeoutCaps(opts?: {
|
|
|
37
48
|
defaultMs: number;
|
|
38
49
|
maxMs: number;
|
|
39
50
|
};
|
|
51
|
+
export declare function __resetBashTimeoutAnnouncements(): void;
|
|
40
52
|
export declare function bashTimeoutCapsSec(caps: {
|
|
41
53
|
defaultMs: number;
|
|
42
54
|
maxMs: number;
|
|
@@ -59,19 +59,83 @@ export const BASH_DEFAULT_TIMEOUT_SEC = 120;
|
|
|
59
59
|
export const BASH_MAX_TIMEOUT_SEC = 600;
|
|
60
60
|
export const BASH_DEFAULT_TIMEOUT_MS = BASH_DEFAULT_TIMEOUT_SEC * 1000;
|
|
61
61
|
export const BASH_MAX_TIMEOUT_MS = BASH_MAX_TIMEOUT_SEC * 1000;
|
|
62
|
+
export const MIN_BASH_TIMEOUT_MS = 1000;
|
|
63
|
+
export const BASH_TIMEOUT_PLATEAU_END_MS = 1500;
|
|
64
|
+
function bashTimeoutDefect(ms) {
|
|
65
|
+
if (!Number.isFinite(ms))
|
|
66
|
+
return "must be a finite number of milliseconds";
|
|
67
|
+
if (ms <= 0)
|
|
68
|
+
return `must be a positive number of milliseconds (received ${ms})`;
|
|
69
|
+
if (ms < MIN_BASH_TIMEOUT_MS) {
|
|
70
|
+
return (`is in MILLISECONDS and ${ms} is under one second — if ${ms} seconds was meant, the value is ` +
|
|
71
|
+
`${ms * 1000}. Values below ${MIN_BASH_TIMEOUT_MS}ms all collapse to the same one-second budget`);
|
|
72
|
+
}
|
|
73
|
+
if (ms > MIN_BASH_TIMEOUT_MS && ms < BASH_TIMEOUT_PLATEAU_END_MS) {
|
|
74
|
+
return (`is ${ms}ms, which rounds down to the same one-second budget as ${MIN_BASH_TIMEOUT_MS}ms — the ` +
|
|
75
|
+
`shell's granularity is whole seconds, so write ${MIN_BASH_TIMEOUT_MS} for one second or at least ` +
|
|
76
|
+
`${BASH_TIMEOUT_PLATEAU_END_MS} for two`);
|
|
77
|
+
}
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
62
80
|
function validTimeoutMs(n) {
|
|
63
81
|
if (n === undefined || !Number.isFinite(n))
|
|
64
82
|
return undefined;
|
|
65
83
|
const floored = Math.floor(n);
|
|
66
|
-
return floored
|
|
84
|
+
return bashTimeoutDefect(floored) === undefined ? floored : undefined;
|
|
85
|
+
}
|
|
86
|
+
export function envErrorDetail(message) {
|
|
87
|
+
const trimmed = (message ?? "").trim();
|
|
88
|
+
return trimmed === "" ? "the execution environment reported no reason" : trimmed;
|
|
89
|
+
}
|
|
90
|
+
const announcedTimeoutConfig = new Set();
|
|
91
|
+
function announceDiscardedTimeout(knob, raw, usedMs) {
|
|
92
|
+
if (!Number.isFinite(raw) || raw <= 0)
|
|
93
|
+
return;
|
|
94
|
+
const defect = bashTimeoutDefect(Math.floor(raw));
|
|
95
|
+
if (defect === undefined)
|
|
96
|
+
return;
|
|
97
|
+
const line = `${knob}=${raw} was ignored — it ${defect}. Using ${usedMs}ms instead.`;
|
|
98
|
+
if (announcedTimeoutConfig.has(line))
|
|
99
|
+
return;
|
|
100
|
+
announcedTimeoutConfig.add(line);
|
|
101
|
+
console.warn(line);
|
|
102
|
+
}
|
|
103
|
+
export function bashTimeoutParamDescription(caps, withDefault) {
|
|
104
|
+
const bounds = `${withDefault ? `default ${caps.defaultMs}, ` : ""}min ${MIN_BASH_TIMEOUT_MS}, max ${caps.maxMs}`;
|
|
105
|
+
const escape = caps.maxMs >= BASH_TIMEOUT_PLATEAU_END_MS
|
|
106
|
+
? ` — pass exactly ${MIN_BASH_TIMEOUT_MS} for one second, or ${BASH_TIMEOUT_PLATEAU_END_MS}+ for longer`
|
|
107
|
+
: ` — ${MIN_BASH_TIMEOUT_MS} is the only value this deployment accepts below its ${caps.maxMs}ms ceiling`;
|
|
108
|
+
return (`Timeout in milliseconds (${bounds}). The shell's granularity is whole seconds, so ${MIN_BASH_TIMEOUT_MS} is the ` +
|
|
109
|
+
`smallest accepted value and anything between ${MIN_BASH_TIMEOUT_MS} and ${BASH_TIMEOUT_PLATEAU_END_MS} is REJECTED ` +
|
|
110
|
+
`(it would round back down to ${MIN_BASH_TIMEOUT_MS})${escape}. Requests above the max are capped to it.`);
|
|
111
|
+
}
|
|
112
|
+
export function bashTimeoutArgRefusal(timeoutMs, caps) {
|
|
113
|
+
if (timeoutMs === undefined)
|
|
114
|
+
return undefined;
|
|
115
|
+
const defect = bashTimeoutDefect(timeoutMs);
|
|
116
|
+
if (defect === undefined)
|
|
117
|
+
return undefined;
|
|
118
|
+
return (`Error (Bash): \`timeout\` ${defect}. The command was NOT run. Omit \`timeout\` to use the default ` +
|
|
119
|
+
`budget of ${caps.defaultMs}ms (maximum ${caps.maxMs}ms).`);
|
|
67
120
|
}
|
|
68
121
|
export function resolveBashTimeoutCaps(opts) {
|
|
69
122
|
const defaultMs = validTimeoutMs(opts?.bashDefaultTimeoutMs) ??
|
|
70
123
|
validTimeoutMs(Number(process.env.BASH_DEFAULT_TIMEOUT_MS)) ??
|
|
71
124
|
BASH_DEFAULT_TIMEOUT_MS;
|
|
72
125
|
const maxMs = Math.max(validTimeoutMs(opts?.bashMaxTimeoutMs) ?? validTimeoutMs(Number(process.env.BASH_MAX_TIMEOUT_MS)) ?? BASH_MAX_TIMEOUT_MS, defaultMs);
|
|
126
|
+
if (opts?.bashDefaultTimeoutMs !== undefined)
|
|
127
|
+
announceDiscardedTimeout("bashDefaultTimeoutMs", opts.bashDefaultTimeoutMs, defaultMs);
|
|
128
|
+
else if (process.env.BASH_DEFAULT_TIMEOUT_MS !== undefined)
|
|
129
|
+
announceDiscardedTimeout("BASH_DEFAULT_TIMEOUT_MS", Number(process.env.BASH_DEFAULT_TIMEOUT_MS), defaultMs);
|
|
130
|
+
if (opts?.bashMaxTimeoutMs !== undefined)
|
|
131
|
+
announceDiscardedTimeout("bashMaxTimeoutMs", opts.bashMaxTimeoutMs, maxMs);
|
|
132
|
+
else if (process.env.BASH_MAX_TIMEOUT_MS !== undefined)
|
|
133
|
+
announceDiscardedTimeout("BASH_MAX_TIMEOUT_MS", Number(process.env.BASH_MAX_TIMEOUT_MS), maxMs);
|
|
73
134
|
return { defaultMs, maxMs };
|
|
74
135
|
}
|
|
136
|
+
export function __resetBashTimeoutAnnouncements() {
|
|
137
|
+
announcedTimeoutConfig.clear();
|
|
138
|
+
}
|
|
75
139
|
export function bashTimeoutCapsSec(caps) {
|
|
76
140
|
return { defaultSec: Math.max(1, Math.round(caps.defaultMs / 1000)), maxSec: Math.max(1, Math.round(caps.maxMs / 1000)) };
|
|
77
141
|
}
|
package/dist/tools/web.js
CHANGED
|
@@ -251,6 +251,7 @@ export function webFetchToolSpec(config = {}) {
|
|
|
251
251
|
: {}),
|
|
252
252
|
}),
|
|
253
253
|
effect: "read",
|
|
254
|
+
contentOrigin: "external",
|
|
254
255
|
execute: async (args, ctx) => {
|
|
255
256
|
const { url, prompt } = args;
|
|
256
257
|
const startedAt = Date.now();
|
|
@@ -811,6 +812,7 @@ export function createWebSearchTool(config) {
|
|
|
811
812
|
blocked_domains: Type.Optional(Type.Array(Type.String(), { description: "Never include search results from these domains" })),
|
|
812
813
|
}),
|
|
813
814
|
effect: "read",
|
|
815
|
+
contentOrigin: "external",
|
|
814
816
|
execute: async (args, ctx) => {
|
|
815
817
|
const { query, allowed_domains, blocked_domains } = args;
|
|
816
818
|
const startedAt = Date.now();
|