@sema-agent/core 7.50.2 → 7.51.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 (200) hide show
  1. package/CHANGELOG.md +313 -0
  2. package/dist/agents/child-failure.d.ts +5 -16
  3. package/dist/agents/child-failure.js +4 -24
  4. package/dist/agents/subagent-steps.js +2 -2
  5. package/dist/agents/subagent.d.ts +4 -13
  6. package/dist/agents/subagent.js +94 -204
  7. package/dist/agents/tool-filter.d.ts +2 -2
  8. package/dist/brain/anthropic.d.ts +1 -1
  9. package/dist/brain/anthropic.js +7 -5
  10. package/dist/brain/open-responses.d.ts +1 -1
  11. package/dist/brain/open-responses.js +3 -2
  12. package/dist/brain/openai.d.ts +1 -1
  13. package/dist/brain/openai.js +3 -2
  14. package/dist/brain/request-route.d.ts +2 -2
  15. package/dist/brain/route-adjudicator.d.ts +16 -7
  16. package/dist/brain/route-adjudicator.js +23 -7
  17. package/dist/brain/route-conformance.js +23 -5
  18. package/dist/brain/stream-engine.js +9 -3
  19. package/dist/core/a2a.js +6 -8
  20. package/dist/core/allow-entry.d.ts +45 -26
  21. package/dist/core/allow-entry.js +48 -24
  22. package/dist/core/arg-summary.d.ts +8 -85
  23. package/dist/core/arg-summary.js +4 -386
  24. package/dist/core/ask-origin.d.ts +35 -33
  25. package/dist/core/ask-origin.js +9 -11
  26. package/dist/core/checkpoint-requirements.d.ts +6 -0
  27. package/dist/core/checkpoint-requirements.js +1 -0
  28. package/dist/core/checkpoint-store.d.ts +15 -34
  29. package/dist/core/checkpoint-store.js +5 -4
  30. package/dist/core/classifier-unavailable-arm.d.ts +12 -8
  31. package/dist/core/classifier-unavailable-arm.js +7 -2
  32. package/dist/core/credential-vocabulary.d.ts +3 -2
  33. package/dist/core/credential-vocabulary.js +27 -18
  34. package/dist/core/delegation-frames.d.ts +40 -15
  35. package/dist/core/delegation-frames.js +34 -0
  36. package/dist/core/engine-capabilities.d.ts +15 -0
  37. package/dist/core/engine-capabilities.js +1 -0
  38. package/dist/core/engine-notice.d.ts +41 -14
  39. package/dist/core/gate-fold.js +9 -7
  40. package/dist/core/gate-lanes.js +6 -4
  41. package/dist/core/gate-outcome.d.ts +81 -15
  42. package/dist/core/gate-outcome.js +168 -67
  43. package/dist/core/governance-codes.d.ts +7 -1
  44. package/dist/core/governance-codes.js +9 -0
  45. package/dist/core/hooks.d.ts +4 -4
  46. package/dist/core/hooks.js +4 -3
  47. package/dist/core/mcp-connection-pool.d.ts +2 -0
  48. package/dist/core/mcp-connection-pool.js +2 -1
  49. package/dist/core/mcp-failure.js +2 -1
  50. package/dist/core/mcp-server-spec.d.ts +27 -5
  51. package/dist/core/mcp.d.ts +13 -38
  52. package/dist/core/mcp.js +28 -23
  53. package/dist/core/media-byte-cap.d.ts +25 -0
  54. package/dist/core/media-byte-cap.js +18 -2
  55. package/dist/core/memory-engine/consolidation-driver.js +1 -1
  56. package/dist/core/mode-policy.d.ts +25 -1
  57. package/dist/core/mode-policy.js +28 -13
  58. package/dist/core/own-property-receipt.d.ts +18 -0
  59. package/dist/core/own-property-receipt.js +39 -0
  60. package/dist/core/permission-rule-model.d.ts +32 -11
  61. package/dist/core/permission-rule-model.js +12 -7
  62. package/dist/core/persisted-rule-arms.d.ts +3 -3
  63. package/dist/core/prompt-section-seat.d.ts +54 -0
  64. package/dist/core/prompt-section-seat.js +49 -0
  65. package/dist/core/protocol-http-auth.d.ts +44 -0
  66. package/dist/core/protocol-http-auth.js +45 -0
  67. package/dist/core/read-face-seats.d.ts +30 -0
  68. package/dist/core/read-face-seats.js +38 -0
  69. package/dist/core/redaction-passes.d.ts +82 -0
  70. package/dist/core/redaction-passes.js +378 -0
  71. package/dist/core/remote-env.d.ts +4 -3
  72. package/dist/core/resume-startup-cause.d.ts +56 -0
  73. package/dist/core/resume-startup-cause.js +43 -0
  74. package/dist/core/rule-denial-sentence.d.ts +2 -2
  75. package/dist/core/runner/announce-once-ledger.js +2 -2
  76. package/dist/core/runner/classifier-ladder.d.ts +17 -13
  77. package/dist/core/runner/classifier-ladder.js +12 -10
  78. package/dist/core/runner/construction-doors.js +9 -20
  79. package/dist/core/runner/contracts.d.ts +4 -4
  80. package/dist/core/runner/gate-exit.js +2 -2
  81. package/dist/core/runner/memory-consolidation-write-judge.d.ts +53 -9
  82. package/dist/core/runner/memory-consolidation-write-judge.js +39 -1
  83. package/dist/core/runner/permission-rule-lanes.d.ts +8 -13
  84. package/dist/core/runner/person-path-rows.d.ts +4 -3
  85. package/dist/core/runner/policy-throw.js +4 -4
  86. package/dist/core/runner/prepare-caps-and-workflow.d.ts +4 -3
  87. package/dist/core/runner/prepare-caps-and-workflow.js +8 -6
  88. package/dist/core/runner/prepare-gate-stations.js +4 -4
  89. package/dist/core/runner/prepare-hands-readface.js +1 -19
  90. package/dist/core/runner/prepare-inherited-gate.js +10 -9
  91. package/dist/core/runner/prepare-policy-chain.d.ts +3 -5
  92. package/dist/core/runner/prepare-policy-chain.js +10 -13
  93. package/dist/core/runner/prepare-project-context.d.ts +4 -4
  94. package/dist/core/runner/prepare-project-context.js +2 -1
  95. package/dist/core/runner/prepare-prompt-assembly.js +1 -0
  96. package/dist/core/runner/prepare-prompt-inputs.d.ts +1 -1
  97. package/dist/core/runner/prepare-prompt-inputs.js +4 -4
  98. package/dist/core/runner/prepare-protocol-tools.d.ts +4 -3
  99. package/dist/core/runner/prepare-protocol-tools.js +3 -2
  100. package/dist/core/runner/prepare-task.js +6 -4
  101. package/dist/core/runner/prepare-turn-wiring.d.ts +16 -13
  102. package/dist/core/runner/prepare-turn-wiring.js +8 -7
  103. package/dist/core/runner/run-harness-handlers.js +2 -2
  104. package/dist/core/runner/run-leg.js +10 -9
  105. package/dist/core/runner/run-settle-and-teardown.js +5 -5
  106. package/dist/core/runner/run-turn-boundary.js +3 -2
  107. package/dist/core/runner/runtask.d.ts +1 -2
  108. package/dist/core/runner/runtask.js +8 -12
  109. package/dist/core/runner/session-rule-policy.d.ts +15 -11
  110. package/dist/core/runner/session-rule-policy.js +43 -33
  111. package/dist/core/runner/stream-lifecycle-verbs.js +2 -2
  112. package/dist/core/runner/stream-settle-backstop.js +2 -1
  113. package/dist/core/runner/sub-agent-deny-lists.d.ts +12 -8
  114. package/dist/core/runner/sub-agent-deny-lists.js +23 -12
  115. package/dist/core/runner/terminal-account.js +3 -3
  116. package/dist/core/runner/tool-output-projection.d.ts +5 -0
  117. package/dist/core/runner/tool-output-projection.js +6 -11
  118. package/dist/core/runner/tool-result-media-seat.d.ts +19 -0
  119. package/dist/core/runner/tool-result-media-seat.js +13 -0
  120. package/dist/core/runner/turn-attachments.d.ts +13 -12
  121. package/dist/core/runner/turn-attachments.js +11 -7
  122. package/dist/core/runner-deps.d.ts +4 -3
  123. package/dist/core/safety-merge-corpus.js +21 -21
  124. package/dist/core/secret-env.d.ts +41 -15
  125. package/dist/core/secret-env.js +85 -19
  126. package/dist/core/session-policy-store.d.ts +89 -72
  127. package/dist/core/session-policy-store.js +100 -93
  128. package/dist/core/shell-read-silence.d.ts +38 -4
  129. package/dist/core/shell-read-silence.js +102 -0
  130. package/dist/core/side-query.d.ts +7 -8
  131. package/dist/core/skill-grant-binding.d.ts +32 -11
  132. package/dist/core/skill-grant-binding.js +11 -3
  133. package/dist/core/skill-grant.d.ts +4 -3
  134. package/dist/core/skill-grant.js +5 -4
  135. package/dist/core/skills-directory.d.ts +12 -1
  136. package/dist/core/skills-directory.js +17 -8
  137. package/dist/core/spec-contract.d.ts +5 -3
  138. package/dist/core/store-contracts/checkpoint-store-contract.d.ts +2 -0
  139. package/dist/core/store-contracts/checkpoint-store-contract.js +3 -0
  140. package/dist/core/swappable-deps.d.ts +15 -1
  141. package/dist/core/swappable-deps.js +25 -1
  142. package/dist/core/synthetic-read-verdict.d.ts +16 -14
  143. package/dist/core/synthetic-read-verdict.js +28 -8
  144. package/dist/core/task-notification.d.ts +13 -3
  145. package/dist/core/task-notification.js +12 -1
  146. package/dist/core/task-spec.d.ts +14 -15
  147. package/dist/core/tool-name-readings.d.ts +0 -4
  148. package/dist/core/tool-name-readings.js +0 -1
  149. package/dist/core/tool-policy.d.ts +40 -9
  150. package/dist/core/tool-policy.js +25 -7
  151. package/dist/core/types.d.ts +3 -3
  152. package/dist/core/types.js +1 -1
  153. package/dist/core/untrusted-egress.d.ts +60 -11
  154. package/dist/core/untrusted-egress.js +120 -9
  155. package/dist/core/untrusted-text.js +16 -0
  156. package/dist/core/wiring-manifest.d.ts +22 -8
  157. package/dist/core/wiring-manifest.js +12 -4
  158. package/dist/engine/execution-env/kill-tree.js +4 -4
  159. package/dist/engine/execution-env/node-execution-env.d.ts +11 -4
  160. package/dist/engine/execution-env/node-execution-env.js +9 -4
  161. package/dist/engine/harness/agent-harness.d.ts +2 -1
  162. package/dist/engine/harness/agent-harness.js +4 -1
  163. package/dist/engine/harness/types.d.ts +3 -0
  164. package/dist/engine/llm/types.d.ts +7 -9
  165. package/dist/engine/loop/agent-loop.d.ts +8 -3
  166. package/dist/engine/loop/agent-loop.js +26 -9
  167. package/dist/engine/loop/types.d.ts +10 -0
  168. package/dist/engine/lsp/node-lsp-manager.d.ts +2 -1
  169. package/dist/engine/lsp/node-lsp-manager.js +2 -2
  170. package/dist/index.d.ts +22 -5
  171. package/dist/index.js +18 -5
  172. package/dist/orchestration/run-workflow-tool.js +8 -8
  173. package/dist/orchestration/workflow.js +2 -2
  174. package/dist/prompt-assembly/assemble.d.ts +3 -1
  175. package/dist/prompt-assembly/assemble.js +6 -0
  176. package/dist/prompts/cowork.d.ts +42 -0
  177. package/dist/prompts/cowork.js +529 -0
  178. package/dist/prompts/default.d.ts +14 -1
  179. package/dist/prompts/default.js +3 -0
  180. package/dist/stores/file/fs-atomic.js +2 -2
  181. package/dist/stores/file/session-policy-store.d.ts +6 -3
  182. package/dist/stores/file/session-policy-store.js +5 -16
  183. package/dist/tools/artifact/artifact-tool.js +2 -0
  184. package/dist/tools/fs/image-admission.d.ts +57 -4
  185. package/dist/tools/fs/image-admission.js +85 -0
  186. package/dist/tools/fs/path-containment.d.ts +2 -0
  187. package/dist/tools/fs/path-containment.js +5 -0
  188. package/dist/tools/fs/shell-read-check.d.ts +31 -6
  189. package/dist/tools/fs/shell-read-check.js +2 -4
  190. package/dist/tools/fs/shell-read-environment.d.ts +10 -25
  191. package/dist/tools/fs/shell-read-environment.js +6 -18
  192. package/dist/tools/fs/shell-read-establish.d.ts +3 -2
  193. package/dist/tools/fs/shell-read-establish.js +5 -5
  194. package/dist/tools/fs/shell-read-judge.d.ts +38 -8
  195. package/dist/tools/fs/shell-read-judge.js +12 -10
  196. package/dist/tools/web.js +2 -2
  197. package/package.json +1 -1
  198. package/test/export-surface.snapshot.json +112 -10
  199. package/wire-changes/core@7.51.0.json +2421 -0
  200. package/wire-changes/core@7.51.1.json +259 -0
