@sema-agent/core 7.53.0 → 7.54.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 (244) hide show
  1. package/CHANGELOG.md +277 -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 +3 -4
  104. package/dist/core/runner/prepare-acquire-reconcile.js +3 -3
  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 +639 -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/repo-utils.d.ts +5 -0
  195. package/dist/engine/session/repo-utils.js +9 -0
  196. package/dist/engine/session/storage-base.js +2 -2
  197. package/dist/index.d.ts +23 -9
  198. package/dist/index.js +19 -8
  199. package/dist/internal/llm.d.ts +1 -1
  200. package/dist/internal/llm.js +1 -1
  201. package/dist/orchestration/run-workflow-tool.d.ts +3 -0
  202. package/dist/orchestration/run-workflow-tool.js +1 -0
  203. package/dist/orchestration/workflow-governance.d.ts +1 -1
  204. package/dist/orchestration/workflow.d.ts +3 -0
  205. package/dist/orchestration/workflow.js +2 -1
  206. package/dist/prompt-assembly/catalog/schema.d.ts +3 -0
  207. package/dist/prompt-assembly/catalog/schema.js +3 -0
  208. package/dist/prompt-assembly/overlay.js +2 -2
  209. package/dist/prompt-assembly/select.js +2 -2
  210. package/dist/prompts/default.d.ts +1 -1
  211. package/dist/shell/lex.d.ts +87 -8
  212. package/dist/shell/lex.js +283 -49
  213. package/dist/stores/file/index.d.ts +8 -6
  214. package/dist/tools/artifact/artifact-tool.js +10 -0
  215. package/dist/tools/fs/bash-program-position.d.ts +3 -15
  216. package/dist/tools/fs/bash-program-position.js +3 -10
  217. package/dist/tools/fs/bash-readonly-classifier.js +2 -0
  218. package/dist/tools/fs/fs-bash.d.ts +3 -3
  219. package/dist/tools/fs/fs-bash.js +72 -101
  220. package/dist/tools/fs/fs-read.d.ts +12 -0
  221. package/dist/tools/fs/fs-read.js +10 -9
  222. package/dist/tools/fs/fs-shared.d.ts +27 -31
  223. package/dist/tools/fs/fs-shared.js +12 -15
  224. package/dist/tools/fs/fs-write.js +1 -1
  225. package/dist/tools/fs/index.js +2 -2
  226. package/dist/tools/fs/read-deny-home.d.ts +1 -1
  227. package/dist/tools/fs/read-deny-home.js +5 -4
  228. package/dist/tools/fs/read-deny.d.ts +16 -3
  229. package/dist/tools/fs/read-deny.js +12 -9
  230. package/dist/tools/fs/safety.d.ts +7 -6
  231. package/dist/tools/fs/shell-read-bindings.d.ts +5 -3
  232. package/dist/tools/fs/shell-read-bindings.js +15 -16
  233. package/dist/tools/fs/shell-read-device.js +1 -1
  234. package/dist/tools/fs/shell-read-plan.js +36 -47
  235. package/dist/tools/fs/shell-read-word.d.ts +1 -6
  236. package/dist/tools/fs/shell-read-word.js +2 -20
  237. package/dist/tools/fs/shell-reentry-text.js +1 -1
  238. package/dist/tools/web.d.ts +2 -2
  239. package/package.json +4 -1
  240. package/test/export-surface.snapshot.json +5654 -5520
  241. package/wire-changes/core@7.53.0.json +30 -8
  242. package/wire-changes/core@7.54.0.json +2870 -0
  243. package/dist/core/shell-scan.d.ts +0 -60
  244. package/dist/core/shell-scan.js +0 -183
@@ -1,6 +1,6 @@
1
1
  import { argsAsTheToolReads, withArgsReading } from "./effective-path-target.js";
2
- import { attest, attestationOf, decisionText, foldHookAsk, ownArgs, refusalOf, refuseOutOfContractDecision } from "./tool-policy.js";
3
- import { arbitrateByMode, classifyAsk, decisionAskClassesOf, fileCommandOptionsSentence, persistedRuleMandateOf, ruleClearanceOf, safetyAxisOf, stampAskOrigin } from "./ask-origin.js";
2
+ import { attest, attestationOf, bindableArgsOf, decisionText, foldHookAsk, mergeAttestation, refusalOf, refuseOutOfContractDecision, withAttestation } from "./tool-policy.js";
3
+ import { arbitrateByMode, classifyAsk, decisionAskClassesOf, fileCommandOptionsSentence, persistedRuleMandateOf, safetyAxisOf, stampAskOrigin } from "./ask-origin.js";
4
4
  import { classifierParseFailureDenyMessage, denialLimitFallbackMessage, rungDidNotRule, unarmedWindow } from "./auto-mode.js";
5
5
  import { classifierNoRulingArm, settleNoRulingAsk } from "./classifier-unavailable-arm.js";
6
6
  import { SETTLEMENT_NOBODY_JUDGED } from "./gate-outcome.js";
@@ -10,14 +10,14 @@ import { applyPersistedTightening, disclosedRuleSet } from "./persisted-rule-arm
10
10
  import { skillAllowDecision, skillRowRefusal } from "./skill-grant.js";
11
11
  import { allowRuleShadowOf } from "./allow-entry.js";
12
12
  import { CD_GIT_COMPOUND_SENTENCE, changesDirectoryBeforeGit, readOnlyShellVerdict } from "./read-only-shell.js";
13
- import { readShellCommand } from "./shell-lexer.js";
13
+ import { readShellCommand } from "./shell-run-candidates.js";
14
14
  import { COMMAND_RULE_TOOL } from "./permission-rule-model.js";
15
15
  import { catalogRuleFaceOf } from "./tool-registry.js";
16
16
  import { callFaceSeat } from "./tool-roster.js";
17
17
  import { heldBy, wireOf } from "./tool-name-readings.js";
