@ai-sdk/harness 1.0.86 → 1.0.90

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.
@@ -301,6 +301,9 @@ export function runPrompt<
301
301
  let stepText = '';
302
302
  let stepReasoning = '';
303
303
  let stepToolCalls: TurnContentPart[] = [];
304
+ let expectedStepToolCallCount: number | undefined;
305
+ let observedStepToolCallCount = 0;
306
+ let pauseAfterStepToolCalls = false;
304
307
  const buildStepContent = (): TurnContentPart[] => {
305
308
  const parts: TurnContentPart[] = [];
306
309
  if (stepText) parts.push({ type: 'text', text: stepText });
@@ -312,6 +315,9 @@ export function runPrompt<
312
315
  stepText = '';
313
316
  stepReasoning = '';
314
317
  stepToolCalls = [];
318
+ expectedStepToolCallCount = undefined;
319
+ observedStepToolCallCount = 0;
320
+ pauseAfterStepToolCalls = false;
315
321
  };
316
322
  const zeroUsage: LanguageModelV4Usage = {
317
323
  inputTokens: {
@@ -599,6 +605,15 @@ export function runPrompt<
599
605
  }
600
606
 
601
607
  while (true) {
608
+ if (
609
+ pauseAfterStepToolCalls &&
610
+ expectedStepToolCallCount != null &&
611
+ observedStepToolCallCount >= expectedStepToolCallCount
612
+ ) {
613
+ await finishForHostInputPause({ completeCurrentStep: true });
614
+ return;
615
+ }
616
+
602
617
  const { value, done } = await reader.read();
603
618
  if (done) {
604
619
  releasePendingStopBoundary();
@@ -759,6 +774,8 @@ export function runPrompt<
759
774
 
760
775
  // Telemetry: a tool execution begins on its `tool-call`.
761
776
  if (value.type === 'tool-call') {
777
+ observedStepToolCallCount += 1;
778
+ expectedStepToolCallCount ??= value.stepToolCallCount;
762
779
  stepToolCalls.push({
763
780
  type: 'tool-call',
764
781
  toolCallId: value.toolCallId,
@@ -962,11 +979,25 @@ export function runPrompt<
962
979
  approvalId: pendingApproval.approvalId,
963
980
  toolCall: pendingParsedToolCall,
964
981
  });
982
+ if (
983
+ expectedStepToolCallCount != null &&
984
+ observedStepToolCallCount < expectedStepToolCallCount
985
+ ) {
986
+ pauseAfterStepToolCalls = true;
987
+ continue;
988
+ }
965
989
  await finishForHostInputPause({ completeCurrentStep: true });
966
990
  return;
967
991
  }
968
992
  if (!isExecutableTool(activeTools[toolCall.toolName])) {
969
993
  recordPendingToolResult({ toolCall });
994
+ if (
995
+ expectedStepToolCallCount != null &&
996
+ observedStepToolCallCount < expectedStepToolCallCount
997
+ ) {
998
+ pauseAfterStepToolCalls = true;
999
+ continue;
1000
+ }
970
1001
  await finishForHostInputPause({ completeCurrentStep: true });
971
1002
  return;
972
1003
  }
@@ -1191,10 +1222,10 @@ async function maybeExecuteHostTool<TOOLS extends ToolSet>(input: {
1191
1222
  * part on failure (unknown tool, schema mismatch, malformed JSON).
1192
1223
  *
1193
1224
  * The harness `tool-call` event is structurally a `LanguageModelV4ToolCall`
1194
- * (plus an optional harness-only `nativeName`). `providerExecuted` already
1195
- * lives on the V4 type — `true` for adapter builtins (Claude Code's `Bash`,
1196
- * Codex's `shell`), false/undefined for host tools — and is passed through
1197
- * to the AI SDK part by `parseToolCall`.
1225
+ * plus optional harness-only fields. `providerExecuted` already lives on the
1226
+ * V4 type — `true` for adapter builtins (Claude Code's `Bash`, Codex's
1227
+ * `shell`), false/undefined for host tools — and is passed through to the AI
1228
+ * SDK part by `parseToolCall`.
1198
1229
  */
1199
1230
  export async function validateToolCall<TOOLS extends ToolSet>(args: {
1200
1231
  event: Extract<HarnessV1StreamPart, { type: 'tool-call' }>;
@@ -0,0 +1,58 @@
1
+ import type { HarnessV1CredentialForwarding } from '../v1';
2
+ import { generateSandboxCredentialPlaceholder } from './sandbox-credential-brokering';
3
+
4
+ export async function applyCredentialForwarding({
5
+ environment,
6
+ credentialEnvironmentVariables,
7
+ credentialForwarding,
8
+ }: {
9
+ environment: Readonly<Record<string, string>>;
10
+ credentialEnvironmentVariables: ReadonlyArray<string>;
11
+ credentialForwarding: HarnessV1CredentialForwarding | undefined;
12
+ }): Promise<Record<string, string>> {
13
+ const forwardedEnvironment = { ...environment };
14
+ if (credentialForwarding == null) return forwardedEnvironment;
15
+
16
+ for (const environmentVariableName of new Set(
17
+ credentialEnvironmentVariables,
18
+ )) {
19
+ const credential = forwardedEnvironment[environmentVariableName];
20
+ if (credential == null) continue;
21
+
22
+ forwardedEnvironment[environmentVariableName] = await credentialForwarding({
23
+ credential,
24
+ environmentVariableName,
25
+ });
26
+ }
27
+
28
+ return forwardedEnvironment;
29
+ }
30
+
31
+ export async function createSandboxCredentialEnvironment({
32
+ environment,
33
+ credentialEnvironmentVariables,
34
+ credentialForwarding,
35
+ }: {
36
+ environment: Readonly<Record<string, string>>;
37
+ credentialEnvironmentVariables: ReadonlyArray<string>;
38
+ credentialForwarding: HarnessV1CredentialForwarding | undefined;
39
+ }): Promise<Record<string, string>> {
40
+ const sandboxCredentialEnvironment: Record<string, string> = {};
41
+
42
+ for (const environmentVariableName of new Set(
43
+ credentialEnvironmentVariables,
44
+ )) {
45
+ if (environment[environmentVariableName] == null) continue;
46
+
47
+ const placeholder = generateSandboxCredentialPlaceholder();
48
+ sandboxCredentialEnvironment[environmentVariableName] =
49
+ credentialForwarding == null
50
+ ? placeholder
51
+ : await credentialForwarding({
52
+ credential: placeholder,
53
+ environmentVariableName,
54
+ });
55
+ }
56
+
57
+ return sandboxCredentialEnvironment;
58
+ }
@@ -12,8 +12,14 @@ export {
12
12
  } from './bridge-user-message-submitter';
13
13
  export { classifyDiskLog, type DiskLogRecoveryMode } from './classify-disk-log';
14
14
  export { getAiGatewayAuthFromEnv } from './ai-gateway-auth';
15
+ export {
16
+ applyCredentialForwarding,
17
+ createSandboxCredentialEnvironment,
18
+ } from './credential-forwarding';
15
19
  export {
16
20
  createCredentialRequestTransformation,
21
+ generateSandboxCredentialPlaceholder,
22
+ isSandboxCredentialPlaceholder,
17
23
  maskSandboxCredentials,
18
24
  warnCredentialBrokeringUnavailable,
19
25
  } from './sandbox-credential-brokering';
@@ -1,5 +1,16 @@
1
+ import { randomBytes } from 'node:crypto';
1
2
  import type { HarnessV1RequestTransformation } from '../v1';
2
3
 
4
+ const SANDBOX_CREDENTIAL_PLACEHOLDER_PREFIX = 'aisdkhc_';
5
+
6
+ export function generateSandboxCredentialPlaceholder(): string {
7
+ return `${SANDBOX_CREDENTIAL_PLACEHOLDER_PREFIX}${randomBytes(32).toString('base64url')}`;
8
+ }
9
+
10
+ export function isSandboxCredentialPlaceholder(value: string): boolean {
11
+ return /^aisdkhc_[A-Za-z0-9_-]{43}$/.test(value);
12
+ }
13
+
3
14
  export function warnCredentialBrokeringUnavailable(): void {
4
15
  console.warn(
5
16
  'The sandbox implementation does not support configuring request transformations, so credential brokering does not work. Falling back to less secure credential forwarding.',
@@ -23,19 +34,25 @@ export function maskSandboxCredentials({
23
34
  }
24
35
 
25
36
  export function createCredentialRequestTransformation({
26
- baseUrl,
27
- headers,
37
+ matchUrl,
38
+ matchHeaders,
39
+ transformHeaders,
28
40
  }: {
29
- baseUrl: string;
30
- headers: Readonly<Record<string, string>>;
41
+ matchUrl: string;
42
+ matchHeaders: Readonly<Record<string, string>>;
43
+ transformHeaders: Readonly<Record<string, string>>;
31
44
  }): HarnessV1RequestTransformation {
32
- const url = new URL(baseUrl);
45
+ const url = new URL(matchUrl);
33
46
  const pathname = url.pathname.replace(/\/+$/, '');
34
47
  return {
35
48
  match: {
36
49
  host: url.hostname,
37
50
  ...(pathname.length === 0 ? {} : { path: { startsWith: pathname } }),
51
+ headers: Object.entries(matchHeaders).map(([key, value]) => ({
52
+ key: { exact: key },
53
+ value: { exact: value },
54
+ })),
38
55
  },
39
- transform: { headers },
56
+ transform: { headers: transformHeaders },
40
57
  };
41
58
  }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Customizes a credential value immediately before a harness adapter forwards
3
+ * it into a sandbox process.
4
+ *
5
+ * This callback only controls the value exposed to sandbox processes. It does
6
+ * not restrict which credentials the harness adapter can discover, read, or
7
+ * otherwise access in the host process.
8
+ */
9
+ export type HarnessV1CredentialForwarding = (options: {
10
+ /**
11
+ * The credential value that the adapter would otherwise forward. This is a
12
+ * generated sandbox placeholder when credential brokering is available and
13
+ * the real credential otherwise. Use `isSandboxCredentialPlaceholder` from
14
+ * `@ai-sdk/harness/utils` to distinguish generated placeholders.
15
+ */
16
+ readonly credential: string;
17
+ /** The environment variable name used to expose the value in the sandbox. */
18
+ readonly environmentVariableName: string;
19
+ }) => string | PromiseLike<string>;
@@ -205,3 +205,9 @@ export type HarnessV1RequestTransformation = {
205
205
  readonly headers: Readonly<Record<string, string>>;
206
206
  };
207
207
  };
208
+
209
+ export type HarnessV1RequestTransformationSources<AuthenticationMode> = {
210
+ env: Record<string, string>;
211
+ sandboxEnv: Record<string, string>;
212
+ auth: AuthenticationMode;
213
+ };
@@ -60,9 +60,14 @@ export type HarnessV1StreamPart =
60
60
 
61
61
  // Tool calls, approvals, results — reuse V4 primitives.
62
62
  //
63
- // `nativeName` is the only harness-only extension on `tool-call`. It lets
64
- // adapters surface the runtime's native name for a builtin when it differs
65
- // from the wire `toolName` (e.g. `toolName: 'bash'`, `nativeName: 'Bash'`).
63
+ // `nativeName` lets adapters surface the runtime's native name for a builtin
64
+ // when it differs from the wire `toolName` (e.g. `toolName: 'bash'`,
65
+ // `nativeName: 'Bash'`).
66
+ //
67
+ // `stepToolCallCount` lets adapters that know a step's complete tool-call
68
+ // set up front report its cardinality. The host uses it to collect every
69
+ // approval/result request from the step before pausing; adapters that cannot
70
+ // know the count omit it and retain pause-on-first behavior.
66
71
  //
67
72
  // Whether the call was executed by the underlying runtime (Claude Code's
68
73
  // built-in `Bash`, Codex's `shell`) vs. needs host dispatch is signalled by
@@ -70,6 +75,11 @@ export type HarnessV1StreamPart =
70
75
  // `true` for runtime-executed builtins, false/undefined for host tools.
71
76
  | (LanguageModelV4ToolCall & {
72
77
  nativeName?: string;
78
+ /**
79
+ * Total tool calls in the current model step, when known before tool
80
+ * execution begins. Populate this on every tool call in the step.
81
+ */
82
+ stepToolCallCount?: number;
73
83
  })
74
84
  | LanguageModelV4ToolApprovalRequest
75
85
  | LanguageModelV4ToolResult
@@ -267,6 +277,7 @@ export const harnessV1ToolCallPartSchema = z.object({
267
277
  dynamic: z.boolean().optional(),
268
278
  providerMetadata: harnessV1ProviderMetadataSchema.optional(),
269
279
  nativeName: z.string().optional(),
280
+ stepToolCallCount: z.number().int().positive().optional(),
270
281
  });
271
282
 
272
283
  export const harnessV1ToolApprovalRequestPartSchema = z.object({
package/src/v1/index.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export type { HarnessV1 } from './harness-v1';
2
+ export type { HarnessV1CredentialForwarding } from './harness-v1-credential-forwarding';
2
3
  export type {
3
4
  HarnessV1Bootstrap,
4
5
  HarnessV1BootstrapCommand,
@@ -45,6 +46,7 @@ export type {
45
46
  HarnessV1NetworkSandboxSession,
46
47
  HarnessV1PortEndpoint,
47
48
  HarnessV1RequestTransformation,
49
+ HarnessV1RequestTransformationSources,
48
50
  } from './harness-v1-network-sandbox-session';
49
51
  export type { HarnessV1Skill } from './harness-v1-skill';
50
52
  export type { HarnessV1StreamPart } from './harness-v1-stream-part';