@ai-sdk/harness 1.0.124 → 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 +20 -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 +340 -248
- 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/sandbox-bootstrap.ts +19 -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
|
+
}
|
|
@@ -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({
|
|
@@ -25,8 +25,8 @@ type SandboxBootstrapSettings = Omit<HarnessAgentSandboxConfig, 'onSession'>;
|
|
|
25
25
|
*
|
|
26
26
|
* The temporary network sandbox session created during preparation is stopped
|
|
27
27
|
* before the function resolves; the snapshot/template state persists in the
|
|
28
|
-
* provider's native storage
|
|
29
|
-
*
|
|
28
|
+
* provider's native storage.
|
|
29
|
+
* @deprecated Use `createHarnessSandboxTemplate` and pass its template to a sandbox session creator instead.
|
|
30
30
|
*/
|
|
31
31
|
export async function prepareHarnessSandboxTemplate(options: {
|
|
32
32
|
readonly harness: HarnessAgentAdapter;
|
|
@@ -34,6 +34,9 @@ export async function prepareHarnessSandboxTemplate(options: {
|
|
|
34
34
|
readonly sandboxConfig?: SandboxBootstrapSettings;
|
|
35
35
|
readonly abortSignal?: AbortSignal;
|
|
36
36
|
}): Promise<void> {
|
|
37
|
+
console.warn(
|
|
38
|
+
'prepareHarnessSandboxTemplate (prewarmHarness) is deprecated. Use createHarnessSandboxTemplate and a sandbox session creator instead.',
|
|
39
|
+
);
|
|
37
40
|
const sandboxConfig = options.sandboxConfig ?? {};
|
|
38
41
|
validateSandboxBootstrapSettings(sandboxConfig);
|
|
39
42
|
|
|
@@ -13,9 +13,9 @@ import {
|
|
|
13
13
|
runSandboxBootstrap,
|
|
14
14
|
validateSandboxBootstrapSettings,
|
|
15
15
|
} from './internal/sandbox-bootstrap';
|
|
16
|
+
import { resolvePreparedSandboxIdentity } from './internal/prepared-sandbox-identity';
|
|
16
17
|
|
|
17
|
-
|
|
18
|
-
|
|
18
|
+
/** @deprecated Use `createHarnessSandboxTemplate` and `template.prepare` instead. */
|
|
19
19
|
export type PrepareSandboxForHarnessResult = {
|
|
20
20
|
readonly identity?: string;
|
|
21
21
|
readonly recipeIdentities: Record<string, string>;
|
|
@@ -41,6 +41,7 @@ export type PrepareSandboxForHarnessResult = {
|
|
|
41
41
|
*
|
|
42
42
|
* Repeated harness IDs are prepared once. When multiple adapters use the same
|
|
43
43
|
* ID, the last adapter in `harnesses` is used.
|
|
44
|
+
* @deprecated Use `createHarnessSandboxTemplate` and `template.prepare` instead.
|
|
44
45
|
*/
|
|
45
46
|
export async function prepareSandboxForHarness(options: {
|
|
46
47
|
readonly session: SandboxSession;
|
|
@@ -48,6 +49,9 @@ export async function prepareSandboxForHarness(options: {
|
|
|
48
49
|
readonly sandboxConfig?: HarnessAgentSandboxConfig;
|
|
49
50
|
readonly abortSignal?: AbortSignal;
|
|
50
51
|
}): Promise<PrepareSandboxForHarnessResult> {
|
|
52
|
+
console.warn(
|
|
53
|
+
'prepareSandboxForHarness is deprecated. Use createHarnessSandboxTemplate and template.prepare instead.',
|
|
54
|
+
);
|
|
51
55
|
const sandboxConfig = options.sandboxConfig ?? {};
|
|
52
56
|
validateSandboxBootstrapSettings(sandboxConfig);
|
|
53
57
|
|
|
@@ -107,6 +111,7 @@ export async function prepareSandboxForHarness(options: {
|
|
|
107
111
|
session: options.session,
|
|
108
112
|
workDir,
|
|
109
113
|
onBootstrap: sandboxConfig.onBootstrap,
|
|
114
|
+
bootstrapHash: sandboxConfig.bootstrapHash,
|
|
110
115
|
defaultWorkingDirectory,
|
|
111
116
|
abortSignal: options.abortSignal,
|
|
112
117
|
});
|
|
@@ -124,52 +129,3 @@ export async function prepareSandboxForHarness(options: {
|
|
|
124
129
|
skippedHarnessIds,
|
|
125
130
|
};
|
|
126
131
|
}
|
|
127
|
-
|
|
128
|
-
async function resolvePreparedSandboxIdentity({
|
|
129
|
-
recipeIdentities,
|
|
130
|
-
bootstrapHash,
|
|
131
|
-
workDir,
|
|
132
|
-
}: {
|
|
133
|
-
readonly recipeIdentities: Record<string, string>;
|
|
134
|
-
readonly bootstrapHash?: string;
|
|
135
|
-
readonly workDir?: string;
|
|
136
|
-
}): Promise<string | undefined> {
|
|
137
|
-
const entries = Object.entries(recipeIdentities).sort(([a], [b]) =>
|
|
138
|
-
a.localeCompare(b),
|
|
139
|
-
);
|
|
140
|
-
if (entries.length === 0 && bootstrapHash == null) {
|
|
141
|
-
return undefined;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
const encoder = new TextEncoder();
|
|
145
|
-
const chunks: Uint8Array[] = [];
|
|
146
|
-
const pushString = (value: string) => {
|
|
147
|
-
chunks.push(encoder.encode(value));
|
|
148
|
-
chunks.push(encoder.encode('\0'));
|
|
149
|
-
};
|
|
150
|
-
|
|
151
|
-
pushString(String(PREPARED_SANDBOX_IDENTITY_VERSION));
|
|
152
|
-
pushString(workDir ?? '');
|
|
153
|
-
pushString(bootstrapHash ?? '');
|
|
154
|
-
|
|
155
|
-
for (const [harnessId, identity] of entries) {
|
|
156
|
-
pushString(harnessId);
|
|
157
|
-
pushString(identity);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
|
|
161
|
-
const buffer = new Uint8Array(totalLength);
|
|
162
|
-
let offset = 0;
|
|
163
|
-
for (const chunk of chunks) {
|
|
164
|
-
buffer.set(chunk, offset);
|
|
165
|
-
offset += chunk.length;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
const digest = await crypto.subtle.digest('SHA-256', buffer);
|
|
169
|
-
const bytes = new Uint8Array(digest);
|
|
170
|
-
let hex = '';
|
|
171
|
-
for (let i = 0; i < 8; i++) {
|
|
172
|
-
hex += bytes[i].toString(16).padStart(2, '0');
|
|
173
|
-
}
|
|
174
|
-
return hex;
|
|
175
|
-
}
|
|
@@ -34,8 +34,8 @@ export function harnessStateDirectoryPath({
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
/**
|
|
37
|
-
* Network sandbox session returned by
|
|
38
|
-
* harness keeps this for the lifetime of a session. It is itself a
|
|
37
|
+
* Network sandbox session returned by sandbox adapter creation and resume
|
|
38
|
+
* functions. The harness keeps this for the lifetime of a session. It is itself a
|
|
39
39
|
* {@link SandboxSession} (file I/O, exec, spawn) and adds the infra surface on
|
|
40
40
|
* top: port resolution, lifecycle, and network-policy mutation.
|
|
41
41
|
*
|
|
@@ -46,22 +46,15 @@ export function harnessStateDirectoryPath({
|
|
|
46
46
|
*/
|
|
47
47
|
export interface HarnessV1NetworkSandboxSession extends SandboxSession {
|
|
48
48
|
/**
|
|
49
|
-
*
|
|
50
|
-
* harness session
|
|
51
|
-
* resume — the framework persists this on lifecycle state so a future
|
|
52
|
-
* process can call `HarnessV1SandboxProvider.resume?({ sessionId })` and
|
|
53
|
-
* reach the same resource. Providers populate it from their native
|
|
54
|
-
* identifier (Vercel: the sandbox name; just-bash: a UUID minted at
|
|
55
|
-
* create time).
|
|
49
|
+
* Identifier for the sandbox session. Persist this separately from the
|
|
50
|
+
* harness session ID and resume state when reattaching across processes.
|
|
56
51
|
*/
|
|
57
52
|
readonly id: string;
|
|
58
53
|
|
|
59
54
|
/**
|
|
60
55
|
* The sandbox's default working directory — the absolute path that
|
|
61
56
|
* `run`/`spawn` resolve relative commands against when no `workingDirectory`
|
|
62
|
-
* is given. Read from the live sandbox
|
|
63
|
-
* configurable at create time: Vercel defaults to `/vercel/sandbox`,
|
|
64
|
-
* just-bash to `/home/user`), never hardcoded.
|
|
57
|
+
* is given. Read from the live sandbox, never hardcoded.
|
|
65
58
|
*
|
|
66
59
|
* The framework composes each session's working directory underneath this
|
|
67
60
|
* path (`<defaultWorkingDirectory>/<harnessId>-<sessionId>`) so adapters do
|
|
@@ -105,7 +98,7 @@ export interface HarnessV1NetworkSandboxSession extends SandboxSession {
|
|
|
105
98
|
|
|
106
99
|
/**
|
|
107
100
|
* Update the sandbox's outbound network policy. Optional — implementations
|
|
108
|
-
* without a local enforcement primitive
|
|
101
|
+
* without a local enforcement primitive omit this. Callers
|
|
109
102
|
* use optional-call (`sandboxSession.setNetworkPolicy?.(policy)`); a
|
|
110
103
|
* missing implementation is a no-op.
|
|
111
104
|
*/
|
|
@@ -139,7 +132,7 @@ export interface HarnessV1NetworkSandboxSession extends SandboxSession {
|
|
|
139
132
|
/**
|
|
140
133
|
* Replace the set of ports exposed by the sandbox. Full-replacement
|
|
141
134
|
* semantics: ports omitted from the array are deregistered. Optional —
|
|
142
|
-
* implementations that cannot expose ports
|
|
135
|
+
* implementations that cannot expose ports omit this.
|
|
143
136
|
*/
|
|
144
137
|
readonly setPorts?: (
|
|
145
138
|
ports: ReadonlyArray<number>,
|
|
@@ -7,6 +7,7 @@ import type { HarnessV1NetworkSandboxSession } from './harness-v1-network-sandbo
|
|
|
7
7
|
* `LanguageModelV4` providers, no I/O performed at construction. The actual
|
|
8
8
|
* sandbox is created (or wrapped) when `HarnessAgent` calls `createSession()`.
|
|
9
9
|
*/
|
|
10
|
+
/** @deprecated Supply a sandbox session to `HarnessAgent.createSession()` instead. */
|
|
10
11
|
export interface HarnessV1SandboxProvider {
|
|
11
12
|
readonly specificationVersion: 'harness-sandbox-v1';
|
|
12
13
|
readonly providerId: string;
|
|
@@ -54,8 +55,8 @@ export interface HarnessV1SandboxProvider {
|
|
|
54
55
|
|
|
55
56
|
/**
|
|
56
57
|
* Reattach to an existing sandbox previously created with the same
|
|
57
|
-
* `sessionId`. Optional — providers that cannot rehydrate by id
|
|
58
|
-
*
|
|
58
|
+
* `sessionId`. Optional — providers that cannot rehydrate by id omit
|
|
59
|
+
* this; the harness throws
|
|
59
60
|
* `HarnessCapabilityUnsupportedError` when resume is attempted against
|
|
60
61
|
* them.
|
|
61
62
|
*
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { HarnessV1SandboxTemplate } from './harness-v1-sandbox-template';
|
|
2
|
+
|
|
3
|
+
export type HarnessV1SandboxSessionCreateOptions<
|
|
4
|
+
TProviderOptions extends object,
|
|
5
|
+
> = TProviderOptions & {
|
|
6
|
+
/**
|
|
7
|
+
* Manually assign the ID of a newly created sandbox session. Creation never
|
|
8
|
+
* looks up or resumes an existing session. An existing sandbox resource with
|
|
9
|
+
* the same ID is a creation conflict.
|
|
10
|
+
*/
|
|
11
|
+
readonly sandboxId?: string;
|
|
12
|
+
readonly template?: HarnessV1SandboxTemplate;
|
|
13
|
+
readonly abortSignal?: AbortSignal;
|
|
14
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type HarnessV1SandboxSessionResumeOptions<
|
|
2
|
+
TProviderOptions extends object,
|
|
3
|
+
> = TProviderOptions & {
|
|
4
|
+
/**
|
|
5
|
+
* ID of the existing sandbox session to reattach. Resume never creates a
|
|
6
|
+
* sandbox when the ID cannot be found.
|
|
7
|
+
*/
|
|
8
|
+
readonly sandboxId: string;
|
|
9
|
+
readonly abortSignal?: AbortSignal;
|
|
10
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Experimental_SandboxSession } from '@ai-sdk/provider-utils';
|
|
2
|
+
|
|
3
|
+
export type HarnessV1SandboxTemplate = {
|
|
4
|
+
readonly identity: string;
|
|
5
|
+
readonly prepare: (options: {
|
|
6
|
+
readonly session: Experimental_SandboxSession;
|
|
7
|
+
readonly abortSignal?: AbortSignal;
|
|
8
|
+
}) => Promise<void>;
|
|
9
|
+
};
|
package/src/v1/index.ts
CHANGED
|
@@ -48,6 +48,9 @@ export type {
|
|
|
48
48
|
HarnessV1ResponseFormat,
|
|
49
49
|
} from './harness-v1-response-format';
|
|
50
50
|
export type { HarnessV1SandboxProvider } from './harness-v1-sandbox-provider';
|
|
51
|
+
export type { HarnessV1SandboxTemplate } from './harness-v1-sandbox-template';
|
|
52
|
+
export type { HarnessV1SandboxSessionCreateOptions } from './harness-v1-sandbox-session-create-options';
|
|
53
|
+
export type { HarnessV1SandboxSessionResumeOptions } from './harness-v1-sandbox-session-resume-options';
|
|
51
54
|
export type {
|
|
52
55
|
HarnessV1ContinueTurnState,
|
|
53
56
|
HarnessV1LifecycleState,
|