@ai-sdk/harness 1.0.51 → 1.0.53

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/dist/index.d.ts CHANGED
@@ -6,7 +6,9 @@ import { JSONValue, LanguageModelV4ToolCall, LanguageModelV4ToolApprovalRequest,
6
6
 
7
7
  /**
8
8
  * One file to write into the sandbox as part of an adapter's bootstrap recipe.
9
- * Paths should live under {@link HarnessV1Bootstrap.bootstrapDir}.
9
+ * Absolute paths are used as-is. Relative paths are resolved against the
10
+ * sandbox's default working directory. Paths should live under
11
+ * {@link HarnessV1Bootstrap.bootstrapDir}.
10
12
  */
11
13
  interface HarnessV1BootstrapFile {
12
14
  readonly path: string;
@@ -19,6 +21,11 @@ interface HarnessV1BootstrapFile {
19
21
  */
20
22
  interface HarnessV1BootstrapCommand {
21
23
  readonly command: string;
24
+ /**
25
+ * Absolute working directories are used as-is. Relative working directories
26
+ * are resolved against the sandbox's default working directory. When
27
+ * omitted, the command runs from the recipe's bootstrap directory.
28
+ */
22
29
  readonly workingDirectory?: string;
23
30
  }
24
31
  /**
@@ -34,15 +41,16 @@ interface HarnessV1Bootstrap {
34
41
  */
35
42
  readonly harnessId: string;
36
43
  /**
37
- * Absolute path inside the sandbox where this recipe writes its state.
38
- * The marker file lives directly under it. Files declared in {@link files}
39
- * should also use this prefix so an adapter upgrade can sweep stale state
40
- * by clearing the directory.
44
+ * Path inside the sandbox where this recipe writes its state. Absolute paths
45
+ * are used as-is. Relative paths are resolved against the sandbox's default
46
+ * working directory. The marker file lives directly under it. Files
47
+ * declared in {@link files} should also use this prefix so an adapter upgrade
48
+ * can sweep stale state by clearing the directory.
41
49
  */
42
50
  readonly bootstrapDir: string;
43
51
  /** Files to write into the sandbox before any command runs. */
44
52
  readonly files: ReadonlyArray<HarnessV1BootstrapFile>;
45
- /** Commands to run after files are written, in order. */
53
+ /** Commands to run from the bootstrap directory after files are written. */
46
54
  readonly commands: ReadonlyArray<HarnessV1BootstrapCommand>;
47
55
  }
48
56
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-sdk/harness",
3
- "version": "1.0.51",
3
+ "version": "1.0.53",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "sideEffects": false,
@@ -45,8 +45,8 @@
45
45
  },
46
46
  "dependencies": {
47
47
  "@ai-sdk/provider": "4.0.4",
48
- "@ai-sdk/provider-utils": "5.0.17",
49
- "ai": "7.0.45"
48
+ "@ai-sdk/provider-utils": "5.0.18",
49
+ "ai": "7.0.47"
50
50
  },
51
51
  "peerDependencies": {
52
52
  "ws": "^8.21.0",
@@ -65,7 +65,7 @@
65
65
  "typescript": "5.8.3",
66
66
  "ws": "^8.21.0",
67
67
  "zod": "3.25.76",
68
- "@ai-sdk/otel": "1.0.45",
68
+ "@ai-sdk/otel": "1.0.47",
69
69
  "@vercel/ai-tsconfig": "0.0.0"
70
70
  },
71
71
  "engines": {
@@ -310,12 +310,13 @@ export class HarnessAgent<
310
310
  sandboxBootstrapPlan.recipe != null &&
311
311
  sandboxBootstrapPlan.recipeIdentity != null
312
312
  ) {
313
- await applyBootstrapRecipe(
314
- sandboxSession.restricted(),
315
- sandboxBootstrapPlan.recipe,
316
- sandboxBootstrapPlan.recipeIdentity,
317
- { abortSignal },
318
- );
313
+ await applyBootstrapRecipe({
314
+ session: sandboxSession.restricted(),
315
+ recipe: sandboxBootstrapPlan.recipe,
316
+ identity: sandboxBootstrapPlan.recipeIdentity,
317
+ defaultWorkingDirectory: sandboxSession.defaultWorkingDirectory,
318
+ abortSignal,
319
+ });
319
320
  }
320
321
  await ensureSandboxDirectory({
321
322
  session: sandboxSession,
@@ -1,3 +1,4 @@
1
+ import { posix } from 'node:path';
1
2
  import type { Experimental_SandboxSession as SandboxSession } from '@ai-sdk/provider-utils';
2
3
  import type { HarnessV1Bootstrap } from '../../v1';
3
4
 
@@ -59,55 +60,102 @@ export async function hashHarnessBootstrap(
59
60
 
60
61
  /**
61
62
  * Absolute path of the marker file the framework writes after a recipe runs
62
- * successfully. Presence of this path inside the sandbox indicates the
63
- * recipe with the matching `identity` has already been applied.
63
+ * successfully. Presence of this path inside the sandbox indicates the recipe
64
+ * with the matching `identity` has already been applied.
64
65
  */
65
- export function bootstrapMarkerPath(
66
- recipe: HarnessV1Bootstrap,
67
- identity: string,
68
- ): string {
69
- return `${recipe.bootstrapDir}/.bootstrap-${identity}.ok`;
66
+ export function bootstrapMarkerPath({
67
+ recipe,
68
+ identity,
69
+ defaultWorkingDirectory,
70
+ }: {
71
+ recipe: HarnessV1Bootstrap;
72
+ identity: string;
73
+ defaultWorkingDirectory: string;
74
+ }): string {
75
+ return posix.join(
76
+ resolveBootstrapPath({
77
+ path: recipe.bootstrapDir,
78
+ defaultWorkingDirectory,
79
+ }),
80
+ `.bootstrap-${identity}.ok`,
81
+ );
70
82
  }
71
83
 
72
84
  /**
73
85
  * Apply a bootstrap recipe to a sandbox session idempotently. Reads the
74
- * marker file; if it exists, returns immediately. Otherwise writes the
75
- * recipe's files, runs its commands sequentially, and writes the marker
76
- * on success.
86
+ * marker file; if it exists, returns immediately. Otherwise creates the
87
+ * bootstrap directory, writes the recipe's files, runs its commands
88
+ * sequentially, and writes the marker on success.
77
89
  *
78
90
  * Safe to call multiple times. For sandboxes that already contain the
79
91
  * recipe (resumed from snapshot, reused across sessions, or applied by
80
92
  * an earlier process) this is a single fast read.
81
93
  */
82
- export async function applyBootstrapRecipe(
83
- session: SandboxSession,
84
- recipe: HarnessV1Bootstrap,
85
- identity: string,
86
- options?: { abortSignal?: AbortSignal },
87
- ): Promise<void> {
88
- const markerPath = bootstrapMarkerPath(recipe, identity);
94
+ export async function applyBootstrapRecipe({
95
+ session,
96
+ recipe,
97
+ identity,
98
+ defaultWorkingDirectory,
99
+ abortSignal,
100
+ }: {
101
+ session: SandboxSession;
102
+ recipe: HarnessV1Bootstrap;
103
+ identity: string;
104
+ defaultWorkingDirectory: string;
105
+ abortSignal?: AbortSignal;
106
+ }): Promise<void> {
107
+ const markerPath = bootstrapMarkerPath({
108
+ recipe,
109
+ identity,
110
+ defaultWorkingDirectory,
111
+ });
89
112
 
90
113
  const existingMarker = await session.readTextFile({
91
114
  path: markerPath,
92
- abortSignal: options?.abortSignal,
115
+ abortSignal,
93
116
  });
94
117
  if (existingMarker !== null) {
95
118
  return;
96
119
  }
97
120
 
121
+ const bootstrapDir = resolveBootstrapPath({
122
+ path: recipe.bootstrapDir,
123
+ defaultWorkingDirectory,
124
+ });
125
+ const mkdirResult = await session.run({
126
+ command: 'mkdir -p "$BOOTSTRAP_DIR"',
127
+ workingDirectory: defaultWorkingDirectory,
128
+ env: { BOOTSTRAP_DIR: bootstrapDir },
129
+ abortSignal,
130
+ });
131
+ if (mkdirResult.exitCode !== 0) {
132
+ throw new Error(
133
+ `Failed to create bootstrap directory for harness '${recipe.harnessId}' (exit ${mkdirResult.exitCode}): ${bootstrapDir}\n${mkdirResult.stderr || mkdirResult.stdout}`,
134
+ );
135
+ }
136
+
98
137
  for (const file of recipe.files) {
99
138
  await session.writeTextFile({
100
- path: file.path,
139
+ path: resolveBootstrapPath({
140
+ path: file.path,
141
+ defaultWorkingDirectory,
142
+ }),
101
143
  content: file.content,
102
- abortSignal: options?.abortSignal,
144
+ abortSignal,
103
145
  });
104
146
  }
105
147
 
106
148
  for (const cmd of recipe.commands) {
107
149
  const result = await session.run({
108
150
  command: cmd.command,
109
- workingDirectory: cmd.workingDirectory,
110
- abortSignal: options?.abortSignal,
151
+ workingDirectory:
152
+ cmd.workingDirectory == null
153
+ ? bootstrapDir
154
+ : resolveBootstrapPath({
155
+ path: cmd.workingDirectory,
156
+ defaultWorkingDirectory,
157
+ }),
158
+ abortSignal,
111
159
  });
112
160
  if (result.exitCode !== 0) {
113
161
  throw new Error(
@@ -119,6 +167,18 @@ export async function applyBootstrapRecipe(
119
167
  await session.writeTextFile({
120
168
  path: markerPath,
121
169
  content: '',
122
- abortSignal: options?.abortSignal,
170
+ abortSignal,
123
171
  });
124
172
  }
173
+
174
+ function resolveBootstrapPath({
175
+ path,
176
+ defaultWorkingDirectory,
177
+ }: {
178
+ path: string;
179
+ defaultWorkingDirectory: string;
180
+ }): string {
181
+ return posix.isAbsolute(path)
182
+ ? path
183
+ : posix.resolve(defaultWorkingDirectory, path);
184
+ }
@@ -133,6 +133,7 @@ export async function runSandboxBootstrap({
133
133
  recipeIdentity,
134
134
  workDir,
135
135
  onBootstrap,
136
+ defaultWorkingDirectory,
136
137
  abortSignal,
137
138
  }: {
138
139
  readonly session: SandboxSession;
@@ -140,25 +141,35 @@ export async function runSandboxBootstrap({
140
141
  readonly recipeIdentity?: string;
141
142
  readonly workDir?: string;
142
143
  readonly onBootstrap?: SandboxBootstrapSettings['onBootstrap'];
144
+ readonly defaultWorkingDirectory?: string;
143
145
  readonly abortSignal?: AbortSignal;
144
146
  }): Promise<void> {
147
+ if (recipe == null && onBootstrap == null) return;
148
+
149
+ const resolvedDefaultWorkingDirectory =
150
+ defaultWorkingDirectory ??
151
+ (await resolveDefaultWorkingDirectory({
152
+ session,
153
+ abortSignal,
154
+ }));
155
+
145
156
  if (recipe != null && recipeIdentity != null) {
146
- await applyBootstrapRecipe(session, recipe, recipeIdentity, {
157
+ await applyBootstrapRecipe({
158
+ session,
159
+ recipe,
160
+ identity: recipeIdentity,
161
+ defaultWorkingDirectory: resolvedDefaultWorkingDirectory,
147
162
  abortSignal,
148
163
  });
149
164
  }
150
165
 
151
166
  if (onBootstrap == null) return;
152
167
 
153
- const defaultWorkingDirectory = await resolveDefaultWorkingDirectory({
154
- session,
155
- abortSignal,
156
- });
157
168
  const bootstrapWorkDir =
158
169
  workDir == null
159
- ? defaultWorkingDirectory
170
+ ? resolvedDefaultWorkingDirectory
160
171
  : joinSandboxPath({
161
- base: defaultWorkingDirectory,
172
+ base: resolvedDefaultWorkingDirectory,
162
173
  path: workDir,
163
174
  });
164
175
 
@@ -50,14 +50,13 @@ export async function prepareHarnessSandboxTemplate(options: {
50
50
 
51
51
  try {
52
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
- );
53
+ await applyBootstrapRecipe({
54
+ session: sandboxSession.restricted(),
55
+ recipe: bootstrapPlan.recipe,
56
+ identity: bootstrapPlan.recipeIdentity,
57
+ defaultWorkingDirectory: sandboxSession.defaultWorkingDirectory,
58
+ abortSignal: options.abortSignal,
59
+ });
61
60
  }
62
61
  } finally {
63
62
  await Promise.resolve(sandboxSession.stop()).catch(() => {});
@@ -7,6 +7,7 @@ import {
7
7
  } from './internal/bootstrap-recipe';
8
8
  import {
9
9
  normalizeSandboxWorkDir,
10
+ resolveDefaultWorkingDirectory,
10
11
  runSandboxBootstrap,
11
12
  validateSandboxBootstrapSettings,
12
13
  } from './internal/sandbox-bootstrap';
@@ -62,6 +63,7 @@ export async function prepareSandboxForHarness(options: {
62
63
  : normalizeSandboxWorkDir(sandboxConfig.workDir);
63
64
  const recipeIdentities: Record<string, string> = {};
64
65
  const skippedHarnessIds: string[] = [];
66
+ let defaultWorkingDirectory: string | undefined;
65
67
 
66
68
  for (const harness of harnesses) {
67
69
  const recipe = await harness.getBootstrap?.({
@@ -74,7 +76,15 @@ export async function prepareSandboxForHarness(options: {
74
76
 
75
77
  const recipeIdentity = await hashHarnessBootstrap(recipe);
76
78
  recipeIdentities[harness.harnessId] = recipeIdentity;
77
- await applyBootstrapRecipe(options.session, recipe, recipeIdentity, {
79
+ defaultWorkingDirectory ??= await resolveDefaultWorkingDirectory({
80
+ session: options.session,
81
+ abortSignal: options.abortSignal,
82
+ });
83
+ await applyBootstrapRecipe({
84
+ session: options.session,
85
+ recipe,
86
+ identity: recipeIdentity,
87
+ defaultWorkingDirectory,
78
88
  abortSignal: options.abortSignal,
79
89
  });
80
90
  }
@@ -84,6 +94,7 @@ export async function prepareSandboxForHarness(options: {
84
94
  session: options.session,
85
95
  workDir,
86
96
  onBootstrap: sandboxConfig.onBootstrap,
97
+ defaultWorkingDirectory,
87
98
  abortSignal: options.abortSignal,
88
99
  });
89
100
  }
@@ -1,6 +1,8 @@
1
1
  /**
2
2
  * One file to write into the sandbox as part of an adapter's bootstrap recipe.
3
- * Paths should live under {@link HarnessV1Bootstrap.bootstrapDir}.
3
+ * Absolute paths are used as-is. Relative paths are resolved against the
4
+ * sandbox's default working directory. Paths should live under
5
+ * {@link HarnessV1Bootstrap.bootstrapDir}.
4
6
  */
5
7
  export interface HarnessV1BootstrapFile {
6
8
  readonly path: string;
@@ -14,6 +16,11 @@ export interface HarnessV1BootstrapFile {
14
16
  */
15
17
  export interface HarnessV1BootstrapCommand {
16
18
  readonly command: string;
19
+ /**
20
+ * Absolute working directories are used as-is. Relative working directories
21
+ * are resolved against the sandbox's default working directory. When
22
+ * omitted, the command runs from the recipe's bootstrap directory.
23
+ */
17
24
  readonly workingDirectory?: string;
18
25
  }
19
26
 
@@ -31,16 +38,17 @@ export interface HarnessV1Bootstrap {
31
38
  readonly harnessId: string;
32
39
 
33
40
  /**
34
- * Absolute path inside the sandbox where this recipe writes its state.
35
- * The marker file lives directly under it. Files declared in {@link files}
36
- * should also use this prefix so an adapter upgrade can sweep stale state
37
- * by clearing the directory.
41
+ * Path inside the sandbox where this recipe writes its state. Absolute paths
42
+ * are used as-is. Relative paths are resolved against the sandbox's default
43
+ * working directory. The marker file lives directly under it. Files
44
+ * declared in {@link files} should also use this prefix so an adapter upgrade
45
+ * can sweep stale state by clearing the directory.
38
46
  */
39
47
  readonly bootstrapDir: string;
40
48
 
41
49
  /** Files to write into the sandbox before any command runs. */
42
50
  readonly files: ReadonlyArray<HarnessV1BootstrapFile>;
43
51
 
44
- /** Commands to run after files are written, in order. */
52
+ /** Commands to run from the bootstrap directory after files are written. */
45
53
  readonly commands: ReadonlyArray<HarnessV1BootstrapCommand>;
46
54
  }