@sema-agent/core 7.56.0 → 7.57.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (173) hide show
  1. package/CHANGELOG.md +541 -0
  2. package/LICENSE +1 -1
  3. package/dist/agents/org-delivery-gate.js +3 -4
  4. package/dist/agents/peer-held-queue.d.ts +69 -49
  5. package/dist/agents/peer-held-queue.js +255 -106
  6. package/dist/agents/peer-session-drain.js +63 -43
  7. package/dist/agents/send-message-tool.js +3 -2
  8. package/dist/agents/subagent.js +8 -8
  9. package/dist/brain/anthropic.js +16 -8
  10. package/dist/brain/open-responses.js +10 -8
  11. package/dist/brain/openai.js +11 -9
  12. package/dist/brain/provider-dialect.d.ts +3 -2
  13. package/dist/brain/provider-dialect.js +2 -2
  14. package/dist/brain/request-params.d.ts +6 -0
  15. package/dist/brain/request-params.js +14 -1
  16. package/dist/brain/stream-engine.d.ts +9 -0
  17. package/dist/brain/stream-engine.js +24 -10
  18. package/dist/brain/stream-shared.d.ts +5 -4
  19. package/dist/brain/stream-shared.js +4 -4
  20. package/dist/core/allow-entry.d.ts +3 -10
  21. package/dist/core/allow-entry.js +17 -25
  22. package/dist/core/approval-object.d.ts +21 -5
  23. package/dist/core/approval-object.js +44 -17
  24. package/dist/core/ask-origin.d.ts +2 -2
  25. package/dist/core/auto-compaction.d.ts +71 -6
  26. package/dist/core/auto-compaction.js +42 -26
  27. package/dist/core/background-agent-store.d.ts +6 -2
  28. package/dist/core/background-agent-store.js +1 -1
  29. package/dist/core/checkpoint-requirements.d.ts +7 -1
  30. package/dist/core/checkpoint-requirements.js +2 -0
  31. package/dist/core/checkpoint-store.d.ts +17 -29
  32. package/dist/core/checkpoint-store.js +19 -11
  33. package/dist/core/cross-boundary-taint.js +4 -2
  34. package/dist/core/engine-notice.d.ts +3 -2
  35. package/dist/core/engine-notice.js +6 -0
  36. package/dist/core/file-history-store.d.ts +14 -8
  37. package/dist/core/file-history-store.js +14 -4
  38. package/dist/core/gate-lanes.js +2 -3
  39. package/dist/core/hooks.d.ts +2 -2
  40. package/dist/core/mailbox-store.d.ts +62 -1
  41. package/dist/core/mailbox-store.js +59 -0
  42. package/dist/core/mcp-name-judge.d.ts +46 -0
  43. package/dist/core/mcp-name-judge.js +61 -0
  44. package/dist/core/mcp-probe.d.ts +1 -1
  45. package/dist/core/mcp-probe.js +2 -5
  46. package/dist/core/mcp.js +10 -30
  47. package/dist/core/memory-engine/consolidation-driver.js +4 -3
  48. package/dist/core/memory-engine/distiller.js +26 -17
  49. package/dist/core/memory-engine/layout.js +5 -2
  50. package/dist/core/park-selfcheck.js +2 -0
  51. package/dist/core/permission-rule-consent.d.ts +5 -3
  52. package/dist/core/permission-rule-consent.js +20 -15
  53. package/dist/core/permission-rule-model.d.ts +43 -2
  54. package/dist/core/permission-rule-model.js +52 -8
  55. package/dist/core/permission-rule-org.js +1 -1
  56. package/dist/core/protocol-http-auth.d.ts +16 -2
  57. package/dist/core/protocol-http-auth.js +42 -11
  58. package/dist/core/protocol-naming.d.ts +20 -0
  59. package/dist/core/protocol-naming.js +20 -3
  60. package/dist/core/read-only-shell.d.ts +20 -5
  61. package/dist/core/read-only-shell.js +29 -14
  62. package/dist/core/remote-env.d.ts +37 -1
  63. package/dist/core/remote-env.js +11 -0
  64. package/dist/core/resume-startup-cause.d.ts +24 -2
  65. package/dist/core/resume-startup-cause.js +20 -1
  66. package/dist/core/roles.js +50 -7
  67. package/dist/core/runner/compact-session.d.ts +10 -0
  68. package/dist/core/runner/compact-session.js +202 -0
  69. package/dist/core/runner/compaction-call-options.d.ts +2 -2
  70. package/dist/core/runner/compaction-seams.d.ts +21 -1
  71. package/dist/core/runner/compaction-seams.js +11 -8
  72. package/dist/core/runner/contracts.d.ts +23 -16
  73. package/dist/core/runner/derived-route-fallback.d.ts +13 -0
  74. package/dist/core/runner/derived-route-fallback.js +6 -0
  75. package/dist/core/runner/env-capability-census.d.ts +3 -2
  76. package/dist/core/runner/env-capability-census.js +9 -3
  77. package/dist/core/runner/permission-rule-lanes.d.ts +7 -7
  78. package/dist/core/runner/permission-rule-lanes.js +17 -11
  79. package/dist/core/runner/prepare-acquire-reconcile.d.ts +3 -0
  80. package/dist/core/runner/prepare-acquire-reconcile.js +2 -1
  81. package/dist/core/runner/prepare-config-doors.d.ts +6 -0
  82. package/dist/core/runner/prepare-config-doors.js +4 -3
  83. package/dist/core/runner/prepare-hands-readface.d.ts +3 -1
  84. package/dist/core/runner/prepare-hands-readface.js +2 -0
  85. package/dist/core/runner/prepare-park-ask.d.ts +2 -0
  86. package/dist/core/runner/prepare-park-ask.js +2 -2
  87. package/dist/core/runner/prepare-policy-chain.js +2 -4
  88. package/dist/core/runner/prepare-task.d.ts +4 -1
  89. package/dist/core/runner/prepare-task.js +23 -27
  90. package/dist/core/runner/prepare-tool-disclosure-mount.d.ts +13 -0
  91. package/dist/core/runner/prepare-tool-disclosure-mount.js +13 -7
  92. package/dist/core/runner/rewind-files.d.ts +2 -5
  93. package/dist/core/runner/rewind-files.js +9 -51
  94. package/dist/core/runner/run-leg.js +10 -21
  95. package/dist/core/runner/run-notification-lane.d.ts +2 -0
  96. package/dist/core/runner/run-notification-lane.js +2 -2
  97. package/dist/core/runner/run-recovery-lanes.js +3 -4
  98. package/dist/core/runner/run-settle-and-teardown.js +5 -11
  99. package/dist/core/runner/run-terminal-adoption.js +3 -4
  100. package/dist/core/runner/run-turn-boundary.js +3 -5
  101. package/dist/core/runner/runtask.d.ts +8 -1
  102. package/dist/core/runner/runtask.js +28 -34
  103. package/dist/core/runner/send-message-continuation.js +1 -1
  104. package/dist/core/runner/session-verb-doors.d.ts +28 -0
  105. package/dist/core/runner/session-verb-doors.js +48 -0
  106. package/dist/core/runner/session-write-ledger.d.ts +30 -0
  107. package/dist/core/runner/session-write-ledger.js +101 -0
  108. package/dist/core/runner/stream-settle-backstop.js +11 -14
  109. package/dist/core/runner/sub-agent-deny-lists.d.ts +25 -3
  110. package/dist/core/runner/sub-agent-deny-lists.js +21 -10
  111. package/dist/core/runner/teardown-bounded.d.ts +1 -0
  112. package/dist/core/runner/teardown-bounded.js +1 -0
  113. package/dist/core/runner-deps.d.ts +2 -2
  114. package/dist/core/secret-env.d.ts +4 -0
  115. package/dist/core/secret-env.js +3 -0
  116. package/dist/core/session-policy-store.d.ts +7 -2
  117. package/dist/core/session-policy-store.js +1 -1
  118. package/dist/core/shell-wrapper-projection.d.ts +1 -1
  119. package/dist/core/store-contracts/background-agent-store-contract.js +1 -0
  120. package/dist/core/store-contracts/checkpoint-store-contract.js +2 -1
  121. package/dist/core/store-contracts/file-history-store-contract.js +9 -0
  122. package/dist/core/store-contracts/mailbox-store-contract.js +49 -1
  123. package/dist/core/task-registry-agent.d.ts +8 -4
  124. package/dist/core/task-registry-agent.js +14 -8
  125. package/dist/core/task-registry-shared.d.ts +35 -6
  126. package/dist/core/task-registry-shared.js +11 -0
  127. package/dist/core/task-registry.d.ts +1 -1
  128. package/dist/core/task-registry.js +1 -1
  129. package/dist/core/tool-catalog-entries.d.ts +6 -0
  130. package/dist/core/tool-catalog-entries.js +1 -0
  131. package/dist/core/tool-name-aliases.d.ts +32 -2
  132. package/dist/core/tool-name-aliases.js +23 -0
  133. package/dist/core/tool-name-readings.d.ts +4 -0
  134. package/dist/core/tool-name-readings.js +1 -0
  135. package/dist/core/tool-policy.d.ts +10 -6
  136. package/dist/core/tool-policy.js +4 -7
  137. package/dist/core/trace.d.ts +4 -1
  138. package/dist/core/trace.js +2 -1
  139. package/dist/core/untrusted-egress.d.ts +20 -0
  140. package/dist/core/untrusted-egress.js +77 -3
  141. package/dist/engine/harness/agent-harness.d.ts +7 -5
  142. package/dist/engine/harness/agent-harness.js +11 -2
  143. package/dist/engine/loop/agent-loop.d.ts +1 -4
  144. package/dist/engine/loop/agent-loop.js +26 -4
  145. package/dist/index.d.ts +12 -7
  146. package/dist/index.js +7 -4
  147. package/dist/internal/harness.d.ts +2 -1
  148. package/dist/internal/harness.js +1 -0
  149. package/dist/primitives/credential-ref.d.ts +89 -0
  150. package/dist/primitives/credential-ref.js +125 -0
  151. package/dist/primitives/index.d.ts +2 -0
  152. package/dist/primitives/index.js +1 -0
  153. package/dist/stores/file/checkpoint-store.d.ts +0 -4
  154. package/dist/stores/file/checkpoint-store.js +0 -1
  155. package/dist/stores/file/file-history-store.js +12 -4
  156. package/dist/stores/file/mailbox-store.d.ts +2 -1
  157. package/dist/stores/file/mailbox-store.js +26 -1
  158. package/dist/tools/fs/bash-readonly-classifier.d.ts +15 -19
  159. package/dist/tools/fs/bash-readonly-classifier.js +5 -10
  160. package/dist/tools/fs/fs-bash.js +10 -3
  161. package/dist/tools/fs/pattern-kill-guard.d.ts +3 -2
  162. package/dist/tools/fs/pattern-kill-guard.js +68 -36
  163. package/dist/tools/fs/shell-read-check.d.ts +10 -0
  164. package/dist/tools/fs/shell-read-check.js +7 -1
  165. package/dist/tools/fs/shell-read-identity.d.ts +12 -9
  166. package/dist/tools/fs/shell-read-identity.js +9 -4
  167. package/dist/tools/web.js +9 -7
  168. package/package.json +1 -1
  169. package/test/export-surface.snapshot.json +79 -3
  170. package/wire-changes/core-stores-sql@7.57.0.json +29 -0
  171. package/wire-changes/core@7.57.0.json +1487 -0
  172. package/wire-changes/mods-kernel@7.57.0.json +29 -0
  173. package/wire-changes/primitives@7.57.0.json +245 -0
