@sema-agent/core 7.23.1 → 7.23.3

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 (124) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/dist/agents/agent-transcript-tool.js +2 -1
  3. package/dist/agents/cascade.js +2 -1
  4. package/dist/agents/list-agents-tool.js +2 -1
  5. package/dist/agents/observer.js +3 -2
  6. package/dist/agents/peer-session-drain.js +2 -1
  7. package/dist/agents/retain-ledger.js +2 -1
  8. package/dist/agents/send-message-tool.js +11 -10
  9. package/dist/agents/subagent.js +18 -17
  10. package/dist/agents/teacher.js +5 -4
  11. package/dist/brain/circuit-breaker.js +2 -1
  12. package/dist/brain/degrading.js +2 -1
  13. package/dist/brain/errors.js +24 -15
  14. package/dist/brain/failover.js +2 -1
  15. package/dist/brain/stream-engine.js +2 -1
  16. package/dist/core/a2a.js +4 -3
  17. package/dist/core/artifact-host.js +2 -1
  18. package/dist/core/ask-question.js +3 -2
  19. package/dist/core/auto-promote.js +3 -2
  20. package/dist/core/gate-bounded-await.js +2 -1
  21. package/dist/core/gate-fold.js +4 -1
  22. package/dist/core/gate-lanes.js +4 -2
  23. package/dist/core/git-worktree-env.js +6 -5
  24. package/dist/core/hooks.js +4 -4
  25. package/dist/core/lsp-session.js +3 -2
  26. package/dist/core/mcp.js +4 -3
  27. package/dist/core/memory-admission.js +2 -1
  28. package/dist/core/memory-engine/consolidation-driver.js +5 -4
  29. package/dist/core/memory-engine/consolidation.js +2 -1
  30. package/dist/core/memory-engine/delegation-settlement.js +4 -3
  31. package/dist/core/memory-engine/distiller.js +8 -4
  32. package/dist/core/memory-engine/engine.js +63 -55
  33. package/dist/core/memory-engine/file-backend.js +11 -10
  34. package/dist/core/memory-engine/layout.js +5 -4
  35. package/dist/core/memory-engine/session-incarnation.js +2 -1
  36. package/dist/core/memory.js +2 -1
  37. package/dist/core/oracle-isolation.js +3 -2
  38. package/dist/core/permission-rule-store.js +2 -1
  39. package/dist/core/runner/announce-once-ledger.js +2 -1
  40. package/dist/core/runner/compaction-seams.js +3 -2
  41. package/dist/core/runner/gate-exit.d.ts +5 -0
  42. package/dist/core/runner/git-leg-delivery.js +4 -3
  43. package/dist/core/runner/git-status-frame.js +2 -1
  44. package/dist/core/runner/mcp-redial.js +2 -1
  45. package/dist/core/runner/memory-consolidation-driver.js +2 -1
  46. package/dist/core/runner/permission-rule-lanes.js +3 -2
  47. package/dist/core/runner/prepare-artifact.js +2 -1
  48. package/dist/core/runner/prepare-ask-lane.js +2 -1
  49. package/dist/core/runner/prepare-caps-and-workflow.js +4 -3
  50. package/dist/core/runner/prepare-inherited-gate.js +2 -1
  51. package/dist/core/runner/prepare-memory-engine-session.js +2 -1
  52. package/dist/core/runner/prepare-memory.js +5 -4
  53. package/dist/core/runner/prepare-policy-chain.js +3 -2
  54. package/dist/core/runner/prepare-project-context.js +3 -2
  55. package/dist/core/runner/prepare-protocol-tools.js +3 -2
  56. package/dist/core/runner/prepare-suspend-saga.js +2 -1
  57. package/dist/core/runner/prepare-task.js +6 -5
  58. package/dist/core/runner/prepare-turn-wiring.js +2 -1
  59. package/dist/core/runner/resume-apply.js +5 -4
  60. package/dist/core/runner/resume-preflight.js +3 -2
  61. package/dist/core/runner/run-git-lane.js +4 -3
  62. package/dist/core/runner/run-leg.js +4 -3
  63. package/dist/core/runner/run-recovery-lanes.js +3 -2
  64. package/dist/core/runner/run-settle-and-teardown.js +2 -1
  65. package/dist/core/runner/run-stop-and-final-verify.js +2 -1
  66. package/dist/core/runner/run-telemetry-and-budget-seats.js +2 -1
  67. package/dist/core/runner/run-terminal-adoption.js +6 -5
  68. package/dist/core/runner/run-turn-boundary.js +5 -4
  69. package/dist/core/runner/runtask.js +2 -1
  70. package/dist/core/runner/stream-settle-backstop.js +5 -4
  71. package/dist/core/runner/stream-steer-verb.js +2 -1
  72. package/dist/core/runner/strict-output-schema.js +2 -1
  73. package/dist/core/runner/teardown-bounded.js +2 -1
  74. package/dist/core/safe-notify.js +2 -1
  75. package/dist/core/skills-directory.js +2 -1
  76. package/dist/core/strategy-store.js +2 -1
  77. package/dist/core/task-registry-agent.js +2 -1
  78. package/dist/core/task-registry-monitor.js +2 -1
  79. package/dist/core/task-registry-workflow.js +2 -1
  80. package/dist/core/task-registry.js +3 -2
  81. package/dist/core/thrown-value.d.ts +79 -0
  82. package/dist/core/thrown-value.js +56 -0
  83. package/dist/core/tool-errors.d.ts +0 -6
  84. package/dist/core/tool-errors.js +36 -14
  85. package/dist/core/tool-model-gate.js +2 -1
  86. package/dist/core/tool-policy.js +6 -13
  87. package/dist/core/tool-result-store.js +2 -1
  88. package/dist/core/tool-spec.d.ts +12 -0
  89. package/dist/core/write-protect.js +3 -2
  90. package/dist/engine/compaction/compaction.js +3 -4
  91. package/dist/engine/harness/agent-harness.js +2 -1
  92. package/dist/engine/harness/types.js +8 -7
  93. package/dist/engine/loop/agent-loop.js +7 -6
  94. package/dist/engine/lsp/node-lsp-manager.js +5 -4
  95. package/dist/engine/lsp/stdio-lsp-transport.js +12 -5
  96. package/dist/orchestration/dev-vm-script-runner.js +2 -1
  97. package/dist/orchestration/run-workflow-tool.js +7 -6
  98. package/dist/orchestration/workflow-governance.js +2 -1
  99. package/dist/orchestration/workflow-sandbox-conformance.js +3 -2
  100. package/dist/orchestration/workflow-script-store.js +2 -1
  101. package/dist/orchestration/workflow.js +15 -14
  102. package/dist/server/http.js +3 -2
  103. package/dist/stores/file/adoption/adopt.js +3 -2
  104. package/dist/stores/file/fs-atomic.js +2 -1
  105. package/dist/stores/file/permission-rule-adopt.js +2 -1
  106. package/dist/stores/file/permission-rule-store.js +2 -1
  107. package/dist/stores/file/session-store.js +4 -3
  108. package/dist/stores/file/strategy-store.js +7 -6
  109. package/dist/tools/artifact/artifact-tool.js +3 -2
  110. package/dist/tools/artifact/conformance.js +4 -3
  111. package/dist/tools/fs/bash-readonly-classifier.d.ts +20 -9
  112. package/dist/tools/fs/bash-readonly-classifier.js +7 -3
  113. package/dist/tools/fs/fs-bash.d.ts +16 -10
  114. package/dist/tools/fs/fs-bash.js +84 -35
  115. package/dist/tools/fs/fs-write.js +3 -2
  116. package/dist/tools/fs/safety.js +2 -1
  117. package/dist/tools/fs/search.js +3 -2
  118. package/dist/tools/monitor.js +3 -2
  119. package/dist/tools/sql.js +2 -1
  120. package/dist/tools/web.js +5 -15
  121. package/dist/tools/worktree.js +12 -9
  122. package/package.json +2 -1
  123. package/wire-changes/core@7.23.2.json +42 -0
  124. package/wire-changes/core@7.23.3.json +25 -0
