@ai-sdk/harness 1.0.122 → 1.0.124

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,32 @@
1
1
  # @ai-sdk/harness
2
2
 
3
+ ## 1.0.124
4
+
5
+ ### Patch Changes
6
+
7
+ - 40231b6: fix(harness): enforce tool input schemas at execution and consistently reject invalid inputs
8
+ - Updated dependencies [b5679a7]
9
+ - Updated dependencies [ca31b89]
10
+ - ai@7.0.114
11
+
12
+ ## 1.0.123
13
+
14
+ ### Patch Changes
15
+
16
+ - 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.
17
+ - 771e74b: chore: enable dead code lint rules
18
+ - 7115a3f: fix(harness): preserve buffered SandboxChannel event order without blocking selective listeners and forward Claude Code compaction events
19
+ - Updated dependencies [dcdb011]
20
+ - Updated dependencies [8f72832]
21
+ - Updated dependencies [fe07867]
22
+ - Updated dependencies [b74c0cb]
23
+ - Updated dependencies [a4b0940]
24
+ - Updated dependencies [2693319]
25
+ - Updated dependencies [c93ee90]
26
+ - Updated dependencies [771e74b]
27
+ - ai@7.0.113
28
+ - @ai-sdk/provider-utils@5.0.47
29
+
3
30
  ## 1.0.122
4
31
 
5
32
  ### 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
  };
@@ -1086,6 +1103,45 @@ function stripWorkDir(part, sessionWorkDir) {
1086
1103
  return part;
1087
1104
  }
1088
1105
  }
