@sema-agent/core 5.56.0 → 5.58.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 +122 -0
- package/dist/agents/cascade.d.ts +1 -1
- package/dist/agents/cumulative-stats.d.ts +1 -1
- package/dist/agents/observer.d.ts +2 -2
- package/dist/agents/peer-admission.d.ts +1 -1
- package/dist/agents/retain-ledger.d.ts +2 -2
- package/dist/agents/roster-store.d.ts +8 -8
- package/dist/agents/send-message-tool.d.ts +13 -2
- package/dist/agents/send-message-tool.js +34 -12
- package/dist/agents/subagent-steps.d.ts +1 -1
- package/dist/agents/subagent.d.ts +13 -13
- package/dist/agents/team.d.ts +15 -6
- package/dist/agents/team.js +1 -0
- package/dist/agents/tool-filter.d.ts +2 -2
- package/dist/agents/verify.d.ts +1 -1
- package/dist/bench/metrics.d.ts +35 -35
- package/dist/brain/anthropic.js +15 -5
- package/dist/brain/circuit-breaker.js +2 -1
- package/dist/brain/degrading.d.ts +1 -1
- package/dist/brain/degrading.js +4 -1
- package/dist/brain/errors.d.ts +3 -3
- package/dist/brain/failover.js +16 -1
- package/dist/brain/open-responses.js +15 -5
- package/dist/brain/openai.js +16 -5
- package/dist/brain/reasoning.d.ts +2 -2
- package/dist/brain/repetition.d.ts +1 -1
- package/dist/brain/request-params.d.ts +30 -27
- package/dist/brain/request-params.js +1 -7
- package/dist/brain/route-adjudicator.d.ts +190 -0
- package/dist/brain/route-adjudicator.js +189 -0
- package/dist/brain/route-conformance.d.ts +55 -0
- package/dist/brain/route-conformance.js +136 -0
- package/dist/brain/routing.js +8 -3
- package/dist/brain/status-sink.d.ts +2 -2
- package/dist/brain/stream-shared.d.ts +1 -1
- package/dist/config/catalog.d.ts +5 -5
- package/dist/core/arg-summary.d.ts +4 -4
- package/dist/core/ask-class.d.ts +2 -2
- package/dist/core/ask-question.d.ts +1 -1
- package/dist/core/auto-compaction.d.ts +15 -15
- package/dist/core/auto-mode.d.ts +5 -5
- package/dist/core/background-agent-store.d.ts +20 -20
- package/dist/core/background-shell.d.ts +4 -4
- package/dist/core/checkpoint-store.d.ts +35 -27
- package/dist/core/context-edit.d.ts +1 -1
- package/dist/core/context-guard.d.ts +1 -1
- package/dist/core/exec-output-tail.d.ts +6 -6
- package/dist/core/file-snapshot-store.d.ts +8 -8
- package/dist/core/git-worktree-env.d.ts +3 -3
- package/dist/core/governance-codes.js +2 -0
- package/dist/core/hooks.d.ts +73 -33
- package/dist/core/hooks.js +87 -25
- package/dist/core/image-downsample.d.ts +1 -1
- package/dist/core/locked-config.d.ts +1 -1
- package/dist/core/lsp.d.ts +1 -1
- package/dist/core/mailbox-store.d.ts +1 -1
- package/dist/core/mcp.d.ts +3 -3
- package/dist/core/mcp.js +4 -4
- package/dist/core/memory-engine/consolidation-driver.d.ts +207 -0
- package/dist/core/memory-engine/consolidation-driver.js +378 -0
- package/dist/core/memory-engine/consolidation.d.ts +46 -2
- package/dist/core/memory-engine/consolidation.js +1 -0
- package/dist/core/memory-engine/data-plane.d.ts +1 -1
- package/dist/core/memory-engine/distiller.d.ts +550 -0
- package/dist/core/memory-engine/distiller.js +598 -0
- package/dist/core/memory-engine/dual-root.d.ts +1 -1
- package/dist/core/memory-engine/engine.d.ts +62 -8
- package/dist/core/memory-engine/engine.js +40 -4
- package/dist/core/memory-engine/file-backend.d.ts +1 -1
- package/dist/core/memory-engine/index.d.ts +4 -2
- package/dist/core/memory-engine/index.js +4 -2
- package/dist/core/memory-engine/origin-clearance.d.ts +1 -1
- package/dist/core/memory-engine/scope-contract.d.ts +4 -4
- package/dist/core/memory-engine/sync-client.d.ts +16 -16
- package/dist/core/memory-engine/sync.d.ts +4 -4
- package/dist/core/memory-recall.d.ts +1 -1
- package/dist/core/memory.d.ts +2 -2
- package/dist/core/permission-rule-consent.d.ts +230 -36
- package/dist/core/permission-rule-consent.js +258 -54
- package/dist/core/permission-rule-model.d.ts +295 -97
- package/dist/core/permission-rule-model.js +132 -41
- package/dist/core/permission-rules.d.ts +9 -9
- package/dist/core/remote-env.d.ts +8 -8
- package/dist/core/roles.d.ts +3 -3
- package/dist/core/roles.js +1 -0
- package/dist/core/runner/assemble-result.d.ts +2 -2
- package/dist/core/runner/compaction-call-options.d.ts +3 -3
- package/dist/core/runner/memory-consolidation-driver.d.ts +49 -0
- package/dist/core/runner/memory-consolidation-driver.js +60 -0
- package/dist/core/runner/memory-consolidation.d.ts +1 -1
- package/dist/core/runner/prepare-config-doors.d.ts +3 -3
- package/dist/core/runner/prepare-task.d.ts +21 -21
- package/dist/core/runner/prepare-task.js +54 -18
- package/dist/core/runner/prepare-workspace-restore.d.ts +2 -2
- package/dist/core/runner/runtask.d.ts +15 -12
- package/dist/core/runner/runtask.js +48 -0
- package/dist/core/runner/session-rule-policy.d.ts +1 -1
- package/dist/core/runner/teardown-bounded.d.ts +1 -1
- package/dist/core/runner/tool-disclosure.d.ts +2 -2
- package/dist/core/runner/turn-attachments.d.ts +11 -11
- package/dist/core/scheduler.d.ts +10 -5
- package/dist/core/secret-env.d.ts +1 -1
- package/dist/core/sensitive-path-policy.d.ts +1 -1
- package/dist/core/session-policy-store.d.ts +2 -2
- package/dist/core/session-reconcile.d.ts +2 -2
- package/dist/core/session-store.d.ts +3 -3
- package/dist/core/session.d.ts +1 -1
- package/dist/core/shutdown-debug.d.ts +2 -2
- package/dist/core/side-query.d.ts +14 -7
- package/dist/core/spec-contract.d.ts +1 -1
- package/dist/core/store-contracts/contract-harness.d.ts +2 -2
- package/dist/core/store-contracts/contract-kit-version.d.ts +2 -2
- package/dist/core/store-contracts/mailbox-store-contract.d.ts +1 -1
- package/dist/core/store-contracts/mailbox-store-contract.js +1 -1
- package/dist/core/task-notification.d.ts +5 -5
- package/dist/core/task-registry-agent.d.ts +12 -12
- package/dist/core/task-registry-monitor.d.ts +1 -1
- package/dist/core/task-registry-shared.d.ts +41 -41
- package/dist/core/task-registry.d.ts +12 -12
- package/dist/core/tool-detach.d.ts +2 -2
- package/dist/core/tool-errors.d.ts +3 -3
- package/dist/core/tool-policy.d.ts +55 -28
- package/dist/core/tool-result-budget.d.ts +1 -1
- package/dist/core/tool-result-store.d.ts +2 -2
- package/dist/core/tools.d.ts +1 -1
- package/dist/core/trace.d.ts +26 -23
- package/dist/core/types.d.ts +155 -70
- package/dist/core/untrusted-egress.d.ts +1 -1
- package/dist/core/untrusted-text.d.ts +7 -7
- package/dist/core/wiring-manifest.d.ts +5 -5
- package/dist/core/workflow-journal-store.d.ts +14 -14
- package/dist/core/workflow-run-store-contract.d.ts +1 -1
- package/dist/core/workflow-run-store-contract.js +1 -1
- package/dist/core/workflow-run-store.d.ts +4 -4
- package/dist/engine/compaction/compaction.d.ts +3 -3
- package/dist/engine/compaction/utils.d.ts +2 -2
- package/dist/engine/execution-env/kill-tree.d.ts +1 -1
- package/dist/engine/execution-env/node-execution-env.d.ts +8 -8
- package/dist/engine/harness/agent-harness.d.ts +6 -6
- package/dist/engine/harness/agent-harness.js +26 -1
- package/dist/engine/harness/messages.d.ts +1 -1
- package/dist/engine/harness/types.d.ts +15 -11
- package/dist/engine/llm/types.d.ts +79 -14
- package/dist/engine/loop/agent-loop.d.ts +3 -3
- package/dist/engine/loop/types.d.ts +4 -4
- package/dist/engine/lsp/node-lsp-manager.d.ts +2 -2
- package/dist/engine/session/import-validate.d.ts +1 -1
- package/dist/engine/session/log-digest.d.ts +1 -1
- package/dist/engine/session/memory-repo.d.ts +2 -2
- package/dist/engine/session/session.d.ts +4 -4
- package/dist/fixtures/index.d.ts +4 -4
- package/dist/index.d.ts +8 -4
- package/dist/index.js +5 -2
- package/dist/internal/llm.d.ts +1 -1
- package/dist/orchestration/goal.d.ts +1 -1
- package/dist/orchestration/run-spec.d.ts +1 -1
- package/dist/orchestration/run-workflow-tool.d.ts +12 -12
- package/dist/orchestration/workflow-governance.d.ts +4 -4
- package/dist/orchestration/workflow-observe.d.ts +1 -1
- package/dist/orchestration/workflow-script-runner.d.ts +1 -1
- package/dist/orchestration/workflow-script-store.d.ts +9 -9
- package/dist/orchestration/workflow-size-guideline.d.ts +1 -1
- package/dist/orchestration/workflow-types.d.ts +5 -5
- package/dist/orchestration/workflow.d.ts +10 -10
- package/dist/prompt-assembly/artifact-store.d.ts +1 -1
- package/dist/prompt-assembly/artifact.d.ts +1 -1
- package/dist/prompt-assembly/assemble.d.ts +1 -1
- package/dist/prompt-assembly/composer.d.ts +2 -2
- package/dist/prompt-assembly/epoch.d.ts +2 -2
- package/dist/prompt-assembly/event-registry.d.ts +1 -1
- package/dist/prompt-assembly/explain.d.ts +3 -3
- package/dist/prompt-assembly/tool-catalog.d.ts +1 -1
- package/dist/prompt-assembly/turn-snapshot.d.ts +4 -4
- package/dist/prompt-assembly/types.d.ts +12 -12
- package/dist/prompts/coordinator.d.ts +1 -1
- package/dist/prompts/default.d.ts +10 -10
- package/dist/prompts/default.js +2 -0
- package/dist/prompts/simple-sections.d.ts +3 -3
- package/dist/prompts/supervisor.d.ts +2 -2
- package/dist/scenarios/full-body.d.ts +3 -3
- package/dist/scenarios/scenario-registry.d.ts +6 -2
- package/dist/scenarios/scenario-registry.js +4 -2
- package/dist/stores/cc/sidecar-transcript.d.ts +3 -3
- package/dist/stores/file/fs-atomic.d.ts +2 -2
- package/dist/stores/file/index.d.ts +1 -1
- package/dist/stores/file/session-store.d.ts +2 -2
- package/dist/stores/file/workflow-journal-store.d.ts +4 -4
- package/dist/tools/fs/bash-readonly-classifier.d.ts +1 -1
- package/dist/tools/fs/encoding.d.ts +4 -4
- package/dist/tools/fs/fs-bash.d.ts +3 -3
- package/dist/tools/fs/fs-pdf.d.ts +1 -1
- package/dist/tools/fs/fs-shared.d.ts +6 -6
- package/dist/tools/fs/index.d.ts +2 -2
- package/dist/tools/fs/index.js +8 -1
- package/dist/tools/fs/notebook.d.ts +1 -1
- package/dist/tools/fs/pdf.d.ts +1 -1
- package/dist/tools/fs/read-deny.d.ts +1 -1
- package/dist/tools/fs/safety.d.ts +9 -9
- package/dist/tools/fs/search.d.ts +2 -2
- package/dist/tools/monitor.d.ts +3 -3
- package/dist/tools/scheduler-tools.js +28 -6
- package/dist/tools/task-list.d.ts +2 -2
- package/dist/tools/web.d.ts +19 -4
- package/dist/tools/web.js +8 -2
- package/dist/tools/worktree.d.ts +5 -5
- package/dist/tools/worktree.js +2 -2
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +74 -3
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* key↔URL route pairing adjudication — the SINGLE source of the pairing law.
|
|
3
|
+
*
|
|
4
|
+
* The invariant: **the credential a request carries must belong to the URL the request targets.**
|
|
5
|
+
* A request's URL half is per-ENTRY (`Model.baseUrl`, a required field) while the historical
|
|
6
|
+
* credential fallback was per-BRAIN-INSTANCE (`options.apiKey ?? config.apiKey`): with more than one
|
|
7
|
+
* entry flowing through one brain, the deployment credential silently followed EVERY entry's URL —
|
|
8
|
+
* including a third-party host it was never meant for. This module is what the three first-party
|
|
9
|
+
* brains now consult before shaping a request, what the brain decorators re-walk per hop (each
|
|
10
|
+
* hop/route re-resolves the three credential states for ITS model against ITS config), and what
|
|
11
|
+
* external consumers (a server annotating a model roster, a conformance suite) import so there is
|
|
12
|
+
* exactly one implementation of the law.
|
|
13
|
+
*
|
|
14
|
+
* The judgment, by credential source (see {@link RouteCredentialSource}):
|
|
15
|
+
* - `per-model` → OK always. A per-model credential was resolved FOR this entry (auth hook or an
|
|
16
|
+
* auth header on `Model.headers`); it is paired by construction, so the per-model lane pays no
|
|
17
|
+
* new cost.
|
|
18
|
+
* - `deployment-config` → OK only where the pairing is verifiable-and-true: the deployment DECLARED
|
|
19
|
+
* its credential's URL half (`config.baseUrl`) and the request targets exactly that root.
|
|
20
|
+
* A declared root with a diverging entry URL is the torn pair this module exists to stop
|
|
21
|
+
* (`route.credential_mismatch`). An UNDECLARED root (`config.apiKey` with no `config.baseUrl`)
|
|
22
|
+
* stays allowed as the `"unpinned"` posture — that is the documented single-endpoint quick-start
|
|
23
|
+
* shape (key on config, URL on the model) and refusing it would break every deployment built from
|
|
24
|
+
* it; the posture is surfaced so read faces can annotate the unverifiable pairing instead.
|
|
25
|
+
* - `absent` → OK for keyless deployments (nothing to protect); refused as
|
|
26
|
+
* `route.credential_missing` only where an entry declares its OWN URL away from a DECLARED
|
|
27
|
+
* deployment root — a distinct endpoint with no credential route is a configuration hole, and a
|
|
28
|
+
* loud refusal with a fix hint beats an unauthenticated request to a third-party host.
|
|
29
|
+
*
|
|
30
|
+
* Fail-closed direction: "no credential leak" outranks "silently works". A deployment that only
|
|
31
|
+
* worked because the gateway key HAPPENED to be valid at another entry's host now refuses loudly,
|
|
32
|
+
* and every refusal names the entry, both URL halves, and a one-line fix.
|
|
33
|
+
*
|
|
34
|
+
* Bad-value loudness (garbage / contradiction / absence):
|
|
35
|
+
* - garbage — non-string URL/credential inputs throw a `TypeError` here (a programmer error must
|
|
36
|
+
* not be laundered into a polite verdict); whitespace-only URLs normalize to "" (undeclared).
|
|
37
|
+
* - contradiction — the torn pair refuses with `route.credential_mismatch` + both halves named.
|
|
38
|
+
* - absence — `route.credential_missing` with the fix hint, exactly where absence is a hole.
|
|
39
|
+
*/
|
|
40
|
+
import type { Model } from "../internal/llm.js";
|
|
41
|
+
import type { RouteAdjudication, RouteCredential, RoutePairingConfig, RoutePairingPosture } from "../internal/llm.js";
|
|
42
|
+
import type { Brain, EngineNotice } from "../core/types.js";
|
|
43
|
+
/** Normalize a base URL for pairing comparison, to WIRE equivalence: trim + strip trailing
|
|
44
|
+
* slashes, then — for a string that parses as a URL — fold the halves the wire itself treats as
|
|
45
|
+
* case-insensitive (scheme and host, RFC 9110) and elide the scheme's default port, while keeping
|
|
46
|
+
* the PATH byte-preserved (paths are case-sensitive; folding them would compare two genuinely
|
|
47
|
+
* different endpoints equal — a widening of exactly the leak this module exists to stop). Two
|
|
48
|
+
* spellings that resolve to the same origin+path on the wire therefore compare equal, and a
|
|
49
|
+
* refusal can never be minted over a spelling difference the wire cannot see. A value that does
|
|
50
|
+
* not parse as a URL keeps the raw trimmed spelling — the same fail-closed raw comparison as
|
|
51
|
+
* before. Non-string input throws — see the module note on garbage. */
|
|
52
|
+
export declare function normalizeBaseUrl(url: string | undefined): string;
|
|
53
|
+
/** Whether a header bag carries an auth-bearing header (any case). */
|
|
54
|
+
export declare function hasAuthCarrier(headers: Record<string, string> | undefined): boolean;
|
|
55
|
+
/**
|
|
56
|
+
* Resolve WHICH credential a request would carry, tagged with its source — the input half of the
|
|
57
|
+
* adjudication. Precedence (most-specific wins):
|
|
58
|
+
* 1. `options.apiKey` (per-call, from the per-model auth hook — presence, not truthiness: an
|
|
59
|
+
* empty string is a present credential that fails closed at emit),
|
|
60
|
+
* 2. an auth carrier in `options.headers` (per-call headers-borne). The bag MUST be per-model
|
|
61
|
+
* sourced (the per-model auth hook's headers) — that provenance is what licenses the
|
|
62
|
+
* "paired by construction" verdict below. A caller that folds run-wide headers into the
|
|
63
|
+
* per-call options must strip auth carriers first (the harness merge does), or a deployment
|
|
64
|
+
* credential would ride here and be judged per-model,
|
|
65
|
+
* 3. an auth carrier in `model.headers` (the ENTRY's own declared credential),
|
|
66
|
+
* 4. the deployment config (`config.apiKey` and/or an auth carrier in `config.headers` —
|
|
67
|
+
* deliberately ONE rank, not two: a config carrying BOTH forms is the documented gateway
|
|
68
|
+
* dual-auth escape shape, and both ride exactly as they always have; the pairing gate judges
|
|
69
|
+
* the pair as one deployment credential),
|
|
70
|
+
* 5. absent.
|
|
71
|
+
* 1–3 are `per-model`; 4 is `deployment-config`.
|
|
72
|
+
*/
|
|
73
|
+
export declare function resolveRouteCredential(input: {
|
|
74
|
+
optionsApiKey?: string;
|
|
75
|
+
optionsHeaders?: Record<string, string>;
|
|
76
|
+
modelHeaders?: Record<string, string>;
|
|
77
|
+
configApiKey?: string;
|
|
78
|
+
configHeaders?: Record<string, string>;
|
|
79
|
+
}): RouteCredential;
|
|
80
|
+
/**
|
|
81
|
+
* The pairing law (module note above). Pure and deterministic: same inputs, same verdict — safe to
|
|
82
|
+
* call both at a resolution seat (pre-flight) and again inside a brain's request build.
|
|
83
|
+
*/
|
|
84
|
+
export declare function adjudicateModelRoute(model: Pick<Model, "id" | "baseUrl">, credential: RouteCredential, config: RoutePairingConfig): RouteAdjudication;
|
|
85
|
+
/** Render a refusal as the one-line detail text the brains put in their thrown error (the machine
|
|
86
|
+
* code leads so log/transcript consumers can grep it; the fix hint closes it). */
|
|
87
|
+
export declare function routeRefusalText(verdict: Extract<RouteAdjudication, {
|
|
88
|
+
ok: false;
|
|
89
|
+
}>): string;
|
|
90
|
+
/**
|
|
91
|
+
* Enforce "the resolved credential is the request's ONLY credential" on an already-merged header
|
|
92
|
+
* bag — the per-call-auth-replaces rule, generalized to the per-model sources:
|
|
93
|
+
* - PER-MODEL apiKey-form winner → every auth carrier is stripped; the brain then emits the key in
|
|
94
|
+
* its own wire posture, the single credential on the wire (the pre-existing per-call rule).
|
|
95
|
+
* - PER-MODEL headers-borne winner (per-call or entry bag) → carriers are stripped and the winning
|
|
96
|
+
* bag's own carriers re-asserted (an entry-declared credential must survive a same- or
|
|
97
|
+
* variant-case deployment spelling that the plain merge order would have let win).
|
|
98
|
+
* - DEPLOYMENT-CONFIG winner → untouched. The historical bytes stand exactly: a header-only config
|
|
99
|
+
* keeps its own spelling, and a config carrying BOTH the apiKey knob and an auth header is the
|
|
100
|
+
* documented gateway dual-auth escape shape — the knob's hard-lock write plus the surviving
|
|
101
|
+
* header, both on the wire, as they always were.
|
|
102
|
+
* - absent → untouched (no carriers exist).
|
|
103
|
+
*/
|
|
104
|
+
export declare function applyRouteCredentialHeaders(merged: Record<string, string>, credential: RouteCredential, bags: {
|
|
105
|
+
model?: Record<string, string>;
|
|
106
|
+
options?: Record<string, string>;
|
|
107
|
+
}): void;
|
|
108
|
+
/**
|
|
109
|
+
* The first-party brains' `adjudicateRoute` face, closed over the brain's construction config —
|
|
110
|
+
* EXACTLY the resolution + judgment the brain's own request build runs (single source, two call
|
|
111
|
+
* sites), so a pre-flight verdict and the request-time verdict can never disagree.
|
|
112
|
+
*/
|
|
113
|
+
export declare function createBrainRouteJudge(config: {
|
|
114
|
+
apiKey?: string;
|
|
115
|
+
baseUrl?: string;
|
|
116
|
+
headers?: Record<string, string>;
|
|
117
|
+
}): (model: Model, perModelAuth?: {
|
|
118
|
+
apiKey?: string;
|
|
119
|
+
headers?: Record<string, string>;
|
|
120
|
+
}) => RouteAdjudication;
|
|
121
|
+
/**
|
|
122
|
+
* Per-entry pairing status — the read-face predicate a roster/diagnostic surface annotates entries
|
|
123
|
+
* with, so a torn pair is visible at configuration time instead of on the first real request.
|
|
124
|
+
* `judge` is a brain's optional `adjudicateRoute` face; absent ⇒ `"unknown"` (a custom brain core
|
|
125
|
+
* cannot see into — never guessed at).
|
|
126
|
+
*/
|
|
127
|
+
export type RoutePairingStatus = `ok:${RoutePairingPosture}` | `broken:${"credential_mismatch" | "credential_missing"}` | "unknown";
|
|
128
|
+
export declare function routePairingStatus(brain: {
|
|
129
|
+
adjudicateRoute?: (model: Model, perModelAuth?: {
|
|
130
|
+
apiKey?: string;
|
|
131
|
+
headers?: Record<string, string>;
|
|
132
|
+
}) => RouteAdjudication | undefined;
|
|
133
|
+
}, model: Model, perModelAuth?: {
|
|
134
|
+
apiKey?: string;
|
|
135
|
+
headers?: Record<string, string>;
|
|
136
|
+
}): RoutePairingStatus;
|
|
137
|
+
/**
|
|
138
|
+
* Pre-flight a DERIVED-leg model (one the engine resolved from a role/tier/system default rather
|
|
139
|
+
* than an explicit caller `model`) against the deployment brain's pairing judge. The two-leg
|
|
140
|
+
* disposition for a broken pairing:
|
|
141
|
+
* - a NAMED model (explicit in the spec) refuses loudly at the brain's request gate — the caller
|
|
142
|
+
* asked for that exact route and gets the refusal + fix hint;
|
|
143
|
+
* - a DERIVED model must not sink the main-model task it serves — the seat falls back to the
|
|
144
|
+
* primary model and announces `route.fallback_to_primary` (the notice IS the loudness; a silent
|
|
145
|
+
* swap would hide the misconfiguration this whole surface exists to expose).
|
|
146
|
+
* Returns the verdict, or `undefined` when no judgment is possible (the brain has no judge face,
|
|
147
|
+
* the judge itself threw, or the auth hook threw or outran the advisory bound — in which case the
|
|
148
|
+
* seat proceeds unchanged and the failure/latency surfaces at its own request-time call site
|
|
149
|
+
* exactly as before).
|
|
150
|
+
*
|
|
151
|
+
* The hook call is BOUNDED. The pre-flight is advisory (the request gate re-runs the same law), so
|
|
152
|
+
* it must never widen a slow secret manager's blast radius from "the derived call is slow" to "task
|
|
153
|
+
* preparation is wedged": past the bound the pre-flight simply abstains. The eventual request still
|
|
154
|
+
* resolves the hook itself (the pre-flight result is never reused as a credential — same-inputs
|
|
155
|
+
* determinism of the law is what keeps the two calls' verdicts aligned, not a cached secret).
|
|
156
|
+
*/
|
|
157
|
+
export declare const DERIVED_ROUTE_PREFLIGHT_HOOK_BOUND_MS = 5000;
|
|
158
|
+
export declare function adjudicateDerivedRoute(args: {
|
|
159
|
+
brain: Brain;
|
|
160
|
+
model: Model;
|
|
161
|
+
getApiKeyAndHeaders?: ((model: Model) => Promise<{
|
|
162
|
+
apiKey?: string;
|
|
163
|
+
headers?: Record<string, string>;
|
|
164
|
+
} | undefined> | {
|
|
165
|
+
apiKey?: string;
|
|
166
|
+
headers?: Record<string, string>;
|
|
167
|
+
} | undefined) | undefined;
|
|
168
|
+
}): Promise<RouteAdjudication | undefined>;
|
|
169
|
+
/**
|
|
170
|
+
* Whether two resolved models denote the SAME route for pre-flight purposes: identical wire id AND
|
|
171
|
+
* identical normalized entry URL. `Model.id` alone is NOT route identity — two catalog entries can
|
|
172
|
+
* legitimately share a wire id (the same model served from two endpoints), and a same-id guard
|
|
173
|
+
* would silently skip the pre-flight for exactly the split the pairing law exists to judge.
|
|
174
|
+
*/
|
|
175
|
+
export declare function sameRouteIdentity(a: Pick<Model, "id" | "baseUrl">, b: Pick<Model, "id" | "baseUrl">): boolean;
|
|
176
|
+
/** The `route.fallback_to_primary` announcement — one shared shape for every derived-leg seat, so
|
|
177
|
+
* a consumer can rely on `detail.{seat, from, to, cause}` regardless of which seat fell back. */
|
|
178
|
+
export declare function fallbackToPrimaryNotice(args: {
|
|
179
|
+
seat: string;
|
|
180
|
+
from: string;
|
|
181
|
+
to: string;
|
|
182
|
+
verdict: Extract<RouteAdjudication, {
|
|
183
|
+
ok: false;
|
|
184
|
+
}>;
|
|
185
|
+
}): EngineNotice;
|
|
186
|
+
/** The auth carriers of a header bag, canonicalized (case-fold name → value, sorted) — the
|
|
187
|
+
* Model-visible credential fingerprint used by catalog-change disclosure (a same-name entry whose
|
|
188
|
+
* URL moved while this fingerprint did not is the "address changed, credential reference did not"
|
|
189
|
+
* shape worth announcing). */
|
|
190
|
+
export declare function authCarrierFingerprint(headers: Record<string, string> | undefined): string;
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { AUTH_CARRIER_NAMES } from "./request-params.js";
|
|
2
|
+
export function normalizeBaseUrl(url) {
|
|
3
|
+
if (url === undefined)
|
|
4
|
+
return "";
|
|
5
|
+
if (typeof url !== "string") {
|
|
6
|
+
throw new TypeError(`route pairing: baseUrl must be a string (got ${typeof url})`);
|
|
7
|
+
}
|
|
8
|
+
const raw = url.trim().replace(/\/+$/, "");
|
|
9
|
+
if (raw === "")
|
|
10
|
+
return "";
|
|
11
|
+
try {
|
|
12
|
+
const u = new URL(raw);
|
|
13
|
+
return `${u.protocol}//${u.host}${u.pathname}${u.search}`.replace(/\/+$/, "");
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return raw;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export function hasAuthCarrier(headers) {
|
|
20
|
+
if (!headers)
|
|
21
|
+
return false;
|
|
22
|
+
for (const k of Object.keys(headers)) {
|
|
23
|
+
if (AUTH_CARRIER_NAMES.has(k.toLowerCase()))
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
export function resolveRouteCredential(input) {
|
|
29
|
+
const { optionsApiKey, optionsHeaders, modelHeaders, configApiKey, configHeaders } = input;
|
|
30
|
+
if (optionsApiKey !== undefined) {
|
|
31
|
+
if (typeof optionsApiKey !== "string")
|
|
32
|
+
throw new TypeError(`route pairing: apiKey must be a string (got ${typeof optionsApiKey})`);
|
|
33
|
+
return { source: "per-model", apiKey: optionsApiKey };
|
|
34
|
+
}
|
|
35
|
+
if (hasAuthCarrier(optionsHeaders))
|
|
36
|
+
return { source: "per-model", headersBorne: true, carrierBag: "options" };
|
|
37
|
+
if (hasAuthCarrier(modelHeaders))
|
|
38
|
+
return { source: "per-model", headersBorne: true, carrierBag: "model" };
|
|
39
|
+
const configHeadersBorne = hasAuthCarrier(configHeaders);
|
|
40
|
+
if (configApiKey !== undefined || configHeadersBorne) {
|
|
41
|
+
if (configApiKey !== undefined && typeof configApiKey !== "string") {
|
|
42
|
+
throw new TypeError(`route pairing: apiKey must be a string (got ${typeof configApiKey})`);
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
source: "deployment-config",
|
|
46
|
+
...(configApiKey !== undefined ? { apiKey: configApiKey } : {}),
|
|
47
|
+
...(configHeadersBorne ? { headersBorne: true, carrierBag: "config" } : {}),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
return { source: "absent" };
|
|
51
|
+
}
|
|
52
|
+
export function adjudicateModelRoute(model, credential, config) {
|
|
53
|
+
const entry = normalizeBaseUrl(model.baseUrl);
|
|
54
|
+
const declared = normalizeBaseUrl(config.baseUrl);
|
|
55
|
+
const request = entry || declared;
|
|
56
|
+
if (credential.source === "per-model")
|
|
57
|
+
return { ok: true, posture: "per-model" };
|
|
58
|
+
if (credential.source === "deployment-config") {
|
|
59
|
+
if (declared === "")
|
|
60
|
+
return { ok: true, posture: "unpinned" };
|
|
61
|
+
if (request === declared)
|
|
62
|
+
return { ok: true, posture: "paired" };
|
|
63
|
+
return {
|
|
64
|
+
ok: false,
|
|
65
|
+
code: "route.credential_mismatch",
|
|
66
|
+
detail: {
|
|
67
|
+
modelId: model.id,
|
|
68
|
+
entryBaseUrl: entry,
|
|
69
|
+
requestBaseUrl: request,
|
|
70
|
+
configBaseUrl: declared,
|
|
71
|
+
fixHint: `model "${model.id}" targets ${request} but the deployment credential is paired with ${declared}: ` +
|
|
72
|
+
`give the model its own credential (a per-model key via getApiKeyAndHeaders, or an auth header on Model.headers), ` +
|
|
73
|
+
`or serve it through a brain whose config.baseUrl is ${request}`,
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
if (declared === "" || entry === "" || request === declared)
|
|
78
|
+
return { ok: true, posture: "keyless" };
|
|
79
|
+
return {
|
|
80
|
+
ok: false,
|
|
81
|
+
code: "route.credential_missing",
|
|
82
|
+
detail: {
|
|
83
|
+
modelId: model.id,
|
|
84
|
+
entryBaseUrl: entry,
|
|
85
|
+
requestBaseUrl: request,
|
|
86
|
+
configBaseUrl: declared,
|
|
87
|
+
fixHint: `model "${model.id}" targets ${request} but no credential is configured for that endpoint ` +
|
|
88
|
+
`(the deployment credential root is ${declared}): supply a per-model credential ` +
|
|
89
|
+
`(getApiKeyAndHeaders, or an auth header on Model.headers), or serve it through a brain constructed for ${request}`,
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
export function routeRefusalText(verdict) {
|
|
94
|
+
return `${verdict.code}: ${verdict.detail.fixHint}`;
|
|
95
|
+
}
|
|
96
|
+
export function applyRouteCredentialHeaders(merged, credential, bags) {
|
|
97
|
+
if (credential.source !== "per-model")
|
|
98
|
+
return;
|
|
99
|
+
for (const k of Object.keys(merged)) {
|
|
100
|
+
if (AUTH_CARRIER_NAMES.has(k.toLowerCase()))
|
|
101
|
+
delete merged[k];
|
|
102
|
+
}
|
|
103
|
+
if (credential.headersBorne !== true)
|
|
104
|
+
return;
|
|
105
|
+
const winner = credential.carrierBag === "options" ? bags.options : bags.model;
|
|
106
|
+
if (!winner)
|
|
107
|
+
return;
|
|
108
|
+
for (const [k, v] of Object.entries(winner)) {
|
|
109
|
+
if (AUTH_CARRIER_NAMES.has(k.toLowerCase()))
|
|
110
|
+
merged[k] = v;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
export function createBrainRouteJudge(config) {
|
|
114
|
+
return (model, perModelAuth) => {
|
|
115
|
+
const credential = resolveRouteCredential({
|
|
116
|
+
optionsApiKey: perModelAuth?.apiKey,
|
|
117
|
+
optionsHeaders: perModelAuth?.headers,
|
|
118
|
+
modelHeaders: model.headers,
|
|
119
|
+
configApiKey: config.apiKey,
|
|
120
|
+
configHeaders: config.headers,
|
|
121
|
+
});
|
|
122
|
+
return adjudicateModelRoute(model, credential, { baseUrl: config.baseUrl });
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
export function routePairingStatus(brain, model, perModelAuth) {
|
|
126
|
+
const verdict = brain.adjudicateRoute?.(model, perModelAuth);
|
|
127
|
+
if (verdict === undefined)
|
|
128
|
+
return "unknown";
|
|
129
|
+
if (verdict.ok)
|
|
130
|
+
return `ok:${verdict.posture}`;
|
|
131
|
+
return verdict.code === "route.credential_mismatch" ? "broken:credential_mismatch" : "broken:credential_missing";
|
|
132
|
+
}
|
|
133
|
+
export const DERIVED_ROUTE_PREFLIGHT_HOOK_BOUND_MS = 5_000;
|
|
134
|
+
export async function adjudicateDerivedRoute(args) {
|
|
135
|
+
const judge = args.brain.adjudicateRoute;
|
|
136
|
+
if (judge === undefined)
|
|
137
|
+
return undefined;
|
|
138
|
+
let auth;
|
|
139
|
+
if (args.getApiKeyAndHeaders !== undefined) {
|
|
140
|
+
let timer;
|
|
141
|
+
const ABSTAIN = Symbol("preflight-abstain");
|
|
142
|
+
try {
|
|
143
|
+
const bound = new Promise((resolve) => {
|
|
144
|
+
timer = setTimeout(() => resolve(ABSTAIN), DERIVED_ROUTE_PREFLIGHT_HOOK_BOUND_MS);
|
|
145
|
+
});
|
|
146
|
+
const hookResult = Promise.resolve(args.getApiKeyAndHeaders(args.model));
|
|
147
|
+
hookResult.catch(() => { });
|
|
148
|
+
const raced = await Promise.race([hookResult, bound]);
|
|
149
|
+
if (raced === ABSTAIN)
|
|
150
|
+
return undefined;
|
|
151
|
+
auth = raced;
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
return undefined;
|
|
155
|
+
}
|
|
156
|
+
finally {
|
|
157
|
+
if (timer !== undefined)
|
|
158
|
+
clearTimeout(timer);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
try {
|
|
162
|
+
return judge(args.model, auth);
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
return undefined;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
export function sameRouteIdentity(a, b) {
|
|
169
|
+
return a.id === b.id && normalizeBaseUrl(a.baseUrl) === normalizeBaseUrl(b.baseUrl);
|
|
170
|
+
}
|
|
171
|
+
export function fallbackToPrimaryNotice(args) {
|
|
172
|
+
return {
|
|
173
|
+
code: "route.fallback_to_primary",
|
|
174
|
+
message: `derived model "${args.from}" (${args.seat}) is not routable (${args.verdict.code}) — ` +
|
|
175
|
+
`falling back to the primary model "${args.to}". ${args.verdict.detail.fixHint}`,
|
|
176
|
+
detail: { seat: args.seat, from: args.from, to: args.to, cause: args.verdict.code, fixHint: args.verdict.detail.fixHint },
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
export function authCarrierFingerprint(headers) {
|
|
180
|
+
if (!headers)
|
|
181
|
+
return "";
|
|
182
|
+
const rows = [];
|
|
183
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
184
|
+
const fold = k.toLowerCase();
|
|
185
|
+
if (AUTH_CARRIER_NAMES.has(fold))
|
|
186
|
+
rows.push(`${fold}=${v}`);
|
|
187
|
+
}
|
|
188
|
+
return rows.sort().join("\n");
|
|
189
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { RouteAdjudication, RouteCredential, RoutePairingConfig } from "../internal/llm.js";
|
|
2
|
+
/**
|
|
3
|
+
* The PUBLISHED golden conformance corpus for the key↔URL pairing adjudicator
|
|
4
|
+
* ({@link import("./route-adjudicator.js").adjudicateModelRoute}).
|
|
5
|
+
*
|
|
6
|
+
* THE SEAM. The pairing law has exactly ONE implementation — core's `adjudicateModelRoute` — and
|
|
7
|
+
* every consumer that surfaces routing verdicts (a server annotating its model roster, a brain
|
|
8
|
+
* degradation leg replacing a hand-rolled check) MUST delegate to it rather than re-derive
|
|
9
|
+
* "does this key belong to this URL". This corpus is the anti-drift anchor between codebases
|
|
10
|
+
* (the gold-corpus discipline):
|
|
11
|
+
* - CORE runs it as a regression test (`test/route-adjudication.test.ts`): every vector's `expect`
|
|
12
|
+
* MUST equal `adjudicateModelRoute(model, credential, config)` on the `ok`/`posture`/`code`
|
|
13
|
+
* axes. A change to the law fails the test and updates the corpus in lockstep with the engine
|
|
14
|
+
* version, so the published contract can never silently drift from the implementation.
|
|
15
|
+
* - A CONSUMER imports this corpus from `@sema-agent/core` and runs it as a CONTRACT test against
|
|
16
|
+
* whatever face it wired the adjudicator into: for each vector, its surface's
|
|
17
|
+
* allow/annotate/refuse of the described route MUST match `expect.ok` (and forward
|
|
18
|
+
* `expect.code` verbatim on refusals). That proves the surface actually routes through core's
|
|
19
|
+
* predicate.
|
|
20
|
+
*
|
|
21
|
+
* ⚠️ CONSUMER CONTRACT: depend on `expect.ok` + `expect.code` (+ `expect.posture` if the surface
|
|
22
|
+
* annotates postures). `fixHint` wording is core-internal and may be reworded without notice.
|
|
23
|
+
*
|
|
24
|
+
* `leg` is ADVISORY: how the refusal is DISPOSED of in the engine's two-leg discipline — a
|
|
25
|
+
* `"named"` (caller-explicit) model surfaces the refusal loudly at the request gate; a `"derived"`
|
|
26
|
+
* (role/tier/system-default) model falls back to the primary model with a
|
|
27
|
+
* `route.fallback_to_primary` notice. The verdict itself is leg-independent (same law both legs).
|
|
28
|
+
*/
|
|
29
|
+
export interface RouteAdjudicationVector {
|
|
30
|
+
/** Stable case id (appears in test output + the published contract). */
|
|
31
|
+
name: string;
|
|
32
|
+
/** What this vector proves. */
|
|
33
|
+
intent: string;
|
|
34
|
+
model: {
|
|
35
|
+
id: string;
|
|
36
|
+
baseUrl: string;
|
|
37
|
+
};
|
|
38
|
+
credential: RouteCredential;
|
|
39
|
+
config: RoutePairingConfig;
|
|
40
|
+
/** The `ok` / `posture` / `code` axes of the expected {@link RouteAdjudication}. */
|
|
41
|
+
expect: {
|
|
42
|
+
ok: true;
|
|
43
|
+
posture: Extract<RouteAdjudication, {
|
|
44
|
+
ok: true;
|
|
45
|
+
}>["posture"];
|
|
46
|
+
} | {
|
|
47
|
+
ok: false;
|
|
48
|
+
code: Extract<RouteAdjudication, {
|
|
49
|
+
ok: false;
|
|
50
|
+
}>["code"];
|
|
51
|
+
};
|
|
52
|
+
/** Advisory: how the engine's two-leg discipline disposes of this shape (see module note). */
|
|
53
|
+
leg?: "named" | "derived";
|
|
54
|
+
}
|
|
55
|
+
export declare const ROUTE_ADJUDICATION_CONFORMANCE_CORPUS: readonly RouteAdjudicationVector[];
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
export const ROUTE_ADJUDICATION_CONFORMANCE_CORPUS = [
|
|
2
|
+
{
|
|
3
|
+
name: "per-model-on-root",
|
|
4
|
+
intent: "a per-model credential is paired by construction, on the deployment root",
|
|
5
|
+
model: { id: "m-main", baseUrl: "https://gw.example.com" },
|
|
6
|
+
credential: { source: "per-model", apiKey: "k-main" },
|
|
7
|
+
config: { baseUrl: "https://gw.example.com" },
|
|
8
|
+
expect: { ok: true, posture: "per-model" },
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
name: "per-model-off-root",
|
|
12
|
+
intent: "a per-model credential is paired by construction even off the deployment root (the resolver was asked about THIS entry)",
|
|
13
|
+
model: { id: "m-third", baseUrl: "https://third-party.example.net/v1" },
|
|
14
|
+
credential: { source: "per-model", apiKey: "k-third" },
|
|
15
|
+
config: { baseUrl: "https://gw.example.com" },
|
|
16
|
+
expect: { ok: true, posture: "per-model" },
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
name: "per-model-headers-borne",
|
|
20
|
+
intent: "an entry-declared auth header is a per-model credential (headers-borne)",
|
|
21
|
+
model: { id: "m-hdr", baseUrl: "https://third-party.example.net" },
|
|
22
|
+
credential: { source: "per-model", headersBorne: true, carrierBag: "model" },
|
|
23
|
+
config: { baseUrl: "https://gw.example.com" },
|
|
24
|
+
expect: { ok: true, posture: "per-model" },
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
name: "per-model-empty-string-key",
|
|
28
|
+
intent: "an empty-string per-model key is a PRESENT credential (fails closed at emit, never falls to the deployment key)",
|
|
29
|
+
model: { id: "m-degraded", baseUrl: "https://third-party.example.net" },
|
|
30
|
+
credential: { source: "per-model", apiKey: "" },
|
|
31
|
+
config: { baseUrl: "https://gw.example.com" },
|
|
32
|
+
expect: { ok: true, posture: "per-model" },
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: "deployment-paired",
|
|
36
|
+
intent: "the main-gateway pass: entry URL === declared config root, deployment credential allowed",
|
|
37
|
+
model: { id: "m-main", baseUrl: "https://gw.example.com" },
|
|
38
|
+
credential: { source: "deployment-config", apiKey: "k-gw" },
|
|
39
|
+
config: { baseUrl: "https://gw.example.com" },
|
|
40
|
+
expect: { ok: true, posture: "paired" },
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: "deployment-paired-empty-entry",
|
|
44
|
+
intent: "an entry with an empty baseUrl targets the config root — deployment credential paired",
|
|
45
|
+
model: { id: "m-implicit", baseUrl: "" },
|
|
46
|
+
credential: { source: "deployment-config", apiKey: "k-gw" },
|
|
47
|
+
config: { baseUrl: "https://gw.example.com" },
|
|
48
|
+
expect: { ok: true, posture: "paired" },
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: "deployment-paired-slash-normalized",
|
|
52
|
+
intent: "trailing slashes do not break the pairing (both halves normalize before compare)",
|
|
53
|
+
model: { id: "m-main", baseUrl: "https://gw.example.com/" },
|
|
54
|
+
credential: { source: "deployment-config", apiKey: "k-gw" },
|
|
55
|
+
config: { baseUrl: "https://gw.example.com" },
|
|
56
|
+
expect: { ok: true, posture: "paired" },
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
name: "deployment-mismatch",
|
|
60
|
+
intent: "THE torn pair: deployment credential + entry URL off the declared root = refusal, not a silently-followed key",
|
|
61
|
+
model: { id: "m-third", baseUrl: "https://third-party.example.net/v1" },
|
|
62
|
+
credential: { source: "deployment-config", apiKey: "k-gw" },
|
|
63
|
+
config: { baseUrl: "https://gw.example.com" },
|
|
64
|
+
expect: { ok: false, code: "route.credential_mismatch" },
|
|
65
|
+
leg: "named",
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
name: "deployment-mismatch-path-differs",
|
|
69
|
+
intent: "a differing path is a differing root (no prefix leniency — the credential goes to the exact declared root only)",
|
|
70
|
+
model: { id: "m-path", baseUrl: "https://gw.example.com/other" },
|
|
71
|
+
credential: { source: "deployment-config", apiKey: "k-gw" },
|
|
72
|
+
config: { baseUrl: "https://gw.example.com/v1" },
|
|
73
|
+
expect: { ok: false, code: "route.credential_mismatch" },
|
|
74
|
+
leg: "named",
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
name: "deployment-mismatch-derived-leg",
|
|
78
|
+
intent: "the same torn pair on a DERIVED model: same verdict; the engine disposes of it as fallback-to-primary + notice",
|
|
79
|
+
model: { id: "m-summarize", baseUrl: "https://third-party.example.net" },
|
|
80
|
+
credential: { source: "deployment-config", apiKey: "k-gw" },
|
|
81
|
+
config: { baseUrl: "https://gw.example.com" },
|
|
82
|
+
expect: { ok: false, code: "route.credential_mismatch" },
|
|
83
|
+
leg: "derived",
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
name: "deployment-headers-borne-mismatch",
|
|
87
|
+
intent: "a deployment auth HEADER is the same fallback arm: off the declared root it is refused, not sent",
|
|
88
|
+
model: { id: "m-third", baseUrl: "https://third-party.example.net" },
|
|
89
|
+
credential: { source: "deployment-config", headersBorne: true, carrierBag: "config" },
|
|
90
|
+
config: { baseUrl: "https://gw.example.com" },
|
|
91
|
+
expect: { ok: false, code: "route.credential_mismatch" },
|
|
92
|
+
leg: "named",
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
name: "deployment-unpinned",
|
|
96
|
+
intent: "the quick-start posture: config key with NO declared config root stays allowed (single-endpoint form: key on config, URL on the model) — surfaced as unpinned, not refused",
|
|
97
|
+
model: { id: "m-quickstart", baseUrl: "https://api.example.com/v1" },
|
|
98
|
+
credential: { source: "deployment-config", apiKey: "k-quickstart" },
|
|
99
|
+
config: {},
|
|
100
|
+
expect: { ok: true, posture: "unpinned" },
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
name: "absent-keyless-on-root",
|
|
104
|
+
intent: "a keyless deployment on its own root has nothing to protect — allowed",
|
|
105
|
+
model: { id: "m-local", baseUrl: "http://127.0.0.1:8000/v1" },
|
|
106
|
+
credential: { source: "absent" },
|
|
107
|
+
config: { baseUrl: "http://127.0.0.1:8000/v1" },
|
|
108
|
+
expect: { ok: true, posture: "keyless" },
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
name: "absent-keyless-undeclared",
|
|
112
|
+
intent: "keyless with no declared config root (URL only on the model) — allowed",
|
|
113
|
+
model: { id: "m-local", baseUrl: "http://127.0.0.1:8000/v1" },
|
|
114
|
+
credential: { source: "absent" },
|
|
115
|
+
config: {},
|
|
116
|
+
expect: { ok: true, posture: "keyless" },
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
name: "absent-missing-off-root",
|
|
120
|
+
intent: "an entry declaring its OWN URL away from a DECLARED deployment root with no credential anywhere is a configuration hole — loud refusal beats an unauthenticated request to a third-party host",
|
|
121
|
+
model: { id: "m-third", baseUrl: "https://third-party.example.net" },
|
|
122
|
+
credential: { source: "absent" },
|
|
123
|
+
config: { baseUrl: "https://gw.example.com" },
|
|
124
|
+
expect: { ok: false, code: "route.credential_missing" },
|
|
125
|
+
leg: "named",
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
name: "absent-missing-derived-leg",
|
|
129
|
+
intent: "the same hole on a DERIVED model: same verdict; engine disposition is fallback-to-primary + notice",
|
|
130
|
+
model: { id: "m-summarize", baseUrl: "https://third-party.example.net" },
|
|
131
|
+
credential: { source: "absent" },
|
|
132
|
+
config: { baseUrl: "https://gw.example.com" },
|
|
133
|
+
expect: { ok: false, code: "route.credential_missing" },
|
|
134
|
+
leg: "derived",
|
|
135
|
+
},
|
|
136
|
+
];
|
package/dist/brain/routing.js
CHANGED
|
@@ -15,13 +15,17 @@ function errorMessage(model, text) {
|
|
|
15
15
|
}
|
|
16
16
|
export function createRoutingBrain(routes, opts = {}) {
|
|
17
17
|
const by = opts.by ?? "provider";
|
|
18
|
-
const
|
|
18
|
+
const resolveTarget = (model) => {
|
|
19
19
|
const key = by === "api" ? model.api : model.provider;
|
|
20
20
|
const routed = Object.hasOwn(routes, key) ? routes[key] : undefined;
|
|
21
21
|
const matched = routed !== null && typeof routed === "object" && typeof routed.stream === "function"
|
|
22
22
|
? routed
|
|
23
23
|
: undefined;
|
|
24
|
-
|
|
24
|
+
return matched ?? opts.fallback;
|
|
25
|
+
};
|
|
26
|
+
const stream = (model, context, options) => {
|
|
27
|
+
const key = by === "api" ? model.api : model.provider;
|
|
28
|
+
const target = resolveTarget(model);
|
|
25
29
|
if (!target) {
|
|
26
30
|
const out = createAssistantMessageEventStream();
|
|
27
31
|
out.push({
|
|
@@ -33,5 +37,6 @@ export function createRoutingBrain(routes, opts = {}) {
|
|
|
33
37
|
}
|
|
34
38
|
return target.stream(model, context, options);
|
|
35
39
|
};
|
|
36
|
-
|
|
40
|
+
const adjudicateRoute = (model, perModelAuth) => resolveTarget(model)?.adjudicateRoute?.(model, perModelAuth);
|
|
41
|
+
return { stream, adjudicateRoute };
|
|
37
42
|
}
|
|
@@ -18,7 +18,7 @@ export declare function runWithStatusSink<T>(emit: (s: BrainStatus) => void, fn:
|
|
|
18
18
|
*/
|
|
19
19
|
export declare function emitBrainStatus(status: BrainStatus): void;
|
|
20
20
|
/**
|
|
21
|
-
* Silent-fallback telemetry (
|
|
21
|
+
* Silent-fallback telemetry (C1/C4/C5/C6) — the brain→runner TELEMETRY channel, a
|
|
22
22
|
* SECOND ALS sink parallel to the status one. Statuses are user-facing liveness frames (wire
|
|
23
23
|
* `status` events); telemetry is operator-facing fallback accounting the Runner bridges to trace
|
|
24
24
|
* events (`brain.failover` / `breaker.transition` / `brain.retry` / `vision.placeholder`). Kept
|
|
@@ -52,7 +52,7 @@ export type BrainTelemetry = {
|
|
|
52
52
|
}
|
|
53
53
|
/** PDF read: a `document` content block was substituted with a text placeholder because the serving
|
|
54
54
|
* API/model cannot take native document input (openai brain always; anthropic brain when the model
|
|
55
|
-
* declares no vision). Silent quality loss → frame (same
|
|
55
|
+
* declares no vision). Silent quality loss → frame (same C4 discipline as vision_placeholder). */
|
|
56
56
|
| {
|
|
57
57
|
kind: "document_placeholder";
|
|
58
58
|
count: number;
|
|
@@ -35,7 +35,7 @@ export interface RepetitionPoll {
|
|
|
35
35
|
readonly spared: RepetitionEvent[];
|
|
36
36
|
}
|
|
37
37
|
/**
|
|
38
|
-
* Repetition telemetry (
|
|
38
|
+
* Repetition telemetry (2026-07-10): the cut event + allowance-spared hits for THIS stream.
|
|
39
39
|
* Polls re-inspect the same growing text every ~64 chars — dedupe spared by rule+segment so one
|
|
40
40
|
* boot block doesn't log once per poll. Attached to the final message as `repetition`.
|
|
41
41
|
*/
|
package/dist/config/catalog.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Config catalog (
|
|
2
|
+
* Config catalog (ruled 2026-07-19) — the SINGLE machine-readable source for the engineering
|
|
3
3
|
* configuration scale across every client (server / cli / TB harness / future shells).
|
|
4
4
|
*
|
|
5
5
|
* Why this exists: the same knob was found carrying FIVE different values across layers (maxTurns:
|
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
* nothing) or overrides it and DECLARES the override (`TaskSpec.configOverrides`, with a
|
|
13
13
|
* reason). Declared overrides ride the `config.assembled` trace, so every run self-reports its
|
|
14
14
|
* true knobs and their provenance — "who set 80" becomes a trace read, not archaeology.
|
|
15
|
-
* 3. **Announced change discipline
|
|
16
|
-
* changing one (or overriding persistently) posts the change to the shared
|
|
15
|
+
* 3. **Announced change discipline**: the recommendations are a living baseline — any repo
|
|
16
|
+
* changing one (or overriding persistently) posts the change to the shared config record so the
|
|
17
17
|
* scale evolves deliberately, together.
|
|
18
18
|
*
|
|
19
19
|
* v1 scope: the cross-client TASK-LAUNCH knobs (the chaos surface). Internal engine tuning
|
|
@@ -62,12 +62,12 @@ export interface EffectiveConfigField {
|
|
|
62
62
|
value: number | boolean | string | null;
|
|
63
63
|
provenance: ConfigProvenance;
|
|
64
64
|
}
|
|
65
|
-
/** Declaration bounds
|
|
65
|
+
/** Declaration bounds: host strings ride the trace stream — bounded and sanitized, never
|
|
66
66
|
* free-form. Beyond `MAX_OVERRIDE_DECLARATIONS` entries are dropped (count-capped, first-N kept). */
|
|
67
67
|
export declare const MAX_OVERRIDE_DECLARATIONS = 32;
|
|
68
68
|
/**
|
|
69
69
|
* Build the effective task-launch config + provenance from a spec's launch fields. Pure. The
|
|
70
|
-
* manifest must mirror ENGINE truth, not the spec's surface
|
|
70
|
+
* manifest must mirror ENGINE truth, not the spec's surface: `maxOutputTokens` falls back
|
|
71
71
|
* to the model's own cap (provenance `derived`) before "provider default".
|
|
72
72
|
*/
|
|
73
73
|
export declare function resolveEffectiveConfig(spec: {
|