@ai-sdk/harness 1.0.0-beta.24 → 1.0.0-beta.26
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 +12 -0
- package/README.md +27 -15
- package/dist/agent/index.d.ts +45 -12
- package/dist/agent/index.js +242 -29
- package/dist/agent/index.js.map +1 -1
- package/package.json +2 -2
- package/src/agent/harness-agent-settings.ts +47 -12
- package/src/agent/harness-agent.ts +70 -35
- package/src/agent/internal/bootstrap-recipe.ts +1 -1
- package/src/agent/internal/sandbox-bootstrap.ts +266 -0
- package/src/agent/prewarm.ts +29 -11
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @ai-sdk/harness
|
|
2
2
|
|
|
3
|
+
## 1.0.0-beta.26
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- a83a367: feat(harness): allow pre-snapshot `sandboxConfig.onBootstrap` callback in `HarnessAgent`
|
|
8
|
+
|
|
9
|
+
## 1.0.0-beta.25
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- ai@7.0.0-beta.186
|
|
14
|
+
|
|
3
15
|
## 1.0.0-beta.24
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -27,12 +27,25 @@ const agent = new HarnessAgent({
|
|
|
27
27
|
runtime: 'node24',
|
|
28
28
|
ports: [4000],
|
|
29
29
|
}),
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
30
|
+
sandboxConfig: {
|
|
31
|
+
bootstrapHash: 'ripgrep-v1',
|
|
32
|
+
onBootstrap: async ({ session, abortSignal }) => {
|
|
33
|
+
const result = await session.run({
|
|
34
|
+
command:
|
|
35
|
+
'command -v rg >/dev/null || (apt-get update && apt-get install -y ripgrep)',
|
|
36
|
+
abortSignal,
|
|
37
|
+
});
|
|
38
|
+
if (result.exitCode !== 0) {
|
|
39
|
+
throw new Error(`Failed to install ripgrep: ${result.stderr}`);
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
onSession: async ({ session, sessionWorkDir, abortSignal }) => {
|
|
43
|
+
await session.writeTextFile({
|
|
44
|
+
path: `${sessionWorkDir}/README.md`,
|
|
45
|
+
content: 'Workspace notes for this session.',
|
|
46
|
+
abortSignal,
|
|
47
|
+
});
|
|
48
|
+
},
|
|
36
49
|
},
|
|
37
50
|
tools: {
|
|
38
51
|
deploy: tool({
|
|
@@ -70,18 +83,17 @@ try {
|
|
|
70
83
|
}
|
|
71
84
|
```
|
|
72
85
|
|
|
73
|
-
|
|
86
|
+
Use `session.detach()` to park a bridge-backed session for later attach, `session.stop()` to save state and stop the sandbox, or `session.destroy()` to clean up without keeping resume state. Bridge-backed adapters (claude-code, codex) require a sandbox provider that exposes ports — `@ai-sdk/sandbox-vercel` is the supported choice today. `@ai-sdk/sandbox-just-bash` is suitable only for non-bridge flows.
|
|
87
|
+
|
|
88
|
+
`sandbox` is a required `HarnessV1SandboxProvider` — the agent calls `provider.createSession()` when a session starts. Use `sandboxConfig` for agent specific sandbox configuration that works independently from the sandbox provider that is used:
|
|
89
|
+
|
|
90
|
+
- Use `sandboxConfig.onSession` to prepare the acquired sandbox before the harness adapter starts. The hook runs for fresh and resumed sessions, so keep it idempotent.
|
|
91
|
+
- Use `sandboxConfig.onBootstrap` for expensive sandbox setup that should be baked into a reusable snapshot, such as installing tools or cloning a large repository. Provide `sandboxConfig.bootstrapHash` with it and change that value whenever the bootstrap output should invalidate the cached snapshot.
|
|
92
|
+
- Use `sandboxConfig.workDir` to set a stable working directory for the agent, relative to the sandbox's default working directory; otherwise regular sessions use the existing `<harnessId>-<sessionId>` directory. In that case, the `onBootstrap` callback receives the sandbox's default working directory.
|
|
74
93
|
|
|
75
94
|
### Available harnesses
|
|
76
95
|
|
|
77
|
-
|
|
78
|
-
- `@ai-sdk/harness-codex`
|
|
79
|
-
- `@ai-sdk/harness-opencode` (WIP)
|
|
80
|
-
- `@ai-sdk/harness-goose` (WIP)
|
|
81
|
-
- `@ai-sdk/harness-mastra` (WIP)
|
|
82
|
-
- `@ai-sdk/harness-deepagents` (WIP)
|
|
83
|
-
- `@ai-sdk/harness-openai-agents` (WIP)
|
|
84
|
-
- `@ai-sdk/harness-pi` (WIP)
|
|
96
|
+
See the [harness adapters documentation](https://ai-sdk.dev/v7/docs/ai-sdk-harnesses/harness-adapters).
|
|
85
97
|
|
|
86
98
|
## Implementing a harness
|
|
87
99
|
|
package/dist/agent/index.d.ts
CHANGED
|
@@ -1039,6 +1039,44 @@ type HarnessAgentSkill = HarnessV1Skill;
|
|
|
1039
1039
|
type HarnessAgentPermissionMode = HarnessV1PermissionMode;
|
|
1040
1040
|
|
|
1041
1041
|
type HarnessAgentToolApprovalConfiguration = Readonly<Record<string, ToolApprovalStatus>>;
|
|
1042
|
+
type HarnessAgentSandboxConfig = {
|
|
1043
|
+
/**
|
|
1044
|
+
* Optional fixed working directory for all sessions, relative to the
|
|
1045
|
+
* sandbox's default working directory. When omitted, sessions keep the
|
|
1046
|
+
* existing `<harnessId>-<sessionId>` work directory.
|
|
1047
|
+
*/
|
|
1048
|
+
readonly workDir?: string;
|
|
1049
|
+
/**
|
|
1050
|
+
* Caller-controlled identity for `onBootstrap`. Change this whenever the
|
|
1051
|
+
* bootstrap side effects should invalidate the reusable sandbox snapshot.
|
|
1052
|
+
*/
|
|
1053
|
+
readonly bootstrapHash?: string;
|
|
1054
|
+
/**
|
|
1055
|
+
* Called during sandbox template creation after the harness adapter's own
|
|
1056
|
+
* bootstrap has run and before snapshot-capable providers publish a snapshot.
|
|
1057
|
+
*
|
|
1058
|
+
* `bootstrapHash` must be provided with this callback.
|
|
1059
|
+
*/
|
|
1060
|
+
readonly onBootstrap?: (opts: {
|
|
1061
|
+
readonly session: Experimental_SandboxSession;
|
|
1062
|
+
readonly workDir: string;
|
|
1063
|
+
readonly abortSignal?: AbortSignal;
|
|
1064
|
+
}) => Promise<void>;
|
|
1065
|
+
/**
|
|
1066
|
+
* Called after each sandbox session is acquired and the session work
|
|
1067
|
+
* directory exists, before the harness adapter starts. Runs for fresh and
|
|
1068
|
+
* resumed sessions.
|
|
1069
|
+
*
|
|
1070
|
+
* Use this to write per-session config, install lightweight tools, activate
|
|
1071
|
+
* licenses, or prepare files in `sessionWorkDir`. Keep it idempotent if the
|
|
1072
|
+
* agent may resume sessions.
|
|
1073
|
+
*/
|
|
1074
|
+
readonly onSession?: (opts: {
|
|
1075
|
+
readonly session: Experimental_SandboxSession;
|
|
1076
|
+
readonly sessionWorkDir: string;
|
|
1077
|
+
readonly abortSignal?: AbortSignal;
|
|
1078
|
+
}) => Promise<void>;
|
|
1079
|
+
};
|
|
1042
1080
|
/**
|
|
1043
1081
|
* Construction-time settings for a `HarnessAgent`.
|
|
1044
1082
|
*
|
|
@@ -1102,19 +1140,11 @@ type HarnessAgentSettings<THarness extends HarnessAgentAdapter<any> = HarnessAge
|
|
|
1102
1140
|
*/
|
|
1103
1141
|
readonly sandbox: HarnessV1SandboxProvider;
|
|
1104
1142
|
/**
|
|
1105
|
-
*
|
|
1106
|
-
* directory exists, before the harness adapter starts. Runs for fresh and
|
|
1107
|
-
* resumed sessions.
|
|
1108
|
-
*
|
|
1109
|
-
* Use this to write per-session config, install lightweight tools, activate
|
|
1110
|
-
* licenses, or prepare files in `sessionWorkDir`. Keep it idempotent if the
|
|
1111
|
-
* agent may resume sessions.
|
|
1143
|
+
* Sandbox working-directory and lifecycle hook configuration.
|
|
1112
1144
|
*/
|
|
1113
|
-
readonly
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
readonly abortSignal?: AbortSignal;
|
|
1117
|
-
}) => Promise<void>;
|
|
1145
|
+
readonly sandboxConfig?: HarnessAgentSandboxConfig;
|
|
1146
|
+
/** @deprecated Use `sandboxConfig.onSession` instead. */
|
|
1147
|
+
readonly onSandboxSession?: HarnessAgentSandboxConfig['onSession'];
|
|
1118
1148
|
/**
|
|
1119
1149
|
* Telemetry configuration. The harness drives AI SDK's pluggable
|
|
1120
1150
|
* `Telemetry` integration contract from the turn lifecycle, so a harness turn
|
|
@@ -1356,6 +1386,7 @@ declare class HarnessAgent<THarness extends HarnessAgentAdapter<any> = HarnessAg
|
|
|
1356
1386
|
*/
|
|
1357
1387
|
readonly tools: HarnessAllTools<THarness, TUserTools>;
|
|
1358
1388
|
private readonly settings;
|
|
1389
|
+
private readonly sandboxConfig;
|
|
1359
1390
|
private readonly userTools;
|
|
1360
1391
|
private readonly permissionMode;
|
|
1361
1392
|
constructor(settings: HarnessAgentSettings<THarness, TUserTools>);
|
|
@@ -1423,6 +1454,7 @@ declare class HarnessAgent<THarness extends HarnessAgentAdapter<any> = HarnessAg
|
|
|
1423
1454
|
private _toGenerateResult;
|
|
1424
1455
|
}
|
|
1425
1456
|
|
|
1457
|
+
type SandboxBootstrapSettings = Omit<HarnessAgentSandboxConfig, 'onSession'>;
|
|
1426
1458
|
/**
|
|
1427
1459
|
* Pre-build a harness's sandbox template without running an agent. Idempotent:
|
|
1428
1460
|
* if the template already exists (snapshot present, or marker on a non-snapshot
|
|
@@ -1440,6 +1472,7 @@ declare class HarnessAgent<THarness extends HarnessAgentAdapter<any> = HarnessAg
|
|
|
1440
1472
|
declare function prewarmHarness(options: {
|
|
1441
1473
|
readonly harness: HarnessAgentAdapter;
|
|
1442
1474
|
readonly sandboxProvider: HarnessV1SandboxProvider;
|
|
1475
|
+
readonly sandboxConfig?: SandboxBootstrapSettings;
|
|
1443
1476
|
readonly abortSignal?: AbortSignal;
|
|
1444
1477
|
}): Promise<void>;
|
|
1445
1478
|
|
package/dist/agent/index.js
CHANGED
|
@@ -2132,7 +2132,7 @@ function collectHarnessAgentToolApprovalContinuations(input) {
|
|
|
2132
2132
|
|
|
2133
2133
|
// src/agent/internal/bootstrap-recipe.ts
|
|
2134
2134
|
var BOOTSTRAP_SCHEMA_VERSION = 1;
|
|
2135
|
-
async function
|
|
2135
|
+
async function hashHarnessBootstrap(recipe) {
|
|
2136
2136
|
const encoder = new TextEncoder();
|
|
2137
2137
|
const chunks = [];
|
|
2138
2138
|
const pushString = (value) => {
|
|
@@ -2204,6 +2204,188 @@ ${result.stderr || result.stdout}`
|
|
|
2204
2204
|
});
|
|
2205
2205
|
}
|
|
2206
2206
|
|
|
2207
|
+
// src/agent/internal/sandbox-bootstrap.ts
|
|
2208
|
+
import { posix } from "path";
|
|
2209
|
+
var SANDBOX_BOOTSTRAP_IDENTITY_VERSION = 1;
|
|
2210
|
+
function validateSandboxBootstrapSettings(settings) {
|
|
2211
|
+
if (settings.onBootstrap == null !== (settings.bootstrapHash == null)) {
|
|
2212
|
+
throw new Error(
|
|
2213
|
+
"HarnessAgent: `sandboxConfig.onBootstrap` and `sandboxConfig.bootstrapHash` must be provided together."
|
|
2214
|
+
);
|
|
2215
|
+
}
|
|
2216
|
+
if (settings.workDir != null) {
|
|
2217
|
+
normalizeSandboxWorkDir(settings.workDir);
|
|
2218
|
+
}
|
|
2219
|
+
}
|
|
2220
|
+
function normalizeSandboxWorkDir(workDir) {
|
|
2221
|
+
if (workDir.length === 0) {
|
|
2222
|
+
throw new Error("HarnessAgent: `sandboxConfig.workDir` must not be empty.");
|
|
2223
|
+
}
|
|
2224
|
+
if (workDir.includes("\0")) {
|
|
2225
|
+
throw new Error(
|
|
2226
|
+
"HarnessAgent: `sandboxConfig.workDir` must not contain NUL."
|
|
2227
|
+
);
|
|
2228
|
+
}
|
|
2229
|
+
if (workDir.includes("\\")) {
|
|
2230
|
+
throw new Error(
|
|
2231
|
+
"HarnessAgent: `sandboxConfig.workDir` must use POSIX path separators."
|
|
2232
|
+
);
|
|
2233
|
+
}
|
|
2234
|
+
if (posix.isAbsolute(workDir)) {
|
|
2235
|
+
throw new Error("HarnessAgent: `sandboxConfig.workDir` must be relative.");
|
|
2236
|
+
}
|
|
2237
|
+
const normalized = posix.normalize(workDir);
|
|
2238
|
+
if (normalized === "." || normalized === ".." || normalized.startsWith("../")) {
|
|
2239
|
+
throw new Error(
|
|
2240
|
+
"HarnessAgent: `sandboxConfig.workDir` must stay inside the sandbox default working directory."
|
|
2241
|
+
);
|
|
2242
|
+
}
|
|
2243
|
+
return normalized;
|
|
2244
|
+
}
|
|
2245
|
+
function resolveSessionWorkDir({
|
|
2246
|
+
defaultWorkingDirectory,
|
|
2247
|
+
harnessId,
|
|
2248
|
+
sessionId,
|
|
2249
|
+
workDir
|
|
2250
|
+
}) {
|
|
2251
|
+
return joinSandboxPath({
|
|
2252
|
+
base: defaultWorkingDirectory,
|
|
2253
|
+
path: workDir != null ? workDir : `${harnessId}-${sessionId}`
|
|
2254
|
+
});
|
|
2255
|
+
}
|
|
2256
|
+
async function createSandboxBootstrapPlan({
|
|
2257
|
+
recipe,
|
|
2258
|
+
settings
|
|
2259
|
+
}) {
|
|
2260
|
+
const workDir = settings.workDir == null ? void 0 : normalizeSandboxWorkDir(settings.workDir);
|
|
2261
|
+
const recipeIdentity = recipe == null ? void 0 : await hashHarnessBootstrap(recipe);
|
|
2262
|
+
const hasCallerBootstrap = settings.onBootstrap != null;
|
|
2263
|
+
const needsCombinedIdentity = hasCallerBootstrap || recipeIdentity != null && workDir != null;
|
|
2264
|
+
const identity = needsCombinedIdentity && (recipeIdentity != null || hasCallerBootstrap) ? await hashSandboxBootstrapIdentity({
|
|
2265
|
+
recipeIdentity,
|
|
2266
|
+
bootstrapHash: settings.bootstrapHash,
|
|
2267
|
+
workDir
|
|
2268
|
+
}) : recipeIdentity;
|
|
2269
|
+
return {
|
|
2270
|
+
...recipe != null ? { recipe } : {},
|
|
2271
|
+
...recipeIdentity != null ? { recipeIdentity } : {},
|
|
2272
|
+
...identity != null ? { identity } : {},
|
|
2273
|
+
...workDir != null ? { workDir } : {},
|
|
2274
|
+
...recipe != null || settings.onBootstrap != null ? {
|
|
2275
|
+
onFirstCreate: (session, opts) => runSandboxBootstrap({
|
|
2276
|
+
session,
|
|
2277
|
+
recipe,
|
|
2278
|
+
recipeIdentity,
|
|
2279
|
+
workDir,
|
|
2280
|
+
onBootstrap: settings.onBootstrap,
|
|
2281
|
+
abortSignal: opts.abortSignal
|
|
2282
|
+
})
|
|
2283
|
+
} : {}
|
|
2284
|
+
};
|
|
2285
|
+
}
|
|
2286
|
+
async function runSandboxBootstrap({
|
|
2287
|
+
session,
|
|
2288
|
+
recipe,
|
|
2289
|
+
recipeIdentity,
|
|
2290
|
+
workDir,
|
|
2291
|
+
onBootstrap,
|
|
2292
|
+
abortSignal
|
|
2293
|
+
}) {
|
|
2294
|
+
if (recipe != null && recipeIdentity != null) {
|
|
2295
|
+
await applyBootstrapRecipe(session, recipe, recipeIdentity, {
|
|
2296
|
+
abortSignal
|
|
2297
|
+
});
|
|
2298
|
+
}
|
|
2299
|
+
if (onBootstrap == null) return;
|
|
2300
|
+
const defaultWorkingDirectory = await resolveDefaultWorkingDirectory({
|
|
2301
|
+
session,
|
|
2302
|
+
abortSignal
|
|
2303
|
+
});
|
|
2304
|
+
const bootstrapWorkDir = workDir == null ? defaultWorkingDirectory : joinSandboxPath({
|
|
2305
|
+
base: defaultWorkingDirectory,
|
|
2306
|
+
path: workDir
|
|
2307
|
+
});
|
|
2308
|
+
await ensureSandboxDirectory({
|
|
2309
|
+
session,
|
|
2310
|
+
workDir: bootstrapWorkDir,
|
|
2311
|
+
abortSignal
|
|
2312
|
+
});
|
|
2313
|
+
await onBootstrap({ session, workDir: bootstrapWorkDir, abortSignal });
|
|
2314
|
+
}
|
|
2315
|
+
async function resolveDefaultWorkingDirectory({
|
|
2316
|
+
session,
|
|
2317
|
+
abortSignal
|
|
2318
|
+
}) {
|
|
2319
|
+
const result = await session.run({
|
|
2320
|
+
command: "pwd",
|
|
2321
|
+
abortSignal
|
|
2322
|
+
});
|
|
2323
|
+
if (result.exitCode !== 0) {
|
|
2324
|
+
throw new Error(
|
|
2325
|
+
`Failed to resolve sandbox default working directory (exit ${result.exitCode}): ${result.stderr || result.stdout}`
|
|
2326
|
+
);
|
|
2327
|
+
}
|
|
2328
|
+
const cwd = result.stdout.trim();
|
|
2329
|
+
if (!posix.isAbsolute(cwd)) {
|
|
2330
|
+
throw new Error(
|
|
2331
|
+
`Failed to resolve sandbox default working directory: expected an absolute path, got ${JSON.stringify(cwd)}.`
|
|
2332
|
+
);
|
|
2333
|
+
}
|
|
2334
|
+
return cwd === "/" ? cwd : cwd.replace(/\/+$/, "");
|
|
2335
|
+
}
|
|
2336
|
+
async function ensureSandboxDirectory({
|
|
2337
|
+
session,
|
|
2338
|
+
workDir,
|
|
2339
|
+
abortSignal
|
|
2340
|
+
}) {
|
|
2341
|
+
const result = await session.run({
|
|
2342
|
+
command: 'mkdir -p "$WORK_DIR"',
|
|
2343
|
+
env: { WORK_DIR: workDir },
|
|
2344
|
+
abortSignal
|
|
2345
|
+
});
|
|
2346
|
+
if (result.exitCode !== 0) {
|
|
2347
|
+
throw new Error(
|
|
2348
|
+
`Failed to create sandbox work directory ${workDir} (exit ${result.exitCode}): ${result.stderr || result.stdout}`
|
|
2349
|
+
);
|
|
2350
|
+
}
|
|
2351
|
+
}
|
|
2352
|
+
async function hashSandboxBootstrapIdentity({
|
|
2353
|
+
recipeIdentity,
|
|
2354
|
+
bootstrapHash,
|
|
2355
|
+
workDir
|
|
2356
|
+
}) {
|
|
2357
|
+
const encoder = new TextEncoder();
|
|
2358
|
+
const chunks = [];
|
|
2359
|
+
const pushString = (value) => {
|
|
2360
|
+
chunks.push(encoder.encode(value));
|
|
2361
|
+
chunks.push(encoder.encode("\0"));
|
|
2362
|
+
};
|
|
2363
|
+
pushString(String(SANDBOX_BOOTSTRAP_IDENTITY_VERSION));
|
|
2364
|
+
pushString(recipeIdentity != null ? recipeIdentity : "");
|
|
2365
|
+
pushString(bootstrapHash != null ? bootstrapHash : "");
|
|
2366
|
+
pushString(workDir != null ? workDir : "");
|
|
2367
|
+
const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
|
|
2368
|
+
const buffer = new Uint8Array(totalLength);
|
|
2369
|
+
let offset = 0;
|
|
2370
|
+
for (const chunk of chunks) {
|
|
2371
|
+
buffer.set(chunk, offset);
|
|
2372
|
+
offset += chunk.length;
|
|
2373
|
+
}
|
|
2374
|
+
const digest = await crypto.subtle.digest("SHA-256", buffer);
|
|
2375
|
+
const bytes = new Uint8Array(digest);
|
|
2376
|
+
let hex = "";
|
|
2377
|
+
for (let i = 0; i < 8; i++) {
|
|
2378
|
+
hex += bytes[i].toString(16).padStart(2, "0");
|
|
2379
|
+
}
|
|
2380
|
+
return hex;
|
|
2381
|
+
}
|
|
2382
|
+
function joinSandboxPath({
|
|
2383
|
+
base,
|
|
2384
|
+
path
|
|
2385
|
+
}) {
|
|
2386
|
+
return posix.join(base, path);
|
|
2387
|
+
}
|
|
2388
|
+
|
|
2207
2389
|
// src/agent/internal/resolve-observability.ts
|
|
2208
2390
|
import { asArray } from "@ai-sdk/provider-utils";
|
|
2209
2391
|
var ENV_TRUTHY = /* @__PURE__ */ new Set(["1", "true", "yes", "on"]);
|
|
@@ -2281,7 +2463,10 @@ var HarnessAgent = class {
|
|
|
2281
2463
|
constructor(settings) {
|
|
2282
2464
|
this.version = "agent-v1";
|
|
2283
2465
|
var _a3;
|
|
2466
|
+
const sandboxConfig = resolveSandboxConfig(settings);
|
|
2467
|
+
validateSandboxBootstrapSettings(sandboxConfig);
|
|
2284
2468
|
this.settings = settings;
|
|
2469
|
+
this.sandboxConfig = sandboxConfig;
|
|
2285
2470
|
this.id = settings.id;
|
|
2286
2471
|
this.userTools = (_a3 = settings.tools) != null ? _a3 : {};
|
|
2287
2472
|
this.permissionMode = resolvePermissionMode({
|
|
@@ -2343,17 +2528,18 @@ var HarnessAgent = class {
|
|
|
2343
2528
|
const effectiveContinueFrom = validatedContinueFrom != null ? validatedContinueFrom : validatedResumeFrom == null ? void 0 : validatedResumeFrom.continueFrom;
|
|
2344
2529
|
const isResumedSession = validatedResumeFrom != null || effectiveContinueFrom != null;
|
|
2345
2530
|
let recipe;
|
|
2346
|
-
let identity;
|
|
2347
2531
|
if (harness.getBootstrap != null) {
|
|
2348
2532
|
recipe = await harness.getBootstrap({ abortSignal });
|
|
2349
|
-
identity = await hashBootstrap(recipe);
|
|
2350
2533
|
}
|
|
2534
|
+
const sandboxBootstrapPlan = await createSandboxBootstrapPlan({
|
|
2535
|
+
recipe,
|
|
2536
|
+
settings: this.sandboxConfig
|
|
2537
|
+
});
|
|
2351
2538
|
const acquiredSandboxSession = await this._acquireSandbox({
|
|
2352
2539
|
sandboxProvider,
|
|
2353
2540
|
sessionId,
|
|
2354
2541
|
isResume: isResumedSession,
|
|
2355
|
-
|
|
2356
|
-
identity,
|
|
2542
|
+
bootstrapPlan: sandboxBootstrapPlan,
|
|
2357
2543
|
abortSignal
|
|
2358
2544
|
});
|
|
2359
2545
|
const leased = applyPortLease({
|
|
@@ -2363,22 +2549,28 @@ var HarnessAgent = class {
|
|
|
2363
2549
|
});
|
|
2364
2550
|
const sandboxSession = leased.sandboxSession;
|
|
2365
2551
|
const leasedBridgePort = leased.port;
|
|
2366
|
-
const sessionWorkDir =
|
|
2552
|
+
const sessionWorkDir = resolveSessionWorkDir({
|
|
2553
|
+
defaultWorkingDirectory: sandboxSession.defaultWorkingDirectory,
|
|
2554
|
+
harnessId: harness.harnessId,
|
|
2555
|
+
sessionId,
|
|
2556
|
+
workDir: sandboxBootstrapPlan.workDir
|
|
2557
|
+
});
|
|
2367
2558
|
try {
|
|
2368
|
-
if (!isResumedSession && recipe != null &&
|
|
2559
|
+
if (!isResumedSession && sandboxBootstrapPlan.recipe != null && sandboxBootstrapPlan.recipeIdentity != null) {
|
|
2369
2560
|
await applyBootstrapRecipe(
|
|
2370
2561
|
sandboxSession.restricted(),
|
|
2371
|
-
recipe,
|
|
2372
|
-
|
|
2562
|
+
sandboxBootstrapPlan.recipe,
|
|
2563
|
+
sandboxBootstrapPlan.recipeIdentity,
|
|
2373
2564
|
{ abortSignal }
|
|
2374
2565
|
);
|
|
2375
2566
|
}
|
|
2376
|
-
await
|
|
2377
|
-
|
|
2567
|
+
await ensureSandboxDirectory({
|
|
2568
|
+
session: sandboxSession,
|
|
2569
|
+
workDir: sessionWorkDir,
|
|
2378
2570
|
abortSignal
|
|
2379
2571
|
});
|
|
2380
|
-
if (this.
|
|
2381
|
-
await this.
|
|
2572
|
+
if (this.sandboxConfig.onSession != null) {
|
|
2573
|
+
await this.sandboxConfig.onSession({
|
|
2382
2574
|
session: sandboxSession.restricted(),
|
|
2383
2575
|
sessionWorkDir,
|
|
2384
2576
|
abortSignal
|
|
@@ -2535,13 +2727,8 @@ var HarnessAgent = class {
|
|
|
2535
2727
|
return sandboxProvider.createSession({
|
|
2536
2728
|
sessionId: input.sessionId,
|
|
2537
2729
|
abortSignal: input.abortSignal,
|
|
2538
|
-
identity: input.identity,
|
|
2539
|
-
onFirstCreate: input.
|
|
2540
|
-
session,
|
|
2541
|
-
input.recipe,
|
|
2542
|
-
input.identity,
|
|
2543
|
-
opts
|
|
2544
|
-
) : void 0
|
|
2730
|
+
identity: input.bootstrapPlan.identity,
|
|
2731
|
+
onFirstCreate: input.bootstrapPlan.onFirstCreate
|
|
2545
2732
|
});
|
|
2546
2733
|
}
|
|
2547
2734
|
/*
|
|
@@ -2682,6 +2869,18 @@ var HarnessGenerateTextResult = class {
|
|
|
2682
2869
|
return this.finalStep.providerMetadata;
|
|
2683
2870
|
}
|
|
2684
2871
|
};
|
|
2872
|
+
function resolveSandboxConfig(settings) {
|
|
2873
|
+
var _a3;
|
|
2874
|
+
if (settings.onSandboxSession != null) {
|
|
2875
|
+
console.warn(
|
|
2876
|
+
"HarnessAgent: `onSandboxSession` is deprecated. Use `sandboxConfig.onSession` instead."
|
|
2877
|
+
);
|
|
2878
|
+
}
|
|
2879
|
+
return {
|
|
2880
|
+
...settings.sandboxConfig,
|
|
2881
|
+
...((_a3 = settings.sandboxConfig) == null ? void 0 : _a3.onSession) == null && settings.onSandboxSession != null ? { onSession: settings.onSandboxSession } : {}
|
|
2882
|
+
};
|
|
2883
|
+
}
|
|
2685
2884
|
function applyPortLease(input) {
|
|
2686
2885
|
const pool = input.provider.bridgePorts;
|
|
2687
2886
|
if (pool == null || pool.length === 0) {
|
|
@@ -2718,21 +2917,35 @@ async function cleanupAfterStartFailure(input) {
|
|
|
2718
2917
|
|
|
2719
2918
|
// src/agent/prewarm.ts
|
|
2720
2919
|
async function prewarmHarness(options) {
|
|
2721
|
-
var _a3, _b3;
|
|
2722
|
-
const
|
|
2920
|
+
var _a3, _b3, _c;
|
|
2921
|
+
const sandboxConfig = (_a3 = options.sandboxConfig) != null ? _a3 : {};
|
|
2922
|
+
validateSandboxBootstrapSettings(sandboxConfig);
|
|
2923
|
+
const recipe = await ((_c = (_b3 = options.harness).getBootstrap) == null ? void 0 : _c.call(_b3, {
|
|
2723
2924
|
abortSignal: options.abortSignal
|
|
2724
2925
|
}));
|
|
2725
|
-
|
|
2726
|
-
|
|
2926
|
+
const bootstrapPlan = await createSandboxBootstrapPlan({
|
|
2927
|
+
recipe,
|
|
2928
|
+
settings: sandboxConfig
|
|
2929
|
+
});
|
|
2930
|
+
if (bootstrapPlan.identity == null || bootstrapPlan.onFirstCreate == null) {
|
|
2931
|
+
return;
|
|
2932
|
+
}
|
|
2727
2933
|
const sandboxSession = await options.sandboxProvider.createSession({
|
|
2728
2934
|
abortSignal: options.abortSignal,
|
|
2729
|
-
identity,
|
|
2730
|
-
onFirstCreate:
|
|
2935
|
+
identity: bootstrapPlan.identity,
|
|
2936
|
+
onFirstCreate: bootstrapPlan.onFirstCreate
|
|
2731
2937
|
});
|
|
2732
2938
|
try {
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2939
|
+
if (bootstrapPlan.recipe != null && bootstrapPlan.recipeIdentity != null) {
|
|
2940
|
+
await applyBootstrapRecipe(
|
|
2941
|
+
sandboxSession.restricted(),
|
|
2942
|
+
bootstrapPlan.recipe,
|
|
2943
|
+
bootstrapPlan.recipeIdentity,
|
|
2944
|
+
{
|
|
2945
|
+
abortSignal: options.abortSignal
|
|
2946
|
+
}
|
|
2947
|
+
);
|
|
2948
|
+
}
|
|
2736
2949
|
} finally {
|
|
2737
2950
|
await Promise.resolve(sandboxSession.stop()).catch(() => {
|
|
2738
2951
|
});
|