@sema-agent/core 7.56.0 → 7.57.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 (173) hide show
  1. package/CHANGELOG.md +541 -0
  2. package/LICENSE +1 -1
  3. package/dist/agents/org-delivery-gate.js +3 -4
  4. package/dist/agents/peer-held-queue.d.ts +69 -49
  5. package/dist/agents/peer-held-queue.js +255 -106
  6. package/dist/agents/peer-session-drain.js +63 -43
  7. package/dist/agents/send-message-tool.js +3 -2
  8. package/dist/agents/subagent.js +8 -8
  9. package/dist/brain/anthropic.js +16 -8
  10. package/dist/brain/open-responses.js +10 -8
  11. package/dist/brain/openai.js +11 -9
  12. package/dist/brain/provider-dialect.d.ts +3 -2
  13. package/dist/brain/provider-dialect.js +2 -2
  14. package/dist/brain/request-params.d.ts +6 -0
  15. package/dist/brain/request-params.js +14 -1
  16. package/dist/brain/stream-engine.d.ts +9 -0
  17. package/dist/brain/stream-engine.js +24 -10
  18. package/dist/brain/stream-shared.d.ts +5 -4
  19. package/dist/brain/stream-shared.js +4 -4
  20. package/dist/core/allow-entry.d.ts +3 -10
  21. package/dist/core/allow-entry.js +17 -25
  22. package/dist/core/approval-object.d.ts +21 -5
  23. package/dist/core/approval-object.js +44 -17
  24. package/dist/core/ask-origin.d.ts +2 -2
  25. package/dist/core/auto-compaction.d.ts +71 -6
  26. package/dist/core/auto-compaction.js +42 -26
  27. package/dist/core/background-agent-store.d.ts +6 -2
  28. package/dist/core/background-agent-store.js +1 -1
  29. package/dist/core/checkpoint-requirements.d.ts +7 -1
  30. package/dist/core/checkpoint-requirements.js +2 -0
  31. package/dist/core/checkpoint-store.d.ts +17 -29
  32. package/dist/core/checkpoint-store.js +19 -11
  33. package/dist/core/cross-boundary-taint.js +4 -2
  34. package/dist/core/engine-notice.d.ts +3 -2
  35. package/dist/core/engine-notice.js +6 -0
  36. package/dist/core/file-history-store.d.ts +14 -8
  37. package/dist/core/file-history-store.js +14 -4
  38. package/dist/core/gate-lanes.js +2 -3
  39. package/dist/core/hooks.d.ts +2 -2
  40. package/dist/core/mailbox-store.d.ts +62 -1
  41. package/dist/core/mailbox-store.js +59 -0
  42. package/dist/core/mcp-name-judge.d.ts +46 -0
  43. package/dist/core/mcp-name-judge.js +61 -0
  44. package/dist/core/mcp-probe.d.ts +1 -1
  45. package/dist/core/mcp-probe.js +2 -5
  46. package/dist/core/mcp.js +10 -30
  47. package/dist/core/memory-engine/consolidation-driver.js +4 -3
  48. package/dist/core/memory-engine/distiller.js +26 -17
  49. package/dist/core/memory-engine/layout.js +5 -2
  50. package/dist/core/park-selfcheck.js +2 -0
  51. package/dist/core/permission-rule-consent.d.ts +5 -3
  52. package/dist/core/permission-rule-consent.js +20 -15
  53. package/dist/core/permission-rule-model.d.ts +43 -2
  54. package/dist/core/permission-rule-model.js +52 -8
  55. package/dist/core/permission-rule-org.js +1 -1
  56. package/dist/core/protocol-http-auth.d.ts +16 -2
  57. package/dist/core/protocol-http-auth.js +42 -11
  58. package/dist/core/protocol-naming.d.ts +20 -0
  59. package/dist/core/protocol-naming.js +20 -3
  60. package/dist/core/read-only-shell.d.ts +20 -5
  61. package/dist/core/read-only-shell.js +29 -14
  62. package/dist/core/remote-env.d.ts +37 -1
  63. package/dist/core/remote-env.js +11 -0
  64. package/dist/core/resume-startup-cause.d.ts +24 -2
  65. package/dist/core/resume-startup-cause.js +20 -1
  66. package/dist/core/roles.js +50 -7
  67. package/dist/core/runner/compact-session.d.ts +10 -0
  68. package/dist/core/runner/compact-session.js +202 -0
  69. package/dist/core/runner/compaction-call-options.d.ts +2 -2
  70. package/dist/core/runner/compaction-seams.d.ts +21 -1
  71. package/dist/core/runner/compaction-seams.js +11 -8
  72. package/dist/core/runner/contracts.d.ts +23 -16
  73. package/dist/core/runner/derived-route-fallback.d.ts +13 -0
  74. package/dist/core/runner/derived-route-fallback.js +6 -0
  75. package/dist/core/runner/env-capability-census.d.ts +3 -2
  76. package/dist/core/runner/env-capability-census.js +9 -3
  77. package/dist/core/runner/permission-rule-lanes.d.ts +7 -7
  78. package/dist/core/runner/permission-rule-lanes.js +17 -11
  79. package/dist/core/runner/prepare-acquire-reconcile.d.ts +3 -0
  80. package/dist/core/runner/prepare-acquire-reconcile.js +2 -1
  81. package/dist/core/runner/prepare-config-doors.d.ts +6 -0
  82. package/dist/core/runner/prepare-config-doors.js +4 -3
  83. package/dist/core/runner/prepare-hands-readface.d.ts +3 -1
  84. package/dist/core/runner/prepare-hands-readface.js +2 -0
  85. package/dist/core/runner/prepare-park-ask.d.ts +2 -0
  86. package/dist/core/runner/prepare-park-ask.js +2 -2
  87. package/dist/core/runner/prepare-policy-chain.js +2 -4
  88. package/dist/core/runner/prepare-task.d.ts +4 -1
  89. package/dist/core/runner/prepare-task.js +23 -27
  90. package/dist/core/runner/prepare-tool-disclosure-mount.d.ts +13 -0
  91. package/dist/core/runner/prepare-tool-disclosure-mount.js +13 -7
  92. package/dist/core/runner/rewind-files.d.ts +2 -5
  93. package/dist/core/runner/rewind-files.js +9 -51
  94. package/dist/core/runner/run-leg.js +10 -21
  95. package/dist/core/runner/run-notification-lane.d.ts +2 -0
  96. package/dist/core/runner/run-notification-lane.js +2 -2
  97. package/dist/core/runner/run-recovery-lanes.js +3 -4
  98. package/dist/core/runner/run-settle-and-teardown.js +5 -11
  99. package/dist/core/runner/run-terminal-adoption.js +3 -4
  100. package/dist/core/runner/run-turn-boundary.js +3 -5
  101. package/dist/core/runner/runtask.d.ts +8 -1
  102. package/dist/core/runner/runtask.js +28 -34
  103. package/dist/core/runner/send-message-continuation.js +1 -1
  104. package/dist/core/runner/session-verb-doors.d.ts +28 -0
  105. package/dist/core/runner/session-verb-doors.js +48 -0
  106. package/dist/core/runner/session-write-ledger.d.ts +30 -0
  107. package/dist/core/runner/session-write-ledger.js +101 -0
  108. package/dist/core/runner/stream-settle-backstop.js +11 -14
  109. package/dist/core/runner/sub-agent-deny-lists.d.ts +25 -3
  110. package/dist/core/runner/sub-agent-deny-lists.js +21 -10
  111. package/dist/core/runner/teardown-bounded.d.ts +1 -0
  112. package/dist/core/runner/teardown-bounded.js +1 -0
  113. package/dist/core/runner-deps.d.ts +2 -2
  114. package/dist/core/secret-env.d.ts +4 -0
  115. package/dist/core/secret-env.js +3 -0
  116. package/dist/core/session-policy-store.d.ts +7 -2
  117. package/dist/core/session-policy-store.js +1 -1
  118. package/dist/core/shell-wrapper-projection.d.ts +1 -1
  119. package/dist/core/store-contracts/background-agent-store-contract.js +1 -0
  120. package/dist/core/store-contracts/checkpoint-store-contract.js +2 -1
  121. package/dist/core/store-contracts/file-history-store-contract.js +9 -0
  122. package/dist/core/store-contracts/mailbox-store-contract.js +49 -1
  123. package/dist/core/task-registry-agent.d.ts +8 -4
  124. package/dist/core/task-registry-agent.js +14 -8
  125. package/dist/core/task-registry-shared.d.ts +35 -6
  126. package/dist/core/task-registry-shared.js +11 -0
  127. package/dist/core/task-registry.d.ts +1 -1
  128. package/dist/core/task-registry.js +1 -1
  129. package/dist/core/tool-catalog-entries.d.ts +6 -0
  130. package/dist/core/tool-catalog-entries.js +1 -0
  131. package/dist/core/tool-name-aliases.d.ts +32 -2
  132. package/dist/core/tool-name-aliases.js +23 -0
  133. package/dist/core/tool-name-readings.d.ts +4 -0
  134. package/dist/core/tool-name-readings.js +1 -0
  135. package/dist/core/tool-policy.d.ts +10 -6
  136. package/dist/core/tool-policy.js +4 -7
  137. package/dist/core/trace.d.ts +4 -1
  138. package/dist/core/trace.js +2 -1
  139. package/dist/core/untrusted-egress.d.ts +20 -0
  140. package/dist/core/untrusted-egress.js +77 -3
  141. package/dist/engine/harness/agent-harness.d.ts +7 -5
  142. package/dist/engine/harness/agent-harness.js +11 -2
  143. package/dist/engine/loop/agent-loop.d.ts +1 -4
  144. package/dist/engine/loop/agent-loop.js +26 -4
  145. package/dist/index.d.ts +12 -7
  146. package/dist/index.js +7 -4
  147. package/dist/internal/harness.d.ts +2 -1
  148. package/dist/internal/harness.js +1 -0
  149. package/dist/primitives/credential-ref.d.ts +89 -0
  150. package/dist/primitives/credential-ref.js +125 -0
  151. package/dist/primitives/index.d.ts +2 -0
  152. package/dist/primitives/index.js +1 -0
  153. package/dist/stores/file/checkpoint-store.d.ts +0 -4
  154. package/dist/stores/file/checkpoint-store.js +0 -1
  155. package/dist/stores/file/file-history-store.js +12 -4
  156. package/dist/stores/file/mailbox-store.d.ts +2 -1
  157. package/dist/stores/file/mailbox-store.js +26 -1
  158. package/dist/tools/fs/bash-readonly-classifier.d.ts +15 -19
  159. package/dist/tools/fs/bash-readonly-classifier.js +5 -10
  160. package/dist/tools/fs/fs-bash.js +10 -3
  161. package/dist/tools/fs/pattern-kill-guard.d.ts +3 -2
  162. package/dist/tools/fs/pattern-kill-guard.js +68 -36
  163. package/dist/tools/fs/shell-read-check.d.ts +10 -0
  164. package/dist/tools/fs/shell-read-check.js +7 -1
  165. package/dist/tools/fs/shell-read-identity.d.ts +12 -9
  166. package/dist/tools/fs/shell-read-identity.js +9 -4
  167. package/dist/tools/web.js +9 -7
  168. package/package.json +1 -1
  169. package/test/export-surface.snapshot.json +79 -3
  170. package/wire-changes/core-stores-sql@7.57.0.json +29 -0
  171. package/wire-changes/core@7.57.0.json +1487 -0
  172. package/wire-changes/mods-kernel@7.57.0.json +29 -0
  173. package/wire-changes/primitives@7.57.0.json +245 -0
