@ai-sdk/harness 1.0.121 → 1.0.123

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,35 @@
1
1
  # @ai-sdk/harness
2
2
 
3
+ ## 1.0.123
4
+
5
+ ### Patch Changes
6
+
7
+ - 9c8c0c1: feat (harness): keep harness-generated state out of the sandbox workspace. The framework and every bridge adapter now resolve their generated state — bootstrap recipes and their dependencies (`.harness-bootstrap/…`) and per-session run state (`.agent-runs/…`) — through the new `harnessV1StateDirectory()` helper, which always resolves to a fixed directory (`~/.ai-sdk-harness`) under the sandbox's own HOME, never the sandbox's working directory. This is not configurable: every sandbox provider gets the same layout, so harness infrastructure never lands in a user-owned workspace. This changes the on-disk layout for every provider (a breaking change, acceptable given the experimental nature of this API) — sandboxes bootstrapped under the previous layout re-bootstrap once under the new path.
8
+ - 771e74b: chore: enable dead code lint rules
9
+ - 7115a3f: fix(harness): preserve buffered SandboxChannel event order without blocking selective listeners and forward Claude Code compaction events
10
+ - Updated dependencies [dcdb011]
11
+ - Updated dependencies [8f72832]
12
+ - Updated dependencies [fe07867]
13
+ - Updated dependencies [b74c0cb]
14
+ - Updated dependencies [a4b0940]
15
+ - Updated dependencies [2693319]
16
+ - Updated dependencies [c93ee90]
17
+ - Updated dependencies [771e74b]
18
+ - ai@7.0.113
19
+ - @ai-sdk/provider-utils@5.0.47
20
+
21
+ ## 1.0.122
22
+
23
+ ### Patch Changes
24
+
25
+ - Updated dependencies [9a98fd9]
26
+ - Updated dependencies [a0553d6]
27
+ - Updated dependencies [ffb0e76]
28
+ - Updated dependencies [fde0d66]
29
+ - ai@7.0.112
30
+ - @ai-sdk/provider@4.0.18
31
+ - @ai-sdk/provider-utils@5.0.46
32
+
3
33
  ## 1.0.121
4
34
 
5
35
  ### Patch Changes