18
18
  import { asksUnstated, modeAcceptsFileCommand, permissionSeatAt, unstatedAskMessage } from "./mode-policy.js";
19
19
  import { ORG_RULE_DECISION_REASON, ORG_UNAVAILABLE_DECISION_REASON, orgUnreadableClause } from "./permission-rule-org.js";
20
- import { exitGate, passAskFace, unownedArgsMessage } from "./runner/gate-exit.js";
20
+ import { exitGate, passAskFace, untakenValueExit } from "./runner/gate-exit.js";
21
21
  import { readOrgRuleLane, readPersistedRuleLane } from "./rule-lane-reads.js";
22
22
  import { traceHookCrash } from "./gate-crash-trace.js";
23
23
  import { createGateDeadline, awaitWriteProtectionJudgment, withGateTimeout } from "./gate-bounded-await.js";
@@ -34,9 +34,9 @@ const ruleStoreUnreadableOf = (lane) => lane?.unreadable === true && lane.reason
34
34
  export async function runGateLanes(pass) {
35
35
  const { input, toolName, toolCallId, callSignal, ledger, notifier, screening, adjudicate, resolveAsk, suspendAsk } = pass;
36
36
  const boundedAwaitDeadlineMs = createGateDeadline(input, notifier);
37
- let orgRealApprovalRequired = false;
38
37
  let markedThisRound;
39
38
  let approvedWithArgs = false;
39
+ let editAskedOrigin;
40
40
  let carriedOwnAsk;
41
41
  const takeApprovedEdit = (read) => {
42
42
  carriedOwnAsk = read.outstanding ?? (pass.decision.action === "ask" ? pass.decision : undefined);
@@ -112,7 +112,6 @@ export async function runGateLanes(pass) {
112
112
  const answer = await readOrgRuleLane(input.orgRules, Object.freeze({ toolName, args, toolCallId, ...callFaceSeat(pass.req.face) }), input.abortSignal);
113
113
  if (answer.status === "unavailable") {
114
114
  orgEvidence = { revisionAbsent: "unavailable", ruleAbsent: "unavailable" };
115
- orgRealApprovalRequired = true;
116
115
  await notifier.notifyAsync(() => input.orgRules?.onUnavailable?.({ toolName, toolCallId, message: answer.disclosures.join("; ") }), "toolGate.orgSnapshotUnavailable");
117
116
  const orgUnavailableMessage = `tool "${toolName}" needs approval: this deployment is org-governed and cannot currently adjudicate ` +
118
117
  `against an organization policy snapshot — every allow tightens to a real approval until it can`;
@@ -142,7 +141,6 @@ export async function runGateLanes(pass) {
142
141
  if (decided.action === "deny")
143
142
  return { decision: decided, org: undefined };
144
143
  orgTightenCount += 1;
145
- orgRealApprovalRequired = true;
146
144
  const orgRuleMessage = unreadable !== undefined
147
145
  ? `an organization policy rule (${rule}) may reach this call, ${orgUnreadableClause(unreadable)} — a person must decide`
148
146
  : `an organization policy rule (${rule}) requires approval for this call`;
@@ -223,19 +221,21 @@ export async function runGateLanes(pass) {
223
221
  pass.currentInput = pass.policyRewrite;
224
222
  pass.req = Object.freeze({ ...pass.req, args: pass.policyRewrite, ...(laneReq.targetReading !== undefined ? { targetReading: laneReq.targetReading } : {}) });
225
223
  }
226
- const ruleMayClear = () => ruleClearanceOf(pass.decision.action === "ask" ? pass.decision : undefined, mandateMarksNow()).ruleMayClear;
227
- const classMayClear = () => pass.decision.action === "ask" && classifyAsk(pass.decision, askOriginFacts(orgAnswer.org, laneAnswer)).row.ruleMayClear;
224
+ const originFacts = askOriginFacts(orgAnswer.org, laneAnswer);
225
+ if (pass.decision.action === "ask")
226
+ classifyStanding(pass.decision, originFacts);
227
+ const allowMayClear = () => pass.decision.action === "ask" && pass.askClassification?.row.ruleMayClear === true;
228
228
  let personalEvidence = { dotsAbsent: input.persistedRules === undefined ? "not_wired" : "not_adjudicated" };
229
229
  if (laneAnswer?.unreadable === true)
230
230
  personalEvidence = { dotsAbsent: "unavailable" };
231
231
  let laneCoverage;
232
232
  const questionTool = wireOf("lanes.allowLayerQuestionSkip", pass.req) === ASK_USER_QUESTION_TOOL_NAME;
233
- const allowLayerMayClear = () => !orgRealApprovalRequired &&
233
+ const laneIsProvenance = orgAnswer.org === undefined &&
234
234
  pass.decision.action === "ask" &&
235
235
  pass.decision.requiresRealApproval !== true &&
236
236
  pass.decision.decisionReason !== "hook" &&
237
237
  !questionTool &&
238
- !markedUnresolvable();
238
+ !originFacts.markedUnresolvable;
239
239
  const unionDotsOf = (rules) => {
240
240
  const seen = new Set();
241
241
  const union = [];
@@ -250,7 +250,7 @@ export async function runGateLanes(pass) {
250
250
  }
251
251
  return { dots: union };
252
252
  };
253
- if (input.persistedRules && allowLayerMayClear()) {
253
+ if (input.persistedRules && laneIsProvenance) {
254
254
  const answer = laneAnswer ?? {};
255
255
  laneCoverage = answer.coverage;
256
256
  personalEvidence = answer.hit?.behavior === "ask" ? unionDotsOf(answer.hit.rules) : { dotsAbsent: answer.unreadable === true ? "unavailable" : "no_match" };
@@ -297,7 +297,7 @@ export async function runGateLanes(pass) {
297
297
  const hit = match();
298
298
  if (hit === undefined)
299
299
  continue;
300
- if (allowLayerMayClear() && ruleMayClear() && classMayClear()) {
300
+ if (allowMayClear()) {
301
301
  pass.decision = hit.clear();
302
302
  await hit.afterClear();
303
303
  if (member !== "hook")
@@ -312,7 +312,6 @@ export async function runGateLanes(pass) {
312
312
  const { allowRuleShadowed: _claimed, ...standing } = pass.decision;
313
313
  pass.decision = shadows.length > 0 ? { ...standing, allowRuleShadowed: shadows } : standing;
314
314
  }
315
- const originFacts = askOriginFacts(orgAnswer.org, laneAnswer);
316
315
  let noRulingMinted = false;
317
316
  const fallbackAskOf = (standingAsk, fallback, latestBlockedAction) => {
318
317
  const { classifierNoRuling: _why, ...standing } = standingAsk;
@@ -323,7 +322,7 @@ export async function runGateLanes(pass) {
323
322
  const stamped = { ...pass.decision, ruleEvidence: mintRuleEvidence(personalEvidence), ...(laneCoverage !== undefined ? { segmentCoverage: laneCoverage } : {}) };
324
323
  pass.decision = stampAskOrigin(stamped, originFacts, classifyStanding(stamped, originFacts));
325
324
  }
326
- if (allowLayerMayClear() && pass.decision.action === "ask" && ruleMayClear() && classMayClear() && pass.probeBoundaryDeclined !== true && wireOf("lanes.readOnlyShellAllow", pass.req) === COMMAND_RULE_TOOL) {
325
+ if (allowMayClear() && pass.probeBoundaryDeclined !== true && wireOf("lanes.readOnlyShellAllow", pass.req) === COMMAND_RULE_TOOL) {
327
326
  const commandParam = catalogRuleFaceOf(COMMAND_RULE_TOOL)?.primaryParams[0] ?? "command";
328
327
  const args = pass.req.args;
329
328
  const command = typeof args === "object" && args !== null && !Array.isArray(args) ? args[commandParam] : undefined;
@@ -344,7 +343,7 @@ export async function runGateLanes(pass) {
344
343
  }
345
344
  }
346
345
  if (pass.toolCheckRaised !== true && pass.decision.action !== "deny") {
347
- const read = await checkTool(pass.decision, pass.currentInput, orgAnswer.org, pass.policyRewrite, () => allowLayerMayClear() && ruleMayClear() && classMayClear());
346
+ const read = await checkTool(pass.decision, pass.currentInput, orgAnswer.org, pass.policyRewrite, allowMayClear);
348
347
  if (read?.kind === "denied")
349
348
  refuseByExtension(read.deny);
350
349
  else if (read?.kind === "cleared")
@@ -542,20 +541,25 @@ export async function runGateLanes(pass) {
542
541
  }
543
542
  }
544
543
  approvedWithArgs = pass.decision.action === "allow" && pass.decision.updatedInput !== undefined;
544
+ if (approvedWithArgs && askBeforeResolve.action === "ask")
545
+ editAskedOrigin = askBeforeResolve.origin;
545
546
  }
546
547
  }
547
548
  if (approvedWithArgs && pass.decision.action === "allow" && pass.decision.updatedInput !== undefined) {
548
549
  let editArgs = pass.decision.updatedInput;
549
550
  let editDenied;
550
551
  let editRewrittenSinceHuman = false;
552
+ let editAnswer = pass.decision;
551
553
  let unanswered;
552
554
  const refusedAtResolution = (d) => { editDenied = d; pass.deniedBy = "ask_resolution"; };
553
555
  for (let round = 0;; round++) {
554
- const taken = ownArgs(editArgs);
556
+ const taken = bindableArgsOf(toolName, editArgs);
555
557
  if (!taken.ok) {
556
- editDenied = { action: "deny", message: unownedArgsMessage(`the approved edit for "${toolName}"`, taken.reason) };
557
- pass.deniedBy = "policy";
558
- break;
558
+ const origin = attestationOf(editAnswer, { toolCallId, toolName })?.origin ?? editAskedOrigin;
559
+ if (origin === undefined)
560
+ throw new Error(`the approved edit for "${toolName}" reached its refusal without an origin word — the origin stamp precedes every resolution`);
561
+ ledger.set({ settlement: { kind: "presentation_failed", who: { party: "none" }, when: Date.now() }, origin });
562
+ return await untakenValueExit(pass, { value: editArgs, message: taken.refusal, deniedBy: "ask_resolution" });
559
563
  }
560
564
  editArgs = argsAsTheToolReads(pass.req, taken.value);
561
565
  if (round >= 3) {
@@ -654,7 +658,8 @@ export async function runGateLanes(pass) {
654
658
  editArgs = argsAsTheToolReads(pass.req, recheck.updatedInput);
655
659
  editRewrittenSinceHuman = true;
656
660
  }
657
- if (editRewrittenSinceHuman && asksUnstated(recheck, permissionSeatAt(input, editArgs)?.seat, input.permissionMode?.())) {
661
+ const answeredRecheck = withAttestation(recheck, mergeAttestation(pass.req, attestationOf(editAnswer, pass.req), attestationOf(recheck, pass.req), "answer"));
662
+ if (asksUnstated(answeredRecheck, pass.req, editArgs, permissionSeatAt(input, editArgs)?.seat, input.permissionMode?.())) {
658
663
  recheck = { action: "ask", message: permissionSeatAt(input, editArgs)?.question ?? unstatedAskMessage(toolName) };
659
664
  pass.tightenedBy = "unstated";
660
665
  }
@@ -690,6 +695,7 @@ export async function runGateLanes(pass) {
690
695
  if (editRead?.kind === "approved") {
691
696
  carriedOwnAsk = editRead.outstanding ?? (editDecision.action === "ask" ? editDecision : undefined);
692
697
  editArgs = editRead.allow.updatedInput;
698
+ editAnswer = editRead.allow;
693
699
  editRewrittenSinceHuman = false;
694
700
  continue;
695
701
  }
@@ -735,6 +741,9 @@ export async function runGateLanes(pass) {
735
741
  break;
736
742
  }
737
743
  editArgs = rr.updatedInput;
744
+ editAnswer = rr;
745
+ if (editAskDecision.action === "ask")
746
+ editAskedOrigin = editAskDecision.origin;
738
747
  editRewrittenSinceHuman = false;
739
748
  }
740
749
  const rejudged = () => editDenied === undefined || editDenied === unanswered?.deny;
@@ -31,13 +31,16 @@ import { type AutoModeClassifierRound, type AutoModeFallbackCause } from "./auto
31
31
  * timer over the synchronous leg) elapsed with no answer.
32
32
  * - `park_sla_expired` — a DURABLE park's SLA deadline passed and the host's sweep resolved it as a deny
33
33
  * (FACET A). A store `expire`/`reap` produces no `tool_end` and therefore no settlement.
34
- * - `no_approver` — headless: no approver (or no question face) is wired, or the deny posture string.
34
+ * - `no_approver` — headless: no approver (or no question face) is wired, or the deny posture string (arguments no approval
35
+ * can be bound to are refused there in the binding's own sentence, under this word).
35
36
  * - `approver_unavailable` — the approver (or an ancestor's frozen approver) answered the ROUTING question
36
37
  * "nobody reachable", and no park took the ask.
37
38
  * - `approver_error` — the approver threw.
38
39
  * - `approver_contract` — the approver answered outside its contract (a non-boolean verdict, an
39
40
  * unreadable member, a timeout-settled allow, a refused attribution…).
40
- * - `presentation_failed` — the args or the edit could not be safely presented/adopted (unclonable).
41
+ * - `presentation_failed` — at a seat with an approver, the arguments could not be bound (`bindableArgsOf`: not a record of plain
42
+ * JSON data nested at most 256 levels deep): the asked ones — nobody was asked — or a resolution's edit (the engine's resolver's
43
+ * or, recorded by the gate, a host resolver's) — its yes is not used.
41
44
  * - `blanket_allow_refused` — an allow NO PERSON JUDGED met a `requiresRealApproval` ask: a blanket
42
45
  * `onAsk:"allow"`, or an approver's allow it reported as the deployment's policy (`settledBy: "policy"`).
43
46
  * - `task_aborted` — the wait's abort signal ended it (the run's own end, or a turn-level interrupt).
@@ -218,7 +221,8 @@ export type SettlementRefusalTableCoversEveryKind = AssertAllKeysHandled<Exclude
218
221
  * the ask on a durable queue instead), and the two sites that treat it as headless anyway say so at
219
222
  * their own call, because they are the ones with no park route left.
220
223
  * - `approver_error` / `approver_contract` / `presentation_failed` — the approver was there and the
221
- * engine could not use it. A defect to report, not a deployment with nobody home.
224
+ * engine could not use its answer, or could not put these arguments before it (or adopt its edit). Not a
225
+ * deployment with nobody home: with no approver, such arguments are refused under `no_approver`.
222
226
  * - `task_aborted` — the run ended; nothing is escalating anywhere.
223
227
  */
224
228
  export declare const SETTLEMENT_NOBODY_JUDGED: {
@@ -48,7 +48,8 @@ export declare function governanceRetryClass(code: string): GovernanceRetryClass
48
48
  * fixed the roster (`config.tool_mount_denied`), a
49
49
  * member over a roster row's wire bound (`config.tool_roster_bound` / `config.tool_name_too_long` /
50
50
  * `config.tool_contract_too_long`), a schema over the row's key bounds (`config.tool_schema_bound`), a
51
- * declared face or result card the schema / namespace cannot carry (`config.tool_face_invalid`), a name or
51
+ * declared face or result card the schema / namespace cannot carry (`config.tool_face_invalid` — also the code an MCP server entry's
52
+ * unreadable `toolFaces` is refused with at the task preparation door and at `materializeMcpTools`), a name or
52
53
  * alias outside the caller tool name contract — one no rule can name, or one carrying the protocol namespace separator
53
54
  * (`config.tool_name_invalid`), an egress tool declared other than
54
55
  * `write` (`config.egress_requires_write_effect` — also minted, same meaning, for a protocol tool's resolved
@@ -138,7 +139,7 @@ export type NoticeAudience = "user" | "operator";
138
139
  * the union's catalog ({@link EngineNotice.code}). The groups below say only which family a row joins,
139
140
  * so the catalog is not written a second time here and the two cannot drift.
140
141
  */
141
- export declare const ENGINE_NOTICE_CODES: readonly ["config.autocompact_window_clamped", "config.env_timeout_discarded", "config.materialize_env_discarded", "config.models_swapped", "config.context_settings_swapped", "config.read_face_swapped", "config.read_face_deployment_clamped", "config.write_face_swapped", "config.write_face_deployment_clamped", "config.read_deny_swapped", "config.tool_model_gate_removed", "config.tool_model_gate_unknown_class", "config.tool_model_gate_env_invalid", "config.artifact_host_invalid", "config.task_root_not_canonical", "config.write_protection_target_view_absent", "config.path_view_unresolved", "config.execution_env_capability_invalid", "config.tool_card_undeclared", "config.tool_face_undeclared", "config.tool_face_invalid", "config.forward_sink_failed", "config.durable_gate_unavailable", "config.peer_admission_out_of_range", "config.peer_lane_unmounted", "config.secret_env_scrubbed", "config.credential_not_withheld", "config.permission_mode_unavailable", "permission.ask_released_by_mode", "permission.read_deny_unapplied", "permission.rule_store_unavailable", "permission.rule_row_dropped", "config.session_rules_earlier_grammar", "permission.allowed_tool_not_granted", "mods.registration_refused", "peer.inbound_disposition", "peer.held_settled", "peer.idle_subscription", "classifier.denial_limit", "classifier.fallback", "classifier.role_defaulted", "classifier.contract_broken", "classifier.unavailable", "checkpoint.execution_outcome_unrecorded", "delegation.transcript_integrity", "delegation.ask_unresolvable", "mcp.injection_dropped", "mcp.revocation_probe_failed", "mcp.server_redialed", "workflow.governance_key_stripped", "workflow.agent_option_ignored", "memory.session_polluted", "memory.harvest_quarantined", "memory.delegation_static_mark_waived", "memory.content_class_declared", "memory.hold_opened", "memory.hold_released", "memory.hold_disposed", "memory.consolidation_recommended", "memory.consolidation_committed", "memory.consolidation_conflict", "memory.consolidation_incomplete", "memory.consolidation_refused", "memory.consolidation_withheld", "memory.restore_withheld", "memory.project_scope_absent", "route.fallback_to_primary", "route.classifier_unroutable", "route.credential_origin_mismatch", "route.response_model_mismatch", "route.tool_call_id_folded", "route.finish_reason_unknown", "route.stream_end_ignored", "route.extra_body_reserved_dropped", "context.tool_turn_repaired", "task.user_steer_undrained", "task.user_followup_undrained", "steering.parked_input_blocked", "task.turn_interrupted", "task.halt_unconsumed", "task.late_approval", "task.interrupt_unconsumed", "task.tool_turn_incomplete", "approval.read_root_granted", "approval.read_root_grant_rejected", "memory.capture_opted_out", "memory.capture_optout_unpersisted", "skills.listing_truncated", "agents.listing_truncated", "tool_result.offload_put_failed", "tool_result.media_refused", "instructions.source_changed", "session.records_skipped", "memory.layer_locked_legacy", "server.model_unreachable", "permission.entry_mode_clamped"];
142
+ export declare const ENGINE_NOTICE_CODES: readonly ["config.autocompact_window_clamped", "config.env_timeout_discarded", "config.materialize_env_discarded", "config.models_swapped", "config.context_settings_swapped", "config.read_face_swapped", "config.read_face_deployment_clamped", "config.write_face_swapped", "config.write_face_deployment_clamped", "config.read_deny_swapped", "config.tool_model_gate_removed", "config.tool_model_gate_unknown_class", "config.tool_model_gate_env_invalid", "config.artifact_host_invalid", "config.task_root_not_canonical", "config.write_protection_target_view_absent", "config.path_view_unresolved", "config.execution_env_capability_invalid", "config.tool_card_undeclared", "config.tool_face_undeclared", "config.tool_face_invalid", "config.forward_sink_failed", "config.durable_gate_unavailable", "config.peer_admission_out_of_range", "config.peer_lane_unmounted", "config.secret_env_scrubbed", "config.credential_not_withheld", "config.cross_boundary_posture_undeclared", "config.cross_boundary_record_unwritten", "config.permission_mode_unavailable", "permission.ask_released_by_mode", "permission.read_deny_unapplied", "permission.rule_store_unavailable", "permission.rule_row_dropped", "config.session_rules_earlier_grammar", "permission.allowed_tool_not_granted", "mods.registration_refused", "peer.inbound_disposition", "peer.held_settled", "peer.idle_subscription", "classifier.denial_limit", "classifier.fallback", "classifier.role_defaulted", "classifier.contract_broken", "classifier.unavailable", "checkpoint.execution_outcome_unrecorded", "delegation.transcript_integrity", "delegation.ask_unresolvable", "mcp.injection_dropped", "mcp.revocation_probe_failed", "mcp.server_redialed", "workflow.governance_key_stripped", "workflow.agent_option_ignored", "memory.session_polluted", "memory.harvest_quarantined", "memory.delegation_static_mark_waived", "memory.content_class_declared", "memory.hold_opened", "memory.hold_released", "memory.hold_disposed", "memory.consolidation_recommended", "memory.consolidation_committed", "memory.consolidation_conflict", "memory.consolidation_incomplete", "memory.consolidation_refused", "memory.consolidation_withheld", "memory.restore_withheld", "memory.project_scope_absent", "route.fallback_to_primary", "route.classifier_unroutable", "route.credential_origin_mismatch", "route.response_model_mismatch", "route.tool_call_id_folded", "route.finish_reason_unknown", "route.stream_end_ignored", "route.extra_body_reserved_dropped", "context.tool_turn_repaired", "task.user_steer_undrained", "task.user_followup_undrained", "steering.parked_input_blocked", "task.turn_interrupted", "task.halt_unconsumed", "task.late_approval", "task.interrupt_unconsumed", "task.tool_turn_incomplete", "approval.read_root_granted", "approval.read_root_grant_rejected", "memory.capture_opted_out", "memory.capture_optout_unpersisted", "skills.listing_truncated", "agents.listing_truncated", "tool_result.offload_put_failed", "tool_result.media_refused", "instructions.source_changed", "session.records_skipped", "memory.layer_locked_legacy", "server.model_unreachable", "permission.entry_mode_clamped"];
142
143
  /** A catalogued code (see {@link ENGINE_NOTICE_CODES}); {@link NOTICE_MINTER} says who mints it. NOT the type of
143
144
  * `EngineNotice.code`, which stays `string` — a host forwarding its own notices through the same
144
145
  * sink is a supported shape, and narrowing that field would break it. */
@@ -219,6 +220,8 @@ declare const NOTICE_AUDIENCE_TABLE: {
219
220
  readonly "config.peer_admission_out_of_range": "operator";
220
221
  readonly "config.secret_env_scrubbed": "operator";
221
222
  readonly "config.credential_not_withheld": "operator";
223
+ readonly "config.cross_boundary_posture_undeclared": "operator";
224
+ readonly "config.cross_boundary_record_unwritten": "operator";
222
225
  readonly "peer.inbound_disposition": "user";
223
226
  readonly "peer.held_settled": "user";
224
227
  readonly "peer.idle_subscription": "user";
@@ -305,6 +308,8 @@ declare const NOTICE_MINTER_TABLE: {
305
308
  readonly "config.peer_lane_unmounted": "engine";
306
309
  readonly "config.secret_env_scrubbed": "engine";
307
310
  readonly "config.credential_not_withheld": "engine";
311
+ readonly "config.cross_boundary_posture_undeclared": "engine";
312
+ readonly "config.cross_boundary_record_unwritten": "engine";
308
313
  readonly "config.permission_mode_unavailable": "engine";
309
314
  readonly "permission.ask_released_by_mode": "engine";
310
315
  readonly "permission.read_deny_unapplied": "engine";
@@ -146,6 +146,8 @@ export const ENGINE_NOTICE_CODES = Object.freeze([
146
146
  "config.peer_lane_unmounted",
147
147
  "config.secret_env_scrubbed",
148
148
  "config.credential_not_withheld",
149
+ "config.cross_boundary_posture_undeclared",
150
+ "config.cross_boundary_record_unwritten",
149
151
  "config.permission_mode_unavailable",
150
152
  "permission.ask_released_by_mode",
151
153
  "permission.read_deny_unapplied",
@@ -279,6 +281,8 @@ const NOTICE_AUDIENCE_TABLE = {
279
281
  "config.peer_admission_out_of_range": "operator",
280
282
  "config.secret_env_scrubbed": "operator",
281
283
  "config.credential_not_withheld": "operator",
284
+ "config.cross_boundary_posture_undeclared": "operator",
285
+ "config.cross_boundary_record_unwritten": "operator",
282
286
  "peer.inbound_disposition": "user",
283
287
  "peer.held_settled": "user",
284
288
  "peer.idle_subscription": "user",
@@ -346,6 +350,8 @@ const NOTICE_MINTER_TABLE = {
346
350
  "config.peer_lane_unmounted": "engine",
347
351
  "config.secret_env_scrubbed": "engine",
348
352
  "config.credential_not_withheld": "engine",
353
+ "config.cross_boundary_posture_undeclared": "engine",
354
+ "config.cross_boundary_record_unwritten": "engine",
349
355
  "config.permission_mode_unavailable": "engine",
350
356
  "permission.ask_released_by_mode": "engine",
351
357
  "permission.read_deny_unapplied": "engine",
@@ -13,6 +13,7 @@ import type { WiringLegKind } from "./wiring-manifest.js";
13
13
  import type { PermissionSeatInputs } from "./mode-policy.js";
14
14
  import type { SkillGateSeats } from "./skill-grant.js";
15
15
  import type { AllowEntryGateSeats } from "./allow-entry.js";
16
+ import type { CrossBoundaryGateSeat } from "./cross-boundary-taint.js";
16
17
  /**
17
18
  * The classic stations' faces and the two-phase tool gate. A deployment extends the engine through the mods bus
18
19
  * (`RunnerDeps.mods` / `TaskSpec.mods`, `src/core/mods/`): each sema station raises one `classic.*` event, and the
@@ -554,9 +555,10 @@ export interface ToolGateResult {
554
555
  * as taken at the gate's entry, or what a hook, a policy or a person rewrote them to); re-validated by the loop. Present
555
556
  * on every allow exit, whether or not anything rewrote (compare values to tell); absent on a block and a suspend. */
556
557
  updatedInput?: unknown;
557
- /** The input this pass decided on (what a refusal refused, what an allowed call runs with) as a deep, FROZEN copy taken
558
- * at the exit — never the live args. Beside {@link gate} on both terminal exits; absent on the suspend exit. The deny
559
- * observer gets a detached, writable copy of it. */
558
+ /** The input this pass decided on (what a refusal refused, what an allowed call runs with) as a deep, FROZEN plain-data projection
559
+ * taken at the exit — never the live args: on a refusal every member that is not data (a bigint, a Map, a Date, a class instance, a
560
+ * function, an accessor, a symbol-keyed member) is a `[not recorded: …]` marker; an allow exit meets only the gate's own copy, so there
561
+ * just a bigint, a cycle's closing member and containers past 256 levels are. Beside {@link gate} on both terminal exits; absent on the suspend exit. */
560
562
  finalInput?: unknown;
561
563
  /** A blocking refusal's structured details, when the refusing station minted any (the shell read station's
562
564
  * `read_path_denied` / `readonly_out_of_root` cards): the loop attaches them to the error result it mints
@@ -611,9 +613,8 @@ export type ContentAskOutcome = {
611
613
  * happened" (no facility armed, or the closure took its sync-first decline) — that case hides nothing,
612
614
  * while this one does: the gate falls back to the synchronous approver chain either way (the
613
615
  * compensation contract — an uncommitted checkpoint is a safe fallback — is unchanged), but on a
614
- * headless deployment that fallback is the auto-deny whose text is "no approver is wired". True as far
615
- * as it goes, and the whole reason the call did not park lives somewhere else entirely (the host's
616
- * `onError` face). So the reason travels with the fall-through and the deny quotes it.
616
+ * headless deployment that fallback is the auto-deny whose text is "no approver is wired". True as far as it goes, and the whole
617
+ * reason the call did not park lives somewhere else entirely (the host's `onError` face). So the reason travels with the fall-through and the deny quotes it.
617
618
  */
618
619
  export interface ParkAttemptFailed {
619
620
  /** Why the park could not be minted — a sentence, already worded for a reader. */
@@ -720,7 +721,7 @@ export type PersistedRuleAnswer = PersistedRuleHit | PersistedRuleCoverage | Per
720
721
  * signal; when the caller also supplies {@link ToolGateInput.callSignal}, the Runner's closures
721
722
  * additionally bind their waits to that per-call signal (`AbortSignal.any` of the two), so a turn
722
723
  * interrupt releases a pending ask instead of leaving it hanging on the run's lifetime alone. */
723
- export interface ToolGateInput extends PermissionSeatInputs, SkillGateSeats, AllowEntryGateSeats {
724
+ export interface ToolGateInput extends PermissionSeatInputs, SkillGateSeats, AllowEntryGateSeats, CrossBoundaryGateSeat {
724
725
  /** RB-473 batch 2b — bounded disclosure for a HOST callback that throws inside this gate call's
725
726
  * isolation scope (onHookError itself, the deny observer, the durable-park observer). The runner
726
727
  * wires this to an `observer.notify_failed` trace event; absent = counted only (unchanged). */
@@ -1094,12 +1095,11 @@ export interface ToolGateInput extends PermissionSeatInputs, SkillGateSeats, All
1094
1095
  * The deny exit nobody judged: the call's approval could not be RESOLVED (`cause` — the approver
1095
1096
  * answered `unavailable`, no approver is wired, or an inherited durable mandate has no park facility on
1096
1097
  * this leg) and no durable park took the call, so the fail-closed deny STANDS. Fired once per such call
1097
- * from the gate's single deny exit, after every park re-route has had its attempt, with the facts the
1098
- * `delegation.ask_unresolvable` notice is composed from (`ask-unresolvable-notice.ts`);
1099
- * `parkLaneExisted` = a park lane was armed for this run; `remedy` = the caller-side change that would have let the
1100
- * call run, present for the causes that say nobody could be asked (`approver_unavailable`, `no_approver_wired`).
1101
- * Observe-only: a throwing seat never alters the deny. Absent ⇒ the deny stands silently (a host driving the gate
1102
- * directly, no notice sink).
1098
+ * from the gate's single deny exit, after every park re-route has had its attempt, with the facts the `delegation.ask_unresolvable`
1099
+ * notice is composed from (`ask-unresolvable-notice.ts`); `parkLaneExisted` = a park lane was armed for this run; `remedy` = the
1100
+ * caller-side change that would have let the call run, present for the causes that say nobody could be asked (`approver_unavailable`,
1101
+ * `no_approver_wired`) — for arguments no approval can be bound to, the binding's own sentence (the remedy's approver half would not
1102
+ * let them run). Observe-only: a throwing seat never alters the deny. Absent ⇒ the deny stands silently (no notice sink).
1103
1103
  */
1104
1104
  onAskUnresolvable?: (info: {
1105
1105
  toolName: string;
@@ -1311,7 +1311,7 @@ export declare function createPreToolUseScreening(input: Pick<ToolGateInput, "ev
1311
1311
  * structural rather than restated.
1312
1312
  *
1313
1313
  * The mapping mirrors the gate's own phase 1 decision for decision:
1314
- * - no opinion → `allow`, contributing nothing;
1314
+ * - no opinion, and an `allow` → `allow`, contributing nothing (a constraint only tightens: it clears no question);
1315
1315
  * - `deny` → `deny` in the fold's own words;
1316
1316
  * - `ask` → `ask`, stamped `decisionReason:"hook"` (and the mandate, when the answer set it) so every consumer that
1317
1317
  * distinguishes a screening ask from a policy ask reads the same fact one level down;
@@ -186,8 +186,7 @@ export function createPreToolUseConstraintPolicy(chain, env) {
186
186
  ...(r.updatedInput !== undefined ? { updatedInput: r.updatedInput } : {}),
187
187
  };
188
188
  }
189
- const stated = r.action === "allow" ? { decisionReason: "hook" } : {};
190
- return r.updatedInput !== undefined ? { action: "allow", updatedInput: r.updatedInput, ...stated } : { action: "allow", ...stated };
189
+ return r.updatedInput !== undefined ? { action: "allow", updatedInput: r.updatedInput } : { action: "allow" };
191
190
  },
192
191
  }, "hook");
193
192
  recordScreeningChain(minted, chain);
@@ -109,7 +109,7 @@ export declare function resolveLockedKeys(config: LockedConfig | undefined): Rea
109
109
  * and a peer's `headers` and `allowSkills` are the copy's own, and a host that edits its own nested object after
110
110
  * the door (a header value, an `allowSkills.push`) reaches nothing the engine holds: the dial, a refresh and a
111
111
  * re-dial all work from this copy, on both surfaces. The axes are then judged on that copy by their own reader
112
- * ({@link judgeEntryToolAxes}).
112
+ * ({@link judgeEntryToolAxes}), and an MCP server's faces by theirs ({@link judgeEntryToolFaces}).
113
113
  *
114
114
  * There is no third outcome between "copied whole" and "refused": a value the copy cannot carry whole is a
115
115
  * declaration the engine cannot read — a Proxy at any level, an object whose prototype is neither
@@ -167,6 +167,33 @@ export declare function judgeEntryToolAxes<E extends McpServerSpec | A2aServerSp
167
167
  code: string;
168
168
  };
169
169
  };
170
+ /**
171
+ * The `toolFaces` question for ONE snapshot entry of an MCP server — asked by the preparation door and by the materialization entrance exactly
172
+ * as {@link judgeEntryToolAxes} asks its own, so the engine reads a face declaration one way (`readToolFaces`) and the mount consumes the copy
173
+ * that way gave it, never the caller's object: a mistyped member, an axis word written as a face member, a member of the wrong kind or an empty
174
+ * tool name is a refusal here, not a protection dropped in silence or a throw at the listing. `ok: true` carries the entry with the reader's
175
+ * frozen copy in place of the declaration (an empty record declares nothing and leaves no key); `ok: false` carries the coded refusal
176
+ * (`config.tool_face_invalid`) naming the server, the tool and the member.
177
+ */
178
+ export declare function judgeEntryToolFaces(copy: McpServerSpec): {
179
+ ok: true;
180
+ entry: McpServerSpec;
181
+ } | {
182
+ ok: false;
183
+ error: Error & {
184
+ code: string;
185
+ };
186
+ };
187
+ /** Everything a snapshot MCP entry declares per tool, judged: its `toolAxes` ({@link judgeEntryToolAxes}), then its `toolFaces` ({@link judgeEntryToolFaces}). */
188
+ export declare function judgeMcpEntryDeclarations(copy: McpServerSpec): {
189
+ ok: true;
190
+ entry: McpServerSpec;
191
+ } | {
192
+ ok: false;
193
+ error: Error & {
194
+ code: string;
195
+ };
196
+ };
170
197
  /**
171
198
  * The single preflight resolver: validate the lock declaration, refuse a spec that tries to occupy
172
199
  * a locked slot (`config.locked_key`, two-state — the whole prepare is rejected, nothing is
@@ -1,6 +1,7 @@
1
1
  import { types } from "node:util";
2
- import { DECLARATION_PATH_RULES, declarationFaultOf, declarerWord, readToolAxes, receiveDeclaration, toolAxesRefusal } from "./mcp-server-spec.js";
2
+ import { DECLARATION_PATH_RULES, NO_DECLARATION_PATH_RULES, declarationFaultOf, declarerWord, readToolAxes, receiveDeclaration, toolAxesRefusal } from "./mcp-server-spec.js";
3
3
  import { isPlainRecord, receive } from "./mods/kernel/receive.js";
4
+ import { readToolFaces } from "./tool-face-reader.js";
4
5
  import { CONFIG_RECEIPT_MODE } from "./own-property-receipt.js";
5
6
  import { isToolContentOrigin, TOOL_CONTENT_ORIGINS } from "./memory-engine/content-origin.js";
6
7
  import { inlineUntrusted } from "./untrusted-text.js";
@@ -32,7 +33,7 @@ export function resolveLockedKeys(config) {
32
33
  return keys;
33
34
  }
34
35
  function declaredLockKeys(config) {
35
- const received = receiveDeclaration(config);
36
+ const received = receiveDeclaration(config, NO_DECLARATION_PATH_RULES);
36
37
  if (!received.ok)
37
38
  throw lockDeclarationRefusal(memberPath(received.fault.path, "RunnerDeps.lockedConfig"), `cannot be read: ${received.fault.problem}`);
38
39
  const copy = received.value;
@@ -116,6 +117,32 @@ export function judgeEntryToolAxes(copy, at) {
116
117
  }
117
118
  return { ok: true, entry: Object.freeze((axes.toolAxes === undefined ? rest : { ...rest, toolAxes: axes.toolAxes })) };
118
119
  }
120
+ function deepFrozen(value) {
121
+ if (typeof value === "object" && value !== null && !Object.isFrozen(value)) {
122
+ Object.freeze(value);
123
+ for (const member of Object.values(value))
124
+ deepFrozen(member);
125
+ }
126
+ return value;
127
+ }
128
+ function toolFacesRefusal(name, problem) {
129
+ return codedError("config.tool_face_invalid", `${declarerWord("mcp")} ${name} declares toolFaces the engine cannot read: ${inlineUntrusted(problem, 240)}. A per-tool face declaration is the deployment's own ` +
130
+ `narrowing (which argument names a tool's path, how a write is confined, which approval card and rule vocabulary the tool wears), so it is refused ` +
131
+ `rather than partly applied — a member dropped or a word defaulted would mount the tool without the protection its operator declared.`);
132
+ }
133
+ export function judgeEntryToolFaces(copy) {
134
+ const { toolFaces: declared, ...rest } = copy;
135
+ if (declared === undefined)
136
+ return { ok: true, entry: copy };
137
+ const faces = readToolFaces(declared);
138
+ if (!faces.ok)
139
+ return { ok: false, error: toolFacesRefusal(declarerNameOf(copy.name), faces.problem) };
140
+ return { ok: true, entry: Object.freeze((faces.faces === undefined ? rest : { ...rest, toolFaces: deepFrozen(faces.faces) })) };
141
+ }
142
+ export function judgeMcpEntryDeclarations(copy) {
143
+ const axes = judgeEntryToolAxes(copy, "mcp");
144
+ return axes.ok ? judgeEntryToolFaces(axes.entry) : axes;
145
+ }
119
146
  function declaredEntriesOf(declared, at) {
120
147
  const field = at;
121
148
  const received = receive(declared, CONFIG_RECEIPT_MODE);
@@ -157,7 +184,7 @@ function snapshotAndValidateMcpEntries(declared) {
157
184
  `than folded to a class, because folding it would let a deployment believe a declaration is in force while every ` +
158
185
  `call keeps writing a one-way durable mark.`);
159
186
  }
160
- const judged = judgeEntryToolAxes(copy, "mcp");
187
+ const judged = judgeMcpEntryDeclarations(copy);
161
188
  if (!judged.ok)
162
189
  throw judged.error;
163
190
  return judged.entry;
@@ -260,7 +260,7 @@ export interface LspServerManager {
260
260
  * ExecutionEnv (the SAME sandbox the agent runs in) — core passes it because a real in-env language
261
261
  * server must run inside that sandbox, and core already holds the per-task env at mount time. Supplying
262
262
  * it here means a manager can be **stateless** (it doesn't need a sessionId→env registry). It is
263
- * optional/last for back-compat: a manager that resolves the env another way (e.g. a sessionId registry
263
+ * optional/last by contract: a manager that resolves the env another way (e.g. a sessionId registry
264
264
  * keyed off `executionEnvFactory`) may ignore it.
265
265
  */
266
266
  sessionFor(filePath: string, signal?: AbortSignal, env?: ExecutionEnv): Promise<LspSession | undefined>;