Skip to content

Commit 867b0f5

Browse files
authored
fix: validate originalToolCallId in durable approval to prevent replay (#3053)
* fix: validate originalToolCallId in durable approval to prevent replay The durable approval flow dequeues pre-approved decisions by toolName only, without checking originalToolCallId. This allows a delegated agent to change tool call arguments after approval and still receive the pre-approval, bypassing user consent for the specific call they reviewed. Add a check that rejects the approval if originalToolCallId is present but doesn't match the current toolCallId. * Add tracing span for toolCallId mismatch rejection, add changeset
1 parent b33134a commit 867b0f5

2 files changed

Lines changed: 34 additions & 0 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@inkeep/agents-api": patch
3+
---
4+
5+
Fix durable approval replay: validate originalToolCallId before applying pre-approved decisions

agents-api/src/domains/run/agents/tools/tool-approval.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,35 @@ export async function waitForToolApproval(
4747
delete approvedToolCalls[toolName];
4848
}
4949
if (preApproved !== undefined) {
50+
if (preApproved.originalToolCallId && preApproved.originalToolCallId !== toolCallId) {
51+
const deniedResult = tracer.startActiveSpan(
52+
'tool.approval_denied',
53+
{
54+
attributes: {
55+
...baseSpanAttributes,
56+
'tool.approval.reason': 'originalToolCallId mismatch',
57+
'tool.approval.originalToolCallId': preApproved.originalToolCallId,
58+
},
59+
},
60+
(denialSpan: Span) => {
61+
logger.warn(
62+
{
63+
toolName,
64+
toolCallId,
65+
originalToolCallId: preApproved.originalToolCallId,
66+
},
67+
'Durable approval rejected: originalToolCallId mismatch — tool call may have changed since approval'
68+
);
69+
denialSpan.setStatus({ code: SpanStatusCode.OK });
70+
denialSpan.end();
71+
return createDeniedToolResult(
72+
toolCallId,
73+
'Tool approval rejected: the tool call changed since it was approved.'
74+
);
75+
}
76+
);
77+
return { approved: false, deniedResult };
78+
}
5079
if (!preApproved.approved) {
5180
const deniedResult = tracer.startActiveSpan(
5281
'tool.approval_denied',

0 commit comments

Comments
 (0)