@sema-agent/core 7.23.0 → 7.23.2

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 (132) hide show
  1. package/CHANGELOG.md +64 -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.d.ts +1 -1
  21. package/dist/core/gate-bounded-await.js +3 -2
  22. package/dist/core/gate-crash-trace.d.ts +7 -0
  23. package/dist/core/gate-crash-trace.js +3 -0
  24. package/dist/core/gate-fold.js +6 -2
  25. package/dist/core/gate-lanes.js +6 -3
  26. package/dist/core/git-worktree-env.js +6 -5
  27. package/dist/core/hooks.js +6 -5
  28. package/dist/core/lsp-session.js +3 -2
  29. package/dist/core/mcp.js +4 -3
  30. package/dist/core/memory-admission.js +2 -1
  31. package/dist/core/memory-engine/consolidation-driver.js +5 -4
  32. package/dist/core/memory-engine/consolidation.js +2 -1
  33. package/dist/core/memory-engine/delegation-settlement.js +4 -3
  34. package/dist/core/memory-engine/distiller.js +8 -4
  35. package/dist/core/memory-engine/engine.js +63 -55
  36. package/dist/core/memory-engine/file-backend.js +11 -10
  37. package/dist/core/memory-engine/layout.js +5 -4
  38. package/dist/core/memory-engine/session-incarnation.js +2 -1
  39. package/dist/core/memory.js +2 -1
  40. package/dist/core/oracle-isolation.js +3 -2
  41. package/dist/core/permission-rule-store.js +2 -1
  42. package/dist/core/runner/announce-once-ledger.js +2 -1
  43. package/dist/core/runner/compaction-seams.js +3 -2
  44. package/dist/core/runner/gate-exit.d.ts +5 -5
  45. package/dist/core/runner/gate-exit.js +0 -3
  46. package/dist/core/runner/git-leg-delivery.js +4 -3
  47. package/dist/core/runner/git-status-frame.js +2 -1
  48. package/dist/core/runner/mcp-redial.js +2 -1
  49. package/dist/core/runner/memory-consolidation-driver.js +2 -1
  50. package/dist/core/runner/permission-rule-lanes.js +3 -2
  51. package/dist/core/runner/prepare-artifact.js +2 -1
  52. package/dist/core/runner/prepare-ask-lane.js +2 -1
  53. package/dist/core/runner/prepare-caps-and-workflow.js +4 -3
  54. package/dist/core/runner/prepare-inherited-gate.js +2 -1
  55. package/dist/core/runner/prepare-memory-engine-session.js +2 -1
  56. package/dist/core/runner/prepare-memory.js +5 -4
  57. package/dist/core/runner/prepare-policy-chain.js +3 -2
  58. package/dist/core/runner/prepare-project-context.js +3 -2
  59. package/dist/core/runner/prepare-protocol-tools.js +3 -2
  60. package/dist/core/runner/prepare-suspend-saga.js +2 -1
  61. package/dist/core/runner/prepare-task.js +6 -5
  62. package/dist/core/runner/prepare-turn-wiring.js +2 -1
  63. package/dist/core/runner/resume-apply.js +5 -4
  64. package/dist/core/runner/resume-preflight.js +3 -2
  65. package/dist/core/runner/run-git-lane.js +4 -3
  66. package/dist/core/runner/run-leg.js +4 -3
  67. package/dist/core/runner/run-recovery-lanes.js +3 -2
  68. package/dist/core/runner/run-settle-and-teardown.js +2 -1
  69. package/dist/core/runner/run-stop-and-final-verify.js +2 -1
  70. package/dist/core/runner/run-telemetry-and-budget-seats.js +2 -1
  71. package/dist/core/runner/run-terminal-adoption.js +6 -5
  72. package/dist/core/runner/run-turn-boundary.js +5 -4
  73. package/dist/core/runner/runtask.js +2 -1
  74. package/dist/core/runner/stream-settle-backstop.js +5 -4
  75. package/dist/core/runner/stream-steer-verb.js +2 -1
  76. package/dist/core/runner/strict-output-schema.js +2 -1
  77. package/dist/core/runner/teardown-bounded.js +2 -1
  78. package/dist/core/safe-notify.js +2 -1
  79. package/dist/core/skills-directory.js +2 -1
  80. package/dist/core/strategy-store.js +2 -1
  81. package/dist/core/task-registry-agent.js +2 -1
  82. package/dist/core/task-registry-monitor.js +2 -1
  83. package/dist/core/task-registry-workflow.js +2 -1
  84. package/dist/core/task-registry.js +3 -2
  85. package/dist/core/thrown-value.d.ts +79 -0
  86. package/dist/core/thrown-value.js +56 -0
  87. package/dist/core/tool-errors.d.ts +0 -6
  88. package/dist/core/tool-errors.js +36 -14
  89. package/dist/core/tool-model-gate.js +2 -1
  90. package/dist/core/tool-policy.js +6 -13
  91. package/dist/core/tool-result-store.js +2 -1
  92. package/dist/core/tool-spec.d.ts +12 -0
  93. package/dist/core/write-protect.js +4 -4
  94. package/dist/engine/compaction/compaction.js +3 -4
  95. package/dist/engine/harness/agent-harness.js +2 -1
  96. package/dist/engine/harness/types.js +8 -7
  97. package/dist/engine/loop/agent-loop.js +7 -6
  98. package/dist/engine/lsp/node-lsp-manager.js +5 -4
  99. package/dist/engine/lsp/stdio-lsp-transport.js +12 -5
  100. package/dist/orchestration/dev-vm-script-runner.js +2 -1
  101. package/dist/orchestration/run-workflow-tool.js +7 -6
  102. package/dist/orchestration/workflow-governance.js +2 -1
  103. package/dist/orchestration/workflow-sandbox-conformance.js +3 -2
  104. package/dist/orchestration/workflow-script-store.js +2 -1
  105. package/dist/orchestration/workflow.js +15 -14
  106. package/dist/server/http.js +3 -2
  107. package/dist/stores/file/adoption/adopt.js +3 -2
  108. package/dist/stores/file/fs-atomic.js +2 -1
  109. package/dist/stores/file/permission-rule-adopt.js +2 -1
  110. package/dist/stores/file/permission-rule-store.js +2 -1
  111. package/dist/stores/file/session-store.js +4 -3
  112. package/dist/stores/file/strategy-store.js +7 -6
  113. package/dist/tools/artifact/artifact-tool.js +3 -2
  114. package/dist/tools/artifact/conformance.js +4 -3
  115. package/dist/tools/fs/bash-lexer.d.ts +12 -5
  116. package/dist/tools/fs/bash-lexer.js +5 -2
  117. package/dist/tools/fs/bash-program-position.d.ts +18 -0
  118. package/dist/tools/fs/bash-program-position.js +13 -1
  119. package/dist/tools/fs/bash-readonly-classifier.d.ts +23 -7
  120. package/dist/tools/fs/bash-readonly-classifier.js +74 -41
  121. package/dist/tools/fs/fs-bash.d.ts +28 -0
  122. package/dist/tools/fs/fs-bash.js +31 -13
  123. package/dist/tools/fs/fs-write.js +3 -2
  124. package/dist/tools/fs/safety.js +2 -1
  125. package/dist/tools/fs/search.js +3 -2
  126. package/dist/tools/monitor.js +3 -2
  127. package/dist/tools/sql.js +2 -1
  128. package/dist/tools/web.js +5 -15
  129. package/dist/tools/worktree.js +12 -9
  130. package/package.json +2 -1
  131. package/wire-changes/core@7.23.1.json +205 -0
  132. package/wire-changes/core@7.23.2.json +42 -0
