Skip to content

Approval decisions are not de-duplicated, so an approval-gated tool can execute twice #518

Description

@lullu57

Summary

When resuming a paused conversation, the SDK iterates approveToolCalls and rejectToolCalls as plain lists with no de-duplication or intersection check. A call id listed twice in approveToolCalls executes the matching tool twice. A call id present in both arrays executes the tool and records a rejection for the same id.

This only matters for applications that use the human-in-the-loop approval feature and pass decision arrays they don't validate (for example, arrays built from end-user or client input). Apps that don't use approvals are unaffected.

Affected code

src/lib/model-result.ts (on main, d26f835), in processApprovalDecisions():

for (const callId of this.approvedToolCalls) {       // no dedup
  const toolCall = pendingCalls.find(tc => tc.id === callId);
  ...
  const result = await executeTool(tool, ...);       // runs once per duplicate
}
for (const callId of this.rejectedToolCalls) { ... }  // runs even if also approved
const processedIds = new Set([...this.approvedToolCalls, ...this.rejectedToolCalls]);

The Set is only used to filter the remaining pending list; it does not gate execution.

Impact

Approval-gated tools usually guard side effects (a payment, an outbound call, a record write). A duplicate id can double that side effect. An id in both arrays produces a contradictory record — the tool ran, yet the trail shows it was rejected.

How to reproduce / confirm

  1. Resume a paused conversation with a pending tool call call_1.
  2. Case A: pass approveToolCalls: ['call_1', 'call_1'] → the tool executes twice (two result entries).
  3. Case B: pass approveToolCalls: ['call_1'] and rejectToolCalls: ['call_1'] → the tool executes and a rejection is also recorded.

Suggested fix

Validate decisions before executing: reject duplicate ids within each array and reject any id present in both arrays. Execute approvals and record rejections only from the validated, de-duplicated sets.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions