@sema-agent/core 7.12.0 → 7.14.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 (122) hide show
  1. package/CHANGELOG.md +124 -0
  2. package/dist/agents/subagent.d.ts +4 -9
  3. package/dist/agents/subagent.js +16 -12
  4. package/dist/agents/teacher.js +1 -1
  5. package/dist/brain/reasoning.d.ts +23 -0
  6. package/dist/brain/reasoning.js +35 -4
  7. package/dist/brain/stream-engine.js +35 -17
  8. package/dist/brain/timeout.d.ts +25 -4
  9. package/dist/brain/timeout.js +1 -1
  10. package/dist/core/ask-origin.d.ts +83 -9
  11. package/dist/core/ask-origin.js +31 -5
  12. package/dist/core/auto-mode.d.ts +3 -3
  13. package/dist/core/checkpoint-seat.d.ts +18 -0
  14. package/dist/core/checkpoint-seat.js +6 -0
  15. package/dist/core/checkpoint-store.d.ts +9 -16
  16. package/dist/core/checkpoint-store.js +7 -4
  17. package/dist/core/engine-notice.d.ts +21 -1
  18. package/dist/core/env-knob-announce.d.ts +16 -0
  19. package/dist/core/env-knob-announce.js +14 -0
  20. package/dist/core/gate-fold.js +2 -0
  21. package/dist/core/gate-lanes.d.ts +0 -40
  22. package/dist/core/gate-lanes.js +24 -29
  23. package/dist/core/gate-outcome.d.ts +42 -3
  24. package/dist/core/gate-outcome.js +22 -0
  25. package/dist/core/governance-codes.d.ts +1 -1
  26. package/dist/core/governance-codes.js +3 -1
  27. package/dist/core/hooks.d.ts +6 -17
  28. package/dist/core/hooks.js +7 -5
  29. package/dist/core/mcp.d.ts +3 -2
  30. package/dist/core/mcp.js +5 -11
  31. package/dist/core/memory-engine/engine.d.ts +2 -2
  32. package/dist/core/runner/advertised-writable-dirs.d.ts +74 -0
  33. package/dist/core/runner/advertised-writable-dirs.js +48 -0
  34. package/dist/core/runner/assemble-result.d.ts +50 -1
  35. package/dist/core/runner/assemble-result.js +4 -1
  36. package/dist/core/runner/compaction-seams.d.ts +42 -0
  37. package/dist/core/runner/compaction-seams.js +80 -0
  38. package/dist/core/runner/contracts.d.ts +44 -6
  39. package/dist/core/runner/denial-limit-arms.d.ts +7 -9
  40. package/dist/core/runner/denial-limit-arms.js +11 -12
  41. package/dist/core/runner/gate-exit.d.ts +15 -3
  42. package/dist/core/runner/gate-exit.js +6 -4
  43. package/dist/core/runner/permission-rule-lanes.d.ts +2 -1
  44. package/dist/core/runner/permission-rule-lanes.js +2 -1
  45. package/dist/core/runner/prepare-caps-and-workflow.d.ts +34 -4
  46. package/dist/core/runner/prepare-caps-and-workflow.js +47 -12
  47. package/dist/core/runner/prepare-gate-stations.d.ts +1 -1
  48. package/dist/core/runner/prepare-hands-readface.d.ts +10 -4
  49. package/dist/core/runner/prepare-hands-readface.js +5 -14
  50. package/dist/core/runner/prepare-inherited-gate.d.ts +5 -3
  51. package/dist/core/runner/prepare-inherited-gate.js +1 -1
  52. package/dist/core/runner/prepare-memory.d.ts +31 -34
  53. package/dist/core/runner/prepare-memory.js +73 -51
  54. package/dist/core/runner/prepare-policy-chain.js +8 -4
  55. package/dist/core/runner/prepare-prompt-inputs.d.ts +4 -0
  56. package/dist/core/runner/prepare-prompt-inputs.js +2 -2
  57. package/dist/core/runner/prepare-protocol-tools.js +5 -0
  58. package/dist/core/runner/prepare-question-face.d.ts +10 -8
  59. package/dist/core/runner/prepare-question-face.js +1 -3
  60. package/dist/core/runner/prepare-safety-scan.js +11 -16
  61. package/dist/core/runner/prepare-task.d.ts +9 -1
  62. package/dist/core/runner/prepare-task.js +63 -49
  63. package/dist/core/runner/prepare-wiring-manifest.d.ts +2 -2
  64. package/dist/core/runner/prepare-wiring-manifest.js +1 -1
  65. package/dist/core/runner/resume-claim.d.ts +2 -2
  66. package/dist/core/runner/resume-preflight.d.ts +2 -2
  67. package/dist/core/runner/run-attachment-seats.d.ts +2 -2
  68. package/dist/core/runner/run-compaction-machinery.js +2 -0
  69. package/dist/core/runner/run-git-lane.d.ts +1 -1
  70. package/dist/core/runner/run-harness-handlers.js +5 -1
  71. package/dist/core/runner/run-identity-wiring.d.ts +7 -0
  72. package/dist/core/runner/run-identity-wiring.js +3 -1
  73. package/dist/core/runner/run-leg.d.ts +7 -5
  74. package/dist/core/runner/run-leg.js +256 -5
  75. package/dist/core/runner/run-notification-lane.d.ts +4 -3
  76. package/dist/core/runner/run-recovery-lanes.d.ts +4 -17
  77. package/dist/core/runner/run-recovery-lanes.js +5 -4
  78. package/dist/core/runner/run-settle-and-teardown.d.ts +14 -10
  79. package/dist/core/runner/run-settle-and-teardown.js +112 -4
  80. package/dist/core/runner/run-stop-and-final-verify.d.ts +2 -2
  81. package/dist/core/runner/run-terminal-adoption.d.ts +16 -16
  82. package/dist/core/runner/run-terminal-adoption.js +87 -7
  83. package/dist/core/runner/run-turn-boundary.js +5 -4
  84. package/dist/core/runner/runtask.d.ts +10 -90
  85. package/dist/core/runner/runtask.js +77 -596
  86. package/dist/core/runner/stream-lifecycle-verbs.js +10 -1
  87. package/dist/core/runner/stream-settle-backstop.js +1 -1
  88. package/dist/core/runtime-caps.d.ts +21 -0
  89. package/dist/core/runtime-caps.js +5 -1
  90. package/dist/core/task-event.d.ts +11 -2
  91. package/dist/core/task-registry-shared.js +8 -6
  92. package/dist/core/task-result.d.ts +15 -0
  93. package/dist/core/task-stream.d.ts +10 -1
  94. package/dist/core/terminal-cause.d.ts +6 -2
  95. package/dist/core/tool-face.d.ts +8 -0
  96. package/dist/core/tool-face.js +1 -0
  97. package/dist/core/tool-policy.d.ts +39 -31
  98. package/dist/core/tool-policy.js +29 -5
  99. package/dist/core/tool-registry.d.ts +11 -3
  100. package/dist/core/tool-registry.js +7 -1
  101. package/dist/core/tool-roster.d.ts +26 -0
  102. package/dist/core/tool-roster.js +40 -7
  103. package/dist/core/tool-spec.d.ts +13 -12
  104. package/dist/core/types.d.ts +2 -1
  105. package/dist/core/types.js +1 -0
  106. package/dist/core/wiring-manifest.d.ts +16 -9
  107. package/dist/core/wiring-manifest.js +8 -3
  108. package/dist/index.d.ts +8 -7
  109. package/dist/index.js +7 -6
  110. package/dist/orchestration/run-workflow-tool.d.ts +8 -8
  111. package/dist/orchestration/run-workflow-tool.js +1 -1
  112. package/dist/orchestration/workflow-primitives.d.ts +4 -3
  113. package/dist/orchestration/workflow-primitives.js +3 -3
  114. package/dist/orchestration/workflow-types.d.ts +14 -0
  115. package/dist/orchestration/workflow.d.ts +41 -4
  116. package/dist/orchestration/workflow.js +23 -5
  117. package/dist/server/http.js +2 -1
  118. package/dist/tools/fs/fs-shared.js +9 -3
  119. package/dist/tools/fs/read-deny.d.ts +24 -8
  120. package/dist/tools/fs/read-deny.js +20 -1
  121. package/package.json +2 -1
  122. package/test/export-surface.snapshot.json +35 -1
