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.
Files changed (204) hide show
  1. package/README.md +81 -25
  2. package/esm/mod.d.ts +48 -29
  3. package/esm/mod.js +37 -24
  4. package/esm/src/cli/commands/bench.js +13 -16
  5. package/esm/src/cli/commands/fuzz-canary.js +25 -29
  6. package/esm/src/cli/commands/guardrails-eval.d.ts +14 -0
  7. package/esm/src/cli/commands/guardrails-eval.js +15 -0
  8. package/esm/src/cli/commands/profile.js +12 -3
  9. package/esm/src/cli/commands/run.js +8 -9
  10. package/esm/src/cli/commands/test.js +8 -8
  11. package/esm/src/cli/event-log.d.ts +1 -1
  12. package/esm/src/cli/index.js +12 -0
  13. package/esm/src/cli/matrix/synthesizer.d.ts +6 -6
  14. package/esm/src/cli/matrix/synthesizer.js +23 -22
  15. package/esm/src/guardrails/canary-gate.d.ts +2 -1
  16. package/esm/src/guardrails/canary-gate.js +2 -1
  17. package/esm/src/guardrails/canary.d.ts +9 -1
  18. package/esm/src/guardrails/canary.js +21 -10
  19. package/esm/src/guardrails/corpus/canary-egress-attacks.d.ts +1 -0
  20. package/esm/src/guardrails/corpus/fuzz-inbound.js +12 -15
  21. package/esm/src/guardrails/corpus/inbound-payloads.js +1 -0
  22. package/esm/src/guardrails/corpus/live-attacks.js +0 -1
  23. package/esm/src/guardrails/corpus/mod.d.ts +1 -0
  24. package/esm/src/guardrails/corpus/secrets.d.ts +1 -0
  25. package/esm/src/guardrails/corpus/secrets.js +1 -0
  26. package/esm/src/guardrails/corpus/strings.d.ts +1 -0
  27. package/esm/src/guardrails/corpus/strings.js +1 -0
  28. package/esm/src/guardrails/corpus/types.d.ts +1 -0
  29. package/esm/src/guardrails/egress.d.ts +26 -3
  30. package/esm/src/guardrails/egress.js +69 -18
  31. package/esm/src/guardrails/error.d.ts +14 -23
  32. package/esm/src/guardrails/error.js +86 -77
  33. package/esm/src/guardrails/eval/corpus.d.ts +108 -0
  34. package/esm/src/guardrails/eval/corpus.js +978 -0
  35. package/esm/src/guardrails/eval/mod.d.ts +51 -0
  36. package/esm/src/guardrails/eval/mod.js +133 -0
  37. package/esm/src/guardrails/eval/score.d.ts +66 -0
  38. package/esm/src/guardrails/eval/score.js +114 -0
  39. package/esm/src/guardrails/events.d.ts +25 -0
  40. package/esm/src/guardrails/events.js +56 -0
  41. package/esm/src/guardrails/hits.d.ts +24 -0
  42. package/esm/src/guardrails/hits.js +45 -0
  43. package/esm/src/guardrails/lexicon.d.ts +39 -0
  44. package/esm/src/guardrails/lexicon.js +200 -0
  45. package/esm/src/guardrails/live-outbound-gate.d.ts +15 -13
  46. package/esm/src/guardrails/live-outbound-gate.js +131 -89
  47. package/esm/src/guardrails/mod.d.ts +19 -4
  48. package/esm/src/guardrails/mod.js +12 -3
  49. package/esm/src/guardrails/network.d.ts +19 -0
  50. package/esm/src/guardrails/network.js +234 -0
  51. package/esm/src/guardrails/policy.d.ts +35 -0
  52. package/esm/src/guardrails/policy.js +50 -0
  53. package/esm/src/guardrails/progressive-yield.d.ts +51 -0
  54. package/esm/src/guardrails/progressive-yield.js +98 -0
  55. package/esm/src/guardrails/quota.d.ts +17 -3
  56. package/esm/src/guardrails/quota.js +18 -4
  57. package/esm/src/guardrails/sanitize.d.ts +36 -6
  58. package/esm/src/guardrails/sanitize.js +152 -60
  59. package/esm/src/guardrails/serialize.d.ts +35 -0
  60. package/esm/src/guardrails/serialize.js +58 -0
  61. package/esm/src/guardrails/testing.d.ts +7 -0
  62. package/esm/src/guardrails/testing.js +4 -0
  63. package/esm/src/guardrails/theorum-error.d.ts +12 -0
  64. package/esm/src/guardrails/theorum-error.js +15 -0
  65. package/esm/src/guardrails/tool-directives.d.ts +48 -0
  66. package/esm/src/guardrails/tool-directives.js +124 -0
  67. package/esm/src/guardrails/tool-result.d.ts +93 -0
  68. package/esm/src/guardrails/tool-result.js +276 -0
  69. package/esm/src/guardrails/types.d.ts +291 -0
  70. package/esm/src/guardrails/types.js +72 -0
  71. package/esm/src/host/client-turn.js +4 -0
  72. package/esm/src/host/mint-trace.d.ts +1 -1
  73. package/esm/src/kernel/auth/crypto.d.ts +42 -0
  74. package/esm/src/kernel/auth/crypto.js +106 -0
  75. package/esm/src/kernel/auth/mod.d.ts +11 -0
  76. package/esm/src/kernel/auth/mod.js +11 -0
  77. package/esm/src/kernel/auth/oauth.d.ts +47 -0
  78. package/esm/src/kernel/auth/oauth.js +278 -0
  79. package/esm/src/kernel/auth/types.d.ts +133 -0
  80. package/esm/src/kernel/auth/types.js +13 -0
  81. package/esm/src/kernel/engine/delta.js +43 -2
  82. package/esm/src/kernel/engine/live-inbound.d.ts +15 -3
  83. package/esm/src/kernel/engine/live-inbound.js +22 -8
  84. package/esm/src/kernel/engine/live-ingress.d.ts +19 -0
  85. package/esm/src/kernel/engine/live-ingress.js +47 -0
  86. package/esm/src/kernel/engine/repair.js +13 -12
  87. package/esm/src/kernel/engine/runner/gates.js +127 -40
  88. package/esm/src/kernel/engine/runner/mod.d.ts +6 -4
  89. package/esm/src/kernel/engine/runner/mod.js +182 -43
  90. package/esm/src/kernel/engine/runner/schema-validation.js +3 -3
  91. package/esm/src/kernel/engine/runner/stages.d.ts +39 -0
  92. package/esm/src/kernel/engine/runner/stages.js +89 -0
  93. package/esm/src/kernel/engine/runner/state.d.ts +24 -0
  94. package/esm/src/kernel/engine/runner/steps.js +128 -61
  95. package/esm/src/kernel/engine/runner/stream.d.ts +9 -2
  96. package/esm/src/kernel/engine/runner/stream.js +114 -55
  97. package/esm/src/kernel/engine/session/mod.d.ts +3 -1
  98. package/esm/src/kernel/engine/session/mod.js +386 -34
  99. package/esm/src/kernel/interaction-parts.d.ts +14 -0
  100. package/esm/src/kernel/interaction-parts.js +23 -0
  101. package/esm/src/kernel/mod.d.ts +19 -7
  102. package/esm/src/kernel/mod.js +10 -5
  103. package/esm/src/kernel/profile-graph.d.ts +159 -0
  104. package/esm/src/kernel/profile-graph.js +156 -0
  105. package/esm/src/kernel/registry/attachments.d.ts +10 -10
  106. package/esm/src/kernel/registry/attachments.js +29 -26
  107. package/esm/src/kernel/registry/catalog.d.ts +25 -11
  108. package/esm/src/kernel/registry/catalog.js +59 -24
  109. package/esm/src/kernel/registry/ingress.js +34 -22
  110. package/esm/src/kernel/registry/profile-outputs.d.ts +4 -0
  111. package/esm/src/kernel/registry/profile-outputs.js +8 -0
  112. package/esm/src/kernel/registry/profiles.d.ts +34 -14
  113. package/esm/src/kernel/registry/profiles.js +306 -59
  114. package/esm/src/kernel/registry/provider-request.js +2 -0
  115. package/esm/src/kernel/registry/resolve.d.ts +7 -6
  116. package/esm/src/kernel/registry/resolve.js +102 -117
  117. package/esm/src/kernel/registry/schemas.js +1 -1
  118. package/esm/src/kernel/registry/sole-model.d.ts +8 -0
  119. package/esm/src/kernel/registry/sole-model.js +10 -0
  120. package/esm/src/kernel/registry/system-prompt.d.ts +10 -0
  121. package/esm/src/kernel/registry/system-prompt.js +40 -0
  122. package/esm/src/kernel/registry/system-role.d.ts +8 -0
  123. package/esm/src/kernel/registry/system-role.js +14 -0
  124. package/esm/src/kernel/registry/vault.d.ts +2 -2
  125. package/esm/src/kernel/registry/vault.js +4 -4
  126. package/esm/src/kernel/schema.d.ts +97 -11
  127. package/esm/src/kernel/schema.js +256 -111
  128. package/esm/src/kernel/stages.d.ts +175 -0
  129. package/esm/src/kernel/stages.js +476 -0
  130. package/esm/src/kernel/stop.d.ts +65 -11
  131. package/esm/src/kernel/stop.js +46 -11
  132. package/esm/src/kernel/tools/events.d.ts +41 -0
  133. package/esm/src/kernel/tools/events.js +71 -0
  134. package/esm/src/kernel/tools/execute.d.ts +63 -24
  135. package/esm/src/kernel/tools/execute.js +475 -224
  136. package/esm/src/kernel/tools/harness.js +16 -14
  137. package/esm/src/kernel/tools/invoke.js +26 -8
  138. package/esm/src/kernel/tools/mod.d.ts +3 -1
  139. package/esm/src/kernel/tools/mod.js +2 -1
  140. package/esm/src/kernel/tools/permission.d.ts +15 -0
  141. package/esm/src/kernel/tools/permission.js +47 -0
  142. package/esm/src/kernel/tools/project.js +5 -3
  143. package/esm/src/kernel/tools/registry.js +20 -10
  144. package/esm/src/kernel/tools/remote.d.ts +94 -0
  145. package/esm/src/kernel/tools/remote.js +577 -0
  146. package/esm/src/kernel/tools/resolve.d.ts +17 -7
  147. package/esm/src/kernel/tools/resolve.js +38 -17
  148. package/esm/src/kernel/tools/schema.d.ts +2 -0
  149. package/esm/src/kernel/tools/schema.js +23 -6
  150. package/esm/src/kernel/tools/stage-run.d.ts +105 -0
  151. package/esm/src/kernel/tools/stage-run.js +155 -0
  152. package/esm/src/kernel/tools/types.d.ts +198 -20
  153. package/esm/src/kernel/types.d.ts +253 -114
  154. package/esm/src/kernel/util/find-last.d.ts +2 -0
  155. package/esm/src/kernel/util/find-last.js +10 -0
  156. package/esm/src/observability/destinations.d.ts +31 -0
  157. package/esm/src/observability/destinations.js +67 -0
  158. package/esm/src/observability/mod.d.ts +10 -3
  159. package/esm/src/observability/mod.js +6 -2
  160. package/esm/src/observability/policy.d.ts +27 -0
  161. package/esm/src/observability/policy.js +80 -0
  162. package/esm/src/observability/resolve-policy.d.ts +16 -0
  163. package/esm/src/observability/resolve-policy.js +64 -0
  164. package/esm/src/observability/trace-attach.d.ts +3 -1
  165. package/esm/src/observability/trace-attach.js +17 -10
  166. package/esm/src/observability/trace-record.d.ts +12 -4
  167. package/esm/src/observability/trace-record.js +63 -19
  168. package/esm/src/observability/trace-sink.d.ts +19 -0
  169. package/esm/src/observability/trace-sink.js +10 -0
  170. package/esm/src/observability/trace.d.ts +14 -11
  171. package/esm/src/observability/trace.js +23 -14
  172. package/esm/src/observability/types.d.ts +113 -0
  173. package/esm/src/observability/types.js +11 -0
  174. package/esm/src/providers/create-provider.d.ts +7 -4
  175. package/esm/src/providers/create-provider.js +21 -7
  176. package/esm/src/providers/google/interactions/framing.d.ts +8 -0
  177. package/esm/src/providers/google/interactions/framing.js +74 -13
  178. package/esm/src/providers/google/live/framing.d.ts +5 -2
  179. package/esm/src/providers/google/live/framing.js +49 -12
  180. package/esm/src/providers/google/live/openapi-schema.js +3 -2
  181. package/esm/src/providers/google/live/stream.d.ts +9 -0
  182. package/esm/src/providers/google/live/stream.js +16 -3
  183. package/esm/src/providers/openrouter/cache-control.d.ts +24 -0
  184. package/esm/src/providers/openrouter/cache-control.js +23 -0
  185. package/esm/src/providers/openrouter/chat.d.ts +21 -1
  186. package/esm/src/providers/openrouter/chat.js +51 -6
  187. package/esm/src/providers/openrouter/image.js +8 -19
  188. package/esm/src/providers/openrouter/openai/chat-payload.js +23 -1
  189. package/esm/src/providers/openrouter/openai/compat.d.ts +7 -1
  190. package/esm/src/providers/openrouter/openai/compat.js +23 -4
  191. package/esm/src/providers/openrouter/openai/image-payload.js +8 -2
  192. package/esm/src/providers/openrouter/openai/sdk-messages.d.ts +1 -1
  193. package/esm/src/providers/openrouter/openai/sdk-messages.js +32 -5
  194. package/package.json +1 -1
  195. package/esm/src/interface/blocks.d.ts +0 -20
  196. package/esm/src/interface/blocks.js +0 -180
  197. package/esm/src/interface/from-profile.d.ts +0 -14
  198. package/esm/src/interface/from-profile.js +0 -104
  199. package/esm/src/interface/inputs.d.ts +0 -8
  200. package/esm/src/interface/inputs.js +0 -127
  201. package/esm/src/interface/mod.d.ts +0 -10
  202. package/esm/src/interface/mod.js +0 -9
  203. package/esm/src/interface/types.d.ts +0 -153
  204. 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