1106
+ function stripParsedToolInputWorkDir({
1107
+ value,
1108
+ sessionWorkDir
1109
+ }) {
1110
+ if (sessionWorkDir.length === 0) return value;
1111
+ const projected = /* @__PURE__ */ new WeakMap();
1112
+ const pending = [];
1113
+ const project = (item) => {
1114
+ if (typeof item === "string") return stripString(item, sessionWorkDir);
1115
+ if (item === null || typeof item !== "object") return item;
1116
+ if (!Array.isArray(item) && Object.getPrototypeOf(item) !== Object.prototype && Object.getPrototypeOf(item) !== null) {
1117
+ return item;
1118
+ }
1119
+ if (projected.has(item)) return projected.get(item);
1120
+ const target = Array.isArray(item) ? new Array(item.length) : {};
1121
+ projected.set(item, target);
1122
+ pending.push({ source: item, target });
1123
+ return target;
1124
+ };
1125
+ const result = project(value);
1126
+ while (pending.length > 0) {
1127
+ const { source, target } = pending.pop();
1128
+ if (Array.isArray(source) && Array.isArray(target)) {
1129
+ for (let index = 0; index < source.length; index++) {
1130
+ if (index in source) target[index] = project(source[index]);
1131
+ }
1132
+ } else {
1133
+ for (const [key, item] of Object.entries(source)) {
1134
+ Object.defineProperty(target, key, {
1135
+ value: project(item),
1136
+ enumerable: true,
1137
+ writable: true,
1138
+ configurable: true
1139
+ });
1140
+ }
1141
+ }
1142
+ }
1143
+ return result;
1144
+ }
1089
1145
  function stripString(value, workDir) {
1090
1146
  return stripStreamingString({
1091
1147
  value,
@@ -1514,6 +1570,7 @@ function pinSandboxChannelEventCheckpoint(event) {
1514
1570
  }
1515
1571
 
1516
1572
  // src/agent/internal/run-prompt.ts
1573
+ var invalidToolInputMessage = "Tool input validation failed.";
1517
1574
  function unwrapToolResultOutput(toolResult) {
1518
1575
  switch (toolResult.output.type) {
1519
1576
  case "text":
@@ -1928,7 +1985,7 @@ function runPrompt(input) {
1928
1985
  };
1929
1986
  const processPendingApprovalContinuation = async (approval, continuation) => {
1930
1987
  var _a6;
1931
- const rawToolCall = (_a6 = rawToolCallsByToolCallId.get(approval.toolCallId)) != null ? _a6 : {
1988
+ const rawToolCall = (_a6 = approval.kind === "builtin" ? rawToolCallsByToolCallId.get(approval.toolCallId) : void 0) != null ? _a6 : {
1932
1989
  type: "tool-call",
1933
1990
  toolCallId: approval.toolCallId,
1934
1991
  toolName: approval.toolName,
@@ -1936,14 +1993,29 @@ function runPrompt(input) {
1936
1993
  providerExecuted: approval.providerExecuted,
1937
1994
  nativeName: approval.nativeName
1938
1995
  };
1939
- const parsedInput = await safeParseJSON2({ text: rawToolCall.input });
1940
- const toolCall = {
1941
- type: "tool-call",
1942
- toolCallId: rawToolCall.toolCallId,
1943
- toolName: rawToolCall.toolName,
1944
- input: parsedInput.success ? parsedInput.value : rawToolCall.input,
1945
- ...rawToolCall.providerExecuted !== void 0 ? { providerExecuted: rawToolCall.providerExecuted } : {}
1946
- };
1996
+ let validatedToolCall;
1997
+ let toolCall;
1998
+ if (approval.kind === "custom" && continuation.approved) {
1999
+ validatedToolCall = asToolCallTextStreamPart({
2000
+ part: await validateToolCall({
2001
+ event: rawToolCall,
2002
+ tools: input.tools
2003
+ })
2004
+ });
2005
+ toolCall = displayHostToolCall({
2006
+ toolCall: validatedToolCall,
2007
+ sessionWorkDir: input.sessionWorkDir
2008
+ });
2009
+ } else {
2010
+ const parsedInput = await safeParseJSON2({ text: rawToolCall.input });
2011
+ toolCall = {
2012
+ type: "tool-call",
2013
+ toolCallId: rawToolCall.toolCallId,
2014
+ toolName: rawToolCall.toolName,
2015
+ input: parsedInput.success ? parsedInput.value : rawToolCall.input,
2016
+ ...rawToolCall.providerExecuted !== void 0 ? { providerExecuted: rawToolCall.providerExecuted } : {}
2017
+ };
2018
+ }
1947
2019
  enqueueApprovalResponse(approval, continuation, toolCall);
1948
2020
  onToolApprovalSettled(approval.approvalId);
1949
2021
  pendingApprovalsByApprovalId.delete(approval.approvalId);
@@ -1973,6 +2045,27 @@ function runPrompt(input) {
1973
2045
  });
1974
2046
  return "continued";
1975
2047
  }
2048
+ if (validatedToolCall == null) {
2049
+ throw new Error(
2050
+ `Harness '${input.harness.harnessId}' could not validate approved host tool '${approval.toolName}'.`
2051
+ );
2052
+ }
2053
+ if (validatedToolCall.invalid) {
2054
+ const error = new Error(invalidToolInputMessage);
2055
+ await submitToolResult({
2056
+ toolCallId: approval.toolCallId,
2057
+ output: { error: invalidToolInputMessage },
2058
+ isError: true
2059
+ });
2060
+ bufferedToolOutcomes.push(() => {
2061
+ enqueueHostToolOutcome({
2062
+ toolCall,
2063
+ outcome: { ok: false, error }
2064
+ });
2065
+ });
2066
+ await publishToolExecutions();
2067
+ return "continued";
2068
+ }
1976
2069
  await lifecycle.start(input.model);
1977
2070
  toolExecutions.set(rawToolCall.toolCallId, {
1978
2071
  toolCall
@@ -1980,6 +2073,7 @@ function runPrompt(input) {
1980
2073
  const executionStartedAt = Date.now();
1981
2074
  const execution = await maybeExecuteHostTool({
1982
2075
  event: rawToolCall,
2076
+ parsedToolCall: validatedToolCall,
1983
2077
  tools: activeTools,
1984
2078
  toolsContext,
1985
2079
  wrappedExecuteTool: lifecycle.executeTool,
@@ -2174,15 +2268,25 @@ function runPrompt(input) {
2174
2268
  } else {
2175
2269
  for (const part of translatedParts) result.enqueue(part);
2176
2270
  }
2271
+ let validatedHostToolCall;
2177
2272
  if (displayValue.type === "tool-call") {
2273
+ const isHostTool = value.type === "tool-call" && !value.providerExecuted && hasTool({ tools: activeTools, toolName: value.toolName }) && !(value.toolName === "askUserQuestions" && hasTool({
2274
+ tools: input.harness.builtinTools,
2275
+ toolName: value.toolName
2276
+ }));
2178
2277
  const parsed = await validateToolCall({
2179
- event: displayValue,
2278
+ event: isHostTool ? value : displayValue,
2180
2279
  tools: input.tools
2181
2280
  });
2182
2281
  const parsedToolCall = asToolCallTextStreamPart({ part: parsed });
2282
+ validatedHostToolCall = isHostTool ? parsedToolCall : void 0;
2283
+ const displayToolCall = isHostTool ? displayHostToolCall({
2284
+ toolCall: parsedToolCall,
2285
+ sessionWorkDir: input.sessionWorkDir
2286
+ }) : parsedToolCall;
2183
2287
  rawToolCallsByToolCallId.set(displayValue.toolCallId, displayValue);
2184
- toolCallsByToolCallId.set(displayValue.toolCallId, parsedToolCall);
2185
- result.enqueue(parsed);
2288
+ toolCallsByToolCallId.set(displayValue.toolCallId, displayToolCall);
2289
+ result.enqueue(displayToolCall);
2186
2290
  }
2187
2291
  if (value.type === "text-delta") {
2188
2292
  stepText += value.delta;
@@ -2328,6 +2432,31 @@ function runPrompt(input) {
2328
2432
  await finishForHostInputPause({ completeCurrentStep: true });
2329
2433
  return;
2330
2434
  }
2435
+ if (validatedHostToolCall == null) {
2436
+ throw new Error(
2437
+ `Harness '${input.harness.harnessId}' could not validate host tool '${toolCall.toolName}'.`
2438
+ );
2439
+ }
2440
+ if (validatedHostToolCall.invalid) {
2441
+ settledHostToolCallIds.add(toolCall.toolCallId);
2442
+ toolExecutions.delete(toolCall.toolCallId);
2443
+ bufferedToolOutcomes.push(() => {
2444
+ result.enqueue({
2445
+ type: "tool-error",
2446
+ toolCallId: toolCall.toolCallId,
2447
+ toolName: toolCall.toolName,
2448
+ input: parsedToolCall.input,
2449
+ error: new Error(invalidToolInputMessage),
2450
+ dynamic: true
2451
+ });
2452
+ });
2453
+ await submitToolResult({
2454
+ toolCallId: toolCall.toolCallId,
2455
+ output: { error: invalidToolInputMessage },
2456
+ isError: true
2457
+ });
2458
+ continue;
2459
+ }
2331
2460
  const customToolApprovalDecision = resolveCustomToolApproval({
2332
2461
  toolName: toolCall.toolName,
2333
2462
  toolApproval: input.toolApproval
@@ -2427,6 +2556,7 @@ function runPrompt(input) {
2427
2556
  const executionStartedAt = Date.now();
2428
2557
  const execution = await maybeExecuteHostTool({
2429
2558
  event: toolCall,
2559
+ parsedToolCall: validatedHostToolCall,
2430
2560
  tools: activeTools,
2431
2561
  toolsContext,
2432
2562
  wrappedExecuteTool: lifecycle.executeTool,
@@ -2532,14 +2662,31 @@ function asToolCallTextStreamPart(input) {
2532
2662
  }
2533
2663
  return input.part;
2534
2664
  }
2665
+ function displayHostToolCall(input) {
2666
+ return {
2667
+ ...input.toolCall,
2668
+ input: stripParsedToolInputWorkDir({
2669
+ value: input.toolCall.input,
2670
+ sessionWorkDir: input.sessionWorkDir
2671
+ }),
2672
+ ...input.toolCall.invalid ? { error: new Error(invalidToolInputMessage) } : {}
2673
+ };
2674
+ }
2535
2675
  function hasTool(input) {
2536
2676
  return Object.prototype.hasOwnProperty.call(input.tools, input.toolName);
2537
2677
  }
2538
2678
  async function maybeExecuteHostTool(input) {
2539
2679
  const tool = input.tools[input.event.toolName];
2540
2680
  if (!isExecutableTool(tool)) return { executed: false };
2541
- const parsed = await safeParseJSON2({ text: input.event.input });
2542
- const args = parsed.success ? parsed.value : input.event.input;
2681
+ if (input.parsedToolCall.invalid) {
2682
+ const error = new Error(invalidToolInputMessage);
2683
+ await input.submitToolResult({
2684
+ toolCallId: input.event.toolCallId,
2685
+ output: { error: invalidToolInputMessage },
2686
+ isError: true
2687
+ });
2688
+ return { executed: true, outcome: { ok: false, error } };
2689
+ }
2543
2690
  let context;
2544
2691
  try {
2545
2692
  context = await validateToolContext({
@@ -2562,7 +2709,7 @@ async function maybeExecuteHostTool(input) {
2562
2709
  let output2;
2563
2710
  const stream = executeTool({
2564
2711
  tool,
2565
- input: args,
2712
+ input: input.parsedToolCall.input,
2566
2713
  options: {
2567
2714
  toolCallId: input.event.toolCallId,
2568
2715
  messages: [],
@@ -3251,7 +3398,7 @@ function collectHarnessAgentToolResultContinuations(input) {
3251
3398
  }
3252
3399
 
3253
3400
  // src/agent/internal/bootstrap-recipe.ts
3254
- import { posix } from "path";
3401
+ import { posix as posix3 } from "path";
3255
3402
  var BOOTSTRAP_SCHEMA_VERSION = 1;
3256
3403
  async function hashHarnessBootstrap(recipe) {
3257
3404
  const encoder = new TextEncoder();
@@ -3260,6 +3407,7 @@ async function hashHarnessBootstrap(recipe) {
3260
3407
  chunks.push(encoder.encode(value));
3261
3408
  chunks.push(encoder.encode("\0"));
3262
3409
  };
3410
+ pushString(harnessStateDirectoryPath({ sandboxHomeDir: "$HOME" }));
3263
3411
  pushString(recipe.harnessId);
3264
3412
  pushString(recipe.bootstrapDir);
3265
3413
  const sortedFiles = [...recipe.files].sort(
@@ -3289,12 +3437,12 @@ async function hashHarnessBootstrap(recipe) {
3289
3437
  function bootstrapMarkerPath({
3290
3438
  recipe,
3291
3439
  identity,
3292
- defaultWorkingDirectory
3440
+ stateDirectory
3293
3441
  }) {
3294
- return posix.join(
3442
+ return posix3.join(
3295
3443
  resolveBootstrapPath({
3296
3444
  path: recipe.bootstrapDir,
3297
- defaultWorkingDirectory
3445
+ stateDirectory
3298
3446
  }),
3299
3447
  `.bootstrap-${identity}.ok`
3300
3448
  );
@@ -3303,13 +3451,13 @@ async function applyBootstrapRecipe({
3303
3451
  session,
3304
3452
  recipe,
3305
3453
  identity,
3306
- defaultWorkingDirectory,
3454
+ stateDirectory,
3307
3455
  abortSignal
3308
3456
  }) {
3309
3457
  const markerPath = bootstrapMarkerPath({
3310
3458
  recipe,
3311
3459
  identity,
3312
- defaultWorkingDirectory
3460
+ stateDirectory
3313
3461
  });
3314
3462
  const existingMarker = await session.readTextFile({
3315
3463
  path: markerPath,
@@ -3320,11 +3468,10 @@ async function applyBootstrapRecipe({
3320
3468
  }
3321
3469
  const bootstrapDir = resolveBootstrapPath({
3322
3470
  path: recipe.bootstrapDir,
3323
- defaultWorkingDirectory
3471
+ stateDirectory
3324
3472
  });
3325
3473
  const mkdirResult = await session.run({
3326
3474
  command: 'mkdir -p "$BOOTSTRAP_DIR"',
3327
- workingDirectory: defaultWorkingDirectory,
3328
3475
  env: { BOOTSTRAP_DIR: bootstrapDir },
3329
3476
  abortSignal
3330
3477
  });
@@ -3338,7 +3485,7 @@ ${mkdirResult.stderr || mkdirResult.stdout}`
3338
3485
  await session.writeTextFile({
3339
3486
  path: resolveBootstrapPath({
3340
3487
  path: file.path,
3341
- defaultWorkingDirectory
3488
+ stateDirectory
3342
3489
  }),
3343
3490
  content: file.content,
3344
3491
  abortSignal
@@ -3364,17 +3511,17 @@ ${result.stderr || result.stdout}`
3364
3511
  });
3365
3512
  }
3366
3513
  function resolveBootstrapPath({
3367
- path,
3368
- defaultWorkingDirectory
3514
+ path: path2,
3515
+ stateDirectory
3369
3516
  }) {
3370
- return posix.isAbsolute(path) ? path : posix.resolve(defaultWorkingDirectory, path);
3517
+ return posix3.isAbsolute(path2) ? path2 : posix3.resolve(stateDirectory, path2);
3371
3518
  }
3372
3519
 
3373
3520
  // src/agent/internal/sandbox-bootstrap.ts
3374
- import { posix as posix3 } from "path";
3521
+ import { posix as posix5 } from "path";
3375
3522
 
3376
3523
  // src/utils/resolve-sandbox-default-working-directory.ts
3377
- import { posix as posix2 } from "path";
3524
+ import { posix as posix4 } from "path";
3378
3525
  async function resolveSandboxDefaultWorkingDirectory({
3379
3526
  sandboxSession,
3380
3527
  abortSignal
@@ -3392,7 +3539,7 @@ async function resolveSandboxDefaultWorkingDirectory({
3392
3539
  );
3393
3540
  }
3394
3541
  const cwd = result.stdout.trim();
3395
- if (!posix2.isAbsolute(cwd)) {
3542
+ if (!posix4.isAbsolute(cwd)) {
3396
3543
  throw new Error(
3397
3544
  `Failed to resolve sandbox default working directory: expected an absolute path, got ${JSON.stringify(cwd)}.`
3398
3545
  );
@@ -3400,6 +3547,25 @@ async function resolveSandboxDefaultWorkingDirectory({
3400
3547
  return cwd === "/" ? cwd : cwd.replace(/\/+$/, "");
3401
3548
  }
3402
3549
 
3550
+ // src/utils/sandbox-home-dir.ts
3551
+ import path from "path";
3552
+ async function resolveSandboxHomeDir({
3553
+ sandbox,
3554
+ abortSignal
3555
+ }) {
3556
+ const result = await sandbox.run({
3557
+ command: 'printf "%s" "$HOME"',
3558
+ abortSignal
3559
+ });
3560
+ const homeDir = result.stdout.trim();
3561
+ if (result.exitCode !== 0 || !homeDir || !path.posix.isAbsolute(homeDir)) {
3562
+ throw new Error(
3563
+ `Unable to resolve sandbox HOME directory: ${result.stderr || result.stdout}`
3564
+ );
3565
+ }
3566
+ return homeDir;
3567
+ }
3568
+
3403
3569
  // src/agent/internal/sandbox-bootstrap.ts
3404
3570
  var SANDBOX_BOOTSTRAP_IDENTITY_VERSION = 1;
3405
3571
  function validateSandboxBootstrapSettings(settings) {
@@ -3426,10 +3592,10 @@ function normalizeSandboxWorkDir(workDir) {
3426
3592
  "HarnessAgent: `sandboxConfig.workDir` must use POSIX path separators."
3427
3593
  );
3428
3594
  }
3429
- if (posix3.isAbsolute(workDir)) {
3595
+ if (posix5.isAbsolute(workDir)) {
3430
3596
  throw new Error("HarnessAgent: `sandboxConfig.workDir` must be relative.");
3431
3597
  }
3432
- const normalized = posix3.normalize(workDir);
3598
+ const normalized = posix5.normalize(workDir);
3433
3599
  if (normalized === "." || normalized === ".." || normalized.startsWith("../")) {
3434
3600
  throw new Error(
3435
3601
  "HarnessAgent: `sandboxConfig.workDir` must stay inside the sandbox default working directory."
@@ -3445,7 +3611,7 @@ function resolveSessionWorkDir({
3445
3611
  }) {
3446
3612
  return joinSandboxPath({
3447
3613
  base: defaultWorkingDirectory,
3448
- path: workDir != null ? workDir : `${harnessId}-${sessionId}`
3614
+ path: workDir != null ? workDir : `${encodeHarnessPathSegment(harnessId)}-${encodeHarnessPathSegment(sessionId)}`
3449
3615
  });
3450
3616
  }
3451
3617
  async function createSandboxBootstrapPlan({
@@ -3488,20 +3654,30 @@ async function runSandboxBootstrap({
3488
3654
  abortSignal
3489
3655
  }) {
3490
3656
  if (recipe == null && onBootstrap == null) return;
3491
- const resolvedDefaultWorkingDirectory = defaultWorkingDirectory != null ? defaultWorkingDirectory : await resolveSandboxDefaultWorkingDirectory({
3492
- sandboxSession: session,
3493
- abortSignal
3494
- });
3495
3657
  if (recipe != null && recipeIdentity != null) {
3496
3658
  await applyBootstrapRecipe({
3497
3659
  session,
3498
3660
  recipe,
3499
3661
  identity: recipeIdentity,
3500
- defaultWorkingDirectory: resolvedDefaultWorkingDirectory,
3662
+ // Harness infrastructure always lives under the sandbox's own HOME,
3663
+ // never the working directory resolved below for the caller's own
3664
+ // `onBootstrap` hook. Resolved directly from `session` — this runs
3665
+ // from a provider's `onFirstCreate`, before a
3666
+ // `HarnessV1NetworkSandboxSession` even exists.
3667
+ stateDirectory: harnessStateDirectoryPath({
3668
+ sandboxHomeDir: await resolveSandboxHomeDir({
3669
+ sandbox: session,
3670
+ abortSignal
3671
+ })
3672
+ }),
3501
3673
  abortSignal
3502
3674
  });
3503
3675
  }
3504
3676
  if (onBootstrap == null) return;
3677
+ const resolvedDefaultWorkingDirectory = defaultWorkingDirectory != null ? defaultWorkingDirectory : await resolveSandboxDefaultWorkingDirectory({
3678
+ sandboxSession: session,
3679
+ abortSignal
3680
+ });
3505
3681
  const bootstrapWorkDir = workDir == null ? resolvedDefaultWorkingDirectory : joinSandboxPath({
3506
3682
  base: resolvedDefaultWorkingDirectory,
3507
3683
  path: workDir
@@ -3561,9 +3737,9 @@ async function hashSandboxBootstrapIdentity({
3561
3737
  }
3562
3738
  function joinSandboxPath({
3563
3739
  base,
3564
- path
3740
+ path: path2
3565
3741
  }) {
3566
- return posix3.join(base, path);
3742
+ return posix5.join(base, path2);
3567
3743
  }
3568
3744
 
3569
3745
  // src/agent/internal/resolve-observability.ts
@@ -3829,7 +4005,14 @@ var HarnessAgent = class {
3829
4005
  session: toolSafeSandboxSession,
3830
4006
  recipe,
3831
4007
  identity: recipeIdentity,
3832
- defaultWorkingDirectory,
4008
+ // Harness infrastructure always lives under the sandbox's own
4009
+ // HOME, never the session's working directory.
4010
+ stateDirectory: harnessStateDirectoryPath({
4011
+ sandboxHomeDir: await resolveSandboxHomeDir({
4012
+ sandbox: toolSafeSandboxSession,
4013
+ abortSignal
4014
+ })
4015
+ }),
3833
4016
  abortSignal
3834
4017
  });
3835
4018
  } catch (err) {
@@ -3872,7 +4055,14 @@ var HarnessAgent = class {
3872
4055
  session: resumedSandboxSession.restricted(),
3873
4056
  recipe,
3874
4057
  identity: recipeIdentity,
3875
- defaultWorkingDirectory: resumedSandboxSession.defaultWorkingDirectory,
4058
+ // Harness infrastructure always lives under the sandbox's own
4059
+ // HOME, never the working directory.
4060
+ stateDirectory: harnessStateDirectoryPath({
4061
+ sandboxHomeDir: await resolveSandboxHomeDir({
4062
+ sandbox: resumedSandboxSession,
4063
+ abortSignal
4064
+ })
4065
+ }),
3876
4066
  abortSignal
3877
4067
  });
3878
4068
  } catch (err) {
@@ -3907,7 +4097,14 @@ var HarnessAgent = class {
3907
4097
  session: createdSandboxSession.restricted(),
3908
4098
  recipe: sandboxBootstrapPlan.recipe,
3909
4099
  identity: sandboxBootstrapPlan.recipeIdentity,
3910
- defaultWorkingDirectory: createdSandboxSession.defaultWorkingDirectory,
4100
+ // Harness infrastructure always lives under the sandbox's own
4101
+ // HOME, never the working directory.
4102
+ stateDirectory: harnessStateDirectoryPath({
4103
+ sandboxHomeDir: await resolveSandboxHomeDir({
4104
+ sandbox: createdSandboxSession,
4105
+ abortSignal
4106
+ })
4107
+ }),
3911
4108
  abortSignal
3912
4109
  });
3913
4110
  } catch (err) {
@@ -4147,7 +4344,6 @@ var HarnessAgent = class {
4147
4344
  };
4148
4345
  }
4149
4346
  }
4150
- return void 0;
4151
4347
  }
4152
4348
  /*
4153
4349
  * Reduce AI SDK input to the single user message the harness should run
@@ -4451,11 +4647,19 @@ async function prepareHarnessSandboxTemplate(options) {
4451
4647
  });
4452
4648
  try {
4453
4649
  if (bootstrapPlan.recipe != null && bootstrapPlan.recipeIdentity != null) {
4650
+ const restrictedSession = sandboxSession.restricted();
4454
4651
  await applyBootstrapRecipe({
4455
- session: sandboxSession.restricted(),
4652
+ session: restrictedSession,
4456
4653
  recipe: bootstrapPlan.recipe,
4457
4654
  identity: bootstrapPlan.recipeIdentity,
4458
- defaultWorkingDirectory: sandboxSession.defaultWorkingDirectory,
4655
+ // Harness infrastructure always lives under the sandbox's own HOME,
4656
+ // never the working directory.
4657
+ stateDirectory: harnessStateDirectoryPath({
4658
+ sandboxHomeDir: await resolveSandboxHomeDir({
4659
+ sandbox: restrictedSession,
4660
+ abortSignal
4661
+ })
4662
+ }),
4459
4663
  abortSignal
4460
4664
  });
4461
4665
  }
@@ -4485,7 +4689,7 @@ async function prepareSandboxForHarness(options) {
4485
4689
  const workDir = sandboxConfig.workDir == null ? void 0 : normalizeSandboxWorkDir(sandboxConfig.workDir);
4486
4690
  const recipeIdentities = {};
4487
4691
  const skippedHarnessIds = [];
4488
- let defaultWorkingDirectory;
4692
+ let stateDirectory;
4489
4693
  for (const harness of harnesses) {
4490
4694
  const recipe = await ((_b4 = harness.getBootstrap) == null ? void 0 : _b4.call(harness, {
4491
4695
  abortSignal: options.abortSignal
@@ -4496,19 +4700,24 @@ async function prepareSandboxForHarness(options) {
4496
4700
  }
4497
4701
  const recipeIdentity = await hashHarnessBootstrap(recipe);
4498
4702
  recipeIdentities[harness.harnessId] = recipeIdentity;
4499
- defaultWorkingDirectory != null ? defaultWorkingDirectory : defaultWorkingDirectory = await resolveSandboxDefaultWorkingDirectory({
4500
- sandboxSession: options.session,
4501
- abortSignal: options.abortSignal
4703
+ stateDirectory != null ? stateDirectory : stateDirectory = harnessStateDirectoryPath({
4704
+ sandboxHomeDir: await resolveSandboxHomeDir({
4705
+ sandbox: options.session,
4706
+ abortSignal: options.abortSignal
4707
+ })
4502
4708
  });
4503
4709
  await applyBootstrapRecipe({
4504
4710
  session: options.session,
4505
4711
  recipe,
4506
4712
  identity: recipeIdentity,
4507
- defaultWorkingDirectory,
4713
+ stateDirectory,
4508
4714
  abortSignal: options.abortSignal
4509
4715
  });
4510
4716
  }
4511
4717
  if (sandboxConfig.onBootstrap != null) {
4718
+ const defaultWorkingDirectory = await resolveSandboxDefaultWorkingDirectory(
4719
+ { sandboxSession: options.session, abortSignal: options.abortSignal }
4720
+ );
4512
4721
  await runSandboxBootstrap({
4513
4722
  session: options.session,
4514
4723
  workDir,
@@ -4604,7 +4813,7 @@ import { appendFileSync, mkdirSync } from "fs";
4604
4813
  function createFileReporter(options) {
4605
4814
  var _a4, _b4;
4606
4815
  const fileName = (_a4 = options.fileName) != null ? _a4 : "events.jsonl";
4607
- const path = `${options.dir}/${fileName}`;
4816
+ const path2 = `${options.dir}/${fileName}`;
4608
4817
  const failOnly = (_b4 = options.failOnly) != null ? _b4 : false;
4609
4818
  const turns = /* @__PURE__ */ new Map();
4610
4819
  let lastOpenCallId;
@@ -4632,7 +4841,7 @@ function createFileReporter(options) {
4632
4841
  mkdirSync(options.dir, { recursive: true });
4633
4842
  dirReady = true;
4634
4843
  }
4635
- appendFileSync(path, lines.map((l) => JSON.stringify(l)).join("\n") + "\n");
4844
+ appendFileSync(path2, lines.map((l) => JSON.stringify(l)).join("\n") + "\n");
4636
4845
  } catch (e) {
4637
4846
  }
4638
4847
  };