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
|
@@ -33,12 +33,13 @@ export function resolveAllowedCustomToolIds(profile, req) {
|
|
|
33
33
|
if (!tool || tool.type === 'builtin') {
|
|
34
34
|
return false;
|
|
35
35
|
}
|
|
36
|
-
|
|
36
|
+
// Host profiles execute every allowed tool — path gating does not apply.
|
|
37
|
+
return profile.type === 'host' || pathMatches(tool.paths, req.path);
|
|
37
38
|
});
|
|
38
39
|
}
|
|
39
40
|
/** Provider builtins listed on the selected model — on for the turn (path-filtered). */
|
|
40
41
|
export function resolveModelBuiltinIds(profile, req, modelId) {
|
|
41
|
-
const spec = profile.
|
|
42
|
+
const spec = profile.models[modelId];
|
|
42
43
|
if (!spec) {
|
|
43
44
|
return [];
|
|
44
45
|
}
|
|
@@ -100,22 +101,37 @@ export function promoteBuiltin(state, id) {
|
|
|
100
101
|
});
|
|
101
102
|
state.builtins.push(id);
|
|
102
103
|
}
|
|
103
|
-
|
|
104
|
+
/**
|
|
105
|
+
* Custom tools wired at turn start. Text/image: T0 only (T1/T2 pending).
|
|
106
|
+
* Live: every gated tool — declarations are fixed at session setup, so every
|
|
107
|
+
* allowed tool is effectively T0. Host: every gated tool — no tiers at all.
|
|
108
|
+
*/
|
|
109
|
+
export function initialVisible(profile, gated) {
|
|
110
|
+
if (profile.type === 'live' || profile.type === 'host') {
|
|
111
|
+
return [...gated];
|
|
112
|
+
}
|
|
104
113
|
return gated.filter((id) => getTool(id)?.loadTier === 'T0');
|
|
105
114
|
}
|
|
106
|
-
|
|
115
|
+
/** Builtins on at turn start — every gated builtin on live, T0 elsewhere (mutual exclusions applied). */
|
|
116
|
+
export function initialBuiltins(profile, gated) {
|
|
107
117
|
return applyBuiltinMutualExclusions(gated.filter((id) => {
|
|
108
118
|
const tool = getTool(id);
|
|
109
|
-
return tool?.type === 'builtin' && tool.loadTier === 'T0';
|
|
119
|
+
return tool?.type === 'builtin' && (profile.type === 'live' || tool.loadTier === 'T0');
|
|
110
120
|
}));
|
|
111
121
|
}
|
|
112
|
-
/**
|
|
122
|
+
/**
|
|
123
|
+
* Build the initial tool snapshot for a turn.
|
|
124
|
+
* Text/image: T0 wired, T1/T2 pending. Live: the whole allow list and every
|
|
125
|
+
* model builtin. Host: the whole allow list, no builtins, no path gating.
|
|
126
|
+
*/
|
|
113
127
|
export function resolveTurnTools(profile, req, modelId) {
|
|
114
128
|
const customAllowed = resolveAllowedCustomToolIds(profile, req);
|
|
115
|
-
const modelBuiltins =
|
|
129
|
+
const modelBuiltins = profile.type === 'host' || modelId === undefined
|
|
130
|
+
? []
|
|
131
|
+
: resolveModelBuiltinIds(profile, req, modelId);
|
|
116
132
|
const gated = [...customAllowed, ...modelBuiltins];
|
|
117
|
-
const builtins = initialBuiltins(gated);
|
|
118
|
-
const visible = initialVisible(gated);
|
|
133
|
+
const builtins = initialBuiltins(profile, gated);
|
|
134
|
+
const visible = initialVisible(profile, gated);
|
|
119
135
|
const executable = visible.filter((id) => getTool(id)?.type !== 'builtin');
|
|
120
136
|
return {
|
|
121
137
|
builtins,
|
|
@@ -147,7 +163,7 @@ export function cloneTurnToolSnapshot(state) {
|
|
|
147
163
|
}
|
|
148
164
|
/** Wire T1 tools selected by `profile.tools.t1Policy`. */
|
|
149
165
|
export async function expandT1Policy(state, profile, req) {
|
|
150
|
-
if (profile.type === 'speech') {
|
|
166
|
+
if (profile.type === 'speech' || profile.type === 'live' || profile.type === 'host') {
|
|
151
167
|
return;
|
|
152
168
|
}
|
|
153
169
|
const t1Policy = profile.tools.t1Policy;
|
|
@@ -162,6 +178,7 @@ export async function expandT1Policy(state, profile, req) {
|
|
|
162
178
|
path: req.path,
|
|
163
179
|
sessionPermissions: req.sessionPermissions,
|
|
164
180
|
gated: state.gated,
|
|
181
|
+
host: req.host,
|
|
165
182
|
});
|
|
166
183
|
}
|
|
167
184
|
catch (err) {
|
|
@@ -171,7 +188,7 @@ export async function expandT1Policy(state, profile, req) {
|
|
|
171
188
|
});
|
|
172
189
|
}
|
|
173
190
|
if (!Array.isArray(selected)) {
|
|
174
|
-
throw new TheorumError(`Profile '${profile.id}' tools.t1Policy must return ToolId[]`);
|
|
191
|
+
throw new TheorumError(`Profile '${profile.id}' tools.t1Policy must return ToolId[]`); // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
|
|
175
192
|
}
|
|
176
193
|
for (const id of selected) {
|
|
177
194
|
if (!state.gated.includes(id)) {
|
|
@@ -194,6 +211,10 @@ export async function expandT1Policy(state, profile, req) {
|
|
|
194
211
|
const LOADED_ID_BLOCKLIST = new Set(['__proto__', 'constructor', 'prototype']);
|
|
195
212
|
/** Promote T2 tools into the visible set after tools.t2Loader returns { loaded }. */
|
|
196
213
|
export function promoteLoadedTools(state, loaded, profile) {
|
|
214
|
+
if (profile.type === 'live' || profile.type === 'host') {
|
|
215
|
+
// Every allowed tool is already visible — there is nothing to promote.
|
|
216
|
+
return { promoted: [] };
|
|
217
|
+
}
|
|
197
218
|
const toPromote = [];
|
|
198
219
|
for (const id of loaded) {
|
|
199
220
|
if (typeof id !== 'string' || LOADED_ID_BLOCKLIST.has(id)) {
|
|
@@ -201,7 +222,7 @@ export function promoteLoadedTools(state, loaded, profile) {
|
|
|
201
222
|
promoted: [],
|
|
202
223
|
failure: {
|
|
203
224
|
code: 'invalid_output',
|
|
204
|
-
message: 'tools.t2Loader loaded ids must be plain strings',
|
|
225
|
+
message: 'tools.t2Loader loaded ids must be plain strings', // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
|
|
205
226
|
},
|
|
206
227
|
};
|
|
207
228
|
}
|
|
@@ -215,7 +236,7 @@ export function promoteLoadedTools(state, loaded, profile) {
|
|
|
215
236
|
promoted: [],
|
|
216
237
|
failure: {
|
|
217
238
|
code: 'invalid_output',
|
|
218
|
-
message: `Tool '${id}' is not registered`,
|
|
239
|
+
message: `Tool '${id}' is not registered`, // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
|
|
219
240
|
},
|
|
220
241
|
};
|
|
221
242
|
}
|
|
@@ -236,26 +257,26 @@ export function promotionFailure(id, profile) {
|
|
|
236
257
|
if (profile.type === 'speech' || !profile.tools.allow.includes(id)) {
|
|
237
258
|
return {
|
|
238
259
|
code: 'invalid_output',
|
|
239
|
-
message: `tools.t2Loader attempted to promote tool '${id}' outside profile allow`,
|
|
260
|
+
message: `tools.t2Loader attempted to promote tool '${id}' outside profile allow`, // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
|
|
240
261
|
};
|
|
241
262
|
}
|
|
242
263
|
const tool = getTool(id);
|
|
243
264
|
if (!tool) {
|
|
244
265
|
return {
|
|
245
266
|
code: 'invalid_output',
|
|
246
|
-
message: `tools.t2Loader attempted to promote unknown tool '${id}'`,
|
|
267
|
+
message: `tools.t2Loader attempted to promote unknown tool '${id}'`, // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
|
|
247
268
|
};
|
|
248
269
|
}
|
|
249
270
|
if (tool.type === 'builtin') {
|
|
250
271
|
return {
|
|
251
272
|
code: 'invalid_output',
|
|
252
|
-
message: `tools.t2Loader attempted to promote builtin '${id}' — only custom tools may be promoted`,
|
|
273
|
+
message: `tools.t2Loader attempted to promote builtin '${id}' — only custom tools may be promoted`, // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
|
|
253
274
|
};
|
|
254
275
|
}
|
|
255
276
|
if (tool.loadTier !== 'T2') {
|
|
256
277
|
return {
|
|
257
278
|
code: 'invalid_output',
|
|
258
|
-
message: `tools.t2Loader attempted to promote tool '${id}' with loadTier '${tool.loadTier}' — only T2 tools may be promoted`,
|
|
279
|
+
message: `tools.t2Loader attempted to promote tool '${id}' with loadTier '${tool.loadTier}' — only T2 tools may be promoted`, // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
|
|
259
280
|
};
|
|
260
281
|
}
|
|
261
282
|
return undefined;
|
|
@@ -11,3 +11,5 @@ declare function validateToolInputSchema(schema: JsonSchema, mode?: 'gemini' | '
|
|
|
11
11
|
declare function validateToolOutputSchema(schema: JsonSchema, mode?: 'gemini' | 'structural'): void;
|
|
12
12
|
declare function jsonSchemaFromZod(schema: ZodType, io?: 'input' | 'output'): JsonSchema;
|
|
13
13
|
export { jsonSchemaFromZod, validateToolInputSchema, validateToolOutputSchema };
|
|
14
|
+
/** Strip prototype-pollution keys from provider/host tool args or host-mutated values before validation. */
|
|
15
|
+
export declare function plainToolInput(input: unknown): unknown;
|
|
@@ -33,13 +33,13 @@ const GEMINI_SUPPORTED_SCHEMA_KEYS = [
|
|
|
33
33
|
function validateGeminiKeys(schema, path, errors) {
|
|
34
34
|
for (const key of Object.keys(schema)) {
|
|
35
35
|
if (!GEMINI_SUPPORTED_SCHEMA_KEYS.includes(key)) {
|
|
36
|
-
errors.push(`${path}: unsupported Gemini schema key '${key}'`);
|
|
36
|
+
errors.push(`${path}: unsupported Gemini schema key '${key}'`); // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
40
|
function validateSchemaShape(schema, path, mode, errors) {
|
|
41
41
|
if (!schema || typeof schema !== 'object' || Array.isArray(schema)) {
|
|
42
|
-
errors.push(`${path}: schema must be an object`);
|
|
42
|
+
errors.push(`${path}: schema must be an object`); // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
|
|
43
43
|
return false;
|
|
44
44
|
}
|
|
45
45
|
const hasType = typeof schema.type === 'string' || Array.isArray(schema.type);
|
|
@@ -48,13 +48,13 @@ function validateSchemaShape(schema, path, mode, errors) {
|
|
|
48
48
|
if (mode === 'structural' && path !== '$') {
|
|
49
49
|
return true;
|
|
50
50
|
}
|
|
51
|
-
errors.push(`${path}: missing type or combinator`);
|
|
51
|
+
errors.push(`${path}: missing type or combinator`); // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
|
|
52
52
|
}
|
|
53
53
|
if (mode === 'gemini') {
|
|
54
54
|
validateGeminiKeys(schema, path, errors);
|
|
55
55
|
}
|
|
56
56
|
if (schema.type === 'array' && schema.items === undefined) {
|
|
57
|
-
errors.push(`${path}: array schema must define items`);
|
|
57
|
+
errors.push(`${path}: array schema must define items`); // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
|
|
58
58
|
}
|
|
59
59
|
return true;
|
|
60
60
|
}
|
|
@@ -62,7 +62,7 @@ function walkSchemaProperties(props, required, path, mode, errors) {
|
|
|
62
62
|
const reqList = Array.isArray(required) ? required : [];
|
|
63
63
|
for (const req of reqList) {
|
|
64
64
|
if (typeof req === 'string' && !(req in props)) {
|
|
65
|
-
errors.push(`${path}: required key '${req}' missing from properties`);
|
|
65
|
+
errors.push(`${path}: required key '${req}' missing from properties`); // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
68
|
for (const [key, child] of Object.entries(props)) {
|
|
@@ -97,7 +97,7 @@ function validateToolWireSchema(schema, mode = 'gemini', label = 'input') {
|
|
|
97
97
|
const errors = [];
|
|
98
98
|
walkSchema(schema, '$', mode, errors);
|
|
99
99
|
if (errors.length > 0) {
|
|
100
|
-
throw new TheorumError(`Invalid tool ${label} schema: ${errors.join('; ')}`);
|
|
100
|
+
throw new TheorumError(`Invalid tool ${label} schema: ${errors.join('; ')}`); // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
|
|
101
101
|
}
|
|
102
102
|
}
|
|
103
103
|
/** Validate a tool parameter schema for provider compatibility. */
|
|
@@ -157,3 +157,20 @@ function jsonSchemaFromZod(schema, io = 'output') {
|
|
|
157
157
|
return stripUnsupportedGeminiKeys(json);
|
|
158
158
|
}
|
|
159
159
|
export { jsonSchemaFromZod, validateToolInputSchema, validateToolOutputSchema };
|
|
160
|
+
/** Strip prototype-pollution keys from provider/host tool args or host-mutated values before validation. */
|
|
161
|
+
export function plainToolInput(input) {
|
|
162
|
+
if (input === null || typeof input !== 'object') {
|
|
163
|
+
return input;
|
|
164
|
+
}
|
|
165
|
+
if (Array.isArray(input)) {
|
|
166
|
+
return input.map(plainToolInput);
|
|
167
|
+
}
|
|
168
|
+
const out = {};
|
|
169
|
+
for (const key of Object.keys(input)) {
|
|
170
|
+
if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
out[key] = plainToolInput(input[key]);
|
|
174
|
+
}
|
|
175
|
+
return out;
|
|
176
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool execute wiring around the stage spine: `pre_tool` outcomes (deny, gate,
|
|
3
|
+
* mutate, abort) and `post_tool` inject hand-off.
|
|
4
|
+
*
|
|
5
|
+
* Contract: `docs/contracts/stages.md` (tool execute pipeline).
|
|
6
|
+
*
|
|
7
|
+
* @module
|
|
8
|
+
*/
|
|
9
|
+
import type { z } from 'zod';
|
|
10
|
+
import { type StageHandler, type StageResult } from '../stages.js';
|
|
11
|
+
import type { Profile, TurnEvent, TurnHistoryMessage } from '../types.js';
|
|
12
|
+
import type { ToolCallBase } from './events.js';
|
|
13
|
+
import type { ModelToolResult, ToolContext, ToolFailure, ToolGate } from './types.js';
|
|
14
|
+
/** Stage wiring passed into `executeRegisteredTool`. */
|
|
15
|
+
export interface ToolStageSupport {
|
|
16
|
+
/** Ordered host handlers: request `onStage`, then turn/session ambient. */
|
|
17
|
+
handlers: StageHandler[];
|
|
18
|
+
/** Profile whose guardrails sanitize injects and whose inject gate applies. */
|
|
19
|
+
profile: Profile;
|
|
20
|
+
step: number;
|
|
21
|
+
history: () => readonly TurnHistoryMessage[];
|
|
22
|
+
injectAllowed: boolean;
|
|
23
|
+
injectWouldExceedMaxSteps?: boolean;
|
|
24
|
+
/** Apply sanitized inject messages immediately instead of handing them back. */
|
|
25
|
+
applyInject?: (messages: TurnHistoryMessage[]) => void;
|
|
26
|
+
host?: unknown;
|
|
27
|
+
signal?: AbortSignal;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Everything before a tool body runs, after schema + permission: tool `preTool`,
|
|
31
|
+
* host `pre_tool`, and the mutate re-parse. Each execute path maps the terminal
|
|
32
|
+
* shapes onto its own settlement; the pipeline itself lives here once.
|
|
33
|
+
*/
|
|
34
|
+
export type PreBodyOutcome = {
|
|
35
|
+
ok: true;
|
|
36
|
+
input: unknown;
|
|
37
|
+
} | {
|
|
38
|
+
ok: false;
|
|
39
|
+
kind: 'aborted';
|
|
40
|
+
aborted: true | {
|
|
41
|
+
reason?: string;
|
|
42
|
+
};
|
|
43
|
+
} | {
|
|
44
|
+
ok: false;
|
|
45
|
+
kind: 'gated';
|
|
46
|
+
gate: ToolGate;
|
|
47
|
+
} | {
|
|
48
|
+
ok: false;
|
|
49
|
+
kind: 'failed';
|
|
50
|
+
failure: ToolFailure;
|
|
51
|
+
};
|
|
52
|
+
export type PostToolStageOutcome = {
|
|
53
|
+
abort?: boolean | {
|
|
54
|
+
reason?: string;
|
|
55
|
+
};
|
|
56
|
+
/** Sanitized inject messages for the caller to apply after recording the tool result. */
|
|
57
|
+
inject?: TurnHistoryMessage[];
|
|
58
|
+
/** Host refused the result: the model gets this failure instead. */
|
|
59
|
+
deny?: {
|
|
60
|
+
code: string;
|
|
61
|
+
message: string;
|
|
62
|
+
};
|
|
63
|
+
/** Host replaced the raw output; the caller re-validates and re-projects it. */
|
|
64
|
+
mutate?: {
|
|
65
|
+
output: unknown;
|
|
66
|
+
};
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* Tool-local `preTool` (skipped when resume.granted) → host `pre_tool` →
|
|
70
|
+
* mutate re-parse. Emits the gate wire for a host confirm.
|
|
71
|
+
*/
|
|
72
|
+
export declare function runPreToolPipeline(args: {
|
|
73
|
+
tool: {
|
|
74
|
+
name: string;
|
|
75
|
+
input: {
|
|
76
|
+
safeParse: (value: unknown) => z.ZodSafeParseResult<unknown>;
|
|
77
|
+
};
|
|
78
|
+
preTool?: (input: never, ctx: ToolContext) => StageResult | undefined | Promise<StageResult | undefined>;
|
|
79
|
+
};
|
|
80
|
+
input: unknown;
|
|
81
|
+
ctx: ToolContext;
|
|
82
|
+
base: ToolCallBase;
|
|
83
|
+
stages?: ToolStageSupport;
|
|
84
|
+
}): AsyncGenerator<TurnEvent, PreBodyOutcome>;
|
|
85
|
+
/** Emit `post_tool`, run host handlers, hand back inject/abort. */
|
|
86
|
+
export declare function runPostToolStages(args: {
|
|
87
|
+
stages: ToolStageSupport;
|
|
88
|
+
toolName: string;
|
|
89
|
+
callId: string;
|
|
90
|
+
input?: unknown;
|
|
91
|
+
callNotStarted?: boolean;
|
|
92
|
+
outputRaw?: unknown;
|
|
93
|
+
outputModel?: ModelToolResult;
|
|
94
|
+
failure?: ToolFailure;
|
|
95
|
+
awaiting?: boolean;
|
|
96
|
+
/** False when there is no completed output for `mutate` to replace. */
|
|
97
|
+
mutable: boolean;
|
|
98
|
+
}): AsyncGenerator<TurnEvent, PostToolStageOutcome>;
|
|
99
|
+
/** Emit observe `pre_tool` (callNotStarted) then the tool `gate` wire. */
|
|
100
|
+
export declare function emitGateSettlement(args: {
|
|
101
|
+
base: ToolCallBase;
|
|
102
|
+
gate: ToolGate;
|
|
103
|
+
callId: string;
|
|
104
|
+
toolName: string;
|
|
105
|
+
}): AsyncGenerator<TurnEvent, void>;
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool execute wiring around the stage spine: `pre_tool` outcomes (deny, gate,
|
|
3
|
+
* mutate, abort) and `post_tool` inject hand-off.
|
|
4
|
+
*
|
|
5
|
+
* Contract: `docs/contracts/stages.md` (tool execute pipeline).
|
|
6
|
+
*
|
|
7
|
+
* @module
|
|
8
|
+
*/
|
|
9
|
+
import { lexiconText } from '../../guardrails/lexicon.js';
|
|
10
|
+
import { runStage, stageEventFields } from '../stages.js';
|
|
11
|
+
import { isGateResumeGranted } from './permission.js';
|
|
12
|
+
import { plainToolInput } from './schema.js';
|
|
13
|
+
/** Stage support when only a tool-local `preTool` is present. */
|
|
14
|
+
function defaultToolStageSupport(ctx) {
|
|
15
|
+
return {
|
|
16
|
+
handlers: [],
|
|
17
|
+
profile: ctx.profile,
|
|
18
|
+
step: ctx.turn?.step ?? 1,
|
|
19
|
+
history: () => [],
|
|
20
|
+
injectAllowed: false,
|
|
21
|
+
host: ctx.host,
|
|
22
|
+
signal: ctx.signal,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Tool-local `preTool` (skipped when resume.granted) → host `pre_tool` →
|
|
27
|
+
* mutate re-parse. Emits the gate wire for a host confirm.
|
|
28
|
+
*/
|
|
29
|
+
export async function* runPreToolPipeline(args) {
|
|
30
|
+
const { tool, ctx, base, stages } = args;
|
|
31
|
+
let toolPreTool;
|
|
32
|
+
if (tool.preTool && !isGateResumeGranted(ctx.resume)) {
|
|
33
|
+
toolPreTool = (await tool.preTool(args.input, ctx)) ?? undefined;
|
|
34
|
+
}
|
|
35
|
+
if (!stages && toolPreTool === undefined) {
|
|
36
|
+
return { ok: true, input: args.input };
|
|
37
|
+
}
|
|
38
|
+
const pre = yield* runPreToolStages({
|
|
39
|
+
stages: stages ?? defaultToolStageSupport(ctx),
|
|
40
|
+
toolName: tool.name,
|
|
41
|
+
callId: base.callId ?? '',
|
|
42
|
+
input: args.input,
|
|
43
|
+
toolPreTool,
|
|
44
|
+
});
|
|
45
|
+
if (!pre.ok) {
|
|
46
|
+
if (pre.kind === 'gated')
|
|
47
|
+
yield gateEvent(base, pre.gate);
|
|
48
|
+
return pre;
|
|
49
|
+
}
|
|
50
|
+
if (!pre.mutated)
|
|
51
|
+
return { ok: true, input: pre.input };
|
|
52
|
+
const reparsed = tool.input.safeParse(plainToolInput(pre.input));
|
|
53
|
+
if (!reparsed.success) {
|
|
54
|
+
return {
|
|
55
|
+
ok: false,
|
|
56
|
+
kind: 'failed',
|
|
57
|
+
failure: {
|
|
58
|
+
code: 'invalid_input',
|
|
59
|
+
message: lexiconText('tool.input_invalid_after_mutate'),
|
|
60
|
+
details: reparsed.error.flatten(),
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
return { ok: true, input: reparsed.data };
|
|
65
|
+
}
|
|
66
|
+
/** Emit `pre_tool`, run tool `preTool` then host handlers, map affordances to an outcome. */
|
|
67
|
+
async function* runPreToolStages(args) {
|
|
68
|
+
const { stages, toolName, callId, input, toolPreTool } = args;
|
|
69
|
+
const applied = yield* runStage({
|
|
70
|
+
stage: 'pre_tool',
|
|
71
|
+
step: stages.step,
|
|
72
|
+
history: stages.history(),
|
|
73
|
+
handlers: [...(toolPreTool !== undefined ? [() => toolPreTool] : []), ...stages.handlers],
|
|
74
|
+
guardrails: stages.profile.guardrails,
|
|
75
|
+
injectAllowed: false,
|
|
76
|
+
host: stages.host,
|
|
77
|
+
signal: stages.signal,
|
|
78
|
+
callId,
|
|
79
|
+
tool: toolName,
|
|
80
|
+
input,
|
|
81
|
+
});
|
|
82
|
+
if (applied.abort) {
|
|
83
|
+
return { ok: false, kind: 'aborted', aborted: applied.abort };
|
|
84
|
+
}
|
|
85
|
+
if (applied.deny) {
|
|
86
|
+
return { ok: false, kind: 'failed', failure: applied.deny };
|
|
87
|
+
}
|
|
88
|
+
if (applied.confirm) {
|
|
89
|
+
const gate = {
|
|
90
|
+
kind: 'confirmation',
|
|
91
|
+
tool: toolName,
|
|
92
|
+
...(applied.confirm.summary ? { summary: applied.confirm.summary } : {}),
|
|
93
|
+
};
|
|
94
|
+
yield stageEventFields('pre_tool', { callId, toolName, callNotStarted: true, gate });
|
|
95
|
+
return { ok: false, kind: 'gated', gate };
|
|
96
|
+
}
|
|
97
|
+
if (applied.mutate && 'input' in applied.mutate) {
|
|
98
|
+
return { ok: true, input: applied.mutate.input, mutated: true };
|
|
99
|
+
}
|
|
100
|
+
return { ok: true, input, mutated: false };
|
|
101
|
+
}
|
|
102
|
+
/** Emit `post_tool`, run host handlers, hand back inject/abort. */
|
|
103
|
+
export async function* runPostToolStages(args) {
|
|
104
|
+
const { stages, toolName, callId, mutable, ...call } = args;
|
|
105
|
+
const applied = yield* runStage({
|
|
106
|
+
...call,
|
|
107
|
+
stage: 'post_tool',
|
|
108
|
+
step: stages.step,
|
|
109
|
+
history: stages.history(),
|
|
110
|
+
handlers: stages.handlers,
|
|
111
|
+
guardrails: stages.profile.guardrails,
|
|
112
|
+
injectAllowed: stages.injectAllowed,
|
|
113
|
+
injectWouldExceedMaxSteps: stages.injectWouldExceedMaxSteps,
|
|
114
|
+
mutable,
|
|
115
|
+
host: stages.host,
|
|
116
|
+
signal: stages.signal,
|
|
117
|
+
callId,
|
|
118
|
+
tool: toolName,
|
|
119
|
+
});
|
|
120
|
+
const terminal = {
|
|
121
|
+
...(applied.abort !== undefined ? { abort: applied.abort } : {}),
|
|
122
|
+
...(applied.deny ? { deny: applied.deny } : {}),
|
|
123
|
+
...(applied.mutate && 'output' in applied.mutate ? { mutate: applied.mutate } : {}),
|
|
124
|
+
};
|
|
125
|
+
if (applied.inject.length === 0)
|
|
126
|
+
return terminal;
|
|
127
|
+
// Prefer returning inject for the runner to apply after recording the provider
|
|
128
|
+
// tool result (Interactions continuation must exist first). When applyInject is
|
|
129
|
+
// set the caller wants immediate apply (e.g. invokeTool).
|
|
130
|
+
if (stages.applyInject) {
|
|
131
|
+
stages.applyInject(applied.inject);
|
|
132
|
+
return terminal;
|
|
133
|
+
}
|
|
134
|
+
return { ...terminal, inject: applied.inject };
|
|
135
|
+
}
|
|
136
|
+
function gateEvent(base, gate) {
|
|
137
|
+
return {
|
|
138
|
+
type: 'tool',
|
|
139
|
+
tool: {
|
|
140
|
+
...base,
|
|
141
|
+
phase: 'gate',
|
|
142
|
+
gate,
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
/** Emit observe `pre_tool` (callNotStarted) then the tool `gate` wire. */
|
|
147
|
+
export async function* emitGateSettlement(args) {
|
|
148
|
+
yield stageEventFields('pre_tool', {
|
|
149
|
+
callId: args.callId,
|
|
150
|
+
toolName: args.toolName,
|
|
151
|
+
callNotStarted: true,
|
|
152
|
+
gate: args.gate,
|
|
153
|
+
});
|
|
154
|
+
yield gateEvent(args.base, args.gate);
|
|
155
|
+
}
|