@ai-sdk/harness 1.0.0-canary.9 → 1.0.0

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 CHANGED
@@ -1,5 +1,149 @@
1
1
  # @ai-sdk/harness
2
2
 
3
+ ## 1.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - 9d6dbe0: feat(harness): add sandbox specific expansion for harness abstraction, add `sandbox-just-bash` and `sandbox-vercel`
8
+
9
+ ### Patch Changes
10
+
11
+ - e5d4a24: chore(harness): update ws package
12
+ - aae0138: fix(harness): make listening for sandbox bridge readiness compatible with Bun
13
+ - be83911: fix(harness): reject bridge startup when the WebSocket port cannot be bound
14
+ - 3d87086: fix(harness): guard against invalid resuming a session vs continuing a turn
15
+ - d77bed4: chore(harness): separate harness spec types (v1) from consumer-facing types
16
+ - 21d3d60: feat(harness): implement harness specification
17
+ - 3d9a50c: feat(harness): implement harness adapters for Claude Code, Codex, Pi
18
+ - 57e0a59: fix(harness): ensure finish chunk's total usage is actually coming from total usage
19
+ - 6c7a3e5: Start the `1.0.0` canary release line for the experimental harness and sandbox packages. They were unintentionally published as `0.0.0-canary.*` because they were scaffolded with a `0.0.0-canary.0` premajor version, which semver could not advance past on a major bump.
20
+ - 1ea15a3: fix(harness): fix various bugs with harness skills not being correctly processed by the harness adapters
21
+ - a83a367: feat(harness): allow pre-snapshot `sandboxConfig.onBootstrap` callback in `HarnessAgent`
22
+ - b8396f0: trigger initial beta release
23
+ - 534dac6: fix(harness): fix incomplete OIDC token support for AI Gateway auth in harness adapters
24
+
25
+ ## 1.0.0-beta.27
26
+
27
+ ### Patch Changes
28
+
29
+ - ai@7.0.0-beta.187
30
+
31
+ ## 1.0.0-beta.26
32
+
33
+ ### Patch Changes
34
+
35
+ - a83a367: feat(harness): allow pre-snapshot `sandboxConfig.onBootstrap` callback in `HarnessAgent`
36
+
37
+ ## 1.0.0-beta.25
38
+
39
+ ### Patch Changes
40
+
41
+ - ai@7.0.0-beta.186
42
+
43
+ ## 1.0.0-beta.24
44
+
45
+ ### Patch Changes
46
+
47
+ - Updated dependencies [75763b0]
48
+ - ai@7.0.0-beta.185
49
+
50
+ ## 1.0.0-beta.23
51
+
52
+ ### Patch Changes
53
+
54
+ - 57e0a59: fix(harness): ensure finish chunk's total usage is actually coming from total usage
55
+
56
+ ## 1.0.0-beta.22
57
+
58
+ ### Patch Changes
59
+
60
+ - Updated dependencies [0416e3e]
61
+ - @ai-sdk/provider@4.0.0-beta.20
62
+ - ai@7.0.0-beta.184
63
+ - @ai-sdk/provider-utils@5.0.0-beta.50
64
+
65
+ ## 1.0.0-beta.21
66
+
67
+ ### Patch Changes
68
+
69
+ - ai@7.0.0-beta.183
70
+
71
+ ## 1.0.0-beta.20
72
+
73
+ ### Patch Changes
74
+
75
+ - e5d4a24: chore(harness): update ws package
76
+ - Updated dependencies [cc6ab90]
77
+ - ai@7.0.0-beta.182
78
+
79
+ ## 1.0.0-beta.19
80
+
81
+ ### Patch Changes
82
+
83
+ - Updated dependencies [6a2caf9]
84
+ - ai@7.0.0-beta.181
85
+
86
+ ## 1.0.0-beta.18
87
+
88
+ ### Patch Changes
89
+
90
+ - Updated dependencies [81a284b]
91
+ - ai@7.0.0-beta.180
92
+
93
+ ## 1.0.0-beta.17
94
+
95
+ ### Patch Changes
96
+
97
+ - 534dac6: fix(harness): fix incomplete OIDC token support for AI Gateway auth in harness adapters
98
+
99
+ ## 1.0.0-beta.16
100
+
101
+ ### Patch Changes
102
+
103
+ - ai@7.0.0-beta.179
104
+
105
+ ## 1.0.0-beta.15
106
+
107
+ ### Patch Changes
108
+
109
+ - Updated dependencies [b097c52]
110
+ - ai@7.0.0-beta.178
111
+
112
+ ## 1.0.0-beta.14
113
+
114
+ ### Patch Changes
115
+
116
+ - b8396f0: trigger initial beta release
117
+ - Updated dependencies [b8396f0]
118
+ - @ai-sdk/provider-utils@5.0.0-beta.49
119
+ - @ai-sdk/provider@4.0.0-beta.19
120
+ - ai@7.0.0-beta.177
121
+
122
+ ## 1.0.0-canary.13
123
+
124
+ ### Patch Changes
125
+
126
+ - ai@7.0.0-canary.176
127
+
128
+ ## 1.0.0-canary.12
129
+
130
+ ### Patch Changes
131
+
132
+ - Updated dependencies [6ec57f5]
133
+ - ai@7.0.0-canary.175
134
+
135
+ ## 1.0.0-canary.11
136
+
137
+ ### Patch Changes
138
+
139
+ - be83911: fix(harness): reject bridge startup when the WebSocket port cannot be bound
140
+
141
+ ## 1.0.0-canary.10
142
+
143
+ ### Patch Changes
144
+
145
+ - ai@7.0.0-canary.174
146
+
3
147
  ## 1.0.0-canary.9
