Repository navigation
SEC-770: fix(call-model): refuse tool batches with duplicate call IDs and match approvals per row - #1355
Open
alexb-openrouter wants to merge 1 commit into
Open
SEC-770: fix(call-model): refuse tool batches with duplicate call IDs and match approvals per row#1355alexb-openrouter wants to merge 1 commit into
alexb-openrouter wants to merge 1 commit into
Conversation
…h approvals per row
Contributor
|
I'll fix CI failures and address comments from users with write access that start with 'Devin'.
|
This branch has not been deployed
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.
The risk in plain terms
callModel, tool calls and approvals are matched bycall_id. If one response has twofunction_callitems with the samecall_id, both are saved as pending. When the app approves the one it shows as harmless (read_file),processApprovalDecisionsruns the first pending row with that ID, which can be the protected one (delete_repo). Then both rows are cleared.How this PR fixes it
call_idthrows. No tool runs, no pending state is saved, no follow-up request is sent. Same semantics and error text as@openrouter/agent(assertUniqueCallIds, typescript-agent PR App attribution headers don't work #133).processApprovalDecisionsre-checks before any decision runs a tool. It throws if two pending calls share acall_id, or a pending call shares one with an already-executedunsentToolResultsentry.pendingToolCalls. Before, every row with a matching ID was removed.What changed
src/lib/model-result.tsassertUniqueCallIds(), called beforehandleApprovalCheckin the initial round and in each loop round.processApprovalDecisions(): duplicate-ID check at the start, plus ahandledset used for matching and removal:tests/unit/duplicate-call-ids.test.ts: new regression tests.Decisions:
execute: false) are left alone, as in the agent: the SDK runs nothing for them and the caller still gets every call as emitted.approveToolCallsnow runs that call once, because the second entry finds no unhandled row. That is the separate developer-input bug (Approval decisions are not de-duplicated, so an approval-gated tool can execute twice #518) that fix: harden tool approval resume flow #1180 and fix: harden tool approval decisions and predicates #1344 address. This PR does not change the constructor, so it rebases cleanly on either one.Testing
tests/unit/duplicate-call-ids.test.ts: 6 of its 7 tests fail onmainand all pass with the fix. The primary regression test isduplicate call ids in one model response > approving the harmless call cannot run a protected call that shares its call_id. The manual-tool test passes on both, as expected.esm/output, using a 127.0.0.1http.createServerthat returns Responses-API JSON, with publicOpenRouter.callModeland a memoryStateAccessor:main: turn 1 pauses with pending[["call_x","delete_repo"],["call_x","read_file"]]. Approvingcall_xruns["delete_repo:prod"].Response contains more than one function_call with call_id "call_x"; .... Nothing runs, and only 1 request is sent.pnpm run lint,pnpm run typecheck,pnpm run build, andpnpm test(217 passed, 1 skipped) all pass locally.validate: build,tsc --noEmit, and the unit suite pass, including the new file. The job fails in the live-API e2e step attests/e2e/image-generation.test.ts(message.imagesis undefined). That failure is not caused by this PR: the same test fails on the main-based Speakeasy regen PR (run 41930964842) and on an unrelated PR opened at the same time (run 41940057234). This PR does not touch chat or image code.Existing-PR search (open, plus closed in the last 90 days, for duplicate call IDs, approvals, and
handleApprovalCheck): no PR fixes duplicate call IDs in the model response. #1344 and #1180 fix duplicate or conflicting IDs in the developer's ownapproveToolCalls/rejectToolCallslists, plus predicate parsing and resume without decisions. That is a different bug, and they touch different lines.Fixes SEC-770
Link to Devin session: https://openrouter.devinenterprise.com/sessions/139eac9fcef74e0c89373b59c3966f8e
Open in Devin Desktop: https://openrouter.devinenterprise.com/desktop/session/139eac9fcef74e0c89373b59c3966f8e?variant=devin
Requested by: @alexb-openrouter