@ai-sdk/harness 1.0.76 → 1.0.78

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.
@@ -1,4 +1,8 @@
1
- import type { Context, ToolSet } from '@ai-sdk/provider-utils';
1
+ import type {
2
+ Context,
3
+ Experimental_SandboxSession as SandboxSession,
4
+ ToolSet,
5
+ } from '@ai-sdk/provider-utils';
2
6
  import type {
3
7
  OutputInterface as Output,
4
8
  StopCondition,
@@ -9,8 +13,10 @@ import type { HarnessAgentToolApprovalConfiguration } from './harness-agent-sett
9
13
  import type {
10
14
  HarnessV1BuiltinToolFiltering,
11
15
  HarnessV1NetworkSandboxSession,
16
+ HarnessV1PromptControl,
12
17
  HarnessV1ResponseFormat,
13
18
  } from '../v1';
19
+ import { HarnessCapabilityUnsupportedError } from '../errors/harness-capability-unsupported-error';
14
20
  import type {
15
21
  HarnessAgentAdapter,
16
22
  HarnessAgentAdapterSession,
@@ -25,6 +31,7 @@ import type { HarnessAgentToolApprovalContinuation } from './harness-agent-tool-
25
31
  import type { HarnessAgentToolResultContinuation } from './harness-agent-tool-result-continuation';
26
32
  import { validateLifecycleStateData } from './internal/lifecycle-state-validation';
27
33
  import { runPrompt } from './internal/run-prompt';
34
+ import { getRestrictedSandboxSession } from '../utils/get-restricted-sandbox-session';
28
35
 
29
36
  type HarnessAgentTurnResult<
30
37
  TOOLS extends ToolSet,
@@ -44,11 +51,18 @@ type HarnessAgentTurnState =
44
51
  | 'awaiting-tool-result'
45
52
  | 'suspended';
46
53
 
54
+ type ActivePromptControl = {
55
+ readonly turnId: number;
56
+ readonly promise: Promise<HarnessV1PromptControl | undefined>;
57
+ resolve(control: HarnessV1PromptControl | undefined): void;
58
+ settled: boolean;
59
+ };
60
+
47
61
  /**
48
62
  * Live harness session held by the caller.
49
63
  *
50
64
  * Created by {@link import('./harness-agent').HarnessAgent.createSession}.
51
- * Owns the underlying adapter session and the network sandbox session.
65
+ * Owns the underlying adapter session and holds its sandbox session.
52
66
  *
53
67
  * Pass the instance back to `agent.generate` / `agent.stream` on every
54
68
  * call; end the local handle with `detach()`, `stop()`, or `destroy()`.
@@ -68,7 +82,10 @@ export class HarnessAgentSession {
68
82
  private readonly sessionWorkDir: string;
69
83
  private readonly ownsSandboxLifecycle: boolean;
70
84
  private underlyingSession: HarnessAgentAdapterSession | undefined;
71
- private sandboxSession: HarnessV1NetworkSandboxSession | undefined;
85
+ private sandboxSession:
86
+ | HarnessV1NetworkSandboxSession
87
+ | SandboxSession
88
+ | undefined;
72
89
  private readonly toolApproval:
73
90
  | HarnessAgentToolApprovalConfiguration
74
91
  | undefined;
@@ -84,6 +101,7 @@ export class HarnessAgentSession {
84
101
  private turnState: HarnessAgentTurnState;
85
102
  private turnSequence = 0;
86
103
  private activeTurnSequence = 0;
104
+ private activePromptControl: ActivePromptControl | undefined;
87
105
  private suspendedTurnState:
88
106
  | Promise<HarnessAgentContinueTurnState>
89
107
  | undefined;
@@ -98,7 +116,7 @@ export class HarnessAgentSession {
98
116
  sessionId: string;
99
117
  harness: HarnessAgentAdapter;
100
118
  underlyingSession: HarnessAgentAdapterSession;
101
- sandboxSession: HarnessV1NetworkSandboxSession;
119
+ sandboxSession: HarnessV1NetworkSandboxSession | SandboxSession;
102
120
  ownsSandboxLifecycle?: boolean;
103
121
  sessionWorkDir: string;
104
122
  toolApproval: HarnessAgentToolApprovalConfiguration | undefined;
@@ -130,11 +148,11 @@ export class HarnessAgentSession {
130
148
  }
131
149
 
132
150
  /**
133
- * Active network sandbox session.
151
+ * Active sandbox session.
134
152
  *
135
153
  * @internal — accessed by session turn and lifecycle drivers.
136
154
  */
137
- getSandboxSession(): HarnessV1NetworkSandboxSession {
155
+ getSandboxSession(): HarnessV1NetworkSandboxSession | SandboxSession {
138
156
  if (this.sessionState !== 'active' || this.sandboxSession == null) {
139
157
  throw new Error(
140
158
  `Harness session ${this.sessionId} has ended and cannot be reused.`,
@@ -189,7 +207,7 @@ export class HarnessAgentSession {
189
207
  activeTools: options.activeTools,
190
208
  toolSpecs: options.toolSpecs,
191
209
  builtinToolFiltering: options.builtinToolFiltering,
192
- sandboxSession: sandboxSession.restricted(),
210
+ sandboxSession: getRestrictedSandboxSession(sandboxSession),
193
211
  sessionWorkDir: this.sessionWorkDir,
194
212
  runtimeContext: options.runtimeContext,
195
213
  abortSignal: options.abortSignal,
@@ -220,6 +238,9 @@ export class HarnessAgentSession {
220
238
  onTurnFailed: () => {
221
239
  this.finishTrackedTurn({ turnId });
222
240
  },
241
+ onPromptControlAvailable: control => {
242
+ this.setPromptControl({ turnId, control });
243
+ },
223
244
  isTurnSuspending: () =>
224
245
  this.activeTurnSequence === turnId && this.suspendedTurnState != null,
225
246
  onStopConditionMet: () =>
@@ -269,7 +290,7 @@ export class HarnessAgentSession {
269
290
  activeTools: options.activeTools,
270
291
  toolSpecs: options.toolSpecs,
271
292
  builtinToolFiltering: options.builtinToolFiltering,
272
- sandboxSession: sandboxSession.restricted(),
293
+ sandboxSession: getRestrictedSandboxSession(sandboxSession),
273
294
  sessionWorkDir: this.sessionWorkDir,
274
295
  runtimeContext: options.runtimeContext,
275
296
  abortSignal: options.abortSignal,
@@ -302,6 +323,9 @@ export class HarnessAgentSession {
302
323
  onTurnFailed: () => {
303
324
  this.finishTrackedTurn({ turnId });
304
325
  },
326
+ onPromptControlAvailable: control => {
327
+ this.setPromptControl({ turnId, control });
328
+ },
305
329
  isTurnSuspending: () =>
306
330
  this.activeTurnSequence === turnId && this.suspendedTurnState != null,
307
331
  onStopConditionMet: () =>
@@ -328,6 +352,49 @@ export class HarnessAgentSession {
328
352
  await this.requireReusableSession().doCompact(customInstructions);
329
353
  }
330
354
 
355
+ /**
356
+ * Submit another user message to the active turn.
357
+ *
358
+ * The runtime accepts the message for its next safe input boundary. Output
359
+ * caused by the message remains part of the active turn's result stream.
360
+ */
361
+ async experimental_steerTurn(text: string): Promise<void> {
362
+ this.requireReusableSession();
363
+ const activePromptControl = this.activePromptControl;
364
+ if (
365
+ this.turnState !== 'running' ||
366
+ this.suspendedTurnState != null ||
367
+ activePromptControl == null
368
+ ) {
369
+ throw new Error(
370
+ `Harness session ${this.sessionId} has no running turn to steer.`,
371
+ );
372
+ }
373
+
374
+ const control = await activePromptControl.promise;
375
+ if (
376
+ control == null ||
377
+ this.sessionState !== 'active' ||
378
+ this.turnState !== 'running' ||
379
+ this.suspendedTurnState != null ||
380
+ this.activePromptControl !== activePromptControl ||
381
+ this.activeTurnSequence !== activePromptControl.turnId
382
+ ) {
383
+ throw new Error(
384
+ `Harness session ${this.sessionId} no longer has the running turn targeted for steering.`,
385
+ );
386
+ }
387
+
388
+ if (control.submitUserMessage == null) {
389
+ throw new HarnessCapabilityUnsupportedError({
390
+ message: `Harness '${this.harness.harnessId}' does not support steering active turns.`,
391
+ harnessId: this.harness.harnessId,
392
+ });
393
+ }
394
+
395
+ await control.submitUserMessage(text);
396
+ }
397
+
331
398
  /**
332
399
  * Park the session, returning a payload the caller can persist and later
333
400
  * pass to `agent.createSession({ sessionId, resumeFrom })` to reconnect.
@@ -388,7 +455,7 @@ export class HarnessAgentSession {
388
455
  return validated;
389
456
  } finally {
390
457
  this.endLocalHandle({ sessionState: 'stopped' });
391
- if (this.ownsSandboxLifecycle) {
458
+ if (this.ownsSandboxLifecycle && 'stop' in sandboxSession) {
392
459
  await Promise.resolve(sandboxSession.stop()).catch(() => {});
393
460
  }
394
461
  }
@@ -407,9 +474,11 @@ export class HarnessAgentSession {
407
474
  await Promise.resolve(session.doDestroy()).catch(() => {});
408
475
  }
409
476
  if (!this.ownsSandboxLifecycle) return;
410
- await Promise.resolve(
411
- sandboxSession.destroy?.() ?? sandboxSession.stop(),
412
- ).catch(() => {});
477
+ if ('stop' in sandboxSession) {
478
+ await Promise.resolve(
479
+ sandboxSession.destroy?.() ?? sandboxSession.stop(),
480
+ ).catch(() => {});
481
+ }
413
482
  }
414
483
 
415
484
  /**
@@ -474,6 +543,7 @@ export class HarnessAgentSession {
474
543
  private async suspendCurrentTurn(options: {
475
544
  session: HarnessAgentAdapterSession;
476
545
  }): Promise<HarnessAgentContinueTurnState> {
546
+ this.clearActivePromptControl();
477
547
  this.suspendedTurnState ??= (async () => {
478
548
  const raw = await options.session.doSuspendTurn();
479
549
  const validated = await validateLifecycleStateData({
@@ -546,12 +616,14 @@ export class HarnessAgentSession {
546
616
 
547
617
  private markAwaitingApprovalIfActive(): void {
548
618
  if (this.sessionState === 'active') {
619
+ this.clearActivePromptControl();
549
620
  this.turnState = 'awaiting-approval';
550
621
  }
551
622
  }
552
623
 
553
624
  private markAwaitingToolResultIfActive(): void {
554
625
  if (this.sessionState === 'active') {
626
+ this.clearActivePromptControl();
555
627
  this.turnState = 'awaiting-tool-result';
556
628
  }
557
629
  }
@@ -561,12 +633,63 @@ export class HarnessAgentSession {
561
633
  this.activeTurnSequence = turnId;
562
634
  this.suspendedTurnState = undefined;
563
635
  this.turnState = 'running';
636
+ this.clearActivePromptControl();
637
+ let resolve!: (control: HarnessV1PromptControl | undefined) => void;
638
+ const promise = new Promise<HarnessV1PromptControl | undefined>(
639
+ resolvePromise => {
640
+ resolve = resolvePromise;
641
+ },
642
+ );
643
+ this.activePromptControl = {
644
+ turnId,
645
+ promise,
646
+ resolve,
647
+ settled: false,
648
+ };
564
649
  return turnId;
565
650
  }
566
651
 
652
+ private setPromptControl(options: {
653
+ turnId: number;
654
+ control: HarnessV1PromptControl;
655
+ }): void {
656
+ const activePromptControl = this.activePromptControl;
657
+ if (
658
+ this.sessionState !== 'active' ||
659
+ this.turnState !== 'running' ||
660
+ this.activeTurnSequence !== options.turnId ||
661
+ activePromptControl?.turnId !== options.turnId
662
+ ) {
663
+ return;
664
+ }
665
+ this.settleActivePromptControl(options.control);
666
+ }
667
+
668
+ private settleActivePromptControl(
669
+ control: HarnessV1PromptControl | undefined,
670
+ ): void {
671
+ const activePromptControl = this.activePromptControl;
672
+ if (activePromptControl == null || activePromptControl.settled) return;
673
+ activePromptControl.settled = true;
674
+ activePromptControl.resolve(control);
675
+ }
676
+
677
+ private clearActivePromptControl(turnId?: number): void {
678
+ if (
679
+ turnId != null &&
680
+ this.activePromptControl != null &&
681
+ this.activePromptControl.turnId !== turnId
682
+ ) {
683
+ return;
684
+ }
685
+ this.settleActivePromptControl(undefined);
686
+ this.activePromptControl = undefined;
687
+ }
688
+
567
689
  private finishTrackedTurn(options: { turnId: number }): void {
568
690
  if (this.sessionState !== 'active') return;
569
691
  if (this.activeTurnSequence !== options.turnId) return;
692
+ this.clearActivePromptControl(options.turnId);
570
693
  this.pendingToolApprovals.clear();
571
694
  this.pendingToolResults.clear();
572
695
  this.suspendedTurnState = undefined;
@@ -576,6 +699,7 @@ export class HarnessAgentSession {
576
699
  private endLocalHandle(options: {
577
700
  sessionState: Exclude<HarnessAgentSessionState, 'active'>;
578
701
  }): void {
702
+ this.clearActivePromptControl();
579
703
  this.sessionState = options.sessionState;
580
704
  this.underlyingSession = undefined;
581
705
  this.sandboxSession = undefined;
@@ -11,6 +11,7 @@ import {
11
11
  asSchema,
12
12
  generateId,
13
13
  type Context,
14
+ type Experimental_SandboxSession as SandboxSession,
14
15
  type ModelMessage,
15
16
  type ToolSet,
16
17
  } from '@ai-sdk/provider-utils';
@@ -64,6 +65,8 @@ import {
64
65
  resolvePermissionMode,
65
66
  } from './internal/permission-mode';
66
67
  import { resolveHarnessAgentToolFiltering } from './internal/tool-filtering';
68
+ import { resolveSandboxDefaultWorkingDirectory } from '../utils/resolve-sandbox-default-working-directory';
69
+ import { getRestrictedSandboxSession } from '../utils/get-restricted-sandbox-session';
67
70
 
68
71
  export type { HarnessAllTools } from './harness-agent-tool-types';
69
72
 
@@ -108,12 +111,13 @@ export interface HarnessAgentCallExtensions {
108
111
  * Adapter builtin tools (e.g. Claude Code's `Bash`) pass through
109
112
  * untouched.
110
113
  * - **Sandbox propagation.** On `createSession`, the agent uses a
111
- * caller-provided network sandbox session when present; otherwise it calls
112
- * the configured provider's `createSession()` (or `resumeSession()`). It
113
- * passes the selected session into `doStart`. Its `restricted()` view (a tool-safe
114
- * `Experimental_SandboxSession`) is handed to user-tool `execute()` calls
115
- * via `experimental_sandbox`. Caller-provided sandboxes remain owned by the
116
- * caller and are not stopped or destroyed by the harness layer.
114
+ * caller-provided network or basic sandbox session when present; otherwise
115
+ * it calls the configured provider's `createSession()` (or
116
+ * `resumeSession()`). It passes the selected session into `doStart`. A
117
+ * tool-safe `SandboxSession` is handed to user-tool
118
+ * `execute()` calls via `experimental_sandbox`. Caller-provided sandboxes
119
+ * remain owned by the caller and are not stopped or destroyed by the
120
+ * harness layer.
117
121
  */
118
122
  export class HarnessAgent<
119
123
  THarness extends HarnessAgentAdapter<any> = HarnessAgentAdapter,
@@ -234,10 +238,10 @@ export class HarnessAgent<
234
238
  */
235
239
  continueFrom?: HarnessAgentContinueTurnState;
236
240
  /**
237
- * Existing network sandbox session to run the harness in. When provided,
238
- * the caller retains ownership of the sandbox lifecycle.
241
+ * Existing sandbox session to run the harness in. When provided, the
242
+ * caller retains ownership of the sandbox lifecycle.
239
243
  */
240
- sandboxSession?: HarnessV1NetworkSandboxSession;
244
+ sandboxSession?: HarnessV1NetworkSandboxSession | SandboxSession;
241
245
  abortSignal?: AbortSignal;
242
246
  }): Promise<HarnessAgentSession> {
243
247
  const sessionId = options?.sessionId ?? generateId();
@@ -281,12 +285,19 @@ export class HarnessAgent<
281
285
  // Acquires the concrete sandbox session, either by starting fresh and then
282
286
  // creating a post-bootstrap snapshot, or by reusing a previously created
283
287
  // snapshot based on the bootstrap-based hashes.
284
- let sandboxSession: HarnessV1NetworkSandboxSession;
288
+ let sandboxSession: HarnessV1NetworkSandboxSession | SandboxSession;
285
289
  let sessionWorkDir: string;
286
290
  if (providedSandboxSession != null) {
287
291
  sandboxSession = providedSandboxSession;
292
+ const toolSafeSandboxSession =
293
+ getRestrictedSandboxSession(sandboxSession);
294
+ const defaultWorkingDirectory =
295
+ await resolveSandboxDefaultWorkingDirectory({
296
+ sandboxSession,
297
+ abortSignal,
298
+ });
288
299
  sessionWorkDir = resolveSessionWorkDir({
289
- defaultWorkingDirectory: sandboxSession.defaultWorkingDirectory,
300
+ defaultWorkingDirectory,
290
301
  harnessId: harness.harnessId,
291
302
  sessionId,
292
303
  workDir: this.sandboxConfig.workDir,
@@ -297,10 +308,10 @@ export class HarnessAgent<
297
308
  const recipeIdentity = await hashHarnessBootstrap(recipe);
298
309
  try {
299
310
  await applyBootstrapRecipe({
300
- session: sandboxSession.restricted(),
311
+ session: toolSafeSandboxSession,
301
312
  recipe,
302
313
  identity: recipeIdentity,
303
- defaultWorkingDirectory: sandboxSession.defaultWorkingDirectory,
314
+ defaultWorkingDirectory,
304
315
  abortSignal,
305
316
  });
306
317
  } catch (err) {
@@ -325,12 +336,14 @@ export class HarnessAgent<
325
336
  harnessId: harness.harnessId,
326
337
  });
327
338
  }
328
- sandboxSession = await sandboxProvider.resumeSession({
339
+ const resumedSandboxSession = await sandboxProvider.resumeSession({
329
340
  sessionId,
330
341
  abortSignal,
331
342
  });
343
+ sandboxSession = resumedSandboxSession;
332
344
  sessionWorkDir = resolveSessionWorkDir({
333
- defaultWorkingDirectory: sandboxSession.defaultWorkingDirectory,
345
+ defaultWorkingDirectory:
346
+ resumedSandboxSession.defaultWorkingDirectory,
334
347
  harnessId: harness.harnessId,
335
348
  sessionId,
336
349
  workDir: this.sandboxConfig.workDir,
@@ -352,14 +365,16 @@ export class HarnessAgent<
352
365
  settings: this.sandboxConfig,
353
366
  });
354
367
 
355
- sandboxSession = await sandboxProvider.createSession({
368
+ const createdSandboxSession = await sandboxProvider.createSession({
356
369
  sessionId,
357
370
  abortSignal,
358
371
  identity: sandboxBootstrapPlan.identity,
359
372
  onFirstCreate: sandboxBootstrapPlan.onFirstCreate,
360
373
  });
374
+ sandboxSession = createdSandboxSession;
361
375
  sessionWorkDir = resolveSessionWorkDir({
362
- defaultWorkingDirectory: sandboxSession.defaultWorkingDirectory,
376
+ defaultWorkingDirectory:
377
+ createdSandboxSession.defaultWorkingDirectory,
363
378
  harnessId: harness.harnessId,
364
379
  sessionId,
365
380
  workDir: sandboxBootstrapPlan.workDir,
@@ -375,10 +390,11 @@ export class HarnessAgent<
375
390
  ) {
376
391
  try {
377
392
  await applyBootstrapRecipe({
378
- session: sandboxSession.restricted(),
393
+ session: createdSandboxSession.restricted(),
379
394
  recipe: sandboxBootstrapPlan.recipe,
380
395
  identity: sandboxBootstrapPlan.recipeIdentity,
381
- defaultWorkingDirectory: sandboxSession.defaultWorkingDirectory,
396
+ defaultWorkingDirectory:
397
+ createdSandboxSession.defaultWorkingDirectory,
382
398
  abortSignal,
383
399
  });
384
400
  } catch (err) {
@@ -400,7 +416,7 @@ export class HarnessAgent<
400
416
  });
401
417
  if (this.sandboxConfig.onSession != null) {
402
418
  await this.sandboxConfig.onSession({
403
- session: sandboxSession.restricted(),
419
+ session: getRestrictedSandboxSession(sandboxSession),
404
420
  sessionWorkDir,
405
421
  abortSignal,
406
422
  });
@@ -586,6 +602,20 @@ export class HarnessAgent<
586
602
  return result;
587
603
  }
588
604
 
605
+ /**
606
+ * Submit another user message to a currently running session turn.
607
+ *
608
+ * The returned promise resolves after the runtime has accepted the message
609
+ * for its next safe input boundary. Output caused by the message remains in
610
+ * the current turn's stream.
611
+ */
612
+ async experimental_steer(options: {
613
+ session: HarnessAgentSession;
614
+ text: string;
615
+ }): Promise<void> {
616
+ await options.session.experimental_steerTurn(options.text);
617
+ }
618
+
589
619
  // ─── Internals ──────────────────────────────────────────────────────
590
620
 
591
621
  private _startTurn(input: {
@@ -923,9 +953,11 @@ function resolveSandboxConfig(
923
953
  }
924
954
 
925
955
  async function cleanupAfterStartFailure(input: {
926
- sandboxSession: HarnessV1NetworkSandboxSession;
956
+ sandboxSession: HarnessV1NetworkSandboxSession | SandboxSession;
927
957
  ownsSandboxLifecycle: boolean;
928
958
  }): Promise<void> {
929
959
  if (!input.ownsSandboxLifecycle) return;
930
- await Promise.resolve(input.sandboxSession.stop()).catch(() => {});
960
+ if ('stop' in input.sandboxSession) {
961
+ await Promise.resolve(input.sandboxSession.stop()).catch(() => {});
962
+ }
931
963
  }
@@ -108,6 +108,7 @@ export function runPrompt<
108
108
  onToolResultSettled?: (toolCallId: string) => void;
109
109
  onTurnFinished?: () => void;
110
110
  onTurnFailed?: () => void;
111
+ onPromptControlAvailable?: (control: HarnessV1PromptControl) => void;
111
112
  /**
112
113
  * Reports that the adapter stream closed because the host intentionally
113
114
  * suspended the still-running turn at a workflow slice boundary.
@@ -213,6 +214,7 @@ export function runPrompt<
213
214
  }
214
215
 
215
216
  const { stream, control } = bridge;
217
+ input.onPromptControlAvailable?.(control);
216
218
  const reader = stream.getReader();
217
219
  const toolCallsByToolCallId = new Map<string, ToolCallTextStreamPart>();
218
220
  const rawToolCallsByToolCallId = new Map<
@@ -1,6 +1,7 @@
1
1
  import { posix } from 'node:path';
2
2
  import type { Experimental_SandboxSession as SandboxSession } from '@ai-sdk/provider-utils';
3
3
  import type { HarnessV1Bootstrap } from '../../v1';
4
+ import { resolveSandboxDefaultWorkingDirectory } from '../../utils/resolve-sandbox-default-working-directory';
4
5
  import type { HarnessAgentSandboxConfig } from '../harness-agent-settings';
5
6
  import { applyBootstrapRecipe, hashHarnessBootstrap } from './bootstrap-recipe';
6
7
 
@@ -148,8 +149,8 @@ export async function runSandboxBootstrap({
148
149
 
149
150
  const resolvedDefaultWorkingDirectory =
150
151
  defaultWorkingDirectory ??
151
- (await resolveDefaultWorkingDirectory({
152
- session,
152
+ (await resolveSandboxDefaultWorkingDirectory({
153
+ sandboxSession: session,
153
154
  abortSignal,
154
155
  }));
155
156
 
@@ -181,32 +182,6 @@ export async function runSandboxBootstrap({
181
182
  await onBootstrap({ session, workDir: bootstrapWorkDir, abortSignal });
182
183
  }
183
184
 
184
- export async function resolveDefaultWorkingDirectory({
185
- session,
186
- abortSignal,
187
- }: {
188
- readonly session: SandboxSession;
189
- readonly abortSignal?: AbortSignal;
190
- }): Promise<string> {
191
- const result = await session.run({
192
- command: 'pwd',
193
- abortSignal,
194
- });
195
- if (result.exitCode !== 0) {
196
- throw new Error(
197
- `Failed to resolve sandbox default working directory (exit ${result.exitCode}): ${result.stderr || result.stdout}`,
198
- );
199
- }
200
-
201
- const cwd = result.stdout.trim();
202
- if (!posix.isAbsolute(cwd)) {
203
- throw new Error(
204
- `Failed to resolve sandbox default working directory: expected an absolute path, got ${JSON.stringify(cwd)}.`,
205
- );
206
- }
207
- return cwd === '/' ? cwd : cwd.replace(/\/+$/, '');
208
- }
209
-
210
185
  export async function ensureSandboxDirectory({
211
186
  session,
212
187
  workDir,
@@ -1,13 +1,13 @@
1
1
  import type { Experimental_SandboxSession as SandboxSession } from '@ai-sdk/provider-utils';
2
2
  import type { HarnessAgentSandboxConfig } from './harness-agent-settings';
3
3
  import type { HarnessAgentAdapter } from './harness-agent-types';
4
+ import { resolveSandboxDefaultWorkingDirectory } from '../utils/resolve-sandbox-default-working-directory';
4
5
  import {
5
6
  applyBootstrapRecipe,
6
7
  hashHarnessBootstrap,
7
8
  } from './internal/bootstrap-recipe';
8
9
  import {
9
10
  normalizeSandboxWorkDir,
10
- resolveDefaultWorkingDirectory,
11
11
  runSandboxBootstrap,
12
12
  validateSandboxBootstrapSettings,
13
13
  } from './internal/sandbox-bootstrap';
@@ -80,8 +80,8 @@ export async function prepareSandboxForHarness(options: {
80
80
 
81
81
  const recipeIdentity = await hashHarnessBootstrap(recipe);
82
82
  recipeIdentities[harness.harnessId] = recipeIdentity;
83
- defaultWorkingDirectory ??= await resolveDefaultWorkingDirectory({
84
- session: options.session,
83
+ defaultWorkingDirectory ??= await resolveSandboxDefaultWorkingDirectory({
84
+ sandboxSession: options.session,
85
85
  abortSignal: options.abortSignal,
86
86
  });
87
87
  await applyBootstrapRecipe({