@ai-sdk/workflow 2.0.15 → 2.0.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-sdk/workflow",
3
- "version": "2.0.15",
3
+ "version": "2.0.17",
4
4
  "type": "module",
5
5
  "description": "WorkflowAgent for building AI agents with AI SDK",
6
6
  "license": "Apache-2.0",
@@ -33,7 +33,7 @@
33
33
  "dependencies": {
34
34
  "@ai-sdk/provider": "4.0.9",
35
35
  "@ai-sdk/provider-utils": "5.0.34",
36
- "ai": "7.0.85",
36
+ "ai": "7.0.87",
37
37
  "ajv": "^8.20.0"
38
38
  },
39
39
  "devDependencies": {
@@ -25,6 +25,12 @@ import {
25
25
 
26
26
  export type ModelCallStreamPart<TTools extends ToolSet = ToolSet> =
27
27
  | Experimental_LanguageModelStreamPart<TTools>
28
+ | {
29
+ type: 'tool-approval-request';
30
+ approvalId: string;
31
+ toolCallId: string;
32
+ signature?: string;
33
+ }
28
34
  | { type: 'reset-step' };
29
35
 
30
36
  export type ModelStopCondition = StopCondition<NoInfer<ToolSet>, any>;
@@ -326,7 +332,16 @@ export async function doStreamStep(
326
332
 
327
333
  // Write to writable in real-time
328
334
  if (writer) {
329
- await writer.write(part);
335
+ if (part.type === 'tool-approval-request') {
336
+ await writer.write({
337
+ type: 'tool-approval-request',
338
+ approvalId: part.approvalId,
339
+ toolCallId: part.toolCall.toolCallId,
340
+ ...(part.signature != null ? { signature: part.signature } : {}),
341
+ });
342
+ } else {
343
+ await writer.write(part);
344
+ }
330
345
  }
331
346
 
332
347
  if (part.type === 'error' && !hasTerminalError) {
package/src/index.ts CHANGED
@@ -6,6 +6,7 @@ export {
6
6
  type WorkflowAgentOptions,
7
7
  type WorkflowAgentStreamOptions,
8
8
  type WorkflowAgentStreamResult,
9
+ type WorkflowToolApprovalSecret,
9
10
  type GenerationSettings,
10
11
  type InferWorkflowAgentTools,
11
12
  type InferWorkflowAgentUIMessage,
@@ -28,6 +28,13 @@ async function throwingStep(): Promise<string> {
28
28
  throw new FatalError('Tool execution failed fatally');
29
29
  }
30
30
 
31
+ async function executeApprovedAction(input: {
32
+ action: string;
33
+ }): Promise<string> {
34
+ 'use step';
35
+ return `executed:${input.action}`;
36
+ }
37
+
31
38
  // ============================================================================
32
39
  // Core agent tests
33
40
  // ============================================================================
@@ -514,6 +521,96 @@ export async function agentToolApprovalE2e() {
514
521
  };
515
522
  }
516
523
 
524
+ const signedToolApprovalSecret = {
525
+ environmentVariable: 'WORKFLOW_TOOL_APPROVAL_SECRET',
526
+ };
527
+
528
+ export async function agentSignedToolApprovalIssueE2e() {
529
+ 'use workflow';
530
+ const agent = new WorkflowAgent({
531
+ model: mockSequenceModel([
532
+ {
533
+ type: 'tool-call',
534
+ toolName: 'riskyTool',
535
+ input: JSON.stringify({ action: 'delete' }),
536
+ },
537
+ ]),
538
+ tools: {
539
+ riskyTool: tool({
540
+ description: 'A dangerous tool that needs approval',
541
+ inputSchema: z.object({ action: z.string() }),
542
+ execute: executeApprovedAction,
543
+ needsApproval: true,
544
+ }),
545
+ },
546
+ experimental_toolApprovalSecret: signedToolApprovalSecret,
547
+ });
548
+
549
+ const result = await agent.stream({
550
+ messages: [{ role: 'user', content: 'do something risky' }],
551
+ writable: getWritable(),
552
+ });
553
+
554
+ return { toolCallsCount: result.toolCalls.length };
555
+ }
556
+
557
+ export async function agentSignedToolApprovalResumeE2e(signature: string) {
558
+ 'use workflow';
559
+ const agent = new WorkflowAgent({
560
+ model: mockSequenceModel([{ type: 'text', text: 'approved action done' }]),
561
+ tools: {
562
+ riskyTool: tool({
563
+ description: 'A dangerous tool that needs approval',
564
+ inputSchema: z.object({ action: z.string() }),
565
+ execute: executeApprovedAction,
566
+ needsApproval: true,
567
+ }),
568
+ },
569
+ experimental_toolApprovalSecret: signedToolApprovalSecret,
570
+ });
571
+
572
+ const result = await agent.stream({
573
+ messages: [
574
+ { role: 'user', content: 'do something risky' },
575
+ {
576
+ role: 'assistant',
577
+ content: [
578
+ {
579
+ type: 'tool-call',
580
+ toolCallId: 'call-1',
581
+ toolName: 'riskyTool',
582
+ input: { action: 'delete' },
583
+ },
584
+ {
585
+ type: 'tool-approval-request',
586
+ approvalId: 'approval-call-1',
587
+ toolCallId: 'call-1',
588
+ signature,
589
+ },
590
+ ],
591
+ },
592
+ {
593
+ role: 'tool',
594
+ content: [
595
+ {
596
+ type: 'tool-approval-response',
597
+ approvalId: 'approval-call-1',
598
+ approved: true,
599
+ },
600
+ ],
601
+ },
602
+ ],
603
+ writable: getWritable(),
604
+ });
605
+
606
+ return {
607
+ lastStepText: result.steps.at(-1)?.text,
608
+ containsApprovedToolResult: JSON.stringify(result.messages).includes(
609
+ 'executed:delete',
610
+ ),
611
+ };
612
+ }
613
+
517
614
  // ============================================================================
518
615
  // Tool with input schema (tests serialization across step boundary)
519
616
  // ============================================================================
@@ -185,20 +185,22 @@ export function toUIMessageChunk(
185
185
  case 'raw':
186
186
  return undefined;
187
187
 
188
+ case 'tool-approval-request':
189
+ return {
190
+ type: 'tool-approval-request',
191
+ approvalId: part.approvalId,
192
+ toolCallId:
193
+ 'toolCallId' in part ? part.toolCallId : part.toolCall.toolCallId,
194
+ ...(part.signature != null ? { signature: part.signature } : {}),
195
+ };
196
+
188
197
  default: {
189
- // Pass through tool-approval-request, step boundaries, and other
190
- // chunks as-is. Step boundaries (finish-step/start-step) are not
191
- // standard ModelCallStreamPart types but are written by the
192
- // WorkflowAgent between tool execution and the next model step
193
- // to ensure proper message splitting in convertToModelMessages.
198
+ // Pass through step boundaries and other chunks as-is. Step boundaries
199
+ // (finish-step/start-step) are not standard ModelCallStreamPart types
200
+ // but are written by the WorkflowAgent between tool execution and the
201
+ // next model step to ensure proper message splitting in
202
+ // convertToModelMessages.
194
203
  const passthroughPart = part as any;
195
- if (passthroughPart.type === 'tool-approval-request') {
196
- return {
197
- type: 'tool-approval-request',
198
- approvalId: passthroughPart.approvalId,
199
- toolCallId: passthroughPart.toolCallId,
200
- } as UIMessageChunk;
201
- }
202
204
  if (
203
205
  passthroughPart.type === 'finish-step' ||
204
206
  passthroughPart.type === 'start-step' ||
@@ -34,14 +34,17 @@ import {
34
34
  type TelemetryOptions as CoreTelemetryOptions,
35
35
  type Instructions,
36
36
  type Experimental_SandboxSession as SandboxSession,
37
+ InvalidToolApprovalSignatureError,
37
38
  } from 'ai';
38
39
  import {
39
40
  createRestrictedTelemetryDispatcher,
40
41
  collectToolApprovals,
41
42
  convertToLanguageModelPrompt,
42
43
  mergeCallbacks,
44
+ signToolApproval,
43
45
  standardizePrompt,
44
46
  validateApprovedToolApprovals,
47
+ verifyToolApprovalSignature,
45
48
  } from 'ai/internal';
46
49
  import { createLanguageModelToolResultOutput } from './create-language-model-tool-result-output.js';
47
50
  import type { ModelCallStreamPart } from './do-stream-step.js';
@@ -116,6 +119,17 @@ export interface OutputSpecification<OUTPUT, PARTIAL> {
116
119
  */
117
120
  export type ProviderOptions = SharedV4ProviderOptions;
118
121
 
122
+ /**
123
+ * Workflow-safe reference to the environment variable that contains the
124
+ * secret used to sign and verify tool approvals.
125
+ */
126
+ export type WorkflowToolApprovalSecret = {
127
+ /**
128
+ * Name of an environment variable containing a high-entropy secret.
129
+ */
130
+ environmentVariable: string;
131
+ };
132
+
119
133
  type WorkflowAgentToolsContextParameter<TTools extends ToolSet> =
120
134
  HasRequiredKey<InferToolSetContext<TTools>> extends true
121
135
  ? { toolsContext: InferToolSetContext<TTools> }
@@ -543,6 +557,18 @@ export type WorkflowAgentOptions<
543
557
  */
544
558
  experimental_sandbox?: SandboxSession;
545
559
 
560
+ /**
561
+ * Workflow-safe reference to the environment variable containing the
562
+ * secret for HMAC-signing tool approval requests. When set, the agent signs
563
+ * each approval request and verifies the signature before executing an
564
+ * approved tool replayed from client-supplied message history.
565
+ *
566
+ * Only the environment variable name crosses workflow boundaries. The
567
+ * secret value is read inside signing and verification steps and is never
568
+ * serialized. Per-stream values override this default.
569
+ */
570
+ experimental_toolApprovalSecret?: WorkflowToolApprovalSecret;
571
+
546
572
  /**
547
573
  * Default callback function called before each step in the agent loop.
548
574
  * Use this to modify settings, manage context, or inject messages dynamically
@@ -1002,6 +1028,18 @@ export type WorkflowAgentStreamOptions<
1002
1028
  */
1003
1029
  experimental_sandbox?: SandboxSession;
1004
1030
 
1031
+ /**
1032
+ * Workflow-safe reference to the environment variable containing the
1033
+ * secret for HMAC-signing tool approval requests. When set, the agent signs
1034
+ * each approval request and verifies the signature before executing an
1035
+ * approved tool replayed from client-supplied message history.
1036
+ *
1037
+ * Only the environment variable name crosses workflow boundaries. The
1038
+ * secret value is read inside signing and verification steps and is never
1039
+ * serialized. Overrides the constructor-level value if provided.
1040
+ */
1041
+ experimental_toolApprovalSecret?: WorkflowToolApprovalSecret;
1042
+
1005
1043
  /**
1006
1044
  * Callback function to be called after each step completes.
1007
1045
  */
@@ -1266,6 +1304,7 @@ export class WorkflowAgent<
1266
1304
  private repairToolCall?: ToolCallRepairFunction<TBaseTools>;
1267
1305
  private experimentalDownload?: DownloadFunction;
1268
1306
  private experimentalSandbox?: SandboxSession;
1307
+ private experimentalToolApprovalSecret?: WorkflowToolApprovalSecret;
1269
1308
  private prepareStep?: PrepareStepCallback<TBaseTools, TRuntimeContext>;
1270
1309
  private allowSystemInMessages: boolean;
1271
1310
  private constructorOnStepEnd?: WorkflowAgentOnStepEndCallback<
@@ -1305,6 +1344,8 @@ export class WorkflowAgent<
1305
1344
  options.repairToolCall ?? options.experimental_repairToolCall;
1306
1345
  this.experimentalDownload = options.experimental_download;
1307
1346
  this.experimentalSandbox = options.experimental_sandbox;
1347
+ this.experimentalToolApprovalSecret =
1348
+ options.experimental_toolApprovalSecret;
1308
1349
  this.prepareStep = options.prepareStep;
1309
1350
  this.constructorOnStepEnd = options.onStepEnd ?? options.onStepFinish;
1310
1351
  const { onFinish, onEnd = onFinish } = options;
@@ -1374,6 +1415,9 @@ export class WorkflowAgent<
1374
1415
  options.activeTools ?? this.activeTools;
1375
1416
  let effectiveDownloadFromPrepare =
1376
1417
  options.experimental_download ?? this.experimentalDownload;
1418
+ const effectiveToolApprovalSecret =
1419
+ options.experimental_toolApprovalSecret ??
1420
+ this.experimentalToolApprovalSecret;
1377
1421
  let effectiveTelemetryFromPrepare = options.telemetry ?? this.telemetry;
1378
1422
 
1379
1423
  // Resolve messages for prepareCall: use messages directly, or convert prompt
@@ -1562,11 +1606,20 @@ export class WorkflowAgent<
1562
1606
  }
1563
1607
 
1564
1608
  // Re-validate through the shared core implementation: input schema,
1565
- // HMAC signature (when configured), and approval policy. Convert
1566
- // invalid input, denial, or signature errors to a tool error result
1567
- // so the agent loop can continue gracefully.
1609
+ // approval signature (when configured), and approval policy.
1610
+ // Convert invalid input, denial, or signature errors to a tool error
1611
+ // result so the agent loop can continue gracefully.
1568
1612
  let revalidationReason: string | undefined;
1569
1613
  try {
1614
+ await validateWorkflowToolApprovalSignature({
1615
+ secret: effectiveToolApprovalSecret,
1616
+ approvalId: approval.collected.approvalRequest.approvalId,
1617
+ toolCallId: approval.toolCallId,
1618
+ toolName: approval.toolName,
1619
+ input: approval.input,
1620
+ signature: approval.collected.approvalRequest.signature,
1621
+ });
1622
+
1570
1623
  const { deniedToolApprovals: policyDenied, invalidToolApprovals } =
1571
1624
  await validateApprovedToolApprovals({
1572
1625
  approvedToolApprovals: [approval.collected],
@@ -2394,13 +2447,31 @@ export class WorkflowAgent<
2394
2447
  return approvalNeeded[tcIndex];
2395
2448
  });
2396
2449
  if (approvalToolCalls.length > 0) {
2397
- await writeApprovalRequests(
2398
- options.writable,
2399
- approvalToolCalls.map(tc => ({
2400
- toolCallId: tc.toolCallId,
2401
- toolName: tc.toolName,
2402
- })),
2450
+ // The signing step receives only a non-secret environment
2451
+ // variable reference. It resolves the secret inside the step,
2452
+ // and only the resulting signature is persisted in the stream.
2453
+ const approvalRequests = await Promise.all(
2454
+ approvalToolCalls.map(async tc => {
2455
+ const approvalId = `approval-${tc.toolCallId}`;
2456
+ const signature =
2457
+ effectiveToolApprovalSecret == null
2458
+ ? undefined
2459
+ : await signWorkflowToolApproval({
2460
+ secret: effectiveToolApprovalSecret,
2461
+ approvalId,
2462
+ toolCallId: tc.toolCallId,
2463
+ toolName: tc.toolName,
2464
+ input: tc.input,
2465
+ });
2466
+
2467
+ return {
2468
+ approvalId,
2469
+ toolCallId: tc.toolCallId,
2470
+ ...(signature != null ? { signature } : {}),
2471
+ };
2472
+ }),
2403
2473
  );
2474
+ await writeApprovalRequests(options.writable, approvalRequests);
2404
2475
  }
2405
2476
  }
2406
2477
 
@@ -2727,16 +2798,21 @@ async function closeStream(
2727
2798
  */
2728
2799
  async function writeApprovalRequests(
2729
2800
  writable: WritableStream<any>,
2730
- toolCalls: Array<{ toolCallId: string; toolName: string }>,
2801
+ approvalRequests: Array<{
2802
+ approvalId: string;
2803
+ toolCallId: string;
2804
+ signature?: string;
2805
+ }>,
2731
2806
  ) {
2732
2807
  'use step';
2733
2808
  const writer = writable.getWriter();
2734
2809
  try {
2735
- for (const tc of toolCalls) {
2810
+ for (const request of approvalRequests) {
2736
2811
  await writer.write({
2737
2812
  type: 'tool-approval-request',
2738
- approvalId: `approval-${tc.toolCallId}`,
2739
- toolCallId: tc.toolCallId,
2813
+ approvalId: request.approvalId,
2814
+ toolCallId: request.toolCallId,
2815
+ ...(request.signature != null ? { signature: request.signature } : {}),
2740
2816
  });
2741
2817
  }
2742
2818
  } finally {
@@ -2744,6 +2820,116 @@ async function writeApprovalRequests(
2744
2820
  }
2745
2821
  }
2746
2822
 
2823
+ async function signWorkflowToolApproval({
2824
+ secret,
2825
+ approvalId,
2826
+ toolCallId,
2827
+ toolName,
2828
+ input,
2829
+ }: {
2830
+ secret: WorkflowToolApprovalSecret;
2831
+ approvalId: string;
2832
+ toolCallId: string;
2833
+ toolName: string;
2834
+ input: unknown;
2835
+ }): Promise<string> {
2836
+ 'use step';
2837
+
2838
+ return signToolApproval({
2839
+ secret: getWorkflowToolApprovalSecret(secret),
2840
+ approvalId,
2841
+ toolCallId,
2842
+ toolName,
2843
+ input,
2844
+ });
2845
+ }
2846
+
2847
+ async function verifyWorkflowToolApprovalSignature({
2848
+ secret,
2849
+ signature,
2850
+ approvalId,
2851
+ toolCallId,
2852
+ toolName,
2853
+ input,
2854
+ }: {
2855
+ secret: WorkflowToolApprovalSecret;
2856
+ signature: string;
2857
+ approvalId: string;
2858
+ toolCallId: string;
2859
+ toolName: string;
2860
+ input: unknown;
2861
+ }): Promise<boolean> {
2862
+ 'use step';
2863
+
2864
+ return verifyToolApprovalSignature({
2865
+ secret: getWorkflowToolApprovalSecret(secret),
2866
+ signature,
2867
+ approvalId,
2868
+ toolCallId,
2869
+ toolName,
2870
+ input,
2871
+ });
2872
+ }
2873
+
2874
+ async function validateWorkflowToolApprovalSignature({
2875
+ secret,
2876
+ signature,
2877
+ approvalId,
2878
+ toolCallId,
2879
+ toolName,
2880
+ input,
2881
+ }: {
2882
+ secret: WorkflowToolApprovalSecret | undefined;
2883
+ signature: string | undefined;
2884
+ approvalId: string;
2885
+ toolCallId: string;
2886
+ toolName: string;
2887
+ input: unknown;
2888
+ }): Promise<void> {
2889
+ if (secret == null) {
2890
+ return;
2891
+ }
2892
+
2893
+ if (signature == null) {
2894
+ throw new InvalidToolApprovalSignatureError({
2895
+ approvalId,
2896
+ toolCallId,
2897
+ reason: 'missing signature',
2898
+ });
2899
+ }
2900
+
2901
+ const valid = await verifyWorkflowToolApprovalSignature({
2902
+ secret,
2903
+ signature,
2904
+ approvalId,
2905
+ toolCallId,
2906
+ toolName,
2907
+ input,
2908
+ });
2909
+
2910
+ if (!valid) {
2911
+ throw new InvalidToolApprovalSignatureError({
2912
+ approvalId,
2913
+ toolCallId,
2914
+ reason: 'invalid signature',
2915
+ });
2916
+ }
2917
+ }
2918
+
2919
+ function getWorkflowToolApprovalSecret({
2920
+ environmentVariable,
2921
+ }: WorkflowToolApprovalSecret): string {
2922
+ const secret = process.env[environmentVariable];
2923
+
2924
+ if (secret == null) {
2925
+ throw new Error(
2926
+ `Tool approval secret environment variable "${environmentVariable}" is not set`,
2927
+ );
2928
+ }
2929
+
2930
+ return secret;
2931
+ }
2932
+
2747
2933
  async function writeToolResults(
2748
2934
  writable: WritableStream<any>,
2749
2935
  results: Array<{