Skip to content

Commit ee55a07

Browse files
fix: preserve tool approval signatures when approvals are responded (#16589)
## Background Tool approvals signed via experimental_toolApprovalSecret lost their signature when moving from approval-requested to approval-responded, causing resumed requests to fail signature validation with a missing signature error. ## Summary Updated manual chat approval responses and streamed tool approval responses to carry forward an existing approval.signature while preserving existing approval metadata. ## Testing Added regression coverage for manual addToolApprovalResponse and streamed tool-approval-response transitions to assert approval signatures remain on the responded approval state. ## Related Issues Fixes #16543 --------- Co-authored-by: dnukumamras <mukund.sarma@vercel.com>
1 parent 5f0d553 commit ee55a07

3 files changed

Lines changed: 74 additions & 0 deletions

File tree

‎.changeset/tidy-camels-promise.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'ai': patch
3+
---
4+
5+
Preserve tool approval signatures when approvals transition to responded.

‎packages/ai/src/ui/process-ui-message-stream.test.ts‎

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7698,6 +7698,72 @@ describe('processUIMessageStream', () => {
76987698
});
76997699
});
77007700

7701+
describe('tool approval response with signature', () => {
7702+
beforeEach(async () => {
7703+
const stream = createUIMessageStream([
7704+
{
7705+
type: 'start',
7706+
},
7707+
{
7708+
type: 'start-step',
7709+
},
7710+
{
7711+
input: {
7712+
value: 'value',
7713+
},
7714+
toolCallId: 'call-1',
7715+
toolName: 'tool1',
7716+
type: 'tool-input-available',
7717+
},
7718+
{
7719+
approvalId: 'id-1',
7720+
toolCallId: 'call-1',
7721+
type: 'tool-approval-request',
7722+
signature: 'test-sig',
7723+
},
7724+
{
7725+
approvalId: 'id-1',
7726+
approved: true,
7727+
type: 'tool-approval-response',
7728+
},
7729+
{
7730+
type: 'finish-step',
7731+
},
7732+
{
7733+
type: 'finish',
7734+
},
7735+
]);
7736+
7737+
state = createStreamingUIMessageState({
7738+
messageId: 'msg-123',
7739+
lastMessage: undefined,
7740+
});
7741+
7742+
await consumeStream({
7743+
stream: processUIMessageStream({
7744+
stream,
7745+
runUpdateMessageJob,
7746+
onError: error => {
7747+
throw error;
7748+
},
7749+
}),
7750+
});
7751+
});
7752+
7753+
it('preserves signature when transitioning to approval-responded', async () => {
7754+
const toolPart = state!.message.parts.find(
7755+
part => part.type === 'tool-tool1',
7756+
) as any;
7757+
7758+
expect(toolPart.state).toBe('approval-responded');
7759+
expect(toolPart.approval).toEqual({
7760+
id: 'id-1',
7761+
approved: true,
7762+
signature: 'test-sig',
7763+
});
7764+
});
7765+
});
7766+
77017767
describe('tool approval request without signature', () => {
77027768
beforeEach(async () => {
77037769
const stream = createUIMessageStream([

‎packages/ai/src/ui/process-ui-message-stream.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -731,6 +731,9 @@ export function processUIMessageStream<UI_MESSAGE extends UIMessage>({
731731
approved: chunk.approved,
732732
...(chunk.reason != null ? { reason: chunk.reason } : {}),
733733
...(approval.isAutomatic === true ? { isAutomatic: true } : {}),
734+
...(approval.signature != null
735+
? { signature: approval.signature }
736+
: {}),
734737
};
735738
if (chunk.providerExecuted != null) {
736739
toolInvocation.providerExecuted = chunk.providerExecuted;

0 commit comments

Comments
 (0)