@@ -9,6 +9,7 @@ function record(over = {}) {
9
9
  owner: "task-1",
10
10
  sessionScoped: false,
11
11
  parentSessionId: "sess-1",
12
+ name: "contract-agent",
12
13
  writerId: "writer-1",
13
14
  spawnedAt: 1_000,
14
15
  updatedAt: 1_000,
@@ -308,7 +308,7 @@ export async function checkpointStoreContract(make, runAssertion, options = {})
308
308
  const stored = (await store.get(bitless.token)).pendingAction;
309
309
  assert.equal("hasBidiControls" in stored, false, "projection backfill must not write back to the row");
310
310
  });
311
- run("the approval object's members: a row's identity + face group and its targets (capabilityId/contractId/shapeDigest/implementationRevision + family/pathTarget/hostTarget/names + targets + engineMounted) survive the round-trip verbatim; an absent group stays absent", async () => {
311
+ run("the approval object's members: a row's identity + face group and its targets (capabilityId/contractId/shapeDigest/implementationRevision + family/pathTarget/hostTarget/names + targets + targetReads + engineMounted) survive the round-trip verbatim; an absent group stays absent", async () => {
312
312
  const store = make();
313
313
  const carried = createCheckpointFixture({ token: mintCheckpointToken(), sessionId: "id-carried" });
314
314
  const group = {
@@ -320,6 +320,7 @@ export async function checkpointStoreContract(make, runAssertion, options = {})
320
320
  pathTarget: { param: "destination", aliases: ["path"], access: "create", skillScopeEligible: true },
321
321
  names: ["AcmeWrite", "acme_write"],
322
322
  targets: { protective: "/srv/work/out.txt", declared: "/srv/work/out.txt" },
323
+ targetReads: { ["0123456789abcdef".repeat(4)]: "fedcba9876543210".repeat(4) },
323
324
  engineMounted: true,
324
325
  };
325
326
  Object.assign(carried.pendingAction, group);
@@ -473,9 +473,18 @@ export async function fileHistoryStoreContract(make, runAssertion) {
473
473
  throw Object.assign(new Error("source blob unreadable"), { code: "EACCES" });
474
474
  });
475
475
  assert.equal(thrown.ok, false);
476
+ if (!thrown.ok)
477
+ assert.equal(thrown.error.code, "adopt_failed", "a source whose read throws cannot supply a referenced blob");
476
478
  if (!thrown.ok)
477
479
  assertPermanenceWord(thrown.error, "importHistory whose source fetch threw");
478
480
  assert.equal(await store.canRestore("sc", "b2"), false);
481
+ const wrongBytes = await store.importHistory("sc", missingBlob, async () => new TextEncoder().encode("bytes that are not the content this address names"));
482
+ assert.equal(wrongBytes.ok, false);
483
+ if (!wrongBytes.ok)
484
+ assert.equal(wrongBytes.error.code, "adopt_failed", "a source whose bytes fail their content address cannot supply a referenced blob");
485
+ if (!wrongBytes.ok)
486
+ assertPermanenceWord(wrongBytes.error, "importHistory whose source served the wrong bytes");
487
+ assert.equal(await store.canRestore("sc", "b2"), false);
479
488
  const duplicated = wireEnvelope({
480
489
  formatVersion: "file-history/1",
481
490
  tracked: [
@@ -1,5 +1,5 @@
1
1
  import { strict as assert } from "node:assert";
2
- import { MAILBOX_INVALID_PEER_META_CODE, MAILBOX_TOMBSTONED_RECIPIENT_CODE } from "../mailbox-store.js";
2
+ import { MAILBOX_INVALID_PEER_META_CODE, MAILBOX_INVALID_DECISION_MOVE_CODE, MAILBOX_TOMBSTONED_RECIPIENT_CODE } from "../mailbox-store.js";
3
3
  import { beginContract } from "./contract-harness.js";
4
4
  import { PRINCIPAL_INBOX_HANDLE } from "../../agents/cross-session-ref.js";
5
5
  import { mintCrossPrincipalRecord } from "../../agents/org-delivery-gate.js";
@@ -183,6 +183,54 @@ export async function mailboxStoreContract(mk, runAssertion) {
183
183
  const again = await s.claimLease(S, "a1", "w2", 1_000, 20_000);
184
184
  assert.deepEqual(again.messages[0].peerMeta, { fromMode: "bypass", fromSession: "sess_1" }, "重投的记录必须是原样");
185
185
  });
186
+ run("#972b hold decision:CAS 一胜一负、负者拿到真值;不受租约围栏;每个租约快照都带它", async (make) => {
187
+ const s = make();
188
+ await s.append(S, "a1", msg("m1"));
189
+ await s.append(S, "a1", msg("m2"));
190
+ const held = await s.claimLease(S, "a1", "holder", 60_000, 10_000);
191
+ assert.equal(held.messages[0].decision, undefined, "从未 held 的记录没有 decision");
192
+ assert.deepEqual(await s.moveDecision(S, "a1", 1, undefined, "held"), { ok: true });
193
+ assert.deepEqual(await s.moveDecision(S, "a1", 1, undefined, "held"), { ok: false, current: "held" }, "同一步再走一次:负者拿到当前值");
194
+ assert.deepEqual(await s.moveDecision(S, "a1", 1, "held", "denied"), { ok: true }, "不持租约的一方也能落人的决定");
195
+ assert.deepEqual(await s.moveDecision(S, "a1", 1, "held", "approved"), { ok: false, current: "denied" }, "后到的答复输给先到的决定");
196
+ assert.deepEqual(await s.moveDecision(S, "a1", 2, "held", "approved"), { ok: false, current: "unset" }, "未 held 的记录答 unset");
197
+ const snap = await s.claimLease(S, "a1", "holder", 60_000, 11_000);
198
+ assert.deepEqual(snap.messages.map((m) => [m.seq, m.decision]), [[1, "denied"], [2, undefined]], "租约快照带每条记录的 decision");
199
+ await s.ack(S, "a1", "holder", 1);
200
+ assert.deepEqual(await s.moveDecision(S, "a1", 1, "denied", "denied").catch((e) => e.code), MAILBOX_INVALID_DECISION_MOVE_CODE, "终局不再移动(先于存在性判)");
201
+ assert.deepEqual(await s.moveDecision(S, "a1", 1, "held", "approved"), { ok: false, current: "gone" }, "ack 掉的记录答 gone");
202
+ });
203
+ run("#972b decision 律:只向前走,终局不动;坏词 / 坏 seq / 非法边先于任何副作用被拒(带码)", async (make) => {
204
+ const s = make();
205
+ await s.append(S, "a1", msg("m1"));
206
+ const codeOf = async (p) => {
207
+ try {
208
+ await p;
209
+ return undefined;
210
+ }
211
+ catch (e) {
212
+ return e.code;
213
+ }
214
+ };
215
+ for (const [label, seq, expect, next] of [
216
+ ["unset → approved", 1, undefined, "approved"],
217
+ ["unset → delivered", 1, undefined, "delivered"],
218
+ ["held → held", 1, "held", "held"],
219
+ ["held → dropped", 1, "held", "dropped"],
220
+ ["delivered → denied", 1, "delivered", "denied"],
221
+ ["denied → approved", 1, "denied", "approved"],
222
+ ["unknown word", 1, undefined, "consumed"],
223
+ ["seq 0", 0, undefined, "held"],
224
+ ["seq 1.5", 1.5, undefined, "held"],
225
+ ]) {
226
+ assert.equal(await codeOf(s.moveDecision(S, "a1", seq, expect, next)), MAILBOX_INVALID_DECISION_MOVE_CODE, label);
227
+ }
228
+ const snap = await s.claimLease(S, "a1", "w", 60_000, 10_000);
229
+ assert.equal(snap.messages[0].decision, undefined, "被拒的调用零副作用");
230
+ for (const [from, to] of [[undefined, "held"], ["held", "approved"], ["approved", "delivered"], ["delivered", "dropped"]]) {
231
+ assert.deepEqual(await s.moveDecision(S, "a1", 1, from, to), { ok: true }, `${String(from)} → ${to}`);
232
+ }
233
+ });
186
234
  await settle();
187
235
  }
188
236
  export async function mailboxAckOwnershipContract(mk, runAssertion) {
@@ -443,19 +443,23 @@ export declare function deliverToRunningAgentLane(core: DurableAgentCore, id: st
443
443
  * counterpart rows here). This variant is the SENDMESSAGE footer's source and lists EVERY running
444
444
  * agent, named or not: SendMessage addresses by name AND by id, so dropping named rows here (the way
445
445
  * {@link runningAgentFooterLane} does for the task faces, per CC `rxo`'s `!n.has(i.id)`) would hide the
446
- * very rows a retry needs. The task faces' split lives in `runningAgentFooterLane` (RB-332). */
446
+ * very rows a retry needs. The label is the name someone chose, else the description: the name an agent
447
+ * carries only because it must carry one ({@link chosenAgentName}) says nothing about what it does.
448
+ * The task faces' split lives in `runningAgentFooterLane` (RB-332). */
447
449
  export declare function runningBackgroundAgentLabelsLane(core: DurableAgentCore, access: TaskAccess): string[];
448
450
  /**
449
451
  * The not-found FOOTER's two rows, split the way the reference assembles them:
450
452
  * - `named` = CC `uG_` (220:467941-467953): the agentNameRegistry's KEYS whose task is a running local
451
453
  * agent, listed as BARE NAMES — the string a retry should pass verbatim. sema's counterpart
452
- * of "has a registry entry" is the handle carrying an explicit `name` (spawn-time label),
453
- * the same field TaskStop's name leg and SendMessage address by.
454
+ * of "has a registry entry" is the handle carrying a CHOSEN name (the spawn-time label; the
455
+ * name an agent carries only because every agent has one is not a registry entry —
456
+ * {@link chosenAgentName}).
454
457
  * - `background` = CC `rxo` (220:467937-467951): the remaining running background agents as `id (label)`
455
458
  * rows. CC filters `!n.has(i.id)` — a NAMED agent is deliberately NOT repeated here, because
456
459
  * its addressable form is the name, not the id. The pre-RB-332 sema code merged both legs
457
460
  * into one list and justified it with "named has no counterpart", a claim [c209-D] retired
458
- * when the explicit `name` field landed.
461
+ * when the explicit `name` field landed. The id stays in these rows because TaskOutput
462
+ * resolves exact ids only: an agent nobody named is the one a retry can reach by its id alone.
459
463
  * CC's `YEd` "Running teammates:" leg has no sema counterpart (no teammate registry on this face) and is
460
464
  * honestly absent rather than faked.
461
465
  */
@@ -5,7 +5,7 @@ import { canAccessAgentRecord, BackgroundAgentStoreError, clearRevivedRowTermina
5
5
  import { shutdownDebug } from "./shutdown-debug.js";
6
6
  import { delimitUntrusted } from "./untrusted-text.js";
7
7
  import { boundedRedactedSummary } from "./untrusted-egress.js";
8
- import { mintCompletionId, commitCompletionIdIfEmpty, clipTaskOutput, assertOwnership, sleepPollStep, alreadyTerminalStopNote, canAccess, normalizeAgentName, closestName, DURABLE_AGENT_HEARTBEAT_MS, DURABLE_AGENT_HANDLE_RE, BG_AGENT_REAP_STOP_ERROR, } from "./task-registry-shared.js";
8
+ import { mintCompletionId, commitCompletionIdIfEmpty, clipTaskOutput, assertOwnership, sleepPollStep, alreadyTerminalStopNote, canAccess, normalizeAgentName, engineAgentName, chosenAgentName, closestName, DURABLE_AGENT_HEARTBEAT_MS, DURABLE_AGENT_HANDLE_RE, BG_AGENT_REAP_STOP_ERROR, } from "./task-registry-shared.js";
9
9
  import { buildToolResultRef, OFFLOAD_TOOL_NAME, toolResultProvenanceOf } from "./tool-result-store.js";
10
10
  import { thrownLabel } from "./thrown-value.js";
11
11
  import { agentStateSourceOfHandle, agentStateSummary, readAgentState } from "./agent-state.js";
@@ -367,6 +367,10 @@ export function registerBackgroundAgentLane(core, input) {
367
367
  if (core.handles.has(id))
368
368
  throw new Error(`TaskRegistry: task id already registered: ${id}`);
369
369
  const now = input.now ?? Date.now();
370
+ if (input.name !== undefined && (typeof input.name !== "string" || input.name === "")) {
371
+ throw new Error(`TaskRegistry: background-agent name must be a non-empty string (got ${JSON.stringify(input.name)}); omit it to take the derived one`);
372
+ }
373
+ const name = input.name ?? engineAgentName(id);
370
374
  core.handles.set(id, {
371
375
  id,
372
376
  type: "background_agent",
@@ -375,7 +379,7 @@ export function registerBackgroundAgentLane(core, input) {
375
379
  ...(input.resolveParkedStop !== undefined ? { resolveParkedStop: input.resolveParkedStop } : {}),
376
380
  ...(input.sessionScoped ? { sessionScoped: true } : {}),
377
381
  ...(input.description !== undefined ? { description: input.description } : {}),
378
- ...(input.name !== undefined ? { name: input.name } : {}),
382
+ name,
379
383
  ...(input.agentType !== undefined ? { agentType: input.agentType } : {}),
380
384
  ...(input.parentTaskId !== undefined ? { parentTaskId: input.parentTaskId } : {}),
381
385
  ...(input.parentSessionId !== undefined ? { parentSessionId: input.parentSessionId } : {}),
@@ -396,6 +400,7 @@ export function registerBackgroundAgentLane(core, input) {
396
400
  if (input.durableSeed !== undefined) {
397
401
  const seeded = structuredClone(input.durableSeed.record);
398
402
  seeded.rev = input.durableSeed.rev;
403
+ seeded.name = name;
399
404
  handle.durable = {
400
405
  store: input.store,
401
406
  record: seeded,
@@ -417,7 +422,7 @@ export function registerBackgroundAgentLane(core, input) {
417
422
  ...(input.parentSessionId !== undefined ? { parentSessionId: input.parentSessionId } : {}),
418
423
  ...(input.rootSessionId !== undefined ? { rootSessionId: input.rootSessionId } : {}),
419
424
  ...(input.parentTaskId !== undefined ? { parentTaskId: input.parentTaskId } : {}),
420
- ...(input.name !== undefined ? { name: input.name } : {}),
425
+ name,
421
426
  ...(input.agentType !== undefined ? { agentType: input.agentType } : {}),
422
427
  ...(input.description !== undefined ? { description: input.description } : {}),
423
428
  ...(input.model !== undefined ? { model: input.model } : {}),
@@ -892,7 +897,7 @@ export function resolveBackgroundAgentByNameLane(core, name, access, opts) {
892
897
  };
893
898
  const named = [];
894
899
  for (const h of core.handles.values()) {
895
- if (h.type !== "background_agent" || h.name === undefined || !canAccess(h, access))
900
+ if (h.type !== "background_agent" || !canAccess(h, access))
896
901
  continue;
897
902
  if (h.name === name || normalizeAgentName(h.name) === key)
898
903
  named.push(h);
@@ -902,7 +907,7 @@ export function resolveBackgroundAgentByNameLane(core, name, access, opts) {
902
907
  if (namedRunning.length > 1) {
903
908
  return {
904
909
  status: "ambiguous",
905
- message: `Multiple RUNNING background agents match "${name}": ${namedRunning.map((h) => `${h.id} (${h.name ?? h.description ?? "unnamed"})`).join(", ")}. Use the task ID.`,
910
+ message: `Multiple RUNNING background agents match "${name}": ${namedRunning.map((h) => `${h.id} (${h.name})`).join(", ")}. Use the task ID.`,
906
911
  };
907
912
  }
908
913
  if (namedRunning.length === 1)
@@ -1256,7 +1261,7 @@ export function runningBackgroundAgentLabelsLane(core, access) {
1256
1261
  for (const h of core.handles.values()) {
1257
1262
  if (h.type !== "background_agent" || h.status !== "running" || !canAccess(h, access))
1258
1263
  continue;
1259
- const label = h.name ?? h.description;
1264
+ const label = chosenAgentName(h.id, h.name) ?? h.description;
1260
1265
  out.push(label ? `${h.id} (${label})` : h.id);
1261
1266
  }
1262
1267
  return out;
@@ -1267,8 +1272,9 @@ export function runningAgentFooterLane(core, access) {
1267
1272
  for (const h of core.handles.values()) {
1268
1273
  if (h.type !== "background_agent" || h.status !== "running" || !canAccess(h, access))
1269
1274
  continue;
1270
- if (h.name !== undefined && h.name !== "") {
1271
- named.push(h.name);
1275
+ const chosen = chosenAgentName(h.id, h.name);
1276
+ if (chosen !== undefined) {
1277
+ named.push(chosen);
1272
1278
  continue;
1273
1279
  }
1274
1280
  background.push(h.description ? `${h.id} (${h.description})` : h.id);
@@ -321,11 +321,12 @@ export interface BackgroundAgentTaskHandle extends SemaTaskHandle {
321
321
  reason: "not_found" | "not_running" | "no_channel";
322
322
  }) => void) | undefined)?
323
323
  ]>;
324
- /** [c209-D] the EXPLICIT addressing name (CC Agent.name parity, validated at spawn: charset regex +
325
- * "main" reserved). Distinct from `description` (the display/legacy-matching label): the name layer
326
- * resolves latest-wins ("a newer agent took the name"), the description layers keep their
327
- * running-preferred + ambiguous posture. Absent on pre-[c209] rows and unnamed spawns. */
328
- name?: string;
324
+ /** The addressing name: the spawner's explicit one (CC Agent.name parity, validated at spawn: charset
325
+ * regex + reserved set), or — when the spawn named none — the one {@link engineAgentName} derives from
326
+ * the handle, written by the registration itself, so every background agent has exactly one. Distinct
327
+ * from `description` (the display/legacy-matching label): the name layer resolves latest-wins ("a newer
328
+ * agent took the name"), the description layers keep their running-preferred + ambiguous posture. */
329
+ name: string;
329
330
  /** design/129: this child OUTLIVES its parent turn — owner is the SESSION id; the parent-teardown reap
330
331
  * skips it; `reapSessionBackground` is its terminal anchor. Explicit flag, never inferred from key values
331
332
  * (hostTaskId===sessionId deployments would otherwise be ambiguous). */
@@ -836,6 +837,32 @@ export declare const DURABLE_AGENT_HEARTBEAT_MS = 60000;
836
837
  * durable arm (a bare exact-key miss lets padded/legacy/name-shaped ids leak into live branches —
837
838
  * the server 1.250 HIGH) — one shared regex kills the drift, roster-normalize precedent. */
838
839
  export declare const DURABLE_AGENT_HANDLE_RE: RegExp;
840
+ /**
841
+ * The name a background agent carries when its spawner named none: `agent-` + the first twelve hex
842
+ * digits of its handle (`a3fa9c2d1b7e04a15` → `agent-3fa9c2d1b7e0`; a handle with fewer digits keeps
843
+ * them all). A pure function of the handle, so the same agent id reads the same name in every process
844
+ * and every store — a host that finds a row from an earlier version without a name derives it here, it
845
+ * does not invent one.
846
+ *
847
+ * Shorter than the id's sixteen digits on purpose: a name of the SHAPE of an agent id (`a`, an optional
848
+ * `<label>-`, sixteen hex) already addresses an agent directly and is refused as a name everywhere
849
+ * (`reservedPeerNameReason`), so a minted name is an ordinary admissible name — nothing in the name
850
+ * grammar singles it out. Twelve digits (48 bits) because the name layer resolves a name to the NEWEST
851
+ * agent that holds it: two agents of one caller would have to share twelve digits of their ids to collide.
852
+ *
853
+ * The registry's registration is the one place this is called (see `registerBackgroundAgentLane`); every
854
+ * reader that needs to know whether SOMEONE chose a name goes through {@link chosenAgentName}.
855
+ */
856
+ export declare function engineAgentName(handle: string): string;
857
+ /**
858
+ * The name SOMEONE chose for an agent — `undefined` when the agent's name is the one {@link engineAgentName}
859
+ * derives from its handle (or there is none). Every agent has a name to be addressed by; only a chosen one is
860
+ * also a spawn identity (the teammate key, `spawnName` on frames, a roster entry, the unnamed-child tool pool),
861
+ * and a reader that hands a row's name back into a place that reads it as an identity — a revival's `name`
862
+ * argument, a retained resume's row name — goes through here, so an agent nobody named stays unnamed in every
863
+ * later cycle. A reader that only needs to ADDRESS the agent reads `name` as it is.
864
+ */
865
+ export declare function chosenAgentName(handle: string, name: string | undefined): string | undefined;
839
866
  /** Synthetic settle error the subagent reap path passes after a stop/parent teardown. It is NOT a
840
867
  * real child error — settleBackgroundAgent never stores it (status "killed" + stoppedBy already say
841
868
  * it); exported so the subagent call sites and the registry agree on the exact sentinel. */
@@ -937,7 +964,9 @@ export interface RegisterBackgroundAgentInput extends TaskAccess {
937
964
  description?: string;
938
965
  /** [c209-D] explicit addressing name (CC Agent.name parity) — the CALLER validates it (charset +
939
966
  * reserved "main") before registration; the registry stores it verbatim. Duplicate names are legal
940
- * (latest-wins at resolution; older rows stay task_id-addressable). */
967
+ * (latest-wins at resolution; older rows stay task_id-addressable). ABSENT = the spawner named none:
968
+ * the registration gives the agent the name {@link engineAgentName} derives from its handle (this is
969
+ * the only place a name is minted), on the live handle and the durable row alike. */
941
970
  name?: string;
942
971
  /** the RESOLVED subagent type ("general-purpose"/"fork"/a roster name), persisted
943
972
  * so a REVIVE cycle's observer frames can reproduce the row's TYPE column (never the description). */
@@ -279,6 +279,17 @@ export function formatWorkflowRun(run) {
279
279
  }
280
280
  export const DURABLE_AGENT_HEARTBEAT_MS = 60_000;
281
281
  export const DURABLE_AGENT_HANDLE_RE = /^a[0-9a-f]{6,32}$/;
282
+ export function engineAgentName(handle) {
283
+ if (!DURABLE_AGENT_HANDLE_RE.test(handle)) {
284
+ throw new TypeError(`engineAgentName: ${JSON.stringify(handle)} is not an agent handle (expected "a" followed by 6-32 hex digits)`);
285
+ }
286
+ return `agent-${handle.slice(1, 13)}`;
287
+ }
288
+ export function chosenAgentName(handle, name) {
289
+ if (name === undefined)
290
+ return undefined;
291
+ return DURABLE_AGENT_HANDLE_RE.test(handle) && name === engineAgentName(handle) ? undefined : name;
292
+ }
282
293
  export const BG_AGENT_REAP_STOP_ERROR = "stopped (parent task ended or TaskStop)";
283
294
  export function normalizeAgentName(s) {
284
295
  return s
@@ -7,7 +7,7 @@ import { type BackgroundAgentStore } from "./background-agent-store.js";
7
7
  import type { TaskNotificationPayload } from "./task-notification.js";
8
8
  import type { ToolResultStore } from "./tool-result-store.js";
9
9
  import { type SemaTaskType, type StopSource, type SemaTaskStatus, type TaskRetrievalStatus, type UnifiedTaskOutput, type UnifiedTaskResult, type SemaTaskHandle, type TaskAccess, type BackgroundAgentTaskHandle, type MonitorTimers, type RegisterMonitorInput, type RegisterWorkflowInput, canAccessWorkflowRun, clipTaskOutput, MONITOR_BATCH_WINDOW_MS, MONITOR_DEFAULT_TIMEOUT_MS, MONITOR_MAX_TIMEOUT_MS, MONITOR_MAX_BATCHES_PER_MINUTE } from "./task-registry-shared.js";
10
- export { normalizeAgentName, DURABLE_AGENT_HEARTBEAT_MS, DURABLE_AGENT_HANDLE_RE, BG_AGENT_REAP_STOP_ERROR } from "./task-registry-shared.js";
10
+ export { normalizeAgentName, DURABLE_AGENT_HEARTBEAT_MS, DURABLE_AGENT_HANDLE_RE, BG_AGENT_REAP_STOP_ERROR, engineAgentName, chosenAgentName } from "./task-registry-shared.js";
11
11
  export type { ParkedClaimTicket, RegisterBackgroundAgentInput } from "./task-registry-shared.js";
12
12
  import type { RegisterBackgroundAgentInput, ParkedClaimTicket } from "./task-registry-shared.js";
13
13
  import { type AgentStateSource } from "./agent-state.js";
@@ -9,7 +9,7 @@ import { stripCwdSentinel } from "../tools/fs/fs-shared.js";
9
9
  import { registerMonitorLane, pollMonitorLane, stopMonitorLane } from "./task-registry-monitor.js";
10
10
  import { registerWorkflowLane, pollWorkflowLane, stopWorkflowLane } from "./task-registry-workflow.js";
11
11
  import { mintCompletionId, canAccessWorkflowRun, formatWorkflowRun, clipTaskOutput, assertOwnership, sleepPollStep, statusFromBackground, rollSpoolText, accountDroppedBytes, renderSpoolBody, stdoutHeadingOf, REPLAY_FACE, SPOOL_REPLAY, INCREMENT_REPLAY, spoolDropNote, droppedGapNote, alreadyTerminalStopNote, terminalTaskSummary, TASK_OUTPUT_MAX_CHARS, MONITOR_BATCH_WINDOW_MS, MONITOR_DEFAULT_TIMEOUT_MS, MONITOR_MAX_TIMEOUT_MS, MONITOR_MAX_BATCHES_PER_MINUTE, canAccess, DURABLE_AGENT_HANDLE_RE, } from "./task-registry-shared.js";
12
- export { normalizeAgentName, DURABLE_AGENT_HEARTBEAT_MS, DURABLE_AGENT_HANDLE_RE, BG_AGENT_REAP_STOP_ERROR } from "./task-registry-shared.js";
12
+ export { normalizeAgentName, DURABLE_AGENT_HEARTBEAT_MS, DURABLE_AGENT_HANDLE_RE, BG_AGENT_REAP_STOP_ERROR, engineAgentName, chosenAgentName } from "./task-registry-shared.js";
13
13
  import { TASK_OUTPUT_TOOL_NAME, TASK_STOP_TOOL_NAME, TASK_OUTPUT_MISSING_ID_MESSAGE, TASK_STOP_MISSING_ID_MESSAGE, TASK_STOP_PARAMS, resolveTaskIdArg, REGISTRY_TASK_TOOL_CAPS, composeTaskOutputDescription, composeTaskOutputParams, composeTaskStopDescription, } from "./task-tool-shape.js";
14
14
  import { durableAgentArmedLane, durableAgentRowProbeLane, beginDurableClaimLane, endDurableClaimLane, reapDurableAgentsLane, noteBackgroundAgentActivityLane, reapStaleSessionBackgroundAgentsLane, releaseDurableTranscriptAnchorLane, bindBackgroundAgentSessionLane, registerBackgroundAgentLane, recordBackgroundAgentOrgAdmissionLane, parkBackgroundAgentLane, reconcileParkedAgentsLane, claimParkedAgentLane, rollbackParkedClaimLane, consumeParkedFlipLane, finalizeParkedResumeLane, settleBackgroundAgentLane, abortBackgroundAgentsForOwnerLane, serveDurableAgentRowLane, resolveBackgroundAgentByNameLane, backgroundAgentCycleSeqLane, activeDelegationHandlesLane, markRetainedContinuationLane, reviveBackgroundAgentLane, settleRevivedAgentLane, unmarkRetainedContinuationLane, attachAgentNotifyLane, deliverToRunningAgentLane, runningBackgroundAgentLabelsLane, runningAgentFooterLane, notFoundRunningAgentsTail, pollBackgroundAgentLane, stopBackgroundAgentLane, } from "./task-registry-agent.js";
15
15
  import { mountRuleFace, toolFace } from "./tool-catalog-entries.js";
@@ -71,6 +71,12 @@ export declare const isEngineShellFace: (face: {
71
71
  readonly engineMounted?: true;
72
72
  readonly contractId: string;
73
73
  } | undefined) => boolean;
74
+ /** Is this call's face one of the content-routing tools the engine mounts (question, answer, blocked report)? They route content,
75
+ * act on nothing, and no stored rule governs them: the persisted-rule lane does not judge them and their card offers no rule. */
76
+ export declare const routesContent: (face: {
77
+ readonly engineMounted?: true;
78
+ readonly contractId: string;
79
+ } | undefined) => boolean;
74
80
  /**
75
81
  * The rule face a definition whose schema is BUILT PER MOUNT puts on its instance: its row's primaries only (none ⇒ no rule
76
82
  * face). The row's `params` is the static view's vocabulary, the fullest mount's; a mount's own is read off the schema it
@@ -207,6 +207,7 @@ export function engineMountedAs(ids) {
207
207
  return (face) => face?.engineMounted === true && contracts.has(face.contractId);
208
208
  }
209
209
  export const isEngineShellFace = engineMountedAs(["bash", "bash-readonly"]);
210
+ export const routesContent = engineMountedAs(["ask-user-question", "structured-output", "report-blocked"]);
210
211
  export function mountRuleFace(id) {
211
212
  const primaryParams = toolFace(id).ruleFace?.primaryParams;
212
213
  return primaryParams === undefined ? undefined : { primaryParams: [...primaryParams] };
@@ -16,8 +16,8 @@ import type { RuleBehavior } from "./permission-rule-model.js";
16
16
  * with the successor, where any other unknown name stays the generic roster miss.
17
17
  *
18
18
  * What REMAINS is validation data: {@link RETIRED_TOOL_NAMES} maps every name the engine ever
19
- * retired to a one-line guidance string, and {@link judgeRuleToolName} is the ONE reading of it a rule's
20
- * tool name gets. They exist because deleting the mapping flips stale POLICY entries from "silently
19
+ * retired to a one-line guidance string, and {@link judgeRuleToolName} is the ONE judgment of it a rule's
20
+ * tool name gets ({@link toolNameFaultOf} adds the near-miss step: the one reading). They exist because deleting the mapping flips stale POLICY entries from "silently
21
21
  * matched" to "silently matches nothing" — and for a deny/requireApproval entry that direction WIDENS
22
22
  * the surface. `validatePermissionRules` (permission-rules.ts) does NOT classify retired names itself:
23
23
  * which names a deployment mounts its OWN tool under is a roster fact, and the judgment takes the roster.
@@ -100,6 +100,36 @@ export declare function grantWildcardRefusalOf(name: string): string | undefined
100
100
  * as a glob ({@link readsNameGlob}) — such a name outside every namespace, or with a glob in its peer segment, is a pattern and
101
101
  * `current`; with `allow` or none a wildcard peer names no peer and is `unmintable_segment`. */
102
102
  export declare function judgeRuleToolName(name: string, roster: ReadonlySet<string>, behavior?: RuleBehavior): RuleToolNameVerdict;
103
+ /**
104
+ * THE near-miss reading of a tool name the judgment calls current: the spelling the roster carries when it carries no tool of this
105
+ * exact name but one that differs from it only by case (`BASH` beside `Bash`), else `undefined`. Two kinds of name are never a near
106
+ * miss, because the roster they would be read against is not the final one: an engine tool's (the engine mounts some of its tools
107
+ * after a list is read) and a protocol tool's (a refresh mounts and unmounts them while the run goes on) — in both the exact
108
+ * spelling can be the right one by the time it matters. Every station that asks "is this name a case twin of a carried tool" reads
109
+ * this answer: an entry list and the persisted grammar's whole-tool rule through {@link toolNameFaultOf}, and a tool policy's name
110
+ * lists (the prepare-time policy-name audit, which reads two rosters and skips an inherited layer) directly.
111
+ */
112
+ export declare function nearMissSpellingOf(name: string, roster: ReadonlySet<string>): string | undefined;
113
+ /** What {@link toolNameFaultOf} finds wrong with a rule's tool name: the judgment's stale words (`tool_name`, with the verdict and
114
+ * its facts), or another spelling of a carried tool (`near_miss`, the spelling to write). */
115
+ export type ToolNameFault = {
116
+ readonly reason: "tool_name";
117
+ readonly verdict: Exclude<RuleToolNameVerdict["kind"], "current">;
118
+ readonly guidance?: string;
119
+ readonly currentName?: string;
120
+ } | {
121
+ readonly reason: "near_miss";
122
+ readonly currentName: string;
123
+ };
124
+ /**
125
+ * THE ONE reading of a rule's tool name against a roster — the name judgment's stale words, then, for a name the judgment calls
126
+ * current but the roster does not carry exactly, whether the roster carries it under another case ({@link nearMissSpellingOf}).
127
+ * `undefined` = no fault: the name is carried, or nothing carried is another spelling of it. A name the roster simply does not
128
+ * carry is silent: a tool of that name may be mounted later, and a list shared across runs is not wrong because one run lacks a
129
+ * tool. A list entry (`readRuleEntry`) and a stored whole-tool rule (`parseRuleText`) read a name through this and nothing else;
130
+ * the policy-name audit composes the same two steps itself over two rosters (a candidate to fold in).
131
+ */
132
+ export declare function toolNameFaultOf(name: string, roster: ReadonlySet<string>, behavior: RuleBehavior | undefined): ToolNameFault | undefined;
103
133
  /** The two kinds of tool-name miss — a closed set. `unknown`: the name is none the engine ever mounted (a typo, a
104
134
  * guess). `expected-absent`: the name is one the engine RETIRED ({@link RETIRED_TOOL_NAMES}), so its absence is no
105
135
  * surprise and the table says what replaced it (the reference spells the same two words for its render-time miss). */
@@ -1,5 +1,6 @@
1
1
  import { carriesNamespaceSeparator, protocolOf } from "./protocol-table.js";
2
2
  import { mintNamespacePrefix } from "./protocol-naming.js";
3
+ import { catalogToolNames } from "./tool-registry.js";
3
4
  export const RETIRED_TOOL_NAMES = new Map([
4
5
  ["read_file", 'renamed "Read"'],
5
6
  ["edit_file", 'renamed "Edit"'],
@@ -98,6 +99,28 @@ export function judgeRuleToolName(name, roster, behavior) {
98
99
  return carriesNamespaceSeparator(name) && !(readsNameGlob(behavior) && name.includes("*")) ? UNPREFIXED : CURRENT;
99
100
  return unmintableSegmentOf(split, behavior) ?? CURRENT;
100
101
  }
102
+ function mountedSpellingOf(name, roster) {
103
+ const folded = name.toLowerCase();
104
+ for (const mounted of roster)
105
+ if (mounted.toLowerCase() === folded)
106
+ return mounted;
107
+ return undefined;
108
+ }
109
+ export function nearMissSpellingOf(name, roster) {
110
+ if (roster.has(name) || catalogToolNames().has(name) || splitNamespacedName(name) !== undefined)
111
+ return undefined;
112
+ return mountedSpellingOf(name, roster);
113
+ }
114
+ export function toolNameFaultOf(name, roster, behavior) {
115
+ const verdict = judgeRuleToolName(name, roster, behavior);
116
+ if (verdict.kind === "unprefixed_namespace")
117
+ return { reason: "tool_name", verdict: verdict.kind };
118
+ if (verdict.kind !== "current") {
119
+ return { reason: "tool_name", verdict: verdict.kind, guidance: verdict.guidance, ...(verdict.currentName !== undefined ? { currentName: verdict.currentName } : {}) };
120
+ }
121
+ const mounted = nearMissSpellingOf(name, roster);
122
+ return mounted === undefined ? undefined : { reason: "near_miss", currentName: mounted };
123
+ }
101
124
  export const TOOL_MISS_KINDS = Object.freeze(["unknown", "expected-absent"]);
102
125
  const [UNKNOWN_MISS, ABSENT_MISS] = TOOL_MISS_KINDS;
103
126
  export function judgeToolNameMiss(name, roster) {
@@ -127,6 +127,10 @@ export declare const NAME_READINGS: {
127
127
  readonly kind: "any";
128
128
  readonly file: string;
129
129
  };
130
+ readonly "policy.persistedToolRule": {
131
+ readonly kind: "any";
132
+ readonly file: string;
133
+ };
130
134
  readonly "policy.sensitivePathGuardedByName": {
131
135
  readonly kind: "any";
132
136
  readonly file: string;
@@ -33,6 +33,7 @@ export const NAME_READINGS = {
33
33
  "gate.skillActivationSeat": wire(GATE),
34
34
  "policy.permissionRules": any("src/core/permission-rules.ts"),
35
35
  "policy.persistedGrammar": any("src/core/permission-rule-model.ts"),
36
+ "policy.persistedToolRule": any("src/core/permission-rule-model.ts"),
36
37
  "policy.sensitivePathGuardedByName": any("src/core/sensitive-path-policy.ts"),
37
38
  "floor.protectivePathTarget": seat("member", "src/core/tool-registry.ts"),
38
39
  "floor.protectiveHostTarget": seat("member", "src/core/host-target.ts"),
@@ -1125,7 +1125,8 @@ export declare function createUnverifiableDeletePolicy(opts?: {
1125
1125
  export declare function createTranscriptIntegrityPolicy(opts?: {
1126
1126
  /** Transcript directories to protect. Default: `($AGENT_DATA_DIR ?? ~/.ai-agent)/sessions`. */
1127
1127
  sessionsDirs?: readonly string[];
1128
- /** argv[0] allowlist for read-only access to the transcript dir (default {@link BASH_READONLY_DEFAULT_ALLOW}). */
1128
+ /** The programs a read-only command over the transcript dir may run (default {@link BASH_READONLY_DEFAULT_ALLOW}); it only
1129
+ * narrows the read-only reader — a listed program whose call writes still asks. */
1129
1130
  readAllow?: readonly string[];
1130
1131
  /** Override the shell tool names this gate parses (default {@link COARSE_SHELL_TOOLS}, the shell face). */
1131
1132
  tools?: string[];
@@ -1328,13 +1329,16 @@ export interface AskRequest {
1328
1329
  * command whose exact form the validator refuses offers nothing rather than promoting a lone
1329
1330
  * prefix to index 0 — see `suggestRulesForCommand`). The seat says "this card has no rule to
1330
1331
  * offer you", which is true of both; it does not claim the grammar is empty, and a surface must
1331
- * not word it as though no rule could ever be written for the command.
1332
+ * not word it as though no rule could ever be written for the command. It is also the word of a
1333
+ * card whose tool's form mints no offer: a path tool's, a shell-face sibling's, and a tool that
1334
+ * answers to one of those under an alias.
1332
1335
  *
1333
1336
  * ABSENT is not a claim, exactly as {@link previewWithheld}'s absence is not: it also covers the
1334
- * ordinary "there are offers" case and the three STRUCTURAL doors that answer with silence on
1335
- * purpose (no rule lane wired, a tool the lane does not speak for, a task that cannot hold a rule at
1336
- * all) — a reason line on every ask of a deployment that never had the feature would be noise about
1337
- * a lane that does not exist. Read presence, never absence.
1337
+ * ordinary "there are offers" case and the STRUCTURAL door that answers with silence on purpose — no
1338
+ * rule speaks for this call: no rule lane wired, a task that cannot hold a rule at all, or a call the
1339
+ * lane does not judge (a content-routing tool the engine mounts) — a reason line on every ask of a
1340
+ * deployment that never had the feature would be noise about a lane that does not exist. Read
1341
+ * presence, never absence.
1338
1342
  *
1339
1343
  * ADVISORY disclosure metadata, never adjudication input: it changes nothing about what is allowed,
1340
1344
  * and a surface that ignores it renders exactly the card it rendered before. The durable park row
@@ -12,6 +12,7 @@ export { policyCannotAnswer, rowDemandsAPerson } from "./real-approval.js";
12
12
  import { sanitizeAutoModeArmingRecipe } from "./auto-mode-arming.js";
13
13
  import { join, normalize as normalizePath, sep } from "node:path";
14
14
  import { BASH_READONLY_DEFAULT_ALLOW, parseLeadingCommandName } from "../tools/fs/index.js";
15
+ import { readOnlyShellVerdict } from "./read-only-shell.js";
15
16
  import { protectivePathTargetOf, shellFaceToolNames } from "./tool-registry.js";
16
17
  import { heldBy } from "./tool-name-readings.js";
17
18
  import { bindableValueOf, boundInputHashOf, describeBindingFault, exactBoundInputHashOf } from "./canonical-json.js";
@@ -830,14 +831,10 @@ export function createTranscriptIntegrityPolicy(opts) {
830
831
  const cwdInsideProtected = req.cwd !== undefined && inProtectedDir(req.cwd);
831
832
  if (!referencesProtectedDir(cmd) && !cwdInsideProtected)
832
833
  return ALLOW;
833
- const parsed = parseLeadingCommandName(command);
834
- if ("name" in parsed && readAllow.has(parsed.name))
834
+ const verdict = readOnlyShellVerdict(command, { cwd: req.cwd, readers: readAllow });
835
+ if (verdict.readOnly)
835
836
  return ALLOW;
836
- return askReason(req, "name" in parsed
837
- ? `\`${parsed.name}\` is not a read-only command${cwdInsideProtected ? " and the shell's working directory is inside the transcript directory" : ""}`
838
- : cwdInsideProtected
839
- ? "the shell's working directory is inside the transcript directory and this is not a single read-only command (fail-closed)"
840
- : "the command references the transcript directory and is not a single read-only command (fail-closed)");
837
+ return askReason(req, `${cwdInsideProtected ? "the shell's working directory is inside the transcript directory" : "the command references the transcript directory"} and is not read-only: ${verdict.reason}`);
841
838
  }
842
839
  const writeTarget = protectivePathTargetOf(req);
843
840
  if (writeTarget !== undefined && writeTarget.access !== "read") {
@@ -984,5 +984,8 @@ export type TraceEvent = {
984
984
  * await it; the default (absent) hook is a no-op the JIT can elide. Must not throw or do sync I/O.
985
985
  */
986
986
  export type TracerHook = (e: TraceEvent) => void;
987
- /** Invoke a tracer without ever letting it break the task (absent → no-op; throws swallowed). */
987
+ /** Invoke a tracer without ever letting it break the task (absent → no-op; throws swallowed). A frame is the engine's own account: it
988
+ * leaves minted HERE, the one throat — every string leaf through the same-principal tier's text pass, member names as written
989
+ * (`egressAccount`) — so a frame that carries a host's, a store's or a remote's words (a thrown text, a failure reason) needs no
990
+ * remembering at its mint site. */
988
991
  export declare function emitTrace(tracer: TracerHook | undefined, build: () => TraceEvent): void;
@@ -1,8 +1,9 @@
1
+ import { egressAccount } from "./untrusted-egress.js";
1
2
  export function emitTrace(tracer, build) {
2
3
  if (!tracer)
3
4
  return;
4
5
  try {
5
- tracer(build());
6
+ tracer(egressAccount("same-principal", build()));
6
7
  }
7
8
  catch {
8
9
  }
@@ -60,6 +60,26 @@ export declare function redactHostLeaks(s: string, report?: RedactionReport): Eg
60
60
  * - cycles and shared members stay shared on the snapshot.
61
61
  */
62
62
  export declare function egressValue<T extends EgressTier, V>(tier: T, value: V): V;
63
+ /**
64
+ * Tier `tier`'s pass over an engine ACCOUNT value — a notice's detail, a trace frame: data the engine shaped, carrying words that may have
65
+ * come from a host, a store or a remote. Returned as a new value, never the one handed in. The rules, one each:
66
+ * - a string, at any depth, goes through {@link egressText};
67
+ * - a member NAME of plain data is the engine's own closed word and is kept as written (a detail's `key` or `tokenCount` is not a
68
+ * credential name, and reading it as one would blank the engine's own facts);
69
+ * - plain data is an array or an object whose prototype is `Object.prototype` or `null`, and is not a proxy (whose traps answer what
70
+ * they choose, a fresh value at every read included); its own enumerable members are read once, a function member is code and is
71
+ * not carried;
72
+ * - any other object (an error, a class instance, a `Map`, a `Date`, a proxy) came from elsewhere and leaves as
73
+ * {@link egressValue} reads it — there, and only there, a credential-named member's string is the marker whole, and there, and
74
+ * only there, a member bound applies (one per such value);
75
+ * - no member bound applies to plain data: the account is the engine's own, and its size is the engine's to answer for;
76
+ * - one walk: an object reached twice — through the data and through a value from elsewhere included — is read once and answers one
77
+ * snapshot (cycles and shared members stay shared); past {@link VALUE_DEPTH_MAX} the marker stands in.
78
+ * Every pass it runs is a fixed point, so an account value minted twice reads the same.
79
+ */
80
+ export declare function egressAccount<T extends EgressTier, V>(tier: T, value: V): V;
81
+ /** Where the markers sit in `s`, as [start, end) spans, leftmost first. */
82
+ export declare function redactionMarkerSpans(s: string): Array<[number, number]>;
63
83
  /** The `same-principal` tier's pass + size-bound (NO injection fence): workflow outputs/results/errors riding back to
64
84
  * the launching model/script, run rows behind scope-gated observe. Paths/URLs survive (the assertion pattern depends on
65
85
  * them); secrets never do. */