@@ -23,6 +23,7 @@ import { compileOutputSchema } from "../core/runner/strict-output-schema.js";
23
23
  import { WorkflowBudgetExceededError, WorkflowNestingError, WorkflowAgentBlockedError, WorkflowAgentParkedError, WorkflowAgentSchemaError, WorkflowAgentStalledError, WorkflowMaxAgentsError, WORKFLOW_SPAWN_BLOCKED_ERROR_CODE } from "./workflow-types.js";
24
24
  import { WorkflowScriptError } from "./workflow-meta.js";
25
25
  import { STALL_CONNECT_MS, STALL_FIRST_TOKEN_MS } from "../brain/timeout.js";
26
+ import { asThrownError, thrownText } from "../core/thrown-value.js";
26
27
  export * from "./workflow-types.js";
27
28
  const MAX_TRANSCRIPT_CHARS = 4000;
28
29
  const WORKFLOW_RESULT_MAX = 4000;
@@ -193,7 +194,7 @@ function foldFanOutSettled(settled, lane, errorsOut, emitRunLog) {
193
194
  }
194
195
  rethrowIfMaxAgents(s.e);
195
196
  errorsOut?.push(workflowFanOutSlotError(s.i, s.e));
196
- emitRunLog(`${lane}[${s.i}] failed: ${boundedRedactedSummary(s.e instanceof Error ? s.e.message : String(s.e), 300)}`);
197
+ emitRunLog(`${lane}[${s.i}] failed: ${boundedRedactedSummary(thrownText(s.e), 300)}`);
197
198
  return null;
198
199
  });
199
200
  if (dropped > 0)
@@ -203,7 +204,7 @@ function foldFanOutSettled(settled, lane, errorsOut, emitRunLog) {
203
204
  function workflowFanOutSlotError(index, e) {
204
205
  const code = e?.code;
205
206
  const kind = typeof code === "string" && code.length > 0 ? code : e instanceof Error ? e.name : "Error";
206
- return { index, kind, message: boundedRedactedSummary(e instanceof Error ? e.message : String(e), 300) };
207
+ return { index, kind, message: boundedRedactedSummary(thrownText(e), 300) };
207
208
  }
208
209
  export class WorkflowResultTooLargeError extends Error {
209
210
  size;
@@ -841,7 +842,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
841
842
  };
842
843
  const parkSeat = opts.checkpointStore;
843
844
  const parkedByOrdinal = [];
844
- const errText = (err) => (err instanceof Error ? err.message : String(err));
845
+ const errText = (err) => (thrownText(err));
845
846
  const withoutToken = (text, token) => (token === "" ? text : text.split(token).join("<token>"));
846
847
  const parkBindingBroken = (ordinal, why) => new WorkflowParkRefusal("workflow.park_binding_broken", `workflow resume: ordinal ${ordinal}'s park cannot be bound to its checkpoint (${why}) — the recorded park is inconsistent. Decide the checkpoint through the deployment's approval channel and start a fresh run.`, { ordinal });
847
848
  const readParkTruth = async (seat, key, ordinal) => {
@@ -980,7 +981,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
980
981
  }
981
982
  catch (err) {
982
983
  if (dbg)
983
- console.error(`[sema:wf-journal] runId=${runId} callKey=${callKey} TOMBSTONE-ERROR ${err instanceof Error ? err.message : String(err)}`);
984
+ console.error(`[sema:wf-journal] runId=${runId} callKey=${callKey} TOMBSTONE-ERROR ${thrownText(err)}`);
984
985
  }
985
986
  return;
986
987
  }
@@ -993,7 +994,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
993
994
  }
994
995
  catch (err) {
995
996
  if (dbg)
996
- console.error(`[sema:wf-journal] runId=${runId} callKey=${callKey} APPEND-ERROR ${err instanceof Error ? err.message : String(err)}`);
997
+ console.error(`[sema:wf-journal] runId=${runId} callKey=${callKey} APPEND-ERROR ${thrownText(err)}`);
997
998
  throw err;
998
999
  }
999
1000
  };