@@ -18,7 +18,56 @@ export interface MailboxMessage {
18
18
  * ABSENT = a pre-385 or foreign record (same semantics as an absent `hopChain`): the drain treats it
19
19
  * as foreign, never retro-admits it. Validated at `append` ({@link readMailboxPeerMeta}). */
20
20
  peerMeta?: MailboxPeerMeta;
21
+ /** The record's HOLD DECISION ({@link MailboxDecision}): absent = never held. Never appended — it
22
+ * starts absent and moves only through {@link MailboxStore.moveDecision}; every lease snapshot
23
+ * carries it, so every consumer of the box reads the same decision. */
24
+ decision?: MailboxDecision;
21
25
  }
26
+ /**
27
+ * A parked record's hold decision — the durable answer about a record a recipient's setting put on
28
+ * hold, kept ON the record so every consumer of the box (another terminal, another process, the session's
29
+ * next run) reads the same answer. `held` — waiting for a person; `approved` — a person approved it, not yet
30
+ * handed over; `delivered` — a consumer holding the lease took the move and handed it over (a later
31
+ * answer about it learns that instead of claiming a decision); `denied` / `expired` / `refused` /
32
+ * `dropped` — final: the record is never delivered and only waits for the ack that removes it.
33
+ */
34
+ export declare const MAILBOX_DECISIONS: readonly ["held", "approved", "delivered", "denied", "expired", "refused", "dropped"];
35
+ export type MailboxDecision = (typeof MAILBOX_DECISIONS)[number];
36
+ /** A stored decision word as a reader takes it: a word this engine does not know (a newer engine's) reads
37
+ * as `held` — never delivered on a guess, and left for a person (or that newer engine) to decide. */
38
+ export declare function readStoredMailboxDecision(raw: unknown): MailboxDecision | undefined;
39
+ /** The refusal code of a malformed {@link MailboxStore.moveDecision} call (a word outside the
40
+ * closed set, a non-positive seq, or an edge the decision law does not allow). */
41
+ export declare const MAILBOX_INVALID_DECISION_MOVE_CODE = "mailbox.invalid_decision_move";
42
+ export declare function isMailboxDecision(value: unknown): value is MailboxDecision;
43
+ /** Final states: the record is never delivered again, by any consumer. */
44
+ export declare function isMailboxDecisionFinal(state: MailboxDecision | undefined): boolean;
45
+ /** Deliverable states: a consumer holding the lease delivers the record (a setting that now refuses still wins). */
46
+ export declare function isMailboxDecisionDeliverable(state: MailboxDecision | undefined): boolean;
47
+ /**
48
+ * THE decision law — a decision only moves forward, and every hand-over of a held record is itself a move:
49
+ * absent → `held`; `held` → `approved` / `delivered` / `denied` / `expired` / `refused`; `approved` →
50
+ * `delivered` or a final state; `delivered` → `dropped` (the handed-over frame was refused by policy inside a lease-holding pass) only;
51
+ * a final state never moves.
52
+ */
53
+ export declare function mailboxDecisionEdgeAllowed(from: MailboxDecision | undefined, to: MailboxDecision): boolean;
54
+ /** What {@link MailboxStore.moveDecision} answers: the move happened, or the record's actual state
55
+ * (`unset` = never held, `gone` = no such record any more — acked, reaped or dropped). */
56
+ export type MailboxDecisionOutcome = {
57
+ readonly ok: true;
58
+ } | {
59
+ readonly ok: false;
60
+ readonly current: MailboxDecision | "unset" | "gone";
61
+ };
62
+ /**
63
+ * THE caller-side reading of {@link MailboxStore.moveDecision}: a fault can arrive AFTER the move
64
+ * committed (a lock release that failed, a reply lost on the wire), so a call that throws is asked once
65
+ * more, and a store that already holds `next` answers the first call's success. A second fault throws.
66
+ * Every engine-side decision move goes through this one function.
67
+ */
68
+ export declare function moveMailboxDecision(store: Pick<MailboxStore, "moveDecision">, scope: string, handle: string, seq: number, expect: MailboxDecision | undefined, next: MailboxDecision): Promise<MailboxDecisionOutcome>;
69
+ /** The one validator every backend runs BEFORE touching storage: a malformed call has zero side effects. */
70
+ export declare function assertMailboxDecisionMove(seq: unknown, expect: unknown, next: unknown): void;
22
71
  /** design/385 — the typed peer-metadata record on a parked message. ONE optional object, one-time
23
72
  * contract extension; every field optional. Bundled backends persist it verbatim and hand back a
24
73
  * detached copy. */
