theorum 1.0.0 → 1.1.3
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/README.md +81 -25
- package/esm/mod.d.ts +48 -29
- package/esm/mod.js +37 -24
- package/esm/src/cli/commands/bench.js +13 -16
- package/esm/src/cli/commands/fuzz-canary.js +25 -29
- package/esm/src/cli/commands/guardrails-eval.d.ts +14 -0
- package/esm/src/cli/commands/guardrails-eval.js +15 -0
- package/esm/src/cli/commands/profile.js +12 -3
- package/esm/src/cli/commands/run.js +8 -9
- package/esm/src/cli/commands/test.js +8 -8
- package/esm/src/cli/event-log.d.ts +1 -1
- package/esm/src/cli/index.js +12 -0
- package/esm/src/cli/matrix/synthesizer.d.ts +6 -6
- package/esm/src/cli/matrix/synthesizer.js +23 -22
- package/esm/src/guardrails/canary-gate.d.ts +2 -1
- package/esm/src/guardrails/canary-gate.js +2 -1
- package/esm/src/guardrails/canary.d.ts +9 -1
- package/esm/src/guardrails/canary.js +21 -10
- package/esm/src/guardrails/corpus/canary-egress-attacks.d.ts +1 -0
- package/esm/src/guardrails/corpus/fuzz-inbound.js +12 -15
- package/esm/src/guardrails/corpus/inbound-payloads.js +1 -0
- package/esm/src/guardrails/corpus/live-attacks.js +0 -1
- package/esm/src/guardrails/corpus/mod.d.ts +1 -0
- package/esm/src/guardrails/corpus/secrets.d.ts +1 -0
- package/esm/src/guardrails/corpus/secrets.js +1 -0
- package/esm/src/guardrails/corpus/strings.d.ts +1 -0
- package/esm/src/guardrails/corpus/strings.js +1 -0
- package/esm/src/guardrails/corpus/types.d.ts +1 -0
- package/esm/src/guardrails/egress.d.ts +26 -3
- package/esm/src/guardrails/egress.js +69 -18
- package/esm/src/guardrails/error.d.ts +14 -23
- package/esm/src/guardrails/error.js +86 -77
- package/esm/src/guardrails/eval/corpus.d.ts +108 -0
- package/esm/src/guardrails/eval/corpus.js +978 -0
- package/esm/src/guardrails/eval/mod.d.ts +51 -0
- package/esm/src/guardrails/eval/mod.js +133 -0
- package/esm/src/guardrails/eval/score.d.ts +66 -0
- package/esm/src/guardrails/eval/score.js +114 -0
- package/esm/src/guardrails/events.d.ts +25 -0
- package/esm/src/guardrails/events.js +56 -0
- package/esm/src/guardrails/hits.d.ts +24 -0
- package/esm/src/guardrails/hits.js +45 -0
- package/esm/src/guardrails/lexicon.d.ts +39 -0
- package/esm/src/guardrails/lexicon.js +200 -0
- package/esm/src/guardrails/live-outbound-gate.d.ts +15 -13
- package/esm/src/guardrails/live-outbound-gate.js +131 -89
- package/esm/src/guardrails/mod.d.ts +19 -4
- package/esm/src/guardrails/mod.js +12 -3
- package/esm/src/guardrails/network.d.ts +19 -0
- package/esm/src/guardrails/network.js +234 -0
- package/esm/src/guardrails/policy.d.ts +35 -0
- package/esm/src/guardrails/policy.js +50 -0
- package/esm/src/guardrails/progressive-yield.d.ts +51 -0
- package/esm/src/guardrails/progressive-yield.js +98 -0
- package/esm/src/guardrails/quota.d.ts +17 -3
- package/esm/src/guardrails/quota.js +18 -4
- package/esm/src/guardrails/sanitize.d.ts +36 -6
- package/esm/src/guardrails/sanitize.js +152 -60
- package/esm/src/guardrails/serialize.d.ts +35 -0
- package/esm/src/guardrails/serialize.js +58 -0
- package/esm/src/guardrails/testing.d.ts +7 -0
- package/esm/src/guardrails/testing.js +4 -0
- package/esm/src/guardrails/theorum-error.d.ts +12 -0
- package/esm/src/guardrails/theorum-error.js +15 -0
- package/esm/src/guardrails/tool-directives.d.ts +48 -0
- package/esm/src/guardrails/tool-directives.js +124 -0
- package/esm/src/guardrails/tool-result.d.ts +93 -0
- package/esm/src/guardrails/tool-result.js +276 -0
- package/esm/src/guardrails/types.d.ts +291 -0
- package/esm/src/guardrails/types.js +72 -0
- package/esm/src/host/client-turn.js +4 -0
- package/esm/src/host/mint-trace.d.ts +1 -1
- package/esm/src/kernel/auth/crypto.d.ts +42 -0
- package/esm/src/kernel/auth/crypto.js +106 -0
- package/esm/src/kernel/auth/mod.d.ts +11 -0
- package/esm/src/kernel/auth/mod.js +11 -0
- package/esm/src/kernel/auth/oauth.d.ts +47 -0
- package/esm/src/kernel/auth/oauth.js +278 -0
- package/esm/src/kernel/auth/types.d.ts +133 -0
- package/esm/src/kernel/auth/types.js +13 -0
- package/esm/src/kernel/engine/delta.js +43 -2
- package/esm/src/kernel/engine/live-inbound.d.ts +15 -3
- package/esm/src/kernel/engine/live-inbound.js +22 -8
- package/esm/src/kernel/engine/live-ingress.d.ts +19 -0
- package/esm/src/kernel/engine/live-ingress.js +47 -0
- package/esm/src/kernel/engine/repair.js +13 -12
- package/esm/src/kernel/engine/runner/gates.js +127 -40
- package/esm/src/kernel/engine/runner/mod.d.ts +6 -4
- package/esm/src/kernel/engine/runner/mod.js +182 -43
- package/esm/src/kernel/engine/runner/schema-validation.js +3 -3
- package/esm/src/kernel/engine/runner/stages.d.ts +39 -0
- package/esm/src/kernel/engine/runner/stages.js +89 -0
- package/esm/src/kernel/engine/runner/state.d.ts +24 -0
- package/esm/src/kernel/engine/runner/steps.js +128 -61
- package/esm/src/kernel/engine/runner/stream.d.ts +9 -2
- package/esm/src/kernel/engine/runner/stream.js +114 -55
- package/esm/src/kernel/engine/session/mod.d.ts +3 -1
- package/esm/src/kernel/engine/session/mod.js +386 -34
- package/esm/src/kernel/interaction-parts.d.ts +14 -0
- package/esm/src/kernel/interaction-parts.js +23 -0
- package/esm/src/kernel/mod.d.ts +19 -7
- package/esm/src/kernel/mod.js +10 -5
- package/esm/src/kernel/profile-graph.d.ts +159 -0
- package/esm/src/kernel/profile-graph.js +156 -0
- package/esm/src/kernel/registry/attachments.d.ts +10 -10
- package/esm/src/kernel/registry/attachments.js +29 -26
- package/esm/src/kernel/registry/catalog.d.ts +25 -11
- package/esm/src/kernel/registry/catalog.js +59 -24
- package/esm/src/kernel/registry/ingress.js +34 -22
- package/esm/src/kernel/registry/profile-outputs.d.ts +4 -0
- package/esm/src/kernel/registry/profile-outputs.js +8 -0
- package/esm/src/kernel/registry/profiles.d.ts +34 -14
- package/esm/src/kernel/registry/profiles.js +306 -59
- package/esm/src/kernel/registry/provider-request.js +2 -0
- package/esm/src/kernel/registry/resolve.d.ts +7 -6
- package/esm/src/kernel/registry/resolve.js +102 -117
- package/esm/src/kernel/registry/schemas.js +1 -1
- package/esm/src/kernel/registry/sole-model.d.ts +8 -0
- package/esm/src/kernel/registry/sole-model.js +10 -0
- package/esm/src/kernel/registry/system-prompt.d.ts +10 -0
- package/esm/src/kernel/registry/system-prompt.js +40 -0
- package/esm/src/kernel/registry/system-role.d.ts +8 -0
- package/esm/src/kernel/registry/system-role.js +14 -0
- package/esm/src/kernel/registry/vault.d.ts +2 -2
- package/esm/src/kernel/registry/vault.js +4 -4
- package/esm/src/kernel/schema.d.ts +97 -11
- package/esm/src/kernel/schema.js +256 -111
- package/esm/src/kernel/stages.d.ts +175 -0
- package/esm/src/kernel/stages.js +476 -0
- package/esm/src/kernel/stop.d.ts +65 -11
- package/esm/src/kernel/stop.js +46 -11
- package/esm/src/kernel/tools/events.d.ts +41 -0
- package/esm/src/kernel/tools/events.js +71 -0
- package/esm/src/kernel/tools/execute.d.ts +63 -24
- package/esm/src/kernel/tools/execute.js +475 -224
- package/esm/src/kernel/tools/harness.js +16 -14
- package/esm/src/kernel/tools/invoke.js +26 -8
- package/esm/src/kernel/tools/mod.d.ts +3 -1
- package/esm/src/kernel/tools/mod.js +2 -1
- package/esm/src/kernel/tools/permission.d.ts +15 -0
- package/esm/src/kernel/tools/permission.js +47 -0
- package/esm/src/kernel/tools/project.js +5 -3
- package/esm/src/kernel/tools/registry.js +20 -10
- package/esm/src/kernel/tools/remote.d.ts +94 -0
- package/esm/src/kernel/tools/remote.js +577 -0
- package/esm/src/kernel/tools/resolve.d.ts +17 -7
- package/esm/src/kernel/tools/resolve.js +38 -17
- package/esm/src/kernel/tools/schema.d.ts +2 -0
- package/esm/src/kernel/tools/schema.js +23 -6
- package/esm/src/kernel/tools/stage-run.d.ts +105 -0
- package/esm/src/kernel/tools/stage-run.js +155 -0
- package/esm/src/kernel/tools/types.d.ts +198 -20
- package/esm/src/kernel/types.d.ts +253 -114
- package/esm/src/kernel/util/find-last.d.ts +2 -0
- package/esm/src/kernel/util/find-last.js +10 -0
- package/esm/src/observability/destinations.d.ts +31 -0
- package/esm/src/observability/destinations.js +67 -0
- package/esm/src/observability/mod.d.ts +10 -3
- package/esm/src/observability/mod.js +6 -2
- package/esm/src/observability/policy.d.ts +27 -0
- package/esm/src/observability/policy.js +80 -0
- package/esm/src/observability/resolve-policy.d.ts +16 -0
- package/esm/src/observability/resolve-policy.js +64 -0
- package/esm/src/observability/trace-attach.d.ts +3 -1
- package/esm/src/observability/trace-attach.js +17 -10
- package/esm/src/observability/trace-record.d.ts +12 -4
- package/esm/src/observability/trace-record.js +63 -19
- package/esm/src/observability/trace-sink.d.ts +19 -0
- package/esm/src/observability/trace-sink.js +10 -0
- package/esm/src/observability/trace.d.ts +14 -11
- package/esm/src/observability/trace.js +23 -14
- package/esm/src/observability/types.d.ts +113 -0
- package/esm/src/observability/types.js +11 -0
- package/esm/src/providers/create-provider.d.ts +7 -4
- package/esm/src/providers/create-provider.js +21 -7
- package/esm/src/providers/google/interactions/framing.d.ts +8 -0
- package/esm/src/providers/google/interactions/framing.js +74 -13
- package/esm/src/providers/google/live/framing.d.ts +5 -2
- package/esm/src/providers/google/live/framing.js +49 -12
- package/esm/src/providers/google/live/openapi-schema.js +3 -2
- package/esm/src/providers/google/live/stream.d.ts +9 -0
- package/esm/src/providers/google/live/stream.js +16 -3
- package/esm/src/providers/openrouter/cache-control.d.ts +24 -0
- package/esm/src/providers/openrouter/cache-control.js +23 -0
- package/esm/src/providers/openrouter/chat.d.ts +21 -1
- package/esm/src/providers/openrouter/chat.js +51 -6
- package/esm/src/providers/openrouter/image.js +8 -19
- package/esm/src/providers/openrouter/openai/chat-payload.js +23 -1
- package/esm/src/providers/openrouter/openai/compat.d.ts +7 -1
- package/esm/src/providers/openrouter/openai/compat.js +23 -4
- package/esm/src/providers/openrouter/openai/image-payload.js +8 -2
- package/esm/src/providers/openrouter/openai/sdk-messages.d.ts +1 -1
- package/esm/src/providers/openrouter/openai/sdk-messages.js +32 -5
- package/package.json +1 -1
- package/esm/src/interface/blocks.d.ts +0 -20
- package/esm/src/interface/blocks.js +0 -180
- package/esm/src/interface/from-profile.d.ts +0 -14
- package/esm/src/interface/from-profile.js +0 -104
- package/esm/src/interface/inputs.d.ts +0 -8
- package/esm/src/interface/inputs.js +0 -127
- package/esm/src/interface/mod.d.ts +0 -10
- package/esm/src/interface/mod.js +0 -9
- package/esm/src/interface/types.d.ts +0 -153
- package/esm/src/interface/types.js +0 -13
|
@@ -0,0 +1,577 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Execution runners for Declarative HTTP and Remote MCP tools.
|
|
3
|
+
*
|
|
4
|
+
* Implements:
|
|
5
|
+
* - URL parameter substitution and query/body mapping
|
|
6
|
+
* - Network SSRF guardrail enforcement via `assertSafeUrl`
|
|
7
|
+
* - Credential resolution (bearer, api_key, oauth2)
|
|
8
|
+
* - Proactive OAuth token refresh with progress event emission
|
|
9
|
+
* - ToolGate { kind: 'auth' } emission or model error reporting
|
|
10
|
+
* - Streamable HTTP MCP JSON-RPC protocol (`tools/call`) per 2026-07-28 spec
|
|
11
|
+
*
|
|
12
|
+
* @module
|
|
13
|
+
*/
|
|
14
|
+
import { lexiconText } from '../../guardrails/lexicon.js';
|
|
15
|
+
import { refreshOAuthToken } from '../auth/oauth.js';
|
|
16
|
+
import { guardToolTarget, messageOf, startToolExecution, toolEvent, } from './events.js';
|
|
17
|
+
import { checkPermission } from './permission.js';
|
|
18
|
+
import { runPreToolPipeline } from './stage-run.js';
|
|
19
|
+
function authHeaderPair(authConfig, value, defaults) {
|
|
20
|
+
const headerName = authConfig.headerName ?? defaults.headerName;
|
|
21
|
+
const headerPrefix = authConfig.headerPrefix ?? defaults.headerPrefix;
|
|
22
|
+
return { [headerName]: `${headerPrefix}${value}` };
|
|
23
|
+
}
|
|
24
|
+
function buildAuthGate(toolName, authConfig, message, extras) {
|
|
25
|
+
return {
|
|
26
|
+
kind: 'auth',
|
|
27
|
+
tool: toolName,
|
|
28
|
+
authChallenge: {
|
|
29
|
+
slot: authConfig.slot,
|
|
30
|
+
authType: authConfig.type,
|
|
31
|
+
message,
|
|
32
|
+
issuer: extras?.issuer ?? authConfig.preResolved?.issuer,
|
|
33
|
+
resource: extras?.resource ?? authConfig.preResolved?.resource,
|
|
34
|
+
requiredScopes: authConfig.scopes,
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function unauthenticatedResult(toolName, authConfig, message, policy, extras) {
|
|
39
|
+
if (policy === 'pause') {
|
|
40
|
+
// Policy name remains `pause` in schema; wire is honest `gate`.
|
|
41
|
+
// Do not emit gate here — caller uses emitGateSettlement (pre_tool + gate).
|
|
42
|
+
const gate = buildAuthGate(toolName, authConfig, message, extras);
|
|
43
|
+
return { headers: {}, unauthenticated: true, gate };
|
|
44
|
+
}
|
|
45
|
+
return { headers: {}, unauthenticated: true, modelMessage: message };
|
|
46
|
+
}
|
|
47
|
+
function resolveStaticCredential(credential, authConfig) {
|
|
48
|
+
if (credential.type === 'bearer') {
|
|
49
|
+
return {
|
|
50
|
+
headers: authHeaderPair(authConfig, credential.token, {
|
|
51
|
+
headerName: 'Authorization',
|
|
52
|
+
headerPrefix: 'Bearer ',
|
|
53
|
+
}),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
if (credential.type === 'api_key') {
|
|
57
|
+
const headerName = credential.headerName ?? authConfig.headerName ?? 'Authorization';
|
|
58
|
+
const headerPrefix = credential.headerPrefix ?? authConfig.headerPrefix ?? '';
|
|
59
|
+
return { headers: { [headerName]: `${headerPrefix}${credential.key}` } };
|
|
60
|
+
}
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
async function* resolveOAuth2Credential(toolName, authConfig, credential, ctx, base, policy) {
|
|
64
|
+
const slot = authConfig.slot;
|
|
65
|
+
let activeToken = credential.accessToken;
|
|
66
|
+
const now = Date.now();
|
|
67
|
+
const isExpired = credential.expiresAt !== undefined && credential.expiresAt <= now + 30000;
|
|
68
|
+
if (isExpired && credential.refreshToken) {
|
|
69
|
+
try {
|
|
70
|
+
const refreshResult = await refreshOAuthToken({
|
|
71
|
+
refreshToken: credential.refreshToken,
|
|
72
|
+
tokenEndpoint: credential.tokenEndpoint,
|
|
73
|
+
clientId: credential.clientId,
|
|
74
|
+
resource: credential.resource,
|
|
75
|
+
scope: credential.scope,
|
|
76
|
+
issuer: credential.issuer,
|
|
77
|
+
});
|
|
78
|
+
activeToken = refreshResult.credential.accessToken;
|
|
79
|
+
yield toolEvent(base, {
|
|
80
|
+
phase: 'progress',
|
|
81
|
+
data: {
|
|
82
|
+
kind: 'auth_token_refreshed',
|
|
83
|
+
slot,
|
|
84
|
+
credential: refreshResult.credential,
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
if (ctx.credentials) {
|
|
88
|
+
ctx.credentials[slot] = refreshResult.credential;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
catch (err) {
|
|
92
|
+
const message = `Failed to refresh OAuth token for '${toolName}' (slot: '${slot}'): ${err instanceof Error ? err.message : String(err)}`; // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
|
|
93
|
+
return unauthenticatedResult(toolName, authConfig, message, policy, {
|
|
94
|
+
issuer: credential.issuer,
|
|
95
|
+
resource: credential.resource,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
else if (isExpired && !credential.refreshToken) {
|
|
100
|
+
const message = `OAuth token expired for '${toolName}' (slot: '${slot}') and no refresh token is available.`; // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
|
|
101
|
+
return unauthenticatedResult(toolName, authConfig, message, policy, {
|
|
102
|
+
issuer: credential.issuer,
|
|
103
|
+
resource: credential.resource,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
headers: authHeaderPair(authConfig, activeToken, {
|
|
108
|
+
headerName: 'Authorization',
|
|
109
|
+
headerPrefix: 'Bearer ',
|
|
110
|
+
}),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Resolves credentials and handles missing/expired tokens.
|
|
115
|
+
* Yields TurnEvents for auth pauses or progress (e.g. refreshed tokens).
|
|
116
|
+
*/
|
|
117
|
+
export async function* resolveToolAuth(toolName, authConfig, ctx, base) {
|
|
118
|
+
if (!authConfig) {
|
|
119
|
+
return { headers: {} };
|
|
120
|
+
}
|
|
121
|
+
const credential = ctx.credentials?.[authConfig.slot];
|
|
122
|
+
const policy = authConfig.onUnauthenticated ?? 'pause';
|
|
123
|
+
if (!credential) {
|
|
124
|
+
const message = `Authentication required for '${toolName}' (auth slot: '${authConfig.slot}').`; // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
|
|
125
|
+
return unauthenticatedResult(toolName, authConfig, message, policy);
|
|
126
|
+
}
|
|
127
|
+
const staticResolved = resolveStaticCredential(credential, authConfig);
|
|
128
|
+
if (staticResolved) {
|
|
129
|
+
return staticResolved;
|
|
130
|
+
}
|
|
131
|
+
if (credential.type === 'oauth2') {
|
|
132
|
+
return yield* resolveOAuth2Credential(toolName, authConfig, credential, ctx, base, policy);
|
|
133
|
+
}
|
|
134
|
+
return { headers: {} };
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Build the request URL (and optional JSON body) for a declarative HTTP tool
|
|
138
|
+
* from endpoint template, mapping, and validated input.
|
|
139
|
+
*/
|
|
140
|
+
export function buildHttpToolTarget(endpoint, method, input, mapping) {
|
|
141
|
+
let urlStr = endpoint;
|
|
142
|
+
const pathParams = mapping?.pathParams ?? [];
|
|
143
|
+
for (const param of pathParams) {
|
|
144
|
+
const val = input[param];
|
|
145
|
+
if (val !== undefined) {
|
|
146
|
+
urlStr = urlStr.replaceAll(`{${param}}`, encodeURIComponent(String(val)));
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
const unreplacedMatch = urlStr.match(/\{([a-zA-Z0-9_-]+)\}/);
|
|
150
|
+
if (unreplacedMatch) {
|
|
151
|
+
throw new Error(`Missing required path parameter "${unreplacedMatch[1]}" for endpoint "${endpoint}"`);
|
|
152
|
+
}
|
|
153
|
+
const targetUrl = new URL(urlStr);
|
|
154
|
+
const queryParams = mapping?.queryParams ?? [];
|
|
155
|
+
for (const param of queryParams) {
|
|
156
|
+
const val = input[param];
|
|
157
|
+
if (val !== undefined) {
|
|
158
|
+
targetUrl.searchParams.set(param, String(val));
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (method === 'GET') {
|
|
162
|
+
return { url: targetUrl.toString() };
|
|
163
|
+
}
|
|
164
|
+
if (mapping?.bodyParam) {
|
|
165
|
+
return { url: targetUrl.toString(), body: JSON.stringify(input[mapping.bodyParam]) };
|
|
166
|
+
}
|
|
167
|
+
const bodyObj = {};
|
|
168
|
+
const excluded = new Set([...pathParams, ...queryParams]);
|
|
169
|
+
for (const [k, v] of Object.entries(input)) {
|
|
170
|
+
if (!excluded.has(k)) {
|
|
171
|
+
bodyObj[k] = v;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return { url: targetUrl.toString(), body: JSON.stringify(bodyObj) };
|
|
175
|
+
}
|
|
176
|
+
function parseHttpResponseData(contentType, text, status) {
|
|
177
|
+
if (contentType.includes('application/json') && text.trim().length > 0) {
|
|
178
|
+
try {
|
|
179
|
+
return JSON.parse(text);
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
return text;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (text.trim().length === 0 && status === 204) {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
return text;
|
|
189
|
+
}
|
|
190
|
+
export function parseToolOutput(schema, responseData) {
|
|
191
|
+
let checked = schema.safeParse(responseData);
|
|
192
|
+
if (!checked.success && typeof responseData === 'string' && responseData.trim().length > 0) {
|
|
193
|
+
try {
|
|
194
|
+
const jsonParsed = JSON.parse(responseData);
|
|
195
|
+
const retryChecked = schema.safeParse(jsonParsed);
|
|
196
|
+
if (retryChecked.success) {
|
|
197
|
+
checked = retryChecked;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
// Not JSON
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return checked;
|
|
205
|
+
}
|
|
206
|
+
export function modelResultFromOutput(data) {
|
|
207
|
+
return {
|
|
208
|
+
finding: typeof data === 'string' ? data : JSON.stringify(data),
|
|
209
|
+
data,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
function failureOutcome(failure, callNotStarted) {
|
|
213
|
+
return { kind: 'failed', failure, callNotStarted };
|
|
214
|
+
}
|
|
215
|
+
/** Tool `preTool` + host `pre_tool` + mutate re-parse, mapped onto a remote outcome. */
|
|
216
|
+
async function* runRemotePreBodyStages(args) {
|
|
217
|
+
const pre = yield* runPreToolPipeline(args);
|
|
218
|
+
if (pre.ok)
|
|
219
|
+
return pre;
|
|
220
|
+
if (pre.kind === 'aborted') {
|
|
221
|
+
return { kind: 'aborted', aborted: pre.aborted };
|
|
222
|
+
}
|
|
223
|
+
if (pre.kind === 'gated') {
|
|
224
|
+
return { kind: 'gated', gate: pre.gate };
|
|
225
|
+
}
|
|
226
|
+
return failureOutcome(pre.failure, true);
|
|
227
|
+
}
|
|
228
|
+
async function* remoteParseAndPermit(tool, rawInput, ctx, base) {
|
|
229
|
+
const started = yield* startToolExecution(tool, rawInput, ctx, base);
|
|
230
|
+
if (!started.ok) {
|
|
231
|
+
return {
|
|
232
|
+
ok: false,
|
|
233
|
+
outcome: failureOutcome({ code: 'invalid_input', message: lexiconText('tool.input_invalid') }, true),
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
const permissionGate = checkPermission(tool.name, tool.permission, ctx.sessionPermissions, ctx.resume);
|
|
237
|
+
if (permissionGate) {
|
|
238
|
+
return { ok: false, outcome: { kind: 'gated', gate: permissionGate } };
|
|
239
|
+
}
|
|
240
|
+
return { ok: true, input: started.data };
|
|
241
|
+
}
|
|
242
|
+
function outcomeFromUnauth(authRes) {
|
|
243
|
+
if (!authRes.unauthenticated)
|
|
244
|
+
return undefined;
|
|
245
|
+
if (authRes.gate)
|
|
246
|
+
return { kind: 'gated', gate: authRes.gate };
|
|
247
|
+
if (authRes.modelMessage) {
|
|
248
|
+
return {
|
|
249
|
+
kind: 'ok',
|
|
250
|
+
modelResult: { finding: authRes.modelMessage },
|
|
251
|
+
outputRaw: { unauthenticated: true, message: authRes.modelMessage },
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
return failureOutcome({ code: 'not_authorized', message: 'Tool authentication required' }, // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
|
|
255
|
+
true);
|
|
256
|
+
}
|
|
257
|
+
async function* remoteAuthAndPreBody(args) {
|
|
258
|
+
const authRes = yield* resolveToolAuth(args.tool.name, args.tool.auth, args.ctx, args.base);
|
|
259
|
+
const unauth = outcomeFromUnauth(authRes);
|
|
260
|
+
if (unauth)
|
|
261
|
+
return unauth;
|
|
262
|
+
const preBody = yield* runRemotePreBodyStages({
|
|
263
|
+
tool: args.tool,
|
|
264
|
+
input: args.input,
|
|
265
|
+
ctx: args.ctx,
|
|
266
|
+
base: args.base,
|
|
267
|
+
stages: args.stages,
|
|
268
|
+
});
|
|
269
|
+
if (!('ok' in preBody))
|
|
270
|
+
return preBody;
|
|
271
|
+
return { ok: true, input: preBody.input, authHeaders: authRes.headers ?? {} };
|
|
272
|
+
}
|
|
273
|
+
function networkFailureOutcome(err) {
|
|
274
|
+
return failureOutcome({ code: 'network_error', message: messageOf(err) }, false);
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Executes a Declarative HTTP tool.
|
|
278
|
+
* Order: schema → permission → auth → preTool/host stages → body.
|
|
279
|
+
*/
|
|
280
|
+
export async function* executeHttpTool(tool, rawInput, ctx, base, stages) {
|
|
281
|
+
const parsed = yield* remoteParseAndPermit(tool, rawInput, ctx, base);
|
|
282
|
+
if (!parsed.ok)
|
|
283
|
+
return parsed.outcome;
|
|
284
|
+
let input = parsed.input;
|
|
285
|
+
const prepared = yield* remoteAuthAndPreBody({ tool, input, ctx, base, stages });
|
|
286
|
+
if (!('ok' in prepared))
|
|
287
|
+
return prepared;
|
|
288
|
+
input = prepared.input;
|
|
289
|
+
let target;
|
|
290
|
+
try {
|
|
291
|
+
target = buildHttpToolTarget(tool.endpoint, tool.method, input, tool.mapping);
|
|
292
|
+
}
|
|
293
|
+
catch (err) {
|
|
294
|
+
const failure = { code: 'invalid_input', message: messageOf(err) };
|
|
295
|
+
return failureOutcome(failure, true);
|
|
296
|
+
}
|
|
297
|
+
const targetUrl = yield* guardToolTarget(target.url, ctx, base);
|
|
298
|
+
if (!targetUrl) {
|
|
299
|
+
const failure = {
|
|
300
|
+
code: 'network_blocked',
|
|
301
|
+
message: 'HTTP target blocked by network policy', // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
|
|
302
|
+
};
|
|
303
|
+
return failureOutcome(failure, true);
|
|
304
|
+
}
|
|
305
|
+
const headers = {
|
|
306
|
+
Accept: 'application/json',
|
|
307
|
+
...tool.headers,
|
|
308
|
+
...prepared.authHeaders,
|
|
309
|
+
};
|
|
310
|
+
if (tool.method !== 'GET') {
|
|
311
|
+
headers['Content-Type'] = 'application/json';
|
|
312
|
+
}
|
|
313
|
+
try {
|
|
314
|
+
const response = await fetch(targetUrl.toString(), {
|
|
315
|
+
method: tool.method,
|
|
316
|
+
headers,
|
|
317
|
+
body: target.body,
|
|
318
|
+
signal: ctx.signal,
|
|
319
|
+
});
|
|
320
|
+
if (!response.ok) {
|
|
321
|
+
const errText = await response.text();
|
|
322
|
+
const failure = {
|
|
323
|
+
code: `http_${response.status}`,
|
|
324
|
+
message: `HTTP ${response.status} from ${targetUrl.hostname}: ${errText}`,
|
|
325
|
+
};
|
|
326
|
+
return failureOutcome(failure, false);
|
|
327
|
+
}
|
|
328
|
+
const text = await response.text();
|
|
329
|
+
const responseData = parseHttpResponseData(response.headers.get('content-type') ?? '', text, response.status);
|
|
330
|
+
const checked = parseToolOutput(tool.output, responseData);
|
|
331
|
+
if (!checked.success) {
|
|
332
|
+
const failure = {
|
|
333
|
+
code: 'invalid_output',
|
|
334
|
+
message: 'HTTP response did not match tool output schema', // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
|
|
335
|
+
details: checked.error.flatten(),
|
|
336
|
+
};
|
|
337
|
+
return failureOutcome(failure, false);
|
|
338
|
+
}
|
|
339
|
+
return {
|
|
340
|
+
kind: 'ok',
|
|
341
|
+
modelResult: modelResultFromOutput(checked.data),
|
|
342
|
+
outputRaw: checked.data,
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
catch (err) {
|
|
346
|
+
return networkFailureOutcome(err);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
/** Preferred-first Streamable HTTP protocol revisions the kernel will negotiate. */
|
|
350
|
+
export const MCP_PROTOCOL_VERSIONS = [
|
|
351
|
+
'2026-07-28',
|
|
352
|
+
'2025-11-25',
|
|
353
|
+
'2025-06-18',
|
|
354
|
+
'2025-03-26',
|
|
355
|
+
];
|
|
356
|
+
/** Parse MCP JSON or SSE (`event: message` / `data:`) bodies into a JSON-RPC object. */
|
|
357
|
+
export function parseMcpRpcResponse(text) {
|
|
358
|
+
const trimmed = text.trim();
|
|
359
|
+
if (trimmed.startsWith('{')) {
|
|
360
|
+
return JSON.parse(trimmed);
|
|
361
|
+
}
|
|
362
|
+
const messages = [];
|
|
363
|
+
for (const line of text.split('\n')) {
|
|
364
|
+
if (!line.startsWith('data: '))
|
|
365
|
+
continue;
|
|
366
|
+
const payload = line.slice(6).trim();
|
|
367
|
+
if (!payload || payload === '[DONE]')
|
|
368
|
+
continue;
|
|
369
|
+
try {
|
|
370
|
+
messages.push(JSON.parse(payload));
|
|
371
|
+
}
|
|
372
|
+
catch {
|
|
373
|
+
// Ignore non-JSON SSE payloads (e.g. pings).
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
377
|
+
const msg = messages[i];
|
|
378
|
+
if (msg.result !== undefined && msg.method === undefined)
|
|
379
|
+
return msg;
|
|
380
|
+
}
|
|
381
|
+
const last = messages.at(-1);
|
|
382
|
+
if (!last) {
|
|
383
|
+
throw new Error(`MCP server returned non-JSON response: ${text.slice(0, 200)}`); // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
|
|
384
|
+
}
|
|
385
|
+
return last;
|
|
386
|
+
}
|
|
387
|
+
/** True when a JSON-RPC error indicates the server rejected our protocol revision. */
|
|
388
|
+
export function isUnsupportedMcpProtocolError(error) {
|
|
389
|
+
if (!error)
|
|
390
|
+
return false;
|
|
391
|
+
const message = error.message.toLowerCase();
|
|
392
|
+
return (message.includes('unsupported protocol version') || // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
|
|
393
|
+
message.includes('inconsistent mcp protocol version') // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
function unsupportedProtocolFromHttpBody(text) {
|
|
397
|
+
try {
|
|
398
|
+
const rpcResponse = parseMcpRpcResponse(text);
|
|
399
|
+
if (rpcResponse.error && isUnsupportedMcpProtocolError(rpcResponse.error)) {
|
|
400
|
+
return rpcResponse.error;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
catch {
|
|
404
|
+
// Fall through to raw-text heuristic for non-JSON error pages.
|
|
405
|
+
}
|
|
406
|
+
// lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
|
|
407
|
+
if (text.toLowerCase().includes('unsupported protocol version')) {
|
|
408
|
+
return { code: -32600, message: text.slice(0, 300) };
|
|
409
|
+
}
|
|
410
|
+
return undefined;
|
|
411
|
+
}
|
|
412
|
+
async function fetchMcpProtocolAttempt(targetUrl, baseHeaders, rpcId, mcpToolName, input, protocolVersion, signal) {
|
|
413
|
+
const jsonRpcPayload = {
|
|
414
|
+
jsonrpc: '2.0',
|
|
415
|
+
id: rpcId,
|
|
416
|
+
method: 'tools/call',
|
|
417
|
+
params: {
|
|
418
|
+
name: mcpToolName,
|
|
419
|
+
arguments: input,
|
|
420
|
+
_meta: {
|
|
421
|
+
'io.modelcontextprotocol/protocolVersion': protocolVersion,
|
|
422
|
+
},
|
|
423
|
+
},
|
|
424
|
+
};
|
|
425
|
+
const response = await fetch(targetUrl, {
|
|
426
|
+
method: 'POST',
|
|
427
|
+
headers: {
|
|
428
|
+
...baseHeaders,
|
|
429
|
+
'MCP-Protocol-Version': protocolVersion,
|
|
430
|
+
},
|
|
431
|
+
body: JSON.stringify(jsonRpcPayload),
|
|
432
|
+
signal,
|
|
433
|
+
});
|
|
434
|
+
const text = await response.text();
|
|
435
|
+
if (!response.ok) {
|
|
436
|
+
const acceptRejected = response.status === 406 &&
|
|
437
|
+
text.toLowerCase().includes('accept') &&
|
|
438
|
+
protocolVersion !== MCP_PROTOCOL_VERSIONS.at(-1);
|
|
439
|
+
if (acceptRejected)
|
|
440
|
+
return { kind: 'retry' };
|
|
441
|
+
const protocolError = unsupportedProtocolFromHttpBody(text);
|
|
442
|
+
if (protocolError)
|
|
443
|
+
return { kind: 'protocol_retry', error: protocolError };
|
|
444
|
+
return {
|
|
445
|
+
kind: 'failure',
|
|
446
|
+
failure: {
|
|
447
|
+
code: `mcp_http_${response.status}`,
|
|
448
|
+
message: `MCP server error HTTP ${response.status}: ${text.slice(0, 300)}`, // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
|
|
449
|
+
},
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
try {
|
|
453
|
+
const rpcResponse = parseMcpRpcResponse(text);
|
|
454
|
+
if (rpcResponse.error && isUnsupportedMcpProtocolError(rpcResponse.error)) {
|
|
455
|
+
return { kind: 'protocol_retry', error: rpcResponse.error };
|
|
456
|
+
}
|
|
457
|
+
return { kind: 'rpc', response: rpcResponse };
|
|
458
|
+
}
|
|
459
|
+
catch {
|
|
460
|
+
return {
|
|
461
|
+
kind: 'failure',
|
|
462
|
+
failure: {
|
|
463
|
+
code: 'invalid_response',
|
|
464
|
+
message: `MCP server returned non-JSON response: ${text.slice(0, 200)}`, // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
|
|
465
|
+
},
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
async function negotiateMcpRpc(targetUrl, baseHeaders, rpcId, mcpToolName, input, signal) {
|
|
470
|
+
let lastProtocolError;
|
|
471
|
+
for (const protocolVersion of MCP_PROTOCOL_VERSIONS) {
|
|
472
|
+
const outcome = await fetchMcpProtocolAttempt(targetUrl, baseHeaders, rpcId, mcpToolName, input, protocolVersion, signal);
|
|
473
|
+
if (outcome.kind === 'retry' || outcome.kind === 'protocol_retry') {
|
|
474
|
+
if (outcome.kind === 'protocol_retry')
|
|
475
|
+
lastProtocolError = outcome.error;
|
|
476
|
+
continue;
|
|
477
|
+
}
|
|
478
|
+
if (outcome.kind === 'failure') {
|
|
479
|
+
return { failure: outcome.failure };
|
|
480
|
+
}
|
|
481
|
+
return { rpc: outcome.response };
|
|
482
|
+
}
|
|
483
|
+
return { lastProtocolError };
|
|
484
|
+
}
|
|
485
|
+
function extractMcpOutput(result) {
|
|
486
|
+
if (result?.content && Array.isArray(result.content)) {
|
|
487
|
+
return result.content.map((c) => c.text ?? '').join('\n');
|
|
488
|
+
}
|
|
489
|
+
return result;
|
|
490
|
+
}
|
|
491
|
+
function mcpResultFailure(rpcResponse) {
|
|
492
|
+
if (rpcResponse.error) {
|
|
493
|
+
return {
|
|
494
|
+
code: `mcp_rpc_error_${rpcResponse.error.code}`,
|
|
495
|
+
message: rpcResponse.error.message,
|
|
496
|
+
details: rpcResponse.error.data,
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
const result = rpcResponse.result;
|
|
500
|
+
if (result?.isError) {
|
|
501
|
+
return {
|
|
502
|
+
code: 'mcp_tool_execution_failed',
|
|
503
|
+
message: result.content?.map((c) => c.text ?? '').join('\n') ?? 'MCP Tool execution error', // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
return undefined;
|
|
507
|
+
}
|
|
508
|
+
function interpretMcpRpc(tool, negotiated) {
|
|
509
|
+
if (negotiated.failure) {
|
|
510
|
+
return { ok: false, failure: negotiated.failure };
|
|
511
|
+
}
|
|
512
|
+
const rpcResponse = negotiated.rpc;
|
|
513
|
+
if (!rpcResponse) {
|
|
514
|
+
return {
|
|
515
|
+
ok: false,
|
|
516
|
+
failure: {
|
|
517
|
+
code: 'mcp_protocol_error',
|
|
518
|
+
message: negotiated.lastProtocolError?.message ?? 'MCP protocol negotiation failed', // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
|
|
519
|
+
details: negotiated.lastProtocolError?.data,
|
|
520
|
+
},
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
const resultFailure = mcpResultFailure(rpcResponse);
|
|
524
|
+
if (resultFailure) {
|
|
525
|
+
return { ok: false, failure: resultFailure };
|
|
526
|
+
}
|
|
527
|
+
const checked = parseToolOutput(tool.output, extractMcpOutput(rpcResponse.result));
|
|
528
|
+
if (!checked.success) {
|
|
529
|
+
return {
|
|
530
|
+
ok: false,
|
|
531
|
+
failure: {
|
|
532
|
+
code: 'invalid_output',
|
|
533
|
+
message: 'MCP output schema validation failed', // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
|
|
534
|
+
details: checked.error.flatten(),
|
|
535
|
+
},
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
return { ok: true, data: checked.data };
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
541
|
+
* Executes a Remote MCP tool over Streamable HTTP (spec revision 2026-07-28).
|
|
542
|
+
* Order: schema → permission → auth → preTool/host stages → body.
|
|
543
|
+
*/
|
|
544
|
+
export async function* executeMcpTool(tool, rawInput, ctx, base, stages) {
|
|
545
|
+
const permitted = yield* remoteParseAndPermit(tool, rawInput, ctx, base);
|
|
546
|
+
if (!permitted.ok)
|
|
547
|
+
return permitted.outcome;
|
|
548
|
+
const targetUrl = yield* guardToolTarget(tool.serverUrl, ctx, base);
|
|
549
|
+
if (!targetUrl) {
|
|
550
|
+
return failureOutcome({ code: 'network_blocked', message: 'MCP target blocked by network policy' }, // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
|
|
551
|
+
true);
|
|
552
|
+
}
|
|
553
|
+
let input = permitted.input;
|
|
554
|
+
const prepared = yield* remoteAuthAndPreBody({ tool, input, ctx, base, stages });
|
|
555
|
+
if (!('ok' in prepared))
|
|
556
|
+
return prepared;
|
|
557
|
+
input = prepared.input;
|
|
558
|
+
try {
|
|
559
|
+
const interpreted = interpretMcpRpc(tool, await negotiateMcpRpc(targetUrl.toString(), {
|
|
560
|
+
'Content-Type': 'application/json',
|
|
561
|
+
Accept: 'application/json, text/event-stream',
|
|
562
|
+
...tool.headers,
|
|
563
|
+
...prepared.authHeaders,
|
|
564
|
+
}, base.callId ?? Date.now(), tool.mcpToolName, input, ctx.signal));
|
|
565
|
+
if (!interpreted.ok) {
|
|
566
|
+
return failureOutcome(interpreted.failure, false);
|
|
567
|
+
}
|
|
568
|
+
return {
|
|
569
|
+
kind: 'ok',
|
|
570
|
+
modelResult: modelResultFromOutput(interpreted.data),
|
|
571
|
+
outputRaw: interpreted.data,
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
catch (err) {
|
|
575
|
+
return networkFailureOutcome(err);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
@@ -3,23 +3,33 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @module
|
|
5
5
|
*/
|
|
6
|
-
import type { ModelId, Profile, ToolId, TurnRequest } from '../types.js';
|
|
6
|
+
import type { ModelId, ModelProfile, Profile, ToolId, TurnRequest } from '../types.js';
|
|
7
7
|
import type { PromoteLoadedResult, ToolFailure, TurnToolSnapshot, WireFunctionTool } from './types.js';
|
|
8
8
|
export declare function pathMatches(catalogPaths?: string[], turnPath?: string): boolean;
|
|
9
9
|
export declare function applyBuiltinMutualExclusions(requested: string[]): string[];
|
|
10
10
|
export declare function resolveAllowedCustomToolIds(profile: Profile, req: TurnRequest): ToolId[];
|
|
11
11
|
/** Provider builtins listed on the selected model — on for the turn (path-filtered). */
|
|
12
|
-
export declare function resolveModelBuiltinIds(profile:
|
|
12
|
+
export declare function resolveModelBuiltinIds(profile: ModelProfile, req: TurnRequest, modelId: ModelId): ToolId[];
|
|
13
13
|
export declare function wireForTool(name: string): WireFunctionTool | undefined;
|
|
14
14
|
export declare function buildWire(visible: ToolId[]): WireFunctionTool[];
|
|
15
15
|
export declare function promoteTool(state: TurnToolSnapshot, id: ToolId): void;
|
|
16
16
|
export declare function promoteBuiltin(state: TurnToolSnapshot, id: ToolId): void;
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
/**
|
|
18
|
+
* Custom tools wired at turn start. Text/image: T0 only (T1/T2 pending).
|
|
19
|
+
* Live: every gated tool — declarations are fixed at session setup, so every
|
|
20
|
+
* allowed tool is effectively T0. Host: every gated tool — no tiers at all.
|
|
21
|
+
*/
|
|
22
|
+
export declare function initialVisible(profile: Profile, gated: ToolId[]): ToolId[];
|
|
23
|
+
/** Builtins on at turn start — every gated builtin on live, T0 elsewhere (mutual exclusions applied). */
|
|
24
|
+
export declare function initialBuiltins(profile: Profile, gated: ToolId[]): ToolId[];
|
|
25
|
+
/**
|
|
26
|
+
* Build the initial tool snapshot for a turn.
|
|
27
|
+
* Text/image: T0 wired, T1/T2 pending. Live: the whole allow list and every
|
|
28
|
+
* model builtin. Host: the whole allow list, no builtins, no path gating.
|
|
29
|
+
*/
|
|
30
|
+
export declare function resolveTurnTools(profile: Profile, req: TurnRequest, modelId: ModelId | undefined): TurnToolSnapshot;
|
|
21
31
|
/** Resolve T0 snapshot and expand T1 selections from `profile.tools.t1Policy`. */
|
|
22
|
-
export declare function prepareTurnToolSnapshot(profile: Profile, req: TurnRequest, modelId: ModelId): Promise<TurnToolSnapshot>;
|
|
32
|
+
export declare function prepareTurnToolSnapshot(profile: Profile, req: TurnRequest, modelId: ModelId | undefined): Promise<TurnToolSnapshot>;
|
|
23
33
|
/** Deep-clone a turn snapshot so host-side concurrent invokes do not share mutable state. */
|
|
24
34
|
export declare function cloneTurnToolSnapshot(state: TurnToolSnapshot): TurnToolSnapshot;
|
|
25
35
|
/** Wire T1 tools selected by `profile.tools.t1Policy`. */
|