@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 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
- 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
 
@@ -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 hashBootstrap(recipe) {
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
- recipe,
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 = `${sandboxSession.defaultWorkingDirectory}/${harness.harnessId}-${sessionId}`;
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 && identity != null) {
2559
+ if (!isResumedSession && sandboxBootstrapPlan.recipe != null && sandboxBootstrapPlan.recipeIdentity != null) {
2369
2560
  await applyBootstrapRecipe(
2370
2561
  sandboxSession.restricted(),
2371
- recipe,
2372
- identity,
2562
+ sandboxBootstrapPlan.recipe,
2563
+ sandboxBootstrapPlan.recipeIdentity,
2373
2564
  { abortSignal }
2374
2565
  );
2375
2566
  }
2376
- await sandboxSession.run({
2377
- command: `mkdir -p ${sessionWorkDir}`,
2567
+ await ensureSandboxDirectory({
2568
+ session: sandboxSession,
2569
+ workDir: sessionWorkDir,
2378
2570
  abortSignal
2379
2571
  });
2380
- if (this.settings.onSandboxSession != null) {
2381
- await this.settings.onSandboxSession({
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.recipe != null && input.identity != null ? (session, opts) => applyBootstrapRecipe(
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 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, {
2723
2924
  abortSignal: options.abortSignal
2724
2925
  }));
2725
- if (recipe == null) return;
2726
- 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
+ }
2727
2933
  const sandboxSession = await options.sandboxProvider.createSession({
2728
2934
  abortSignal: options.abortSignal,
2729
- identity,
2730
- onFirstCreate: (session, opts) => applyBootstrapRecipe(session, recipe, identity, opts)
2935
+ identity: bootstrapPlan.identity,
2936
+ onFirstCreate: bootstrapPlan.onFirstCreate
2731
2937
  });
2732
2938
  try {
2733
- await applyBootstrapRecipe(sandboxSession.restricted(), recipe, identity, {
2734
- abortSignal: options.abortSignal
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
  });