-
Notifications
You must be signed in to change notification settings - Fork 12
fix(agent): enforce approval gate on allowFinalResponse path and validate predicate args (#54) #94
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
LukasParke
wants to merge
14
commits into
main
Choose a base branch
from
fix/54-approval-gate-validated-args
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
b6cd7ce
fix(agent): enforce approval gate on allowFinalResponse path and vali…
LukasParke 3ae034e
test(agent): cover hook deny on allowFinalResponse path
LukasParke 639e7bb
fix(agent): gate each response once and let schema-invalid calls reac…
LukasParke 0418b07
fix(agent): keep fail-closed on invalid args for manual tools
LukasParke 631edb2
test(agent): lock in the validate-before-execute invariant behind the…
LukasParke bc76be8
fix(agent): normalize call-level approval args
LukasParke 9a415f1
fix(agent): preserve call-level approval precedence
LukasParke 54a5aef
fix(agent): gate post-hook tool arguments
LukasParke f51b2dc
fix(agent): split tool approval into two phases
LukasParke 9bd6d01
refactor(agent): split approval gate phases
LukasParke 84feaca
fix(agent): harden mutated approval preparation
LukasParke 0719be2
fix(agent): prevent approval key collisions
LukasParke 292f9da
fix(agent): distinguish duplicate approval calls
LukasParke 25ec586
fix(agent): stabilize uncanonicalizable approval identities
LukasParke File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| --- | ||
| '@openrouter/agent': patch | ||
| --- | ||
|
|
||
| Fix two ways the tool-approval gate could be bypassed. | ||
|
|
||
| **`allowFinalResponse` executed pending tool calls with no approval check.** When a `stopWhen` condition halted the loop on a turn that still carried tool calls, the final-response path ran those calls directly — skipping the approval gate the normal loop applies on every round. A tool marked `requireApproval: true` (or gated by a predicate) would execute unguarded, and because the `PermissionRequest` hook's deny bookkeeping lives inside the approval check, hook-based `deny` never fired on this path either. That path now runs the same check as the in-loop call sites, so the run pauses with `status: 'awaiting_approval'` and the gated calls on `pendingToolCalls` instead of executing them. | ||
|
|
||
| **Function-based `requireApproval` received unvalidated arguments.** Tool-level and call-level predicates were called with the raw JSON-parsed tool arguments, while `execute` receives the arguments *after* the tool's Zod `inputSchema` runs. Any default, coercion, or transform in the schema made them disagree — e.g. with `inputSchema: z.object({ dangerous: z.boolean().default(true) })`, a model emitting `{}` showed a predicate `dangerous: undefined` (no approval required) and then executed with `dangerous: true`. Predicates now see a parsed copy, so they decide on exactly what `execute` will receive without mutating the original executable call or parsing transformed output a second time. `PreToolUse` now runs before every auto-resolvable call is partitioned, so approval hooks and persisted pending calls see its effective arguments. Pending calls record an additive marker when preparation ran, preventing a resumed `ModelResult` from applying the hook twice while legacy state without the marker retains its prior behavior. Call-level checks remain unconditional and receive raw arguments when parsing fails; tool-level checks fail closed when schema parsing fails because a hook may later repair the input. | ||
|
|
||
| **Duplicate approval prompts for the same tool call.** The approval gate could run more than once over the same response — e.g. the pre-loop check plus the post-loop `allowFinalResponse` gate when a stop condition fired on the first iteration — re-emitting the `PermissionRequest` hook and re-running `requireApproval` predicates for calls that were already resolved. Each call occurrence in a response is now gated at most once per run, including responses containing duplicate call IDs and arguments. | ||
|
|
||
| ```ts | ||
| import { z } from 'zod/v4'; | ||
| import { tool, type PendingToolCall } from '@openrouter/agent'; | ||
|
|
||
| const deploy = tool({ | ||
| name: 'deploy', | ||
| inputSchema: z.object({ | ||
| environment: z.enum(['staging', 'production']).default('production'), | ||
| }), | ||
| requireApproval: ({ environment }) => environment === 'production', | ||
| execute: async ({ environment }) => deployEnvironment(environment), | ||
| }); | ||
|
|
||
| // `requireApproval` sees the normalized default: { environment: 'production' }. | ||
| // Persist this additive marker when PreToolUse already produced effective args. | ||
| const pending: PendingToolCall<typeof deploy> = { | ||
| id: 'call_deploy', | ||
| name: 'deploy', | ||
| arguments: { environment: 'production' }, | ||
| preToolUseApplied: true, | ||
| }; | ||
| ``` |
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
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
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
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
Oops, something went wrong.
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.