@sema-agent/core 5.17.0 → 5.18.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.
Files changed (56) hide show
  1. package/CHANGELOG.md +79 -0
  2. package/dist/agents/subagent.js +24 -0
  3. package/dist/core/auto-compaction.d.ts +6 -0
  4. package/dist/core/auto-compaction.js +15 -1
  5. package/dist/core/checkpoint-store.d.ts +1 -0
  6. package/dist/core/governance-codes.js +1 -0
  7. package/dist/core/hooks.d.ts +8 -0
  8. package/dist/core/hooks.js +17 -0
  9. package/dist/core/mcp.js +3 -0
  10. package/dist/core/memory-engine/content-origin.d.ts +27 -0
  11. package/dist/core/memory-engine/content-origin.js +38 -0
  12. package/dist/core/memory-engine/engine.d.ts +12 -2
  13. package/dist/core/memory-engine/engine.js +172 -12
  14. package/dist/core/memory-engine/file-backend.d.ts +4 -0
  15. package/dist/core/memory-engine/file-backend.js +25 -3
  16. package/dist/core/memory-engine/index.d.ts +2 -1
  17. package/dist/core/memory-engine/index.js +2 -1
  18. package/dist/core/memory-engine/layout.d.ts +16 -0
  19. package/dist/core/memory-engine/layout.js +90 -2
  20. package/dist/core/memory-engine/sync-client.d.ts +1 -0
  21. package/dist/core/memory-engine/sync-client.js +23 -5
  22. package/dist/core/memory-engine/tools.d.ts +55 -0
  23. package/dist/core/memory-engine/tools.js +307 -0
  24. package/dist/core/memory-engine/types.d.ts +1 -1
  25. package/dist/core/memory.d.ts +4 -0
  26. package/dist/core/memory.js +15 -2
  27. package/dist/core/permission-rule-consent.d.ts +131 -0
  28. package/dist/core/permission-rule-consent.js +307 -0
  29. package/dist/core/permission-rule-model.d.ts +66 -0
  30. package/dist/core/permission-rule-model.js +135 -0
  31. package/dist/core/permission-rule-store.d.ts +89 -0
  32. package/dist/core/permission-rule-store.js +145 -0
  33. package/dist/core/permission-rules.d.ts +3 -2
  34. package/dist/core/permission-rules.js +9 -4
  35. package/dist/core/runner/prepare-memory.d.ts +3 -1
  36. package/dist/core/runner/prepare-memory.js +54 -14
  37. package/dist/core/runner/prepare-task.d.ts +11 -0
  38. package/dist/core/runner/prepare-task.js +192 -10
  39. package/dist/core/runner/runtask.js +24 -0
  40. package/dist/core/runner/tool-output-projection.js +1 -1
  41. package/dist/core/tool-policy.d.ts +8 -1
  42. package/dist/core/tool-policy.js +36 -0
  43. package/dist/core/tools.js +1 -0
  44. package/dist/core/trace.d.ts +20 -0
  45. package/dist/core/types.d.ts +14 -0
  46. package/dist/core/wiring-manifest.d.ts +5 -1
  47. package/dist/core/wiring-manifest.js +2 -0
  48. package/dist/index.d.ts +6 -2
  49. package/dist/index.js +5 -1
  50. package/dist/stores/file/permission-rule-store.d.ts +32 -0
  51. package/dist/stores/file/permission-rule-store.js +213 -0
  52. package/dist/tools/fs/fs-bash.js +12 -5
  53. package/dist/tools/fs/fs-shared.d.ts +12 -0
  54. package/dist/tools/fs/fs-shared.js +65 -1
  55. package/dist/tools/web.js +2 -0
  56. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -112,9 +112,13 @@ export { parseAutoModeResponse, createAutoModeDecider, } from "./core/auto-mode.
112
112
  export { buildAutoModePrompt, renderAutoModeWindow, renderAutoModeAction, AUTO_MODE_DEFAULTS_SENTINEL, } from "./core/auto-mode-prompt.js";
113
113
  export { AUTO_MODE_BASE_PROMPT, AUTO_MODE_PERMISSIONS_EXTERNAL } from "./core/auto-mode-prompt-assets.js";
114
114
  export { createPermissionRulePolicy, validatePermissionRules, parsePermissionRule, wildcardMatch, } from "./core/permission-rules.js";