@@ -114,9 +114,18 @@ export function streamLifecycleVerbs(input) {
114
114
  detachHub.request(toolCallId);
115
115
  };
116
116
  const interrupt = async () => {
117
+ const interruptRefused = (msg) => {
118
+ const e = new Error(`cannot interrupt: ${msg}`);
119
+ e.code = "steering.not_running";
120
+ return e;
121
+ };
122
+ if (live.resultValue)
123
+ throw interruptRefused("the task has already finished");
117
124
  const h = live.handle ?? (await orTimeout(ready));
118
125
  if (!h)
119
- return;
126
+ throw interruptRefused("the task is not running");
127
+ if (live.resultValue !== undefined || h.loop.ended)
128
+ throw interruptRefused("the task is no longer running");
120
129
  if (!h.abortController.signal.aborted)
121
130
  h.loop.userInterrupted = true;
122
131
  h.abortController.abort();
@@ -83,7 +83,7 @@ export function streamSettleBackstop(input) {
83
83
  ? { retryAfterMs: hinted }
84
84
  : {};
85
85
  })(),
86
- stats: { turns: 0, tokens: 0, toolCalls: 0, cachedTokens: 0, costMicroUsd: 0 },
86
+ stats: { turns: 0, tokens: 0, toolCalls: 0, cachedTokens: 0, costMicroUsd: 0, usageMissing: true },
87
87
  };