@@ -1,4 +1,4 @@
1
- import { isAskClass, isAskOrigin, isModeReleaseReason, isPermissionMode, ASK_FACES, releasableAskClassesFor } from "./ask-origin.js";
1
+ import { isAskClass, isAskOrigin, isModeReleaseReason, isPermissionMode, isPersistedRuleMandate, ASK_FACES, releasableAskClassesFor } from "./ask-origin.js";
2
2
  import { isAutoModeFallbackCause } from "./auto-mode.js";
3
3
  export const SETTLEMENT_KINDS = Object.freeze([
4
4
  "human_allowed",
@@ -108,85 +108,186 @@ export const DENIED_BY_MAY_CARRY_CAUSE = {
108
108
  read_boundary: false,
109
109
  mode: false,
110
110
  };
111
+ const retired = (word, retiredIn) => Object.freeze({ word, retiredIn });
112
+ export const RETIRED_MEMBERS = Object.freeze({
113
+ askClass: Object.freeze([retired("write_protection_unresolved", "7.49.0"), retired("governance_unavailable", "7.51.0")]),
114
+ modeReleaseReason: Object.freeze([retired("write_protection_unresolved", "7.49.0"), retired("rule_unreadable", "7.50.0")]),
115
+ askOrigin: Object.freeze([retired("unresolvable", "7.14.0")]),
116
+ persistedRuleMandate: Object.freeze([retired("write_protection_unresolved", "7.49.0")]),
117
+ deniedBy: Object.freeze([retired("plan_mode", "7.34.0")]),
118
+ settlementKind: Object.freeze([]),
119
+ settlementParty: Object.freeze([]),
120
+ classifierDenyCause: Object.freeze([]),
121
+ permissionMode: Object.freeze([]),
122
+ autoModeFallbackCause: Object.freeze([]),
123
+ });
124
+ const LIVE_MEMBER = Object.freeze({
125
+ "askClass": isAskClass,
126
+ "modeReleaseReason": isModeReleaseReason,
127
+ "askOrigin": isAskOrigin,
128
+ "persistedRuleMandate": isPersistedRuleMandate,
129
+ "deniedBy": isDeniedBy,
130
+ "settlementKind": isSettlementKind,
131
+ "settlementParty": isSettlementParty,
132
+ "classifierDenyCause": isClassifierDenyCause,
133
+ "permissionMode": isPermissionMode,
134
+ "autoModeFallbackCause": isAutoModeFallbackCause,
135
+ });
136
+ function retiredInOf(set, v) {
137
+ return RETIRED_MEMBERS[set].find((r) => r.word === v)?.retiredIn;
138
+ }
139
+ export function isRetiredMember(set, word) {
140
+ if (typeof set !== "string" || !Object.hasOwn(RETIRED_MEMBERS, set))
141
+ throw new TypeError(`isRetiredMember: ${JSON.stringify(String(set))} is not a closed set a gate record carries (${Object.keys(RETIRED_MEMBERS).join(", ")})`);
142
+ return retiredInOf(set, word) !== undefined;
143
+ }
111
144
  export function releasePolicyDefects(g) {
112
- const d = g.disposition;
113
- if (d.kind !== "allowed" || d.released === undefined)
114
- return [];
115
- const askClass = d.released.askClass;
116
- if (!isAskClass(askClass))
145
+ const d = objectOf(objectOf(g)?.disposition);
146
+ const r = d?.kind === "allowed" ? objectOf(d.released) : undefined;
147
+ const askClass = r?.askClass;
148
+ const classes = r?.classes;
149
+ if (!isAskClass(askClass) || !Array.isArray(classes) || !classes.every(isAskClass))
117
150
  return [];
118
- return ASK_FACES.some((face) => releasableAskClassesFor(face).includes(askClass)) ? [] : [`I7: released.askClass "${askClass}" is not a releasable class under this engine's table`];
151
+ if (!ASK_FACES.some((face) => releasableAskClassesFor(face).includes(askClass)))
152
+ return [`I7: released.askClass "${askClass}" is not a releasable class under this engine's table`];
153
+ const held = [askClass, ...classes];
154
+ return ASK_FACES.some((face) => held.every((c) => releasableAskClassesFor(face).includes(c))) ? [] : [`I7: released.classes ${JSON.stringify(classes)} is not a set one face of this engine's table releases`];
155
+ }
156
+ const DENY_ONLY_MEMBERS = Object.freeze(["deniedBy", "cause", "mode", "askClass", "classes"]);
157
+ function objectOf(v) {
158
+ return typeof v === "object" && v !== null && !Array.isArray(v) ? v : undefined;
159
+ }
160
+ function shapeOf(v) {
161
+ return v === null ? "null" : Array.isArray(v) ? "an array" : typeof v === "object" ? "an object" : typeof v;
119
162
  }
120
- export function screenGateOutcome(g) {
163
+ function spelled(v) {
164
+ return v === null || (typeof v !== "object" && typeof v !== "function") ? `"${String(v)}"` : `(${shapeOf(v)})`;
165
+ }
166
+ function readingOf(opts) {
167
+ if (opts === undefined)
168
+ return "archive";
169
+ if (typeof opts !== "object" || opts === null)
170
+ throw new TypeError(`screenGateOutcome: the options are an object ({ reading?: "archive" | "producer" }), not ${opts === null ? "null" : typeof opts}`);
171
+ const reading = opts.reading;
172
+ if (reading === undefined || reading === "archive" || reading === "producer")
173
+ return reading ?? "archive";
174
+ throw new TypeError(`screenGateOutcome: reading ${typeof reading === "string" ? JSON.stringify(reading) : typeof reading} is not a reading (archive, producer)`);
175
+ }
176
+ export function screenGateOutcome(g, opts) {
177
+ const producer = readingOf(opts) === "producer";
178
+ const member = (set, v) => LIVE_MEMBER[set](v) || (!producer && retiredInOf(set, v) !== undefined);
179
+ const outside = (field, set, v) => {
180
+ const was = retiredInOf(set, v);
181
+ return `${field} ${spelled(v)} is outside the closed set${was !== undefined ? ` (retired in ${was})` : ""}`;
182
+ };
183
+ const classList = (v) => Array.isArray(v) && v.length > 0 && v.every((c) => member("askClass", c));
184
+ const notRecord = (field, v) => `${field} is not an object (${shapeOf(v)})`;
185
+ const record = objectOf(g);
186
+ if (record === undefined)
187
+ return [notRecord("the gate record", g)];
121
188
  const defects = [];
122
- const d = g.disposition;
123
- if (d.kind === "denied" && !isDeniedBy(d.deniedBy))
124
- defects.push(`disposition.deniedBy "${String(d.deniedBy)}" is outside the closed set`);
125
- const s = g.settlement;
126
- if (s !== undefined && !isSettlementKind(s.kind))
127
- defects.push(`settlement.kind "${String(s.kind)}" is outside the closed set`);
128
- const party = s === undefined ? undefined : s.who?.party;
129
- if (s !== undefined && !isSettlementParty(party))
130
- defects.push(`settlement.who.party "${String(party)}" is outside the closed set`);
131
- if (g.origin !== undefined && !isAskOrigin(g.origin))
132
- defects.push(`origin "${String(g.origin)}" is outside the closed set`);
133
- if ((s !== undefined) !== (g.origin !== undefined))
189
+ const d = objectOf(record.disposition);
190
+ if (d === undefined)
191
+ defects.push(notRecord("disposition", record.disposition));
192
+ else if (d.kind !== "allowed" && d.kind !== "denied")
193
+ defects.push(`disposition.kind ${spelled(d.kind)} is neither allowed nor denied`);
194
+ const denied = d?.kind === "denied" ? d : undefined;
195
+ const allowed = d?.kind === "allowed" ? d : undefined;
196
+ const layer = denied !== undefined && member("deniedBy", denied.deniedBy);
197
+ if (denied !== undefined && !layer)
198
+ defects.push(outside("disposition.deniedBy", "deniedBy", denied.deniedBy));
199
+ const settled = record.settlement !== undefined;
200
+ const s = objectOf(record.settlement);
201
+ if (settled && s === undefined)
202
+ defects.push(notRecord("settlement", record.settlement));
203
+ if (s !== undefined && !member("settlementKind", s.kind))
204
+ defects.push(outside("settlement.kind", "settlementKind", s.kind));
205
+ const party = objectOf(s?.who)?.party;
206
+ if (s !== undefined && !member("settlementParty", party))
207
+ defects.push(outside("settlement.who.party", "settlementParty", party));
208
+ if (record.origin !== undefined && !member("askOrigin", record.origin))
209
+ defects.push(outside("origin", "askOrigin", record.origin));
210
+ if (settled !== (record.origin !== undefined))
134
211
  defects.push("I1: settlement and origin must be present together — they describe the same ask");
135
- if (d.kind === "denied" && d.deniedBy === "ask_resolution" && (s === undefined || !isSettlementKind(s.kind) || !SETTLEMENT_IS_REFUSAL[s.kind])) {
212
+ const kind = s !== undefined && isSettlementKind(s.kind) ? s.kind : undefined;
213
+ if (denied?.deniedBy === "ask_resolution" && (kind === undefined || !SETTLEMENT_IS_REFUSAL[kind]))
136
214
  defects.push("I2: deniedBy ask_resolution requires a settlement whose kind is a refusal");
215
+ const approval = kind !== undefined && !SETTLEMENT_IS_REFUSAL[kind];
216
+ if (approval && denied !== undefined && layer && !(isDeniedBy(denied.deniedBy) && DENIED_BY_MAY_VETO[denied.deniedBy])) {
217
+ defects.push(`I3: a ${kind} settlement beside a deny must name a veto layer, not "${String(denied.deniedBy)}"`);
137
218
  }
138
- const approval = s !== undefined && isSettlementKind(s.kind) && !SETTLEMENT_IS_REFUSAL[s.kind];
139
- if (approval && d.kind === "denied" && isDeniedBy(d.deniedBy) && !DENIED_BY_MAY_VETO[d.deniedBy]) {
140
- defects.push(`I3: a ${s.kind} settlement beside a deny must name a veto layer, not "${d.deniedBy}"`);
141
- }
142
- if (d.kind === "allowed" && s !== undefined && !approval)
143
- defects.push(`I4: an allowed disposition cannot carry a "${String(s.kind)}" settlement`);
144
- if (d.kind === "denied" && d.cause !== undefined) {
145
- if (!isClassifierDenyCause(d.cause))
146
- defects.push(`disposition.cause "${String(d.cause)}" is outside the closed set`);
147
- else if (isDeniedBy(d.deniedBy) && !DENIED_BY_MAY_CARRY_CAUSE[d.deniedBy])
148
- defects.push(`I5: a classifier cause cannot stand beside deniedBy "${d.deniedBy}"`);
219
+ if (allowed !== undefined && s !== undefined && !approval)
220
+ defects.push(`I4: an allowed disposition cannot carry a ${spelled(s.kind)} settlement`);
221
+ if (denied !== undefined && denied.cause !== undefined) {
222
+ if (!member("classifierDenyCause", denied.cause))
223
+ defects.push(outside("disposition.cause", "classifierDenyCause", denied.cause));
224
+ else if (layer && !(isDeniedBy(denied.deniedBy) && DENIED_BY_MAY_CARRY_CAUSE[denied.deniedBy]))
225
+ defects.push(`I5: a classifier cause cannot stand beside deniedBy "${String(denied.deniedBy)}"`);
149
226
  }
150
- if (d.kind === "allowed" && d.released !== undefined) {
151
- const r = d.released;
152
- if (s !== undefined)
153
- defects.push("I7: a mode release carries no settlement — nobody answered the ask it released");
154
- if (r.mode !== "bypassPermissions")
155
- defects.push(`I7: a mode release names mode "${String(r.mode)}" — only bypassPermissions releases`);
156
- if (!isAskClass(r.askClass))
157
- defects.push(`I7: released.askClass "${String(r.askClass)}" is outside the closed set`);
158
- if (!Array.isArray(r.classes) || r.classes.length === 0 || !r.classes.every((c) => isAskClass(c)))
159
- defects.push("I7: released.classes must be a non-empty list of ask classes");
160
- if (r.origin !== undefined && !isAskOrigin(r.origin))
161
- defects.push(`I7: released.origin "${String(r.origin)}" is outside the closed set`);
162
- if (r.reason !== undefined && !isModeReleaseReason(r.reason))
163
- defects.push(`I7: released.reason "${String(r.reason)}" is outside the closed set`);
164
- else if (r.reason !== undefined && Array.isArray(r.classes) && !r.classes.includes(r.reason))
165
- defects.push(`I7: released.reason "${r.reason}" is not a member of released.classes`);
227
+ for (const k of DENY_ONLY_MEMBERS)
228
+ if (allowed !== undefined && allowed[k] !== undefined)
229
+ defects.push(`disposition.${k} stands only on a denied disposition`);
230
+ if (denied !== undefined && denied.released !== undefined)
231
+ defects.push("I7: disposition.released stands only on an allowed disposition");
232
+ if (d !== undefined && d.released !== undefined) {
233
+ const r = objectOf(d.released);
234
+ if (r === undefined)
235
+ defects.push(notRecord("I7: disposition.released", d.released));
236
+ else {
237
+ if (settled)
238
+ defects.push("I7: a mode release carries no settlement — nobody answered the ask it released");
239
+ if (r.mode !== "bypassPermissions")
240
+ defects.push(`I7: a mode release names mode ${spelled(r.mode)} — only bypassPermissions releases`);
241
+ if (!member("askClass", r.askClass))
242
+ defects.push(outside("I7: released.askClass", "askClass", r.askClass));
243
+ if (!classList(r.classes))
244
+ defects.push("I7: released.classes must be a non-empty list of ask classes");
245
+ if (r.origin !== undefined && !member("askOrigin", r.origin))
246
+ defects.push(outside("I7: released.origin", "askOrigin", r.origin));
247
+ if (r.mandate !== undefined && !member("persistedRuleMandate", r.mandate))
248
+ defects.push(outside("I7: released.mandate", "persistedRuleMandate", r.mandate));
249
+ if (r.reason !== undefined && !member("modeReleaseReason", r.reason))
250
+ defects.push(outside("I7: released.reason", "modeReleaseReason", r.reason));
251
+ else if (r.reason !== undefined && Array.isArray(r.classes) && !r.classes.includes(r.reason))
252
+ defects.push(`I7: released.reason "${String(r.reason)}" is not a member of released.classes`);
253
+ }
166
254
  }
167
- if (d.kind === "denied") {
168
- const modeRefusal = d.deniedBy === "mode";
169
- if (modeRefusal !== (d.mode !== undefined))
255
+ if (denied !== undefined) {
256
+ const modeRefusal = denied.deniedBy === "mode";
257
+ if (modeRefusal !== (denied.mode !== undefined))
170
258
  defects.push("I8: deniedBy mode and disposition.mode must be present together");
171
- if (modeRefusal && (!isPermissionMode(d.mode) || (d.mode !== "bypassPermissions" && d.mode !== "dontAsk")))
172
- defects.push(`I8: a mode refusal names mode "${String(d.mode)}" — only bypassPermissions, dontAsk and plan refuse`);
173
- if (modeRefusal && (!isAskClass(d.askClass) || !Array.isArray(d.classes) || d.classes.length === 0 || !d.classes.every((c) => isAskClass(c))))
174
- defects.push("I8: a mode refusal carries askClass and a non-empty classes list");
259
+ if (modeRefusal && denied.mode !== undefined && (!member("permissionMode", denied.mode) || (denied.mode !== "bypassPermissions" && denied.mode !== "dontAsk"))) {
260
+ defects.push(`I8: a mode refusal names mode ${spelled(denied.mode)} — only bypassPermissions and dontAsk refuse`);
261
+ }
262
+ if (modeRefusal && (denied.askClass === undefined || denied.classes === undefined))
263
+ defects.push("I8: a mode refusal carries askClass and classes");
264
+ if (denied.askClass !== undefined && !member("askClass", denied.askClass))
265
+ defects.push(outside("disposition.askClass", "askClass", denied.askClass));
266
+ if (denied.classes !== undefined && !classList(denied.classes))
267
+ defects.push("disposition.classes must be a non-empty list of ask classes");
175
268
  }
176
- const attribution = d.classifier;
177
- if (attribution !== undefined) {
269
+ if (d !== undefined && d.classifier !== undefined) {
270
+ const attribution = objectOf(d.classifier);
178
271
  const named = (v) => typeof v === "string" && v !== "";
179
- if (!named(attribution.modelRequested) || !named(attribution.modelUsed))
180
- defects.push("I6: disposition.classifier must name both the model the round was for and the model that answered");
181
- const fb = attribution.fallback;
182
- if (fb !== undefined) {
183
- if (!named(fb.from) || !named(fb.to))
184
- defects.push("I6: disposition.classifier.fallback must name the rung it left and the rung it arrived at");
185
- else if (fb.to !== attribution.modelUsed)
186
- defects.push(`I6: the fallback landed on "${fb.to}" but the record names "${attribution.modelUsed}" as the model that answered`);
187
- if (!isAutoModeFallbackCause(fb.cause))
188
- defects.push(`I6: fallback cause "${String(fb.cause)}" is outside the closed set`);
272
+ if (attribution === undefined)
273
+ defects.push(notRecord("I6: disposition.classifier", d.classifier));
274
+ else {
275
+ if (!named(attribution.modelRequested) || !named(attribution.modelUsed))
276
+ defects.push("I6: disposition.classifier must name both the model the round was for and the model that answered");
277
+ const fb = objectOf(attribution.fallback);
278
+ if (attribution.fallback !== undefined && fb === undefined)
279
+ defects.push(notRecord("I6: disposition.classifier.fallback", attribution.fallback));
280
+ if (fb !== undefined) {
281
+ if (!named(fb.from) || !named(fb.to))
282
+ defects.push("I6: disposition.classifier.fallback must name the rung it left and the rung it arrived at");
283
+ else if (fb.to !== attribution.modelUsed)
284
+ defects.push(`I6: the fallback landed on ${spelled(fb.to)} but the record names ${spelled(attribution.modelUsed)} as the model that answered`);
285
+ if (!member("autoModeFallbackCause", fb.cause))
286
+ defects.push(outside("I6: fallback cause", "autoModeFallbackCause", fb.cause));
287
+ }
189
288
  }
190
289
  }
290
+ if (producer)
291
+ defects.push(...releasePolicyDefects(g));
191
292
  return defects;
192
293
  }
@@ -137,7 +137,7 @@ export type NoticeAudience = "user" | "operator";
137
137
  * the union's catalog ({@link EngineNotice.code}). The groups below say only which family a row joins,
138
138
  * so the catalog is not written a second time here and the two cannot drift.
139
139
  */
140
- 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.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.permission_mode_unavailable", "permission.ask_released_by_mode", "permission.read_deny_unapplied", "permission.rule_store_unavailable", "permission.rule_row_dropped", "permission.allowed_tool_not_granted", "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.base_url_changed_key_unchanged", "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", "instructions.source_changed", "session.records_skipped", "memory.layer_locked_legacy", "server.model_unreachable"];
140
+ 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.permission_mode_unavailable", "permission.ask_released_by_mode", "permission.read_deny_unapplied", "permission.rule_store_unavailable", "permission.rule_row_dropped", "permission.allowed_tool_not_granted", "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.base_url_changed_key_unchanged", "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"];
141
141
  /** A catalogued code (see {@link ENGINE_NOTICE_CODES}); {@link NOTICE_MINTER} says who mints it. NOT the type of
142
142
  * `EngineNotice.code`, which stays `string` — a host forwarding its own notices through the same
143
143
  * sink is a supported shape, and narrowing that field would break it. */
@@ -199,6 +199,7 @@ declare const NOTICE_AUDIENCE_TABLE: {
199
199
  readonly "config.read_face_deployment_clamped": "operator";
200
200
  readonly "config.write_face_swapped": "operator";
201
201
  readonly "config.write_face_deployment_clamped": "operator";
202
+ readonly "config.read_deny_swapped": "operator";
202
203
  readonly "config.tool_model_gate_removed": "operator";
203
204
  readonly "config.tool_model_gate_unknown_class": "operator";
204
205
  readonly "config.tool_model_gate_env_invalid": "operator";
@@ -241,10 +242,12 @@ declare const NOTICE_AUDIENCE_TABLE: {
241
242
  readonly "skills.listing_truncated": "operator";
242
243
  readonly "agents.listing_truncated": "operator";
243
244
  readonly "tool_result.offload_put_failed": "operator";
245
+ readonly "tool_result.media_refused": "operator";
244
246
  readonly "config.forward_sink_failed": "operator";
245
247
  readonly "instructions.source_changed": "user";
246
248
  readonly "session.records_skipped": "user";
247
249
  readonly "server.model_unreachable": "user";
250
+ readonly "permission.entry_mode_clamped": "user";
248
251
  readonly "memory.layer_locked_legacy": "operator";
249
252
  };
250
253
  /** Compile-time fence: every catalogued code has an audience row (and, via `satisfies`, only those). */
@@ -280,6 +283,7 @@ declare const NOTICE_MINTER_TABLE: {
280
283
  readonly "config.read_face_deployment_clamped": "engine";
281
284
  readonly "config.write_face_swapped": "engine";
282
285
  readonly "config.write_face_deployment_clamped": "engine";
286
+ readonly "config.read_deny_swapped": "engine";
283
287
  readonly "config.tool_model_gate_removed": "engine";
284
288
  readonly "config.tool_model_gate_unknown_class": "engine";
285
289
  readonly "config.tool_model_gate_env_invalid": "engine";
@@ -357,10 +361,12 @@ declare const NOTICE_MINTER_TABLE: {
357
361
  readonly "skills.listing_truncated": "engine";
358
362
  readonly "agents.listing_truncated": "engine";
359
363
  readonly "tool_result.offload_put_failed": "engine";
364
+ readonly "tool_result.media_refused": "engine";
360
365
  readonly "instructions.source_changed": "server";
361
366
  readonly "session.records_skipped": "server";
362
367
  readonly "memory.layer_locked_legacy": "server";
363
368
  readonly "server.model_unreachable": "server";
369
+ readonly "permission.entry_mode_clamped": "server";
364
370
  };
365
371
  /** Compile-time fence: every catalogued code has a minter row (and, via `satisfies`, only those). */
366
372
  export type MinterTableCoversEveryNoticeCode = AssertAllKeysHandled<Exclude<EngineNoticeCode, keyof typeof NOTICE_MINTER_TABLE>>;
@@ -128,6 +128,7 @@ export const ENGINE_NOTICE_CODES = Object.freeze([
128
128
  "config.read_face_deployment_clamped",
129
129
  "config.write_face_swapped",
130
130
  "config.write_face_deployment_clamped",
131
+ "config.read_deny_swapped",
131
132
  "config.tool_model_gate_removed",
132
133
  "config.tool_model_gate_unknown_class",
133
134
  "config.tool_model_gate_env_invalid",
@@ -205,10 +206,12 @@ export const ENGINE_NOTICE_CODES = Object.freeze([
205
206
  "skills.listing_truncated",
206
207
  "agents.listing_truncated",
207
208
  "tool_result.offload_put_failed",
209
+ "tool_result.media_refused",
208
210
  "instructions.source_changed",
209
211
  "session.records_skipped",
210
212
  "memory.layer_locked_legacy",
211
213
  "server.model_unreachable",
214
+ "permission.entry_mode_clamped",
212
215
  ]);
213
216
  const NOTICE_AUDIENCE_TABLE = {
214
217
  "memory.session_polluted": "user",
@@ -254,6 +257,7 @@ const NOTICE_AUDIENCE_TABLE = {
254
257
  "config.read_face_deployment_clamped": "operator",
255
258
  "config.write_face_swapped": "operator",
256
259
  "config.write_face_deployment_clamped": "operator",
260
+ "config.read_deny_swapped": "operator",
257
261
  "config.tool_model_gate_removed": "operator",
258
262
  "config.tool_model_gate_unknown_class": "operator",
259
263
  "config.tool_model_gate_env_invalid": "operator",
@@ -296,10 +300,12 @@ const NOTICE_AUDIENCE_TABLE = {
296
300
  "skills.listing_truncated": "operator",
297
301
  "agents.listing_truncated": "operator",
298
302
  "tool_result.offload_put_failed": "operator",
303
+ "tool_result.media_refused": "operator",
299
304
  "config.forward_sink_failed": "operator",
300
305
  "instructions.source_changed": "user",
301
306
  "session.records_skipped": "user",
302
307
  "server.model_unreachable": "user",
308
+ "permission.entry_mode_clamped": "user",
303
309
  "memory.layer_locked_legacy": "operator",
304
310
  };
305
311
  export const NOTICE_AUDIENCE = NOTICE_AUDIENCE_TABLE;
@@ -316,6 +322,7 @@ const NOTICE_MINTER_TABLE = {
316
322
  "config.read_face_deployment_clamped": "engine",
317
323
  "config.write_face_swapped": "engine",
318
324
  "config.write_face_deployment_clamped": "engine",
325
+ "config.read_deny_swapped": "engine",
319
326
  "config.tool_model_gate_removed": "engine",
320
327
  "config.tool_model_gate_unknown_class": "engine",
321
328
  "config.tool_model_gate_env_invalid": "engine",
@@ -393,9 +400,11 @@ const NOTICE_MINTER_TABLE = {
393
400
  "skills.listing_truncated": "engine",
394
401
  "agents.listing_truncated": "engine",
395
402
  "tool_result.offload_put_failed": "engine",
403
+ "tool_result.media_refused": "engine",
396
404
  "instructions.source_changed": "server",
397
405
  "session.records_skipped": "server",
398
406
  "memory.layer_locked_legacy": "server",
399
407
  "server.model_unreachable": "server",
408
+ "permission.entry_mode_clamped": "server",
400
409
  };
401
410
  export const NOTICE_MINTER = Object.freeze(NOTICE_MINTER_TABLE);
@@ -1131,7 +1131,7 @@ export interface PersistedRuleHit {
1131
1131
  * design/252 G-2 — the answer a lane gives when it could not READ its store, or a partition of it (no `reason`; the CALL form is `reason`'s).
1132
1132
  *
1133
1133
  * The store form fails CLOSED: a deny or ask row inside what could not be read may forbid this call and an allow beside it
1134
- * is no yes, so the decision becomes a real-approval ask (origin `rule_store_unavailable`, class `governance_unavailable`:
1134
+ * is no yes, so the decision becomes a real-approval ask (origin `rule_store_unavailable`, class `rule_unreadable`:
1135
1135
  * no automatic lane clears it, `bypassPermissions` keeps it for a person, headless it is refused) until a read succeeds
1136
1136
  * again; a deny or ask row that WAS read still stands. The RECORD says `"unavailable"`, not `"no_match"`; the lane
1137
1137
  * announces the skipped partition once per run.
@@ -1139,9 +1139,9 @@ export interface PersistedRuleHit {
1139
1139
  export interface PersistedRuleUnreadable {
1140
1140
  readonly unreadable: true;
1141
1141
  /** WHY the lane could not read: absent for a store (or partition) read failure — the store form, a real-approval ask of
1142
- * class `governance_unavailable`; for a CALL the lane could not read against the person's standing deny/ask rows (an
1142
+ * class `rule_unreadable`; for a CALL the lane could not read against the person's standing deny/ask rows (an
1143
1143
  * expansion where a rule reads a word, an unterminated quote, a syntax error) the lexer's reason — the call form, an allow
1144
- * tightens to a real-approval ask of class `rule_unreadable`. Both carry the origin `rule_store_unavailable`. */
1144
+ * tightens to a real-approval ask of the same class. Both carry the origin `rule_store_unavailable`. */
1145
1145
  readonly reason?: string;
1146
1146
  }
1147
1147
  /**
@@ -1457,7 +1457,7 @@ export interface ToolGateInput extends PermissionSeatInputs, SkillGateSeats, All
1457
1457
  * small-model policy CLASSIFIER before any human/durable resolution:
1458
1458
  * - verdict `allow` → the ask resolves to allow (`decisionReason:"classifier"`) — no suspend, no onAsk;
1459
1459
  * - verdict `block` → deny (`decisionReason:"classifier"`, `source:"classifier"`; CC `automode-blocked`); `parse_error` too;
1460
- * - `unavailable` → where `unavailable.approvalSeat` (a person, live or parked) the ask goes on, its text saying why;
1460
+ * - `unavailable` → where `unavailable.route` names a person (`live_approver`, or `durable_park`) the ask goes on, its text saying why;
1461
1461
  * else CC's refusal (`cause: "unavailable"`). Never counted by the denial limit (`classifierUnavailableArm`).
1462
1462
  * SCOPE (declared semantic): this covers every surviving ask INCLUDING the deterministic egress/
1463
1463
  * irreversibility safety tightens — auto mode is the deployment's explicit choice to let the
@@ -1,4 +1,4 @@
1
- import { abortedSeatDeny, decisionText, describeThrown, receiveHookResult, isEngineRefusal } from "./tool-policy.js";
1
+ import { decisionText, describeThrown, receiveHookResult, isEngineRefusal, unaskedSeatAnswer } from "./tool-policy.js";
2
2
  import { asThrownError } from "./thrown-value.js";
3
3
  import { createSettlementLedger, engineFailClosedExit, hookDenyExit } from "./runner/gate-exit.js";
4
4
  import { traceHookCrash } from "./gate-crash-trace.js";
@@ -196,8 +196,9 @@ function screenPreToolUseResult(r) {
196
196
  export function createPreToolUseConstraintPolicy(preToolUse, env, onCrash, timeoutMs) {
197
197
  return brandPolicyAskClass({
198
198
  check: async (req, signal) => {
199
- if (signal?.aborted)
200
- return abortedSeatDeny(req, `the inherited PreToolUse screening of "${req.toolName}" was not consulted: the run or turn was already interrupted`, "hook");
199
+ const unasked = unaskedSeatAnswer(req, signal, { aborted: `the inherited PreToolUse screening of "${req.toolName}" was not consulted: the run or turn was already interrupted`, unreachable: `the inherited PreToolUse screening of "${req.toolName}" was not consulted: nobody can be asked about this call` }, "hook");
200
+ if (unasked !== undefined)
201
+ return unasked;
201
202
  let r;
202
203
  const boundMs = resolveHookTimeoutMs(timeoutMs, onCrash);
203
204
  try {
@@ -54,6 +54,8 @@ export declare class McpConnectionPool {
54
54
  /**
55
55
  * The key a declaration leases under. `kind` is the dial's own single reading of the transport kind (the dial reads it once and threads it, so a
56
56
  * transport object that answers differently to two readers cannot split the dial from its key); every other transport member is read here.
57
+ * The declaration's mount identity (`fingerprint`) is part of the key when declared: the revocation probe is asked with it, so two mounts the
58
+ * host tells apart never share one connection (an undeclared fingerprint leaves the key as it always was).
57
59
  */
58
60
  static keyOf(spec: McpServerSpec, principal: string | undefined, kind: string | undefined): string;
59
61
  /**
@@ -6,7 +6,8 @@ export class McpConnectionPool {
6
6
  shut = false;
7
7
  static keyOf(spec, principal, kind) {
8
8
  const rest = Object.fromEntries(Object.keys(spec.transport).filter((k) => k !== "kind").map((k) => [k, Reflect.get(spec.transport, k)]));
9
- return canonicalize([spec.name, kind ?? null, rest, principal ?? null]);
9
+ const fingerprint = spec.fingerprint;
10
+ return canonicalize(fingerprint === undefined ? [spec.name, kind ?? null, rest, principal ?? null] : [spec.name, kind ?? null, rest, principal ?? null, fingerprint]);
10
11
  }
11
12
  async acquire(key, dial, opts) {
12
13
  if (this.shut)
@@ -72,6 +72,7 @@ export function networkErrorCode(err, depth = 0) {
72
72
  return networkErrorCode(err.cause, depth + 1);
73
73
  }
74
74
  const isNodeErrCode = (code) => code.startsWith("ERR_");
75
+ const isDeclarationFault = (code) => code === "ERR_INVALID_URL" || (typeof code === "string" && code.startsWith("config."));
75
76
  export const FAILURE_CLASSIFIER = [
76
77
  {
77
78
  name: "sdk connection closed",
@@ -100,7 +101,7 @@ export const FAILURE_CLASSIFIER = [
100
101
  },
101
102
  {
102
103
  name: "malformed declaration",
103
- when: (err, at) => at.phase === "connect" && err instanceof Error && err.code === "ERR_INVALID_URL",
104
+ when: (err, at) => at.phase === "connect" && err instanceof Error && isDeclarationFault(err.code),
104
105
  reads: () => ({ kind: "invalid_config" }),
105
106
  },
106
107
  {
@@ -33,8 +33,9 @@ export interface McpServerSpec {
33
33
  * Per-task **end-user principal** header (design/62). When set, the Runner injects the running task's
34
34
  * authenticated {@link TaskSpec.principal} as this header (e.g. `"X-Agent-Principal"`) on every call to
35
35
  * this server's tools, so the MCP server can enforce per-user RBAC. Absent {@link TaskSpec.principal}
36
- * ⇒ the header is **not** sent (the MCP server must default to deny/public, never admin). Overrides any
37
- * same-named static `headers` entry. The principal is Runner-held and the model/worker can neither read
36
+ * ⇒ the header is **not** sent (the MCP server must default to deny/public, never admin). A static `headers`
37
+ * entry under the same name in any case refuses this server at its dial (`config.protocol_header_collision`,
38
+ * header names being case-insensitive). The principal is Runner-held and the model/worker can neither read
38
39
  * nor set it (it is not a tool argument) — see {@link TaskSpec.principal}.
39
40
  */
40
41
  principalHeader?: string;
@@ -136,6 +137,24 @@ export interface McpServerSpec {
136
137
  * plane and needs no such gate.
137
138
  */
138
139
  contentOrigin?: ToolContentOrigin;
140
+ /**
141
+ * The host's identity of THIS MOUNT of the server — a non-empty string (≤ 128 characters) the host
142
+ * minted from the declaration; the engine computes nothing from it and reads nothing off it. When present
143
+ * it rides the revocation probe ({@link McpRevocationLedger.isRevoked}) as the second argument, so the
144
+ * host can tell one mount generation of a server from a re-declared one under the same name. Absent ⇒
145
+ * the probe is asked with the name alone. Any other value refuses this server at its dial
146
+ * (`config.mcp_fingerprint_invalid`).
147
+ */
148
+ fingerprint?: string;
149
+ }
150
+ /** The identity of one mount of a declared server, as the host minted it ({@link McpServerSpec.fingerprint}). */
151
+ export interface McpMountIdentity {
152
+ readonly fingerprint: string;
153
+ }
154
+ /** The host's MCP revocation ledger ({@link RunnerDeps.mcpRevocations}) — declared once, read by every MCP dispatch outlet. */
155
+ export interface McpRevocationLedger {
156
+ /** Synchronous. `mount` is passed exactly when the server's declaration carries a `fingerprint`. */
157
+ isRevoked(serverName: string, mount?: McpMountIdentity): boolean;
139
158
  }
140
159
  /**
141
160
  * Definition of one A2A (agent-to-agent protocol) PEER to talk to for the duration of one task.
@@ -157,14 +176,17 @@ export interface A2aServerSpec {
157
176
  /** Explicit agent-card location, when the peer does not publish at a well-known path. Set ⇒ it is used
158
177
  * ALONE (no well-known probing behind the operator's back). */
159
178
  cardUrl?: string;
160
- /** Static headers sent on every request to this peer (e.g. an auth bearer). */
179
+ /** Static headers sent on every request to this peer (e.g. an auth bearer). They travel only to the
180
+ * declared origins (`url`, `cardUrl`): a card naming an endpoint on another origin refuses the peer
181
+ * (`config.protocol_endpoint_unbound`), and a redirect is not followed. */
161
182
  headers?: Record<string, string>;
162
183
  /**
163
184
  * Per-task **end-user principal** header (design/62), same contract as the MCP side: when set, the
164
185
  * Runner injects the running task's authenticated {@link TaskSpec.principal} under this header name on
165
186
  * every call to this peer, so the peer can enforce per-user authorization. Absent principal ⇒ the
166
- * header is not sent (the peer must then default to deny/public, never admin). Overrides any same-named
167
- * static entry in {@link headers}. The principal is Runner-held: the model can neither read nor set it.
187
+ * header is not sent (the peer must then default to deny/public, never admin). A static entry in
188
+ * {@link headers} under the same name in any case refuses the peer (`config.protocol_header_collision`).
189
+ * The principal is Runner-held: the model can neither read nor set it.
168
190
  */
169
191
  principalHeader?: string;
170
192
  /** Optional allowlist of skill ids to expose (others are not mounted). */