115
+ export { parseAllowRuleText, formatAllowRuleText, ruleAdmitsCommand, findAdmittingRule, suggestRulesForCommand, scopeCoversCwd, pathWithinRoot, isRuleLive, BARE_INTERPRETER_NAMES, MAX_RULE_TEXT_CHARS, } from "./core/permission-rule-model.js";
116
+ export { removePersistedRule, applyTombstones, sameScope, InMemoryPermissionRuleStore, EMPTY_RULE_STORE, } from "./core/permission-rule-store.js";
117
+ export { prepareCardApproval, confirmRuleApproval, redeemRuleTicket, redeemRuleBatch, prepareCcImport, prepareStarterBatch, mintRuleTicket, STARTER_RULES, InMemoryRuleApprovalRecordStore, } from "./core/permission-rule-consent.js";
118
+ export { FilePermissionRuleStoreProvider } from "./stores/file/permission-rule-store.js";
115
119
  export { formatHookFeedback, runToolGate, createHookEnvCapabilities, } from "./core/hooks.js";
116
120
  export { InMemoryMemoryStore, composeMemoryBlock, composeLayeredMemoryBlock, normalizeMemorySpec, supportsConsolidation, supportsPeriodicConsolidation, firstSentence, expandLexicalTerms, lexicalSearchMatch, classifyPromotable, enforcePromotableWriteGate, guardedMemoryStore, MemoryGateError, detectSecret, enforceSecretWriteGate, enforceStructuredNoteSecretGate, } from "./core/memory.js";
117
- export { MemoryEngine, FileMemoryEngineBackend, memoryBackendContract, assertMemoryBackendSearchEquivalence, buildMemoryInstruction, truncateIndex, scanEntryFiles, deriveRepoKey, deriveRepoMemoryDir, deriveControlPlaneDir, deriveRepoControlPlaneDir, resolveMemoryEngineRoot, scopeDirName, ControlPlaneCorruptError, parseEntryFile, serializeEntryFile, computeEntryRev, entryFromFile, MEMORY_INSTRUCTION_TEMPLATE, MEMORY_INDEX_FILENAME, MEMORY_INDEX_MAX_LINES, MEMORY_INDEX_MAX_BYTES, STUB_ARCHIVED_LINE, DEFAULT_MAX_MEMORY_FILES, DEFAULT_MAX_ENTRY_DEPTH, DEFAULT_HARVEST_DEADLINE_MS, DEFAULT_HARVEST_FILE_BUDGET, MASS_DELETION_FUSE_RATIO, scanMemoryWrite, scanMemoryFileName, scanRemediation, MEMORY_FILENAME_SEGMENT_RE, renderAnnouncements, enqueueMemoryAnnouncement, drainMemoryAnnouncements, peekMemoryAnnouncements, bumpScanFuse, scanFuseCount, clearScanFuse, ANNOUNCEMENTS_FILE, MEMORY_ANNOUNCEMENTS_MAX, SCAN_FUSE_FILE, SCAN_FUSE_THRESHOLD, SCOPE_SEGMENT_MAX_ENCODED, PROJECT_MARKER_PATH, encodeScopeSegment, decodeScopeSegment, parseScopeKey, formatUserScope, formatOrgScope, formatProjScope, formatUserProjScope, isPersonalScope, assertScopeContractPlacement, formatProjectMarker, parseProjectMarker, resolveProjectId, PROJECT_ID_REGEX, migrateScope, materializeEntriesToFiles, harvestFilesToPatches, projectionsToWriteBack, screenInboundEntries, REMOTE_HARVEST_PER_FILE_BYTES, REMOTE_MASS_DELETION_FUSE_RATIO, deriveProjectMemoryDir, deriveProjectControlDir, recordProjectIdHint, lookupProjectIdHint, PROJECT_ID_HINTS_FILE, reconcileMemoryEntries, nextSyncBaseline, mergeRecallHits, syncMemoryScope, } from "./core/memory-engine/index.js";
121
+ export { MemoryEngine, FileMemoryEngineBackend, memoryBackendContract, assertMemoryBackendSearchEquivalence, buildMemoryInstruction, truncateIndex, scanEntryFiles, deriveRepoKey, deriveRepoMemoryDir, deriveControlPlaneDir, deriveRepoControlPlaneDir, resolveMemoryEngineRoot, scopeDirName, ControlPlaneCorruptError, parseEntryFile, serializeEntryFile, computeEntryRev, entryFromFile, MEMORY_INSTRUCTION_TEMPLATE, MEMORY_RECALL_DISCIPLINE, MEMORY_INDEX_FILENAME, MEMORY_INDEX_MAX_LINES, MEMORY_INDEX_MAX_BYTES, MEMORY_SEARCH_TOOL_NAME, MEMORY_GET_TOOL_NAME, MEMORY_ENGINE_TOOL_NAMES, STUB_ARCHIVED_LINE, DEFAULT_MAX_MEMORY_FILES, DEFAULT_MAX_ENTRY_DEPTH, DEFAULT_HARVEST_DEADLINE_MS, DEFAULT_HARVEST_FILE_BUDGET, MASS_DELETION_FUSE_RATIO, scanMemoryWrite, scanMemoryFileName, scanRemediation, MEMORY_FILENAME_SEGMENT_RE, renderAnnouncements, enqueueMemoryAnnouncement, drainMemoryAnnouncements, peekMemoryAnnouncements, bumpScanFuse, scanFuseCount, clearScanFuse, ANNOUNCEMENTS_FILE, MEMORY_ANNOUNCEMENTS_MAX, SCAN_FUSE_FILE, SCAN_FUSE_THRESHOLD, SCOPE_SEGMENT_MAX_ENCODED, PROJECT_MARKER_PATH, encodeScopeSegment, decodeScopeSegment, parseScopeKey, formatUserScope, formatOrgScope, formatProjScope, formatUserProjScope, isPersonalScope, assertScopeContractPlacement, formatProjectMarker, parseProjectMarker, resolveProjectId, PROJECT_ID_REGEX, migrateScope, materializeEntriesToFiles, harvestFilesToPatches, projectionsToWriteBack, screenInboundEntries, REMOTE_HARVEST_PER_FILE_BYTES, REMOTE_MASS_DELETION_FUSE_RATIO, deriveProjectMemoryDir, deriveProjectControlDir, recordProjectIdHint, lookupProjectIdHint, PROJECT_ID_HINTS_FILE, reconcileMemoryEntries, nextSyncBaseline, mergeRecallHits, syncMemoryScope, } from "./core/memory-engine/index.js";
118
122
  export { SHARED_MEMORY_READ_CAP_BYTES, SHARED_MEMORY_LIST_PAGE_SIZE, SharedMemoryStoreError, } from "./core/shared-memory/types.js";
