@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.
- package/CHANGELOG.md +39 -0
- package/dist/agent/index.d.ts +5 -0
- package/dist/agent/index.js +20 -0
- package/dist/agent/index.js.map +1 -1
- package/dist/index.d.ts +34 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/utils/index.d.ts +38 -4
- package/dist/utils/index.js +63 -6
- package/dist/utils/index.js.map +1 -1
- package/package.json +7 -7
- package/src/agent/internal/run-prompt.ts +35 -4
- package/src/utils/credential-forwarding.ts +58 -0
- package/src/utils/index.ts +6 -0
- package/src/utils/sandbox-credential-brokering.ts +23 -6
- package/src/v1/harness-v1-credential-forwarding.ts +19 -0
- package/src/v1/harness-v1-network-sandbox-session.ts +6 -0
- package/src/v1/harness-v1-stream-part.ts +14 -3
- package/src/v1/index.ts +2 -0
|
@@ -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
|
-
*
|
|
1195
|
-
*
|
|
1196
|
-
*
|
|
1197
|
-
*
|
|
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
|
+
}
|
package/src/utils/index.ts
CHANGED
|
@@ -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
|
-
|
|
27
|
-
|
|
37
|
+
matchUrl,
|
|
38
|
+
matchHeaders,
|
|
39
|
+
transformHeaders,
|
|
28
40
|
}: {
|
|
29
|
-
|
|
30
|
-
|
|
41
|
+
matchUrl: string;
|
|
42
|
+
matchHeaders: Readonly<Record<string, string>>;
|
|
43
|
+
transformHeaders: Readonly<Record<string, string>>;
|
|
31
44
|
}): HarnessV1RequestTransformation {
|
|
32
|
-
const url = new URL(
|
|
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`
|
|
64
|
-
//
|
|
65
|
-
//
|
|
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';
|