@ai-sdk/harness 1.0.123 → 1.0.126
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 +29 -0
- package/README.md +9 -8
- package/agent/index.ts +2 -0
- package/dist/agent/index.d.ts +31 -19
- package/dist/agent/index.js +484 -262
- package/dist/agent/index.js.map +1 -1
- package/dist/index.d.ts +39 -17
- package/dist/index.js.map +1 -1
- package/dist/utils/index.d.ts +7 -14
- package/dist/utils/index.js +63 -97
- package/dist/utils/index.js.map +1 -1
- package/package.json +4 -4
- package/src/agent/create-harness-sandbox-template.ts +80 -0
- package/src/agent/harness-agent-settings.ts +1 -0
- package/src/agent/harness-agent-types.ts +2 -0
- package/src/agent/harness-agent.ts +23 -0
- package/src/agent/internal/on-bootstrap-marker.ts +70 -0
- package/src/agent/internal/prepared-sandbox-identity.ts +42 -0
- package/src/agent/internal/run-prompt.ts +134 -19
- package/src/agent/internal/sandbox-bootstrap.ts +19 -0
- package/src/agent/internal/strip-work-dir.ts +57 -0
- package/src/agent/prepare-harness-sandbox-template.ts +5 -2
- package/src/agent/prepare-sandbox-for-harness.ts +7 -51
- package/src/v1/harness-v1-network-sandbox-session.ts +7 -14
- package/src/v1/harness-v1-sandbox-provider.ts +3 -2
- package/src/v1/harness-v1-sandbox-session-create-options.ts +14 -0
- package/src/v1/harness-v1-sandbox-session-resume-options.ts +10 -0
- package/src/v1/harness-v1-sandbox-template.ts +9 -0
- package/src/v1/index.ts +3 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-sdk/harness",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.126",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -45,8 +45,8 @@
|
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
47
|
"@ai-sdk/provider": "4.0.18",
|
|
48
|
-
"@ai-sdk/provider-utils": "5.0.
|
|
49
|
-
"ai": "7.0.
|
|
48
|
+
"@ai-sdk/provider-utils": "5.0.49",
|
|
49
|
+
"ai": "7.0.116"
|
|
50
50
|
},
|
|
51
51
|
"peerDependencies": {
|
|
52
52
|
"ws": "^8.21.0",
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
}
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|
|
61
|
-
"@ai-sdk/otel": "1.0.
|
|
61
|
+
"@ai-sdk/otel": "1.0.116",
|
|
62
62
|
"@opentelemetry/sdk-trace-base": "2.7.1",
|
|
63
63
|
"@types/node": "22.19.19",
|
|
64
64
|
"@types/ws": "^8.5.13",
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
HarnessAgentAdapter,
|
|
3
|
+
HarnessSandboxTemplate,
|
|
4
|
+
} from './harness-agent-types';
|
|
5
|
+
import type { HarnessAgentSandboxConfig } from './harness-agent-settings';
|
|
6
|
+
import { hashHarnessBootstrap } from './internal/bootstrap-recipe';
|
|
7
|
+
import { resolvePreparedSandboxIdentity } from './internal/prepared-sandbox-identity';
|
|
8
|
+
import {
|
|
9
|
+
normalizeSandboxWorkDir,
|
|
10
|
+
runSandboxBootstrap,
|
|
11
|
+
validateSandboxBootstrapSettings,
|
|
12
|
+
} from './internal/sandbox-bootstrap';
|
|
13
|
+
|
|
14
|
+
export async function createHarnessSandboxTemplate(options: {
|
|
15
|
+
readonly harnesses: ReadonlyArray<HarnessAgentAdapter>;
|
|
16
|
+
readonly sandboxConfig?: Omit<HarnessAgentSandboxConfig, 'onSession'>;
|
|
17
|
+
}): Promise<HarnessSandboxTemplate | undefined> {
|
|
18
|
+
if (options.harnesses.length === 0) {
|
|
19
|
+
throw new Error(
|
|
20
|
+
'createHarnessSandboxTemplate: at least one harness must be provided.',
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
const settings = options.sandboxConfig ?? {};
|
|
24
|
+
validateSandboxBootstrapSettings(settings);
|
|
25
|
+
const workDir =
|
|
26
|
+
settings.workDir == null
|
|
27
|
+
? undefined
|
|
28
|
+
: normalizeSandboxWorkDir(settings.workDir);
|
|
29
|
+
const harnesses = [
|
|
30
|
+
...new Map(
|
|
31
|
+
options.harnesses.map(harness => [harness.harnessId, harness]),
|
|
32
|
+
).values(),
|
|
33
|
+
].sort((a, b) => a.harnessId.localeCompare(b.harnessId));
|
|
34
|
+
const recipes = await Promise.all(
|
|
35
|
+
harnesses.map(async harness => {
|
|
36
|
+
const recipe = await harness.getBootstrap?.({});
|
|
37
|
+
return recipe == null
|
|
38
|
+
? undefined
|
|
39
|
+
: {
|
|
40
|
+
recipe,
|
|
41
|
+
identity: await hashHarnessBootstrap(recipe),
|
|
42
|
+
};
|
|
43
|
+
}),
|
|
44
|
+
);
|
|
45
|
+
const recipeIdentities: Record<string, string> = {};
|
|
46
|
+
for (let index = 0; index < harnesses.length; index++) {
|
|
47
|
+
const entry = recipes[index];
|
|
48
|
+
if (entry != null)
|
|
49
|
+
recipeIdentities[harnesses[index].harnessId] = entry.identity;
|
|
50
|
+
}
|
|
51
|
+
const identity = await resolvePreparedSandboxIdentity({
|
|
52
|
+
recipeIdentities,
|
|
53
|
+
bootstrapHash: settings.bootstrapHash,
|
|
54
|
+
workDir,
|
|
55
|
+
});
|
|
56
|
+
if (identity == null) return undefined;
|
|
57
|
+
return {
|
|
58
|
+
identity,
|
|
59
|
+
prepare: async ({ session, abortSignal }) => {
|
|
60
|
+
for (const entry of recipes) {
|
|
61
|
+
if (entry != null) {
|
|
62
|
+
await runSandboxBootstrap({
|
|
63
|
+
session,
|
|
64
|
+
recipe: entry.recipe,
|
|
65
|
+
recipeIdentity: entry.identity,
|
|
66
|
+
abortSignal,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
await runSandboxBootstrap({
|
|
71
|
+
session,
|
|
72
|
+
workDir,
|
|
73
|
+
onBootstrap: settings.onBootstrap,
|
|
74
|
+
bootstrapHash: settings.bootstrapHash,
|
|
75
|
+
skipOnBootstrapIfMarked: true,
|
|
76
|
+
abortSignal,
|
|
77
|
+
});
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
@@ -356,6 +356,7 @@ export type HarnessAgentSettings<
|
|
|
356
356
|
* sessions. When omitted, every `createSession()` call must provide an
|
|
357
357
|
* existing network sandbox session.
|
|
358
358
|
*/
|
|
359
|
+
/** @deprecated Supply `sandboxSession` to `HarnessAgent.createSession()` instead. */
|
|
359
360
|
readonly sandbox?: HarnessV1SandboxProvider;
|
|
360
361
|
|
|
361
362
|
/**
|
|
@@ -15,6 +15,7 @@ import type {
|
|
|
15
15
|
HarnessV1PromptControl,
|
|
16
16
|
HarnessV1PromptTurnOptions,
|
|
17
17
|
HarnessV1ResumeSessionState,
|
|
18
|
+
HarnessV1SandboxTemplate,
|
|
18
19
|
HarnessV1Session,
|
|
19
20
|
HarnessV1Skill,
|
|
20
21
|
HarnessV1StartOptions,
|
|
@@ -24,6 +25,7 @@ import type {
|
|
|
24
25
|
|
|
25
26
|
export type HarnessAgentAdapter<TBuiltinTools extends ToolSet = ToolSet> =
|
|
26
27
|
HarnessV1<TBuiltinTools>;
|
|
28
|
+
export type HarnessSandboxTemplate = HarnessV1SandboxTemplate;
|
|
27
29
|
|
|
28
30
|
export type HarnessAgentBuiltinTool<
|
|
29
31
|
INPUT = unknown,
|
|
@@ -48,6 +48,7 @@ import type {
|
|
|
48
48
|
HarnessAgentResumeSessionState,
|
|
49
49
|
HarnessAgentSkill,
|
|
50
50
|
HarnessAgentToolSpec,
|
|
51
|
+
HarnessSandboxTemplate,
|
|
51
52
|
} from './harness-agent-types';
|
|
52
53
|
import { collectHarnessAgentToolApprovalContinuations } from './harness-agent-tool-approval-continuation';
|
|
53
54
|
import { collectHarnessAgentToolResultContinuations } from './harness-agent-tool-result-continuation';
|
|
@@ -59,6 +60,7 @@ import {
|
|
|
59
60
|
createSandboxBootstrapPlan,
|
|
60
61
|
ensureSandboxDirectory,
|
|
61
62
|
resolveSessionWorkDir,
|
|
63
|
+
runSandboxBootstrap,
|
|
62
64
|
validateSandboxBootstrapSettings,
|
|
63
65
|
} from './internal/sandbox-bootstrap';
|
|
64
66
|
import { buildObservability } from './internal/resolve-observability';
|
|
@@ -72,6 +74,7 @@ import { resolveSandboxDefaultWorkingDirectory } from '../utils/resolve-sandbox-
|
|
|
72
74
|
import { resolveSandboxHomeDir } from '../utils/sandbox-home-dir';
|
|
73
75
|
import { getRestrictedSandboxSession } from '../utils/get-restricted-sandbox-session';
|
|
74
76
|
import type { HarnessAgentLifecycleCallbacks } from './internal/turn-telemetry';
|
|
77
|
+
import { createHarnessSandboxTemplate } from './create-harness-sandbox-template';
|
|
75
78
|
|
|
76
79
|
export type { HarnessAllTools } from './harness-agent-tool-types';
|
|
77
80
|
|
|
@@ -222,6 +225,11 @@ export class HarnessAgent<
|
|
|
222
225
|
CALL_OPTIONS
|
|
223
226
|
>,
|
|
224
227
|
) {
|
|
228
|
+
if (settings.sandbox != null) {
|
|
229
|
+
console.warn(
|
|
230
|
+
'HarnessAgent: `sandbox` is deprecated. Supply `sandboxSession` to createSession() instead.',
|
|
231
|
+
);
|
|
232
|
+
}
|
|
225
233
|
const sandboxConfig = resolveSandboxConfig(settings);
|
|
226
234
|
validateSandboxBootstrapSettings(sandboxConfig);
|
|
227
235
|
this.settings = settings;
|
|
@@ -290,6 +298,13 @@ export class HarnessAgent<
|
|
|
290
298
|
return this.settings.output != null;
|
|
291
299
|
}
|
|
292
300
|
|
|
301
|
+
getSandboxTemplate(): Promise<HarnessSandboxTemplate | undefined> {
|
|
302
|
+
return createHarnessSandboxTemplate({
|
|
303
|
+
harnesses: [this.settings.harness],
|
|
304
|
+
sandboxConfig: this.sandboxConfig,
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
|
|
293
308
|
/**
|
|
294
309
|
* Start a fresh session, or resume from state previously returned by
|
|
295
310
|
* `session.detach()` or `session.stop()`. The returned
|
|
@@ -540,6 +555,14 @@ export class HarnessAgent<
|
|
|
540
555
|
}
|
|
541
556
|
|
|
542
557
|
try {
|
|
558
|
+
await runSandboxBootstrap({
|
|
559
|
+
session: getRestrictedSandboxSession(sandboxSession),
|
|
560
|
+
workDir: this.sandboxConfig.workDir,
|
|
561
|
+
onBootstrap: this.sandboxConfig.onBootstrap,
|
|
562
|
+
bootstrapHash: this.sandboxConfig.bootstrapHash,
|
|
563
|
+
skipOnBootstrapIfMarked: true,
|
|
564
|
+
abortSignal,
|
|
565
|
+
});
|
|
543
566
|
await ensureSandboxDirectory({
|
|
544
567
|
session: sandboxSession,
|
|
545
568
|
workDir: sessionWorkDir,
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { posix } from 'node:path';
|
|
2
|
+
import type { Experimental_SandboxSession as SandboxSession } from '@ai-sdk/provider-utils';
|
|
3
|
+
import { harnessStateDirectoryPath } from '../../v1';
|
|
4
|
+
import { resolveSandboxHomeDir } from '../../utils/sandbox-home-dir';
|
|
5
|
+
|
|
6
|
+
export async function onBootstrapMarkerPath({
|
|
7
|
+
session,
|
|
8
|
+
bootstrapHash,
|
|
9
|
+
abortSignal,
|
|
10
|
+
}: {
|
|
11
|
+
session: SandboxSession;
|
|
12
|
+
bootstrapHash: string;
|
|
13
|
+
abortSignal?: AbortSignal;
|
|
14
|
+
}): Promise<string> {
|
|
15
|
+
const digest = new Uint8Array(
|
|
16
|
+
await crypto.subtle.digest(
|
|
17
|
+
'SHA-256',
|
|
18
|
+
new TextEncoder().encode(bootstrapHash),
|
|
19
|
+
),
|
|
20
|
+
);
|
|
21
|
+
const filename = Array.from(digest, byte =>
|
|
22
|
+
byte.toString(16).padStart(2, '0'),
|
|
23
|
+
).join('');
|
|
24
|
+
return posix.join(
|
|
25
|
+
harnessStateDirectoryPath({
|
|
26
|
+
sandboxHomeDir: await resolveSandboxHomeDir({
|
|
27
|
+
sandbox: session,
|
|
28
|
+
abortSignal,
|
|
29
|
+
}),
|
|
30
|
+
}),
|
|
31
|
+
'.on-bootstrap',
|
|
32
|
+
`${filename}.ok`,
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function hasOnBootstrapMarker(options: {
|
|
37
|
+
session: SandboxSession;
|
|
38
|
+
bootstrapHash: string;
|
|
39
|
+
abortSignal?: AbortSignal;
|
|
40
|
+
}): Promise<boolean> {
|
|
41
|
+
return (
|
|
42
|
+
(await options.session.readTextFile({
|
|
43
|
+
path: await onBootstrapMarkerPath(options),
|
|
44
|
+
abortSignal: options.abortSignal,
|
|
45
|
+
})) !== null
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function writeOnBootstrapMarker(options: {
|
|
50
|
+
session: SandboxSession;
|
|
51
|
+
bootstrapHash: string;
|
|
52
|
+
abortSignal?: AbortSignal;
|
|
53
|
+
}): Promise<void> {
|
|
54
|
+
const path = await onBootstrapMarkerPath(options);
|
|
55
|
+
const result = await options.session.run({
|
|
56
|
+
command: 'mkdir -p "$MARKER_DIR"',
|
|
57
|
+
env: { MARKER_DIR: posix.dirname(path) },
|
|
58
|
+
abortSignal: options.abortSignal,
|
|
59
|
+
});
|
|
60
|
+
if (result.exitCode !== 0) {
|
|
61
|
+
throw new Error(
|
|
62
|
+
`Failed to create onBootstrap marker directory: ${result.stderr || result.stdout}`,
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
await options.session.writeTextFile({
|
|
66
|
+
path,
|
|
67
|
+
content: '',
|
|
68
|
+
abortSignal: options.abortSignal,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
const PREPARED_SANDBOX_IDENTITY_VERSION = 1;
|
|
2
|
+
|
|
3
|
+
export async function resolvePreparedSandboxIdentity({
|
|
4
|
+
recipeIdentities,
|
|
5
|
+
bootstrapHash,
|
|
6
|
+
workDir,
|
|
7
|
+
}: {
|
|
8
|
+
readonly recipeIdentities: Record<string, string>;
|
|
9
|
+
readonly bootstrapHash?: string;
|
|
10
|
+
readonly workDir?: string;
|
|
11
|
+
}): Promise<string | undefined> {
|
|
12
|
+
const entries = Object.entries(recipeIdentities).sort(([a], [b]) =>
|
|
13
|
+
a.localeCompare(b),
|
|
14
|
+
);
|
|
15
|
+
if (entries.length === 0 && bootstrapHash == null) return undefined;
|
|
16
|
+
|
|
17
|
+
const encoder = new TextEncoder();
|
|
18
|
+
const chunks: Uint8Array[] = [];
|
|
19
|
+
const pushString = (value: string) => {
|
|
20
|
+
chunks.push(encoder.encode(value));
|
|
21
|
+
chunks.push(encoder.encode('\0'));
|
|
22
|
+
};
|
|
23
|
+
pushString(String(PREPARED_SANDBOX_IDENTITY_VERSION));
|
|
24
|
+
pushString(workDir ?? '');
|
|
25
|
+
pushString(bootstrapHash ?? '');
|
|
26
|
+
for (const [harnessId, identity] of entries) {
|
|
27
|
+
pushString(harnessId);
|
|
28
|
+
pushString(identity);
|
|
29
|
+
}
|
|
30
|
+
const buffer = new Uint8Array(
|
|
31
|
+
chunks.reduce((sum, chunk) => sum + chunk.length, 0),
|
|
32
|
+
);
|
|
33
|
+
let offset = 0;
|
|
34
|
+
for (const chunk of chunks) {
|
|
35
|
+
buffer.set(chunk, offset);
|
|
36
|
+
offset += chunk.length;
|
|
37
|
+
}
|
|
38
|
+
const digest = new Uint8Array(await crypto.subtle.digest('SHA-256', buffer));
|
|
39
|
+
return Array.from(digest.slice(0, 8), byte =>
|
|
40
|
+
byte.toString(16).padStart(2, '0'),
|
|
41
|
+
).join('');
|
|
42
|
+
}
|
|
@@ -53,7 +53,11 @@ import type { HarnessAgentToolApprovalConfiguration } from '../harness-agent-set
|
|
|
53
53
|
import { HarnessStreamTextResult } from './harness-stream-text-result';
|
|
54
54
|
import { getOwn } from './get-own';
|
|
55
55
|
import { translateStreamPart } from './translate-stream-part';
|
|
56
|
-
import {
|
|
56
|
+
import {
|
|
57
|
+
createToolInputWorkDirStripper,
|
|
58
|
+
stripParsedToolInputWorkDir,
|
|
59
|
+
stripWorkDir,
|
|
60
|
+
} from './strip-work-dir';
|
|
57
61
|
import {
|
|
58
62
|
createTurnLifecycle,
|
|
59
63
|
type HarnessAgentLifecycleCallbacks,
|
|
@@ -63,6 +67,8 @@ import { resolveCustomToolApproval } from './permission-mode';
|
|
|
63
67
|
import { logBridgeError } from '../../utils/bridge-diagnostics';
|
|
64
68
|
import { pinSandboxChannelEventCheckpoint } from '../../utils/sandbox-channel';
|
|
65
69
|
|
|
70
|
+
const invalidToolInputMessage = 'Tool input validation failed.';
|
|
71
|
+
|
|
66
72
|
function unwrapToolResultOutput(toolResult: ToolResultPart): {
|
|
67
73
|
output: unknown;
|
|
68
74
|
isError?: boolean;
|
|
@@ -647,7 +653,9 @@ export function runPrompt<
|
|
|
647
653
|
continuation: ToolApprovalResponse,
|
|
648
654
|
): Promise<'continued' | 'awaiting-tool-result'> => {
|
|
649
655
|
const rawToolCall =
|
|
650
|
-
|
|
656
|
+
(approval.kind === 'builtin'
|
|
657
|
+
? rawToolCallsByToolCallId.get(approval.toolCallId)
|
|
658
|
+
: undefined) ??
|
|
651
659
|
({
|
|
652
660
|
type: 'tool-call',
|
|
653
661
|
toolCallId: approval.toolCallId,
|
|
@@ -656,16 +664,31 @@ export function runPrompt<
|
|
|
656
664
|
providerExecuted: approval.providerExecuted,
|
|
657
665
|
nativeName: approval.nativeName,
|
|
658
666
|
} satisfies Extract<HarnessV1StreamPart, { type: 'tool-call' }>);
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
667
|
+
let validatedToolCall: ToolCallTextStreamPart | undefined;
|
|
668
|
+
let toolCall: ToolCallTextStreamPart;
|
|
669
|
+
if (approval.kind === 'custom' && continuation.approved) {
|
|
670
|
+
validatedToolCall = asToolCallTextStreamPart({
|
|
671
|
+
part: await validateToolCall({
|
|
672
|
+
event: rawToolCall,
|
|
673
|
+
tools: input.tools,
|
|
674
|
+
}),
|
|
675
|
+
});
|
|
676
|
+
toolCall = displayHostToolCall({
|
|
677
|
+
toolCall: validatedToolCall,
|
|
678
|
+
sessionWorkDir: input.sessionWorkDir,
|
|
679
|
+
});
|
|
680
|
+
} else {
|
|
681
|
+
const parsedInput = await safeParseJSON({ text: rawToolCall.input });
|
|
682
|
+
toolCall = {
|
|
683
|
+
type: 'tool-call',
|
|
684
|
+
toolCallId: rawToolCall.toolCallId,
|
|
685
|
+
toolName: rawToolCall.toolName,
|
|
686
|
+
input: parsedInput.success ? parsedInput.value : rawToolCall.input,
|
|
687
|
+
...(rawToolCall.providerExecuted !== undefined
|
|
688
|
+
? { providerExecuted: rawToolCall.providerExecuted }
|
|
689
|
+
: {}),
|
|
690
|
+
};
|
|
691
|
+
}
|
|
669
692
|
|
|
670
693
|
enqueueApprovalResponse(approval, continuation, toolCall);
|
|
671
694
|
onToolApprovalSettled(approval.approvalId);
|
|
@@ -699,6 +722,29 @@ export function runPrompt<
|
|
|
699
722
|
return 'continued';
|
|
700
723
|
}
|
|
701
724
|
|
|
725
|
+
if (validatedToolCall == null) {
|
|
726
|
+
throw new Error(
|
|
727
|
+
`Harness '${input.harness.harnessId}' could not validate approved host tool '${approval.toolName}'.`,
|
|
728
|
+
);
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
if (validatedToolCall.invalid) {
|
|
732
|
+
const error = new Error(invalidToolInputMessage);
|
|
733
|
+
await submitToolResult({
|
|
734
|
+
toolCallId: approval.toolCallId,
|
|
735
|
+
output: { error: invalidToolInputMessage },
|
|
736
|
+
isError: true,
|
|
737
|
+
});
|
|
738
|
+
bufferedToolOutcomes.push(() => {
|
|
739
|
+
enqueueHostToolOutcome({
|
|
740
|
+
toolCall,
|
|
741
|
+
outcome: { ok: false, error },
|
|
742
|
+
});
|
|
743
|
+
});
|
|
744
|
+
await publishToolExecutions();
|
|
745
|
+
return 'continued';
|
|
746
|
+
}
|
|
747
|
+
|
|
702
748
|
await lifecycle.start(input.model);
|
|
703
749
|
toolExecutions.set(rawToolCall.toolCallId, {
|
|
704
750
|
toolCall: toolCall as TypedToolCall<TOOLS>,
|
|
@@ -706,6 +752,7 @@ export function runPrompt<
|
|
|
706
752
|
const executionStartedAt = Date.now();
|
|
707
753
|
const execution = await maybeExecuteHostTool({
|
|
708
754
|
event: rawToolCall,
|
|
755
|
+
parsedToolCall: validatedToolCall,
|
|
709
756
|
tools: activeTools,
|
|
710
757
|
toolsContext,
|
|
711
758
|
wrappedExecuteTool: lifecycle.executeTool,
|
|
@@ -886,7 +933,6 @@ export function runPrompt<
|
|
|
886
933
|
if (settledHostInputReplay || settledBuiltinApprovalReplay) {
|
|
887
934
|
continue;
|
|
888
935
|
}
|
|
889
|
-
|
|
890
936
|
if (displayValue.type === 'finish-step' && closingResumedStep) {
|
|
891
937
|
closingResumedStep = false;
|
|
892
938
|
await publishToolExecutions();
|
|
@@ -971,15 +1017,34 @@ export function runPrompt<
|
|
|
971
1017
|
|
|
972
1018
|
// Tool-call validation lives here (not in translateStreamPart) because
|
|
973
1019
|
// schema parsing is async and needs the merged tool set in scope.
|
|
1020
|
+
let validatedHostToolCall: ToolCallTextStreamPart | undefined;
|
|
974
1021
|
if (displayValue.type === 'tool-call') {
|
|
1022
|
+
const isHostTool =
|
|
1023
|
+
value.type === 'tool-call' &&
|
|
1024
|
+
!value.providerExecuted &&
|
|
1025
|
+
hasTool({ tools: activeTools, toolName: value.toolName }) &&
|
|
1026
|
+
!(
|
|
1027
|
+
value.toolName === 'askUserQuestions' &&
|
|
1028
|
+
hasTool({
|
|
1029
|
+
tools: input.harness.builtinTools,
|
|
1030
|
+
toolName: value.toolName,
|
|
1031
|
+
})
|
|
1032
|
+
);
|
|
975
1033
|
const parsed = await validateToolCall<TOOLS>({
|
|
976
|
-
event: displayValue,
|
|
1034
|
+
event: isHostTool ? value : displayValue,
|
|
977
1035
|
tools: input.tools,
|
|
978
1036
|
});
|
|
979
1037
|
const parsedToolCall = asToolCallTextStreamPart({ part: parsed });
|
|
1038
|
+
validatedHostToolCall = isHostTool ? parsedToolCall : undefined;
|
|
1039
|
+
const displayToolCall = isHostTool
|
|
1040
|
+
? displayHostToolCall({
|
|
1041
|
+
toolCall: parsedToolCall,
|
|
1042
|
+
sessionWorkDir: input.sessionWorkDir,
|
|
1043
|
+
})
|
|
1044
|
+
: parsedToolCall;
|
|
980
1045
|
rawToolCallsByToolCallId.set(displayValue.toolCallId, displayValue);
|
|
981
|
-
toolCallsByToolCallId.set(displayValue.toolCallId,
|
|
982
|
-
result.enqueue(
|
|
1046
|
+
toolCallsByToolCallId.set(displayValue.toolCallId, displayToolCall);
|
|
1047
|
+
result.enqueue(displayToolCall as TextStreamPart<TOOLS>);
|
|
983
1048
|
}
|
|
984
1049
|
|
|
985
1050
|
if (value.type === 'text-delta') {
|
|
@@ -1155,6 +1220,31 @@ export function runPrompt<
|
|
|
1155
1220
|
await finishForHostInputPause({ completeCurrentStep: true });
|
|
1156
1221
|
return;
|
|
1157
1222
|
}
|
|
1223
|
+
if (validatedHostToolCall == null) {
|
|
1224
|
+
throw new Error(
|
|
1225
|
+
`Harness '${input.harness.harnessId}' could not validate host tool '${toolCall.toolName}'.`,
|
|
1226
|
+
);
|
|
1227
|
+
}
|
|
1228
|
+
if (validatedHostToolCall.invalid) {
|
|
1229
|
+
settledHostToolCallIds.add(toolCall.toolCallId);
|
|
1230
|
+
toolExecutions.delete(toolCall.toolCallId);
|
|
1231
|
+
bufferedToolOutcomes.push(() => {
|
|
1232
|
+
result.enqueue({
|
|
1233
|
+
type: 'tool-error',
|
|
1234
|
+
toolCallId: toolCall.toolCallId,
|
|
1235
|
+
toolName: toolCall.toolName,
|
|
1236
|
+
input: parsedToolCall.input,
|
|
1237
|
+
error: new Error(invalidToolInputMessage),
|
|
1238
|
+
dynamic: true,
|
|
1239
|
+
} as TextStreamPart<TOOLS>);
|
|
1240
|
+
});
|
|
1241
|
+
await submitToolResult({
|
|
1242
|
+
toolCallId: toolCall.toolCallId,
|
|
1243
|
+
output: { error: invalidToolInputMessage },
|
|
1244
|
+
isError: true,
|
|
1245
|
+
});
|
|
1246
|
+
continue;
|
|
1247
|
+
}
|
|
1158
1248
|
const customToolApprovalDecision = resolveCustomToolApproval({
|
|
1159
1249
|
toolName: toolCall.toolName,
|
|
1160
1250
|
toolApproval: input.toolApproval,
|
|
@@ -1266,6 +1356,7 @@ export function runPrompt<
|
|
|
1266
1356
|
const executionStartedAt = Date.now();
|
|
1267
1357
|
const execution = await maybeExecuteHostTool({
|
|
1268
1358
|
event: toolCall,
|
|
1359
|
+
parsedToolCall: validatedHostToolCall,
|
|
1269
1360
|
tools: activeTools,
|
|
1270
1361
|
toolsContext,
|
|
1271
1362
|
wrappedExecuteTool: lifecycle.executeTool,
|
|
@@ -1414,6 +1505,22 @@ function asToolCallTextStreamPart<TOOLS extends ToolSet>(input: {
|
|
|
1414
1505
|
return input.part as ToolCallTextStreamPart;
|
|
1415
1506
|
}
|
|
1416
1507
|
|
|
1508
|
+
function displayHostToolCall(input: {
|
|
1509
|
+
toolCall: ToolCallTextStreamPart;
|
|
1510
|
+
sessionWorkDir: string;
|
|
1511
|
+
}): ToolCallTextStreamPart {
|
|
1512
|
+
return {
|
|
1513
|
+
...input.toolCall,
|
|
1514
|
+
input: stripParsedToolInputWorkDir({
|
|
1515
|
+
value: input.toolCall.input,
|
|
1516
|
+
sessionWorkDir: input.sessionWorkDir,
|
|
1517
|
+
}),
|
|
1518
|
+
...(input.toolCall.invalid
|
|
1519
|
+
? { error: new Error(invalidToolInputMessage) }
|
|
1520
|
+
: {}),
|
|
1521
|
+
};
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1417
1524
|
type ToolCallTextStreamPart = {
|
|
1418
1525
|
readonly type: 'tool-call';
|
|
1419
1526
|
readonly toolCallId: string;
|
|
@@ -1433,6 +1540,7 @@ function hasTool(input: { tools: ToolSet; toolName: string }): boolean {
|
|
|
1433
1540
|
|
|
1434
1541
|
async function maybeExecuteHostTool<TOOLS extends ToolSet>(input: {
|
|
1435
1542
|
event: { toolCallId: string; toolName: string; input: string };
|
|
1543
|
+
parsedToolCall: ToolCallTextStreamPart;
|
|
1436
1544
|
tools: TOOLS;
|
|
1437
1545
|
toolsContext: InferToolSetContext<TOOLS>;
|
|
1438
1546
|
wrappedExecuteTool: TurnLifecycle<ToolSet, Context>['executeTool'];
|
|
@@ -1450,8 +1558,15 @@ async function maybeExecuteHostTool<TOOLS extends ToolSet>(input: {
|
|
|
1450
1558
|
|
|
1451
1559
|
if (!isExecutableTool(tool)) return { executed: false };
|
|
1452
1560
|
|
|
1453
|
-
|
|
1454
|
-
|
|
1561
|
+
if (input.parsedToolCall.invalid) {
|
|
1562
|
+
const error = new Error(invalidToolInputMessage);
|
|
1563
|
+
await input.submitToolResult({
|
|
1564
|
+
toolCallId: input.event.toolCallId,
|
|
1565
|
+
output: { error: invalidToolInputMessage },
|
|
1566
|
+
isError: true,
|
|
1567
|
+
});
|
|
1568
|
+
return { executed: true, outcome: { ok: false, error } };
|
|
1569
|
+
}
|
|
1455
1570
|
|
|
1456
1571
|
let context: unknown;
|
|
1457
1572
|
try {
|
|
@@ -1489,7 +1604,7 @@ async function maybeExecuteHostTool<TOOLS extends ToolSet>(input: {
|
|
|
1489
1604
|
let output: unknown;
|
|
1490
1605
|
const stream = executeTool({
|
|
1491
1606
|
tool,
|
|
1492
|
-
input:
|
|
1607
|
+
input: input.parsedToolCall.input as never,
|
|
1493
1608
|
options: {
|
|
1494
1609
|
toolCallId: input.event.toolCallId,
|
|
1495
1610
|
messages: [],
|
|
@@ -6,6 +6,10 @@ import { resolveSandboxDefaultWorkingDirectory } from '../../utils/resolve-sandb
|
|
|
6
6
|
import { resolveSandboxHomeDir } from '../../utils/sandbox-home-dir';
|
|
7
7
|
import type { HarnessAgentSandboxConfig } from '../harness-agent-settings';
|
|
8
8
|
import { applyBootstrapRecipe, hashHarnessBootstrap } from './bootstrap-recipe';
|
|
9
|
+
import {
|
|
10
|
+
hasOnBootstrapMarker,
|
|
11
|
+
writeOnBootstrapMarker,
|
|
12
|
+
} from './on-bootstrap-marker';
|
|
9
13
|
|
|
10
14
|
const SANDBOX_BOOTSTRAP_IDENTITY_VERSION = 1;
|
|
11
15
|
|
|
@@ -125,6 +129,7 @@ export async function createSandboxBootstrapPlan({
|
|
|
125
129
|
recipeIdentity,
|
|
126
130
|
workDir,
|
|
127
131
|
onBootstrap: settings.onBootstrap,
|
|
132
|
+
bootstrapHash: settings.bootstrapHash,
|
|
128
133
|
abortSignal: opts.abortSignal,
|
|
129
134
|
}),
|
|
130
135
|
}
|
|
@@ -138,6 +143,8 @@ export async function runSandboxBootstrap({
|
|
|
138
143
|
recipeIdentity,
|
|
139
144
|
workDir,
|
|
140
145
|
onBootstrap,
|
|
146
|
+
bootstrapHash,
|
|
147
|
+
skipOnBootstrapIfMarked = false,
|
|
141
148
|
defaultWorkingDirectory,
|
|
142
149
|
abortSignal,
|
|
143
150
|
}: {
|
|
@@ -146,6 +153,8 @@ export async function runSandboxBootstrap({
|
|
|
146
153
|
readonly recipeIdentity?: string;
|
|
147
154
|
readonly workDir?: string;
|
|
148
155
|
readonly onBootstrap?: SandboxBootstrapSettings['onBootstrap'];
|
|
156
|
+
readonly bootstrapHash?: string;
|
|
157
|
+
readonly skipOnBootstrapIfMarked?: boolean;
|
|
149
158
|
readonly defaultWorkingDirectory?: string;
|
|
150
159
|
readonly abortSignal?: AbortSignal;
|
|
151
160
|
}): Promise<void> {
|
|
@@ -173,6 +182,13 @@ export async function runSandboxBootstrap({
|
|
|
173
182
|
|
|
174
183
|
if (onBootstrap == null) return;
|
|
175
184
|
|
|
185
|
+
if (
|
|
186
|
+
skipOnBootstrapIfMarked &&
|
|
187
|
+
bootstrapHash != null &&
|
|
188
|
+
(await hasOnBootstrapMarker({ session, bootstrapHash, abortSignal }))
|
|
189
|
+
)
|
|
190
|
+
return;
|
|
191
|
+
|
|
176
192
|
const resolvedDefaultWorkingDirectory =
|
|
177
193
|
defaultWorkingDirectory ??
|
|
178
194
|
(await resolveSandboxDefaultWorkingDirectory({
|
|
@@ -194,6 +210,9 @@ export async function runSandboxBootstrap({
|
|
|
194
210
|
abortSignal,
|
|
195
211
|
});
|
|
196
212
|
await onBootstrap({ session, workDir: bootstrapWorkDir, abortSignal });
|
|
213
|
+
if (bootstrapHash != null) {
|
|
214
|
+
await writeOnBootstrapMarker({ session, bootstrapHash, abortSignal });
|
|
215
|
+
}
|
|
197
216
|
}
|
|
198
217
|
|
|
199
218
|
export async function ensureSandboxDirectory({
|
|
@@ -120,6 +120,63 @@ export function stripWorkDir(
|
|
|
120
120
|
}
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
+
export function stripParsedToolInputWorkDir({
|
|
124
|
+
value,
|
|
125
|
+
sessionWorkDir,
|
|
126
|
+
}: {
|
|
127
|
+
value: unknown;
|
|
128
|
+
sessionWorkDir: string;
|
|
129
|
+
}): unknown {
|
|
130
|
+
if (sessionWorkDir.length === 0) return value;
|
|
131
|
+
|
|
132
|
+
const projected = new WeakMap<object, unknown>();
|
|
133
|
+
const pending: Array<{
|
|
134
|
+
source: object;
|
|
135
|
+
target: Record<string, unknown> | unknown[];
|
|
136
|
+
}> = [];
|
|
137
|
+
|
|
138
|
+
const project = (item: unknown): unknown => {
|
|
139
|
+
if (typeof item === 'string') return stripString(item, sessionWorkDir);
|
|
140
|
+
if (item === null || typeof item !== 'object') return item;
|
|
141
|
+
if (
|
|
142
|
+
!Array.isArray(item) &&
|
|
143
|
+
Object.getPrototypeOf(item) !== Object.prototype &&
|
|
144
|
+
Object.getPrototypeOf(item) !== null
|
|
145
|
+
) {
|
|
146
|
+
return item;
|
|
147
|
+
}
|
|
148
|
+
if (projected.has(item)) return projected.get(item);
|
|
149
|
+
|
|
150
|
+
const target: Record<string, unknown> | unknown[] = Array.isArray(item)
|
|
151
|
+
? new Array(item.length)
|
|
152
|
+
: {};
|
|
153
|
+
projected.set(item, target);
|
|
154
|
+
pending.push({ source: item, target });
|
|
155
|
+
return target;
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const result = project(value);
|
|
159
|
+
while (pending.length > 0) {
|
|
160
|
+
const { source, target } = pending.pop()!;
|
|
161
|
+
if (Array.isArray(source) && Array.isArray(target)) {
|
|
162
|
+
for (let index = 0; index < source.length; index++) {
|
|
163
|
+
if (index in source) target[index] = project(source[index]);
|
|
164
|
+
}
|
|
165
|
+
} else {
|
|
166
|
+
for (const [key, item] of Object.entries(source)) {
|
|
167
|
+
Object.defineProperty(target, key, {
|
|
168
|
+
value: project(item),
|
|
169
|
+
enumerable: true,
|
|
170
|
+
writable: true,
|
|
171
|
+
configurable: true,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return result;
|
|
178
|
+
}
|
|
179
|
+
|
|
123
180
|
/**
|
|
124
181
|
* Replace occurrences of the working directory in a string. A reference to the
|
|
125
182
|
* directory followed by a separator becomes workspace-relative
|