@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-sdk/harness",
3
- "version": "1.0.0-beta.24",
3
+ "version": "1.0.0-beta.26",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "sideEffects": false,
@@ -46,7 +46,7 @@
46
46
  "dependencies": {
47
47
  "@ai-sdk/provider": "4.0.0-beta.20",
48
48
  "@ai-sdk/provider-utils": "5.0.0-beta.50",
49
- "ai": "7.0.0-beta.185"
49
+ "ai": "7.0.0-beta.186"
50
50
  },
51
51
  "peerDependencies": {
52
52
  "ws": "^8.21.0"
@@ -18,6 +18,48 @@ export type HarnessAgentToolApprovalConfiguration = Readonly<
18
18
  Record<string, ToolApprovalStatus>
19
19
  >;
20
20
 
21
+ export type HarnessAgentSandboxConfig = {
22
+ /**
23
+ * Optional fixed working directory for all sessions, relative to the
24
+ * sandbox's default working directory. When omitted, sessions keep the
25
+ * existing `<harnessId>-<sessionId>` work directory.
26
+ */
27
+ readonly workDir?: string;
28
+
29
+ /**
30
+ * Caller-controlled identity for `onBootstrap`. Change this whenever the
31
+ * bootstrap side effects should invalidate the reusable sandbox snapshot.
32
+ */
33
+ readonly bootstrapHash?: string;
34
+
35
+ /**
36
+ * Called during sandbox template creation after the harness adapter's own
37
+ * bootstrap has run and before snapshot-capable providers publish a snapshot.
38
+ *
39
+ * `bootstrapHash` must be provided with this callback.
40
+ */
41
+ readonly onBootstrap?: (opts: {
42
+ readonly session: SandboxSession;
43
+ readonly workDir: string;
44
+ readonly abortSignal?: AbortSignal;
45
+ }) => Promise<void>;
46
+
47
+ /**
48
+ * Called after each sandbox session is acquired and the session work
49
+ * directory exists, before the harness adapter starts. Runs for fresh and
50
+ * resumed sessions.
51
+ *
52
+ * Use this to write per-session config, install lightweight tools, activate
53
+ * licenses, or prepare files in `sessionWorkDir`. Keep it idempotent if the
54
+ * agent may resume sessions.
55
+ */
56
+ readonly onSession?: (opts: {
57
+ readonly session: SandboxSession;
58
+ readonly sessionWorkDir: string;
59
+ readonly abortSignal?: AbortSignal;
60
+ }) => Promise<void>;
61
+ };
62
+
21
63
  /**
22
64
  * Construction-time settings for a `HarnessAgent`.
23
65
  *
@@ -92,19 +134,12 @@ export type HarnessAgentSettings<
92
134
  readonly sandbox: HarnessV1SandboxProvider;
93
135
 
94
136
  /**
95
- * Called after each sandbox session is acquired and the session work
96
- * directory exists, before the harness adapter starts. Runs for fresh and
97
- * resumed sessions.
98
- *
99
- * Use this to write per-session config, install lightweight tools, activate
100
- * licenses, or prepare files in `sessionWorkDir`. Keep it idempotent if the
101
- * agent may resume sessions.
137
+ * Sandbox working-directory and lifecycle hook configuration.
102
138
  */
103
- readonly onSandboxSession?: (opts: {
104
- readonly session: SandboxSession;
105
- readonly sessionWorkDir: string;
106
- readonly abortSignal?: AbortSignal;
107
- }) => Promise<void>;
139
+ readonly sandboxConfig?: HarnessAgentSandboxConfig;
140
+
141
+ /** @deprecated Use `sandboxConfig.onSession` instead. */
142
+ readonly onSandboxSession?: HarnessAgentSandboxConfig['onSession'];
108
143
 
109
144
  /**
110
145
  * Telemetry configuration. The harness drives AI SDK's pluggable
@@ -20,7 +20,10 @@ import type {
20
20
  ReasoningOutput,
21
21
  StreamTextResult,
22
22
  } from 'ai';
23
- import type { HarnessAgentSettings } from './harness-agent-settings';
23
+ import type {
24
+ HarnessAgentSandboxConfig,
25
+ HarnessAgentSettings,
26
+ } from './harness-agent-settings';
24
27
  import { HarnessAgentSession } from './harness-agent-session';
25
28
  import type {
26
29
  HarnessAgentAdapter,
@@ -34,14 +37,18 @@ import {
34
37
  collectHarnessAgentToolApprovalContinuations,
35
38
  type HarnessAgentToolApprovalContinuation,
36
39
  } from './harness-agent-tool-approval-continuation';
37
- import {
38
- applyBootstrapRecipe,
39
- hashBootstrap,
40
- } from './internal/bootstrap-recipe';
40
+ import { applyBootstrapRecipe } from './internal/bootstrap-recipe';
41
41
  import {
42
42
  acquireBridgePort,
43
43
  releaseBridgePort,
44
44
  } from './internal/bridge-port-registry';
45
+ import {
46
+ createSandboxBootstrapPlan,
47
+ ensureSandboxDirectory,
48
+ resolveSessionWorkDir,
49
+ validateSandboxBootstrapSettings,
50
+ type SandboxBootstrapPlan,
51
+ } from './internal/sandbox-bootstrap';
45
52
  import { buildObservability } from './internal/resolve-observability';
46
53
  import { validateLifecycleStateData } from './internal/lifecycle-state-validation';
47
54
  import {
@@ -130,11 +137,15 @@ export class HarnessAgent<
130
137
  readonly tools: HarnessAllTools<THarness, TUserTools>;
131
138
 
132
139
  private readonly settings: HarnessAgentSettings<THarness, TUserTools>;
140
+ private readonly sandboxConfig: HarnessAgentSandboxConfig;
133
141
  private readonly userTools: TUserTools;
134
142
  private readonly permissionMode: HarnessAgentPermissionMode;
135
143
 
136
144
  constructor(settings: HarnessAgentSettings<THarness, TUserTools>) {
145
+ const sandboxConfig = resolveSandboxConfig(settings);
146
+ validateSandboxBootstrapSettings(sandboxConfig);
137
147
  this.settings = settings;
148
+ this.sandboxConfig = sandboxConfig;
138
149
  this.id = settings.id;
139
150
  this.userTools = settings.tools ?? ({} as TUserTools);
140
151
  this.permissionMode = resolvePermissionMode({
@@ -230,19 +241,25 @@ export class HarnessAgent<
230
241
  validatedResumeFrom != null || effectiveContinueFrom != null;
231
242
 
232
243
  let recipe: HarnessV1Bootstrap | undefined;
233
- let identity: string | undefined;
234
-
235
244
  if (harness.getBootstrap != null) {
236
245
  recipe = await harness.getBootstrap({ abortSignal });
237
- identity = await hashBootstrap(recipe);
238
246
  }
239
247
 
248
+ // Defines the hashes based on both harness bootstrap recipe and
249
+ // consumer-defined onBootstrap callback.
250
+ const sandboxBootstrapPlan = await createSandboxBootstrapPlan({
251
+ recipe,
252
+ settings: this.sandboxConfig,
253
+ });
254
+
255
+ // Acquires the concrete sandbox session, either by starting fresh and then
256
+ // creating a post-bootstrap snapshot, or by reusing a previously created
257
+ // snapshot based on the bootstrap-based hashes.
240
258
  const acquiredSandboxSession = await this._acquireSandbox({
241
259
  sandboxProvider,
242
260
  sessionId,
243
261
  isResume: isResumedSession,
244
- recipe,
245
- identity,
262
+ bootstrapPlan: sandboxBootstrapPlan,
246
263
  abortSignal,
247
264
  });
248
265
 
@@ -253,27 +270,37 @@ export class HarnessAgent<
253
270
  });
254
271
  const sandboxSession = leased.sandboxSession;
255
272
  const leasedBridgePort = leased.port;
256
- const sessionWorkDir = `${sandboxSession.defaultWorkingDirectory}/${harness.harnessId}-${sessionId}`;
273
+ const sessionWorkDir = resolveSessionWorkDir({
274
+ defaultWorkingDirectory: sandboxSession.defaultWorkingDirectory,
275
+ harnessId: harness.harnessId,
276
+ sessionId,
277
+ workDir: sandboxBootstrapPlan.workDir,
278
+ });
257
279
 
258
280
  try {
259
- /*
260
- * Adapter bootstrap is one-time work for fresh sessions. The consumer
261
- * hook runs for every acquired sandbox session after the work dir exists.
262
- */
263
- if (!isResumedSession && recipe != null && identity != null) {
281
+ // In case the sandbox session was created with a custom sandbox, or in
282
+ // case the sandbox provider doesn't respect `onFirstCreate`, we still
283
+ // have to ensure the harness bootstrap recipe has run. In the common
284
+ // scenario, this will be a cheap no-op based on just a marker check.
285
+ if (
286
+ !isResumedSession &&
287
+ sandboxBootstrapPlan.recipe != null &&
288
+ sandboxBootstrapPlan.recipeIdentity != null
289
+ ) {
264
290
  await applyBootstrapRecipe(
265
291
  sandboxSession.restricted(),
266
- recipe,
267
- identity,
292
+ sandboxBootstrapPlan.recipe,
293
+ sandboxBootstrapPlan.recipeIdentity,
268
294
  { abortSignal },
269
295
  );
270
296
  }
271
- await sandboxSession.run({
272
- command: `mkdir -p ${sessionWorkDir}`,
297
+ await ensureSandboxDirectory({
298
+ session: sandboxSession,
299
+ workDir: sessionWorkDir,
273
300
  abortSignal,
274
301
  });
275
- if (this.settings.onSandboxSession != null) {
276
- await this.settings.onSandboxSession({
302
+ if (this.sandboxConfig.onSession != null) {
303
+ await this.sandboxConfig.onSession({
277
304
  session: sandboxSession.restricted(),
278
305
  sessionWorkDir,
279
306
  abortSignal,
@@ -501,8 +528,7 @@ export class HarnessAgent<
501
528
  sandboxProvider: HarnessV1SandboxProvider;
502
529
  sessionId: string;
503
530
  isResume: boolean;
504
- recipe: HarnessV1Bootstrap | undefined;
505
- identity: string | undefined;
531
+ bootstrapPlan: SandboxBootstrapPlan;
506
532
  abortSignal: AbortSignal | undefined;
507
533
  }): Promise<HarnessV1NetworkSandboxSession> {
508
534
  const { sandboxProvider } = input;
@@ -521,17 +547,8 @@ export class HarnessAgent<
521
547
  return sandboxProvider.createSession({
522
548
  sessionId: input.sessionId,
523
549
  abortSignal: input.abortSignal,
524
- identity: input.identity,
525
- onFirstCreate:
526
- input.recipe != null && input.identity != null
527
- ? (session, opts) =>
528
- applyBootstrapRecipe(
529
- session,
530
- input.recipe!,
531
- input.identity!,
532
- opts,
533
- )
534
- : undefined,
550
+ identity: input.bootstrapPlan.identity,
551
+ onFirstCreate: input.bootstrapPlan.onFirstCreate,
535
552
  });
536
553
  }
537
554
 
@@ -756,6 +773,24 @@ class HarnessGenerateTextResult<
756
773
  }
757
774
  }
758
775
 
776
+ function resolveSandboxConfig(
777
+ settings: Pick<HarnessAgentSettings, 'sandboxConfig' | 'onSandboxSession'>,
778
+ ): HarnessAgentSandboxConfig {
779
+ if (settings.onSandboxSession != null) {
780
+ console.warn(
781
+ 'HarnessAgent: `onSandboxSession` is deprecated. Use `sandboxConfig.onSession` instead.',
782
+ );
783
+ }
784
+
785
+ return {
786
+ ...settings.sandboxConfig,
787
+ ...(settings.sandboxConfig?.onSession == null &&
788
+ settings.onSandboxSession != null
789
+ ? { onSession: settings.onSandboxSession }
790
+ : {}),
791
+ };
792
+ }
793
+
759
794
  /*
760
795
  * Bridge-port leasing helper. Returns the port-narrowed network sandbox session
761
796
  * plus the leased port (or `undefined` when the provider has no port pool). Kept here
@@ -16,7 +16,7 @@ export const BOOTSTRAP_SCHEMA_VERSION = 1;
16
16
  * Used by sandbox providers as part of the persistent sandbox name so
17
17
  * recipe changes automatically invalidate snapshots.
18
18
  */
19
- export async function hashBootstrap(
19
+ export async function hashHarnessBootstrap(
20
20
  recipe: HarnessV1Bootstrap,
21
21
  ): Promise<string> {
22
22
  const encoder = new TextEncoder();
@@ -0,0 +1,266 @@
1
+ import { posix } from 'node:path';
2
+ import type { Experimental_SandboxSession as SandboxSession } from '@ai-sdk/provider-utils';
3
+ import type { HarnessV1Bootstrap } from '../../v1';
4
+ import type { HarnessAgentSandboxConfig } from '../harness-agent-settings';
5
+ import { applyBootstrapRecipe, hashHarnessBootstrap } from './bootstrap-recipe';
6
+
7
+ const SANDBOX_BOOTSTRAP_IDENTITY_VERSION = 1;
8
+
9
+ type SandboxBootstrapSettings = Omit<HarnessAgentSandboxConfig, 'onSession'>;
10
+
11
+ export type SandboxBootstrapPlan = {
12
+ readonly recipe?: HarnessV1Bootstrap;
13
+ readonly recipeIdentity?: string;
14
+ readonly identity?: string;
15
+ readonly workDir?: string;
16
+ readonly onFirstCreate?: (
17
+ session: SandboxSession,
18
+ opts: { abortSignal?: AbortSignal },
19
+ ) => Promise<void>;
20
+ };
21
+
22
+ export function validateSandboxBootstrapSettings(
23
+ settings: SandboxBootstrapSettings,
24
+ ): void {
25
+ if ((settings.onBootstrap == null) !== (settings.bootstrapHash == null)) {
26
+ throw new Error(
27
+ 'HarnessAgent: `sandboxConfig.onBootstrap` and `sandboxConfig.bootstrapHash` must be provided together.',
28
+ );
29
+ }
30
+
31
+ if (settings.workDir != null) {
32
+ normalizeSandboxWorkDir(settings.workDir);
33
+ }
34
+ }
35
+
36
+ export function normalizeSandboxWorkDir(workDir: string): string {
37
+ if (workDir.length === 0) {
38
+ throw new Error('HarnessAgent: `sandboxConfig.workDir` must not be empty.');
39
+ }
40
+ if (workDir.includes('\0')) {
41
+ throw new Error(
42
+ 'HarnessAgent: `sandboxConfig.workDir` must not contain NUL.',
43
+ );
44
+ }
45
+ if (workDir.includes('\\')) {
46
+ throw new Error(
47
+ 'HarnessAgent: `sandboxConfig.workDir` must use POSIX path separators.',
48
+ );
49
+ }
50
+ if (posix.isAbsolute(workDir)) {
51
+ throw new Error('HarnessAgent: `sandboxConfig.workDir` must be relative.');
52
+ }
53
+
54
+ const normalized = posix.normalize(workDir);
55
+ if (
56
+ normalized === '.' ||
57
+ normalized === '..' ||
58
+ normalized.startsWith('../')
59
+ ) {
60
+ throw new Error(
61
+ 'HarnessAgent: `sandboxConfig.workDir` must stay inside the sandbox default working directory.',
62
+ );
63
+ }
64
+ return normalized;
65
+ }
66
+
67
+ export function resolveSessionWorkDir({
68
+ defaultWorkingDirectory,
69
+ harnessId,
70
+ sessionId,
71
+ workDir,
72
+ }: {
73
+ readonly defaultWorkingDirectory: string;
74
+ readonly harnessId: string;
75
+ readonly sessionId: string;
76
+ readonly workDir?: string;
77
+ }): string {
78
+ return joinSandboxPath({
79
+ base: defaultWorkingDirectory,
80
+ path: workDir ?? `${harnessId}-${sessionId}`,
81
+ });
82
+ }
83
+
84
+ export async function createSandboxBootstrapPlan({
85
+ recipe,
86
+ settings,
87
+ }: {
88
+ readonly recipe?: HarnessV1Bootstrap;
89
+ readonly settings: SandboxBootstrapSettings;
90
+ }): Promise<SandboxBootstrapPlan> {
91
+ const workDir =
92
+ settings.workDir == null
93
+ ? undefined
94
+ : normalizeSandboxWorkDir(settings.workDir);
95
+ const recipeIdentity =
96
+ recipe == null ? undefined : await hashHarnessBootstrap(recipe);
97
+ const hasCallerBootstrap = settings.onBootstrap != null;
98
+ const needsCombinedIdentity =
99
+ hasCallerBootstrap || (recipeIdentity != null && workDir != null);
100
+ const identity =
101
+ needsCombinedIdentity && (recipeIdentity != null || hasCallerBootstrap)
102
+ ? await hashSandboxBootstrapIdentity({
103
+ recipeIdentity,
104
+ bootstrapHash: settings.bootstrapHash,
105
+ workDir,
106
+ })
107
+ : recipeIdentity;
108
+
109
+ return {
110
+ ...(recipe != null ? { recipe } : {}),
111
+ ...(recipeIdentity != null ? { recipeIdentity } : {}),
112
+ ...(identity != null ? { identity } : {}),
113
+ ...(workDir != null ? { workDir } : {}),
114
+ ...(recipe != null || settings.onBootstrap != null
115
+ ? {
116
+ onFirstCreate: (session, opts) =>
117
+ runSandboxBootstrap({
118
+ session,
119
+ recipe,
120
+ recipeIdentity,
121
+ workDir,
122
+ onBootstrap: settings.onBootstrap,
123
+ abortSignal: opts.abortSignal,
124
+ }),
125
+ }
126
+ : {}),
127
+ };
128
+ }
129
+
130
+ export async function runSandboxBootstrap({
131
+ session,
132
+ recipe,
133
+ recipeIdentity,
134
+ workDir,
135
+ onBootstrap,
136
+ abortSignal,
137
+ }: {
138
+ readonly session: SandboxSession;
139
+ readonly recipe?: HarnessV1Bootstrap;
140
+ readonly recipeIdentity?: string;
141
+ readonly workDir?: string;
142
+ readonly onBootstrap?: SandboxBootstrapSettings['onBootstrap'];
143
+ readonly abortSignal?: AbortSignal;
144
+ }): Promise<void> {
145
+ if (recipe != null && recipeIdentity != null) {
146
+ await applyBootstrapRecipe(session, recipe, recipeIdentity, {
147
+ abortSignal,
148
+ });
149
+ }
150
+
151
+ if (onBootstrap == null) return;
152
+
153
+ const defaultWorkingDirectory = await resolveDefaultWorkingDirectory({
154
+ session,
155
+ abortSignal,
156
+ });
157
+ const bootstrapWorkDir =
158
+ workDir == null
159
+ ? defaultWorkingDirectory
160
+ : joinSandboxPath({
161
+ base: defaultWorkingDirectory,
162
+ path: workDir,
163
+ });
164
+
165
+ await ensureSandboxDirectory({
166
+ session,
167
+ workDir: bootstrapWorkDir,
168
+ abortSignal,
169
+ });
170
+ await onBootstrap({ session, workDir: bootstrapWorkDir, abortSignal });
171
+ }
172
+
173
+ export async function resolveDefaultWorkingDirectory({
174
+ session,
175
+ abortSignal,
176
+ }: {
177
+ readonly session: SandboxSession;
178
+ readonly abortSignal?: AbortSignal;
179
+ }): Promise<string> {
180
+ const result = await session.run({
181
+ command: 'pwd',
182
+ abortSignal,
183
+ });
184
+ if (result.exitCode !== 0) {
185
+ throw new Error(
186
+ `Failed to resolve sandbox default working directory (exit ${result.exitCode}): ${result.stderr || result.stdout}`,
187
+ );
188
+ }
189
+
190
+ const cwd = result.stdout.trim();
191
+ if (!posix.isAbsolute(cwd)) {
192
+ throw new Error(
193
+ `Failed to resolve sandbox default working directory: expected an absolute path, got ${JSON.stringify(cwd)}.`,
194
+ );
195
+ }
196
+ return cwd === '/' ? cwd : cwd.replace(/\/+$/, '');
197
+ }
198
+
199
+ export async function ensureSandboxDirectory({
200
+ session,
201
+ workDir,
202
+ abortSignal,
203
+ }: {
204
+ readonly session: SandboxSession;
205
+ readonly workDir: string;
206
+ readonly abortSignal?: AbortSignal;
207
+ }): Promise<void> {
208
+ const result = await session.run({
209
+ command: 'mkdir -p "$WORK_DIR"',
210
+ env: { WORK_DIR: workDir },
211
+ abortSignal,
212
+ });
213
+ if (result.exitCode !== 0) {
214
+ throw new Error(
215
+ `Failed to create sandbox work directory ${workDir} (exit ${result.exitCode}): ${result.stderr || result.stdout}`,
216
+ );
217
+ }
218
+ }
219
+
220
+ async function hashSandboxBootstrapIdentity({
221
+ recipeIdentity,
222
+ bootstrapHash,
223
+ workDir,
224
+ }: {
225
+ readonly recipeIdentity?: string;
226
+ readonly bootstrapHash?: string;
227
+ readonly workDir?: string;
228
+ }): Promise<string> {
229
+ const encoder = new TextEncoder();
230
+ const chunks: Uint8Array[] = [];
231
+ const pushString = (value: string) => {
232
+ chunks.push(encoder.encode(value));
233
+ chunks.push(encoder.encode('\0'));
234
+ };
235
+
236
+ pushString(String(SANDBOX_BOOTSTRAP_IDENTITY_VERSION));
237
+ pushString(recipeIdentity ?? '');
238
+ pushString(bootstrapHash ?? '');
239
+ pushString(workDir ?? '');
240
+
241
+ const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
242
+ const buffer = new Uint8Array(totalLength);
243
+ let offset = 0;
244
+ for (const chunk of chunks) {
245
+ buffer.set(chunk, offset);
246
+ offset += chunk.length;
247
+ }
248
+
249
+ const digest = await crypto.subtle.digest('SHA-256', buffer);
250
+ const bytes = new Uint8Array(digest);
251
+ let hex = '';
252
+ for (let i = 0; i < 8; i++) {
253
+ hex += bytes[i].toString(16).padStart(2, '0');
254
+ }
255
+ return hex;
256
+ }
257
+
258
+ function joinSandboxPath({
259
+ base,
260
+ path,
261
+ }: {
262
+ readonly base: string;
263
+ readonly path: string;
264
+ }): string {
265
+ return posix.join(base, path);
266
+ }
@@ -1,9 +1,13 @@
1
1
  import type { HarnessV1SandboxProvider } from '../v1';
2
2
  import type { HarnessAgentAdapter } from './harness-agent-types';
3
+ import type { HarnessAgentSandboxConfig } from './harness-agent-settings';
4
+ import { applyBootstrapRecipe } from './internal/bootstrap-recipe';
3
5
  import {
4
- applyBootstrapRecipe,
5
- hashBootstrap,
6
- } from './internal/bootstrap-recipe';
6
+ createSandboxBootstrapPlan,
7
+ validateSandboxBootstrapSettings,
8
+ } from './internal/sandbox-bootstrap';
9
+
10
+ type SandboxBootstrapSettings = Omit<HarnessAgentSandboxConfig, 'onSession'>;
7
11
 
8
12
  /**
9
13
  * Pre-build a harness's sandbox template without running an agent. Idempotent:
@@ -22,25 +26,39 @@ import {
22
26
  export async function prewarmHarness(options: {
23
27
  readonly harness: HarnessAgentAdapter;
24
28
  readonly sandboxProvider: HarnessV1SandboxProvider;
29
+ readonly sandboxConfig?: SandboxBootstrapSettings;
25
30
  readonly abortSignal?: AbortSignal;
26
31
  }): Promise<void> {
32
+ const sandboxConfig = options.sandboxConfig ?? {};
33
+ validateSandboxBootstrapSettings(sandboxConfig);
27
34
  const recipe = await options.harness.getBootstrap?.({
28
35
  abortSignal: options.abortSignal,
29
36
  });
30
- if (recipe == null) return;
37
+ const bootstrapPlan = await createSandboxBootstrapPlan({
38
+ recipe,
39
+ settings: sandboxConfig,
40
+ });
41
+ if (bootstrapPlan.identity == null || bootstrapPlan.onFirstCreate == null) {
42
+ return;
43
+ }
31
44
 
32
- const identity = await hashBootstrap(recipe);
33
45
  const sandboxSession = await options.sandboxProvider.createSession({
34
46
  abortSignal: options.abortSignal,
35
- identity,
36
- onFirstCreate: (session, opts) =>
37
- applyBootstrapRecipe(session, recipe, identity, opts),
47
+ identity: bootstrapPlan.identity,
48
+ onFirstCreate: bootstrapPlan.onFirstCreate,
38
49
  });
39
50
 
40
51
  try {
41
- await applyBootstrapRecipe(sandboxSession.restricted(), recipe, identity, {
42
- abortSignal: options.abortSignal,
43
- });
52
+ if (bootstrapPlan.recipe != null && bootstrapPlan.recipeIdentity != null) {
53
+ await applyBootstrapRecipe(
54
+ sandboxSession.restricted(),
55
+ bootstrapPlan.recipe,
56
+ bootstrapPlan.recipeIdentity,
57
+ {
58
+ abortSignal: options.abortSignal,
59
+ },
60
+ );
61
+ }
44
62
  } finally {
45
63
  await Promise.resolve(sandboxSession.stop()).catch(() => {});
46
64
  }