@@ -132,7 +181,7 @@ export declare const MAILBOX_TOMBSTONED_RECIPIENT_CODE = "mailbox.recipient_tomb
132
181
  * A backend may raise the same code with a plain `Error` carrying `.code`; consumers branch on the
133
182
  * string, not on this class (a cross-process/out-of-repo store cannot hand back an instance).
134
183
  */
135
- export type MailboxStoreErrorCode = typeof MAILBOX_TOMBSTONED_RECIPIENT_CODE | typeof MAILBOX_INVALID_PEER_META_CODE | typeof MAILBOX_CROSS_PROCESS_UNSAFE_CODE;
184
+ export type MailboxStoreErrorCode = typeof MAILBOX_TOMBSTONED_RECIPIENT_CODE | typeof MAILBOX_INVALID_PEER_META_CODE | typeof MAILBOX_CROSS_PROCESS_UNSAFE_CODE | typeof MAILBOX_INVALID_DECISION_MOVE_CODE;
136
185
  export declare class MailboxStoreError extends Error {
137
186
  readonly code: MailboxStoreErrorCode;
138
187
  constructor(code: MailboxStoreErrorCode, message: string);
@@ -241,6 +290,17 @@ export interface MailboxStore {
241
290
  * Implementations report `{acked:true}` only when they actually consumed the prefix.
242
291
  */
243
292
  ack(scope: string, handle: string, owner: string, upToSeq: number): Promise<MailboxAckResult>;
293
+ /**
294
+ * Move one record's hold decision from `expect` (absent = never held) to `next`, atomically
295
+ * against every other transition and against `ack` — the compare-and-set every decision about a held
296
+ * record goes through. NOT fenced by the lease: a person answers from whichever process shows the
297
+ * record, whether or not it holds the box. A state that is not `expect` answers `{ ok: false, current }`
298
+ * and changes nothing; a record no longer in the box answers `current: "gone"`. A malformed call (see
299
+ * {@link assertMailboxDecisionMove}) is refused with {@link MAILBOX_INVALID_DECISION_MOVE_CODE}
300
+ * before any side effect. The state is durable like the record, and `ack` / `reap` / `drop` remove it
301
+ * with the record.
302
+ */
303
+ moveDecision(scope: string, handle: string, seq: number, expect: MailboxDecision | undefined, next: MailboxDecision): Promise<MailboxDecisionOutcome>;
244
304
  releaseLease(scope: string, handle: string, owner: string): Promise<void>;
245
305
  /** Observation face (the "N pending" receipt figure) — count of currently-visible messages. */
246
306
  peekCount(scope: string, handle: string): Promise<number>;
@@ -291,6 +351,7 @@ export declare class InMemoryMailboxStore implements MailboxStore {
291
351
  append(scope: string, handle: string, msg: MailboxAppendMessage): Promise<number>;
292
352
  claimLease(scope: string, handle: string, owner: string, ttlMs: number, now?: number): Promise<MailboxLease | null>;
293
353
  ack(scope: string, handle: string, owner: string, upToSeq: number): Promise<MailboxAckResult>;
354
+ moveDecision(scope: string, handle: string, seq: number, expect: MailboxDecision | undefined, next: MailboxDecision): Promise<MailboxDecisionOutcome>;
294
355
  releaseLease(scope: string, handle: string, owner: string): Promise<void>;
295
356
  peekCount(scope: string, handle: string): Promise<number>;
296
357
  drop(scope: string, handle: string): Promise<void>;
@@ -1,4 +1,53 @@
1
1
  import { assertRetentionPolicy } from "./retention-policy.js";
2
+ export const MAILBOX_DECISIONS = Object.freeze(["held", "approved", "delivered", "denied", "expired", "refused", "dropped"]);
3
+ export function readStoredMailboxDecision(raw) {
4
+ if (raw === undefined)
5
+ return undefined;
6
+ return isMailboxDecision(raw) ? raw : "held";
7
+ }
8
+ export const MAILBOX_INVALID_DECISION_MOVE_CODE = "mailbox.invalid_decision_move";
9
+ export function isMailboxDecision(value) {
10
+ return typeof value === "string" && MAILBOX_DECISIONS.includes(value);
11
+ }
12
+ export function isMailboxDecisionFinal(state) {
13
+ return state === "denied" || state === "expired" || state === "refused" || state === "dropped";
14
+ }
15
+ export function isMailboxDecisionDeliverable(state) {
16
+ return state === "approved" || state === "delivered";
17
+ }
18
+ export function mailboxDecisionEdgeAllowed(from, to) {
19
+ if (from === undefined)
20
+ return to === "held";
21
+ if (from === "held")
22
+ return to === "approved" || to === "delivered" || to === "denied" || to === "expired" || to === "refused";
23
+ if (from === "approved")
24
+ return to === "delivered" || isMailboxDecisionFinal(to);
25
+ if (from === "delivered")
26
+ return to === "dropped";
27
+ return false;
28
+ }
29
+ export async function moveMailboxDecision(store, scope, handle, seq, expect, next) {
30
+ try {
31
+ return await store.moveDecision(scope, handle, seq, expect, next);
32
+ }
33
+ catch {
34
+ const again = await store.moveDecision(scope, handle, seq, expect, next);
35
+ return !again.ok && again.current === next ? { ok: true } : again;
36
+ }
37
+ }
38
+ export function assertMailboxDecisionMove(seq, expect, next) {
39
+ const refuse = (why) => {
40
+ throw new MailboxStoreError(MAILBOX_INVALID_DECISION_MOVE_CODE, `MailboxStore.moveDecision: ${why}`);
41
+ };
42
+ if (typeof seq !== "number" || !Number.isInteger(seq) || seq < 1)
43
+ refuse(`seq must be a positive integer (got ${String(seq)})`);
44
+ if (expect !== undefined && !isMailboxDecision(expect))
45
+ refuse(`expect must be absent or one of ${MAILBOX_DECISIONS.join("|")}`);
46
+ if (!isMailboxDecision(next))
47
+ refuse(`next must be one of ${MAILBOX_DECISIONS.join("|")}`);
48
+ if (!mailboxDecisionEdgeAllowed(expect, next))
49
+ refuse(`${String(expect ?? "unset")} → ${String(next)} is not a decision move (a decision only moves forward)`);
50
+ }
2
51
  export const MAILBOX_PEER_FROM_MODES = Object.freeze(["bypass", "prompting"]);
3
52
  export const MAILBOX_PEER_RECORD_KINDS = Object.freeze(["peer_message", "idle_notice", "delivery_notice", "idle_subscription", "engine_frame"]);
4
53
  export const MAILBOX_PEER_NOTICE_STATES = Object.freeze(["held", "denied", "expired", "delivered", "refused", "dropped", "idle", "exited", "unavailable"]);
@@ -184,6 +233,16 @@ export class InMemoryMailboxStore {
184
233
  b.lease = undefined;
185
234
  return { acked: true };
186
235
  }
236
+ async moveDecision(scope, handle, seq, expect, next) {
237
+ assertMailboxDecisionMove(seq, expect, next);
238
+ const m = this.boxes.get(this.key(scope, handle))?.messages.find((x) => x.seq === seq);
239
+ if (m === undefined)
240
+ return { ok: false, current: "gone" };
241
+ if (m.decision !== expect)
242
+ return { ok: false, current: m.decision ?? "unset" };
243
+ m.decision = next;
244
+ return { ok: true };
245
+ }
187
246
  async releaseLease(scope, handle, owner) {
188
247
  const b = this.boxes.get(this.key(scope, handle));
189
248
  if (b?.lease?.owner === owner)
@@ -0,0 +1,46 @@
1
+ /**
2
+ * One declared server, as far as its names go: `McpServerSpec` fits as it is. `tools` are the server's REMOTE tool names the asker
3
+ * already knows (a listing, a host's reading of one — never the minted `mcp__…` names); absent means none are known.
4
+ */
5
+ export interface McpNameSetServer {
6
+ readonly name: string;
7
+ readonly tools?: readonly string[];
8
+ }
9
+ /**
10
+ * One contention: `names` are the two sources, the earlier one first (server names for `server_name`; the remote tool names for
11
+ * `tool_name`, where `server` says whose); `shared` is the name they mint — the prefix for `server_name`, the wire name for `tool_name`.
12
+ */
13
+ export type McpNameConflict = {
14
+ readonly kind: "server_name";
15
+ readonly names: readonly [string, string];
16
+ readonly shared: string;
17
+ } | {
18
+ readonly kind: "tool_name";
19
+ readonly server: string;
20
+ readonly names: readonly [string, string];
21
+ readonly shared: string;
22
+ };
23
+ /** The judge's answer: `ok`, or EVERY contention in the set — server rows first, then tool rows, each in declaration order; never empty. */
24
+ export type McpNameVerdict = {
25
+ readonly ok: true;
26
+ } | {
27
+ readonly ok: false;
28
+ readonly conflicts: readonly [McpNameConflict, ...McpNameConflict[]];
29
+ };
30
+ /** Judge a declared set. Pure: reads nothing but the names it is handed; a malformed set is refused (`config.invalid_argument`), never read as clean. */
31
+ export declare function judgeMcpNames(servers: readonly McpNameSetServer[]): McpNameVerdict;
32
+ /**
33
+ * The sentence that says why a tool of one server is not mounted under its own name: `heldBy` is the raw name that took the wire
34
+ * name `minted` first, `tool` the raw name that came after. TWO FACTS, TWO SENTENCES (a fixed label must not head more than one
35
+ * cause): the same raw name listed again, where namespacing folded nothing, and two different raw names that fold onto one name.
36
+ * It carries no raw name — the caller's own field says which tool.
37
+ */
38
+ export declare function mcpToolDropReason(minted: string, heldBy: string, tool: string): string;
39
+ /** One row as a sentence — the one the engine refuses a run with (`server_name`) or discloses a drop with (`tool_name`). Names are quoted single-line and bounded. */
40
+ export declare function describeMcpNameConflict(conflict: McpNameConflict): string;
41
+ /**
42
+ * The refusal of a declared set whose server names do not each own a namespace domain — thrown by the mount and by the no-run probe,
43
+ * so one set is refused with one code and one sentence wherever it is asked. It takes the NAMES only (a run's refusal reads no
44
+ * tool list); the FIRST `server_name` row speaks, naming both raw spellings.
45
+ */
46
+ export declare function refuseCollidingMcpServerNames(serverNames: readonly string[]): void;
@@ -0,0 +1,61 @@
1
+ import { MCP_NAMESPACE } from "./protocol-table.js";
2
+ import { createToolNameClaims, findNamespacePrefixCollisions } from "./protocol-naming.js";
3
+ import { inlineUntrusted } from "./untrusted-text.js";
4
+ function refuseArgument(problem) {
5
+ throw Object.assign(new Error(problem), { code: "config.invalid_argument" });
6
+ }
7
+ function readNameSet(servers) {
8
+ if (!Array.isArray(servers))
9
+ refuseArgument("judgeMcpNames takes a list of declared servers; what was passed is not a list.");
10
+ return Array.from(servers, (server, index) => {
11
+ if (typeof server !== "object" || server === null)
12
+ refuseArgument(`judgeMcpNames: entry ${index} is not a server declaration (an object with a "name").`);
13
+ const { name, tools } = server;
14
+ if (typeof name !== "string")
15
+ refuseArgument(`judgeMcpNames: entry ${index} has no string "name".`);
16
+ if (tools === undefined)
17
+ return { name };
18
+ const listed = Array.isArray(tools) ? Array.from(tools) : [undefined];
19
+ if (listed.some((tool) => typeof tool !== "string")) {
20
+ refuseArgument(`judgeMcpNames: entry ${index} ("${inlineUntrusted(name, 120)}") names its tools as something other than a list of strings.`);
21
+ }
22
+ return { name, tools: listed };
23
+ });
24
+ }
25
+ export function judgeMcpNames(servers) {
26
+ const set = readNameSet(servers);
27
+ const conflicts = findNamespacePrefixCollisions(MCP_NAMESPACE, set.map((s) => s.name)).map((c) => ({ kind: "server_name", names: c.peers, shared: c.prefix }));
28
+ for (const server of set) {
29
+ const claim = createToolNameClaims(MCP_NAMESPACE, server.name);
30
+ for (const tool of server.tools ?? []) {
31
+ const { minted, heldBy } = claim(tool);
32
+ if (heldBy !== undefined)
33
+ conflicts.push({ kind: "tool_name", server: server.name, names: [heldBy, tool], shared: minted });
34
+ }
35
+ }
36
+ const [first, ...rest] = conflicts;
37
+ return first === undefined ? { ok: true } : { ok: false, conflicts: [first, ...rest] };
38
+ }
39
+ export function mcpToolDropReason(minted, heldBy, tool) {
40
+ return heldBy === tool
41
+ ? `this server listed this tool name more than once in one listing; the first entry is mounted as "${minted}" and the repeat is dropped (one name cannot denote two tools)`
42
+ : `name collides with an earlier tool of this server: both mount as "${minted}" after namespacing (only [a-zA-Z0-9_-] survives), and one name cannot denote two tools`;
43
+ }
44
+ export function describeMcpNameConflict(conflict) {
45
+ const [a, b] = conflict.names;
46
+ if (conflict.kind === "tool_name") {
47
+ return `MCP server "${inlineUntrusted(conflict.server, 120)}", tool "${inlineUntrusted(b, 120)}": ${mcpToolDropReason(conflict.shared, a, b)}`;
48
+ }
49
+ return a === b
50
+ ? `MCP server "${inlineUntrusted(a, 120)}" is declared twice — each server needs its own name (both mount under "${conflict.shared}", so their tools would shadow each other and a refresh of one would unmount the other's).`
51
+ : `MCP server names "${inlineUntrusted(a, 120)}" and "${inlineUntrusted(b, 120)}" both mount under "${conflict.shared}" — the namespaced tool name keeps only [a-zA-Z0-9_-], so they are the same domain to this engine (their tools would shadow each other and a refresh of one would unmount the other's). Rename one.`;
52
+ }
53
+ export function refuseCollidingMcpServerNames(serverNames) {
54
+ const verdict = judgeMcpNames(serverNames.map((name) => ({ name })));
55
+ if (verdict.ok)
56
+ return;
57
+ const row = verdict.conflicts.find((c) => c.kind === "server_name");
58
+ if (row === undefined)
59
+ return;
60
+ throw Object.assign(new Error(describeMcpNameConflict(row)), { code: "config.mcp_server_name_collision" });
61
+ }
@@ -93,7 +93,7 @@ export interface McpProbeOptions {
93
93
  * FAILURE IS PER SERVER, REFUSAL IS NOT. A server that cannot be dialed, that answers a status, or that
94
94
  * outlives `timeoutMs` takes its verdict in its own row and the walk continues. What DOES reject is the
95
95
  * declaration set the materialization itself refuses — two names that mount under one namespace prefix
96
- * are refused before any transport is touched, by that same function, so the probe answers a colliding
96
+ * are refused before any transport is touched, by the judge the mount asks, so the probe answers a colliding
97
97
  * configuration exactly as a task would rather than showing two healthy-looking rows for a pair the
98
98
  * engine would never mount.
99
99
  *
@@ -2,8 +2,7 @@ import { ErrorCode, McpError } from "@modelcontextprotocol/sdk/types.js";
2
2
  import { snapshotMcpEntries } from "./locked-config.js";
3
3
  import { classifyMcpFailure, livenessOf } from "./mcp-failure.js";
4
4
  import { MCP_ENV_MS_MAX, materializeMcpTools } from "./mcp.js";
5
- import { findNamespacePrefixCollision } from "./protocol-naming.js";
6
- import { MCP_NAMESPACE } from "./protocol-table.js";
5
+ import { refuseCollidingMcpServerNames } from "./mcp-name-judge.js";
7
6
  import { mcpManifestEntries } from "./runner/prepare-wiring-manifest.js";
8
7
  const DECLINE_EVERY_ELICITATION = async () => ({ action: "decline" });
9
8
  function wholeInRange(name, value, min, max) {
@@ -66,9 +65,7 @@ export async function probeMcpServers(specs, opts = {}) {
66
65
  const concurrency = wholeInRange("concurrency", opts.concurrency, 1, Number.MAX_SAFE_INTEGER) ?? 1;
67
66
  const onElicit = opts.onElicit ?? DECLINE_EVERY_ELICITATION;
68
67
  const signal = opts.signal;
69
- if (findNamespacePrefixCollision(MCP_NAMESPACE, declared.map((e) => e.name)) !== undefined) {
70
- await materializeMcpTools(declared, opts.principal, onElicit);
71
- }
68
+ refuseCollidingMcpServerNames(declared.map((e) => e.name));
72
69
  const statuses = [];
73
70
  const servers = new Array(declared.length);
74
71
  const teardowns = [];
package/dist/core/mcp.js CHANGED
@@ -1,10 +1,10 @@
1
- import { egressText } from "./untrusted-egress.js";
2
1
  import { __resetEnvKnobAnnouncements } from "./env-knob-announce.js";
3
2
  import { resolveEnvBudget } from "./env-budget.js";
4
3
  import { MCP_NAMESPACE } from "./protocol-table.js";
5
- import { declaredHeaderValues, resolveProtocolHttpHeaders, scrubRequestEcho } from "./protocol-http-auth.js";
4
+ import { declaredHeaderValues, remoteWords, resolveProtocolHttpHeaders } from "./protocol-http-auth.js";
6
5
  import { disposeSectionEntry, sectionAnswerWarning } from "./prompt-section-seat.js";
7
- import { findNamespacePrefixCollision, mintNamespacePrefix, mintNamespacedToolName, normalizeNameSegment } from "./protocol-naming.js";
6
+ import { createToolNameClaims, mintNamespacePrefix, normalizeNameSegment } from "./protocol-naming.js";
7
+ import { mcpToolDropReason, refuseCollidingMcpServerNames } from "./mcp-name-judge.js";
8
8
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
9
9
  import { McpConnectionPool } from "./mcp-connection-pool.js";
10
10
  import { SessionEndingTransport, requestSite } from "./mcp-session-end.js";
@@ -165,7 +165,7 @@ export function collapseMcpErrorPrefix(message) {
165
165
  function sanitizeMcpErrorTextInPlace(err, declared) {
166
166
  if (!(err instanceof Error))
167
167
  return;
168
- const sanitized = mcpRemoteText(err instanceof McpError ? collapseMcpErrorPrefix(err.message) : err.message, declared);
168
+ const sanitized = remoteWords(err instanceof McpError ? collapseMcpErrorPrefix(err.message) : err.message, declared);
169
169
  if (sanitized !== err.message)
170
170
  err.message = sanitized;
171
171
  }
@@ -176,10 +176,7 @@ function namedMcpFailureText(err, declared) {
176
176
  }
177
177
  function remoteErrorText(err, declared, opts) {
178
178
  const raw = thrownText(err);
179
- return mcpRemoteText(opts?.collapse === true ? collapseMcpErrorPrefix(raw) : raw, declared);
180
- }
181
- function mcpRemoteText(text, declared) {
182
- return egressText("same-principal", scrubRequestEcho(text, declared));
179
+ return remoteWords(opts?.collapse === true ? collapseMcpErrorPrefix(raw) : raw, declared);
183
180
  }
184
181
  function writeEffectWarning(writeEffect) {
185
182
  return writeEffect
@@ -655,17 +652,7 @@ export async function materializeMcpTools(specs, principal, onElicit, imageResiz
655
652
  axesRefused.set(spec, judged.error);
656
653
  return spec;
657
654
  });
658
- {
659
- const collision = findNamespacePrefixCollision(MCP_NAMESPACE, entries.map((s) => s.name));
660
- if (collision) {
661
- const [a, b] = collision.peers;
662
- const e = new Error(a === b
663
- ? `MCP server "${inlineUntrusted(a, 120)}" is declared twice — each server needs its own name (both mount under "${collision.prefix}", so their tools would shadow each other and a refresh of one would unmount the other's).`
664
- : `MCP server names "${inlineUntrusted(a, 120)}" and "${inlineUntrusted(b, 120)}" both mount under "${collision.prefix}" — the namespaced tool name keeps only [a-zA-Z0-9_-], so they are the same domain to this engine (their tools would shadow each other and a refresh of one would unmount the other's). Rename one.`);
665
- e.code = "config.mcp_server_name_collision";
666
- throw e;
667
- }
668
- }
655
+ refuseCollidingMcpServerNames(entries.map((s) => s.name));
669
656
  let revocationProbeFailed = false;
670
657
  const isServerRevoked = (serverName) => {
671
658
  if (mcpRevocations === undefined)
@@ -1664,7 +1651,7 @@ function intakeListedTools(listed, spec, link, declared, imageResizer, reminderD
1664
1651
  const dropped = [];
1665
1652
  const accepted = [];
1666
1653
  const advisories = [];
1667
- const mintedNames = new Map();
1654
+ const claimToolName = createToolNameClaims(MCP_NAMESPACE, spec.name);
1668
1655
  for (const t of listed.tools) {
1669
1656
  if (spec.allowTools && !spec.allowTools.includes(t.name)) {
1670
1657
  continue;
@@ -1704,18 +1691,11 @@ function intakeListedTools(listed, spec, link, declared, imageResizer, reminderD
1704
1691
  }
1705
1692
  const remoteName = t.name;
1706
1693
  const taskRequired = t.execution !== null && typeof t.execution === "object" && t.execution.taskSupport === "required";
1707
- const namespacedName = mintNamespacedToolName(MCP_NAMESPACE, spec.name, remoteName);
1708
- const mintedBy = mintedNames.get(namespacedName);
1694
+ const { minted: namespacedName, heldBy: mintedBy } = claimToolName(remoteName);
1709
1695
  if (mintedBy !== undefined) {
1710
- dropped.push({
1711
- tool: inlineUntrusted(t.name),
1712
- reason: mintedBy === t.name
1713
- ? `this server listed this tool name more than once in one listing; the first entry is mounted as "${namespacedName}" and the repeat is dropped (one name cannot denote two tools)`
1714
- : `name collides with an earlier tool of this server: both mount as "${namespacedName}" after namespacing (only [a-zA-Z0-9_-] survives), and one name cannot denote two tools`,
1715
- });
1696
+ dropped.push({ tool: inlineUntrusted(t.name), reason: mcpToolDropReason(namespacedName, mintedBy, t.name) });
1716
1697
  continue;
1717
1698
  }
1718
- mintedNames.set(namespacedName, t.name);
1719
1699
  const hintAxis = mcpAxisFor(namespacedName, t.annotations);
1720
1700
  const axis = applyCallerAxisOverride(namespacedName, hintAxis, spec.toolAxes?.[remoteName]);
1721
1701
  if (axis)
@@ -1819,7 +1799,7 @@ function intakeListedTools(listed, spec, link, declared, imageResizer, reminderD
1819
1799
  const errScLine = structuredContentErrorLine(res.structuredContent, parts.join("\n"));
1820
1800
  if (errScLine !== undefined)
1821
1801
  parts.push(errScLine);
1822
- const body = truncateMcpErrorText(mcpRemoteText(parts.join("\n").trim(), declared));
1802
+ const body = truncateMcpErrorText(remoteWords(parts.join("\n").trim(), declared));
1823
1803
  const msg = body
1824
1804
  ? `MCP tool ${inlineUntrusted(remoteName)} reported an error. The server's error content follows as external/untrusted data:\n${delimitUntrusted(`${spec.name} tool error`, body)}`
1825
1805
  : `MCP tool ${inlineUntrusted(remoteName)} reported an error`;
@@ -2,6 +2,7 @@ import { join } from "node:path";
2
2
  import { uuidv7 } from "../../internal/harness.js";
3
3
  import { atomicWriteFileSync, ensureDirExists, lockedStrictUpdate, readStrictSidecar, enqueueMemoryAnnouncement, ControlPlaneCorruptError } from "./layout.js";
4
4
  import { inlineUntrusted } from "../untrusted-text.js";
5
+ import { extensionWords } from "../untrusted-egress.js";
5
6
  import { deliverEngineNotice } from "../engine-notice.js";
6
7
  import { CONSOLIDATION_FORCE_THROTTLE_FLOOR_MS, screenConsolidationOptions, supersessionFuseCeiling, updateConsolidationGate, recommendationUnconsumed, CONSOLIDATION_WRITE_REFUSED_CODE, ConsolidationRefusedError, } from "./consolidation.js";
7
8
  import { memoryConsolidationIncompleteNotice, memoryConsolidationWithheldNotice } from "./engine.js";
@@ -510,15 +511,15 @@ export async function runMemoryConsolidationDriver(engine, scope, opts) {
510
511
  }
511
512
  catch (err) {
512
513
  if (err instanceof DriverMintFailure) {
513
- announce([`memory consolidation driver run ${run.runId} for scope ${inlineUntrusted(scope, 80)} FAILED before any write: ${inlineUntrusted(err.detail, 300)}`]);
514
+ announce([`memory consolidation driver run ${run.runId} for scope ${inlineUntrusted(scope, 80)} FAILED before any write: ${extensionWords(err.detail, 300)}`]);
514
515
  return settle("driver_failed", undefined, err.detail);
515
516
  }
516
517
  if (err instanceof DriverPurityViolation) {
517
- announce([`memory consolidation driver run ${run.runId} for scope ${inlineUntrusted(scope, 80)} REFUSED on the clean-arm purity gate (no writes): ${inlineUntrusted(err.detail, 300)}`]);
518
+ announce([`memory consolidation driver run ${run.runId} for scope ${inlineUntrusted(scope, 80)} REFUSED on the clean-arm purity gate (no writes): ${extensionWords(err.detail, 300)}`]);
518
519
  return settle("purity_violation", undefined, err.detail);
519
520
  }
520
521
  if (err instanceof MintInvalidated) {
521
- announce([`memory consolidation driver run ${run.runId} for scope ${inlineUntrusted(scope, 80)} MINT INVALIDATED: ${inlineUntrusted(err.detail, 300)}`]);
522
+ announce([`memory consolidation driver run ${run.runId} for scope ${inlineUntrusted(scope, 80)} MINT INVALIDATED: ${extensionWords(err.detail, 300)}`]);
522
523
  if (run.residue === undefined && err.residue.length > 0)
523
524
  run.residue = err.residue;
524
525
  const partial = {
@@ -1,7 +1,8 @@
1
1
  import { requireWriteGate } from "./consolidation-write-gate.js";
2
2
  import { readMember, thrownText } from "../thrown-value.js";
3
3
  import { assertServableRequestRoot, joinRequestUrl } from "../../brain/route-adjudicator.js";
4
- import { holdOwnCredentialValues } from "../secret-env.js";
4
+ import { holdOwnCredentialValues, namesASecret } from "../secret-env.js";
5
+ import { remoteWords } from "../protocol-http-auth.js";
5
6
  export const DISTILLER_DEFAULT_MAX_INPUTS_PER_PRODUCT = 32;
6
7
  const SIZE_CLAUSE = {
7
8
  "dl-2": "- the index should be as small as it can be made without losing either of the above.",
@@ -171,29 +172,37 @@ export function openAiCompatChatSeat(seat) {
171
172
  catch (e) {
172
173
  throw distillerConfigRefusal(thrownText(e), e);
173
174
  }
175
+ const declared = namesASecret(apiKey) ? [apiKey] : [];
174
176
  return async (req) => {
175
- const res = await fetch(joinRequestUrl(requestRoot, "/chat/completions"), {
176
- method: "POST",
177
- headers: { "content-type": "application/json", authorization: `Bearer ${apiKey}` },
178
- body: JSON.stringify({
179
- model: seat.model,
180
- messages: [
181
- { role: "system", content: req.system },
182
- { role: "user", content: req.user },
183
- ],
184
- temperature: req.temperature,
185
- max_tokens: req.maxTokens,
186
- }),
187
- });
188
- const text = await res.text();
177
+ let res;
178
+ let text;
179
+ try {
180
+ res = await fetch(joinRequestUrl(requestRoot, "/chat/completions"), {
181
+ method: "POST",
182
+ headers: { "content-type": "application/json", authorization: `Bearer ${apiKey}` },
183
+ body: JSON.stringify({
184
+ model: seat.model,
185
+ messages: [
186
+ { role: "system", content: req.system },
187
+ { role: "user", content: req.user },
188
+ ],
189
+ temperature: req.temperature,
190
+ max_tokens: req.maxTokens,
191
+ }),
192
+ });
193
+ text = await res.text();
194
+ }
195
+ catch (e) {
196
+ throw new Error(`model call failed: ${remoteWords(thrownText(e), declared)}`);
197
+ }
189
198
  if (!res.ok)
190
- throw new Error(`model call failed ${res.status}: ${text.slice(0, 400)}`);
199
+ throw new Error(`model call failed ${res.status}: ${remoteWords(text, declared).slice(0, 400)}`);
191
200
  let body;
192
201
  try {
193
202
  body = JSON.parse(text);
194
203
  }
195
204
  catch {
196
- throw new Error(`model returned non-JSON envelope: ${text.slice(0, 400)}`);
205
+ throw new Error(`model returned non-JSON envelope: ${remoteWords(text, declared).slice(0, 400)}`);
197
206
  }
198
207
  return {
199
208
  content: body.choices?.[0]?.message?.content ?? "",
@@ -6,6 +6,7 @@ import { basename, dirname, isAbsolute, join, resolve, sep } from "node:path";
6
6
  import { MEMORY_ORIGIN_CAUSES } from "./types.js";
7
7
  import { physicalPathOf, physicalPathOfExisting, priorCallerCasedPathOf } from "../physical-path.js";
8
8
  import { thrownLabel, thrownText } from "../thrown-value.js";
9
+ import { egressText } from "../untrusted-egress.js";
9
10
  const SCOPES_FILE = "scopes.json";
10
11
  export const CURSORS_FILE = "cursors.json";
11
12
  export const CONTROL_PLANE_DIR = ".engine";
@@ -1467,6 +1468,7 @@ function readSidecarJson(controlDir, fileName) {
1467
1468
  }
1468
1469
  return undefined;
1469
1470
  }
1471
+ const mintedItems = (items) => items.map((item) => egressText("same-principal", item));
1470
1472
  function coerceAnnouncements(raw) {
1471
1473
  const rec = { folded: 0, queue: [] };
1472
1474
  if (!raw || typeof raw !== "object" || Array.isArray(raw))
@@ -1488,7 +1490,7 @@ function coerceAnnouncements(raw) {
1488
1490
  at: a.at,
1489
1491
  ...(typeof a.sessionId === "string" ? { sessionId: a.sessionId } : {}),
1490
1492
  ...(typeof a.reportId === "string" ? { reportId: a.reportId } : {}),
1491
- items: a.items,
1493
+ items: mintedItems(a.items),
1492
1494
  });
1493
1495
  }
1494
1496
  }
@@ -1496,7 +1498,8 @@ function coerceAnnouncements(raw) {
1496
1498
  return rec;
1497
1499
  }
1498
1500
  export function enqueueMemoryAnnouncement(controlDir, ann, opts) {
1499
- const family = opts?.supersedesPrefix;
1501
+ ann = { ...ann, items: mintedItems(ann.items) };
1502
+ const family = opts?.supersedesPrefix === undefined ? undefined : egressText("same-principal", opts.supersedesPrefix);
1500
1503
  if (ann.items.length === 0 && family === undefined)
1501
1504
  return;
1502
1505
  lockedJournaledUpdate(controlDir, ANNOUNCEMENTS_FILE, (current) => {
@@ -81,6 +81,8 @@ function syntheticCheckpoint(scope, fidelity) {
81
81
  askClass: "denial_limit_fallback",
82
82
  classes: ["denial_limit_fallback"],
83
83
  engineMounted: true,
84
+ targets: { protective: "/park-selfcheck/target", declared: null },
85
+ targetReads: { ["0".repeat(64)]: "f".repeat(64) },
84
86
  batchToolCallIds: [`${scope}:call`],
85
87
  completedCallIds: [],
86
88
  },
@@ -317,11 +317,13 @@ export declare function prepareCardApproval(opts: {
317
317
  principal?: string;
318
318
  /** design/182 §4.5 (additive): the structural owner — only the local-owner path needs it. */
319
319
  owner?: RuleOwner;
320
- /** The tool whose ask this card belongs to. Only the shell tool the rule lane speaks for, the name tools (the skill-activation
321
- * card) and the host tools (the fetch card) are accepted. */
320
+ /** The tool whose ask this card belongs to: a tool whose card the offer factory mints a rule on (`cardRuleFormOf`) — the shell
321
+ * tool, a name tool (the skill-activation card), a host tool (the fetch card), a tool with no rule grammar (the whole-tool card).
322
+ * Any other tool (a path tool, a shell-face sibling) draws no card. */
322
323
  toolName: string;
323
324
  /** The command as adjudicated — the same bytes the ask carried; for a name tool (`Skill`), the name its call carried (`args.skill`);
324
- * for a host tool (`WebFetch`), the host its call reaches (the offer's `command`). */
325
+ * for a host tool (`WebFetch`), the host its call reaches (the offer's `command`); for a tool with no rule grammar, `""` (the
326
+ * offer's `command`: the whole tool binds no value). */
325
327
  command: string;
326
328
  /** The ask's call id and argument digest, recorded for reconciliation. */
327
329
  toolCallId?: string;