Summary
A function-form requireApproval predicate is called with toolCall.arguments, the raw JSON.parse of the model's tool-call output. The tool's Zod inputSchema (with its defaults and coercions) is only applied later, at execution time. The predicate and the executor can therefore see different values: a field the schema would fill via .default() is undefined when the predicate runs but populated when the tool executes.
This only affects applications using the human-in-the-loop approval feature with a function-form predicate that reads a field carrying a schema default. Apps without such predicates are unaffected.
Affected code
src/lib/conversation-state.ts (on main, d26f835), in toolRequiresApproval:
if (typeof requireApproval === 'function') {
return requireApproval(toolCall.arguments, context); // raw JSON.parse output
}
toolCall.arguments comes from JSON.parse(item.arguments) earlier in the file; the inputSchema is applied only in the executor (src/lib/tool-executor.ts, validateToolInput), which runs after this check.
Impact
The predicate is the gate that decides whether a human must approve a call. If a model can make a call look benign to the gate — e.g. omit a destructive flag so the predicate sees undefined instead of the schema default true — while the executor later runs it with the sensitive normalized value, the approval requirement is bypassed for exactly the calls it was meant to catch. The model output controls the bypass.
How to reproduce / confirm
- Define a tool with
inputSchema including destructive: z.boolean().default(true) and a requireApproval predicate that checks params.destructive === true.
- Have the model emit a tool call with arguments
{} (field omitted).
- The predicate sees
undefined, returns false, and the call runs without approval — yet the executor receives destructive: true.
Suggested fix
Normalize tool-call arguments through the matched tool's inputSchema before running call-level or tool-level approval predicates, and reuse those normalized arguments for execution so the gate and the executor see identical values.
Summary
A function-form
requireApprovalpredicate is called withtoolCall.arguments, the rawJSON.parseof the model's tool-call output. The tool's ZodinputSchema(with its defaults and coercions) is only applied later, at execution time. The predicate and the executor can therefore see different values: a field the schema would fill via.default()isundefinedwhen the predicate runs but populated when the tool executes.This only affects applications using the human-in-the-loop approval feature with a function-form predicate that reads a field carrying a schema default. Apps without such predicates are unaffected.
Affected code
src/lib/conversation-state.ts(onmain,d26f835), intoolRequiresApproval:toolCall.argumentscomes fromJSON.parse(item.arguments)earlier in the file; theinputSchemais applied only in the executor (src/lib/tool-executor.ts,validateToolInput), which runs after this check.Impact
The predicate is the gate that decides whether a human must approve a call. If a model can make a call look benign to the gate — e.g. omit a
destructiveflag so the predicate seesundefinedinstead of the schema defaulttrue— while the executor later runs it with the sensitive normalized value, the approval requirement is bypassed for exactly the calls it was meant to catch. The model output controls the bypass.How to reproduce / confirm
inputSchemaincludingdestructive: z.boolean().default(true)and arequireApprovalpredicate that checksparams.destructive === true.{}(field omitted).undefined, returnsfalse, and the call runs without approval — yet the executor receivesdestructive: true.Suggested fix
Normalize tool-call arguments through the matched tool's
inputSchemabefore running call-level or tool-level approval predicates, and reuse those normalized arguments for execution so the gate and the executor see identical values.