@@ -1271,7 +1272,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
1271
1272
  inheritedModelSnap = opts.defaultModel?.();
1272
1273
  }
1273
1274
  catch (err) {
1274
- inheritedModelError = err instanceof Error ? err : new Error(String(err));
1275
+ inheritedModelError = asThrownError(err);
1275
1276
  }
1276
1277
  }
1277
1278
  const inheritedThinkingSnap = spec.thinking === undefined ? opts.defaultThinking?.() : undefined;
@@ -1781,17 +1782,17 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
1781
1782
  const journaled = journalAppend(callKey, salvaged ?? {
1782
1783
  taskId: callKey,
1783
1784
  sessionId: "",
1784
- result: boundedRedactedSummary(err instanceof Error ? err.message : String(err), 500),
1785
+ result: boundedRedactedSummary(thrownText(err), 500),
1785
1786
  terminal: {
1786
1787
  kind: "failed",
1787
1788
  ...(err instanceof WorkflowAgentBlockedError ? { code: WORKFLOW_SPAWN_BLOCKED_ERROR_CODE } : err instanceof WorkflowBudgetExceededError ? { code: err.code } : {}),
1788
- message: boundedRedactedSummary(err instanceof Error ? err.message : String(err), 500),
1789
+ message: boundedRedactedSummary(thrownText(err), 500),
1789
1790
  },
1790
1791
  stats: { turns: 0, tokens: 0, costMicroUsd: 0, usageMissing: true },
1791
1792
  }, label).catch(() => undefined);
1792
1793
  if (!((err instanceof WorkflowBudgetExceededError || err instanceof WorkflowAgentParkedError) && rec.startedAt === undefined))
1793
1794
  await journaled;
1794
- bceTerminal(callKey, "failed", rec.output ?? (err instanceof Error ? err.message : String(err)), rec.sessionId, rec.stats);
1795
+ bceTerminal(callKey, "failed", rec.output ?? (thrownText(err)), rec.sessionId, rec.stats);
1795
1796
  }
1796
1797
  throw err;
1797
1798
  }
@@ -1969,7 +1970,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
1969
1970
  }
1970
1971
  recordFailed();
1971
1972
  releaseLeg();
1972
- bceTerminal(callKey, "failed", err instanceof Error ? err.message : String(err), undefined, undefined);
1973
+ bceTerminal(callKey, "failed", thrownText(err), undefined, undefined);
1973
1974
  throw err;
1974
1975
  }
1975
1976
  if (childSessionId !== undefined && !finalized) {
@@ -2012,11 +2013,11 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
2012
2013
  void journalAppend(callKey, {
2013
2014
  taskId: callKey,
2014
2015
  sessionId: "",
2015
- terminal: { kind: "failed", message: boundedRedactedSummary(err instanceof Error ? err.message : String(err), 500) },
2016
- result: boundedRedactedSummary(err instanceof Error ? err.message : String(err), 500),
2016
+ terminal: { kind: "failed", message: boundedRedactedSummary(thrownText(err), 500) },
2017
+ result: boundedRedactedSummary(thrownText(err), 500),
2017
2018
  stats: { turns: partialSpend?.turns ?? 0, tokens: partialSpend?.tokens ?? 0, costMicroUsd: partialSpend?.costMicroUsd ?? 0 },
2018
2019
  }).catch(() => undefined);
2019
- bceTerminal(callKey, "failed", err instanceof Error ? err.message : String(err), undefined, rec.stats);
2020
+ bceTerminal(callKey, "failed", thrownText(err), undefined, rec.stats);
2020
2021
  throw err;
2021
2022
  }
2022
2023
  rollbackUsageBeat();
@@ -2377,7 +2378,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
2377
2378
  run.endedAt = now();
2378
2379
  if (parkedLeg !== undefined && !(err instanceof WorkflowAgentParkedError))
2379
2380
  err = parkedError();
2380
- run.error = err instanceof Error ? err.message : String(err);
2381
+ run.error = thrownText(err);
2381
2382
  closeAbandonedAgents(run.endedAt);
