Repository navigation
fix: harden tool approval decisions and predicates - #1344
Closed
mattapperson wants to merge 3 commits into
Closed
mattapperson wants to merge 3 commits into
mattapperson wants to merge 3 commits into
Conversation
- #520: run tool-call arguments through the tool's inputSchema before calling a call-level or tool-level requireApproval predicate, so the predicate sees the same defaults and coercions the executor runs with. The original call is not modified, so the executor's own parse does not apply transforms twice. - #518: reject duplicate IDs within approveToolCalls or rejectToolCalls, and IDs present in both, when ModelResult is constructed, before any tool runs. - #517: resuming an awaiting_approval conversation always goes through decision processing. With no decisions every call stays pending, so the conversation stays paused instead of sending a new model request. Fixes #517 Fixes #518 Fixes #520
The re-validation guard parsed the already-parsed arguments a second time. A schema with a type-changing transform (e.g. a string transformed to a boolean) fails that second parse, so predicates fell back to the raw arguments and could allow a call the executor then ran with the transformed value. Pass the inputSchema output whenever the first parse succeeds. Also explain a pending approval when getText() or getResponse() is called on a resumed conversation that is still awaiting a decision, instead of the generic "Response not available".
Comment on lines
+119
to
+120
| if (isCallApprovalCheck(callLevelCheck)) { | ||
| return callLevelCheck({ ...toolCall, arguments: args }, context); |
Contributor
There was a problem hiding this comment.
Contributor
Author
|
Closing: #1356 moved this code (
One note for the agent code: a review here flagged that an exception thrown inside a tool's schema (e.g. a throwing transform or refine) can escape |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes three bugs in the human-in-the-loop tool approval flow. Each one lets an approval-gated tool run without the approval it was meant to require.
toolRequiresApprovalpassedJSON.parseoutput torequireApprovalpredicates. The tool'sinputSchema(defaults, coercions, transforms) was applied only later, at execution time. A model could omit a field defaulted to a sensitive value (e.g.destructive: z.boolean().default(true)), so the predicate sawundefinedand skipped approval while the executor ran withtrue. Call-level and tool-level predicates now receive theinputSchemaoutput, including type-changing transforms. The original call is left unchanged so the executor's own parse doesn't apply transforms twice. If the arguments fail validation, the raw arguments are passed through; the executor rejects such calls beforeexecuteruns.approveToolCallsexecuted the tool twice. An ID in both arrays executed it and recorded a rejection.ModelResultnow throws at construction, before any tool runs or state changes, next to the existing "decisions require a state accessor" check.awaiting_approvalconversation with no decision advances it without recording an approval #517: resuming without a decision advances the conversation. The resume path ran only when a decision was passed. With none, a persistedawaiting_approvalconversation was saved asin_progressand a new model request was sent. Resuming now always goes through decision processing. With no decisions every call stays pending, so the conversation stays paused, using the existing "still pending" path.Fixes #517
Fixes #518
Fixes #520
Behavior changes
requiresApproval()staystrue. Any new input passed on that call is not sent until a decision is made.getText()andgetResponse()throwConversation is awaiting approval for tool calls: <name> (<id>). Pass approveToolCalls or rejectToolCalls to continue.instead of the genericResponse not available.Testing
tests/unit/approval-hardening.test.ts(12 tests):ModelResultconstructor.awaiting_approval, a recording fetcher sees no requests, andgetResponse()explains the pending approval.conversation-state.tsandmodel-result.ts, 7 of the first 10 tests fail; the other 3 are controls. The transform test also fails against this PR's first commit, which re-parsed the output.