@sema-agent/core 7.11.2 → 7.13.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.
- package/CHANGELOG.md +86 -13
- package/dist/core/ask-origin.d.ts +55 -0
- package/dist/core/ask-origin.js +21 -0
- package/dist/core/auto-mode-arming.d.ts +10 -14
- package/dist/core/auto-mode-arming.js +3 -9
- package/dist/core/auto-mode-defaults.d.ts +0 -2
- package/dist/core/auto-mode-defaults.js +0 -1
- package/dist/core/auto-mode-rebuild.d.ts +6 -13
- package/dist/core/auto-mode-rebuild.js +0 -2
- package/dist/core/auto-mode.d.ts +30 -89
- package/dist/core/auto-mode.js +12 -59
- package/dist/core/checkpoint-store.d.ts +1 -3
- package/dist/core/engine-notice.d.ts +18 -0
- package/dist/core/gate-fold.js +1 -9
- package/dist/core/gate-lanes.d.ts +0 -40
- package/dist/core/gate-lanes.js +18 -26
- package/dist/core/governance-codes.d.ts +1 -1
- package/dist/core/governance-codes.js +2 -0
- package/dist/core/hooks.d.ts +7 -1
- package/dist/core/hooks.js +1 -1
- package/dist/core/runner/assemble-result.d.ts +45 -1
- package/dist/core/runner/assemble-result.js +4 -1
- package/dist/core/runner/compaction-seams.d.ts +42 -0
- package/dist/core/runner/compaction-seams.js +80 -0
- package/dist/core/runner/contracts.d.ts +85 -15
- package/dist/core/runner/denial-limit-arms.d.ts +10 -13
- package/dist/core/runner/denial-limit-arms.js +12 -9
- package/dist/core/runner/gate-exit.js +9 -1
- package/dist/core/runner/permission-rule-lanes.d.ts +2 -1
- package/dist/core/runner/permission-rule-lanes.js +2 -1
- package/dist/core/runner/prepare-caps-and-workflow.d.ts +1 -1
- package/dist/core/runner/prepare-caps-and-workflow.js +0 -5
- package/dist/core/runner/prepare-policy-chain.js +3 -2
- package/dist/core/runner/prepare-protocol-tools.js +5 -0
- package/dist/core/runner/prepare-safety-scan.js +11 -16
- package/dist/core/runner/prepare-suspend-saga.d.ts +0 -2
- package/dist/core/runner/prepare-suspend-saga.js +2 -10
- package/dist/core/runner/prepare-task.d.ts +9 -1
- package/dist/core/runner/prepare-task.js +7 -2
- package/dist/core/runner/prepare-wiring-manifest.d.ts +1 -1
- package/dist/core/runner/prepare-wiring-manifest.js +1 -8
- package/dist/core/runner/resume-claim.d.ts +2 -2
- package/dist/core/runner/resume-preflight.d.ts +2 -2
- package/dist/core/runner/run-attachment-seats.d.ts +6 -4
- package/dist/core/runner/run-attachment-seats.js +2 -2
- package/dist/core/runner/run-git-lane.d.ts +1 -1
- package/dist/core/runner/run-harness-handlers.js +2 -0
- package/dist/core/runner/run-identity-wiring.d.ts +21 -33
- package/dist/core/runner/run-identity-wiring.js +6 -3
- package/dist/core/runner/run-leg.d.ts +108 -0
- package/dist/core/runner/run-leg.js +713 -0
- package/dist/core/runner/run-notification-lane.d.ts +56 -0
- package/dist/core/runner/run-notification-lane.js +128 -0
- package/dist/core/runner/run-reasoning-seat.d.ts +5 -5
- package/dist/core/runner/run-reasoning-seat.js +7 -7
- package/dist/core/runner/run-recovery-lanes.d.ts +4 -17
- package/dist/core/runner/run-recovery-lanes.js +5 -4
- package/dist/core/runner/run-settle-and-teardown.d.ts +113 -0
- package/dist/core/runner/run-settle-and-teardown.js +432 -0
- package/dist/core/runner/run-stop-and-final-verify.d.ts +2 -2
- package/dist/core/runner/run-terminal-adoption.d.ts +99 -0
- package/dist/core/runner/run-terminal-adoption.js +200 -0
- package/dist/core/runner/run-turn-boundary.js +5 -4
- package/dist/core/runner/runtask.d.ts +10 -79
- package/dist/core/runner/runtask.js +83 -1557
- package/dist/core/runner/stream-lifecycle-verbs.js +10 -1
- package/dist/core/runner-deps.d.ts +4 -14
- package/dist/core/store-contracts/workflow-journal-store-contract.d.ts +7 -0
- package/dist/core/store-contracts/workflow-journal-store-contract.js +85 -0
- package/dist/core/task-stream.d.ts +10 -1
- package/dist/core/tool-face.d.ts +8 -0
- package/dist/core/tool-face.js +1 -0
- package/dist/core/tool-policy.d.ts +42 -96
- package/dist/core/tool-policy.js +1 -11
- package/dist/core/tool-registry.d.ts +11 -3
- package/dist/core/tool-registry.js +7 -1
- package/dist/core/tool-roster.d.ts +26 -0
- package/dist/core/tool-roster.js +38 -7
- package/dist/core/trace.d.ts +6 -7
- package/dist/core/wiring-manifest.d.ts +5 -22
- package/dist/core/wiring-manifest.js +3 -11
- package/dist/core/workflow-journal-store.d.ts +35 -4
- package/dist/core/workflow-journal-store.js +19 -2
- package/dist/index.d.ts +6 -5
- package/dist/index.js +6 -5
- package/dist/orchestration/workflow.js +2 -0
- package/dist/server/http.js +2 -1
- package/dist/stores/file/workflow-journal-store.d.ts +7 -10
- package/dist/stores/file/workflow-journal-store.js +2 -4
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +21 -11
|
@@ -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
|
-
|
|
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();
|
|
@@ -687,17 +687,10 @@ export interface RunnerDeps {
|
|
|
687
687
|
sessionContext?: readonly string[];
|
|
688
688
|
/** Transcript-window bounds for the classify prompt (defaults: 40 entries / 2000 chars each). */
|
|
689
689
|
window?: import("./auto-mode-prompt.js").AutoModeWindowOptions;
|
|
690
|
-
/** Classify round-trip cap, ms (default 15_000).
|
|
690
|
+
/** Classify round-trip cap, ms (default 15_000). A round past the cap is `unavailable(timeout)`: the call is
|
|
691
|
+
* DENIED with a sentence that says so (CC 2.1.250 form), and the next call is classified afresh — there is no
|
|
692
|
+
* failure count and no session latch. */
|
|
691
693
|
timeoutMs?: number;
|
|
692
|
-
/** Consecutive-failure threshold opening the one-way session breaker (default 3). */
|
|
693
|
-
failureThreshold?: number;
|
|
694
|
-
/** Fired ONCE when the session breaker opens (the "退回非 auto" alarm — surface it to the operator). The engine
|
|
695
|
-
* records the trip on the session's breaker read face (`WiringManifest.autoMode.breaker`, #616) BEFORE calling
|
|
696
|
-
* this hook, so a deployment that wires nothing here still gets the read face. */
|
|
697
|
-
onBreakerOpen?: (info: {
|
|
698
|
-
consecutiveFailures: number;
|
|
699
|
-
lastCause: import("./auto-mode.js").AutoModeBreakerCause;
|
|
700
|
-
}) => void;
|
|
701
694
|
/**
|
|
702
695
|
* The classifier DENIAL LIMIT (CC 2.1.250 `FO`/`AKe`): a run whose classifier keeps blocking falls
|
|
703
696
|
* back to a PERSON instead of being denied without end. Per run: a `block` first increments the
|
|
@@ -736,10 +729,7 @@ export interface RunnerDeps {
|
|
|
736
729
|
* line); a deployment that treats those as sensitive at rest should keep them out of the arming
|
|
737
730
|
* face rather than out of this flag.
|
|
738
731
|
*
|
|
739
|
-
*
|
|
740
|
-
* already fell back to non-auto hands nothing forward (the redemption must never run wider than the
|
|
741
|
-
* ancestor it inherits). A `true` here with a non-serializable face announces once and records
|
|
742
|
-
* nothing (`phase:"config"`).
|
|
732
|
+
* A `true` here with a non-serializable face announces once and records nothing (`phase:"config"`).
|
|
743
733
|
*/
|
|
744
734
|
persistArming?: boolean;
|
|
745
735
|
/**
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type WorkflowJournalStore } from "../workflow-journal-store.js";
|
|
2
|
+
import { type ContractAssertionRunner } from "./contract-harness.js";
|
|
3
|
+
/**
|
|
4
|
+
* The full cross-backend contract for {@link WorkflowJournalStore}. Every case builds its own store via
|
|
5
|
+
* `make()`; with no runner the cases run sequentially and the first failure throws.
|
|
6
|
+
*/
|
|
7
|
+
export declare function workflowJournalStoreContract(make: () => WorkflowJournalStore, runAssertion?: ContractAssertionRunner): Promise<void>;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { strict as assert } from "node:assert";
|
|
2
|
+
import { JOURNAL_OVERSIZE_ERROR_CODE, MAX_JOURNAL_RESULT_BYTES, } from "../workflow-journal-store.js";
|
|
3
|
+
import { beginContract } from "./contract-harness.js";
|
|
4
|
+
const key = (ordinal) => `${ordinal}:h`;
|
|
5
|
+
const completed = (ordinal, over = {}) => ({
|
|
6
|
+
taskId: `t-${ordinal}`,
|
|
7
|
+
sessionId: `s-${ordinal}`,
|
|
8
|
+
terminal: { kind: "completed" },
|
|
9
|
+
result: `r${ordinal}`,
|
|
10
|
+
structuredOutput: { ordinal, nested: { ok: true } },
|
|
11
|
+
stats: { turns: 1, tokens: 1, costMicroUsd: 0, nested: { tokens: 0, turns: 0, tasks: 0, costMicroUsd: 0 } },
|
|
12
|
+
...over,
|
|
13
|
+
});
|
|
14
|
+
const parked = (ordinal, over = {}) => ({
|
|
15
|
+
taskId: `t-${ordinal}`,
|
|
16
|
+
sessionId: `s-${ordinal}`,
|
|
17
|
+
terminal: { kind: "paused", token: `tok-${ordinal}`, checkpointId: `cp-${ordinal}`, gate: { kind: "human", reason: "approve", toolName: "Write" } },
|
|
18
|
+
result: "",
|
|
19
|
+
structuredOutput: { partial: ordinal },
|
|
20
|
+
stats: { turns: 1, tokens: 1, costMicroUsd: 0, nested: { tokens: 3, turns: 2, tasks: 1, costMicroUsd: 1 } },
|
|
21
|
+
...over,
|
|
22
|
+
});
|
|
23
|
+
const OVER_CAP = "x".repeat(MAX_JOURNAL_RESULT_BYTES + 1024);
|
|
24
|
+
const isOversizeRefusal = (e) => e instanceof Error && e.code === JOURNAL_OVERSIZE_ERROR_CODE;
|
|
25
|
+
export async function workflowJournalStoreContract(make, runAssertion) {
|
|
26
|
+
const { run, settle } = beginContract(runAssertion);
|
|
27
|
+
run("append → load returns entries ASCENDING by ordinal, both arms verbatim; a re-append per ordinal OVERWRITES", async () => {
|
|
28
|
+
const store = make();
|
|
29
|
+
await store.append("run-1", "tenant-a", { callKey: key(10), result: completed(10) });
|
|
30
|
+
await store.append("run-1", "tenant-a", { callKey: key(1), parked: parked(1) });
|
|
31
|
+
await store.append("run-1", "tenant-a", { callKey: key(0), result: completed(0) });
|
|
32
|
+
await store.append("run-1", "tenant-a", { callKey: key(2), result: completed(2) });
|
|
33
|
+
await store.append("run-1", "tenant-a", { callKey: key(0), result: completed(0, { result: "overwritten" }) });
|
|
34
|
+
const entries = await store.load("run-1", "tenant-a");
|
|
35
|
+
assert.deepEqual(entries.map((e) => e.callKey), [key(0), key(1), key(2), key(10)]);
|
|
36
|
+
assert.deepEqual(entries[0], { callKey: key(0), result: completed(0, { result: "overwritten" }) });
|
|
37
|
+
assert.deepEqual(entries[1], { callKey: key(1), parked: parked(1) });
|
|
38
|
+
});
|
|
39
|
+
run("scope wall: a cross-scope load is EMPTY — before and after the owner's own load, and after a refused cross-scope append — on both arms", async () => {
|
|
40
|
+
const store = make();
|
|
41
|
+
await store.append("run-1", "alice", { callKey: key(0), result: completed(0) });
|
|
42
|
+
await store.append("run-1", "alice", { callKey: key(1), parked: parked(1) });
|
|
43
|
+
assert.deepEqual(await store.load("run-1", "mallory"), []);
|
|
44
|
+
assert.equal((await store.load("run-1", "alice")).length, 2);
|
|
45
|
+
assert.deepEqual(await store.load("run-1", "mallory"), []);
|
|
46
|
+
await assert.rejects(store.append("run-1", "mallory", { callKey: key(2), result: completed(2) }));
|
|
47
|
+
await assert.rejects(store.append("run-1", "mallory", { callKey: key(3), parked: parked(3) }));
|
|
48
|
+
assert.deepEqual(await store.load("run-1", "mallory"), []);
|
|
49
|
+
assert.deepEqual((await store.load("run-1", "alice")).map((e) => e.callKey), [key(0), key(1)]);
|
|
50
|
+
});
|
|
51
|
+
run(`oversize RESULT arm ⇒ append rejects with code "${JOURNAL_OVERSIZE_ERROR_CODE}"; nothing journaled; the store stays usable`, async () => {
|
|
52
|
+
const store = make();
|
|
53
|
+
await store.append("run-1", "tenant-a", { callKey: key(0), result: completed(0) });
|
|
54
|
+
await assert.rejects(store.append("run-1", "tenant-a", { callKey: key(1), result: completed(1, { result: OVER_CAP }) }), isOversizeRefusal);
|
|
55
|
+
assert.deepEqual((await store.load("run-1", "tenant-a")).map((e) => e.callKey), [key(0)]);
|
|
56
|
+
await store.append("run-1", "tenant-a", { callKey: key(2), result: completed(2) });
|
|
57
|
+
assert.deepEqual((await store.load("run-1", "tenant-a")).map((e) => e.callKey), [key(0), key(2)]);
|
|
58
|
+
});
|
|
59
|
+
run(`oversize PARKED arm ⇒ append rejects with code "${JOURNAL_OVERSIZE_ERROR_CODE}"; nothing journaled (never a silent skip — a lost park is a resume running the ordinal live)`, async () => {
|
|
60
|
+
const store = make();
|
|
61
|
+
await store.append("run-1", "tenant-a", { callKey: key(0), result: completed(0) });
|
|
62
|
+
await assert.rejects(store.append("run-1", "tenant-a", { callKey: key(1), parked: parked(1, { result: OVER_CAP }) }), isOversizeRefusal);
|
|
63
|
+
assert.deepEqual((await store.load("run-1", "tenant-a")).map((e) => e.callKey), [key(0)]);
|
|
64
|
+
await store.append("run-1", "tenant-a", { callKey: key(1), parked: parked(1) });
|
|
65
|
+
assert.equal((await store.load("run-1", "tenant-a"))[1].parked?.terminal.kind, "paused");
|
|
66
|
+
});
|
|
67
|
+
run("the cap is inclusive: a payload of EXACTLY the cap's byte count is accepted on both arms", async () => {
|
|
68
|
+
const store = make();
|
|
69
|
+
const fit = (payload) => {
|
|
70
|
+
const base = Buffer.byteLength(JSON.stringify({ ...payload, result: "" }), "utf8");
|
|
71
|
+
return { ...payload, result: "y".repeat(MAX_JOURNAL_RESULT_BYTES - base) };
|
|
72
|
+
};
|
|
73
|
+
const r = fit(completed(0));
|
|
74
|
+
const p = fit(parked(1));
|
|
75
|
+
assert.equal(Buffer.byteLength(JSON.stringify(r), "utf8"), MAX_JOURNAL_RESULT_BYTES);
|
|
76
|
+
assert.equal(Buffer.byteLength(JSON.stringify(p), "utf8"), MAX_JOURNAL_RESULT_BYTES);
|
|
77
|
+
await store.append("run-1", "tenant-a", { callKey: key(0), result: r });
|
|
78
|
+
await store.append("run-1", "tenant-a", { callKey: key(1), parked: p });
|
|
79
|
+
const entries = await store.load("run-1", "tenant-a");
|
|
80
|
+
assert.deepEqual(entries.map((e) => e.callKey), [key(0), key(1)]);
|
|
81
|
+
assert.deepEqual(entries[0].result, r);
|
|
82
|
+
assert.deepEqual(entries[1].parked, p);
|
|
83
|
+
});
|
|
84
|
+
await settle();
|
|
85
|
+
}
|
|
@@ -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).
|
|
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
|
*/
|
package/dist/core/tool-face.d.ts
CHANGED
|
@@ -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. */
|
package/dist/core/tool-face.js
CHANGED
|
@@ -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
|
|
23
|
-
* outside a Runner
|
|
24
|
-
*
|
|
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
|
/**
|
|
@@ -207,20 +209,12 @@ export type PermissionResult = {
|
|
|
207
209
|
* same safe direction as `matchedAskRule`); it cannot state a window of its own, because the
|
|
208
210
|
* route it would be a window for has not been chosen yet. */
|
|
209
211
|
denialLimitFallback?: import("./auto-mode.js").UnarmedDenialLimitFallback;
|
|
210
|
-
/** #616 (additive): the
|
|
211
|
-
*
|
|
212
|
-
*
|
|
213
|
-
*
|
|
214
|
-
*
|
|
215
|
-
*
|
|
216
|
-
* the outcome (`automode-unavailable`) beside the decision's provenance, never in place of it, and
|
|
217
|
-
* this engine's divergence (the ask reaches a person instead of being denied) does not move the
|
|
218
|
-
* fact onto the origin axis either. ENGINE-STAMPED at the classifier stations (the gate's own and
|
|
219
|
-
* the inherited-lane arms) only when the verdict was `unavailable`; `parse_error` stamps nothing
|
|
220
|
-
* (the classifier ran and answered outside its contract — a different sentence). Display metadata:
|
|
221
|
-
* nothing reads it to decide anything, so a policy that self-declares it can only put its own
|
|
222
|
-
* sentence on its own card. Carried onto the approval request and the durable row by the carry
|
|
223
|
-
* stations. */
|
|
212
|
+
/** #616 (additive): the classifier-unavailable FACT on an ASK — `cause` is the verdict's own word
|
|
213
|
+
* ({@link import("./auto-mode.js").AutoModeUnavailableCause}). Since #661 the ENGINE no longer writes
|
|
214
|
+
* it here: an `unavailable` verdict is a DENY (the deny arm's member above), never an ask handed to a
|
|
215
|
+
* person. The member stays on the ask arm as display metadata a policy may self-declare on its own
|
|
216
|
+
* ask (nothing reads it to decide anything); the carry stations still copy it onto the approval
|
|
217
|
+
* request and the durable row when present. Retirement candidate for the next wire window. */
|
|
224
218
|
classifierUnavailable?: {
|
|
225
219
|
readonly cause: import("./auto-mode.js").AutoModeUnavailableCause;
|
|
226
220
|
};
|
|
@@ -298,6 +292,16 @@ export type PermissionResult = {
|
|
|
298
292
|
updatedInput?: unknown;
|
|
299
293
|
message?: string;
|
|
300
294
|
decisionReason?: DecisionReason;
|
|
295
|
+
/** #661 (additive): this deny is the classifier's UNAVAILABILITY — the auto-mode classifier was consulted
|
|
296
|
+
* and could not run, and the call is refused with a sentence that says so (CC 2.1.250 `Ze.unavailable` ⇒
|
|
297
|
+
* `deny`, `x1t`). `cause` is the verdict's own word ({@link import("./auto-mode.js").AutoModeUnavailableCause}).
|
|
298
|
+
* ENGINE-STAMPED beside `decisionReason: "classifier"` at the classifier stations (the gate's own and the
|
|
299
|
+
* inherited-lane arms) on an `unavailable` verdict only; a `block` and a `parse_error` deny carry nothing
|
|
300
|
+
* here. Read by the deny observer's payload ({@link import("./hooks.js").PermissionDeniedPayload}) and by
|
|
301
|
+
* nothing that decides. */
|
|
302
|
+
classifierUnavailable?: {
|
|
303
|
+
readonly cause: import("./auto-mode.js").AutoModeUnavailableCause;
|
|
304
|
+
};
|
|
301
305
|
};
|
|
302
306
|
/**
|
|
303
307
|
* design/252 G-2 — WHY a piece of rule-provenance evidence is not on an ask.
|
|
@@ -540,100 +544,42 @@ export declare function constraintChainEntryOf(policy: ToolPolicy, meta?: Constr
|
|
|
540
544
|
/**
|
|
541
545
|
* One inherited-chain LAYER as both chain-entry mint sites see it — structurally an
|
|
542
546
|
* `InheritedGate.parentConstraints[]` element, typed here structurally so this module (which owns the
|
|
543
|
-
* digest) does not depend on the runner's shapes.
|
|
547
|
+
* digest) does not depend on the runner's shapes. The classifier itself is not read: a decider carries no
|
|
548
|
+
* state between rounds, so nothing about its health enters a chain entry.
|
|
544
549
|
*/
|
|
545
550
|
export interface ConstraintChainLayerView {
|
|
546
551
|
policy: ToolPolicy;
|
|
547
552
|
autoMode?: {
|
|
548
|
-
decider: {
|
|
549
|
-
breakerOpen: () => boolean;
|
|
550
|
-
consecutiveFailures?: () => number;
|
|
551
|
-
};
|
|
552
553
|
arming?: unknown;
|
|
553
554
|
};
|
|
554
555
|
durableMandate?: boolean;
|
|
555
556
|
contentMandate?: boolean;
|
|
556
557
|
}
|
|
557
558
|
/**
|
|
558
|
-
* The WRITE-side mint: the chain entry a durable PARK records for a live inherited layer.
|
|
559
|
-
*
|
|
560
|
-
*
|
|
561
|
-
*
|
|
562
|
-
*
|
|
563
|
-
*
|
|
564
|
-
*
|
|
565
|
-
*
|
|
566
|
-
*
|
|
567
|
-
*
|
|
568
|
-
*
|
|
569
|
-
*
|
|
570
|
-
* classification from dropping the whole session to non-auto — would hand a redemption the full budget
|
|
571
|
-
* again, and the redeemed leg stays auto-classified through failures the ancestor would not have
|
|
572
|
-
* survived. So the streak must read ZERO as well. A decider that does not expose it at all is UNKNOWN
|
|
573
|
-
* state, and unknown takes the same arm as tripped: nothing is recorded (which is why a host that hand
|
|
574
|
-
* rolls a decider cannot use the persisted-arming path — it must mint through `createAutoModeDecider`,
|
|
575
|
-
* on both the arming and the redeeming side).
|
|
576
|
-
*
|
|
577
|
-
* Every read here is guarded, and every failure to read is treated as "withdrawn" — the opposite default
|
|
578
|
-
* from the gate's demote-to-ask breaker reader, and for the same reason both are right: there, an
|
|
579
|
-
* unreadable latch must not suppress a tighten; here, it must not license a carry-forward.
|
|
580
|
-
*
|
|
581
|
-
* ⚠️ WRITE SIDE ONLY. This gate reads MUTABLE state (a one-way latch, plus a streak that both grows and
|
|
582
|
-
* resets to zero on any healthy classification). The resume re-supply must NOT re-run it — it samples a
|
|
583
|
-
* different instant, and the two instants disagree for perfectly ordinary reasons: an ancestor keeps
|
|
584
|
-
* running while its delegated child sits parked awaiting a human. See
|
|
585
|
-
* {@link constraintChainEntryOfSuppliedLayer} for the verify side and the full account.
|
|
586
|
-
*
|
|
587
|
-
* ⚖️ WHAT THIS BOUND DOES NOT DO (recorded, precise shape). A withdrawn arming leaves the entry at
|
|
588
|
-
* `autoModeArmed: true` with no recipe — digest-identical to an armed layer whose deployment never opted
|
|
589
|
-
* in. So the digest does not, and cannot, stop a re-supplier from handing back a FRESH healthy decider of
|
|
590
|
-
* its own: that is the pre-existing re-supply trust posture (see `resumeStream`'s contract — a caller
|
|
591
|
-
* able to invoke it already holds full Runner authority, and the chain checks catch shape MISTAKES, not
|
|
592
|
-
* a hostile trusted caller), and it is unchanged here. What this bound governs is what CORE hands
|
|
593
|
-
* forward: a withdrawn ancestor's CRITERIA never travel, so a redemption cannot reconstruct the
|
|
594
|
-
* ancestor's classifier from the row. For a fleet running `persistArming` everywhere, the pair
|
|
595
|
-
* (`autoModeArmed` present, `autoModeArming` absent) is itself the readable signal that the ancestor's
|
|
596
|
-
* arming was withdrawn or unrecordable, and such a host should re-supply a refusing decider rather than a
|
|
597
|
-
* fresh one. Making the WITHDRAWAL itself digest-bound is a coherent stronger design — it needs a new
|
|
598
|
-
* marker plus a new re-supply obligation to reproduce it (a withdrawn row carries no recipe, so the
|
|
599
|
-
* marker cannot be derived from one), which is a scope decision for this seam's owner, not a silent
|
|
600
|
-
* mechanism change.
|
|
559
|
+
* The WRITE-side mint: the chain entry a durable PARK records for a live inherited layer. The recipe is
|
|
560
|
+
* carried whenever the layer has one (the deployment opted into `persistArming`). The former gate — "only
|
|
561
|
+
* while the ancestor's breaker is untripped and untouched" — went with the breaker: a decider carries no
|
|
562
|
+
* state between rounds, so there is no ancestor health to sample here and nothing that could make a
|
|
563
|
+
* redemption run wider than the ancestor it inherits.
|
|
564
|
+
*
|
|
565
|
+
* ⚖️ WHAT THE ROW DOES NOT SAY. An armed layer whose deployment never opted in leaves the entry at
|
|
566
|
+
* `autoModeArmed: true` with no recipe. The digest does not, and cannot, stop a re-supplier from handing
|
|
567
|
+
* back a FRESH decider of its own: that is the pre-existing re-supply trust posture (see `resumeStream`'s
|
|
568
|
+
* contract — a caller able to invoke it already holds full Runner authority, and the chain checks catch
|
|
569
|
+
* shape MISTAKES, not a hostile trusted caller). What CORE hands forward is the recipe when there is one,
|
|
570
|
+
* and nothing otherwise.
|
|
601
571
|
*/
|
|
602
572
|
export declare function constraintChainEntryOfLayer(layer: ConstraintChainLayerView): ConstraintChainEntry;
|
|
603
573
|
/**
|
|
604
574
|
* The VERIFY-side mint: the entry a resume's RE-SUPPLIED layer must reproduce for the row it is
|
|
605
575
|
* redeeming. Same body as the park mint, with the arming carry taken from the ROW rather than from the
|
|
606
|
-
* layer
|
|
607
|
-
*
|
|
608
|
-
*
|
|
609
|
-
*
|
|
610
|
-
*
|
|
611
|
-
*
|
|
612
|
-
*
|
|
613
|
-
* - the ancestor was healthy at the park (the row carries the recipe) and its breaker opened while the
|
|
614
|
-
* child sat parked ⇒ the re-supply mints WITHOUT the recipe, the digests differ, and because the
|
|
615
|
-
* latch is ONE-WAY the row is refused `resume.parent_constraint_mismatch` forever;
|
|
616
|
-
* - the ancestor was one failure into its streak at the park (the row carries no recipe) and the streak
|
|
617
|
-
* cleared on its next healthy classification ⇒ the re-supply mints WITH a recipe the row never
|
|
618
|
-
* carried, same refusal.
|
|
619
|
-
* Neither needs a host mistake: the ancestor and its siblings keep running while the child is parked, and
|
|
620
|
-
* the same-process `resume()` verb re-supplies the very same live layer objects out of the Runner's own
|
|
621
|
-
* parked-constraint registry. Nothing about the row changed; only a classifier's health did.
|
|
622
|
-
*
|
|
623
|
-
* WHAT THIS COSTS, NAMED. A row that recorded NO recipe no longer refuses a re-supply that hands one
|
|
624
|
-
* back. That is not a tunable — the re-supply side literally cannot tell "the row withdrew a recipe this
|
|
625
|
-
* same live layer still declares" (the second bullet above, a legitimate redemption) from "a re-supplier
|
|
626
|
-
* is putting a recipe onto a row that withdrew one": both are the identical pair of inputs. It is also
|
|
627
|
-
* thinner than it looks. The recipe is not an enforcement object — the DECIDER classifies, and the
|
|
628
|
-
* re-supply trust posture already permits handing back a fresh healthy one, which is the capability that
|
|
629
|
-
* would matter. What the row's absence still means is unchanged and is the property bound ① was written
|
|
630
|
-
* for: a withdrawn ancestor's criteria never travel ON THE ROW, so no redemption can reconstruct that
|
|
631
|
-
* ancestor's classifier from the checkpoint.
|
|
632
|
-
*
|
|
633
|
-
* WHAT THE DIGEST STILL BINDS. A row that DID record a recipe must have that recipe handed back: the
|
|
634
|
-
* carry is on, so a re-supply that drops the arming, or hands back a different one, mints a different
|
|
635
|
-
* entry and is refused pre-CAS exactly as before. That is the whole of the documented re-supply contract
|
|
636
|
-
* — "hand back what the row carries".
|
|
576
|
+
* layer — the digest equality is over bytes the row itself recorded, never over what a live object says at
|
|
577
|
+
* a second instant. A row that DID record a recipe must have that recipe handed back: a re-supply that drops
|
|
578
|
+
* the arming, or hands back a different one, mints a different entry and is refused pre-CAS
|
|
579
|
+
* (`resume.parent_constraint_mismatch`). A row that recorded none does not refuse a re-supply that hands one
|
|
580
|
+
* back — the recipe is not an enforcement object (the DECIDER classifies), and the re-supply trust posture
|
|
581
|
+
* already permits a fresh decider; what the row's absence still means is that no redemption can reconstruct
|
|
582
|
+
* the ancestor's classifier FROM THE CHECKPOINT.
|
|
637
583
|
*/
|
|
638
584
|
export declare function constraintChainEntryOfSuppliedLayer(layer: ConstraintChainLayerView, recorded: ConstraintChainEntry | undefined): ConstraintChainEntry;
|
|
639
585
|
/**
|
package/dist/core/tool-policy.js
CHANGED
|
@@ -128,17 +128,7 @@ function chainEntryOfLayerCarrying(layer, carryArming) {
|
|
|
128
128
|
});
|
|
129
129
|
}
|
|
130
130
|
export function constraintChainEntryOfLayer(layer) {
|
|
131
|
-
|
|
132
|
-
if (layer.autoMode?.arming !== undefined) {
|
|
133
|
-
try {
|
|
134
|
-
const d = layer.autoMode.decider;
|
|
135
|
-
armingLive = d.breakerOpen() !== true && typeof d.consecutiveFailures === "function" && d.consecutiveFailures() === 0;
|
|
136
|
-
}
|
|
137
|
-
catch {
|
|
138
|
-
armingLive = false;
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
return chainEntryOfLayerCarrying(layer, armingLive);
|
|
131
|
+
return chainEntryOfLayerCarrying(layer, layer.autoMode?.arming !== undefined);
|
|
142
132
|
}
|
|
143
133
|
export function constraintChainEntryOfSuppliedLayer(layer, recorded) {
|
|
144
134
|
return chainEntryOfLayerCarrying(layer, recorded?.autoModeArming !== 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.
|
|
70
|
-
*
|
|
71
|
-
*
|
|
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
|
-
|
|
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
|
package/dist/core/tool-roster.js
CHANGED
|
@@ -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:
|
|
10
|
-
const Short = Type.String({ maxLength:
|
|
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:
|
|
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,26 @@ 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
|
+
const over = (label, value, bound) => `${label} ${JSON.stringify(value.slice(0, 40) + "…")} is ${value.length} characters; the roster's bound is ${bound}`;
|
|
212
|
+
const of = (text) => `tool ${JSON.stringify(m.name.slice(0, 40))}: ${text}`;
|
|
213
|
+
if (m.name.length > TOOL_WIRE_NAME_MAX_CHARS)
|
|
214
|
+
return { code: "config.tool_name_too_long", message: over("tool name", m.name, TOOL_WIRE_NAME_MAX_CHARS) };
|
|
215
|
+
const alias = m.aliases?.find((a) => a.length > TOOL_WIRE_NAME_MAX_CHARS);
|
|
216
|
+
if (alias !== undefined)
|
|
217
|
+
return { code: "config.tool_name_too_long", message: of(over("alias", alias, TOOL_WIRE_NAME_MAX_CHARS)) };
|
|
218
|
+
if (m.peer !== undefined && m.peer.length > TOOL_WIRE_NAME_MAX_CHARS)
|
|
219
|
+
return { code: "config.tool_name_too_long", message: of(over("peer", m.peer, TOOL_WIRE_NAME_MAX_CHARS)) };
|
|
220
|
+
for (const [label, v] of [["contractId", m.contractId], ["implementationRevision", m.implementationRevision]]) {
|
|
221
|
+
if (v !== undefined && v.length > TOOL_CONTRACT_MAX_CHARS)
|
|
222
|
+
return { code: "config.tool_contract_too_long", message: of(over(`contract ${label}`, v, TOOL_CONTRACT_MAX_CHARS)) };
|
|
223
|
+
}
|
|
224
|
+
if (m.modelGate !== undefined && m.modelGate.length > TOOL_CONTRACT_MAX_CHARS)
|
|
225
|
+
return { code: "config.tool_roster_bound", message: of(over("modelGate", m.modelGate, TOOL_CONTRACT_MAX_CHARS)) };
|
|
226
|
+
if (m.cardId !== undefined && m.cardId.length > TOOL_CARD_ID_MAX_CHARS)
|
|
227
|
+
return { code: "config.tool_roster_bound", message: of(over("cardId", m.cardId, TOOL_CARD_ID_MAX_CHARS)) };
|
|
228
|
+
return undefined;
|
|
229
|
+
}
|
|
210
230
|
export function ruleFaceOfInstance(schema, declared, pathTarget) {
|
|
211
231
|
const { scalar } = inputKeysOf(schema);
|
|
212
232
|
const derived = new Set(scalar);
|
|
@@ -375,15 +395,26 @@ export function mintRosterEntry(tool, meta, facts) {
|
|
|
375
395
|
const pathTarget = face.pathTarget;
|
|
376
396
|
const rf = ruleFaceOfInstance(tool.parameters, face.ruleFace, pathTarget);
|
|
377
397
|
const hints = face.renderHints !== undefined ? capRenderHints(face.renderHints) : undefined;
|
|
398
|
+
const bound = rosterMemberBoundProblem({
|
|
399
|
+
name: tool.name,
|
|
400
|
+
aliases: tool.aliases,
|
|
401
|
+
...(peer !== null ? { peer } : {}),
|
|
402
|
+
contractId: d.contractId,
|
|
403
|
+
implementationRevision: d.implementationRevision,
|
|
404
|
+
...(typeof face.modelGate === "string" ? { modelGate: face.modelGate } : {}),
|
|
405
|
+
cardId,
|
|
406
|
+
});
|
|
407
|
+
if (bound !== undefined)
|
|
408
|
+
throw configError(bound.code, bound.message);
|
|
378
409
|
return {
|
|
379
410
|
name: tool.name,
|
|
380
411
|
aliases: [...new Set(tool.aliases ?? [])],
|
|
381
412
|
source: meta.source,
|
|
382
413
|
...(meta.origin !== undefined ? { origin: { peer: meta.origin.peer, ...(meta.origin.declaredBy !== undefined ? { declaredBy: meta.origin.declaredBy } : {}) } } : {}),
|
|
383
|
-
contract: { contractId: d.contractId
|
|
414
|
+
contract: { contractId: d.contractId, implementationRevision: d.implementationRevision },
|
|
384
415
|
shapeDigest: d.shapeDigest,
|
|
385
416
|
wireSchemaDigest: d.wireSchemaDigest,
|
|
386
|
-
cardId
|
|
417
|
+
cardId,
|
|
387
418
|
capabilityId: mintCapabilityId({ source: meta.source, peer, name: tool.name, contractId: d.contractId }),
|
|
388
419
|
inputKeys,
|
|
389
420
|
effect,
|
|
@@ -417,7 +448,7 @@ export function mintRosterEntry(tool, meta, facts) {
|
|
|
417
448
|
validateInput: typeof tool.validateInput === "function",
|
|
418
449
|
deferred: facts.deferred.has(tool.name),
|
|
419
450
|
alwaysLoad: face.alwaysLoad === true || face.mcpAlwaysLoad === true,
|
|
420
|
-
...(typeof face.modelGate === "string" ? { modelGate: face.modelGate
|
|
451
|
+
...(typeof face.modelGate === "string" ? { modelGate: face.modelGate } : {}),
|
|
421
452
|
...(hints !== undefined ? { renderHints: hints } : {}),
|
|
422
453
|
mountedBy: meta.mountedBy,
|
|
423
454
|
};
|
package/dist/core/trace.d.ts
CHANGED
|
@@ -529,11 +529,10 @@ export type TraceEvent = {
|
|
|
529
529
|
} | {
|
|
530
530
|
/**
|
|
531
531
|
* The auto-mode classifier decided (or declined to decide) one pending ask — one frame per
|
|
532
|
-
* `decide` call,
|
|
533
|
-
*
|
|
534
|
-
*
|
|
535
|
-
*
|
|
536
|
-
* read face a shell renders is a separate, additive wire face. `ms` is the wall time from the
|
|
532
|
+
* `decide` call, so a consumer can read the gate's wall-clock cost per tool call and the failure
|
|
533
|
+
* cause behind a call the classifier could not evaluate. A TRACE frame rather than a task event: it
|
|
534
|
+
* fires once per gated call and it is an operator's read (why did that ask take four seconds / why
|
|
535
|
+
* was that call denied). `ms` is the wall time from the
|
|
537
536
|
* decide call to its verdict (timeout included: a timed-out round reads the deadline).
|
|
538
537
|
*/
|
|
539
538
|
kind: "auto_mode.classified";
|
|
@@ -547,8 +546,8 @@ export type TraceEvent = {
|
|
|
547
546
|
ms: number;
|
|
548
547
|
/** The verdict kind the gate acted on (`allow` / `block` / `unavailable` / `parse_error`). */
|
|
549
548
|
verdict: "allow" | "block" | "unavailable" | "parse_error";
|
|
550
|
-
/** Present iff `verdict === "unavailable"`: `error` / `timeout
|
|
551
|
-
cause?: "error" | "timeout"
|
|
549
|
+
/** Present iff `verdict === "unavailable"`: `error` / `timeout`. */
|
|
550
|
+
cause?: "error" | "timeout";
|
|
552
551
|
ts: number;
|
|
553
552
|
} | {
|
|
554
553
|
/** C1 — the failover brain served this call from a FALLBACK entry (`createFailoverBrain`): the
|