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
|
@@ -4,19 +4,80 @@
|
|
|
4
4
|
* @module
|
|
5
5
|
*/
|
|
6
6
|
import { throwIfAborted } from '../../guardrails/error.js';
|
|
7
|
+
import { lexiconText } from '../../guardrails/lexicon.js';
|
|
8
|
+
import { resolveGuardrailPolicy } from '../../guardrails/policy.js';
|
|
7
9
|
import { sanitizeText } from '../../guardrails/sanitize.js';
|
|
10
|
+
import { checkTaintGate, composeToolText, guardToolFailureText, guardToolResult, inspectToolArguments, toolCallEvent, } from '../../guardrails/tool-result.js';
|
|
11
|
+
import { isAwaitingUserInput } from '../stages.js';
|
|
12
|
+
import { failureEvent, messageOf, startToolExecution, toolEvent } from './events.js';
|
|
13
|
+
import { checkPermission, isGateResumeDenied, isResumeContinuation } from './permission.js';
|
|
8
14
|
import { getTool } from './registry.js';
|
|
15
|
+
import { executeHttpTool, executeMcpTool, modelResultFromOutput, parseToolOutput, } from './remote.js';
|
|
9
16
|
import { promoteLoadedTools } from './resolve.js';
|
|
17
|
+
import { plainToolInput } from './schema.js';
|
|
18
|
+
import { emitGateSettlement, runPostToolStages, runPreToolPipeline, } from './stage-run.js';
|
|
19
|
+
export { checkPermission, isGateResumeGranted, isResumeContinuation, permissionGranted, } from './permission.js';
|
|
20
|
+
/** Map a registered tool's type onto the origin its bytes carry. */
|
|
21
|
+
function originOfTool(type) {
|
|
22
|
+
if (type === 'http')
|
|
23
|
+
return 'http';
|
|
24
|
+
if (type === 'mcp')
|
|
25
|
+
return 'mcp';
|
|
26
|
+
if (type === 'builtin')
|
|
27
|
+
return 'builtin';
|
|
28
|
+
return 'local';
|
|
29
|
+
}
|
|
30
|
+
function provenanceFor(tool, depth = 1) {
|
|
31
|
+
return { origin: originOfTool(tool.type), tool: tool.name, depth };
|
|
32
|
+
}
|
|
10
33
|
function isStreamHandler(handler) {
|
|
11
34
|
return (typeof handler === 'function' &&
|
|
12
35
|
Object.prototype.toString.call(handler) === '[object AsyncGeneratorFunction]');
|
|
13
36
|
}
|
|
14
|
-
|
|
15
|
-
|
|
37
|
+
const MEDIA_PART_TYPES = new Set(['image', 'audio', 'video', 'document']);
|
|
38
|
+
function isRecord(value) {
|
|
39
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
40
|
+
}
|
|
41
|
+
/** Validate host-emitted InteractionPart shapes; drop invalid entries. */
|
|
42
|
+
export function coerceToolResultParts(raw) {
|
|
43
|
+
if (!Array.isArray(raw) || raw.length === 0)
|
|
44
|
+
return undefined;
|
|
45
|
+
const parts = [];
|
|
46
|
+
for (const item of raw) {
|
|
47
|
+
if (!isRecord(item) || typeof item.type !== 'string')
|
|
48
|
+
continue;
|
|
49
|
+
if (item.type === 'text' && typeof item.text === 'string') {
|
|
50
|
+
parts.push({ type: 'text', text: item.text });
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (MEDIA_PART_TYPES.has(item.type) &&
|
|
54
|
+
typeof item.mimeType === 'string' &&
|
|
55
|
+
item.mimeType.trim() &&
|
|
56
|
+
typeof item.data === 'string' &&
|
|
57
|
+
item.data.length > 0) {
|
|
58
|
+
parts.push({
|
|
59
|
+
type: item.type,
|
|
60
|
+
mimeType: item.mimeType,
|
|
61
|
+
data: item.data,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return parts.length > 0 ? parts : undefined;
|
|
66
|
+
}
|
|
67
|
+
/** Copy tool output for model `data`, omitting media `parts`. */
|
|
68
|
+
export function leanToolResultData(output) {
|
|
69
|
+
if (!isRecord(output))
|
|
70
|
+
return output;
|
|
71
|
+
const { parts: _parts, ...rest } = output;
|
|
72
|
+
return rest;
|
|
16
73
|
}
|
|
74
|
+
/** @deprecated Gates use `ToolGate` / `phase: 'gate'`. Kept for type narrowing during migration. */
|
|
17
75
|
export function isToolPause(value) {
|
|
18
76
|
return ('kind' in value &&
|
|
19
|
-
(value.kind === 'interactive' ||
|
|
77
|
+
(value.kind === 'interactive' ||
|
|
78
|
+
value.kind === 'confirmation' ||
|
|
79
|
+
value.kind === 'permission' ||
|
|
80
|
+
value.kind === 'auth'));
|
|
20
81
|
}
|
|
21
82
|
export function* yieldHandlerSideEvent(base, event) {
|
|
22
83
|
if (event.kind === 'progress') {
|
|
@@ -50,214 +111,304 @@ async function* runHandler(handler, input, ctx, base) {
|
|
|
50
111
|
const output = await handler(input, ctx);
|
|
51
112
|
return output;
|
|
52
113
|
}
|
|
53
|
-
export function permissionGranted(toolName, sessionPermissions) {
|
|
54
|
-
if (!sessionPermissions) {
|
|
55
|
-
return false;
|
|
56
|
-
}
|
|
57
|
-
return sessionPermissions.includes('*') || sessionPermissions.includes(toolName);
|
|
58
|
-
}
|
|
59
|
-
export function checkPermission(toolName, permission, sessionPermissions, resume) {
|
|
60
|
-
if (permission === 'auto') {
|
|
61
|
-
return null;
|
|
62
|
-
}
|
|
63
|
-
if (permission === 'always_confirm') {
|
|
64
|
-
if (resume?.granted === true) {
|
|
65
|
-
return null;
|
|
66
|
-
}
|
|
67
|
-
return {
|
|
68
|
-
kind: 'permission',
|
|
69
|
-
tool: toolName,
|
|
70
|
-
permission,
|
|
71
|
-
input: {},
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
if (permissionGranted(toolName, sessionPermissions)) {
|
|
75
|
-
return null;
|
|
76
|
-
}
|
|
77
|
-
return {
|
|
78
|
-
kind: 'permission',
|
|
79
|
-
tool: toolName,
|
|
80
|
-
permission,
|
|
81
|
-
input: {},
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
function toolEvent(base, patch) {
|
|
85
|
-
return {
|
|
86
|
-
type: 'tool',
|
|
87
|
-
tool: { ...base, ...patch },
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
function failureEvent(base, failure) {
|
|
91
|
-
return toolEvent(base, { phase: 'error', failure });
|
|
92
|
-
}
|
|
93
114
|
export function projectForModel(tool, output) {
|
|
94
115
|
if (tool.exposeToModel === false) {
|
|
95
116
|
return { finding: 'Completed.' };
|
|
96
117
|
}
|
|
118
|
+
if (isAwaitingUserInput(output)) {
|
|
119
|
+
return {
|
|
120
|
+
finding: lexiconText('tool.awaiting_user', { kind: output.kind, prompt: output.prompt }),
|
|
121
|
+
data: leanToolResultData(output),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
97
124
|
const finding = typeof output === 'object' && output !== null && 'finding' in output
|
|
98
125
|
? String(output.finding)
|
|
99
126
|
: JSON.stringify(output);
|
|
127
|
+
const parts = isRecord(output) && 'parts' in output ? coerceToolResultParts(output.parts) : undefined;
|
|
100
128
|
return {
|
|
101
129
|
finding: sanitizeText(finding),
|
|
102
|
-
data: output,
|
|
130
|
+
data: leanToolResultData(output),
|
|
131
|
+
...(parts ? { parts } : {}),
|
|
103
132
|
};
|
|
104
133
|
}
|
|
105
|
-
/**
|
|
134
|
+
/**
|
|
135
|
+
* Format model-facing tool output for provider history continuation.
|
|
136
|
+
*
|
|
137
|
+
* Text projection only — never embeds `parts[].data`; media travels on
|
|
138
|
+
* `TurnHistoryMessage.parts` and adapters wire it from there.
|
|
139
|
+
*
|
|
140
|
+
* `executeRegisteredTool` guards at the boundary and leaves `modelText` behind, so
|
|
141
|
+
* the common path returns already-fenced text. A result recorded elsewhere — a
|
|
142
|
+
* host replaying a transcript — is guarded here instead, under full detection.
|
|
143
|
+
*/
|
|
106
144
|
export function formatToolResult(result) {
|
|
107
|
-
if (result.
|
|
108
|
-
return
|
|
145
|
+
if (result.modelText !== undefined) {
|
|
146
|
+
return result.modelText;
|
|
109
147
|
}
|
|
110
|
-
return sanitizeText(result.finding);
|
|
148
|
+
return sanitizeText(composeToolText(result.finding, result.data));
|
|
111
149
|
}
|
|
112
|
-
/**
|
|
113
|
-
|
|
150
|
+
/**
|
|
151
|
+
* Format a tool failure for provider history — structured so the model (or host)
|
|
152
|
+
* sees the code.
|
|
153
|
+
*
|
|
154
|
+
* The message is remote-authored on HTTP and MCP tools, so it is redacted before
|
|
155
|
+
* the kernel frames it as a system report.
|
|
156
|
+
*/
|
|
157
|
+
export function formatToolFailureForModel(failure, provenance, policy = resolveGuardrailPolicy(undefined)) {
|
|
158
|
+
const safe = provenance
|
|
159
|
+
? guardToolFailureText(failure.message, provenance, policy).text
|
|
160
|
+
: sanitizeText(failure.message);
|
|
114
161
|
return {
|
|
115
|
-
finding: `Tool error (${failure.code}): ${
|
|
162
|
+
finding: `Tool error (${failure.code}): ${safe}`,
|
|
116
163
|
data: {
|
|
117
164
|
ok: false,
|
|
118
165
|
code: failure.code,
|
|
119
|
-
message:
|
|
166
|
+
message: safe,
|
|
120
167
|
...(failure.details !== undefined ? { details: failure.details } : {}),
|
|
121
168
|
},
|
|
122
169
|
};
|
|
123
170
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
if (!parsed.success) {
|
|
129
|
-
yield failureEvent(base, {
|
|
130
|
-
code: 'invalid_input',
|
|
131
|
-
message: 'Tool input validation failed',
|
|
132
|
-
details: parsed.error.flatten(),
|
|
133
|
-
});
|
|
134
|
-
return { ok: false };
|
|
171
|
+
/** True when this tool is the profile's T2 loader: its output drives the snapshot. */
|
|
172
|
+
function loadsT2(tool, ctx) {
|
|
173
|
+
if (ctx.profile.type === 'speech' || ctx.profile.type === 'live' || ctx.profile.type === 'host') {
|
|
174
|
+
return false;
|
|
135
175
|
}
|
|
136
|
-
return
|
|
176
|
+
return ctx.profile.tools.t2Loader === tool.name;
|
|
137
177
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
return undefined;
|
|
142
|
-
const input = parsed.data;
|
|
143
|
-
const permissionPause = checkPermission(tool.name, tool.permission, ctx.sessionPermissions, ctx.resume);
|
|
144
|
-
if (permissionPause) {
|
|
145
|
-
permissionPause.input = input;
|
|
146
|
-
yield toolEvent(base, { phase: 'pause', pause: permissionPause });
|
|
147
|
-
return undefined;
|
|
178
|
+
function applyT2LoaderPromotion(tool, checkedData, ctx, snapshot) {
|
|
179
|
+
if (!loadsT2(tool, ctx)) {
|
|
180
|
+
return { ok: true, output: checkedData };
|
|
148
181
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
});
|
|
158
|
-
return undefined;
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
catch (err) {
|
|
162
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
163
|
-
yield failureEvent(base, {
|
|
164
|
-
code: 'not_authorized',
|
|
165
|
-
message: `Authorization failed for '${tool.name}': ${msg}`,
|
|
166
|
-
});
|
|
167
|
-
return undefined;
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
if (tool.preflight) {
|
|
171
|
-
const pre = await tool.preflight(input, ctx);
|
|
172
|
-
if (pre) {
|
|
173
|
-
if (isToolPause(pre)) {
|
|
174
|
-
const pausePayload = { ...pre, input: pre.input ?? input };
|
|
175
|
-
yield toolEvent(base, { phase: 'pause', pause: pausePayload });
|
|
176
|
-
return undefined;
|
|
177
|
-
}
|
|
178
|
-
yield failureEvent(base, pre);
|
|
179
|
-
return undefined;
|
|
180
|
-
}
|
|
182
|
+
if (!snapshot) {
|
|
183
|
+
return {
|
|
184
|
+
ok: false,
|
|
185
|
+
failure: {
|
|
186
|
+
code: 'invalid_output',
|
|
187
|
+
message: lexiconText('tool.t2_loader_needs_snapshot', { tool: tool.name }),
|
|
188
|
+
},
|
|
189
|
+
};
|
|
181
190
|
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
191
|
+
const loaded = extractLoadedIds(checkedData);
|
|
192
|
+
if (!loaded) {
|
|
193
|
+
return {
|
|
194
|
+
ok: false,
|
|
195
|
+
failure: {
|
|
196
|
+
code: 'invalid_output',
|
|
197
|
+
message: lexiconText('tool.t2_loader_shape', { tool: tool.name }),
|
|
198
|
+
},
|
|
189
199
|
};
|
|
190
|
-
yield toolEvent(base, { phase: 'pause', pause: interactivePause });
|
|
191
|
-
return undefined;
|
|
192
200
|
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
201
|
+
const { promoted, failure: promoteFailure } = promoteLoadedTools(snapshot, loaded, ctx.profile);
|
|
202
|
+
if (promoteFailure) {
|
|
203
|
+
return { ok: false, failure: promoteFailure };
|
|
204
|
+
}
|
|
205
|
+
const finalOutput = { ...checkedData, loaded: promoted };
|
|
206
|
+
const rechecked = tool.output.safeParse(finalOutput);
|
|
207
|
+
if (!rechecked.success) {
|
|
208
|
+
return {
|
|
209
|
+
ok: false,
|
|
210
|
+
failure: {
|
|
203
211
|
code: 'invalid_output',
|
|
204
|
-
message: '
|
|
205
|
-
details:
|
|
206
|
-
}
|
|
207
|
-
|
|
212
|
+
message: lexiconText('tool.t2_loader_output_invalid'),
|
|
213
|
+
details: rechecked.error.flatten(),
|
|
214
|
+
},
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
return { ok: true, output: rechecked.data };
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Settle a tool call, once, for every transport: guard the provisional result,
|
|
221
|
+
* run `post_tool`, apply a host `deny` or `mutate` (re-projected and re-guarded),
|
|
222
|
+
* then emit the single terminal `tool` event with what the model actually gets.
|
|
223
|
+
* `reproject` is absent when the kernel owns the output (the T2 loader), which
|
|
224
|
+
* makes `mutate` a warning instead of a replacement.
|
|
225
|
+
*/
|
|
226
|
+
async function* settleToolCall(args) {
|
|
227
|
+
const { base, toolName, callId, input, stages, provisional, guard, reproject } = args;
|
|
228
|
+
let modelResult = yield* guard(provisional.modelResult);
|
|
229
|
+
let failure = 'failure' in provisional ? provisional.failure : undefined;
|
|
230
|
+
let outputRaw = 'outputRaw' in provisional ? provisional.outputRaw : undefined;
|
|
231
|
+
const callNotStarted = 'callNotStarted' in provisional ? provisional.callNotStarted : undefined;
|
|
232
|
+
let awaiting = !failure && isAwaitingUserInput(outputRaw);
|
|
233
|
+
let post;
|
|
234
|
+
if (stages) {
|
|
235
|
+
// Do not applyInject here — the text runner records the tool result first.
|
|
236
|
+
const { applyInject: _apply, ...rest } = stages;
|
|
237
|
+
post = yield* runPostToolStages({
|
|
238
|
+
stages: rest,
|
|
239
|
+
toolName,
|
|
240
|
+
callId,
|
|
241
|
+
input,
|
|
242
|
+
callNotStarted,
|
|
243
|
+
outputRaw,
|
|
244
|
+
outputModel: modelResult,
|
|
245
|
+
failure,
|
|
246
|
+
awaiting: awaiting || undefined,
|
|
247
|
+
mutable: !failure && reproject !== undefined,
|
|
248
|
+
});
|
|
249
|
+
if (post.deny) {
|
|
250
|
+
failure = { code: post.deny.code, message: post.deny.message };
|
|
251
|
+
modelResult = yield* guard(formatToolFailureForModel(failure));
|
|
252
|
+
outputRaw = undefined;
|
|
253
|
+
awaiting = false;
|
|
208
254
|
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
if (
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
return undefined;
|
|
217
|
-
}
|
|
218
|
-
const loaded = extractLoadedIds(checked.data);
|
|
219
|
-
if (!loaded) {
|
|
220
|
-
yield failureEvent(base, {
|
|
221
|
-
code: 'invalid_output',
|
|
222
|
-
message: `T2 loader '${tool.name}' must return { loaded: string[] }`,
|
|
223
|
-
});
|
|
224
|
-
return undefined;
|
|
255
|
+
else if (post.mutate && reproject) {
|
|
256
|
+
const next = reproject(plainToolInput(post.mutate.output));
|
|
257
|
+
if (next.ok) {
|
|
258
|
+
failure = undefined;
|
|
259
|
+
outputRaw = next.outputRaw;
|
|
260
|
+
modelResult = yield* guard(next.modelResult);
|
|
261
|
+
awaiting = isAwaitingUserInput(outputRaw);
|
|
225
262
|
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
yield
|
|
229
|
-
|
|
263
|
+
else {
|
|
264
|
+
failure = next.failure;
|
|
265
|
+
modelResult = yield* guard(formatToolFailureForModel(next.failure));
|
|
266
|
+
outputRaw = undefined;
|
|
267
|
+
awaiting = false;
|
|
230
268
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
yield failure
|
|
272
|
+
? failureEvent(base, failure)
|
|
273
|
+
: toolEvent(base, { phase: 'complete', output: outputRaw });
|
|
274
|
+
return {
|
|
275
|
+
modelResult,
|
|
276
|
+
...(failure ? { failure } : { outputRaw }),
|
|
277
|
+
...(callNotStarted ? { callNotStarted: true } : {}),
|
|
278
|
+
...(awaiting ? { awaiting: true } : {}),
|
|
279
|
+
...(post?.abort !== undefined ? { aborted: post.abort } : {}),
|
|
280
|
+
...(post?.inject?.length ? { pendingInject: post.inject } : {}),
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
/** Settle a call that failed before or during its body. */
|
|
284
|
+
function settleToolFailure(guard, base, failure, stages, args) {
|
|
285
|
+
return settleToolCall({
|
|
286
|
+
base,
|
|
287
|
+
toolName: args.toolName,
|
|
288
|
+
callId: args.callId,
|
|
289
|
+
input: args.input,
|
|
290
|
+
stages,
|
|
291
|
+
guard,
|
|
292
|
+
provisional: {
|
|
293
|
+
failure,
|
|
294
|
+
modelResult: formatToolFailureForModel(failure),
|
|
295
|
+
...(args.callNotStarted ? { callNotStarted: true } : {}),
|
|
296
|
+
},
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
/** The one guard every model-facing tool result passes through, bound to this call. */
|
|
300
|
+
function resultGuard(tool, ctx, snapshot) {
|
|
301
|
+
const provenance = provenanceFor(tool);
|
|
302
|
+
const policy = resolveGuardrailPolicy(ctx.profile.guardrails);
|
|
303
|
+
const callableTools = snapshot?.executable ?? [];
|
|
304
|
+
return (result) => guardResult(result, provenance, policy, callableTools);
|
|
305
|
+
}
|
|
306
|
+
/** Tool `preTool` + host `pre_tool` + mutate re-parse, mapped onto a function-tool settlement. */
|
|
307
|
+
async function* runFunctionPreBodyStages(args) {
|
|
308
|
+
const { tool, base, stages, guard } = args;
|
|
309
|
+
const pre = yield* runPreToolPipeline(args);
|
|
310
|
+
if (pre.ok)
|
|
311
|
+
return pre;
|
|
312
|
+
if (pre.kind === 'aborted') {
|
|
313
|
+
return { aborted: pre.aborted, callNotStarted: true };
|
|
314
|
+
}
|
|
315
|
+
if (pre.kind === 'gated') {
|
|
316
|
+
return { gated: pre.gate, callNotStarted: true };
|
|
317
|
+
}
|
|
318
|
+
return yield* settleToolFailure(guard, base, pre.failure, stages, {
|
|
319
|
+
toolName: tool.name,
|
|
320
|
+
callId: base.callId ?? '',
|
|
321
|
+
input: args.input,
|
|
322
|
+
callNotStarted: true,
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
function makeReproject(parse, project) {
|
|
326
|
+
return (output) => {
|
|
327
|
+
const checked = parse(output);
|
|
328
|
+
if (!checked.success) {
|
|
329
|
+
return {
|
|
330
|
+
ok: false,
|
|
331
|
+
failure: {
|
|
235
332
|
code: 'invalid_output',
|
|
236
|
-
message: '
|
|
237
|
-
details:
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
}
|
|
241
|
-
finalOutput = rechecked.data;
|
|
333
|
+
message: lexiconText('tool.output_invalid_after_mutate'),
|
|
334
|
+
details: checked.error.flatten(),
|
|
335
|
+
},
|
|
336
|
+
};
|
|
242
337
|
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
338
|
+
return { ok: true, outputRaw: checked.data, modelResult: project(checked.data) };
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
export async function* executeFunction(tool, rawInput, ctx, base, snapshot, stages) {
|
|
342
|
+
const guard = resultGuard(tool, ctx, snapshot);
|
|
343
|
+
const callId = base.callId ?? '';
|
|
344
|
+
const parsed = yield* startToolExecution(tool, rawInput, ctx, base);
|
|
345
|
+
if (!parsed.ok) {
|
|
346
|
+
return yield* settleToolFailure(guard, base, { code: 'invalid_input', message: lexiconText('tool.input_invalid') }, stages, { toolName: tool.name, callId, callNotStarted: true });
|
|
347
|
+
}
|
|
348
|
+
let input = parsed.data;
|
|
349
|
+
const permissionGate = checkPermission(tool.name, tool.permission, ctx.sessionPermissions, ctx.resume);
|
|
350
|
+
if (permissionGate) {
|
|
351
|
+
yield* emitGateSettlement({ base, gate: permissionGate, callId, toolName: tool.name });
|
|
352
|
+
return { gated: permissionGate, callNotStarted: true };
|
|
353
|
+
}
|
|
354
|
+
throwIfAborted(ctx.signal);
|
|
355
|
+
const preBody = yield* runFunctionPreBodyStages({ tool, input, ctx, base, stages, guard });
|
|
356
|
+
if (!('ok' in preBody)) {
|
|
357
|
+
return preBody;
|
|
358
|
+
}
|
|
359
|
+
input = preBody.input;
|
|
360
|
+
throwIfAborted(ctx.signal);
|
|
361
|
+
const fail = (failure) => settleToolFailure(guard, base, failure, stages, { toolName: tool.name, callId, input });
|
|
362
|
+
let output;
|
|
363
|
+
try {
|
|
364
|
+
output = yield* runHandler(tool.handler, input, ctx, base);
|
|
246
365
|
}
|
|
247
366
|
catch (err) {
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
367
|
+
return yield* fail({ code: 'handler_error', message: messageOf(err) });
|
|
368
|
+
}
|
|
369
|
+
if (output === undefined) {
|
|
370
|
+
return yield* fail({ code: 'invalid_output', message: lexiconText('tool.handler_no_output') });
|
|
251
371
|
}
|
|
372
|
+
const checked = tool.output.safeParse(output);
|
|
373
|
+
if (!checked.success) {
|
|
374
|
+
return yield* fail({
|
|
375
|
+
code: 'invalid_output',
|
|
376
|
+
message: lexiconText('tool.output_invalid'),
|
|
377
|
+
details: checked.error.flatten(),
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
const promoted = applyT2LoaderPromotion(tool, checked.data, ctx, snapshot);
|
|
381
|
+
if (!promoted.ok) {
|
|
382
|
+
return yield* fail(promoted.failure);
|
|
383
|
+
}
|
|
384
|
+
// The T2 loader's output drives the snapshot; the kernel owns it, so no mutate.
|
|
385
|
+
const ownsOutput = loadsT2(tool, ctx);
|
|
386
|
+
return yield* settleToolCall({
|
|
387
|
+
base,
|
|
388
|
+
toolName: tool.name,
|
|
389
|
+
callId,
|
|
390
|
+
input,
|
|
391
|
+
stages,
|
|
392
|
+
guard,
|
|
393
|
+
provisional: {
|
|
394
|
+
outputRaw: promoted.output,
|
|
395
|
+
modelResult: projectForModel(tool, promoted.output),
|
|
396
|
+
},
|
|
397
|
+
...(ownsOutput
|
|
398
|
+
? {}
|
|
399
|
+
: {
|
|
400
|
+
reproject: makeReproject((v) => tool.output.safeParse(v), (data) => projectForModel(tool, data)),
|
|
401
|
+
}),
|
|
402
|
+
});
|
|
252
403
|
}
|
|
253
404
|
export function notLoadedMessage(tool) {
|
|
254
405
|
if (tool.loadTier === 'T1') {
|
|
255
|
-
return
|
|
406
|
+
return lexiconText('tool.not_wired_t1', { tool: tool.name });
|
|
256
407
|
}
|
|
257
408
|
if (tool.loadTier === 'T2') {
|
|
258
|
-
return
|
|
409
|
+
return lexiconText('tool.not_loaded_t2', { tool: tool.name });
|
|
259
410
|
}
|
|
260
|
-
return
|
|
411
|
+
return lexiconText('tool.not_visible', { tool: tool.name });
|
|
261
412
|
}
|
|
262
413
|
export function extractLoadedIds(output) {
|
|
263
414
|
if (!output || typeof output !== 'object' || Array.isArray(output)) {
|
|
@@ -269,41 +420,84 @@ export function extractLoadedIds(output) {
|
|
|
269
420
|
}
|
|
270
421
|
return loaded;
|
|
271
422
|
}
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
if (input === null || typeof input !== 'object') {
|
|
275
|
-
return input;
|
|
276
|
-
}
|
|
277
|
-
if (Array.isArray(input)) {
|
|
278
|
-
return input.map(plainToolInput);
|
|
279
|
-
}
|
|
280
|
-
const out = {};
|
|
281
|
-
for (const key of Object.keys(input)) {
|
|
282
|
-
if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
|
|
283
|
-
continue;
|
|
284
|
-
}
|
|
285
|
-
out[key] = plainToolInput(input[key]);
|
|
286
|
-
}
|
|
287
|
-
return out;
|
|
423
|
+
function earlyFailure(failure) {
|
|
424
|
+
return { failure, callNotStarted: true, modelResult: formatToolFailureForModel(failure) };
|
|
288
425
|
}
|
|
289
426
|
export async function* executeBuiltin(tool, ctx, base, snapshot) {
|
|
290
427
|
yield toolEvent(base, { phase: 'running' });
|
|
291
428
|
throwIfAborted(ctx.signal);
|
|
292
429
|
if (!snapshot.builtins.includes(tool.name)) {
|
|
293
|
-
|
|
430
|
+
const failure = {
|
|
294
431
|
code: 'not_loaded',
|
|
295
|
-
message:
|
|
296
|
-
}
|
|
297
|
-
|
|
432
|
+
message: lexiconText('tool.builtin_not_enabled', { tool: tool.name }),
|
|
433
|
+
};
|
|
434
|
+
yield failureEvent(base, failure);
|
|
435
|
+
return earlyFailure(failure);
|
|
298
436
|
}
|
|
299
|
-
|
|
437
|
+
const failure = {
|
|
300
438
|
code: 'provider_native',
|
|
301
|
-
message:
|
|
302
|
-
}
|
|
439
|
+
message: lexiconText('tool.provider_native', { tool: tool.name }),
|
|
440
|
+
};
|
|
441
|
+
yield failureEvent(base, failure);
|
|
442
|
+
return earlyFailure(failure);
|
|
443
|
+
}
|
|
444
|
+
/** Snapshot / allowlist / load-tier checks before body execution. */
|
|
445
|
+
function registeredEligibilityFailure(args) {
|
|
446
|
+
const { tool, profile, name, resume, snapshot } = args;
|
|
447
|
+
if (profile.type === 'speech' || !profile.tools.allow.includes(name)) {
|
|
448
|
+
return {
|
|
449
|
+
code: 'not_allowed',
|
|
450
|
+
message: lexiconText('tool.not_allowed', { tool: name, profile: profile.id }),
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
if (!snapshot)
|
|
454
|
+
return undefined;
|
|
455
|
+
const continuing = isResumeContinuation(resume);
|
|
456
|
+
if (!continuing && !snapshot.gated.includes(name)) {
|
|
457
|
+
return { code: 'not_gated', message: lexiconText('tool.not_eligible', { tool: name }) };
|
|
458
|
+
}
|
|
459
|
+
if (!snapshot.visible.includes(name)) {
|
|
460
|
+
const skipLoadCheck = continuing && tool.loadTier === 'T0';
|
|
461
|
+
if (!skipLoadCheck) {
|
|
462
|
+
return { code: 'not_loaded', message: notLoadedMessage(tool) };
|
|
463
|
+
}
|
|
464
|
+
}
|
|
303
465
|
return undefined;
|
|
304
466
|
}
|
|
467
|
+
async function* settleRemoteOutcome(args) {
|
|
468
|
+
const { outcome, tool, base, callId, safeInput, stages, guard } = args;
|
|
469
|
+
const name = tool.name;
|
|
470
|
+
if (outcome.kind === 'gated') {
|
|
471
|
+
if (outcome.gate.kind === 'confirmation') {
|
|
472
|
+
return { gated: outcome.gate, callNotStarted: true };
|
|
473
|
+
}
|
|
474
|
+
yield* emitGateSettlement({ base, gate: outcome.gate, callId, toolName: name });
|
|
475
|
+
return { gated: outcome.gate, callNotStarted: true };
|
|
476
|
+
}
|
|
477
|
+
if (outcome.kind === 'aborted') {
|
|
478
|
+
return { aborted: outcome.aborted, callNotStarted: true };
|
|
479
|
+
}
|
|
480
|
+
if (outcome.kind === 'failed') {
|
|
481
|
+
return yield* settleToolFailure(guard, base, outcome.failure, stages, {
|
|
482
|
+
toolName: name,
|
|
483
|
+
callId,
|
|
484
|
+
input: safeInput,
|
|
485
|
+
callNotStarted: outcome.callNotStarted,
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
return yield* settleToolCall({
|
|
489
|
+
base,
|
|
490
|
+
toolName: name,
|
|
491
|
+
callId,
|
|
492
|
+
input: safeInput,
|
|
493
|
+
stages,
|
|
494
|
+
guard,
|
|
495
|
+
provisional: { outputRaw: outcome.outputRaw, modelResult: outcome.modelResult },
|
|
496
|
+
reproject: makeReproject((v) => parseToolOutput(tool.output, v), modelResultFromOutput),
|
|
497
|
+
});
|
|
498
|
+
}
|
|
305
499
|
export async function* executeRegisteredTool(args) {
|
|
306
|
-
const { profile, name, input, callId, ctx, snapshot } = args;
|
|
500
|
+
const { profile, name, input, callId, ctx, snapshot, stages } = args;
|
|
307
501
|
const tool = getTool(name);
|
|
308
502
|
const safeInput = plainToolInput(input);
|
|
309
503
|
const base = {
|
|
@@ -314,50 +508,107 @@ export async function* executeRegisteredTool(args) {
|
|
|
314
508
|
: { value: safeInput },
|
|
315
509
|
};
|
|
316
510
|
if (!tool) {
|
|
317
|
-
|
|
318
|
-
|
|
511
|
+
const failure = {
|
|
512
|
+
code: 'unknown_tool',
|
|
513
|
+
message: lexiconText('tool.not_registered', { tool: name }),
|
|
514
|
+
};
|
|
515
|
+
yield failureEvent(base, failure);
|
|
516
|
+
return earlyFailure(failure);
|
|
319
517
|
}
|
|
320
518
|
if (tool.type === 'builtin') {
|
|
321
519
|
if (!snapshot) {
|
|
322
|
-
|
|
520
|
+
const failure = {
|
|
323
521
|
code: 'provider_native',
|
|
324
|
-
message:
|
|
325
|
-
}
|
|
326
|
-
|
|
522
|
+
message: lexiconText('tool.builtin_needs_snapshot', { tool: name }),
|
|
523
|
+
};
|
|
524
|
+
yield failureEvent(base, failure);
|
|
525
|
+
return earlyFailure(failure);
|
|
327
526
|
}
|
|
328
527
|
const fullCtx = { ...ctx, callId, profile };
|
|
329
528
|
return yield* executeBuiltin(tool, fullCtx, base, snapshot);
|
|
330
529
|
}
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
530
|
+
const eligibility = registeredEligibilityFailure({
|
|
531
|
+
tool,
|
|
532
|
+
profile,
|
|
533
|
+
name,
|
|
534
|
+
resume: ctx.resume,
|
|
535
|
+
snapshot,
|
|
536
|
+
});
|
|
537
|
+
if (eligibility) {
|
|
538
|
+
yield failureEvent(base, eligibility);
|
|
539
|
+
return earlyFailure(eligibility);
|
|
337
540
|
}
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
yield failureEvent(base, {
|
|
342
|
-
code: 'not_gated',
|
|
343
|
-
message: `Tool '${name}' is not eligible on this turn (allow/path)`,
|
|
344
|
-
});
|
|
345
|
-
return undefined;
|
|
346
|
-
}
|
|
347
|
-
if (!snapshot.visible.includes(name)) {
|
|
348
|
-
const skipLoadCheck = continuing && tool.loadTier === 'T0';
|
|
349
|
-
if (!skipLoadCheck) {
|
|
350
|
-
yield failureEvent(base, {
|
|
351
|
-
code: 'not_loaded',
|
|
352
|
-
message: notLoadedMessage(tool),
|
|
353
|
-
});
|
|
354
|
-
return undefined;
|
|
355
|
-
}
|
|
356
|
-
}
|
|
541
|
+
// Host denied after a gate — synthetic failure + post_tool, no body.
|
|
542
|
+
if (isGateResumeDenied(ctx.resume)) {
|
|
543
|
+
return yield* settleToolFailure(resultGuard(tool, { ...ctx, callId, profile }, snapshot), base, { code: 'denied', message: lexiconText('session.tool_denied', { tool: name }) }, stages, { toolName: name, callId, input: safeInput, callNotStarted: true });
|
|
357
544
|
}
|
|
358
545
|
const fullCtx = { ...ctx, callId, profile };
|
|
359
|
-
|
|
546
|
+
const policy = resolveGuardrailPolicy(profile.guardrails);
|
|
547
|
+
const provenance = provenanceFor(tool);
|
|
548
|
+
const argEvent = toolCallEvent(inspectToolArguments(safeInput, policy), provenance);
|
|
549
|
+
if (argEvent) {
|
|
550
|
+
yield { type: 'guardrail', guardrail: argEvent };
|
|
551
|
+
}
|
|
552
|
+
const taintVerdict = checkTaintGate(ctx.turn?.taint, tool.access, policy);
|
|
553
|
+
const taintEvent = toolCallEvent(taintVerdict, provenance);
|
|
554
|
+
if (taintEvent) {
|
|
555
|
+
yield { type: 'guardrail', guardrail: taintEvent };
|
|
556
|
+
}
|
|
557
|
+
if (taintVerdict.action === 'block') {
|
|
558
|
+
const failure = {
|
|
559
|
+
code: 'tainted_turn',
|
|
560
|
+
message: taintVerdict.rejection,
|
|
561
|
+
};
|
|
562
|
+
yield failureEvent(base, failure);
|
|
563
|
+
return earlyFailure(failure);
|
|
564
|
+
}
|
|
565
|
+
return yield* settleByType(tool, safeInput, fullCtx, base, snapshot, stages);
|
|
566
|
+
}
|
|
567
|
+
/** Run the body for the tool's transport and settle it through `settleToolCall`. */
|
|
568
|
+
async function* settleByType(tool, safeInput, fullCtx, base, snapshot, stages) {
|
|
569
|
+
const { name } = tool;
|
|
570
|
+
if (tool.type === 'function') {
|
|
571
|
+
return yield* executeFunction(tool, safeInput, fullCtx, base, snapshot, stages);
|
|
572
|
+
}
|
|
573
|
+
if (tool.type !== 'http' && tool.type !== 'mcp') {
|
|
574
|
+
return earlyFailure({
|
|
575
|
+
code: 'unknown_tool',
|
|
576
|
+
message: lexiconText('tool.unsupported_type', { tool: name }),
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
// HTTP / MCP: schema → permission → auth → preTool → body (inside remote).
|
|
580
|
+
const remoteOutcome = tool.type === 'http'
|
|
581
|
+
? yield* executeHttpTool(tool, safeInput, fullCtx, base, stages)
|
|
582
|
+
: yield* executeMcpTool(tool, safeInput, fullCtx, base, stages);
|
|
583
|
+
return yield* settleRemoteOutcome({
|
|
584
|
+
outcome: remoteOutcome,
|
|
585
|
+
tool,
|
|
586
|
+
base,
|
|
587
|
+
callId: base.callId ?? '',
|
|
588
|
+
safeInput,
|
|
589
|
+
stages,
|
|
590
|
+
guard: resultGuard(tool, fullCtx, snapshot),
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
/**
|
|
594
|
+
* Guard a tool result before it becomes model context.
|
|
595
|
+
*
|
|
596
|
+
* Every tool returns through here, so the fence, the redaction, and the
|
|
597
|
+
* provenance label are applied once and cannot be skipped by adding a tool type.
|
|
598
|
+
*/
|
|
599
|
+
function* guardResult(result, provenance, policy, callableTools) {
|
|
600
|
+
const guarded = guardToolResult(result.finding, result.data, provenance, policy, callableTools);
|
|
601
|
+
if (guarded.event) {
|
|
602
|
+
yield { type: 'guardrail', guardrail: guarded.event };
|
|
603
|
+
}
|
|
604
|
+
return {
|
|
605
|
+
...result,
|
|
606
|
+
modelText: guarded.text,
|
|
607
|
+
provenance,
|
|
608
|
+
...(guarded.suspicious ? { suspicious: guarded.suspicious } : {}),
|
|
609
|
+
};
|
|
360
610
|
}
|
|
611
|
+
export { startToolExecution };
|
|
361
612
|
export function newCallId(name) {
|
|
362
613
|
return `call_${name}_${Date.now()}`;
|
|
363
614
|
}
|