@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/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # @ai-sdk/harness
2
2
 
3
+ ## 1.0.53
4
+
5
+ ### Patch Changes
6
+
7
+ - bdde5d9: fix(harness): avoid placing harness bootstrap files in `/tmp` and instead use sandbox working directory
8
+ - Updated dependencies [5fc7da5]
9
+ - Updated dependencies [93b2acd]
10
+ - @ai-sdk/provider-utils@5.0.18
11
+ - ai@7.0.47
12
+
13
+ ## 1.0.52
14
+
15
+ ### Patch Changes
16
+
17
+ - ai@7.0.46
18
+
3
19
  ## 1.0.51
4
20
 
5
21
  ### Patch Changes
package/README.md CHANGED
@@ -111,6 +111,15 @@ See the [harness adapters documentation](https://ai-sdk.dev/v7/docs/ai-sdk-harne
111
111
 
112
112
  Implement the `HarnessV1` factory and a `HarnessV1Session` whose `doPromptTurn` emits events; the agent surface, streaming, tool execution, and multi-turn state are handled for you. Read `startOpts.sandboxSession` for the network sandbox session the agent created and will stop on cleanup. Call `sandboxSession.restricted()` for the tool-safe file-IO/exec/spawn surface.
113
113
 
114
+ Bootstrap recipe paths may be absolute or relative. Relative `bootstrapDir` and
115
+ file paths are resolved against `sandboxSession.defaultWorkingDirectory`.
116
+ The framework creates `bootstrapDir` before writing files, and bootstrap
117
+ commands run from that directory by default. A relative command
118
+ `workingDirectory` override is resolved against the sandbox's default working
119
+ directory. Prefer a relative directory such as
120
+ `.harness-bootstrap/my-harness` so bootstrap assets are kept with the sandbox's
121
+ snapshot-persistent working tree.
122
+
114
123
  ```ts
115
124
  import type { HarnessV1, HarnessV1Session } from '@ai-sdk/harness';
116
125
 
@@ -75,7 +75,9 @@ interface HarnessDiagnosticConsumer {
75
75
 
76
76
  /**
77
77
  * One file to write into the sandbox as part of an adapter's bootstrap recipe.
78
- * Paths should live under {@link HarnessV1Bootstrap.bootstrapDir}.
78
+ * Absolute paths are used as-is. Relative paths are resolved against the
79
+ * sandbox's default working directory. Paths should live under
80
+ * {@link HarnessV1Bootstrap.bootstrapDir}.
79
81
  */
80
82
  interface HarnessV1BootstrapFile {
81
83
  readonly path: string;
@@ -88,6 +90,11 @@ interface HarnessV1BootstrapFile {
88
90
  */
89
91
  interface HarnessV1BootstrapCommand {
90
92
  readonly command: string;
93
+ /**
94
+ * Absolute working directories are used as-is. Relative working directories
95
+ * are resolved against the sandbox's default working directory. When
96
+ * omitted, the command runs from the recipe's bootstrap directory.
97
+ */
91
98
  readonly workingDirectory?: string;
92
99
  }
93
100
  /**
@@ -103,15 +110,16 @@ interface HarnessV1Bootstrap {
103
110
  */
104
111
  readonly harnessId: string;
105
112
  /**
106
- * Absolute path inside the sandbox where this recipe writes its state.
107
- * The marker file lives directly under it. Files declared in {@link files}
108
- * should also use this prefix so an adapter upgrade can sweep stale state
109
- * by clearing the directory.
113
+ * Path inside the sandbox where this recipe writes its state. Absolute paths
114
+ * are used as-is. Relative paths are resolved against the sandbox's default
115
+ * working directory. The marker file lives directly under it. Files
116
+ * declared in {@link files} should also use this prefix so an adapter upgrade
117
+ * can sweep stale state by clearing the directory.
110
118
  */
111
119
  readonly bootstrapDir: string;
112
120
  /** Files to write into the sandbox before any command runs. */
113
121
  readonly files: ReadonlyArray<HarnessV1BootstrapFile>;
114
- /** Commands to run after files are written, in order. */
122
+ /** Commands to run from the bootstrap directory after files are written. */
115
123
  readonly commands: ReadonlyArray<HarnessV1BootstrapCommand>;
116
124
  }
117
125
 
@@ -2683,6 +2683,7 @@ function toToolResultContinuation(input) {
2683
2683
  }
2684
2684
 
2685
2685
  // src/agent/internal/bootstrap-recipe.ts
2686
+ import { posix } from "path";
2686
2687
  var BOOTSTRAP_SCHEMA_VERSION = 1;
2687
2688
  async function hashHarnessBootstrap(recipe) {
2688
2689
  const encoder = new TextEncoder();
@@ -2717,30 +2718,72 @@ async function hashHarnessBootstrap(recipe) {
2717
2718
  }
2718
2719
  return hex;
2719
2720
  }
2720
- function bootstrapMarkerPath(recipe, identity) {
2721
- return `${recipe.bootstrapDir}/.bootstrap-${identity}.ok`;
2721
+ function bootstrapMarkerPath({
2722
+ recipe,
2723
+ identity,
2724
+ defaultWorkingDirectory
2725
+ }) {
2726
+ return posix.join(
2727
+ resolveBootstrapPath({
2728
+ path: recipe.bootstrapDir,
2729
+ defaultWorkingDirectory
2730
+ }),
2731
+ `.bootstrap-${identity}.ok`
2732
+ );
2722
2733
  }
2723
- async function applyBootstrapRecipe(session, recipe, identity, options) {
2724
- const markerPath = bootstrapMarkerPath(recipe, identity);
2734
+ async function applyBootstrapRecipe({
2735
+ session,
2736
+ recipe,
2737
+ identity,
2738
+ defaultWorkingDirectory,
2739
+ abortSignal
2740
+ }) {
2741
+ const markerPath = bootstrapMarkerPath({
2742
+ recipe,
2743
+ identity,
2744
+ defaultWorkingDirectory
2745
+ });
2725
2746
  const existingMarker = await session.readTextFile({
2726
2747
  path: markerPath,
2727
- abortSignal: options == null ? void 0 : options.abortSignal
2748
+ abortSignal
2728
2749
  });
2729
2750
  if (existingMarker !== null) {
2730
2751
  return;
2731
2752
  }
2753
+ const bootstrapDir = resolveBootstrapPath({
2754
+ path: recipe.bootstrapDir,
2755
+ defaultWorkingDirectory
2756
+ });
2757
+ const mkdirResult = await session.run({
2758
+ command: 'mkdir -p "$BOOTSTRAP_DIR"',
2759
+ workingDirectory: defaultWorkingDirectory,
2760
+ env: { BOOTSTRAP_DIR: bootstrapDir },
2761
+ abortSignal
2762
+ });
2763
+ if (mkdirResult.exitCode !== 0) {
2764
+ throw new Error(
2765
+ `Failed to create bootstrap directory for harness '${recipe.harnessId}' (exit ${mkdirResult.exitCode}): ${bootstrapDir}
2766
+ ${mkdirResult.stderr || mkdirResult.stdout}`
2767
+ );
2768
+ }
2732
2769
  for (const file of recipe.files) {
2733
2770
  await session.writeTextFile({
2734
- path: file.path,
2771
+ path: resolveBootstrapPath({
2772
+ path: file.path,
2773
+ defaultWorkingDirectory
2774
+ }),
2735
2775
  content: file.content,
2736
- abortSignal: options == null ? void 0 : options.abortSignal
2776
+ abortSignal
2737
2777
  });
2738
2778
  }
2739
2779
  for (const cmd of recipe.commands) {
2740
2780
  const result = await session.run({
2741
2781
  command: cmd.command,
2742
- workingDirectory: cmd.workingDirectory,
2743
- abortSignal: options == null ? void 0 : options.abortSignal
2782
+ workingDirectory: cmd.workingDirectory == null ? bootstrapDir : resolveBootstrapPath({
2783
+ path: cmd.workingDirectory,
2784
+ defaultWorkingDirectory
2785
+ }),
2786
+ abortSignal
2744
2787
  });
2745
2788
  if (result.exitCode !== 0) {
2746
2789
  throw new Error(
@@ -2752,12 +2795,18 @@ ${result.stderr || result.stdout}`
2752
2795
  await session.writeTextFile({
2753
2796
  path: markerPath,
2754
2797
  content: "",
2755
- abortSignal: options == null ? void 0 : options.abortSignal
2798
+ abortSignal
2756
2799
  });
2757
2800
  }
