@sema-agent/core 7.53.0 → 7.54.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (249) hide show
  1. package/CHANGELOG.md +291 -1
  2. package/LICENSE +3 -1
  3. package/dist/agents/peer-session-drain.d.ts +0 -1
  4. package/dist/agents/peer-session-drain.js +0 -2
  5. package/dist/agents/send-message-tool.js +3 -4
  6. package/dist/agents/subagent.d.ts +3 -5
  7. package/dist/agents/subagent.js +18 -11
  8. package/dist/agents/tool-filter.d.ts +7 -4
  9. package/dist/agents/tool-filter.js +7 -3
  10. package/dist/core/a2a.js +4 -2
  11. package/dist/core/allow-entry.d.ts +1 -0
  12. package/dist/core/allow-entry.js +1 -0
  13. package/dist/core/approval-object.d.ts +87 -0
  14. package/dist/core/approval-object.js +85 -0
  15. package/dist/core/arg-summary.d.ts +3 -2
  16. package/dist/core/arg-summary.js +17 -11
  17. package/dist/core/ask-origin.d.ts +43 -27
  18. package/dist/core/ask-origin.js +20 -11
  19. package/dist/core/ask-question.d.ts +0 -1
  20. package/dist/core/ask-question.js +4 -67
  21. package/dist/core/ask-unresolvable-notice.d.ts +3 -2
  22. package/dist/core/auto-compaction.d.ts +49 -50
  23. package/dist/core/auto-compaction.js +41 -51
  24. package/dist/core/auto-mode-prompt.d.ts +4 -2
  25. package/dist/core/auto-mode-prompt.js +4 -3
  26. package/dist/core/canonical-json.d.ts +69 -0
  27. package/dist/core/canonical-json.js +130 -8
  28. package/dist/core/checkpoint-requirements.d.ts +13 -7
  29. package/dist/core/checkpoint-requirements.js +4 -1
  30. package/dist/core/checkpoint-store.d.ts +33 -18
  31. package/dist/core/checkpoint-store.js +5 -4
  32. package/dist/core/child-clamp-seats.d.ts +45 -6
  33. package/dist/core/child-clamp-seats.js +56 -4
  34. package/dist/core/consolidate-scope.d.ts +1 -1
  35. package/dist/core/cross-boundary-taint.d.ts +213 -0
  36. package/dist/core/cross-boundary-taint.js +196 -0
  37. package/dist/core/decision-receipt.d.ts +44 -1
  38. package/dist/core/decision-receipt.js +16 -0
  39. package/dist/core/effective-path-target.d.ts +3 -0
  40. package/dist/core/effective-path-target.js +4 -0
  41. package/dist/core/engine-notice.d.ts +34 -2
  42. package/dist/core/engine-notice.js +17 -1
  43. package/dist/core/engine-storage-backend.d.ts +73 -0
  44. package/dist/core/engine-storage-backend.js +1 -0
  45. package/dist/core/file-history-store.d.ts +6 -2
  46. package/dist/core/file-history-store.js +1 -0
  47. package/dist/core/gate-fold.js +10 -2
  48. package/dist/core/gate-lanes.js +30 -21
  49. package/dist/core/gate-outcome.d.ts +7 -3
  50. package/dist/core/governance-codes.d.ts +7 -2
  51. package/dist/core/governance-codes.js +6 -0
  52. package/dist/core/hooks.d.ts +14 -14
  53. package/dist/core/hooks.js +1 -2
  54. package/dist/core/locked-config.d.ts +28 -1
  55. package/dist/core/locked-config.js +30 -3
  56. package/dist/core/lsp.d.ts +1 -1
  57. package/dist/core/mcp-server-spec.d.ts +66 -9
  58. package/dist/core/mcp-server-spec.js +52 -19
  59. package/dist/core/mcp.js +5 -4
  60. package/dist/core/memory-engine/engine.js +2 -1
  61. package/dist/core/memory-engine/export-bundle.d.ts +1 -1
  62. package/dist/core/memory-engine/export-bundle.js +1 -1
  63. package/dist/core/memory-engine/file-backend.d.ts +2 -6
  64. package/dist/core/memory-engine/file-backend.js +1 -12
  65. package/dist/core/memory.d.ts +3 -3
  66. package/dist/core/mode-policy.d.ts +13 -8
  67. package/dist/core/mode-policy.js +3 -2
  68. package/dist/core/mods/kernel/receive.d.ts +2 -1
  69. package/dist/core/mods/kernel/receive.js +7 -2
  70. package/dist/core/parent-spec-seat.d.ts +10 -13
  71. package/dist/core/parent-spec-seat.js +14 -6
  72. package/dist/core/pause-registry.d.ts +35 -1
  73. package/dist/core/pause-registry.js +10 -0
  74. package/dist/core/permission-rule-consent.d.ts +18 -5
  75. package/dist/core/permission-rule-consent.js +13 -8
  76. package/dist/core/permission-rule-model.d.ts +29 -6
  77. package/dist/core/permission-rule-model.js +6 -1
  78. package/dist/core/permission-rule-org.d.ts +37 -0
  79. package/dist/core/permission-rule-org.js +34 -42
  80. package/dist/core/permission-rule-session.js +2 -2
  81. package/dist/core/permission-rule-store.d.ts +8 -0
  82. package/dist/core/permission-rule-store.js +1 -0
  83. package/dist/core/permission-rule-sync.js +3 -2
  84. package/dist/core/read-only-shell.d.ts +1 -1
  85. package/dist/core/read-only-shell.js +30 -9
  86. package/dist/core/remote-env.d.ts +41 -1
  87. package/dist/core/remote-env.js +18 -0
  88. package/dist/core/resume-startup-cause.d.ts +18 -0
  89. package/dist/core/resume-startup-cause.js +12 -0
  90. package/dist/core/runner/approval-feedback.d.ts +46 -0
  91. package/dist/core/runner/approval-feedback.js +41 -0
  92. package/dist/core/runner/compaction-call-options.d.ts +3 -3
  93. package/dist/core/runner/compaction-call-options.js +2 -2
  94. package/dist/core/runner/contracts.d.ts +33 -41
  95. package/dist/core/runner/cross-boundary-lane.d.ts +87 -0
  96. package/dist/core/runner/cross-boundary-lane.js +190 -0
  97. package/dist/core/runner/decide-continuation.d.ts +4 -4
  98. package/dist/core/runner/decide-continuation.js +3 -1
  99. package/dist/core/runner/gate-exit.d.ts +32 -12
  100. package/dist/core/runner/gate-exit.js +75 -33
  101. package/dist/core/runner/inherited-ask-grants.js +2 -2
  102. package/dist/core/runner/person-path-rows.d.ts +2 -2
  103. package/dist/core/runner/prepare-acquire-reconcile.d.ts +7 -9
  104. package/dist/core/runner/prepare-acquire-reconcile.js +8 -6
  105. package/dist/core/runner/prepare-ask-lane.js +3 -3
  106. package/dist/core/runner/prepare-caps-and-workflow.d.ts +6 -5
  107. package/dist/core/runner/prepare-caps-and-workflow.js +3 -3
  108. package/dist/core/runner/prepare-config-doors.d.ts +12 -15
  109. package/dist/core/runner/prepare-config-doors.js +8 -3
  110. package/dist/core/runner/prepare-context-lane.js +6 -3
  111. package/dist/core/runner/prepare-delegation-surface.d.ts +3 -2
  112. package/dist/core/runner/prepare-delegation-surface.js +2 -3
  113. package/dist/core/runner/prepare-execution-env.d.ts +1 -2
  114. package/dist/core/runner/prepare-execution-env.js +2 -1
  115. package/dist/core/runner/prepare-gate-stations.d.ts +2 -0
  116. package/dist/core/runner/prepare-gate-stations.js +4 -3
  117. package/dist/core/runner/prepare-hands-readface.d.ts +2 -2
  118. package/dist/core/runner/prepare-hands-readface.js +1 -1
  119. package/dist/core/runner/prepare-inherited-gate.d.ts +2 -0
  120. package/dist/core/runner/prepare-inherited-gate.js +1 -0
  121. package/dist/core/runner/prepare-park-ask.d.ts +1 -1
  122. package/dist/core/runner/prepare-park-ask.js +9 -3
  123. package/dist/core/runner/prepare-policy-chain.js +2 -7
  124. package/dist/core/runner/prepare-safety-scan.d.ts +7 -2
  125. package/dist/core/runner/prepare-safety-scan.js +34 -27
  126. package/dist/core/runner/prepare-suspend-saga.d.ts +2 -2
  127. package/dist/core/runner/prepare-suspend-saga.js +6 -2
  128. package/dist/core/runner/prepare-task.js +13 -10
  129. package/dist/core/runner/prepare-turn-wiring.d.ts +10 -5
  130. package/dist/core/runner/prepare-turn-wiring.js +14 -49
  131. package/dist/core/runner/prepare-wiring-manifest.d.ts +2 -2
  132. package/dist/core/runner/prepare-wiring-manifest.js +2 -2
  133. package/dist/core/runner/prepare-workspace-restore.js +2 -2
  134. package/dist/core/runner/resume-admission.js +9 -9
  135. package/dist/core/runner/resume-apply.js +4 -4
  136. package/dist/core/runner/resume-policy-outcome.d.ts +2 -2
  137. package/dist/core/runner/resume-policy-outcome.js +18 -15
  138. package/dist/core/runner/resume-preflight.js +2 -2
  139. package/dist/core/runner/resume-review-outcome.js +3 -3
  140. package/dist/core/runner/run-leg.js +57 -33
  141. package/dist/core/runner/run-settle-and-teardown.js +2 -0
  142. package/dist/core/runner/run-terminal-adoption.js +15 -4
  143. package/dist/core/runner/session-file-state-replay.d.ts +5 -2
  144. package/dist/core/runner/session-file-state-replay.js +3 -2
  145. package/dist/core/runner/stream-steer-verb.js +2 -2
  146. package/dist/core/runner/tool-face-overlay.d.ts +24 -6
  147. package/dist/core/runner/tool-face-overlay.js +24 -15
  148. package/dist/core/runner-deps.d.ts +9 -9
  149. package/dist/core/session-store.d.ts +35 -16
  150. package/dist/core/session-store.js +20 -12
  151. package/dist/core/{shell-lexer.d.ts → shell-run-candidates.d.ts} +36 -12
  152. package/dist/core/{shell-lexer.js → shell-run-candidates.js} +145 -225
  153. package/dist/core/shell-wrapper-table.d.ts +5 -3
  154. package/dist/core/shell-wrapper-table.js +1 -1
  155. package/dist/core/store-contracts/checkpoint-store-contract.js +2 -2
  156. package/dist/core/store-contracts/session-store-contract.d.ts +49 -0
  157. package/dist/core/store-contracts/session-store-contract.js +683 -0
  158. package/dist/core/store-contracts/session-store-incarnation-contract.d.ts +12 -0
  159. package/dist/core/store-contracts/session-store-incarnation-contract.js +5 -2
  160. package/dist/core/task-event.d.ts +6 -4
  161. package/dist/core/task-registry-agent.js +1 -0
  162. package/dist/core/task-registry-shared.d.ts +5 -0
  163. package/dist/core/task-registry.d.ts +7 -0
  164. package/dist/core/task-registry.js +17 -0
  165. package/dist/core/task-result.d.ts +1 -1
  166. package/dist/core/task-spec.d.ts +6 -6
  167. package/dist/core/tighten-task-spec.d.ts +6 -4
  168. package/dist/core/tighten-task-spec.js +2 -2
  169. package/dist/core/tool-catalog-entries.d.ts +1 -1
  170. package/dist/core/tool-catalog-entries.js +52 -52
  171. package/dist/core/tool-face-reader.d.ts +49 -0
  172. package/dist/core/tool-face-reader.js +94 -0
  173. package/dist/core/tool-face.d.ts +17 -8
  174. package/dist/core/tool-face.js +1 -0
  175. package/dist/core/tool-policy.d.ts +64 -23
  176. package/dist/core/tool-policy.js +62 -56
  177. package/dist/core/tool-roster.d.ts +46 -28
  178. package/dist/core/tool-roster.js +91 -14
  179. package/dist/core/tool-spec-members.js +2 -2
  180. package/dist/core/tool-spec.d.ts +20 -4
  181. package/dist/core/tools.js +2 -0
  182. package/dist/core/trace.d.ts +24 -2
  183. package/dist/core/types.d.ts +2 -2
  184. package/dist/core/types.js +1 -1
  185. package/dist/core/untrusted-text.d.ts +7 -0
  186. package/dist/core/untrusted-text.js +3 -8
  187. package/dist/core/user-attachment.d.ts +43 -0
  188. package/dist/core/user-attachment.js +92 -15
  189. package/dist/engine/compaction/compaction.d.ts +2 -2
  190. package/dist/engine/compaction/utils.d.ts +2 -2
  191. package/dist/engine/harness/types.d.ts +7 -4
  192. package/dist/engine/llm/types.d.ts +7 -2
  193. package/dist/engine/llm/types.js +2 -1
  194. package/dist/engine/session/parent-chain-guard.d.ts +10 -0
  195. package/dist/engine/session/parent-chain-guard.js +10 -0
  196. package/dist/engine/session/repo-utils.d.ts +5 -0
  197. package/dist/engine/session/repo-utils.js +9 -0
  198. package/dist/engine/session/storage-base.js +5 -2
  199. package/dist/index.d.ts +23 -9
  200. package/dist/index.js +19 -8
  201. package/dist/internal/harness.d.ts +1 -0
  202. package/dist/internal/harness.js +1 -0
  203. package/dist/internal/llm.d.ts +1 -1
  204. package/dist/internal/llm.js +1 -1
  205. package/dist/orchestration/run-workflow-tool.d.ts +3 -0
  206. package/dist/orchestration/run-workflow-tool.js +1 -0
  207. package/dist/orchestration/workflow-governance.d.ts +1 -1
  208. package/dist/orchestration/workflow.d.ts +3 -0
  209. package/dist/orchestration/workflow.js +2 -1
  210. package/dist/prompt-assembly/catalog/schema.d.ts +3 -0
  211. package/dist/prompt-assembly/catalog/schema.js +3 -0
  212. package/dist/prompt-assembly/overlay.js +2 -2
  213. package/dist/prompt-assembly/select.js +2 -2
  214. package/dist/prompts/default.d.ts +1 -1
  215. package/dist/shell/lex.d.ts +87 -8
  216. package/dist/shell/lex.js +283 -49
  217. package/dist/stores/file/index.d.ts +8 -6
  218. package/dist/tools/artifact/artifact-tool.js +10 -0
  219. package/dist/tools/fs/bash-program-position.d.ts +3 -15
  220. package/dist/tools/fs/bash-program-position.js +3 -10
  221. package/dist/tools/fs/bash-readonly-classifier.js +2 -0
  222. package/dist/tools/fs/fs-bash.d.ts +3 -3
  223. package/dist/tools/fs/fs-bash.js +72 -101
  224. package/dist/tools/fs/fs-read.d.ts +12 -0
  225. package/dist/tools/fs/fs-read.js +10 -9
  226. package/dist/tools/fs/fs-shared.d.ts +27 -31
  227. package/dist/tools/fs/fs-shared.js +12 -15
  228. package/dist/tools/fs/fs-write.js +1 -1
  229. package/dist/tools/fs/index.js +2 -2
  230. package/dist/tools/fs/read-deny-home.d.ts +1 -1
  231. package/dist/tools/fs/read-deny-home.js +5 -4
  232. package/dist/tools/fs/read-deny.d.ts +16 -3
  233. package/dist/tools/fs/read-deny.js +12 -9
  234. package/dist/tools/fs/safety.d.ts +7 -6
  235. package/dist/tools/fs/shell-read-bindings.d.ts +5 -3
  236. package/dist/tools/fs/shell-read-bindings.js +15 -16
  237. package/dist/tools/fs/shell-read-device.js +1 -1
  238. package/dist/tools/fs/shell-read-plan.js +36 -47
  239. package/dist/tools/fs/shell-read-word.d.ts +1 -6
  240. package/dist/tools/fs/shell-read-word.js +2 -20
  241. package/dist/tools/fs/shell-reentry-text.js +1 -1
  242. package/dist/tools/web.d.ts +2 -2
  243. package/package.json +4 -1
  244. package/test/export-surface.snapshot.json +5654 -5520
  245. package/wire-changes/core@7.53.0.json +30 -8
  246. package/wire-changes/core@7.54.0.json +2870 -0
  247. package/wire-changes/core@7.54.1.json +37 -0
  248. package/dist/core/shell-scan.d.ts +0 -60
  249. package/dist/core/shell-scan.js +0 -183
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * The resume ladder's APPROVAL-LANE capture and binding rung (E4), verbatim from
3
3
  * `Runner.resumeStream`: on a human / irreversible_ask row every deployment-supplied field of the