119
123
  export { sharedMemoryStoreContract, } from "./core/shared-memory/contract.js";
120
124
  export { termSet, jaccardDistance, cosineDistance } from "./core/memory-vector.js";
@@ -0,0 +1,32 @@
1
+ import type { PermissionRuleStore, PermissionRuleStoreProvider, PermissionRuleWriter, StoredAllowRules, WritablePermissionRuleStore } from "../../core/permission-rule-store.js";
2
+ import { PERMISSION_RULE_WRITER } from "../../core/permission-rule-store.js";
3
+ import type { StoreDurability, StoreFidelity } from "../../core/checkpoint-store.js";
4
+ declare class FilePermissionRuleStore implements WritablePermissionRuleStore {
5
+ private readonly dir;
6
+ private readonly file;
7
+ private readonly acquireWriteLock;
8
+ private readonly onError?;
9
+ readonly durability: StoreDurability;
10
+ readonly fidelity: StoreFidelity;
11
+ constructor(dir: string, file: string, acquireWriteLock: () => void, onError?: ((message: string) => void) | undefined);
12
+ private read;
13
+ private disclose;
14
+ private write;
15
+ list(): Promise<StoredAllowRules>;
16
+ private current;
17
+ private writeAndVerify;
18
+ private writeChain;
19
+ private serialize;
20
+ get [PERMISSION_RULE_WRITER](): PermissionRuleWriter;
21
+ private readonly writer;
22
+ }
23
+ export declare class FilePermissionRuleStoreProvider implements PermissionRuleStoreProvider {
24
+ private readonly dir;
25
+ private readonly onError?;
26
+ private lock;
27
+ constructor(dir: string, onError?: ((message: string) => void) | undefined);
28
+ private acquireWriteLock;
29
+ forPrincipal(principal: string | undefined): PermissionRuleStore;
30
+ dispose(): void;
31
+ }
32
+ export type { FilePermissionRuleStore };
@@ -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
+ }
@@ -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: `Optional timeout in milliseconds (max ${timeoutCaps.maxMs}; requests above the max are capped to it)` })),
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: `Timeout in milliseconds (default ${timeoutCaps.defaultMs}, max ${timeoutCaps.maxMs}; requests above the max are capped to it).` })),
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 >= 1 ? floored : undefined;
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();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/core",
3
- "version": "5.17.0",
3
+ "version": "5.18.0",
4
4
  "description": "Stateless, task-oriented AI agent core",
5
5
  "type": "module",
6
6
  "license": "BUSL-1.1",