@@ -8,7 +8,7 @@ import { JSONSchema7, JSONValue, LanguageModelV4StreamPart, LanguageModelV4ToolC
8
8
  /**
9
9
  * One file to write into the sandbox as part of an adapter's bootstrap recipe.
10
10
  * Absolute paths are used as-is. Relative paths are resolved against the
11
- * sandbox's default working directory. Paths should live under
11
+ * harness state directory (`$HOME/.ai-sdk-harness`). Paths should live under
12
12
  * {@link HarnessV1Bootstrap.bootstrapDir}.
13
13
  */
14
14
  interface HarnessV1BootstrapFile {
@@ -37,10 +37,10 @@ interface HarnessV1Bootstrap {
37
37
  readonly harnessId: string;
38
38
  /**
39
39
  * Path inside the sandbox where this recipe writes its state. Absolute paths
40
- * are used as-is. Relative paths are resolved against the sandbox's default
41
- * working directory. The marker file lives directly under it. Files
42
- * declared in {@link files} should also use this prefix so an adapter upgrade
43
- * can sweep stale state by clearing the directory.
40
+ * are used as-is. Relative paths are resolved against the harness state
41
+ * directory (`$HOME/.ai-sdk-harness`). The marker file lives directly under
42
+ * it. Files declared in {@link files} should also use this prefix so an
43
+ * adapter upgrade can sweep stale state by clearing the directory.
44
44
  */
45
45
  readonly bootstrapDir: string;
46
46
  /** Files to write into the sandbox before any command runs. */
@@ -38,6 +38,34 @@ var HarnessCapabilityUnsupportedError = class extends (_b2 = HarnessError, _a2 =
38
38
  }
39
39
  };
40
40
 
41
+ // src/v1/harness-v1-network-sandbox-session.ts
42
+ import { posix } from "path";
43
+ var HARNESS_V1_STATE_DIRECTORY_NAME = ".ai-sdk-harness";
44
+ function harnessStateDirectoryPath({
45
+ sandboxHomeDir
46
+ }) {
47
+ return posix.join(sandboxHomeDir, HARNESS_V1_STATE_DIRECTORY_NAME);
48
+ }
49
+
50
+ // src/v1/harness-session-data-directory-path.ts
51
+ import { posix as posix2 } from "path";
52
+ function encodeHarnessPathSegment(value) {
53
+ const encoded = encodeURIComponent(value);
54
+ if (encoded === "") return "%";
55
+ if (encoded === ".") return "%2E";
56
+ if (encoded === "..") return "%2E%2E";
57
+ return encoded;
58
+ }
59
+
60
+ // src/v1/harness-v1-tool-filtering.ts
61
+ function isHarnessV1BuiltinToolIncluded(input) {
62
+ if (input.toolFiltering == null) return true;
63
+ return input.toolFiltering.mode === "allow" ? input.toolFiltering.toolNames.includes(input.toolName) : !input.toolFiltering.toolNames.includes(input.toolName);
64
+ }
65
+ function getHarnessV1BuiltinToolFilteringDenialReason(input) {
66
+ return `Tool '${input.toolName}' is inactive due to the HarnessAgent tool filtering policy.`;
67
+ }
68
+
41
69
  // src/agent/harness-agent.ts
42
70
  import {
43
71
  asArray as asArray2,
@@ -115,15 +143,6 @@ async function validateLifecycleStateData(input) {
115
143
  };
116
144
  }
117
145
 
118
- // src/v1/harness-v1-tool-filtering.ts
119
- function isHarnessV1BuiltinToolIncluded(input) {
120
- if (input.toolFiltering == null) return true;
121
- return input.toolFiltering.mode === "allow" ? input.toolFiltering.toolNames.includes(input.toolName) : !input.toolFiltering.toolNames.includes(input.toolName);
122
- }
123
- function getHarnessV1BuiltinToolFilteringDenialReason(input) {
124
- return `Tool '${input.toolName}' is inactive due to the HarnessAgent tool filtering policy.`;
125
- }
126
-
127
146
  // src/agent/internal/to-harness-stream.ts
128
147
  async function toHarnessStream(options) {
129
148
  let controller;
@@ -764,9 +783,7 @@ var HarnessStreamTextResult = class {
764
783
  this.currentStepContent.push({
765
784
  ...part
766
785
  });
767
- return;
768
786
  default:
769
- return;
770
787
  }
771
788
  }
772
789
  };
@@ -3251,7 +3268,7 @@ function collectHarnessAgentToolResultContinuations(input) {
3251
3268
  }
3252
3269
 
3253
3270
  // src/agent/internal/bootstrap-recipe.ts
3254
- import { posix } from "path";
3271
+ import { posix as posix3 } from "path";
3255
3272
  var BOOTSTRAP_SCHEMA_VERSION = 1;
3256
3273
  async function hashHarnessBootstrap(recipe) {
3257
3274
  const encoder = new TextEncoder();
@@ -3260,6 +3277,7 @@ async function hashHarnessBootstrap(recipe) {
3260
3277
  chunks.push(encoder.encode(value));
3261
3278
  chunks.push(encoder.encode("\0"));
3262
3279
  };
3280
+ pushString(harnessStateDirectoryPath({ sandboxHomeDir: "$HOME" }));
3263
3281
  pushString(recipe.harnessId);
3264
3282
  pushString(recipe.bootstrapDir);
3265
3283
  const sortedFiles = [...recipe.files].sort(
@@ -3289,12 +3307,12 @@ async function hashHarnessBootstrap(recipe) {
3289
3307
  function bootstrapMarkerPath({
3290
3308
  recipe,
3291
3309
  identity,
3292
- defaultWorkingDirectory
3310
+ stateDirectory
3293
3311
  }) {
3294
- return posix.join(
3312
+ return posix3.join(
3295
3313
  resolveBootstrapPath({
3296
3314
  path: recipe.bootstrapDir,
3297
- defaultWorkingDirectory
3315
+ stateDirectory
3298
3316
  }),
3299
3317
  `.bootstrap-${identity}.ok`
3300
3318
  );
@@ -3303,13 +3321,13 @@ async function applyBootstrapRecipe({
3303
3321
  session,
3304
3322
  recipe,
3305
3323
  identity,
3306
- defaultWorkingDirectory,
3324
+ stateDirectory,
3307
3325
  abortSignal
3308
3326
  }) {
3309
3327
  const markerPath = bootstrapMarkerPath({
3310
3328
  recipe,
3311
3329
  identity,
3312
- defaultWorkingDirectory
3330
+ stateDirectory
3313
3331
  });
3314
3332
  const existingMarker = await session.readTextFile({
3315
3333
  path: markerPath,
@@ -3320,11 +3338,10 @@ async function applyBootstrapRecipe({
3320
3338
  }
3321
3339
  const bootstrapDir = resolveBootstrapPath({
3322
3340
  path: recipe.bootstrapDir,
3323
- defaultWorkingDirectory
3341
+ stateDirectory
3324
3342
  });
3325
3343
  const mkdirResult = await session.run({
3326
3344
  command: 'mkdir -p "$BOOTSTRAP_DIR"',
3327
- workingDirectory: defaultWorkingDirectory,
3328
3345
  env: { BOOTSTRAP_DIR: bootstrapDir },
3329
3346
  abortSignal
3330
3347
  });
@@ -3338,7 +3355,7 @@ ${mkdirResult.stderr || mkdirResult.stdout}`
3338
3355
  await session.writeTextFile({
3339
3356
  path: resolveBootstrapPath({
3340
3357
  path: file.path,
3341
- defaultWorkingDirectory
3358
+ stateDirectory
3342
3359
  }),
3343
3360
  content: file.content,
3344
3361
  abortSignal
@@ -3364,17 +3381,17 @@ ${result.stderr || result.stdout}`
3364
3381
  });
3365
3382
  }
3366
3383
  function resolveBootstrapPath({
3367
- path,
3368
- defaultWorkingDirectory
3384
+ path: path2,
3385
+ stateDirectory
3369
3386
  }) {
3370
- return posix.isAbsolute(path) ? path : posix.resolve(defaultWorkingDirectory, path);
3387
+ return posix3.isAbsolute(path2) ? path2 : posix3.resolve(stateDirectory, path2);
3371
3388
  }
3372
3389
 
3373
3390
  // src/agent/internal/sandbox-bootstrap.ts
3374
- import { posix as posix3 } from "path";
3391
+ import { posix as posix5 } from "path";
3375
3392
 
3376
3393
  // src/utils/resolve-sandbox-default-working-directory.ts
3377
- import { posix as posix2 } from "path";
3394
+ import { posix as posix4 } from "path";
3378
3395
  async function resolveSandboxDefaultWorkingDirectory({
3379
3396
  sandboxSession,
3380
3397
  abortSignal
@@ -3392,7 +3409,7 @@ async function resolveSandboxDefaultWorkingDirectory({
3392
3409
  );
3393
3410
  }
3394
3411
  const cwd = result.stdout.trim();
3395
- if (!posix2.isAbsolute(cwd)) {
3412
+ if (!posix4.isAbsolute(cwd)) {
3396
3413
  throw new Error(
3397
3414
  `Failed to resolve sandbox default working directory: expected an absolute path, got ${JSON.stringify(cwd)}.`
3398
3415
  );
@@ -3400,6 +3417,25 @@ async function resolveSandboxDefaultWorkingDirectory({
3400
3417
  return cwd === "/" ? cwd : cwd.replace(/\/+$/, "");
3401
3418
  }
3402
3419
 
3420
+ // src/utils/sandbox-home-dir.ts
3421
+ import path from "path";
3422
+ async function resolveSandboxHomeDir({
3423
+ sandbox,
3424
+ abortSignal
3425
+ }) {
3426
+ const result = await sandbox.run({
3427
+ command: 'printf "%s" "$HOME"',
3428
+ abortSignal
3429
+ });
3430
+ const homeDir = result.stdout.trim();
3431
+ if (result.exitCode !== 0 || !homeDir || !path.posix.isAbsolute(homeDir)) {
3432
+ throw new Error(
3433
+ `Unable to resolve sandbox HOME directory: ${result.stderr || result.stdout}`
3434
+ );
3435
+ }
3436
+ return homeDir;
3437
+ }
3438
+
3403
3439
  // src/agent/internal/sandbox-bootstrap.ts
3404
3440
  var SANDBOX_BOOTSTRAP_IDENTITY_VERSION = 1;
3405
3441
  function validateSandboxBootstrapSettings(settings) {
@@ -3426,10 +3462,10 @@ function normalizeSandboxWorkDir(workDir) {
3426
3462
  "HarnessAgent: `sandboxConfig.workDir` must use POSIX path separators."
3427
3463
  );
3428
3464
  }
3429
- if (posix3.isAbsolute(workDir)) {
3465
+ if (posix5.isAbsolute(workDir)) {
3430
3466
  throw new Error("HarnessAgent: `sandboxConfig.workDir` must be relative.");
3431
3467
  }
3432
- const normalized = posix3.normalize(workDir);
3468
+ const normalized = posix5.normalize(workDir);
3433
3469
  if (normalized === "." || normalized === ".." || normalized.startsWith("../")) {
3434
3470
  throw new Error(
3435
3471
  "HarnessAgent: `sandboxConfig.workDir` must stay inside the sandbox default working directory."
@@ -3445,7 +3481,7 @@ function resolveSessionWorkDir({
3445
3481
  }) {
3446
3482
  return joinSandboxPath({
3447
3483
  base: defaultWorkingDirectory,
3448
- path: workDir != null ? workDir : `${harnessId}-${sessionId}`
3484
+ path: workDir != null ? workDir : `${encodeHarnessPathSegment(harnessId)}-${encodeHarnessPathSegment(sessionId)}`
3449
3485
  });
3450
3486
  }
3451
3487
  async function createSandboxBootstrapPlan({
@@ -3488,20 +3524,30 @@ async function runSandboxBootstrap({
3488
3524
  abortSignal
3489
3525
  }) {
3490
3526
  if (recipe == null && onBootstrap == null) return;
3491
- const resolvedDefaultWorkingDirectory = defaultWorkingDirectory != null ? defaultWorkingDirectory : await resolveSandboxDefaultWorkingDirectory({
3492
- sandboxSession: session,
3493
- abortSignal
3494
- });
3495
3527
  if (recipe != null && recipeIdentity != null) {
3496
3528
  await applyBootstrapRecipe({
3497
3529
  session,
3498
3530
  recipe,
3499
3531
  identity: recipeIdentity,
3500
- defaultWorkingDirectory: resolvedDefaultWorkingDirectory,
3532
+ // Harness infrastructure always lives under the sandbox's own HOME,
3533
+ // never the working directory resolved below for the caller's own
3534
+ // `onBootstrap` hook. Resolved directly from `session` — this runs
3535
+ // from a provider's `onFirstCreate`, before a
3536
+ // `HarnessV1NetworkSandboxSession` even exists.
3537
+ stateDirectory: harnessStateDirectoryPath({
3538
+ sandboxHomeDir: await resolveSandboxHomeDir({
3539
+ sandbox: session,
3540
+ abortSignal
3541
+ })
3542
+ }),
3501
3543
  abortSignal
3502
3544
  });
3503
3545
  }
3504
3546
  if (onBootstrap == null) return;
3547
+ const resolvedDefaultWorkingDirectory = defaultWorkingDirectory != null ? defaultWorkingDirectory : await resolveSandboxDefaultWorkingDirectory({
3548
+ sandboxSession: session,
3549
+ abortSignal
3550
+ });
3505
3551
  const bootstrapWorkDir = workDir == null ? resolvedDefaultWorkingDirectory : joinSandboxPath({
3506
3552
  base: resolvedDefaultWorkingDirectory,
3507
3553
  path: workDir
@@ -3561,9 +3607,9 @@ async function hashSandboxBootstrapIdentity({
3561
3607
  }
3562
3608
  function joinSandboxPath({
3563
3609
  base,
3564
- path
3610
+ path: path2
3565
3611
  }) {
3566
- return posix3.join(base, path);
3612
+ return posix5.join(base, path2);
3567
3613
  }
3568
3614
 
3569
3615
  // src/agent/internal/resolve-observability.ts
@@ -3829,7 +3875,14 @@ var HarnessAgent = class {
3829
3875
  session: toolSafeSandboxSession,
3830
3876
  recipe,
3831
3877
  identity: recipeIdentity,
3832
- defaultWorkingDirectory,
3878
+ // Harness infrastructure always lives under the sandbox's own
3879
+ // HOME, never the session's working directory.
3880
+ stateDirectory: harnessStateDirectoryPath({
3881
+ sandboxHomeDir: await resolveSandboxHomeDir({
3882
+ sandbox: toolSafeSandboxSession,
3883
+ abortSignal
3884
+ })
3885
+ }),
3833
3886
  abortSignal
3834
3887
  });
3835
3888
  } catch (err) {
@@ -3872,7 +3925,14 @@ var HarnessAgent = class {
3872
3925
  session: resumedSandboxSession.restricted(),
3873
3926
  recipe,
3874
3927
  identity: recipeIdentity,
3875
- defaultWorkingDirectory: resumedSandboxSession.defaultWorkingDirectory,
3928
+ // Harness infrastructure always lives under the sandbox's own
3929
+ // HOME, never the working directory.
3930
+ stateDirectory: harnessStateDirectoryPath({
3931
+ sandboxHomeDir: await resolveSandboxHomeDir({
3932
+ sandbox: resumedSandboxSession,
3933
+ abortSignal
3934
+ })
3935
+ }),
3876
3936
  abortSignal
3877
3937
  });
3878
3938
  } catch (err) {
@@ -3907,7 +3967,14 @@ var HarnessAgent = class {
3907
3967
  session: createdSandboxSession.restricted(),
3908
3968
  recipe: sandboxBootstrapPlan.recipe,
3909
3969
  identity: sandboxBootstrapPlan.recipeIdentity,
3910
- defaultWorkingDirectory: createdSandboxSession.defaultWorkingDirectory,
3970
+ // Harness infrastructure always lives under the sandbox's own
3971
+ // HOME, never the working directory.
3972
+ stateDirectory: harnessStateDirectoryPath({
3973
+ sandboxHomeDir: await resolveSandboxHomeDir({
3974
+ sandbox: createdSandboxSession,
3975
+ abortSignal
3976
+ })
3977
+ }),
3911
3978
  abortSignal
3912
3979
  });
3913
3980
  } catch (err) {
@@ -4147,7 +4214,6 @@ var HarnessAgent = class {
4147
4214
  };
4148
4215
  }
4149
4216
  }
4150
- return void 0;
4151
4217
  }
4152
4218
  /*
4153
4219
  * Reduce AI SDK input to the single user message the harness should run
@@ -4451,11 +4517,19 @@ async function prepareHarnessSandboxTemplate(options) {
4451
4517
  });
4452
4518
  try {
4453
4519
  if (bootstrapPlan.recipe != null && bootstrapPlan.recipeIdentity != null) {
4520
+ const restrictedSession = sandboxSession.restricted();
4454
4521
  await applyBootstrapRecipe({
4455
- session: sandboxSession.restricted(),
4522
+ session: restrictedSession,
4456
4523
  recipe: bootstrapPlan.recipe,
4457
4524
  identity: bootstrapPlan.recipeIdentity,
4458
- defaultWorkingDirectory: sandboxSession.defaultWorkingDirectory,
4525
+ // Harness infrastructure always lives under the sandbox's own HOME,
4526
+ // never the working directory.
4527
+ stateDirectory: harnessStateDirectoryPath({
4528
+ sandboxHomeDir: await resolveSandboxHomeDir({
4529
+ sandbox: restrictedSession,
4530
+ abortSignal
4531
+ })
4532
+ }),
4459
4533
  abortSignal
4460
4534
  });
4461
4535
  }
@@ -4485,7 +4559,7 @@ async function prepareSandboxForHarness(options) {
4485
4559
  const workDir = sandboxConfig.workDir == null ? void 0 : normalizeSandboxWorkDir(sandboxConfig.workDir);
4486
4560
  const recipeIdentities = {};
4487
4561
  const skippedHarnessIds = [];
4488
- let defaultWorkingDirectory;
4562
+ let stateDirectory;
4489
4563
  for (const harness of harnesses) {
4490
4564
  const recipe = await ((_b4 = harness.getBootstrap) == null ? void 0 : _b4.call(harness, {
4491
4565
  abortSignal: options.abortSignal
@@ -4496,19 +4570,24 @@ async function prepareSandboxForHarness(options) {
4496
4570
  }
4497
4571
  const recipeIdentity = await hashHarnessBootstrap(recipe);
4498
4572
  recipeIdentities[harness.harnessId] = recipeIdentity;
4499
- defaultWorkingDirectory != null ? defaultWorkingDirectory : defaultWorkingDirectory = await resolveSandboxDefaultWorkingDirectory({
4500
- sandboxSession: options.session,
4501
- abortSignal: options.abortSignal
4573
+ stateDirectory != null ? stateDirectory : stateDirectory = harnessStateDirectoryPath({
4574
+ sandboxHomeDir: await resolveSandboxHomeDir({
4575
+ sandbox: options.session,
4576
+ abortSignal: options.abortSignal
4577
+ })
4502
4578
  });
4503
4579
  await applyBootstrapRecipe({
4504
4580
  session: options.session,
4505
4581
  recipe,
4506
4582
  identity: recipeIdentity,
4507
- defaultWorkingDirectory,
4583
+ stateDirectory,
4508
4584
  abortSignal: options.abortSignal
4509
4585
  });
4510
4586
  }
4511
4587
  if (sandboxConfig.onBootstrap != null) {
4588
+ const defaultWorkingDirectory = await resolveSandboxDefaultWorkingDirectory(
4589
+ { sandboxSession: options.session, abortSignal: options.abortSignal }
4590
+ );
4512
4591
  await runSandboxBootstrap({
4513
4592
  session: options.session,
4514
4593
  workDir,
@@ -4604,7 +4683,7 @@ import { appendFileSync, mkdirSync } from "fs";
4604
4683
  function createFileReporter(options) {
4605
4684
  var _a4, _b4;
4606
4685
  const fileName = (_a4 = options.fileName) != null ? _a4 : "events.jsonl";
4607
- const path = `${options.dir}/${fileName}`;
4686
+ const path2 = `${options.dir}/${fileName}`;
4608
4687
  const failOnly = (_b4 = options.failOnly) != null ? _b4 : false;
4609
4688
  const turns = /* @__PURE__ */ new Map();
4610
4689
  let lastOpenCallId;
@@ -4632,7 +4711,7 @@ function createFileReporter(options) {
4632
4711
  mkdirSync(options.dir, { recursive: true });
4633
4712
  dirReady = true;
4634
4713
  }
4635
- appendFileSync(path, lines.map((l) => JSON.stringify(l)).join("\n") + "\n");
4714
+ appendFileSync(path2, lines.map((l) => JSON.stringify(l)).join("\n") + "\n");
4636
4715
  } catch (e) {
4637
4716
  }
4638
4717
  };