88
88
  emitTrace(entryTracer.tracer, () => ({
89
89
  kind: "task.end",
@@ -84,6 +84,19 @@ export interface WorkflowGovernanceBaseline {
84
84
  * These six are the caps the ENGINE enforces; `allowUltracode`/`allowBypassPermissions` are shell-UX /
85
85
  * service-settings-layer concerns (see {@link RunnerDeps.runtimeCapsResolver}).
86
86
  */
87
+ /**
88
+ * #692 C-d — WHICH plane's ratchet denied auto mode, when the resolver knows: the three settings sources
89
+ * CC 2.1.250's `permissions.disableAutoMode` can come from, as the resolver folds them into the one deny bit
90
+ * ({@link RuntimeCaps.autoMode} `=== false`). `org` = a managed / organization policy plane; `local` = the
91
+ * machine's local settings; `settings` = the user's / project's settings file. Read ONLY beside the deny bit
92
+ * (a source with no denial is a contradiction the value screen refuses as a resolver fault); projected onto
93
+ * the leg's manifest read face (`WiringManifest.autoMode.deniedSource`) so a shell can say "your organization
94
+ * turned auto mode off" vs "your settings did" instead of one word for both.
95
+ */
96
+ export declare const AUTO_MODE_DENY_SOURCES: readonly ["org", "local", "settings"];
97
+ export type AutoModeDenySource = (typeof AUTO_MODE_DENY_SOURCES)[number];
98
+ /** Membership test for {@link AutoModeDenySource}. */
99
+ export declare function isAutoModeDenySource(v: unknown): v is AutoModeDenySource;
87
100
  export interface RuntimeCaps {
88
101
  /** `false` DENIES workflow self-orchestration for this principal server-side — even on a deployment that is
89
102
  * capable ({@link RunnerDeps.workflowScriptRunner} + governance). The third stage of the workflows gate.
@@ -136,6 +149,14 @@ export interface RuntimeCaps {
136
149
  * A denial resolves to a no-op (asks flow the original chain; no warn — the gate, not a mistake).
137
150
  */
138
151
  autoMode?: boolean;
152
+ /**
153
+ * #692 C-d (additive) — WHICH source's ratchet set {@link autoMode} to `false`, when the resolver folded
154
+ * several ({@link AutoModeDenySource}). Optional: a resolver that does not track sources omits it and the
155
+ * manifest reads `denied` with no source. Present with `autoMode` not `false`, or spelled outside the set,
156
+ * is a resolver FAULT (announced through `onError`, the deny bit coined fail-closed, `resolver_fault` on the
157
+ * manifest) — a source named for a denial that did not happen is a marshalling error, never a fact.
158
+ */
159
+ autoModeDenySource?: AutoModeDenySource;
139
160
  /**
140
161
  * design/383 §3.1 — may this principal declare a session memory-capture OPT-OUT
141
162
  * (`TaskSpec.memory.capture: "off"` / the Runner flip verb)? `false` = the deployment REQUIRES
@@ -1 +1,5 @@
1
- export {};
1
+ export const AUTO_MODE_DENY_SOURCES = ["org", "local", "settings"];
2
+ const AUTO_MODE_DENY_SOURCE_SET = new Set(AUTO_MODE_DENY_SOURCES);
3
+ export function isAutoModeDenySource(v) {
4
+ return AUTO_MODE_DENY_SOURCE_SET.has(v);
5
+ }
@@ -97,7 +97,16 @@ export type BrainStatusPhase = "rate_limited" | "retrying" | "reconnecting" | "c
97
97
  * - `connect_refused` — the attempt got a definite negative about the target itself (nothing accepts
98
98
  * at that address, or the name has no address). This is the class the SHORT retry lane serves.
99
99
  * - `transport` — any other transport-level failure: a connect timeout, a reset, a mid-stream
100
- * tear, a stalled stream. No verdict about the target; the full ladder applies.
100
+ * tear. No verdict about the target; the full ladder applies.
101
+ * - `stall` — no failure was observed on the link at all: THIS process cancelled its own
102
+ * request because one of its stall watchdogs reached its deadline while the stream produced
103
+ * nothing. The distinction from `transport` is WHO GAVE UP, and nothing more: a `transport` frame
104
+ * reports something that happened to the call, a `stall` frame reports a local deadline. It is not
105
+ * a verdict on the remedy — a blackholed connection (no reset, no close) produces silence, and
106
+ * silence is what a watchdog measures, so a `stall` can still turn out to be a network fault; what
107
+ * it always tells you truthfully is that the request was cancelled here, at a configured deadline,
108
+ * rather than lost out there. Reporting these as `transport` asserted the opposite — that the link
109
+ * had dropped — when nothing about the link was known at all.
101
110
  * - `rate_limit` — the provider asked the caller to slow down.
102
111
  * - `server` — the provider reported a failure on its own side.
103
112
  * - `http` — a response the status predicate calls terminal, retried anyway because the
@@ -105,7 +114,7 @@ export type BrainStatusPhase = "rate_limited" | "retrying" | "reconnecting" | "c
105
114
  * - `output_cap` — not a failure of the connection at all: the request is being re-sent with a
106
115
  * lowered output cap after the provider reported the context limit exceeded (no backoff).
107
116
  */
108
- export type BrainRetryErrClass = "connect_refused" | "transport" | "rate_limit" | "server" | "http" | "output_cap";
117
+ export type BrainRetryErrClass = "connect_refused" | "transport" | "stall" | "rate_limit" | "server" | "http" | "output_cap";
109
118
  /** design/99 §E3/§E10 — the payload of a {@link TaskEvent} `status` event (and the brain→runner signal). */
110
119
  export interface BrainStatus {
111
120
  phase: BrainStatusPhase;
@@ -3,6 +3,7 @@ import { summarizeWorkflowRun } from "./workflow-run-store.js";
3
3
  import { delimitUntrusted } from "./untrusted-text.js";
4
4
  import { boundedRedactedSummary } from "./untrusted-egress.js";
5
5
  import { clipWithFilePointer } from "./tool-errors.js";
6
+ import { announceEnvKnob } from "./env-knob-announce.js";
6
7
  export function mintCompletionId(target) {
7
8
  if (target.completionId === undefined)
8
9
  target.completionId = uuidv7();
@@ -61,14 +62,15 @@ const TASK_OUTPUT_DEFAULT_CHARS = 32_000;
61
62
  export const TASK_OUTPUT_MAX_CHARS = 160_000;
62
63
  const TASK_OUTPUT_MIN_CHARS = 512;
63
64
  function taskMaxOutputChars() {
64
- const raw = process.env.TASK_MAX_OUTPUT_LENGTH;
65
- if (!raw)
65
+ const rawEnv = process.env.TASK_MAX_OUTPUT_LENGTH;
66
+ if (rawEnv === undefined || rawEnv.trim() === "")
66
67
  return TASK_OUTPUT_DEFAULT_CHARS;
67
- if (!/^\d+$/.test(raw.trim()))
68
- return TASK_OUTPUT_DEFAULT_CHARS;
69
- const n = parseInt(raw.trim(), 10);
70
- if (!Number.isFinite(n) || n <= 0)
68
+ const raw = rawEnv.trim();
69
+ const n = /^\d+$/.test(raw) ? parseInt(raw, 10) : Number.NaN;
70
+ if (!Number.isFinite(n) || n <= 0) {
71
+ announceEnvKnob(`TASK_MAX_OUTPUT_LENGTH=${rawEnv} was ignored — it is not a positive whole number of characters. Using ${TASK_OUTPUT_DEFAULT_CHARS} instead.`);
71
72
  return TASK_OUTPUT_DEFAULT_CHARS;
73
+ }
72
74
  return Math.min(Math.max(n, TASK_OUTPUT_MIN_CHARS), TASK_OUTPUT_MAX_CHARS);
73
75
  }
74
76
  export function clipTaskOutput(s, fullOutputPath) {
@@ -25,6 +25,14 @@ import type { NestedUsage } from "./tool-spec.js";
25
25
  * Orchestrators that map an unexpected `suspended` should map an unexpected `needs_review` the same
26
26
  * hard-boundary way (it is, like `suspended`, only safe at the top-level `runTask` boundary).
27
27
  */
28
+ /**
29
+ * Terminal vs waiting. Three words are TERMINAL — the leg is over and nobody is expected to act: `completed`,
30
+ * `failed`, and `blocked` (the AGENT's own report, through `report_blocked`, that it cannot finish and why —
31
+ * a self-report, not a pause; it waits for nobody). Two words are WAITING — a person's decision resumes the
32
+ * same leg through `runner.resume(token, …)`: `suspended` (a durable pre-action approval of a tool call) and
33
+ * `needs_review` (a durable review pause). The task-registry face spells the two waiting words as `parked`.
34
+ * A consumer that files `blocked` beside `suspended` is waiting on a decision that never comes.
35
+ */
28
36
  export type TaskStatus = "completed" | "blocked" | "failed" | "suspended" | "needs_review";
29
37
  /**
30
38
  * RB-439-a — one remote-workspace lifecycle failure, reported as data on {@link TaskResult.remoteEnvFailures}.
@@ -598,6 +606,13 @@ export interface TaskResult {
598
606
  * the compaction subtotal). OBSERVE-ONLY (never gates budget). The workflow display is unaffected (its agents
599
607
  * fail-on-suspend, never durably resume). A `spentToolCalls` ledger fold is a follow-on. */
600
608
  toolCalls?: number;
609
+ /** #692 C-b (additive): present (`true`) when the usage figures on this face are a LOWER BOUND rather than a
610
+ * measurement — at least one model round of the leg reported no usage frame (the brain's `usageMissing`
611
+ * mark; the same word the per-turn `turn_end` frame carries), or the result was minted by a host / an
612
+ * orchestration lane that never ran a model round it could measure (a refused spawn's journal entry, a
613
+ * prepare-time refusal, a synthetic abort). The numeric members stay required and stay what was counted:
614
+ * `tokens: 0` beside this key reads "unknown", never "free". Absent ⇔ every round reported usage. */
615
+ usageMissing?: true;
601
616
  /** Prompt (input) tokens that MISSED the prefix cache, summed across the task's turns and normalized
602
617
  * across providers (Anthropic already reports `input_tokens` this way; OpenAI/vLLM include the cache
603
618
  * in theirs, so the subsets are removed). Mutually exclusive with `cachedTokens`/`cacheWriteTokens*`;
@@ -277,7 +277,16 @@ export interface TaskStream extends AsyncIterable<TaskEvent> {
277
277
  }>;
278
278
  /**
279
279
  * Hard-**interrupt** the running task — a real abort (not a best-effort hint), equivalent to firing the
280
- * task's `signal` (design/47). No-op if the task already finished. For a SOFT redirect that lets the
280
+ * task's `signal` (design/47). Refused typed (`steering.not_running`, the steer-family liveness code
281
+ * `cannot interrupt: …`) once the task has finished (the loop's end included: the teardown window
282
+ * refuses like `halt`/`steer` do), never silently dropped. **A run that has already given its answer
283
+ * is not cancellable**: once its last turn ended `stopReason:"stop"` with no queued steer / follow-up
284
+ * left to serve (a continuation the person had already asked for makes that "stop" work in progress,
285
+ * not an answer), a stop landing on the run's
286
+ * tail (this verb, the task's `signal`, the walltime or turns limit — one law) still aborts what is
287
+ * left of that tail (a final-boundary compaction is mooted) but the run ends `completed` with the
288
+ * answer as `result`, writes no interruption marker, mints no `limits.*` code, and says so once with
289
+ * the `task.interrupt_unconsumed` notice. For a SOFT redirect that lets the
281
290
  * task keep running and adjust, use {@link steer} instead. For the CC-Esc "stop and wait for my
282
291
  * input" form — cut the turn, keep the ending clean and resumable — use {@link halt}.
283
292
  */
@@ -20,7 +20,10 @@ import type { AssertAllKeysHandled } from "./ask-origin.js";
20
20
  * the one cause→plane derivation, for the faces that still speak the word.
21
21
  * - `completed` — the run finished (a person's clean halt included — see `TaskResult.haltedByUser`).
22
22
  * - `failed` — a limit, a provider failure, an abort, an invalid output…
23
- * - `blocked` — the agent could not finish and said why.
23
+ * - `blocked` — the AGENT reported, through `report_blocked`, that it cannot finish, and why. A TERMINAL
24
+ * cause: the leg is over and it waits for nobody (nothing to resume, no token). The waiting-on-a-person
25
+ * states are the `paused` cause's two status words — `suspended` (a pre-action approval of a tool
26
+ * call) and `needs_review` (a review pause); the task-registry face spells both as `parked`.
24
27
  * - `paused` — a durable pause committed a checkpoint and the run is resumable.
25
28
  * A `GateOutcome` is the record of ONE gate pass (per call); a `TerminalCause` is the record of ONE run
26
29
  * (per leg); a paused run's `gate` and the park row's `gate.kind` are the same word, joined by the token.
@@ -73,7 +76,8 @@ export type TerminalCause = {
73
76
  nestedPause?: PausedCause;
74
77
  } | {
75
78
  kind: "blocked";
76
- /** Why the agent could not finish. */
79
+ /** Why the agent could not finish — the agent's own words from its `report_blocked` call. Terminal: the
80
+ * run is over and nobody is being waited on (contrast `paused`, whose token is the resume capability). */
77
81
  reason: string;
78
82
  } | {
79
83
  kind: "paused";
@@ -150,7 +150,15 @@ export interface ToolRuleFace {
150
150
  * budgets a MINTED name to — a minted name is always inside this wire bound by construction; a CALLER
151
151
  * declaration is judged against it at prepare. */
152
152
  export declare const TOOL_WIRE_NAME_MAX_CHARS = 128;
153
+ /** The roster's wire bound on a SHORT identity member: a contract's `contractId` / `implementationRevision`, and the
154
+ * `modelGate` class a definition carries. Bounded at the mint (`rosterMemberBoundProblem`), never truncated. */
153
155
  export declare const TOOL_CONTRACT_MAX_CHARS = 64;
156
+ /** The roster's wire bound on a row's `cardId` — a COMPOSITE minted from bounded members, so its bound is DERIVED
157
+ * from theirs rather than declared beside them: the widest form is `<mcp|a2a>:<peer>/<tool>` with the peer and the
158
+ * protocol tool segment each inside the wire NAME bound (`builtin:<name>` and `<caller|external>:<contractId>` are
159
+ * narrower). A row minted from in-bound members therefore fits by construction; the mint's own check on it is a
160
+ * tripwire for a new source form, never a working rule. */
161
+ export declare const TOOL_CARD_ID_MAX_CHARS: number;
154
162
  /** The roster's wire bound on a KEY — a schema property name, a path-target `param`/alias, a rule-face param. A key is a
155
163
  * SEMANTIC name every fence reads an argument by, so it is never truncated: a declaration over the bound is refused at
156
164
  * the door (caller) or dropped with a disclosed reason (MCP intake), and a row carries every key exactly. */
@@ -14,6 +14,7 @@ export function pathTargetSlotOnly(pt) {
14
14
  }
15
15
  export const TOOL_WIRE_NAME_MAX_CHARS = 128;
16
16
  export const TOOL_CONTRACT_MAX_CHARS = 64;
17
+ export const TOOL_CARD_ID_MAX_CHARS = "mcp:".length + TOOL_WIRE_NAME_MAX_CHARS + "/".length + TOOL_WIRE_NAME_MAX_CHARS;
17
18
  export const TOOL_KEY_MAX_CHARS = 256;
18
19
  export const TOOL_KEYS_MAX = 1024;
19
20
  export const RENDER_HINT_MAX_CHARS = 64;
@@ -19,9 +19,11 @@ export interface ToolCallRequest {
19
19
  * (how the target may be confirmed by path), the display family, and the engine-minted identity
20
20
  * (`capabilityId` / `contractId` / `shapeDigest`) an exemption store keys on. Stamped at the gate entry
21
21
  * from the live roster, so identity follows the OBJECT the call dispatches to — a caller tool that merely
22
- * took a built-in's name carries its own face, never the built-in's. ABSENT when a policy is invoked
23
- * outside a Runner (a bare producer): a path-confining face then reads the catalog's declaration for the
24
- * NAME (the pre-388 posture, never wider), and a skill-scope face treats the write as unconfirmable.
22
+ * took a built-in's name carries its own face, never the built-in's. ABSENT only on a BARE request (a
23
+ * policy invoked outside a Runner by a producer that stamped nothing): a PROTECTIVE fence (a deny-list)
24
+ * then reads the catalog's declaration for the NAME, tighten-only; a CONTAINMENT fence (a grant admitting a
25
+ * write by its path) REFUSES the request, typed (`policy.bare_request`, #626 ②) — stamp the face yourself
26
+ * (`toolCallFaceOf(roster, name)`) when driving a policy outside a Runner.
25
27
  */
26
28
  face?: ToolCallFace;
27
29
  /**
@@ -104,6 +106,27 @@ export interface ToolCallRequest {
104
106
  * deny/ask lane and the person's own allow rules, never over a mandated ask. */
105
107
  declare const DECISION_REASONS: readonly ["rule", "mode", "hook", "safety", "classifier", "persisted_rule", "sandbox", "org_rule", "org_unavailable", "read_only"];
106
108
  export type DecisionReason = (typeof DECISION_REASONS)[number];
109
+ /** Carry every engine attestation (settlement, classifier cause) from a decision onto a reconstruction of it — the
110
+ * seam for the engine's OWN clone sites outside this module (the inherited chain's final-form re-check carries the
111
+ * post-rewrite args on a spread of an ancestor's deny). Not re-exported from `src/index.ts`. */
112
+ export declare function carryEngineAttestations(from: object, to: object): void;
113
+ /** Carry a PRECEDING decision's settlement onto a later deny that has none of its own — the veto record (a person's
114
+ * yes on the settlement, the refusing layer on the disposition; invariant I3) for the engine's own clone sites,
115
+ * where the deny is minted over a decision that already carried an attested approval. A deny that carries its own
116
+ * settlement keeps it (the latest resolution wins). Not re-exported from `src/index.ts`. */
117
+ export declare function carryPrecedingSettlement(preceding: object, to: object): void;
118
+ /** Attest that `d` is a deny a classifier station minted for `call` out of a round of the named FORM. */
119
+ export declare function attestClassifierDenyCause<T extends object>(d: T, cause: import("./gate-outcome.js").ClassifierDenyCause, call: {
120
+ toolCallId: string;
121
+ toolName: string;
122
+ }): T;
123
+ /** Read the attested classifier cause off a deny FOR the named call (the gate's fold and exit are the
124
+ * consumers): a binding to a different call is a replayed object and answers absence. An internal seam
125
+ * between engine modules — deliberately NOT re-exported from `src/index.ts`. */
126
+ export declare function classifierDenyCauseOf(d: unknown, call: {
127
+ toolCallId: string;
128
+ toolName: string;
129
+ }): import("./gate-outcome.js").ClassifierDenyCause | undefined;
107
130
  /** Read the engine-attested settlement off a funneled decision FOR the named call (the gate's exit is the
108
131
  * one consumer): an attestation bound to a different call is a replayed object and answers absence.
109
132
  * Exported for the gate module only — deliberately NOT re-exported from `src/index.ts` (an internal seam
@@ -207,15 +230,6 @@ export type PermissionResult = {
207
230
  * same safe direction as `matchedAskRule`); it cannot state a window of its own, because the
208
231
  * route it would be a window for has not been chosen yet. */
209
232
  denialLimitFallback?: import("./auto-mode.js").UnarmedDenialLimitFallback;
210
- /** #616 (additive): the classifier-unavailable FACT on an ASK — `cause` is the verdict's own word
211
- * ({@link import("./auto-mode.js").AutoModeUnavailableCause}). Since #661 the ENGINE no longer writes
212
- * it here: an `unavailable` verdict is a DENY (the deny arm's member above), never an ask handed to a
213
- * person. The member stays on the ask arm as display metadata a policy may self-declare on its own
214
- * ask (nothing reads it to decide anything); the carry stations still copy it onto the approval
215
- * request and the durable row when present. Retirement candidate for the next wire window. */
216
- classifierUnavailable?: {
217
- readonly cause: import("./auto-mode.js").AutoModeUnavailableCause;
218
- };
219
233
  /** #144 disclosure (additive): a persisted allow rule MATCHED this call but could not clear the
220
234
  * ask, because the ask is MANDATED (operator shellGate:"always", or the tool's own
221
235
  * egress/irreversibility marks) rather than a classifier's hesitation — "allow rules silence
@@ -285,21 +299,18 @@ export type PermissionResult = {
285
299
  * exclusions; an approval surface may render the card by it. Optional on the TYPE only because a
286
300
  * decision is caller-composable; every ask the gate hands to its resolver carries it. */
287
301
  origin?: import("./ask-origin.js").AskOrigin;
302
+ /** #688 C3 (additive): WHICH mechanism stands behind an `origin: "rule_store_unavailable"` — one word from
303
+ * {@link import("./ask-origin.js").RULE_STORE_UNREADABLE_KINDS} (`store`: the wired store could not be read;
304
+ * `call`: the call could not be read against the person's rows — the lexer's sentence is in `message`).
305
+ * ENGINE-STAMPED at the same single stamp point as `origin`, from the gate's own lane read; a policy's
306
+ * self-declared value is overwritten or removed there. Present ⇔ the origin is `rule_store_unavailable`.
307
+ * Carried onto the approval request and the durable row beside the origin word. */
308
+ ruleStoreUnreadable?: import("./ask-origin.js").RuleStoreUnreadable;
288
309
  } | {
289
310
  action: "deny";
290
311
  updatedInput?: unknown;
291
312
  message?: string;
292
313
  decisionReason?: DecisionReason;
293
- /** #661 (additive): this deny is the classifier's UNAVAILABILITY — the auto-mode classifier was consulted
294
- * and could not run, and the call is refused with a sentence that says so (CC 2.1.250 `Ze.unavailable` ⇒
295
- * `deny`, `x1t`). `cause` is the verdict's own word ({@link import("./auto-mode.js").AutoModeUnavailableCause}).
296
- * ENGINE-STAMPED beside `decisionReason: "classifier"` at the classifier stations (the gate's own and the
297
- * inherited-lane arms) on an `unavailable` verdict only; a `block` and a `parse_error` deny carry nothing
298
- * here. Read by the deny observer's payload ({@link import("./hooks.js").PermissionDeniedPayload}) and by
299
- * nothing that decides. */
300
- classifierUnavailable?: {
301
- readonly cause: import("./auto-mode.js").AutoModeUnavailableCause;
302
- };
303
314
  };
304
315
  /**
305
316
  * design/252 G-2 — WHY a piece of rule-provenance evidence is not on an ask.
@@ -1155,15 +1166,12 @@ export interface AskRequest {
1155
1166
  * that forwards a decision's member here does not compile, which is the point: the window is the
1156
1167
  * route's fact, and forwarding it silently is how a configured window becomes a wait with none. */
1157
1168
  readonly denialLimitFallback?: import("./auto-mode.js").DenialLimitFallback;
1158
- /** #616 (additive) — present ⇔ the auto-mode classifier was consulted on this ask and could not run (see
1159
- * the {@link PermissionResult} ask-arm member of the same name): the card's "asked because the
1160
- * classifier was unavailable" fact, with the verdict's own cause word. Absent on every ask the classifier
1161
- * answered, was not eligible for, or was not wired for read presence, never absence. Filled by the
1162
- * carry stations from the decision, never a caller/worker-settable field; the durable park row carries
1163
- * the same member (`PendingAction.tool_approval.classifierUnavailable`). */
1164
- readonly classifierUnavailable?: {
1165
- readonly cause: import("./auto-mode.js").AutoModeUnavailableCause;
1166
- };
1169
+ /** #688 C3 (additive) — the wire twin of the {@link PermissionResult} ask-arm member of the same name: which
1170
+ * mechanism stands behind an `origin: "rule_store_unavailable"` (`store` / `call`,
1171
+ * {@link import("./ask-origin.js").RULE_STORE_UNREADABLE_KINDS}). Present that origin; a consumer that
1172
+ * renders "the rule store is unreachable" vs "this command could not be read against your rules" branches
1173
+ * here, never on the message text. Filled by the gate's own carry station from the engine-stamped decision. */
1174
+ readonly ruleStoreUnreadable?: import("./ask-origin.js").RuleStoreUnreadable;
1167
1175
  /** (additive) WHICH AUTHORITY raised this ask — the wire twin of the {@link PermissionResult} ask-arm
1168
1176
  * member of the same name, one word from the closed {@link import("./ask-origin.js").AskOrigin} set.
1169
1177
  * The gate's own mint station copies the engine-stamped word; the three inherited-lane stations
@@ -29,6 +29,32 @@ function transferEngineSettlement(from, to) {
29
29
  const v = ENGINE_SETTLEMENTS.get(from);
30
30
  if (v !== undefined)
31
31
  ENGINE_SETTLEMENTS.set(to, v);
32
+ const c = CLASSIFIER_DENY_CAUSES_SEAT.get(from);
33
+ if (c !== undefined)
34
+ CLASSIFIER_DENY_CAUSES_SEAT.set(to, c);
35
+ }
36
+ export function carryEngineAttestations(from, to) {
37
+ transferEngineSettlement(from, to);
38
+ }
39
+ export function carryPrecedingSettlement(preceding, to) {
40
+ if (ENGINE_SETTLEMENTS.get(to) !== undefined)
41
+ return;
42
+ const v = ENGINE_SETTLEMENTS.get(preceding);
43
+ if (v !== undefined)
44
+ ENGINE_SETTLEMENTS.set(to, v);
45
+ }
46
+ const CLASSIFIER_DENY_CAUSES_SEAT = new WeakMap();
47
+ export function attestClassifierDenyCause(d, cause, call) {
48
+ CLASSIFIER_DENY_CAUSES_SEAT.set(d, { cause, toolCallId: call.toolCallId, toolName: call.toolName });
49
+ return d;
50
+ }
51
+ export function classifierDenyCauseOf(d, call) {
52
+ if (typeof d !== "object" || d === null)
53
+ return undefined;
54
+ const v = CLASSIFIER_DENY_CAUSES_SEAT.get(d);
55
+ if (v === undefined || v.toolCallId !== call.toolCallId || v.toolName !== call.toolName)
56
+ return undefined;
57
+ return v.cause;
32
58
  }
33
59
  export function engineSettlementOf(d, call) {
34
60
  if (typeof d !== "object" || d === null)
@@ -400,7 +426,6 @@ export function combinePolicies(...policies) {
400
426
  let ruleAskText;
401
427
  let probeMandateSeen = false;
402
428
  let fallbackSeen;
403
- let classifierUnavailableSeen;
404
429
  for (const p of policies) {
405
430
  const d = refuseOutOfContractDecision(await p.check(current, signal));
406
431
  if (d.action === "deny") {
@@ -410,6 +435,8 @@ export function combinePolicies(...policies) {
410
435
  }
411
436
  if (settledAllow !== undefined && ENGINE_SETTLEMENTS.get(out) === undefined) {
412
437
  const owned = out === d ? { ...d } : out;
438
+ if (owned !== d)
439
+ transferEngineSettlement(d, owned);
413
440
  transferEngineSettlement(settledAllow, owned);
414
441
  return owned;
415
442
  }
@@ -431,16 +458,13 @@ export function combinePolicies(...policies) {
431
458
  probeMandateSeen = true;
432
459
  if (d.action === "ask" && d.denialLimitFallback !== undefined && fallbackSeen === undefined)
433
460
  fallbackSeen = d.denialLimitFallback;
434
- if (d.action === "ask" && d.classifierUnavailable !== undefined && classifierUnavailableSeen === undefined)
435
- classifierUnavailableSeen = d.classifierUnavailable;
436
461
  }
437
462
  if (asked) {
438
463
  const merged = rewrite?.updatedInput;
439
464
  const withRuleAsk = ruleAskText !== undefined && asked.matchedAskRule === undefined ? { ...asked, matchedAskRule: ruleAskText } : asked;
440
465
  const withMark = probeMandateSeen && withRuleAsk.probeMandated !== true ? { ...withRuleAsk, probeMandated: true } : withRuleAsk;
441
466
  const withFallback = fallbackSeen !== undefined && withMark.denialLimitFallback === undefined ? { ...withMark, denialLimitFallback: fallbackSeen, requiresRealApproval: true } : withMark;
442
- const withFact = classifierUnavailableSeen !== undefined && withFallback.classifierUnavailable === undefined ? { ...withFallback, classifierUnavailable: { cause: classifierUnavailableSeen.cause } } : withFallback;
443
- return merged !== undefined ? { ...withFact, updatedInput: merged } : withFact;
467
+ return merged !== undefined ? { ...withFallback, updatedInput: merged } : withFallback;
444
468
  }
445
469
  const allowed = rewrite ?? ALLOW;
446
470
  if (settledAllow === undefined)
@@ -62,13 +62,21 @@ type CallPathTarget = ToolCallPathTarget;
62
62
  * a different file, and the deny this floor exists to preserve simply misses.
63
63
  */
64
64
  export declare function protectivePathTargetOf(call: ToolCallLike): CallPathTarget | undefined;
65
+ /** The typed refusal a CONTAINMENT reader mints for a request that carries no face at all (#626 ② — design/388 B6's
66
+ * bare-producer migration seam, closed). */
67
+ export declare const BARE_REQUEST_CODE = "policy.bare_request";
65
68
  /**
66
69
  * The path target a CONTAINMENT fence reads (a grant admitting a write BY its path: session `allowDirs`, the fs-write
67
70
  * gate's accept/exempt dirs; the active-skill allowPaths face reads its own eligibility bit the same way): the object
68
71
  * face's OWN declaration only — a same-name tool that declares no path target is an UNCONFIRMABLE write (the catalog's
69
- * slot may not be the one it writes; a decoy in that slot would buy an admission), so the fence fails closed. A BARE
70
- * request (no face at all — a policy invoked outside a Runner) reads the catalog for the name: the pre-388 reading, the
71
- * documented migration seam for bare producers.
72
+ * slot may not be the one it writes; a decoy in that slot would buy an admission), so the fence fails closed.
73
+ *
74
+ * #626 ②: a BARE request (no face at all) is REFUSED, typed (`policy.bare_request`), never answered from the catalog
75
+ * for the name. A grant is a claim about the tool's OWN slot, and only a prepared leg's roster can say which object
76
+ * the call dispatches to — the catalog's declaration for a NAME is exactly the reading a same-name caller tool would
77
+ * exploit. Inside a Runner every gated request carries the leg's face (the gate entry, the resume belts and the
78
+ * pre-CAS edit recheck stamp it); a policy invoked outside a Runner stamps its own (`toolCallFaceOf(roster, name)`).
79
+ * The PROTECTIVE floor above keeps reading the catalog for a bare request: tighten-only, a deny can only gain a slot.
72
80
  */
73
81
  export declare function declaredPathTargetOf(call: ToolCallLike): CallPathTarget | undefined;
74
82
  /** Is this call a write a PROTECTIVE fence judges (a protected target whose access ≠ read)? The name sets are its static view. */
@@ -54,8 +54,14 @@ export function protectivePathTargetOf(call) {
54
54
  const floor = pathTargetOf(call.toolName);
55
55
  return floor === undefined || call.face === undefined ? floor : pathTargetSlotOnly(floor);
56
56
  }
57
+ export const BARE_REQUEST_CODE = "policy.bare_request";
57
58
  export function declaredPathTargetOf(call) {
58
- return call.face !== undefined ? call.face.pathTarget : pathTargetOf(call.toolName);
59
+ if (call.face === undefined) {
60
+ const e = new Error(`tool call ${JSON.stringify(call.toolName)} carries no face: a containment fence admits a write only by the tool's OWN declared path target, which a prepared leg's roster stamps on every gated request — a policy invoked outside a Runner must stamp \`face\` itself (toolCallFaceOf) rather than expect the catalog's declaration for the name`);
61
+ e.code = BARE_REQUEST_CODE;
62
+ throw e;
63
+ }
64
+ return call.face.pathTarget;
59
65
  }
60
66
  export function isProtectedWrite(call) {
61
67
  const pt = protectivePathTargetOf(call);
@@ -309,6 +309,32 @@ export declare function inputKeysOf(schema: TSchema | undefined): {
309
309
  /** The roster's KEY bounds over a schema (design/388 B20): a problem sentence, or undefined when every top-level key fits.
310
310
  * Shared by the caller declaration door (typed refusal) and the MCP intake (disclosed drop) — a key is never truncated. */
311
311
  export declare function schemaKeyBoundProblem(schema: TSchema | undefined): string | undefined;
312
+ /** The identity members of one roster row the wire bounds hold, as a DECLARATION (the door) or a MINTED row hands them. */
313
+ export interface RosterBoundMembers {
314
+ name: string;
315
+ aliases?: readonly string[];
316
+ /** An MCP/A2A mount's peer (the row's `origin.peer`) — a NAME on the roster, held to the wire name bound. */
317
+ peer?: string;
318
+ contractId?: string;
319
+ implementationRevision?: string;
320
+ modelGate?: string;
321
+ cardId?: string;
322
+ }
323
+ /** The typed refusal a member over its bound mints — the door's two codes, and the mint's own for the members no
324
+ * declaration door names (`modelGate`, the derived `cardId`). */
325
+ export type RosterBoundCode = "config.tool_name_too_long" | "config.tool_contract_too_long" | "config.tool_roster_bound";
326
+ /**
327
+ * design/388 B20 / #626 ① — the ONE bound judge over a row's identity members: a problem `{ code, message }`, or
328
+ * undefined when every member fits. Read by the declaration door (a `TaskSpec.tools` entry, refused before session
329
+ * acquire) and by the row mint (every source — an MCP/A2A peer, a definition that reached the mint by a route the
330
+ * door does not cover — refused before the row exists). A member over its bound is NEVER truncated onto the row: a
331
+ * truncated identity is a different identity (`judgeParkedToolIdentity` compares it across a park; the card
332
+ * namespace door reads it), and the schema's own bounds would then describe a row the mint never made.
333
+ */
334
+ export declare function rosterMemberBoundProblem(m: RosterBoundMembers): {
335
+ code: RosterBoundCode;
336
+ message: string;
337
+ } | undefined;
312
338
  /**
313
339
  * The rule vocabulary of ONE mounted instance (design/388 §2.2 ③ / D-3): DERIVED = the scalar top-level
314
340
  * keys ∪ the path target's `param` + aliases; a declared `params` list can only NARROW it (a declared
@@ -2,12 +2,12 @@ import { createHash } from "node:crypto";
2
2
  import { Type } from "typebox";
3
3
  import { getToolContract } from "../prompt-assembly/tool-catalog.js";
4
4
  import { protocolOf } from "./protocol-table.js";
5
- import { RENDER_HINT_ACTIVITY_MAX_CHARS, RENDER_HINT_MAX_CHARS, RENDER_HINT_MAX_LIST, TOOL_APPROVAL_CARDS, TOOL_FAMILIES, TOOL_KEYS_MAX, TOOL_KEY_MAX_CHARS, TOOL_MOUNT_TAGS, TOOL_PATH_ABSENCES, TOOL_PATH_BASES, } from "./tool-face.js";
5
+ import { RENDER_HINT_ACTIVITY_MAX_CHARS, RENDER_HINT_MAX_CHARS, RENDER_HINT_MAX_LIST, TOOL_APPROVAL_CARDS, TOOL_CARD_ID_MAX_CHARS, TOOL_CONTRACT_MAX_CHARS, TOOL_FAMILIES, TOOL_KEYS_MAX, TOOL_KEY_MAX_CHARS, TOOL_MOUNT_TAGS, TOOL_WIRE_NAME_MAX_CHARS, TOOL_PATH_ABSENCES, TOOL_PATH_BASES, } from "./tool-face.js";
6
6
  function lit(values) {
7
7
  return Type.Union(values.map((v) => Type.Literal(v)));
8
8
  }
9
- const Name = Type.String({ minLength: 1, maxLength: 128 });
10
- const Short = Type.String({ maxLength: 64 });
9
+ const Name = Type.String({ minLength: 1, maxLength: TOOL_WIRE_NAME_MAX_CHARS });
10
+ const Short = Type.String({ maxLength: TOOL_CONTRACT_MAX_CHARS });
11
11
  const Key = Type.String({ maxLength: TOOL_KEY_MAX_CHARS });
12
12
  const Digest16 = Type.String({ minLength: 16, maxLength: 16 });
13
13
  export const ROSTER_SOURCES = ["builtin", "caller", "host", "mcp", "a2a", "external", "synthetic"];
@@ -26,7 +26,7 @@ export const ToolRosterEntry = Type.Object({
26
26
  contract: Type.Object({ contractId: Short, implementationRevision: Short }, { additionalProperties: false }),
27
27
  shapeDigest: Digest16,
28
28
  wireSchemaDigest: Digest16,
29
- cardId: Type.String({ maxLength: 160 }),
29
+ cardId: Type.String({ maxLength: TOOL_CARD_ID_MAX_CHARS }),
30
30
  capabilityId: Digest16,
31
31
  inputKeys: Type.Array(Type.Object({ name: Key, type: lit(["string", "number", "integer", "boolean", "object", "array", "null", "mixed"]) }, { additionalProperties: false }), { maxItems: TOOL_KEYS_MAX }),
32
32
  effect: lit(["read", "write", "idempotent"]),
@@ -207,6 +207,28 @@ export function schemaKeyBoundProblem(schema) {
207
207
  return `the schema property ${JSON.stringify(long.slice(0, 40) + "…")} is ${long.length} characters; a roster key is at most ${TOOL_KEY_MAX_CHARS}`;
208
208
  return undefined;
209
209
  }
210
+ export function rosterMemberBoundProblem(m) {
211
+ if (typeof m.name !== "string")
212
+ return { code: "config.tool_roster_bound", message: `tool has no string name (got ${m.name === null ? "null" : typeof m.name}); every roster row is keyed by a string \`name\`` };
213
+ const over = (label, value, bound) => `${label} ${JSON.stringify(value.slice(0, 40) + "…")} is ${value.length} characters; the roster's bound is ${bound}`;
214
+ const of = (text) => `tool ${JSON.stringify(m.name.slice(0, 40))}: ${text}`;
215
+ if (m.name.length > TOOL_WIRE_NAME_MAX_CHARS)
216
+ return { code: "config.tool_name_too_long", message: over("tool name", m.name, TOOL_WIRE_NAME_MAX_CHARS) };
217
+ const alias = m.aliases?.find((a) => a.length > TOOL_WIRE_NAME_MAX_CHARS);
218
+ if (alias !== undefined)
219
+ return { code: "config.tool_name_too_long", message: of(over("alias", alias, TOOL_WIRE_NAME_MAX_CHARS)) };
220
+ if (m.peer !== undefined && m.peer.length > TOOL_WIRE_NAME_MAX_CHARS)
221
+ return { code: "config.tool_name_too_long", message: of(over("peer", m.peer, TOOL_WIRE_NAME_MAX_CHARS)) };
222
+ for (const [label, v] of [["contractId", m.contractId], ["implementationRevision", m.implementationRevision]]) {
223
+ if (v !== undefined && v.length > TOOL_CONTRACT_MAX_CHARS)
224
+ return { code: "config.tool_contract_too_long", message: of(over(`contract ${label}`, v, TOOL_CONTRACT_MAX_CHARS)) };
225
+ }
226
+ if (m.modelGate !== undefined && m.modelGate.length > TOOL_CONTRACT_MAX_CHARS)
227
+ return { code: "config.tool_roster_bound", message: of(over("modelGate", m.modelGate, TOOL_CONTRACT_MAX_CHARS)) };
228
+ if (m.cardId !== undefined && m.cardId.length > TOOL_CARD_ID_MAX_CHARS)
229
+ return { code: "config.tool_roster_bound", message: of(over("cardId", m.cardId, TOOL_CARD_ID_MAX_CHARS)) };
230
+ return undefined;
231
+ }
210
232
  export function ruleFaceOfInstance(schema, declared, pathTarget) {
211
233
  const { scalar } = inputKeysOf(schema);
212
234
  const derived = new Set(scalar);
@@ -375,15 +397,26 @@ export function mintRosterEntry(tool, meta, facts) {
375
397
  const pathTarget = face.pathTarget;
376
398
  const rf = ruleFaceOfInstance(tool.parameters, face.ruleFace, pathTarget);
377
399
  const hints = face.renderHints !== undefined ? capRenderHints(face.renderHints) : undefined;
400
+ const bound = rosterMemberBoundProblem({
401
+ name: tool.name,
402
+ aliases: tool.aliases,
403
+ ...(peer !== null ? { peer } : {}),
404
+ contractId: d.contractId,
405
+ implementationRevision: d.implementationRevision,
406
+ ...(typeof face.modelGate === "string" ? { modelGate: face.modelGate } : {}),
407
+ cardId,
408
+ });
409
+ if (bound !== undefined)
410
+ throw configError(bound.code, bound.message);
378
411
  return {
379
412
  name: tool.name,
380
413
  aliases: [...new Set(tool.aliases ?? [])],
381
414
  source: meta.source,
382
415
  ...(meta.origin !== undefined ? { origin: { peer: meta.origin.peer, ...(meta.origin.declaredBy !== undefined ? { declaredBy: meta.origin.declaredBy } : {}) } } : {}),
383
- contract: { contractId: d.contractId.slice(0, 64), implementationRevision: d.implementationRevision.slice(0, 64) },
416
+ contract: { contractId: d.contractId, implementationRevision: d.implementationRevision },
384
417
  shapeDigest: d.shapeDigest,
385
418
  wireSchemaDigest: d.wireSchemaDigest,
386
- cardId: cardId.slice(0, 160),
419
+ cardId,
387
420
  capabilityId: mintCapabilityId({ source: meta.source, peer, name: tool.name, contractId: d.contractId }),
388
421
  inputKeys,
389
422
  effect,
@@ -417,7 +450,7 @@ export function mintRosterEntry(tool, meta, facts) {
417
450
  validateInput: typeof tool.validateInput === "function",
418
451
  deferred: facts.deferred.has(tool.name),
419
452
  alwaysLoad: face.alwaysLoad === true || face.mcpAlwaysLoad === true,
420
- ...(typeof face.modelGate === "string" ? { modelGate: face.modelGate.slice(0, 64) } : {}),
453
+ ...(typeof face.modelGate === "string" ? { modelGate: face.modelGate } : {}),
421
454
  ...(hints !== undefined ? { renderHints: hints } : {}),
422
455
  mountedBy: meta.mountedBy,
423
456
  };