- export function isResumeContinuation(resume) {
15
- return resume?.value !== undefined || resume?.granted === true;
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' || value.kind === 'confirmation' || value.kind === 'permission'));
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
- /** Format model-facing tool output for provider history continuation. */
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.data !== undefined) {
108
- return sanitizeText(`${result.finding}\n${JSON.stringify(result.data)}`);
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
- /** Format a tool failure for provider history — structured so the model (or host) sees the code. */
113
- export function formatToolFailureForModel(failure) {
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}): ${failure.message}`,
162
+ finding: `Tool error (${failure.code}): ${safe}`,
116
163
  data: {
117
164
  ok: false,
118
165
  code: failure.code,
119
- message: failure.message,
166
+ message: safe,
120
167
  ...(failure.details !== undefined ? { details: failure.details } : {}),
121
168
  },
122
169
  };
123
170
  }
124
- export function* startToolExecution(tool, rawInput, ctx, base) {
125
- yield toolEvent(base, { phase: 'running' });
126
- throwIfAborted(ctx.signal);
127
- const parsed = tool.input.safeParse(rawInput);
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 { ok: true, data: parsed.data };
176
+ return ctx.profile.tools.t2Loader === tool.name;
137
177
  }
138
- export async function* executeFunction(tool, rawInput, ctx, base, snapshot) {
139
- const parsed = yield* startToolExecution(tool, rawInput, ctx, base);
140
- if (!parsed.ok)
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
- throwIfAborted(ctx.signal);
150
- if (tool.canExecute) {
151
- try {
152
- const allowed = await tool.canExecute(input, ctx);
153
- if (!allowed) {
154
- yield failureEvent(base, {
155
- code: 'not_authorized',
156
- message: 'Tool execution not authorized',
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
- if (tool.interactive && ctx.resume?.value === undefined) {
183
- const render = tool.interactive.render(input);
184
- const interactivePause = {
185
- kind: 'interactive',
186
- tool: tool.name,
187
- render,
188
- input,
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
- throwIfAborted(ctx.signal);
194
- try {
195
- const output = yield* runHandler(tool.handler, input, ctx, base);
196
- if (output === undefined) {
197
- yield failureEvent(base, { code: 'invalid_output', message: 'Handler returned no output' });
198
- return undefined;
199
- }
200
- const checked = tool.output.safeParse(output);
201
- if (!checked.success) {
202
- yield failureEvent(base, {
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: 'Tool output validation failed',
205
- details: checked.error.flatten(),
206
- });
207
- return undefined;
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
- let finalOutput = checked.data;
210
- if (ctx.profile.type !== 'speech' && ctx.profile.tools.t2Loader === tool.name) {
211
- if (!snapshot) {
212
- yield failureEvent(base, {
213
- code: 'invalid_output',
214
- message: `tools.t2Loader '${tool.name}' requires a turn tool snapshot`,
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
- const { promoted, failure: promoteFailure } = promoteLoadedTools(snapshot, loaded, ctx.profile);
227
- if (promoteFailure) {
228
- yield failureEvent(base, promoteFailure);
229
- return undefined;
263
+ else {
264
+ failure = next.failure;
265
+ modelResult = yield* guard(formatToolFailureForModel(next.failure));
266
+ outputRaw = undefined;
267
+ awaiting = false;
230
268
  }
231
- finalOutput = { ...checked.data, loaded: promoted };
232
- const rechecked = tool.output.safeParse(finalOutput);
233
- if (!rechecked.success) {
234
- yield failureEvent(base, {
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: 'T2 loader output validation failed after promotion',
237
- details: rechecked.error.flatten(),
238
- });
239
- return undefined;
240
- }
241
- finalOutput = rechecked.data;
333
+ message: lexiconText('tool.output_invalid_after_mutate'),
334
+ details: checked.error.flatten(),
335
+ },
336
+ };
242
337
  }
243
- const modelResult = projectForModel(tool, finalOutput);
244
- yield toolEvent(base, { phase: 'complete', output: finalOutput });
245
- return modelResult;
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
- const msg = err instanceof Error ? err.message : String(err);
249
- yield failureEvent(base, { code: 'handler_error', message: msg });
250
- return undefined;
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 `Tool '${tool.name}' is not wired — profile.tools.t1Policy must select it`;
406
+ return lexiconText('tool.not_wired_t1', { tool: tool.name });
256
407
  }
257
408
  if (tool.loadTier === 'T2') {
258
- return `Tool '${tool.name}' is not loaded — run profile.tools.t2Loader first`;
409
+ return lexiconText('tool.not_loaded_t2', { tool: tool.name });
259
410
  }
260
- return `Tool '${tool.name}' is not visible this turn`;
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
- /** Strip prototype-pollution keys from provider/host tool args before validation. */
273
- export function plainToolInput(input) {
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
- yield failureEvent(base, {
430
+ const failure = {
294
431
  code: 'not_loaded',
295
- message: `Builtin '${tool.name}' is not enabled this turn`,
296
- });
297
- return undefined;
432
+ message: lexiconText('tool.builtin_not_enabled', { tool: tool.name }),
433
+ };
434
+ yield failureEvent(base, failure);
435
+ return earlyFailure(failure);
298
436
  }
299
- yield failureEvent(base, {
437
+ const failure = {
300
438
  code: 'provider_native',
301
- message: `Tool '${tool.name}' is a provider builtin — execution is handled by the model provider, not the kernel`,
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
- yield failureEvent(base, { code: 'unknown_tool', message: `Tool '${name}' is not registered` });
318
- return undefined;
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
- yield failureEvent(base, {
520
+ const failure = {
323
521
  code: 'provider_native',
324
- message: `Tool '${name}' is a provider builtin and requires a turn tool snapshot`,
325
- });
326
- return undefined;
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
- if (profile.type === 'speech' || !profile.tools.allow.includes(name)) {
332
- yield failureEvent(base, {
333
- code: 'not_allowed',
334
- message: `Tool '${name}' is not allowed on ${profile.id}`,
335
- });
336
- return undefined;
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
- if (snapshot) {
339
- const continuing = isResumeContinuation(ctx.resume);
340
- if (!continuing && !snapshot.gated.includes(name)) {
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
- return yield* executeFunction(tool, safeInput, fullCtx, base, snapshot);
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
  }