2382
2383
  {
2383
2384
  let thrownCode;
@@ -1,4 +1,5 @@
1
1
  import http from "node:http";
2
+ import { thrownText } from "../core/thrown-value.js";
2
3
  const DEFAULT_MAX_BODY = 5 * 1024 * 1024;
3
4
  class HttpError extends Error {
4
5
  status;
@@ -25,7 +26,7 @@ async function readJson(req, maxBytes) {
25
26
  return JSON.parse(Buffer.concat(chunks).toString("utf8"));
26
27
  }
27
28
  catch (e) {
28
- throw new HttpError(400, `invalid JSON request body: ${e instanceof Error ? e.message : String(e)}`);
29
+ throw new HttpError(400, `invalid JSON request body: ${thrownText(e)}`);
29
30
  }
30
31
  }
31
32
  function setCors(res, origin) {
@@ -47,7 +48,7 @@ export function createTaskServer(opts) {
47
48
  void handle(req, res).catch((err) => {
48
49
  if (!res.headersSent) {
49
50
  const status = err instanceof HttpError ? err.status : 500;
50
- sendJson(res, status, { error: err instanceof Error ? err.message : String(err) });
51
+ sendJson(res, status, { error: thrownText(err) });
51
52
  }
52
53
  else {
53
54
  res.end();
@@ -3,6 +3,7 @@ import { existsSync, readdirSync, readFileSync, unlinkSync } from "node:fs";
3
3
  import { join, resolve } from "node:path";
4
4
  import { adoptFilePermissionRuleStore } from "../permission-rule-adopt.js";
5
5
  import { BootLock, atomicWriteFile, ensureDir, sanitizeScope } from "../fs-atomic.js";
6
+ import { thrownText } from "../../../core/thrown-value.js";
6
7
  import { AdoptionError, NOT_MIGRATED_BY_DESIGN, readRootAdoptionFile, writeRootAdoptionFile, } from "./marker.js";
7
8
  const CONFIG_ACCOUNT_FILE = "adoption-configs.json";
8
9
  function baselineConfigs(toPrincipal) {
@@ -491,7 +492,7 @@ export async function adoptLocalDataRoot(opts) {
491
492
  catch (err) {
492
493
  if (err instanceof AdoptionError)
493
494
  throw err;
494
- throw new AdoptionError("adoption_rule_leg_refused", `the permission-rule leg refused: ${err instanceof Error ? err.message : String(err)}`);
495
+ throw new AdoptionError("adoption_rule_leg_refused", `the permission-rule leg refused: ${thrownText(err)}`);
495
496
  }
496
497
  if (nested.status === "stalled") {
497
498
  return {
@@ -533,7 +534,7 @@ export async function adoptLocalDataRoot(opts) {
533
534
  return {
534
535
  status: "stalled",
535
536
  phase: 4,
536
- error: `the "${leg.store}" carriage leg failed: ${err instanceof Error ? err.message : String(err)} — resume when it is healthy`,
537
+ error: `the "${leg.store}" carriage leg failed: ${thrownText(err)} — resume when it is healthy`,
537
538
  };
538
539
  }
539
540
  if (rows !== undefined && (!Number.isSafeInteger(rows) || rows < 0)) {
@@ -5,6 +5,7 @@ import { basename, dirname, join, resolve as resolvePath } from "node:path";
5
5
  import { createHash, randomBytes } from "node:crypto";
6
6
  import { execFileSync } from "node:child_process";
7
7
  import { physicalPathOf, physicalPathOfExisting } from "../../core/physical-path.js";
8
+ import { thrownText } from "../../core/thrown-value.js";
8
9
  const SAFE_COMPONENT = /^[A-Za-z0-9_.-]+$/;
9
10
  export function sanitizePathComponent(raw) {
10
11
  if (raw === "" || raw === "." || raw === ".." || !SAFE_COMPONENT.test(raw)) {
@@ -411,7 +412,7 @@ export class BootLock {
411
412
  }
412
413
  catch (err) {
413
414
  throw new FileStoreLockError("store.lock_unreadable", `file store: this process still owns the lock file at ${this.lockPath} from an earlier release that ` +
414
- `could not remove it, and removing it now failed too (${String(err.message)}); ` +
415
+ `could not remove it, and removing it now failed too (${thrownText(err)}); ` +
415
416
  `remove it manually`, this.lockPath, process.pid);
416
417
  }
417
418
  abandonedLocks.delete(this.abandonKey);
@@ -3,6 +3,7 @@ import { join } from "node:path";
3
3
  import { writerOf } from "../../core/permission-rule-store.js";
4
4
  import { createPermissionRuleStoreProvider } from "../../core/permission-rule-provider.js";
5
5
  import { syncPermissionRules } from "../../core/permission-rule-sync.js";
6
+ import { thrownText } from "../../core/thrown-value.js";
6
7
  import { FileDurableRulePartitionProvider, LOCAL_OWNER_FILE, principalRuleFileName, readRuleAdoptionFile, writeRuleAdoptionFile, } from "./permission-rule-store.js";
7
8
  export async function adoptFilePermissionRuleStore(opts) {
8
9
  if (typeof opts.toPrincipal !== "string" || opts.toPrincipal === "") {
@@ -63,7 +64,7 @@ export async function adoptFilePermissionRuleStore(opts) {
63
64
  });
64
65
  }
65
66
  catch (err) {
66
- return { status: "stalled", phase: 4, error: `the first sync round failed: ${err instanceof Error ? err.message : String(err)}` };
67
+ return { status: "stalled", phase: 4, error: `the first sync round failed: ${thrownText(err)}` };
67
68
  }
68
69
  if (!syncResult.ok) {
69
70
  return {
@@ -6,6 +6,7 @@ import { PERMISSION_RULE_WRITER, applySyncJoin, applyTombstones, assertDeleteDel
6
6
  import { canonicalize } from "../../core/canonical-json.js";
7
7
  import { BootLock } from "./fs-atomic.js";
8
8
  import { assertAdoptionBootGate } from "./adoption/marker.js";
9
+ import { thrownText } from "../../core/thrown-value.js";
9
10
  function isRuleFile(v) {
10
11
  const p = v;
11
12
  return typeof p === "object" && p !== null && p.schemaVersion === 4 && Array.isArray(p.rules) && Array.isArray(p.tombstones) && typeof p.rev === "number" && (p.quarantined === undefined || Array.isArray(p.quarantined));
@@ -139,7 +140,7 @@ class FileDurableRulePartition {
139
140
  const e = err;
140
141
  if (e?.code === "ENOENT")
141
142
  return { absent: true };
142
- const why = `could not read ${this.file}: ${e?.message ?? String(err)}; loading zero rules`;
143
+ const why = `could not read ${this.file}: ${thrownText(err)}; loading zero rules`;
143
144
  this.disclose(why);
144
145
  return { unreadable: why };
145
146
  }
@@ -3,6 +3,7 @@ import { join } from "node:path";
3
3
  import { BaseSessionStorage, StoredSession, SessionError, getEntriesToFork, uuidv7, validateEntriesForImport, } from "../../internal/harness.js";
4
4
  import { canonicalStoreKey, AppendLog, atomicWriteFile, ensureDir, readJsonlRecords, sanitizePathComponent } from "./fs-atomic.js";
5
5
  import { assertAdoptionBootGate } from "./adoption/marker.js";
6
+ import { thrownText } from "../../core/thrown-value.js";
6
7
  function containSinkThenable(r) {
7
8
  if (typeof r?.then === "function") {
8
9
  r.then(undefined, () => {
@@ -171,7 +172,7 @@ export class FileSessionRepo {
171
172
  }
172
173
  catch (err) {
173
174
  if (!(err instanceof SessionError && err.code === "not_found")) {
174
- this.disclose(this.pathFor(id), `session log unreadable (${err instanceof Error ? err.message : String(err)}) — skipped from the listing`);
175
+ this.disclose(this.pathFor(id), `session log unreadable (${thrownText(err)}) — skipped from the listing`);
175
176
  }
176
177
  }
177
178
  }
@@ -200,7 +201,7 @@ export class FileSessionRepo {
200
201
  }
201
202
  catch (err) {
202
203
  if (!(err instanceof SessionError && err.code === "not_found")) {
203
- this.disclose(this.pathFor(id), `session log unreadable (${err instanceof Error ? err.message : String(err)}) — skipped from the placed listing`);
204
+ this.disclose(this.pathFor(id), `session log unreadable (${thrownText(err)}) — skipped from the placed listing`);
204
205
  }
205
206
  continue;
206
207
  }
@@ -238,7 +239,7 @@ export class FileSessionRepo {
238
239
  rmSync(this.pathFor(metadata.id), { force: true });
239
240
  }
240
241
  catch (err) {
241
- throw new SessionError("storage", `session ${metadata.id} could not be deleted: ${err instanceof Error ? err.message : String(err)}`, err instanceof Error ? err : undefined);
242
+ throw new SessionError("storage", `session ${metadata.id} could not be deleted: ${thrownText(err)}`, err instanceof Error ? err : undefined);
242
243
  }
243
244
  }
244
245
  async fork(sourceMetadata, options = {}) {
@@ -3,6 +3,7 @@ import { join } from "node:path";
3
3
  import { createHash } from "node:crypto";
4
4
  import { atomicWriteFile } from "./fs-atomic.js";
5
5
  import { createSafeNotifier, observeThenableRejection } from "../../core/safe-notify.js";
6
+ import { thrownText } from "../../core/thrown-value.js";
6
7
  import { MAX_STRATEGY_INJECTION_TOTAL_BYTES, MAX_STRATEGY_TEXT_BYTES, compileStrategyQuery, resolveCapacityCap, resolveFindLimit, scoreStoredStrategy, storedStrategyShapeIssue, strategyDedupKey, validateStrategyForWrite, } from "../../core/strategy-store.js";
7
8
  function configRefusal(message, code) {
8
9
  const e = new Error(message);
@@ -32,14 +33,14 @@ export class FileStrategyStore {
32
33
  mkdirSync(root, { recursive: true, mode: 0o700 });
33
34
  }
34
35
  catch (err) {
35
- throw configRefusal(`FileStrategyStore: cannot create root ${root}: ${String(err.message ?? err)}`, "config.strategy_root_invalid");
36
+ throw configRefusal(`FileStrategyStore: cannot create root ${root}: ${thrownText(err)}`, "config.strategy_root_invalid");
36
37
  }
37
38
  let st;
38
39
  try {
39
40
  st = statSync(root);
40
41
  }
41
42
  catch (err) {
42
- throw configRefusal(`FileStrategyStore: cannot stat root ${root}: ${String(err.message ?? err)}`, "config.strategy_root_invalid");
43
+ throw configRefusal(`FileStrategyStore: cannot stat root ${root}: ${thrownText(err)}`, "config.strategy_root_invalid");
43
44
  }
44
45
  if (!st.isDirectory()) {
45
46
  throw configRefusal(`FileStrategyStore: root ${root} exists and is not a directory`, "config.strategy_root_invalid");
@@ -145,7 +146,7 @@ export class FileStrategyStore {
145
146
  catch (err) {
146
147
  if (err.code === "ENOENT")
147
148
  continue;
148
- this.incident({ op: "find", error: `unstat-able entry skipped: ${String(err.message ?? err)}`, path: p });
149
+ this.incident({ op: "find", error: `unstat-able entry skipped: ${thrownText(err)}`, path: p });
149
150
  continue;
150
151
  }
151
152
  if (!fst.isFile()) {
@@ -163,7 +164,7 @@ export class FileStrategyStore {
163
164
  catch (err) {
164
165
  if (err.code === "ENOENT")
165
166
  continue;
166
- this.incident({ op: "find", error: `unreadable entry skipped: ${String(err.message ?? err)}`, path: p });
167
+ this.incident({ op: "find", error: `unreadable entry skipped: ${thrownText(err)}`, path: p });
167
168
  continue;
168
169
  }
169
170
  let parsed;
@@ -217,7 +218,7 @@ export class FileStrategyStore {
217
218
  this.incident({ op: "parse", error: `corrupt entry quarantined (${reason})`, path: p });
218
219
  }
219
220
  catch (err) {
220
- this.incident({ op: "parse", error: `corrupt entry skipped (${reason}); quarantine rename failed: ${String(err.message ?? err)}`, path: p });
221
+ this.incident({ op: "parse", error: `corrupt entry skipped (${reason}); quarantine rename failed: ${thrownText(err)}`, path: p });
221
222
  }
222
223
  }
223
224
  writeEntry(dir, path, entry) {
@@ -286,7 +287,7 @@ export class FileStrategyStore {
286
287
  catch (err) {
287
288
  if (err.code === "ENOENT")
288
289
  continue;
289
- this.incident({ op: "evict", error: String(err.message ?? err), id: le.entry.id, path: le.path });
290
+ this.incident({ op: "evict", error: thrownText(err), id: le.entry.id, path: le.path });
290
291
  }
291
292
  }
292
293
  }
@@ -7,6 +7,7 @@ import { errorResult } from "../../core/tools.js";
7
7
  import { inlineUntrusted } from "../../core/untrusted-text.js";
8
8
  import { actionDescription, artifactDescription, PARAM_TEXT, urlDescription } from "./artifact-text.js";
9
9
  import { ARTIFACT_REJECT_BREAKER_THRESHOLD, artifactRejectBannerFor, artifactRejectKeyOf, createArtifactRejectBreaker } from "./reject-breaker.js";
10
+ import { thrownText } from "../../core/thrown-value.js";
10
11
  export const MAX_FILES_PER_PUBLISH = 255;
11
12
  const LIST_LIMIT_DEFAULT = 25;
12
13
  const LIST_LIMIT_MAX = 100;
@@ -93,10 +94,10 @@ function callHost(action, fn, signal) {
93
94
  }
94
95
  catch (err) {
95
96
  signal?.removeEventListener("abort", onAbort);
96
- resolve({ ok: false, kind: "threw", message: err instanceof Error ? err.message : String(err) });
97
+ resolve({ ok: false, kind: "threw", message: thrownText(err) });
97
98
  return;
98
99
  }
99
- p.then((value) => resolve(OUTCOME_SHAPE[action](value) ? { ok: true, value } : { ok: false, kind: "malformed", message: `the ${action} answer was outside the host contract` }), (err) => resolve({ ok: false, kind: "threw", message: err instanceof Error ? err.message : String(err) })).finally(() => signal?.removeEventListener("abort", onAbort));
100
+ p.then((value) => resolve(OUTCOME_SHAPE[action](value) ? { ok: true, value } : { ok: false, kind: "malformed", message: `the ${action} answer was outside the host contract` }), (err) => resolve({ ok: false, kind: "threw", message: thrownText(err) })).finally(() => signal?.removeEventListener("abort", onAbort));
100
101
  });
101
102
  }
102
103
  function buildSchema(caps) {
@@ -1,4 +1,5 @@
1
1
  import { randomUUID } from "node:crypto";
2
+ import { thrownText } from "../../core/thrown-value.js";
2
3
  import { readArtifactHostCapabilities, } from "../../core/artifact-host.js";
3
4
  class Violation extends Error {
4
5
  }
@@ -25,7 +26,7 @@ async function publish(h, r, attemptId) {
25
26
  return await h.host.publish(r, ctxOf(h, attemptId));
26
27
  }
27
28
  catch (err) {
28
- return fail(`publish threw: ${err instanceof Error ? err.message : String(err)}`);
29
+ return fail(`publish threw: ${thrownText(err)}`);
29
30
  }
30
31
  }
31
32
  async function publishOk(h, r, what, attemptId) {
@@ -282,7 +283,7 @@ define({ id: "read.unknown_url_typed", describe: "`read` of a url this host neve
282
283
  out = await h.host.read({ url: FOREIGN_URL }, ctxOf(h));
283
284
  }
284
285
  catch (err) {
285
- return fail(`read threw: ${err instanceof Error ? err.message : String(err)}`);
286
+ return fail(`read threw: ${thrownText(err)}`);
286
287
  }
287
288
  check(!out.ok && (out.code === "not_found" || out.code === "forbidden"), `an unknown url answered ${out.ok ? "ok" : out.code}`);
288
289
  return out.ok ? "" : out.code;
@@ -340,7 +341,7 @@ export async function runArtifactConformance(host, opts = {}) {
340
341
  row = { id: v.id, status: "pass", detail: await v.run(h) };
341
342
  }
342
343
  catch (err) {
343
- row = { id: v.id, status: "fail", detail: err instanceof Violation ? err.message : `threw: ${err instanceof Error ? err.message : String(err)}` };
344
+ row = { id: v.id, status: "fail", detail: err instanceof Violation ? err.message : `threw: ${thrownText(err)}` };
344
345
  }
345
346
  results.push(row);
346
347
  }
@@ -92,20 +92,27 @@ export interface SegmentTokens {
92
92
  /** What a substitution leaves in a word's FOLDED spelling: a marker no argv word can spell, so a consumer that
93
93
  * judges words sees "this word carries text the shell computes" rather than the computing text itself. */
94
94
  export declare const SUBSTITUTION_PLACEHOLDER = " subst";
95
+ export type QuoteOpen = '"' | "'" | undefined;
95
96
  /**
96
- * Which characters of `s` sit inside a QUOTED region, by the same pairing rule the lexer walks with. The pairing
97
- * quote characters themselves are reported as quoted — they are not operators either way.
97
+ * Which QUOTE holds each character of `s`, by the same pairing rule the lexer walks with, and the boolean view
98
+ * of it. The pairing quote characters themselves belong to the region they bound — they are not operators
99
+ * either way — so `openedBy` names that region's quote character at their own index too.
100
+ *
101
+ * `quoted` is a VIEW of `openedBy`, minted in the same step: a consumer asking only "is this inside quotes"
102
+ * reads it, one asking WHICH quote (single quotes suppress every expansion, double quotes suppress neither `$`
103
+ * nor the backtick) reads `openedBy`, and the two cannot drift.
98
104
  *
99
105
  * `balanced` is false when a region is still open at the end of the string. Callers must then NOT use the mask:
100
106
  * an unterminated quote is a string bash itself cannot parse, and every consumer falls back to the quote-BLIND
101
107
  * reading, which is the stricter one.
102
108
  *
103
109
  * EXACTNESS. This models bash only for the residual character space the strict faces operate in, where backslash
104
- * escapes, `$`, backticks, and subshells are rejected outright — there `'` and `"` pairing is the whole of
105
- * quoting, so a region computed here is exactly a region bash quotes. It is NOT a general shell quoting parser
106
- * and must not be used before those characters are excluded.
110
+ * escapes and subshells are rejected outright — there `'` and `"` pairing is the whole of quoting, so a region
111
+ * computed here is exactly a region bash quotes. A caller deciding about `$` or a backtick on this mask owes
112
+ * the argument in `bash-readonly-classifier.ts`. It is NOT a general shell quoting parser.
107
113
  */
108
114
  export declare function quoteMask(s: string): {
115
+ openedBy: readonly QuoteOpen[];
109
116
  quoted: readonly boolean[];
110
117
  balanced: boolean;
111
118
  };
@@ -5,14 +5,17 @@ function quoteStep(open, ch) {
5
5
  return open === ch ? undefined : open;
6
6
  }
7
7
  export function quoteMask(s) {
8
+ const openedBy = new Array(s.length).fill(undefined);
8
9
  const quoted = new Array(s.length).fill(false);
9
10
  let open;
10
11
  for (let i = 0; i < s.length; i++) {
11
12
  const before = open;
12
13
  open = quoteStep(open, s[i]);
13
- quoted[i] = before !== undefined || open !== undefined;
14
+ const holds = before ?? open;
15
+ openedBy[i] = holds;
16
+ quoted[i] = holds !== undefined;
14
17
  }
15
- return { quoted, balanced: open === undefined };
18
+ return { openedBy, quoted, balanced: open === undefined };
16
19
  }
17
20
  export function literalTildeSpelling(word) {
18
21
  return word.startsWith("~") ? `./${word}` : word;
@@ -114,6 +114,9 @@ export type ProgramPositionRefusal = {
114
114
  readonly kind: "control_syntax_at_program_position";
115
115
  readonly owner: string;
116
116
  readonly word: string;
117
+ /** Not a DECLINE ({@link programPositionRunsShell}): the row WAS read and a SHELL stands at the program
118
+ * position. Absent ⇒ genuine control syntax, which IS one. */
119
+ readonly runsShell?: true;
117
120
  };
118
121
  export interface ProgramPosition {
119
122
  /** Indices of the bare control words stripped in front (`!`, `if`, `time`, …), in order; their declared options
@@ -159,5 +162,20 @@ export declare function readProgramPosition(toks: SegmentTokens): ProgramPositio
159
162
  * stdin AND the program it runs is not one the caller accepts as a stream target. ONE reading, shared by every
160
163
  * consumer; WHICH targets are accepted is the caller's declaration, not this reader's. */
161
164
  export declare function argvReentryUnjudged(pos: ProgramPosition, acceptedTargets: ReadonlySet<string>): boolean;
165
+ /**
166
+ * The two ways this reader DECLINES rather than judges — a CLOSED set, because which of them a seat reads as
167
+ * evidence is a table each seat declares against (`fs-bash.ts`, where the argument is), not a boolean buried
168
+ * in a caller. `unreadable_program_position`: the walk cannot name the program bash will run (`env -- ls`).
169
+ * `stream_fed_operands`: the operands arrive on a STREAM it never saw (`… | xargs cat`). Neither names a WORD.
170
+ */
171
+ export declare const SHELL_SCAN_DECLINES: readonly ["unreadable_program_position", "stream_fed_operands"];
172
+ export type ShellScanDecline = (typeof SHELL_SCAN_DECLINES)[number];
173
+ /** Does a SHELL stand where this segment's program would be — as the program itself (`sh -c`), or as a launcher
174
+ * ROW handing its operands to one (`watch "…"`)? ONE predicate for the re-entry class, so the two spellings
175
+ * cannot be answered differently; which names are shells is the caller's declaration. Not a decline. */
176
+ export declare function programPositionRunsShell(pos: ProgramPosition, shellProgram: RegExp): boolean;
177
+ /** Which decline, if any, this program position raises — ONE reading of the two facts above, so no consumer
178
+ * re-derives either. A refusal that says a SHELL runs is NOT one: it names what runs. */
179
+ export declare function shellScanDeclineOf(pos: ProgramPosition, acceptedStreamTargets: ReadonlySet<string>): ShellScanDecline | undefined;
162
180
  /** ONE sentence per refusal kind, minted here and nowhere else. */
163
181
  export declare function programPositionRefusalSentence(refusal: ProgramPositionRefusal): string;
@@ -107,7 +107,7 @@ export function readProgramPosition(toks) {
107
107
  }
108
108
  const launchers = [{ index: i, name: basename }];
109
109
  const owner = head;
110
- const shellRefusal = () => ({ kind: "control_syntax_at_program_position", owner, word: "sh" });
110
+ const shellRefusal = () => ({ kind: "control_syntax_at_program_position", owner, word: "sh", runsShell: true });
111
111
  let owedOperands = LAUNCHER_TABLE.get(basename)?.requiredOperands ?? 0;
112
112
  let lastRow = LAUNCHER_TABLE.get(basename);
113
113
  const chain0 = (program, refusal) => ({ controlWords, written, assignments, launchers, argvFromStdin: false, runsDefaultProgram: false, program, refusal });
@@ -158,6 +158,18 @@ export function argvReentryUnjudged(pos, acceptedTargets) {
158
158
  const runs = pos.program?.basename ?? (pos.runsDefaultProgram ? ARGV_REENTRY_DEFAULT_PROGRAM : undefined);
159
159
  return runs === undefined || !acceptedTargets.has(runs);
160
160
  }
161
+ export const SHELL_SCAN_DECLINES = ["unreadable_program_position", "stream_fed_operands"];
162
+ export function programPositionRunsShell(pos, shellProgram) {
163
+ if (pos.refusal !== undefined)
164
+ return pos.refusal.kind === "control_syntax_at_program_position" && pos.refusal.runsShell === true;
165
+ return pos.program !== undefined && shellProgram.test(pos.program.basename);
166
+ }
167
+ export function shellScanDeclineOf(pos, acceptedStreamTargets) {
168
+ if (pos.refusal !== undefined) {
169
+ return pos.refusal.kind === "control_syntax_at_program_position" && pos.refusal.runsShell === true ? undefined : "unreadable_program_position";
170
+ }
171
+ return argvReentryUnjudged(pos, acceptedStreamTargets) ? "stream_fed_operands" : undefined;
172
+ }
161
173
  export function programPositionRefusalSentence(refusal) {
162
174
  switch (refusal.kind) {
163
175
  case "option_at_program_position":
@@ -1,4 +1,5 @@
1
1
  import { type ShellSegmentConnector } from "./bash-lexer.js";
2
+ import { type ShellScanDecline } from "./bash-program-position.js";
2
3
  /**
3
4
  * Bare command names `bash_readonly` permits out of the box, ALSO the default reversible set for the
4
5
  * The {@link import("./fs-bash.js").bashReversibilityProbe} classifier. **Coarse first filter, NOT a security
@@ -31,7 +32,6 @@ import { type ShellSegmentConnector } from "./bash-lexer.js";
31
32
  * value-taking options defeat the stdin-floor operand count without a per-verb option-value model
32
33
  * — those are a follow-on slice, not a silent drop.
33
34
  */
34
- export { NOT_AUTO_ALLOWED } from "./bash-program-position.js";
35
35
  export type { ShellSegmentConnector } from "./bash-lexer.js";
36
36
  export declare const BASH_READONLY_DEFAULT_ALLOW: readonly string[];
37
37
  /**
@@ -174,7 +174,7 @@ export interface ShellCompoundSegments {
174
174
  * drift in a segmentation is drift in what a deny rule is even looking at.
175
175
  *
176
176
  * Three steps, in this order and load-bearing:
177
- * 1. WHOLE-STRING hard reject of {@link SHELL_SEGMENT_HARD_REJECT} — redirection, substitution,
177
+ * 1. WHOLE-STRING hard reject ({@link hardRejectReason}) — redirection, substitution,
178
178
  * subshells, escapes, line breaks. This is what makes step 3's quote mask EXACT: with those
179
179
  * characters gone, `'`/`"` pairing is the entirety of quoting, so the mask's regions are exactly
180
180
  * bash's quoted regions and the unquoted connectors are exactly bash's command boundaries. Callers
@@ -524,7 +524,19 @@ export declare function resolveOperandSpellings(base: string | undefined, operan
524
524
  * here-document, a here-string or a literal in-root stdin file; a directory change (which then makes every relative
525
525
  * stdin literal in the command unjudged). A substitution body (`$(…)`, backticks, `<(…)`) is judged on its own: green
526
526
  * under the compound walk, or exempt under this whitelist; a here-document body is text, not commands. */
527
- export declare function refusedCommandMayRead(command: string, allow: ReadonlySet<string>, boundary?: BashReadonlyRootBoundary): boolean;
527
+ export declare function refusedCommandMayRead(command: string, allow: ReadonlySet<string>, boundary: BashReadonlyRootBoundary | undefined,
528
+ /** The {@link ShellScanDecline} members THIS SEAT reads as evidence (tables and argument: `fs-bash.ts`); one
529
+ * the table omits ends the scan with "exempt". */
530
+ declinesAreEvidence: ReadonlySet<ShellScanDecline>): RefusedCommandScan;
531
+ /** What ONE scan of a refused command found — TWO facts, because a seat's table answers only the first. `mayRead`
532
+ * is the whitelist's verdict ("may read a file the boundary never judged"), which a seat turns into a MANDATE.
533
+ * `declined` is the scan's own report that it could not judge where a segment reads AT ALL
534
+ * ({@link SHELL_SCAN_DECLINES}), recorded BEFORE the seat's table and so the SAME on every seat: a seat omitting
535
+ * that decline has ruled only that an ANSWERER may retire the ask, never that nothing happened. */
536
+ export interface RefusedCommandScan {
537
+ readonly mayRead: boolean;
538
+ readonly declined: boolean;
539
+ }
528
540
  /**
529
541
  * The home directory's VARIABLE spellings, SUBSTITUTED with the declared value before a read face segments the
530
542
  * command: `$HOME` / `${HOME}` at the START of a word (an empty quote pair before it included), unquoted or
@@ -602,6 +614,10 @@ export interface OutOfRootReadGateVerdict {
602
614
  * operand is placed against a base bash has already left, so no path can be named honestly. Read it
603
615
  * as display/triage evidence; emptiness is not "not gated". */
604
616
  readonly paths: readonly string[];
617
+ /** Did the deployment's DENY judge speak about an operand this command names? The other half of the same
618
+ * question, over the very same readers, in a second pass WITH the deny judge — which is what lets the
619
+ * containment answer above stay deny-free and keep its `paths` contract. Absent ⇔ it said nothing. */
620
+ readonly denied?: true;
605
621
  }
606
622
  /** Test seam for the lockstep pin that the launcher table (the ONE launcher truth, in the program-position
607
623
  * reader's module) stays a SUBSET of the rule lane's curated `BARE_INTERPRETER_NAMES`. Exported for that one
@@ -638,10 +654,10 @@ export declare const COMMAND_LAUNCHERS_FOR_TEST: ReadonlySet<string>;
638
654
  * base across segments) and dropping it would place every later relative operand against a base bash
639
655
  * has already left. A move the walk cannot model (`pushd`/`popd`) is not silently tolerated either —
640
656
  * see the `paths`-less gated answer below.
641
- * · The CONTAINMENT half only: {@link BashReadonlyRootBoundary.denyMatch} is deliberately NOT passed
642
- * on. The deny judge outranks containment inside the walk and returns early, which would let a
643
- * deny-listed operand HIDE the fact that it also sits outside the roots — the deny demotion is
644
- * unchanged on the full face, and this one keeps answering its own question underneath it.
657
+ * · BOTH halves from ONE extraction, in TWO passes: the CONTAINMENT pass alone is deny-free, because
658
+ * {@link BashReadonlyRootBoundary.denyMatch} outranks containment inside the walk and returns early,
659
+ * which would let a deny-listed operand HIDE the fact that it also sits outside the roots. A SECOND
660
+ * pass over the SAME readers does carry that judge, and is what answers `denied` (see below).
645
661
  * · `face: "open"` is honoured: under the open read face containment is structurally satisfied, an
646
662
  * out-of-roots operand is not a finding, and this gate is silent — a full-read deployment sees no
647
663
  * gate here, by the same one mechanism the face already defines.