2801
+ function resolveBootstrapPath({
2802
+ path,
2803
+ defaultWorkingDirectory
2804
+ }) {
2805
+ return posix.isAbsolute(path) ? path : posix.resolve(defaultWorkingDirectory, path);
2806
+ }
2758
2807
 
2759
2808
  // src/agent/internal/sandbox-bootstrap.ts
2760
- import { posix } from "path";
2809
+ import { posix as posix2 } from "path";
2761
2810
  var SANDBOX_BOOTSTRAP_IDENTITY_VERSION = 1;
2762
2811
  function validateSandboxBootstrapSettings(settings) {
2763
2812
  if (settings.onBootstrap == null !== (settings.bootstrapHash == null)) {
@@ -2783,10 +2832,10 @@ function normalizeSandboxWorkDir(workDir) {
2783
2832
  "HarnessAgent: `sandboxConfig.workDir` must use POSIX path separators."
2784
2833
  );
2785
2834
  }
2786
- if (posix.isAbsolute(workDir)) {
2835
+ if (posix2.isAbsolute(workDir)) {
2787
2836
  throw new Error("HarnessAgent: `sandboxConfig.workDir` must be relative.");
2788
2837
  }
2789
- const normalized = posix.normalize(workDir);
2838
+ const normalized = posix2.normalize(workDir);
2790
2839
  if (normalized === "." || normalized === ".." || normalized.startsWith("../")) {
2791
2840
  throw new Error(
2792
2841
  "HarnessAgent: `sandboxConfig.workDir` must stay inside the sandbox default working directory."
@@ -2841,20 +2890,26 @@ async function runSandboxBootstrap({
2841
2890
  recipeIdentity,
2842
2891
  workDir,
2843
2892
  onBootstrap,
2893
+ defaultWorkingDirectory,
2844
2894
  abortSignal
2845
2895
  }) {
2896
+ if (recipe == null && onBootstrap == null) return;
2897
+ const resolvedDefaultWorkingDirectory = defaultWorkingDirectory != null ? defaultWorkingDirectory : await resolveDefaultWorkingDirectory({
2898
+ session,
2899
+ abortSignal
2900
+ });
2846
2901
  if (recipe != null && recipeIdentity != null) {
2847
- await applyBootstrapRecipe(session, recipe, recipeIdentity, {
2902
+ await applyBootstrapRecipe({
2903
+ session,
2904
+ recipe,
2905
+ identity: recipeIdentity,
2906
+ defaultWorkingDirectory: resolvedDefaultWorkingDirectory,
2848
2907
  abortSignal
2849
2908
  });
2850
2909
  }
2851
2910
  if (onBootstrap == null) return;
2852
- const defaultWorkingDirectory = await resolveDefaultWorkingDirectory({
2853
- session,
2854
- abortSignal
2855
- });
2856
- const bootstrapWorkDir = workDir == null ? defaultWorkingDirectory : joinSandboxPath({
2857
- base: defaultWorkingDirectory,
2911
+ const bootstrapWorkDir = workDir == null ? resolvedDefaultWorkingDirectory : joinSandboxPath({
2912
+ base: resolvedDefaultWorkingDirectory,
2858
2913
  path: workDir
2859
2914
  });
2860
2915
  await ensureSandboxDirectory({
@@ -2878,7 +2933,7 @@ async function resolveDefaultWorkingDirectory({
2878
2933
  );
2879
2934
  }
2880
2935
  const cwd = result.stdout.trim();
2881
- if (!posix.isAbsolute(cwd)) {
2936
+ if (!posix2.isAbsolute(cwd)) {
2882
2937
  throw new Error(
2883
2938
  `Failed to resolve sandbox default working directory: expected an absolute path, got ${JSON.stringify(cwd)}.`
2884
2939
  );
@@ -2935,7 +2990,7 @@ function joinSandboxPath({
2935
2990
  base,
2936
2991
  path
2937
2992
  }) {
2938
- return posix.join(base, path);
2993
+ return posix2.join(base, path);
2939
2994
  }
2940
2995
 
2941
2996
  // src/agent/internal/resolve-observability.ts
@@ -3180,12 +3235,13 @@ var HarnessAgent = class {
3180
3235
  });
3181
3236
  try {
3182
3237
  if (!isResumedSession && sandboxBootstrapPlan.recipe != null && sandboxBootstrapPlan.recipeIdentity != null) {
3183
- await applyBootstrapRecipe(
3184
- sandboxSession.restricted(),
3185
- sandboxBootstrapPlan.recipe,
3186
- sandboxBootstrapPlan.recipeIdentity,
3187
- { abortSignal }
3188
- );
3238
+ await applyBootstrapRecipe({
3239
+ session: sandboxSession.restricted(),
3240
+ recipe: sandboxBootstrapPlan.recipe,
3241
+ identity: sandboxBootstrapPlan.recipeIdentity,
3242
+ defaultWorkingDirectory: sandboxSession.defaultWorkingDirectory,
3243
+ abortSignal
3244
+ });
3189
3245
  }
3190
3246
  await ensureSandboxDirectory({
3191
3247
  session: sandboxSession,
@@ -3573,14 +3629,13 @@ async function prepareHarnessSandboxTemplate(options) {
3573
3629
  });
3574
3630
  try {
3575
3631
  if (bootstrapPlan.recipe != null && bootstrapPlan.recipeIdentity != null) {
3576
- await applyBootstrapRecipe(
3577
- sandboxSession.restricted(),
3578
- bootstrapPlan.recipe,
3579
- bootstrapPlan.recipeIdentity,
3580
- {
3581
- abortSignal: options.abortSignal
3582
- }
3583
- );
3632
+ await applyBootstrapRecipe({
3633
+ session: sandboxSession.restricted(),
3634
+ recipe: bootstrapPlan.recipe,
3635
+ identity: bootstrapPlan.recipeIdentity,
3636
+ defaultWorkingDirectory: sandboxSession.defaultWorkingDirectory,
3637
+ abortSignal: options.abortSignal
3638
+ });
3584
3639
  }
3585
3640
  } finally {
3586
3641
  await Promise.resolve(sandboxSession.stop()).catch(() => {
@@ -3607,6 +3662,7 @@ async function prepareSandboxForHarness(options) {
3607
3662
  const workDir = sandboxConfig.workDir == null ? void 0 : normalizeSandboxWorkDir(sandboxConfig.workDir);
3608
3663
  const recipeIdentities = {};
3609
3664
  const skippedHarnessIds = [];
3665
+ let defaultWorkingDirectory;
3610
3666
  for (const harness of harnesses) {
3611
3667
  const recipe = await ((_b3 = harness.getBootstrap) == null ? void 0 : _b3.call(harness, {
3612
3668
  abortSignal: options.abortSignal
@@ -3617,7 +3673,15 @@ async function prepareSandboxForHarness(options) {
3617
3673
  }
3618
3674
  const recipeIdentity = await hashHarnessBootstrap(recipe);
3619
3675
  recipeIdentities[harness.harnessId] = recipeIdentity;
3620
- await applyBootstrapRecipe(options.session, recipe, recipeIdentity, {
3676
+ defaultWorkingDirectory != null ? defaultWorkingDirectory : defaultWorkingDirectory = await resolveDefaultWorkingDirectory({
3677
+ session: options.session,
3678
+ abortSignal: options.abortSignal
3679
+ });
3680
+ await applyBootstrapRecipe({
3681
+ session: options.session,
3682
+ recipe,
3683
+ identity: recipeIdentity,
3684
+ defaultWorkingDirectory,
3621
3685
  abortSignal: options.abortSignal
3622
3686
  });
3623
3687
  }
@@ -3626,6 +3690,7 @@ async function prepareSandboxForHarness(options) {
3626
3690
  session: options.session,
3627
3691
  workDir,
3628
3692
  onBootstrap: sandboxConfig.onBootstrap,
3693
+ defaultWorkingDirectory,
3629
3694
  abortSignal: options.abortSignal
3630
3695
  });
3631
3696
  }