4
- * `policy_ask` decide is read exactly once into a plain twin (answer canonicalized, updatedInput cloned to the
5
- * JSON domain, the decision word, the refused keys, the host's decision facts and attribution, the deny note),
4
+ * `policy_ask` decide is read exactly once into a plain twin (answer and updatedInput by the one capture, in their
5
+ * JSON form, the decision word, the refused keys, the host's decision facts and attribution, the deny note),
6
6
  * then the decision-action binding (boundCallId, boundInputHash by version), the env_failed replay guard and
7
7
  * the content-ask answer binding. Synchronous; every refusal throws pre-CAS.
8
8
  */
@@ -1,7 +1,11 @@
1
- import { canonicalizeCapturedPlainData, classifyQuestionOutcome, isLiveQuestionFace } from "../ask-question.js";
1
+ import { classifyQuestionOutcome, isLiveQuestionFace } from "../ask-question.js";
2
+ import { capturedJsonOf } from "../canonical-json.js";
3
+ import { bindingRefusalOf } from "../tool-policy.js";
2
4
  import { CHECKPOINT_FEATURES, CheckpointError, checkpointVersionOf, winnerFromOutcome } from "../checkpoint-store.js";
3
- import { sanitizeUntrustedText } from "../untrusted-text.js";
5
+ import { isGateKind, resumeGateMatches } from "../pause-registry.js";
6
+ import { sanitizeUntrustedText, sentinelTagRefusal } from "../untrusted-text.js";
4
7
  import { assertOutcomeText, captureHostDecision, describeSuppliedValue, pendingContentAskCallId, refuseSuppliedKeys, sameWinner, SUPPLIED_RECORD_KEY } from "./decide-continuation.js";
8
+ import { refuseUnfitFeedback } from "./approval-feedback.js";
5
9
  const REFUSED_DECIDE_KEYS = [
6
10
  SUPPLIED_RECORD_KEY,
7
11
  ["settledBy", 'a retired key — the host supplies decision FACTS as hostDecision{decidedBy: "person" | "sla_timeout" | "policy", approver?} and core mints the settlement word from them'],
@@ -12,7 +16,8 @@ export function resumePolicyOutcome(input) {
12
16
  let suppliedAnswer;
13
17
  let redeemedAnswer;
14
18
  let plainPolicyOutcome;
15
- if (cp.gate.kind === "human" || cp.gate.kind === "irreversible_ask") {
19
+ const storedGateKind = cp.gate.kind;
20
+ if (isGateKind(storedGateKind) && resumeGateMatches(storedGateKind, "policy_ask")) {
16
21
  const decide = outcome;
17
22
  suppliedAnswer = decide.answer;
18
23
  if (suppliedAnswer !== undefined) {
@@ -25,17 +30,11 @@ export function resumePolicyOutcome(input) {
25
30
  }
26
31
  const updatedInputRaw = decide.updatedInput;
27
32
  let updatedInput;
28
- try {
29
- updatedInput = updatedInputRaw === undefined ? undefined : structuredClone(updatedInputRaw);
30
- }
31
- catch {
32
- throw new CheckpointError("checkpoint.invalid_outcome", "resume carried an `updatedInput` that could not be captured as plain data — refusing pre-CAS; the checkpoint stays pending");
33
- }
34
- if (Object.is(updatedInput, -0))
35
- updatedInput = 0;
36
- if (updatedInput !== undefined && !canonicalizeCapturedPlainData(updatedInput)) {
37
- throw new CheckpointError("checkpoint.invalid_outcome", "resume carried an `updatedInput` outside the plain-JSON value domain (a Map/Set/Date/buffer, a sparse or expando-carrying array, a non-finite number, or a cycle) — " +
38
- "such a value reads differently on the two sides of the durable store; refusing pre-CAS, the checkpoint stays pending");
33
+ if (updatedInputRaw !== undefined) {
34
+ const captured = capturedJsonOf(updatedInputRaw);
35
+ if (!captured.ok)
36
+ throw new CheckpointError("checkpoint.invalid_outcome", bindingRefusalOf(cp.pendingAction.kind === "tool_approval" ? cp.pendingAction.toolName : "the parked call", updatedInputRaw, captured.fault));
37
+ updatedInput = captured.value;
39
38
  }
40
39
  const decision = decide.decision;
41
40
  if (decision !== "allow" && decision !== "deny") {
@@ -45,6 +44,8 @@ export function resumePolicyOutcome(input) {
45
44
  const hostDecision = captureHostDecision(cp, decide.hostDecision, "policy_ask", { word: decision, allows: decision === "allow" });
46
45
  const reason = decide.reason;
47
46
  assertOutcomeText(reason, "reason");
47
+ const feedback = decide.feedback;
48
+ assertOutcomeText(feedback, "feedback");
48
49
  plainPolicyOutcome = {
49
50
  gate: "policy_ask",
50
51
  boundCallId: decide.boundCallId,
@@ -52,11 +53,13 @@ export function resumePolicyOutcome(input) {
52
53
  decision,
53
54
  ...(updatedInput !== undefined ? { updatedInput } : {}),
54
55
  ...(reason !== undefined ? { reason } : {}),
56
+ ...(feedback !== undefined ? { feedback } : {}),
55
57
  ...(redeemedAnswer !== undefined ? { answer: redeemedAnswer } : {}),
56
58
  hostDecision,
57
59
  };
60
+ refuseUnfitFeedback(plainPolicyOutcome);
58
61
  if (plainPolicyOutcome.decision === "deny" && plainPolicyOutcome.reason && sanitizeUntrustedText(plainPolicyOutcome.reason) !== plainPolicyOutcome.reason) {
59
- throw new CheckpointError("checkpoint.invalid_outcome", "resume deny/reject reason must not contain a </system-reminder> tag");
62
+ throw new CheckpointError("checkpoint.invalid_outcome", sentinelTagRefusal("resume deny/reject reason"));
60
63
  }
61
64
  }
62
65
  if (plainPolicyOutcome !== undefined) {
@@ -1,6 +1,6 @@
1
1
  import { CheckpointError } from "../checkpoint-store.js";
2
2
  import { MAX_TIMER_MS } from "../mods/kernel/index.js";
3
- import { hasDestroy } from "../remote-env.js";
3
+ import { frozenHandleCopy, hasDestroy } from "../remote-env.js";
4
4
  import { inlineUntrusted } from "../untrusted-text.js";
5
5
  import { describeSuppliedValue } from "./decide-continuation.js";
6
6
  import { settleTeardownLeg } from "./teardown-bounded.js";
@@ -63,7 +63,7 @@ export async function resumePreflight(input) {
63
63
  sessionId: cp.sessionId,
64
64
  ...(cp.state.placementRootSessionId !== undefined ? { placementRootSessionId: cp.state.placementRootSessionId } : {}),
65
65
  ...(cp.principal ? { principal: cp.principal } : {}),
66
- ...(cp.state.workspaceHandle !== undefined ? { workspaceHandle: structuredClone(cp.state.workspaceHandle) } : {}),
66
+ ...(cp.state.workspaceHandle !== undefined ? { workspaceHandle: frozenHandleCopy(cp.state.workspaceHandle) } : {}),
67
67
  gateKind: cp.gate.kind,
68
68
  };
69
69
  const refusalTail = "; the checkpoint stays pending and the same token is redeemable once the obstacle clears";
@@ -1,6 +1,6 @@
1
1
  import { CheckpointError } from "../checkpoint-store.js";
2
2
  import { isGateKind, PAUSE_REGISTRY, resumeGateMatches } from "../pause-registry.js";
3
- import { sanitizeUntrustedText } from "../untrusted-text.js";
3
+ import { sanitizeUntrustedText, sentinelTagRefusal } from "../untrusted-text.js";
4
4
  import { assertOutcomeText, captureHostDecision, describeSuppliedValue, refuseSuppliedKeys, SUPPLIED_RECORD_KEY } from "./decide-continuation.js";
5
5
  export function resumeReviewOutcome(input) {
6
6
  const { cp, outcome, outcomeGate, wakeMessage } = input;
@@ -49,13 +49,13 @@ export function resumeReviewOutcome(input) {
49
49
  if (plainReviewOutcome !== undefined) {
50
50
  const reason = plainReviewOutcome.reason;
51
51
  if (plainReviewOutcome.decision === "reject" && reason && sanitizeUntrustedText(reason) !== reason) {
52
- throw new CheckpointError("checkpoint.invalid_outcome", "resume deny/reject reason must not contain a </system-reminder> tag");
52
+ throw new CheckpointError("checkpoint.invalid_outcome", sentinelTagRefusal("resume deny/reject reason"));
53
53
  }
54
54
  const capturedPlan = plainReviewOutcome.gate === "plan_review" ? plainReviewOutcome.editedPlan : undefined;
55
55
  if (plainReviewOutcome.decision === "edit" &&
56
56
  capturedPlan &&
57
57
  sanitizeUntrustedText(capturedPlan) !== capturedPlan) {
58
- throw new CheckpointError("checkpoint.invalid_outcome", "resume editedPlan must not contain a </system-reminder> tag");
58
+ throw new CheckpointError("checkpoint.invalid_outcome", sentinelTagRefusal("resume editedPlan"));
59
59
  }
60
60
  if (cp.reopenReason === "env_failed") {
61
61
  const winner = cp.resolvedOutcome;
@@ -12,15 +12,16 @@ import { readToolCallArguments, uuidv7 } from "../../internal/harness.js";
12
12
  import { ORG_ADJUDICATION_TIMEOUT_MS, settleOrgVerdictWithin } from "../permission-rule-org.js";
13
13
  import { applyPersistedTightening, disclosedRuleSet } from "../persisted-rule-arms.js";
14
14
  import { argsReadBy, stampArgsReading } from "../effective-path-target.js";
15
- import { parkedSkillApprovalRefusal, skillRowRefusal } from "../skill-grant.js";
15
+ import { skillRowRefusal } from "../skill-grant.js";
16
+ import { judgeApprovalObject } from "../approval-object.js";
16
17
  import { SKILL_TOOL_NAME } from "./synthetic-tools.js";
17
18
  import { computeCostMicroUsd } from "../pricing.js";
18
19
  import { mintSystemReminder } from "../reminder-mint.js";
19
20
  import { hasDestroy } from "../remote-env.js";
20
- import { advertisedWritableRootsSeat, attestationOf, exemptRootsSeat, humanRefusalSentence, ownedReading, policyCannotAnswer, policyRefusalMessage, refuseOutOfContractDecision } from "../tool-policy.js";
21
+ import { advertisedWritableRootsSeat, attestationOf, exemptRootsSeat, humanRefusalSentence, licenseCovers, ownedReading, policyCannotAnswer, policyRefusalMessage, refuseOutOfContractDecision } from "../tool-policy.js";
21
22
  import { failedJudgmentDeny } from "./policy-throw.js";
22
23
  import { pathDenialCardOf } from "../../tools/fs/safety.js";
23
- import { callFaceSeat, judgeParkedToolIdentity, parkedRowLiveFace, rosterEntryNamed } from "../tool-roster.js";
24
+ import { callFaceSeat, parkedRowLiveFace, rosterEntryNamed } from "../tool-roster.js";
24
25
  import { deliverEngineNotice } from "../types.js";
25
26
  import { delimitUntrusted, inlineUntrusted, REVIEWER_NOTE_MAX_BODY, sanitizeUntrustedText } from "../untrusted-text.js";
26
27
  import { errorCodeOf } from "./terminal-account.js";
@@ -28,6 +29,7 @@ import { DEFAULT_PRECALL_OUTPUT_TOKENS, platformLimitTerminal } from "./clock-an
28
29
  import { legContextOverlayOption } from "./compaction-call-options.js";
29
30
  import { mintOutputStyleReminder, owedPostureText } from "./compaction-restate.js";
30
31
  import { deepJsonEqual, resumeContinuation } from "./decide-continuation.js";
32
+ import { approvalWordsOf, owedApprovalWords, withApprovalWords } from "./approval-feedback.js";
31
33
  import { chainCannotJudge } from "./call-hook-stations.js";
32
34
  import { settleExecutionRecord } from "./execution-record.js";
33
35
  import { holdPrompt } from "./prompt-block.js";
@@ -64,6 +66,10 @@ export async function runLeg(input) {
64
66
  }
65
67
  if (resume) {
66
68
  const walltimeExhaustedResume = effectiveTimeoutMs !== undefined && effectiveTimeoutMs <= 0;
69
+ const approvalWords = approvalWordsOf(resume.outcome);
70
+ const approvalFrames = owedApprovalWords(approvalWords, uuidv7());
71
+ let approvalCommitted = false;
72
+ prepared.undeliveredContinuationRef.current.parkedSteers.push(...approvalFrames);
67
73
  const resumeWindowRetryAfterMs = !walltimeExhaustedResume && prepared.usageGovernance !== undefined ? await prepared.usageGovernance.check(Date.now()) : undefined;
68
74
  if (resumeWindowRetryAfterMs !== undefined) {
69
75
  const owesNothing = resume.outcome.gate === "resource_limit" &&
@@ -236,13 +242,15 @@ export async function runLeg(input) {
236
242
  ...ident(),
237
243
  });
238
244
  }
245
+ if (approvalWords !== undefined)
246
+ continuation = withApprovalWords(continuation, engineSegments, approvalWords);
239
247
  const pending = resume.cp.pendingAction;
240
248
  const rejectionHalt = pending.kind === "tool_approval" && resume.outcome.gate === "policy_ask"
241
249
  ? maybeHumanRejectionHalt({ gate: resume.outcome.gateOutcome, isDelegatedChild: prepared.hookIdentity.isDelegatedChild, toolName: pending.toolName, toolCallId: pending.toolCallId })
242
250
  : undefined;
243
251
  if (rejectionHalt !== undefined && pending.kind === "tool_approval")
244
252
  prepared.batchHaltRef.current = { rejectedToolCallId: pending.toolCallId };
245
- prepared.undeliveredContinuationRef.current = { parkedSteers: resumeFrames.map(({ entry }) => entry).filter((e) => !withheldSteerIds.has(e.inputId)), orphanTasks };
253
+ prepared.undeliveredContinuationRef.current = { parkedSteers: [...resumeFrames.map(({ entry }) => entry).filter((e) => !withheldSteerIds.has(e.inputId)), ...approvalFrames], orphanTasks };
246
254
  if (!prepared.abortController.signal.aborted) {
247
255
  if (resume !== undefined)
248
256
  resume.decisionDelivered = true;
@@ -264,11 +272,22 @@ export async function runLeg(input) {
264
272
  const preview = redactThenCut(prepared.outputStyleFrame, 220);
265
273
  prepared.frameReceipts.pending.push({ source: "output_style", text: continuation, commit: () => void (queue.push({ type: "steering_injected", source: "output_style", preview, ...ident() }), (rs.attach.attachmentsInjected += 1)) });
266
274
  }
275
+ if (approvalFrames.length > 0) {
276
+ prepared.frameReceipts.pending.push({
277
+ source: "approval_feedback",
278
+ text: continuation,
279
+ commit: () => {
280
+ approvalCommitted = true;
281
+ const seat = prepared.undeliveredContinuationRef.current;
282
+ seat.parkedSteers = seat.parkedSteers.filter((f) => !approvalFrames.includes(f));
283
+ },
284
+ });
285
+ }
267
286
  final = await withBrainSinks(() => prepared.harness.prompt(continuation, {
268
287
  engineSegments,
269
288
  settleUnansweredToolCalls: rejectionHalt !== undefined ? { halt: rejectionHalt } : {},
270
289
  onDelivered: () => {
271
- prepared.undeliveredContinuationRef.current = { parkedSteers: [], orphanTasks: [] };
290
+ prepared.undeliveredContinuationRef.current = { parkedSteers: approvalCommitted ? [] : [...approvalFrames], orphanTasks: [] };
272
291
  for (const e of appliedEvents)
273
292
  queue.push(e);
274
293
  },
@@ -531,19 +550,6 @@ async function applyResumeDecision(runner, prepared, resume, emit, emitCommitted
531
550
  return;
532
551
  if (pendingAction.kind !== "tool_approval")
533
552
  return;
534
- if (resume.outcome.gate === "policy_ask" &&
535
- resume.outcome.decision === "allow" &&
536
- !prepared.tools.some((t) => t.name === pendingAction.toolName)) {
537
- const e = new Error(`the approved tool "${pendingAction.toolName}" is no longer available on resume — refusing to continue as if it ran; the checkpoint is reopened for a retry with the tool present`);
538
- e.code = "resume.tool_unavailable";
539
- throw e;
540
- }
541
- const liveRoster = prepared.toolRosterDeltas.current;
542
- if (pendingAction.kind === "tool_approval" && liveRoster !== undefined && judgeParkedToolIdentity(pendingAction, rosterEntryNamed(liveRoster, pendingAction.toolName)) === "mismatch") {
543
- const e = new Error(`the approved tool "${pendingAction.toolName}" on this leg is not the tool the approval was minted against (its contract / shape / revision differ) — refusing to execute a replacement wearing the name; the checkpoint is reopened for a retry with the original tool present`);
544
- e.code = "resume.tool_contract_mismatch";
545
- throw e;
546
- }
547
553
  await resolvePendingCall(runner, prepared, resume, emit, emitCommitted, onResolvedToolSuccess, onExecuteStart);
548
554
  }
549
555
  async function resolvePendingCall(runner, prepared, resume, emit, emitCommitted, onResolvedToolSuccess, onExecuteStart) {
@@ -551,12 +557,17 @@ async function resolvePendingCall(runner, prepared, resume, emit, emitCommitted,
551
557
  const { outcome } = resume;
552
558
  if (pendingAction.kind !== "tool_approval" || outcome.gate !== "policy_ask")
553
559
  return;
560
+ const mounted = () => { const tool = prepared.tools.find((x) => x.name === pendingAction.toolName); const roster = prepared.toolRosterDeltas.current; return tool === undefined || roster === undefined ? {} : { tool, entry: rosterEntryNamed(roster, pendingAction.toolName) }; };
561
+ const reopen = (miss) => { throw Object.assign(new Error(miss.message), { code: miss.code }); };
562
+ const liveTool = mounted().tool;
563
+ const atSession = outcome.decision === "allow" ? judgeApprovalObject(pendingAction, { tool: mounted().entry }, "session") : "match";
564
+ if (atSession !== "match" && atSession.disposition === "reopen")
565
+ reopen(atSession);
554
566
  const decidedGate = outcome.gateOutcome;
555
567
  if (decidedGate === undefined) {
556
568
  throw new CheckpointError("checkpoint.invalid_outcome", "pending action reached the resolver with no settlement record on its decision (gateOutcome) — refusing to execute or settle a decision that carries no record");
557
569
  }
558
570
  const humanRewrite = wireOf("resume.questionArgsShown", pendingAction) === ASK_USER_QUESTION_TOOL_NAME ? undefined : outcome.updatedInput;
559
- const liveTool = prepared.tools.find((x) => x.name === pendingAction.toolName);
560
571
  const reading = liveTool === undefined ? undefined : ownedReading((a) => readToolCallArguments(liveTool, { type: "toolCall", id: pendingAction.toolCallId, name: pendingAction.toolName, arguments: {} }, a));
561
572
  const beltFace = () => { const f = parkedRowLiveFace(prepared.toolRosterDeltas.current, pendingAction); if (f !== undefined && reading !== undefined)
562
573
  stampArgsReading(f, reading); return f; };
@@ -568,7 +579,7 @@ async function resolvePendingCall(runner, prepared, resume, emit, emitCommitted,
568
579
  } };
569
580
  const decided = outcome.decision === "allow" ? tryRead(humanRewrite !== undefined ? humanRewrite : pendingAction.args) : { args: humanRewrite !== undefined ? humanRewrite : pendingAction.args };
570
581
  const resolvedArgs = decided.args;
571
- const pendingLabel = (() => { const l = prepared.tools.find((t) => t.name === pendingAction.toolName)?.label; return l !== undefined && l !== pendingAction.toolName ? { label: l } : {}; })();
582
+ const pendingLabel = (() => { const l = liveTool?.label; return l !== undefined && l !== pendingAction.toolName ? { label: l } : {}; })();
572
583
  const spelledName = pendingAction.names?.[0] ?? pendingAction.toolName;
573
584
  const occurrences = prepared.toolOccurrences;
574
585
  const callStart = { toolCallId: pendingAction.toolCallId, spelledName, wireName: pendingAction.toolName, ...pendingLabel, args: resolvedArgs };
@@ -591,6 +602,11 @@ async function resolvePendingCall(runner, prepared, resume, emit, emitCommitted,
591
602
  await settleEnd(true, { content: text, ...(marks !== undefined ? { details: marks } : {}) }, undefined, true);
592
603
  emitCommitted(await prepared.session.appendMessage(resultRow(text, true)), "toolResult", pendingAction.toolCallId);
593
604
  };
605
+ const vetoApproval = async (miss) => {
606
+ const text = formatHookFeedback(miss.message, prepared.reminderMark);
607
+ await settleEnd(true, { content: text, ...(miss.code !== undefined ? { details: { code: miss.code } } : {}) }, "policy");
608
+ emitCommitted(await prepared.session.appendMessage(resultRow(text, true)), "toolResult", pendingAction.toolCallId);
609
+ };
594
610
  if (outcome.decision === "deny") {
595
611
  const settlementKind = outcome.gateOutcome?.settlement?.kind;
596
612
  const reason = settlementKind === "policy_refused"
@@ -612,6 +628,8 @@ async function resolvePendingCall(runner, prepared, resume, emit, emitCommitted,
612
628
  if (outcome.decision !== "allow") {
613
629
  throw new CheckpointError("checkpoint.invalid_outcome", `pending action reached the resolver with decision "${String(outcome.decision)}" — only "allow" executes and only "deny" injects a denial; refusing to execute`);
614
630
  }
631
+ if (atSession !== "match")
632
+ return vetoApproval(atSession);
615
633
  if (decided.unread !== undefined)
616
634
  return settleUnreadable(decided.unread.error, humanRewrite !== undefined ? "the arguments the decision edited it to" : "its arguments");
617
635
  const screened = await prepared.approvedCallScreen?.({ toolCallId: pendingAction.toolCallId, toolName: pendingAction.toolName, args: structuredClone(resolvedArgs), decidedBy: outcome.hostDecision.decidedBy });
@@ -625,7 +643,7 @@ async function resolvePendingCall(runner, prepared, resume, emit, emitCommitted,
625
643
  if (rescreened?.unread !== undefined)
626
644
  return settleUnreadable(rescreened.unread.error, "the arguments a PreToolUse hook rewrote it to");
627
645
  const executedArgs = rescreened !== undefined ? rescreened.args : resolvedArgs;
628
- const unseen = !deepJsonEqual(executedArgs, pendingAction.args);
646
+ const unseen = !licenseCovers(pendingAction.boundInputHash, executedArgs);
629
647
  let beltReading;
630
648
  const beltReq = () => ({ toolName: pendingAction.toolName, args: executedArgs, toolCallId: pendingAction.toolCallId, ...callFaceSeat(beltFace()), budget: beltBudget, ...(prepared.cwdRef !== undefined ? { cwd: prepared.cwdRef.current } : {}), ...(prepared.handsRoot !== undefined ? { root: prepared.handsRoot } : {}), ...advertisedWritableRootsSeat(prepared.advertisedWritableDirs), ...exemptRootsSeat(prepared.exemptRoots), ...(beltReading !== undefined ? { targetReading: beltReading } : {}) });
631
649
  const unjudgeable = unseen ? chainCannotJudge(resume.cp.state.inheritedGate) : undefined;
@@ -783,18 +801,24 @@ async function resolvePendingCall(runner, prepared, resume, emit, emitCommitted,
783
801
  reading.verdict.admit?.();
784
802
  }
785
803
  }
786
- const grantRefusal = parkedSkillApprovalRefusal(pendingAction, executedArgs, prepared.skillSpecs, SKILL_TOOL_NAME, "execution");
787
- if (grantRefusal !== undefined) {
788
- const grantDenial = formatHookFeedback(grantRefusal, prepared.reminderMark);
789
- await settleEnd(true, { content: grantDenial }, "policy");
790
- emitCommitted(await prepared.session.appendMessage(resultRow(grantDenial, true)), "toolResult", pendingAction.toolCallId);
791
- return;
792
- }
793
- const tool = prepared.tools.find((t) => t.name === pendingAction.toolName);
794
- if (!tool) {
795
- const e = new Error(`the approved tool "${pendingAction.toolName}" is no longer available on resume`);
796
- e.code = "resume.tool_unavailable";
797
- throw e;
804
+ let tool;
805
+ for (;;) {
806
+ const { targetReading: _belts, ...unread } = beltReq();
807
+ const targetReading = (await prepared.stampCallTarget?.(unread, prepared.abortController.signal))?.targetReading;
808
+ const atExecution = judgeApprovalObject(pendingAction, { tool: mounted().entry, args: executedArgs, skills: prepared.skillSpecs, skillToolName: SKILL_TOOL_NAME, ...(humanRewrite !== undefined ? { decided: resolvedArgs } : {}), targetReading }, "execution");
809
+ if (atExecution !== "match")
810
+ return atExecution.disposition === "reopen" ? reopen(atExecution) : vetoApproval(atExecution);
811
+ tool = mounted().tool;
812
+ const waited = prepared.crossBoundary?.storeWaits() ?? 0;
813
+ const crossBoundaryRefusal = await prepared.crossBoundary?.check?.exit({ toolName: pendingAction.toolName, args: executedArgs, toolCallId: pendingAction.toolCallId }, outcome.hostDecision.decidedBy);
814
+ if (crossBoundaryRefusal !== undefined) {
815
+ const text = formatHookFeedback(crossBoundaryRefusal.message, prepared.reminderMark);
816
+ await settleEnd(true, { content: text }, "policy");
817
+ emitCommitted(await prepared.session.appendMessage(resultRow(text, true)), "toolResult", pendingAction.toolCallId);
818
+ return;
819
+ }
820
+ if ((prepared.crossBoundary?.storeWaits() ?? 0) === waited)
821
+ break;
798
822
  }
799
823
  const args = executedArgs;
800
824
  screened?.admit();
@@ -282,6 +282,8 @@ export async function runSettleAndTeardown(input) {
282
282
  const writeDateAnchor = owedDateAnchor(prepared.session, prepared.dateChange, rs.attach.dateState);
283
283
  if (writeDateAnchor !== undefined)
284
284
  await settleTeardownLeg(writeDateAnchor, "date anchor (settle leg)", (e) => runner.deps.onError?.(e, { phase: "degraded", sessionId: prepared.sessionId }));
285
+ if (prepared.crossBoundary !== undefined)
286
+ await prepared.crossBoundary.settle();
285
287
  const ownedEnvDying = prepared.ownedEnv !== undefined &&
286
288
  hasDestroy(prepared.ownedEnv) &&
287
289
  isIsolated(prepared.ownedEnv) &&
@@ -10,6 +10,7 @@ import { compactionHookOptions, recordCompactionReuse, seamCCompactionOptions }
10
10
  import { compactionRestateOptions } from "./compaction-restate.js";
11
11
  import { MAX_CONSECUTIVE_COMPACTION_FAILURES } from "./compaction-knobs.js";
12
12
  import { emitSettled, reconciledEnd } from "./tool-end-body.js";
13
+ import { isApprovalWordsFrame } from "./approval-feedback.js";
13
14
  import { asThrownError, thrownText } from "../thrown-value.js";
14
15
  export async function runTerminalAdoption(input) {
15
16
  const { spec, queue, prepared, rs, stats, ident, emitCommitted, startedToolCallIds, toolLabels, timeout, compactionBrain, compactionBreaker, windowSafetyOptions, undrainedUserAtEnd, userInputLostAtEnd, final, abortedLive, userInterruptedLive, callerStop, runner, sessions, next } = input;
@@ -78,21 +79,28 @@ export async function runTerminalAdoption(input) {
78
79
  }
79
80
  let migratedParked = { steer: 0, followUp: 0 };
80
81
  let carriedContinuation = 0;
82
+ let carriedApproval = 0;
83
+ let approvalRefusal;
81
84
  if (committedPause !== undefined && runner.deps.checkpointStore !== undefined) {
82
85
  const parkToken = committedPause.cause.token;
83
86
  const parkScope = committedPause.scope;
84
87
  const store = runner.deps.checkpointStore;
85
88
  const carried = [];
86
89
  const owed = [
87
- ...prepared.undeliveredContinuationRef.current.parkedSteers.map(({ seq: _seq, ...input }) => ({ input })),
90
+ ...prepared.undeliveredContinuationRef.current.parkedSteers.map((frame) => {
91
+ const { seq: _seq, ...input } = frame;
92
+ return { input, ...(isApprovalWordsFrame(frame) ? { approval: true } : {}) };
93
+ }),
88
94
  ...prepared.harness.readParkableUserInputs().map((record) => ({ input: record, live: record })),
89
95
  ];
90
- for (const { input, live } of owed) {
96
+ for (const { input, live, approval } of owed) {
91
97
  try {
92
98
  if (!(await store.setPendingSteer(parkToken, parkScope, input)))
93
99
  break;
94
100
  if (live !== undefined)
95
101
  carried.push(live);
102
+ else if (approval)
103
+ carriedApproval += 1;
96
104
  else
97
105
  carriedContinuation += 1;
98
106
  }
@@ -102,14 +110,17 @@ export async function runTerminalAdoption(input) {
102
110
  sessionId: prepared.sessionId,
103
111
  });
104
112
  const parkCode = errorCodeOf(parkErr);
105
- if (parkCode === "steering.invalid_content" || parkCode === "steering.queue_full")
113
+ if (parkCode === "steering.invalid_content" || parkCode === "steering.queue_full") {
114
+ if (approval)
115
+ approvalRefusal = parkCode;
106
116
  continue;
117
+ }
107
118
  break;
108
119
  }
109
120
  }
110
121
  migratedParked = prepared.harness.dropParkedUserInputs(carried);
111
122
  }
112
- const owedUserInput = { held: undrainedUserAtEnd.current, migrated: migratedParked, continuation: { frames: prepared.undeliveredContinuationRef.current.parkedSteers, carried: carriedContinuation } };
123
+ const owedUserInput = { held: undrainedUserAtEnd.current, migrated: migratedParked, continuation: { frames: prepared.undeliveredContinuationRef.current.parkedSteers.filter((f) => !isApprovalWordsFrame(f)), carried: carriedContinuation, approval: { owed: prepared.undeliveredContinuationRef.current.parkedSteers.filter(isApprovalWordsFrame).length, carried: carriedApproval, ...(approvalRefusal !== undefined ? { refusal: approvalRefusal } : {}) } } };
113
124
  if (rs.attach.attachState?.postCompactPending === true && (rs.attach.attachmentsCfg?.backgroundTasks ?? eventDefaultOn("background_tasks")) === true) {
114
125
  emitTrace(rs.telemetry.tracer, () => ({ kind: "compaction.announce_dropped", version: 1, taskId: rs.telemetry.taskId, ts: Date.now() }));
115
126
  }
@@ -41,10 +41,13 @@ export type TranscriptFileRecord = {
41
41
  content: string;
42
42
  /** The recording message's own timestamp — the most honest "last read at" available. */
43
43
  at: number;
44
+ /** A Read result's call id — the transmission that carried the text (a Write's or an Edit's post-condition: none). */ transmittedByToolCallId?: string;
44
45
  } | {
45
46
  path: string;
46
- /** The entry the delivery committed. */
47
- observation: UserAttachmentObservation["entry"];
47
+ /** The entry the delivery committed, stamped with the frame that carried it (a trim that drops the frame retires it). */
48
+ observation: UserAttachmentObservation["entry"] & {
49
+ transmittedByToolCallId: string;
50
+ };
48
51
  at: number;
49
52
  };
50
53
  /**
@@ -3,6 +3,7 @@ import { isAbsoluteForFamily, pathFamilyOf } from "../../tools/fs/safety.js";
3
3
  import { applyRecordedEdit } from "../../tools/fs/fs-shared.js";
4
4
  import { normalizeFileText } from "../../tools/fs/encoding.js";
5
5
  import { isOffloadedDetailReplacement } from "../tool-result-store.js";
6
+ import { attachmentTransmissionId } from "../user-attachment.js";
6
7
  const READ_TOOL = "Read";
7
8
  const WRITE_TOOL = "Write";
8
9
  const EDIT_TOOL = "Edit";
@@ -69,7 +70,7 @@ export function wholeFileRecordsFromTranscript(messages) {
69
70
  if (attachment?.observation !== undefined) {
70
71
  const { key, entry } = attachment.observation;
71
72
  if (isAbsoluteForFamily(pathFamilyOf({ root: key }), key))
72
- byPath.set(key, { path: key, observation: entry, at: m.timestamp });
73
+ byPath.set(key, { path: key, observation: { ...entry, transmittedByToolCallId: attachmentTransmissionId(attachment) }, at: m.timestamp });
73
74
  continue;
74
75
  }
75
76
  if (m.role !== "toolResult" || m.isError)
@@ -112,7 +113,7 @@ export function wholeFileRecordsFromTranscript(messages) {
112
113
  continue;
113
114
  if (isOffloadedDetailReplacement(content))
114
115
  continue;
115
- byPath.set(filePath, { path: filePath, content, at: m.timestamp });
116
+ byPath.set(filePath, { path: filePath, content, at: m.timestamp, ...(isRead ? { transmittedByToolCallId: m.toolCallId } : {}) });
116
117
  }
117
118
  return [...byPath.values()];
118
119
  }
@@ -6,7 +6,7 @@ import { classicEventBase, failedHandlerClause, screenUserPrompt } from "../mods
6
6
  import { buildHumanInputEvent, frameMidTurnUserInput, projectHumanInput } from "../human-input-projection.js";
7
7
  import { injectionLaneOf, isSystemInjectionPriority, steerPriorityOf, SYSTEM_INJECTION_PRIORITIES } from "../task-notification.js";
8
8
  import { deliverEngineNotice } from "../types.js";
9
- import { inlineUntrusted, sanitizeUntrustedText } from "../untrusted-text.js";
9
+ import { inlineUntrusted, sanitizeUntrustedText, sentinelTagRefusal } from "../untrusted-text.js";
10
10
  import { announceAcceptance, armAcceptance, nextHumanInputSeq } from "./steer-admission.js";
11
11
  import { steerReplayRefusal } from "../steer-replay.js";
12
12
  export function streamSteerVerb(input) {
@@ -18,7 +18,7 @@ export function streamSteerVerb(input) {
18
18
  }
19
19
  const trusted = options?.trusted ? true : false;
20
20
  if (trusted && sanitizeUntrustedText(text) !== text) {
21
- throw steeringError("trusted steering text must not contain a </system-reminder> tag", "steering.invalid_content");
21
+ throw steeringError(sentinelTagRefusal("trusted steering text"), "steering.invalid_content");
22
22
  }
23
23
  const inputId = options?.inputId;
24
24
  if (inputId !== undefined) {
@@ -4,13 +4,15 @@
4
4
  * tool name like `allowTools` / `toolAxes`). Validated against the MATERIALIZED schema at every listing
5
5
  * (the first mount and every RefreshMcpTools): `pathTarget.param` must be a top-level property; an
6
6
  * `approvalCard` must find the keys the card needs; a `ruleFace` may only name scalar keys; `family`
7
- * must be a member of the closed set. A failing overlay does NOT mount — the tool mounts BARE (no
7
+ * must be a member of the closed set; every `resultCards` entry must live under the tool's own card namespace.
8
+ * A failing overlay does NOT mount — the tool mounts BARE (no
8
9
  * face) and the leg announces `config.tool_face_invalid` once per tool; a `toolFaces` key naming a
9
10
  * tool the listing does not carry announces `config.tool_face_undeclared` once per name. Never a
10
11
  * silent partial application: an overlay is one declaration, applied whole or not at all.
11
12
  *
12
13
  * Prepare-path machinery (layer 1): pure over the tool objects and the server specs; the notice sink
13
- * is a parameter.
14
+ * is a parameter. The declaration itself is read once, by `readToolFaces` (`core/tool-face-reader.ts`), at the preparation door and at the
15
+ * materialization entrance; the specs this file consumes carry that reader's copy, never the caller's object.
14
16
  */
15
17
  import type { AgentTool } from "../../internal/harness-types.js";
16
18
  import type { McpServerSpec } from "../mcp-server-spec.js";
@@ -20,10 +22,24 @@ import { type EngineNotice } from "../engine-notice.js";
20
22
  * card's full schema; this is the pre-filter the overlay is validated against). */
21
23
  export declare const APPROVAL_CARD_REQUIRED_KEYS: Readonly<Record<ToolApprovalCard, readonly string[]>>;
22
24
  export type McpToolFace = NonNullable<McpServerSpec["toolFaces"]>[string];
23
- /** Why an overlay is refused — a closed reason list, each a distinct declaration mistake. */
24
- export declare function toolFaceProblem(face: McpToolFace, schema: AgentTool["parameters"], tool?: {
25
+ /** The tool a face rides on, as far as the judge needs to know it: its name (a reserved approval card belongs to the engine's own tool of that
26
+ * name) and, when the caller holds the tool's card namespace, the `cardId` its `resultCards` must live under (`<cardId>/<card>`). */
27
+ export interface FaceTool {
25
28
  readonly toolName: string;
26
- }): string | undefined;
29
+ readonly cardId?: string;
30
+ }
31
+ /** Why an overlay is refused — a closed reason list, each a distinct declaration mistake. */
32
+ export declare function toolFaceProblem(face: McpToolFace, schema: AgentTool["parameters"], tool?: FaceTool): string | undefined;
33
+ /**
34
+ * The one judge of a face. With the tool's materialized `schema` it is {@link toolFaceProblem}: the declaration against the
35
+ * schema it rides on. With no schema it is the same judge over the DECLARATION ALONE (`readToolFaces`, `core/tool-face-reader.ts`): every
36
+ * check that needs the schema's keys (a path target's `param` / `patternParam`, an approval card's required keys, the names a summary or a
37
+ * rule face lists) waits for the listing; the closed words, the bounds and the cross-member rules do not need it and are the same
38
+ * checks, in the same order, with the same sentences. A face this judges is a well-typed one (the reader types it first). A tool that
39
+ * holds its card namespace (`tool.cardId`) has its `resultCards` held to it here too, so a caller's face and a server's face are
40
+ * judged by this one function, not by a rule beside it.
41
+ */
42
+ export declare function faceProblem(face: McpToolFace, schema: AgentTool["parameters"] | undefined, tool?: FaceTool): string | undefined;
27
43
  export interface ApplyToolFacesInput {
28
44
  /** The materialized tools of ONE server (namespaced names). */
29
45
  tools: readonly AgentTool[];
@@ -38,5 +54,7 @@ export interface ApplyToolFacesInput {
38
54
  * untyped carry `defineTool` uses, so the roster reads it off the instance). A face that does NOT fit the materialized
39
55
  * schema is a bad declaration: the tool is NOT mounted on this leg (announced once, `config.tool_face_invalid`) —
40
56
  * mounting it bare would silently drop the write protection the operator declared for it (an MCP name has no catalog
41
- * floor), and a schema refresh that invalidates only a display hint must fail loud, never open. */
57
+ * floor), and a schema refresh that invalidates only a display hint must fail loud, never open. The faces it reads are the
58
+ * server entry's own: the reader's copy of the declaration (`readToolFaces`), put there by the preparation door or the
59
+ * materialization entrance, never a caller's object. */
42
60
  export declare function applyMcpToolFaces(input: ApplyToolFacesInput): AgentTool[];
@@ -1,7 +1,8 @@
1
1
  import { MCP_NAMESPACE } from "../protocol-table.js";
2
2
  import { mintNamespacePrefix } from "../protocol-naming.js";
3
3
  import { RESERVED_APPROVAL_CARDS, TOOL_APPROVAL_CARDS, TOOL_FAMILIES, TOOL_PATH_ABSENCES, TOOL_PATH_ACCESSES, TOOL_PATH_BASES, RENDER_HINT_ACTIVITY_MAX_CHARS, RENDER_HINT_MAX_CHARS, RENDER_HINT_MAX_LIST, TOOL_KEY_MAX_CHARS, } from "../tool-face.js";
4
- import { engineCardOf, inputKeysOf } from "../tool-roster.js";
4
+ import { engineCardOf, inputKeysOf, offendingResultCard, protocolToolOf, rosterCardId } from "../tool-roster.js";
5
+ import { engineCardTypes } from "../tool-registry.js";
5
6
  import { deliverEngineNotice } from "../engine-notice.js";
6
7
  export const APPROVAL_CARD_REQUIRED_KEYS = Object.freeze({
7
8
  "file-edit": ["file_path"],
@@ -14,17 +15,20 @@ export const APPROVAL_CARD_REQUIRED_KEYS = Object.freeze({
14
15
  generic: [],
15
16
  });
16
17
  export function toolFaceProblem(face, schema, tool) {
17
- const { keys, scalar } = inputKeysOf(schema);
18
- const top = new Set(keys.map((k) => k.name));
18
+ return faceProblem(face, schema, tool);
19
+ }
20
+ export function faceProblem(face, schema, tool) {
21
+ const bound = schema === undefined ? undefined : inputKeysOf(schema);
22
+ const top = bound === undefined ? undefined : new Set(bound.keys.map((k) => k.name));
19
23
  if (face.family !== undefined && !TOOL_FAMILIES.includes(face.family))
20
24
  return `family ${JSON.stringify(face.family)} is outside the closed set`;
21
25
  const overlong = (label, values) => {
22
26
  const k = values.find((v) => v.length > TOOL_KEY_MAX_CHARS);
23
27
  return k === undefined ? undefined : `${label} ${JSON.stringify(k.slice(0, 40) + "…")} is ${k.length} characters; a roster key is at most ${TOOL_KEY_MAX_CHARS}`;
24
28
  };
25
- const vocabulary = new Set(scalar);
29
+ const vocabulary = new Set(bound?.scalar);
26
30
  if (face.pathTarget !== undefined) {
27
- if (typeof face.pathTarget.param !== "string" || !top.has(face.pathTarget.param))
31
+ if (top !== undefined && (typeof face.pathTarget.param !== "string" || !top.has(face.pathTarget.param)))
28
32
  return `pathTarget.param ${JSON.stringify(face.pathTarget.param)} is not a top-level property of the tool's schema`;
29
33
  if (!TOOL_PATH_ACCESSES.includes(face.pathTarget.access))
30
34
  return `pathTarget.access ${JSON.stringify(face.pathTarget.access)} is outside the closed set`;
@@ -40,7 +44,7 @@ export function toolFaceProblem(face, schema, tool) {
40
44
  return `pathTarget.absent "base" needs pathTarget.access "read" — it says the call is about the whole base directory when the slot is empty, which is a search SCOPE; a write with no path names no file, and the containment fences judge a write on its own target`;
41
45
  const patternParam = face.pathTarget.patternParam;
42
46
  if (patternParam !== undefined) {
43
- if (typeof patternParam !== "string" || !top.has(patternParam))
47
+ if (top !== undefined && (typeof patternParam !== "string" || !top.has(patternParam)))
44
48
  return `pathTarget.patternParam ${JSON.stringify(patternParam)} is not a top-level property of the tool's schema`;
45
49
  if (absent !== "base")
46
50
  return `pathTarget.patternParam ${JSON.stringify(patternParam)} is declared but pathTarget.absent is ${JSON.stringify(absent ?? "none")} — the pattern base is only ever read for an absent slot, so this declaration would be inert`;
@@ -62,7 +66,7 @@ export function toolFaceProblem(face, schema, tool) {
62
66
  return `renderHints.approvalCard ${JSON.stringify(card)} is outside the closed set`;
63
67
  if (RESERVED_APPROVAL_CARDS.includes(card) && (tool === undefined || engineCardOf(tool) !== card))
64
68
  return `renderHints.approvalCard ${JSON.stringify(card)} is reserved for the engine's own tool that owns it; a declared tool takes another card or none`;
65
- const missing = APPROVAL_CARD_REQUIRED_KEYS[card].filter((k) => !top.has(k));
69
+ const missing = top === undefined ? [] : APPROVAL_CARD_REQUIRED_KEYS[card].filter((k) => !top.has(k));
66
70
  if (missing.length > 0)
67
71
  return `renderHints.approvalCard ${JSON.stringify(card)} needs ${missing.join(", ")} on the tool's schema`;
68
72
  }
@@ -77,16 +81,21 @@ export function toolFaceProblem(face, schema, tool) {
77
81
  const long = overlong("renderHints.summaryParams", hints.summaryParams ?? []) ?? overlong("renderHints.resultCards", hints.resultCards ?? []);
78
82
  if (long !== undefined)
79
83
  return long;
80
- const stray = (hints.summaryParams ?? []).find((p) => !top.has(p));
84
+ const foreignCard = tool?.cardId === undefined ? undefined : offendingResultCard(hints, tool.cardId, engineCardTypes());
85
+ if (foreignCard !== undefined)
86
+ return `renderHints.resultCards names ${JSON.stringify(foreignCard.slice(0, 80))}, outside the tool's own card namespace "${tool.cardId}/<card>" (engine cards and other contracts' namespaces are reserved)`;
87
+ const stray = top === undefined ? undefined : (hints.summaryParams ?? []).find((p) => !top.has(p));
81
88
  if (stray !== undefined)
82
89
  return `renderHints.summaryParams names ${JSON.stringify(stray)}, which is not a top-level property`;
83
90
  }
84
- for (const p of face.ruleFace?.params ?? [])
85
- if (!vocabulary.has(p))
86
- return `ruleFace.params names ${JSON.stringify(p)}, which is neither a scalar top-level property nor the declared path target`;
87
- for (const p of face.ruleFace?.primaryParams ?? [])
88
- if (!vocabulary.has(p))
89
- return `ruleFace.primaryParams names ${JSON.stringify(p)}, which is neither a top-level property nor the declared path target`;
91
+ if (top !== undefined) {
92
+ for (const p of face.ruleFace?.params ?? [])
93
+ if (!vocabulary.has(p))
94
+ return `ruleFace.params names ${JSON.stringify(p)}, which is neither a scalar top-level property nor the declared path target`;
95
+ for (const p of face.ruleFace?.primaryParams ?? [])
96
+ if (!vocabulary.has(p))
97
+ return `ruleFace.primaryParams names ${JSON.stringify(p)}, which is neither a top-level property nor the declared path target`;
98
+ }
90
99
  const long = overlong("ruleFace.params", face.ruleFace?.params ?? []) ?? overlong("ruleFace.primaryParams", face.ruleFace?.primaryParams ?? []);
91
100
  if (long !== undefined)
92
101
  return long;
@@ -121,7 +130,7 @@ export function applyMcpToolFaces(input) {
121
130
  if (raw === undefined || face === undefined)
122
131
  return t;
123
132
  const hit = { raw, face };
124
- const problem = toolFaceProblem(hit.face, t.parameters, { toolName: t.name });
133
+ const problem = toolFaceProblem(hit.face, t.parameters, { toolName: t.name, cardId: rosterCardId("mcp", t.name, input.server.name, protocolToolOf(t.name), "") });
125
134
  if (problem !== undefined) {
126
135
  const key = `invalid:${t.name}`;
127
136
  if (!input.announced.has(key)) {