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
@@ -33,12 +33,13 @@ export function resolveAllowedCustomToolIds(profile, req) {
33
33
  if (!tool || tool.type === 'builtin') {
34
34
  return false;
35
35
  }
36
- return pathMatches(tool.paths, req.path);
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.model.config[modelId];
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
- export function initialVisible(gated) {
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
- export function initialBuiltins(gated) {
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
- /** Build the initial tool snapshot for a turn (T0 wired; T1/T2 pending). */
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 = resolveModelBuiltinIds(profile, req, modelId);
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
+ }