@@ -8,7 +8,7 @@ import { TASK_OUTPUT_TOOL_NAME, TASK_STOP_TOOL_NAME, TASK_OUTPUT_MISSING_ID_MESS
8
8
  import { hasBackgroundShell } from "../../core/background-shell.js";
9
9
  import { delimitUntrusted } from "../../core/untrusted-text.js";
10
10
  import { MCP_IMAGE_MAX_BASE64 } from "../../core/mcp.js";
11
- import { imageMagicMatches, normalizeAbsPathLexically, withinAnyRoot } from "./safety.js";
11
+ import { imageMagicMatches, normalizeAbsPathLexically, violationDetails, withinAnyRoot } from "./safety.js";
12
12
  import { boundaryIsTheSandbox, canonicalPathIsAuthoritative, hasDestroy, isIsolated } from "../../core/remote-env.js";
13
13
  import { ghRateLimitHint } from "./gh-rate-limit.js";
14
14
  import { resolveBashTimeoutCaps, bashTimeoutCapsSec, bashTimeoutArgRefusal, bashTimeoutParamDescription, envErrorDetail, bashMaxOutputChars, clipShellOutput, writeShellOverflowFile, createShellOverflowSpoolFence, shellRecoveryHint, CWD_SENTINEL, stripCwdSentinel, BASH_READONLY_CONFINEMENT_NOTE, } from "./fs-shared.js";
@@ -16,6 +16,7 @@ import { PROBE_CAUSE_PATH_MAX, inlineUntrusted } from "../../core/untrusted-text
16
16
  import { bashClassifyDefaultAllow, BASH_READONLY_DEFAULT_ALLOW, coarseReadonlyCheck, classifyBoundedReadonlyPollLoopDetailed, classifyCompoundReadonlyDetailed, classifyOutOfRootReadGate, refusedCommandMayRead, classifySimpleCommandReadBoundary, } from "./bash-readonly-classifier.js";
17
17
  import { NOT_AUTO_ALLOWED, SHELL_SCAN_DECLINES } from "./bash-program-position.js";
18
18
  import { toolFace } from "../../core/tool-catalog-entries.js";
19
+ import { thrownText } from "../../core/thrown-value.js";
19
20
  const RECURSIVE_CAUSE_MAX_PATHS = 3;
20
21
  const RECURSIVE_READ_CAUSE_CODE = "shell.recursive_read_unbounded";
21
22
  function operandFamily(paths) {
@@ -47,7 +48,8 @@ export function bashReversibilityProbe(allow, boundary) {
47
48
  if (loop.reason === undefined)
48
49
  return { reversible: true };
49
50
  const seatOffer = (verdict) => (offers ? readRootCandidateOf(verdict) : {});
50
- if (boundaryDeclared(loop) || unjudgedRead(command, allowSet, resolved, loop, CLASSIFY_SEAT_DECLINE_EVIDENCE))
51
+ const loopScan = refusedCommandScan(command, allowSet, resolved, loop, CLASSIFY_SEAT_DECLINE_EVIDENCE);
52
+ if (boundaryDeclared(loop) || unjudgedRead(command, allowSet, resolved, loop, loopScan))
51
53
  return { reversible: false, mandated: true, ...seatOffer(loop) };
52
54
  const refusedCause = recursiveReadCause(detailed);
53
55
  return { reversible: false, ...boundaryGate(detailed), ...seatOffer(detailed), ...(refusedCause !== undefined ? { cause: refusedCause } : {}) };
@@ -92,20 +94,29 @@ export const CLASSIFY_SEAT_DECLINE_EVIDENCE = new Set();
92
94
  function readBoundaryMandate(command, allowSet, resolved, verdict, declineEvidence) {
93
95
  if (resolved === undefined)
94
96
  return {};
95
- if (denyJudgeSpoke(verdict) || unjudgedRead(command, allowSet, resolved, verdict, declineEvidence))
97
+ const scan = refusedCommandScan(command, allowSet, resolved, verdict, declineEvidence);
98
+ if (denyJudgeSpoke(verdict) || unjudgedRead(command, allowSet, resolved, verdict, scan))
96
99
  return { mandated: true };
97
100
  const named = classifyOutOfRootReadGate(command, allowSet, resolved);
98
- return named.gated || named.denied === true ? { mandated: true } : {};
101
+ if (named.gated || named.denied === true)
102
+ return { mandated: true };
103
+ return declinedRung(scan);
104
+ }
105
+ function declinedRung(scan) {
106
+ return scan.declined ? { boundaryDeclined: true } : {};
107
+ }
108
+ function refusedCommandScan(command, allowSet, resolved, verdict, declineEvidence) {
109
+ return verdict.refusedWhole === true ? refusedCommandMayRead(command, allowSet, resolved, declineEvidence) : { mayRead: false, declined: false };
99
110
  }
100
111
  function boundaryUnjudged(verdict) {
101
112
  return verdict.refusedWhole === true || (verdict.unresolvedOperands !== undefined && verdict.unresolvedOperands.length > 0);
102
113
  }
103
- function unjudgedRead(command, allowSet, resolved, verdict, declineEvidence) {
114
+ function unjudgedRead(command, allowSet, resolved, verdict, scan) {
104
115
  if (verdict.unresolvedOperands !== undefined && verdict.unresolvedOperands.length > 0)
105
116
  return true;
106
117
  if (verdict.refusedWhole !== true)
107
118
  return false;
108
- if (refusedCommandMayRead(command, allowSet, resolved, declineEvidence))
119
+ if (scan.mayRead)
109
120
  return true;
110
121
  if (resolved === undefined)
111
122
  return false;
@@ -128,7 +139,7 @@ export function bashReadBoundaryProbe(boundary) {
128
139
  const allowSet = new Set(bashClassifyDefaultAllow());
129
140
  const judge = (command, resolved, offers) => {
130
141
  const seatVerdict = (verdict) => {
131
- if (boundaryDeclared(verdict) || unjudgedRead(command, allowSet, resolved, verdict, BOUNDARY_SEAT_DECLINE_EVIDENCE)) {
142
+ if (boundaryDeclared(verdict) || unjudgedRead(command, allowSet, resolved, verdict, refusedCommandScan(command, allowSet, resolved, verdict, BOUNDARY_SEAT_DECLINE_EVIDENCE))) {
132
143
  const cause = recursiveReadCause(verdict);
133
144
  return { reversible: false, mandated: true, ...(cause !== undefined ? { cause } : {}) };
134
145
  }
@@ -726,7 +737,7 @@ async function adoptBackgroundShell(req) {
726
737
  return { ok: true, taskId, outputFile };
727
738
  }
728
739
  catch (e) {
729
- const reason = e instanceof Error ? e.message : String(e);
740
+ const reason = thrownText(e);
730
741
  let kill;
731
742
  try {
732
743
  kill = await env.killBackground(shellId);
@@ -761,7 +772,10 @@ export function createBashTool(env, rootCanonical, coAuthor = false, cwdRef = {
761
772
  };
762
773
  const canonicalEscape = async (command, signal) => {
763
774
  const resolvedBoundary = typeof taskOpts.readBoundary === "function" ? taskOpts.readBoundary() : taskOpts.readBoundary;
764
- if (resolvedBoundary === undefined || resolvedBoundary.face === "open")
775
+ if (resolvedBoundary === undefined)
776
+ return undefined;
777
+ const ask = { deny: resolvedBoundary.denyMatch, containment: resolvedBoundary.face !== "open" };
778
+ if (ask.deny === undefined && !ask.containment)
765
779
  return undefined;
766
780
  const faces = [
767
781
  classifyCompoundReadonlyDetailed(command, recheckReaders, resolvedBoundary),
@@ -770,7 +784,7 @@ export function createBashTool(env, rootCanonical, coAuthor = false, cwdRef = {
770
784
  const literal = [...new Set(faces.flatMap((v) => [...(v.checkedPaths ?? []), ...(v.checkedSpellings ?? [])]))];
771
785
  const asked = new Set(faces.flatMap((v) => v.outOfRootPaths ?? []));
772
786
  const patterns = [...new Set(faces.flatMap((v) => [...(v.undecidedPaths ?? []), ...(v.undecidedSpellings ?? [])]))].filter((p) => !asked.has(normalizeAbsPathLexically(p)));
773
- return await canonicalEscapeRefusal(canonicalBoundaryFor(resolvedBoundary.roots), { literal, patterns }, resolvedBoundary.roots, FULL_SHELL_FACE_ESCAPE_PROSE, signal);
787
+ return await canonicalEscapeRefusal(canonicalBoundaryFor(resolvedBoundary.roots), { literal, patterns }, resolvedBoundary.roots, FULL_SHELL_FACE_ESCAPE_PROSE, ask, signal);
774
788
  };
775
789
  return defineTool({
776
790
  name: "Bash",
@@ -1178,6 +1192,7 @@ function createCanonicalReadBoundary(env, readRoots) {
1178
1192
  let frontier = [p.slice(0, segs[first].start).replace(/[\\/]+$/, "") || "/"];
1179
1193
  let dirsUsed = 0;
1180
1194
  let matches = 0;
1195
+ const canonicalOfMatch = new Map();
1181
1196
  for (let j = first; j < segs.length; j++) {
1182
1197
  const seg = segs[j].text;
1183
1198
  if (!GLOB_METACHAR_RE.test(seg)) {
@@ -1211,40 +1226,48 @@ function createCanonicalReadBoundary(env, readRoots) {
1211
1226
  if (re.test(dot))
1212
1227
  next.push(joinCandidate(dir, dot));
1213
1228
  }
1214
- let dirInside = false;
1215
- if (isLast) {
1216
- if (canonDir !== undefined && !withinAnyRoot(roots, canonDir.value)) {
1217
- next.push(canonDir.value);
1218
- continue;
1219
- }
1220
- dirInside = canonDir !== undefined;
1221
- }
1229
+ const canonDirHere = isLast ? canonDir?.value : undefined;
1222
1230
  for (const entry of listed.value) {
1223
1231
  if (!globSegmentMatches(re, seg, entry.name))
1224
1232
  continue;
1225
1233
  if (++matches > MAX_GLOB_MATCHES) {
1226
1234
  return { unverifiable: `"${p}" expands to more than ${MAX_GLOB_MATCHES} names, too many to enumerate to verify` };
1227
1235
  }
1228
- if (isLast && dirInside && entry.kind !== "symlink")
1229
- continue;
1230
- next.push(joinCandidate(dir, entry.name));
1236
+ const candidate = joinCandidate(dir, entry.name);
1237
+ if (canonDirHere !== undefined && entry.kind !== "symlink")
1238
+ canonicalOfMatch.set(candidate, joinCandidate(canonDirHere, entry.name));
1239
+ next.push(candidate);
1231
1240
  }
1232
1241
  }
1233
1242
  frontier = next;
1234
1243
  if (frontier.length === 0)
1235
- return { paths: [] };
1244
+ return { paths: [], canonical: canonicalOfMatch };
1236
1245
  }
1237
- return { paths: frontier };
1246
+ return { paths: frontier, canonical: canonicalOfMatch };
1238
1247
  };
1239
1248
  return {
1240
- escapesAfterResolution: async (paths, signal) => {
1249
+ escapesAfterResolution: async (paths, ask, signal) => {
1250
+ const deny = ask.deny;
1241
1251
  if (lexicalOnly || (paths.literal.length === 0 && paths.patterns.length === 0))
1242
1252
  return { escaping: [] };
1243
1253
  const roots = await canonicalRoots(signal);
1244
1254
  const escaping = [];
1245
1255
  let unresolvable;
1246
- const judge = async (p) => {
1247
- const real = await resolve(p, signal);
1256
+ let denied;
1257
+ const denyHit = (p) => {
1258
+ if (deny === undefined || denied !== undefined)
1259
+ return false;
1260
+ const folded = normalizeAbsPathLexically(p);
1261
+ const pattern = deny(folded);
1262
+ if (pattern === null)
1263
+ return false;
1264
+ denied = { path: folded, pattern };
1265
+ return true;
1266
+ };
1267
+ const judge = async (p, canonicalKnown) => {
1268
+ if (denyHit(p))
1269
+ return;
1270
+ const real = canonicalKnown !== undefined ? { value: canonicalKnown, exact: true } : await resolve(p, signal);
1248
1271
  if (real === undefined) {
1249
1272
  if (resolverIsAuthoritative)
1250
1273
  unresolvable ??= `"${p}" cannot be resolved to verify where it reads`;
@@ -1254,18 +1277,36 @@ function createCanonicalReadBoundary(env, readRoots) {
1254
1277
  unresolvable ??= `"${p}" cannot be resolved to verify where it reads`;
1255
1278
  return;
1256
1279
  }
1257
- if (!withinAnyRoot(roots, real.value) && !escaping.includes(real.value))
1280
+ if (denyHit(real.value))
1281
+ return;
1282
+ if (ask.containment && !withinAnyRoot(roots, real.value) && !escaping.includes(real.value))
1258
1283
  escaping.push(real.value);
1259
1284
  };
1260
- for (const p of paths.literal)
1285
+ for (const p of paths.literal) {
1286
+ if (denied !== undefined)
1287
+ break;
1261
1288
  await judge(p);
1289
+ }
1262
1290
  for (const p of paths.patterns) {
1291
+ if (denied !== undefined)
1292
+ break;
1263
1293
  const expanded = await expandGlobCandidate(p, roots, signal);
1264
- if (expanded !== undefined && "unverifiable" in expanded)
1265
- return { escaping, unverifiable: expanded.unverifiable };
1266
- for (const t of expanded !== undefined && expanded.paths.length > 0 ? expanded.paths : [p])
1267
- await judge(t);
1294
+ if (expanded !== undefined && "unverifiable" in expanded) {
1295
+ unresolvable ??= expanded.unverifiable;
1296
+ continue;
1297
+ }
1298
+ if (expanded !== undefined && expanded.paths.length > 0) {
1299
+ for (const t of expanded.paths) {
1300
+ if (denied !== undefined)
1301
+ break;
1302
+ await judge(t, expanded.canonical.get(t));
1303
+ }
1304
+ }
1305
+ else
1306
+ await judge(p);
1268
1307
  }
1308
+ if (denied !== undefined)
1309
+ return { escaping, denied };
1269
1310
  return unresolvable === undefined ? { escaping } : { escaping, unverifiable: unresolvable };
1270
1311
  },
1271
1312
  canonicalOf: async (path, signal) => {
@@ -1289,8 +1330,14 @@ function createCanonicalReadBoundary(env, readRoots) {
1289
1330
  },
1290
1331
  };
1291
1332
  }
1292
- async function canonicalEscapeRefusal(canonical, candidates, readRoots, face, signal) {
1293
- const resolved = await canonical.escapesAfterResolution(candidates, signal);
1333
+ async function canonicalEscapeRefusal(canonical, candidates, readRoots, face, ask, signal) {
1334
+ const resolved = await canonical.escapesAfterResolution(candidates, ask, signal);
1335
+ if (resolved.denied !== undefined) {
1336
+ const content = `Error (Bash): reading "${resolved.denied.path}" is refused: the target matches the sensitive-path ` +
1337
+ `read deny list (pattern "${resolved.denied.pattern}"). This list guards credential-class paths and ` +
1338
+ `applies regardless of the containment roots. ${DENY_REFUSAL_NOTE}`;
1339
+ return errorResult(content, violationDetails({ code: "read_path_denied", message: content, target: resolved.denied.path, pattern: resolved.denied.pattern }));
1340
+ }
1294
1341
  if (resolved.unverifiable !== undefined) {
1295
1342
  return errorResult(`Error (Bash): ${resolved.unverifiable}, so this command cannot be confirmed to read only inside the allowed directories for this session: ${readRoots.join(", ")}. ` +
1296
1343
  face.note, { type: "readonly_out_of_root", code: "readonly_out_of_root", paths: [] });
@@ -1302,6 +1349,8 @@ async function canonicalEscapeRefusal(canonical, candidates, readRoots, face, si
1302
1349
  }
1303
1350
  return undefined;
1304
1351
  }
1352
+ const DENY_REFUSAL_NOTE = "The read boundary judged this command's written spelling before the call was approved; what the shell " +
1353
+ "would actually read was established only at execution, and a deny-listed read is refused here rather than escalated.";
1305
1354
  const READONLY_FACE_ESCAPE_PROSE = {
1306
1355
  subject: "a path this command reads",
1307
1356
  verdictPhrase: ` ${NOT_AUTO_ALLOWED}`,
@@ -1365,7 +1414,7 @@ export function createBashReadonlyTool(env, rootCanonical, allow, opts) {
1365
1414
  return errorResult(`Error (Bash): ${boundary.reason}. ${BASH_READONLY_CONFINEMENT_NOTE}`, { type: "readonly_out_of_root", code: "readonly_out_of_root", paths: boundary.outOfRootPaths ?? [] });
1366
1415
  }
1367
1416
  }
1368
- const escape = await canonicalEscapeRefusal(canonicalBoundary, { literal: [...(boundary.checkedPaths ?? []), ...(boundary.checkedSpellings ?? [])], patterns: [...(boundary.undecidedPaths ?? []), ...(boundary.undecidedSpellings ?? [])] }, readRoots, READONLY_FACE_ESCAPE_PROSE, ctx.signal);
1417
+ const escape = await canonicalEscapeRefusal(canonicalBoundary, { literal: [...(boundary.checkedPaths ?? []), ...(boundary.checkedSpellings ?? [])], patterns: [...(boundary.undecidedPaths ?? []), ...(boundary.undecidedSpellings ?? [])] }, readRoots, READONLY_FACE_ESCAPE_PROSE, { deny: undefined, containment: true }, ctx.signal);
1369
1418
  if (escape !== undefined)
1370
1419
  return escape;
1371
1420
  const readOnlyTick = opts?.onToolProgress === undefined ? undefined : (t) => describeSafely(opts.onToolProgress, { toolCallId: ctx.toolCallId, toolName: "Bash", ...t });
@@ -5,6 +5,7 @@ import { sha256, resolveKey, violationText, violationDetails, requireRead, check
5
5
  import { decodeTextBytes, encodeTextForFile, normalizeEditText, normalizeFileText } from "./encoding.js";
6
6
  import { MAX_EDIT_BYTES, decodeEditBytes, tooLargeToEditMessage, truncatedUtf16BodyMessage, persistedTextOf, notReadRefusalText, enoentMessage, FILE_STATE_TRAILER, FILE_PATH_PARAMS, ipynbRedirect, countLines, applyRecordedEdit, } from "./fs-shared.js";
7
7
  import { toolFace } from "../../core/tool-catalog-entries.js";
8
+ import { thrownText } from "../../core/thrown-value.js";
8
9
  async function envFinalWrite(env, key, content, signal, opts) {
9
10
  const fireEdited = () => {
10
11
  if (opts?.edited?.hook === undefined)
@@ -66,7 +67,7 @@ async function trackToolEdit(hook, tool, path, key, signal) {
66
67
  res = await hook({ tool, path, key, ...(signal !== undefined ? { signal } : {}) });
67
68
  }
68
69
  catch (err) {
69
- return { refusal: `Error (${tool}): the file-history first-touch backup failed (${clipHookText(err instanceof Error ? err.message : String(err))}) — nothing was written.` };
70
+ return { refusal: `Error (${tool}): the file-history first-touch backup failed (${clipHookText(thrownText(err))}) — nothing was written.` };
70
71
  }
71
72
  if (res.ok === false)
72
73
  return { refusal: clipHookText(res.refusal) };
@@ -80,7 +81,7 @@ async function gateToolWrite(hook, tool, path, key, content) {
80
81
  res = await hook({ tool, path, key, content });
81
82
  }
82
83
  catch (err) {
83
- return `Error (${tool}): the write gate failed (${clipHookText(err instanceof Error ? err.message : String(err))}) — nothing was written.`;
84
+ return `Error (${tool}): the write gate failed (${clipHookText(thrownText(err))}) — nothing was written.`;
84
85
  }
85
86
  if (res !== undefined && res.ok === false)
86
87
  return `Error (${tool}): write rejected by the write gate (${res.code}): ${clipHookText(res.reason)}`;
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
2
2
  import { sliceHeadSafe } from "../../core/surrogate-safe-slice.js";
3
3
  import { pathTargetValue, protectivePathTargetOf } from "../../core/tool-registry.js";
4
4
  import { withGateTimeout, DEFAULT_GATE_AWAIT_TIMEOUT_MS } from "../../core/gate-bounded-await.js";
5
+ import { thrownText } from "../../core/thrown-value.js";
5
6
  export function fileArgPath(args) {
6
7
  const a = args;
7
8
  const fp = a?.file_path;
@@ -524,7 +525,7 @@ export async function canonicalizeTarget(env, path, signal, family, baseCwd) {
524
525
  catch (err) {
525
526
  if (seenThrow && err === thrownByProbe)
526
527
  throw err;
527
- return { ok: false, message: `cannot resolve path "${path}": ${err instanceof Error ? err.message : String(err)}` };
528
+ return { ok: false, message: `cannot resolve path "${path}": ${thrownText(err)}` };
528
529
  }
529
530
  }
530
531
  async function probeRealTarget(env, path, target, family, signal) {
@@ -1,6 +1,7 @@
1
1
  import { delimitUntrusted } from "../../core/untrusted-text.js";
2
2
  import { MAX_EXEC_OUTPUT_BYTES } from "../../core/exec-output-tail.js";
3
3
  import { hasBinaryExtension, isAbsoluteForFamily, isWinFormPath, joinForFamily, pathFamilyOf } from "./safety.js";
4
+ import { thrownText } from "../../core/thrown-value.js";
4
5
  const WALK_MAX_FILES = 5000;
5
6
  const WALK_MAX_DEPTH = 32;
6
7
  const GREP_DEFAULT_CAP = 250;
@@ -772,7 +773,7 @@ export async function jsGrepDetailed(env, root, p, signal, guards, deny, denyOut
772
773
  test = compilePattern(p);
773
774
  }
774
775
  catch (e) {
775
- return { text: `Error (grep): invalid regex: ${e instanceof Error ? e.message : String(e)}` };
776
+ return { text: `Error (grep): invalid regex: ${thrownText(e)}` };
776
777
  }
777
778
  const ignore = await buildIgnore(env, root, signal);
778
779
  const rootPrefix = root.replace(/[\\/]+$/, "") + (root.includes("\\") ? "\\" : "/");
@@ -912,7 +913,7 @@ export async function jsGrepDetailed(env, root, p, signal, guards, deny, denyOut
912
913
  catch (e) {
913
914
  if (e instanceof GrepBudgetExceeded)
914
915
  return { text: budgetError(shownPath, monotonicNow() - fileMatchStart) };
915
- return { text: `Error (grep): invalid regex: ${e instanceof Error ? e.message : String(e)}` };
916
+ return { text: `Error (grep): invalid regex: ${thrownText(e)}` };
916
917
  }
917
918
  fileCount = spans.length;
918
919
  if (mode === "content") {
@@ -3,6 +3,7 @@ import { defineTool, errorResult } from "../core/tools.js";
3
3
  import { hasBackgroundShell } from "../core/background-shell.js";
4
4
  import { MONITOR_DEFAULT_TIMEOUT_MS, MONITOR_MAX_TIMEOUT_MS, } from "../core/task-registry.js";
5
5
  import { toolFace } from "../core/tool-catalog-entries.js";
6
+ import { thrownText } from "../core/thrown-value.js";
6
7
  const MONITOR_DESCRIPTION = "Run a shell command in the background and watch its stdout as a stream of events.\n" +
7
8
  "\n" +
8
9
  "- Each line the command writes to stdout becomes a notification event delivered to you between turns; " +
@@ -98,7 +99,7 @@ export function createMonitorTool(env, opts) {
98
99
  });
99
100
  }
100
101
  catch (e) {
101
- const why = e instanceof Error ? e.message : String(e);
102
+ const why = thrownText(e);
102
103
  let killDetail;
103
104
  try {
104
105
  const kill = await env.killBackground(spawn.value.shellId);
@@ -106,7 +107,7 @@ export function createMonitorTool(env, opts) {
106
107
  killDetail = `${kill.error.code}: ${kill.error.message}`;
107
108
  }
108
109
  catch (killErr) {
109
- killDetail = killErr instanceof Error ? killErr.message : String(killErr);
110
+ killDetail = thrownText(killErr);
110
111
  }
111
112
  return errorResult(killDetail === undefined
112
113
  ? `Error (Monitor): the watch process was started but could not be registered (${why}); it has been terminated.`
package/dist/tools/sql.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { Type } from "typebox";
2
+ import { thrownText } from "../core/thrown-value.js";
2
3
  const FORBIDDEN = /\b(insert|update|delete|drop|alter|create|truncate|grant|revoke|merge|replace|attach|detach|pragma|vacuum|exec|execute|call|copy|lock|reindex)\b/i;
3
4
  class SqlValidationError extends Error {
4
5
  }
@@ -82,7 +83,7 @@ export function createSqlTool(opts) {
82
83
  validateReadOnlySql(sql, opts.allowTables);
83
84
  }
84
85
  catch (e) {
85
- const msg = e instanceof Error ? e.message : String(e);
86
+ const msg = thrownText(e);
86
87
  return { content: `SQL rejected: ${msg}`, details: { error: msg }, terminate: false };
87
88
  }
88
89
  const rows = await opts.query(stripTrailingSemicolon(sql.trim()));
package/dist/tools/web.js CHANGED
@@ -8,6 +8,7 @@ import { DEFAULT_CHARS_PER_TOKEN } from "../internal/harness.js";
8
8
  import { binaryMagicFormat } from "./fs/safety.js";
9
9
  import { pdfMagicMatches } from "./fs/pdf.js";
10
10
  import { toolFace } from "../core/tool-catalog-entries.js";
11
+ import { thrownText } from "../core/thrown-value.js";
11
12
  const MAX_REDIRECTS = 10;
12
13
  const MAX_URL_LENGTH = 2048;
13
14
  const WEBFETCH_ACCEPT = "text/markdown, text/html, */*";
@@ -447,7 +448,7 @@ export function webFetchToolSpec(config = {}) {
447
448
  };
448
449
  }
449
450
  return {
450
- content: `Error (WebFetch): request failed: ${e instanceof Error ? e.message : String(e)}`,
451
+ content: `Error (WebFetch): request failed: ${thrownText(e)}`,
451
452
  details: errDetails,
452
453
  isError: true,
453
454
  };
@@ -594,7 +595,7 @@ export function webFetchToolSpec(config = {}) {
594
595
  catch (e) {
595
596
  out = text;
596
597
  note =
597
- `[note: summarization failed (${inlineUntrusted(e instanceof Error ? e.message : String(e), 160)}) — ` +
598
+ `[note: summarization failed (${inlineUntrusted(thrownText(e), 160)}) — ` +
598
599
  `raw page content follows; the requested analysis ("${inlineUntrusted(prompt, 120)}") was NOT applied]`;
599
600
  }
600
601
  }
@@ -910,7 +911,7 @@ export function createWebSearchTool(config) {
910
911
  return abortedFrame();
911
912
  if (ac.signal.aborted)
912
913
  return timedOutFrame();
913
- const msg = redactSecrets(e instanceof Error ? e.message : String(e)).trim();
914
+ const msg = redactSecrets(thrownText(e)).trim();
914
915
  const headline = "Error (WebSearch): the search backend failed.";
915
916
  const verdict = classifySearchFailure(msg);
916
917
  const composed = (msg ? `${headline} The backend's error text follows:\n\n${delimitUntrusted("WebSearch backend error", msg, SEARCH_ERROR_EXCERPT_CHARS)}` : headline) + `\n\n${verdict.hint}`;
@@ -1029,18 +1030,7 @@ export async function probeSearchBackend(search, options) {
1029
1030
  return { ok: true, results: results.length };
1030
1031
  }
1031
1032
  catch (e) {
1032
- let text;
1033
- if (e instanceof Error)
1034
- text = e.message;
1035
- else {
1036
- try {
1037
- text = String(e);
1038
- }
1039
- catch {
1040
- text = Object.prototype.toString.call(e);
1041
- }
1042
- }
1043
- return { ok: false, error: text };
1033
+ return { ok: false, error: thrownText(e) };
1044
1034
  }
1045
1035
  finally {
1046
1036
  clearTimeout(timer);
@@ -3,6 +3,7 @@ import { Type } from "typebox";
3
3
  import { defineTool, errorResult } from "../core/tools.js";
4
4
  import { addWorktree, pruneWorktrees, WORKTREE_PARENT } from "../core/git-worktree-env.js";
5
5
  import { toolFace } from "../core/tool-catalog-entries.js";
6
+ import { thrownText } from "../core/thrown-value.js";
6
7
  const ENTER_DESCRIPTION = "Use this tool ONLY when explicitly instructed to work in a worktree — either by the user directly, or by " +
7
8
  "project instructions. This tool creates an isolated git worktree and switches the current session into it.\n" +
8
9
  "\n" +
@@ -67,14 +68,16 @@ const EXIT_DESCRIPTION = "Exit a worktree session created by EnterWorktree and r
67
68
  "- Once exited, EnterWorktree can be called again to create a fresh worktree.";
68
69
  const shq = (s) => `'${s.replace(/'/g, "'\\''")}'`;
69
70
  function reasonOf(e) {
70
- if (e instanceof Error)
71
- return e.message;
72
- if (typeof e === "object" && e !== null) {
73
- const m = e.message;
74
- if (typeof m === "string" && m !== "")
75
- return m;
71
+ try {
72
+ if (typeof e === "object" && e !== null && !(e instanceof Error)) {
73
+ const m = e.message;
74
+ if (typeof m === "string" && m !== "")
75
+ return m;
76
+ }
77
+ }
78
+ catch {
76
79
  }
77
- return String(e);
80
+ return thrownText(e);
78
81
  }
79
82
  async function resolveBaseCommit(env, repoRoot, baseRef) {
80
83
  const rev = async (ref) => {
@@ -147,7 +150,7 @@ export function createWorktreeTools(env, opts) {
147
150
  try {
148
151
  const probe = await env.exec("git rev-parse --is-inside-work-tree", { cwd: repoRoot });
149
152
  if (!probe.ok || probe.value.exitCode !== 0) {
150
- const detail = probe.ok ? (probe.value.stderr || probe.value.stdout).trim().split("\n")[0] : String(probe.error);
153
+ const detail = probe.ok ? (probe.value.stderr || probe.value.stdout).trim().split("\n")[0] : thrownText(probe.error);
151
154
  return errorResult(`Error (EnterWorktree): the working directory ${repoRoot} is not a git repository` +
152
155
  `${detail ? ` (${detail})` : ""}. This deployment has no VCS-agnostic worktree extension point, ` +
153
156
  "so worktree isolation is unavailable here.");
@@ -214,7 +217,7 @@ export function createWorktreeTools(env, opts) {
214
217
  createdDir = worktreeDir;
215
218
  }
216
219
  catch (e) {
217
- return errorResult(`Error (EnterWorktree): ${e instanceof Error ? e.message : String(e)}`);
220
+ return errorResult(`Error (EnterWorktree): ${thrownText(e)}`);
218
221
  }
219
222
  session.current = {
220
223
  worktreeDir: createdDir,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/core",
3
- "version": "7.23.1",
3
+ "version": "7.23.3",
4
4
  "description": "Stateless, task-oriented AI agent core",
5
5
  "type": "module",
6
6
  "license": "BUSL-1.1",
@@ -70,6 +70,7 @@
70
70
  "gate:field-liveness": "node scripts/verify-field-liveness.mjs",
71
71
  "gate:debts": "node scripts/verify-debts.mjs",
72
72
  "handoff:diff": "node scripts/dist-handoff-diff.mjs",
73
+ "post-ship": "node scripts/post-ship.mjs",
73
74
  "gate:debts:rc": "node scripts/verify-debts.mjs --rc-gate",
74
75
  "gate:error-surface": "node scripts/verify-error-surface.mjs",
75
76
  "gate:criteria": "node scripts/criteria-lint.mjs",
@@ -0,0 +1,42 @@
1
+ {
2
+ "repo": "core",
3
+ "version": "7.23.2",
4
+ "from": "e9bf8451c623a118a6589a6f860bd7b828c3aa3e",
5
+ "to": "v7.23.2",
6
+ "added": [],
7
+ "removed": [],
8
+ "changed": [],
9
+ "narrowed": [],
10
+ "closedSetMembers": [],
11
+ "closedSetsUnreadable": [],
12
+ "typeKeys": [
13
+ {
14
+ "type": "ReversibilityVerdict",
15
+ "at": [
16
+ "src/core/tool-spec.ts:50"
17
+ ],
18
+ "added": [
19
+ "boundaryDeclined"
20
+ ],
21
+ "removed": [],
22
+ "breaking": false,
23
+ "criteriaCovered": false
24
+ }
25
+ ],
26
+ "declarationMoves": [],
27
+ "mintMoves": [],
28
+ "criteriaObserves": [],
29
+ "consumersHint": {
30
+ "cli": [
31
+ "ReversibilityVerdict"
32
+ ],
33
+ "server": [
34
+ "ReversibilityVerdict"
35
+ ],
36
+ "sdk": [],
37
+ "note": "scanned cli@8dd52617 server@7d768b71 sdk@3b02abb — a name listed is MENTIONED there today, which is evidence of consumption, not proof of it"
38
+ },
39
+ "breakingCount": 0,
40
+ "breakingItems": [],
41
+ "toResolvedAt": "cb5240a4a8289d28ed37ef84df8ee1bfb2139905"
42
+ }
@@ -0,0 +1,25 @@
1
+ {
2
+ "repo": "core",
3
+ "version": "7.23.3",
4
+ "from": "b48d37a658cfaa46695e76339021dc1226a9ba57",
5
+ "to": "v7.23.3",
6
+ "added": [],
7
+ "removed": [],
8
+ "changed": [],
9
+ "narrowed": [],
10
+ "closedSetMembers": [],
11
+ "closedSetsUnreadable": [],
12
+ "typeKeys": [],
13
+ "declarationMoves": [],
14
+ "mintMoves": [],
15
+ "criteriaObserves": [],
16
+ "consumersHint": {
17
+ "cli": [],
18
+ "server": [],
19
+ "sdk": [],
20
+ "note": "scanned cli@bef85633 server@1da4e102 sdk@0a6aeb7 — a name listed is MENTIONED there today, which is evidence of consumption, not proof of it"
21
+ },
22
+ "breakingCount": 0,
23
+ "breakingItems": [],
24
+ "toResolvedAt": "912d7792e95edd36fb9b6bb68866661ac36f7f6a"
25
+ }