4
148
 
5
149
  ### 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
- onSandboxSession: async ({ session, sessionWorkDir, abortSignal }) => {
31
- await session.writeTextFile({
32
- path: `${sessionWorkDir}/README.md`,
33
- content: 'Workspace notes for this session.',
34
- abortSignal,
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
- `sandbox` is a required `HarnessV1SandboxProvider` — the agent calls `provider.createSession()` when a session starts. Use `onSandboxSession` to prepare the acquired sandbox before the harness adapter starts. The hook runs for fresh and resumed sessions, so keep it idempotent. 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 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.
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
- - `@ai-sdk/harness-claude-code`
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
 
@@ -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
- * Called after each sandbox session is acquired and the session work
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 onSandboxSession?: (opts: {
1114
- readonly session: Experimental_SandboxSession;
1115
- readonly sessionWorkDir: string;
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
 
@@ -317,9 +317,15 @@ var HarnessStreamTextResult = class {
317
317
  /**
318
318
  * Resolve every delayed promise and close `fullStream`. Idempotent.
319
319
  */
320
- async finish() {
320
+ async finish(input) {
321
321
  if (this.settled) return;
322
322
  this.settled = true;
323
+ if (input != null) {
324
+ this.finalFinishReason = input.finishReason.unified;
325
+ this.finalRawFinishReason = input.finishReason.raw;
326
+ this.finalProviderMetadata = input.providerMetadata;
327
+ this.accumulatedUsage = asLanguageModelUsage(input.totalUsage);
328
+ }
323
329
  if (this.currentStepContent.length > 0) {
324
330
  const trailingStep = new DefaultStepResult({
325
331
  callId: generateId(),
@@ -1224,6 +1230,7 @@ function runPrompt(input) {
1224
1230
  ])
1225
1231
  );
1226
1232
  const settledApprovalToolCallIds = /* @__PURE__ */ new Set();
1233
+ let finalFinish;
1227
1234
  let stepText = "";
1228
1235
  let stepReasoning = "";
1229
1236
  let stepToolCalls = [];
@@ -1485,6 +1492,7 @@ function runPrompt(input) {
1485
1492
  });
1486
1493
  }
1487
1494
  if (value.type === "finish") {
1495
+ finalFinish = value;
1488
1496
  telemetry.end({
1489
1497
  finishReason: value.finishReason,
1490
1498
  usage: value.totalUsage
@@ -1606,7 +1614,13 @@ function runPrompt(input) {
1606
1614
  }
1607
1615
  }
1608
1616
  (_g = input.onTurnFinished) == null ? void 0 : _g.call(input);
1609
- await result.finish();
1617
+ await result.finish(
1618
+ finalFinish ? {
1619
+ finishReason: finalFinish.finishReason,
1620
+ totalUsage: finalFinish.totalUsage,
1621
+ providerMetadata: finalFinish.harnessMetadata
1622
+ } : void 0
1623
+ );
1610
1624
  } catch (err) {
1611
1625
  telemetry.error(err);
1612
1626
  result.fail(err);
@@ -2118,7 +2132,7 @@ function collectHarnessAgentToolApprovalContinuations(input) {
2118
2132
 
2119
2133
  // src/agent/internal/bootstrap-recipe.ts
2120
2134
  var BOOTSTRAP_SCHEMA_VERSION = 1;
2121
- async function hashBootstrap(recipe) {
2135
+ async function hashHarnessBootstrap(recipe) {
2122
2136
  const encoder = new TextEncoder();
2123
2137
  const chunks = [];
2124
2138
  const pushString = (value) => {
@@ -2190,6 +2204,188 @@ ${result.stderr || result.stdout}`
2190
2204
  });
2191
2205
  }
2192
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
+
2193
2389
  // src/agent/internal/resolve-observability.ts
2194
2390
  import { asArray } from "@ai-sdk/provider-utils";
2195
2391
  var ENV_TRUTHY = /* @__PURE__ */ new Set(["1", "true", "yes", "on"]);
@@ -2267,7 +2463,10 @@ var HarnessAgent = class {
2267
2463
  constructor(settings) {
2268
2464
  this.version = "agent-v1";
2269
2465
  var _a3;
2466
+ const sandboxConfig = resolveSandboxConfig(settings);
2467
+ validateSandboxBootstrapSettings(sandboxConfig);
2270
2468
  this.settings = settings;
2469
+ this.sandboxConfig = sandboxConfig;
2271
2470
  this.id = settings.id;
2272
2471
  this.userTools = (_a3 = settings.tools) != null ? _a3 : {};
2273
2472
  this.permissionMode = resolvePermissionMode({
@@ -2329,17 +2528,18 @@ var HarnessAgent = class {
2329
2528
  const effectiveContinueFrom = validatedContinueFrom != null ? validatedContinueFrom : validatedResumeFrom == null ? void 0 : validatedResumeFrom.continueFrom;
2330
2529
  const isResumedSession = validatedResumeFrom != null || effectiveContinueFrom != null;
2331
2530
  let recipe;
2332
- let identity;
2333
2531
  if (harness.getBootstrap != null) {
2334
2532
  recipe = await harness.getBootstrap({ abortSignal });
2335
- identity = await hashBootstrap(recipe);
2336
2533
  }
2534
+ const sandboxBootstrapPlan = await createSandboxBootstrapPlan({
2535
+ recipe,
2536
+ settings: this.sandboxConfig
2537
+ });
2337
2538
  const acquiredSandboxSession = await this._acquireSandbox({
2338
2539
  sandboxProvider,
2339
2540
  sessionId,
2340
2541
  isResume: isResumedSession,
2341
- recipe,
2342
- identity,
2542
+ bootstrapPlan: sandboxBootstrapPlan,
2343
2543
  abortSignal
2344
2544
  });
2345
2545
  const leased = applyPortLease({
@@ -2349,22 +2549,28 @@ var HarnessAgent = class {
2349
2549
  });
2350
2550
  const sandboxSession = leased.sandboxSession;
2351
2551
  const leasedBridgePort = leased.port;
2352
- const sessionWorkDir = `${sandboxSession.defaultWorkingDirectory}/${harness.harnessId}-${sessionId}`;
2552
+ const sessionWorkDir = resolveSessionWorkDir({
2553
+ defaultWorkingDirectory: sandboxSession.defaultWorkingDirectory,
2554
+ harnessId: harness.harnessId,
2555
+ sessionId,
2556
+ workDir: sandboxBootstrapPlan.workDir
2557
+ });
2353
2558
  try {
2354
- if (!isResumedSession && recipe != null && identity != null) {
2559
+ if (!isResumedSession && sandboxBootstrapPlan.recipe != null && sandboxBootstrapPlan.recipeIdentity != null) {
2355
2560
  await applyBootstrapRecipe(
2356
2561
  sandboxSession.restricted(),
2357
- recipe,
2358
- identity,
2562
+ sandboxBootstrapPlan.recipe,
2563
+ sandboxBootstrapPlan.recipeIdentity,
2359
2564
  { abortSignal }
2360
2565
  );
2361
2566
  }
2362
- await sandboxSession.run({
2363
- command: `mkdir -p ${sessionWorkDir}`,
2567
+ await ensureSandboxDirectory({
2568
+ session: sandboxSession,
2569
+ workDir: sessionWorkDir,
2364
2570
  abortSignal
2365
2571
  });
2366
- if (this.settings.onSandboxSession != null) {
2367
- await this.settings.onSandboxSession({
2572
+ if (this.sandboxConfig.onSession != null) {
2573
+ await this.sandboxConfig.onSession({
2368
2574
  session: sandboxSession.restricted(),
2369
2575
  sessionWorkDir,
2370
2576
  abortSignal
@@ -2521,13 +2727,8 @@ var HarnessAgent = class {
2521
2727
  return sandboxProvider.createSession({
2522
2728
  sessionId: input.sessionId,
2523
2729
  abortSignal: input.abortSignal,
2524
- identity: input.identity,
2525
- onFirstCreate: input.recipe != null && input.identity != null ? (session, opts) => applyBootstrapRecipe(
2526
- session,
2527
- input.recipe,
2528
- input.identity,
2529
- opts
2530
- ) : void 0
2730
+ identity: input.bootstrapPlan.identity,
2731
+ onFirstCreate: input.bootstrapPlan.onFirstCreate
2531
2732
  });
2532
2733
  }
2533
2734
  /*
@@ -2668,6 +2869,18 @@ var HarnessGenerateTextResult = class {
2668
2869
  return this.finalStep.providerMetadata;
2669
2870
  }
2670
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
+ }
2671
2884
  function applyPortLease(input) {
2672
2885
  const pool = input.provider.bridgePorts;
2673
2886
  if (pool == null || pool.length === 0) {
@@ -2704,21 +2917,35 @@ async function cleanupAfterStartFailure(input) {
2704
2917
 
2705
2918
  // src/agent/prewarm.ts
2706
2919
  async function prewarmHarness(options) {
2707
- var _a3, _b3;
2708
- const recipe = await ((_b3 = (_a3 = options.harness).getBootstrap) == null ? void 0 : _b3.call(_a3, {
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, {
2709
2924
  abortSignal: options.abortSignal
2710
2925
  }));
2711
- if (recipe == null) return;
2712
- const identity = await hashBootstrap(recipe);
2926
+ const bootstrapPlan = await createSandboxBootstrapPlan({
2927
+ recipe,
2928
+ settings: sandboxConfig
2929
+ });
2930
+ if (bootstrapPlan.identity == null || bootstrapPlan.onFirstCreate == null) {
2931
+ return;
2932
+ }
2713
2933
  const sandboxSession = await options.sandboxProvider.createSession({
2714
2934
  abortSignal: options.abortSignal,
2715
- identity,
2716
- onFirstCreate: (session, opts) => applyBootstrapRecipe(session, recipe, identity, opts)
2935
+ identity: bootstrapPlan.identity,
2936
+ onFirstCreate: bootstrapPlan.onFirstCreate
2717
2937
  });
2718
2938
  try {
2719
- await applyBootstrapRecipe(sandboxSession.restricted(), recipe, identity, {
2720
- abortSignal: options.abortSignal
2721
- });
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
+ }
2722
2949
  } finally {
2723
2950
  await Promise.resolve(sandboxSession.stop()).catch(() => {
2724
2951
  });