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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions