pi-microsandbox 0.1.1 → 0.3.0

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.
@@ -4,33 +4,17 @@ import {
4
4
  STATE_SCHEMA_VERSION,
5
5
  type BootRequest,
6
6
  type Config,
7
- type GitRepoInfo,
8
7
  type LockHandle,
9
8
  type PersistedSandboxState,
10
- type PreparedStorage,
11
9
  type PruneReport,
12
10
  type RuntimeExecution,
11
+ type RuntimePreparation,
13
12
  type RuntimeState,
14
13
  type SandboxManager,
15
14
  type SandboxTransport,
16
- type SeedResult,
17
- type StorageMode,
18
- type StoragePlan,
19
15
  type ToolOperations,
20
16
  } from "./types.ts";
21
17
 
22
- /**
23
- * Inputs to the storage planner. Kept local because the shared contract only
24
- * describes the values crossing the manager boundary.
25
- */
26
- export interface StoragePlanInput {
27
- cwd: string;
28
- sessionId: string;
29
- config: Config;
30
- git: GitRepoInfo;
31
- restored?: PersistedSandboxState | null;
32
- }
33
-
34
18
  /** The SDK adapter deliberately owns the concrete shape of an inspected handle. */
35
19
  export interface InspectedSandbox {
36
20
  name: string;
@@ -42,21 +26,14 @@ export interface InspectedSandbox {
42
26
  export interface SandboxManagerDeps {
43
27
  acquireOwnerLock(request: BootRequest): Promise<LockHandle | null>;
44
28
  pruneOthers(currentSessionId: string): Promise<PruneReport>;
45
- detectGit(cwd: string): Promise<GitRepoInfo>;
46
- buildStoragePlan(input: StoragePlanInput): StoragePlan;
47
- prepareStorage(
48
- plan: StoragePlan,
49
- restored: PersistedSandboxState | null,
50
- ): Promise<PreparedStorage>;
51
29
  inspectSandbox(name: string): Promise<InspectedSandbox | null>;
52
30
  connectSandbox(value: InspectedSandbox): Promise<unknown>;
53
31
  startSandbox(value: InspectedSandbox): Promise<unknown>;
54
- createSandbox(request: BootRequest, prepared: PreparedStorage): Promise<unknown>;
32
+ createSandbox(request: BootRequest): Promise<unknown>;
55
33
  stopAndRemove(name: string, timeoutMs: number): Promise<void>;
56
34
  createTransport(raw: unknown): SandboxTransport;
57
35
  createOperations(transport: SandboxTransport): ToolOperations;
58
- probeAndBootstrap(runtime: RuntimeExecution, config: Config): Promise<void>;
59
- seed(runtime: RuntimeExecution, prepared: PreparedStorage): Promise<SeedResult>;
36
+ prepareRuntime(runtime: RuntimeExecution, config: Config): Promise<RuntimePreparation>;
60
37
  persist(state: PersistedSandboxState): void;
61
38
  now?: () => number;
62
39
  }
@@ -71,18 +48,6 @@ const STOPPED_STATUSES = new Set([
71
48
  "created",
72
49
  ]);
73
50
 
74
- function modeForPlan(plan: StoragePlan): StorageMode {
75
- return plan.kind === "git-volume"
76
- ? "git"
77
- : plan.kind === "direct-mount"
78
- ? "direct"
79
- : "none";
80
- }
81
-
82
- function volumeNameForPlan(plan: StoragePlan): string | undefined {
83
- return plan.kind === "git-volume" ? plan.volumeName : undefined;
84
- }
85
-
86
51
  function statusOf(value: InspectedSandbox): string | undefined {
87
52
  if (typeof value.status === "string") return value.status.toLowerCase();
88
53
  if (value.running === true) return "running";
@@ -111,44 +76,38 @@ function isManagedOwner(value: InspectedSandbox, request: BootRequest): boolean
111
76
  );
112
77
  }
113
78
 
114
- function matchesPlan(value: InspectedSandbox, request: BootRequest, plan: StoragePlan): boolean {
79
+ function isLegacyManagedOwner(value: InspectedSandbox, request: BootRequest): boolean {
80
+ const labels = labelsOf(value);
81
+ return labels["pi-msb.managed"] === "true" &&
82
+ labels["pi-msb.session"] === request.sessionId &&
83
+ /^\d+$/.test(labels["pi-msb.schema"] ?? "") &&
84
+ Number(labels["pi-msb.schema"]) < STATE_SCHEMA_VERSION;
85
+ }
86
+
87
+ function matchesWorkspace(value: InspectedSandbox, request: BootRequest): boolean {
115
88
  const labels = labelsOf(value);
116
- const expectedMode = modeForPlan(plan);
117
- const expectedVolume = volumeNameForPlan(plan);
118
-
119
- if (request.config.mode !== "auto" && request.config.mode !== expectedMode) return false;
120
- if (!isManagedOwner(value, request)) return false;
121
- if (labels["pi-msb.mode"] !== expectedMode) return false;
122
- if (labels["pi-msb.cwd"] !== request.cwd) return false;
123
- if (labels["pi-msb.image"] !== request.config.image) return false;
124
-
125
- const actualVolume = labels["pi-msb.volume"];
126
- return expectedVolume === undefined
127
- ? actualVolume === undefined
128
- : actualVolume === expectedVolume;
89
+ return isManagedOwner(value, request) &&
90
+ labels["pi-msb.cwd"] === request.cwd &&
91
+ labels["pi-msb.root"] === request.workspace.hostRoot &&
92
+ labels["pi-msb.guest-root"] === request.workspace.guestRoot &&
93
+ labels["pi-msb.image"] === request.config.image;
129
94
  }
130
95
 
131
96
  function infoFor(
132
97
  request: BootRequest,
133
- plan: StoragePlan,
134
- prepared: PreparedStorage,
135
- seedBranch: string | null | undefined,
136
- seedSha: string | null | undefined,
137
98
  createdAt: number,
138
99
  name: string,
100
+ preparation: RuntimePreparation,
139
101
  ): NonNullable<RuntimeState["info"]> {
140
102
  return {
141
103
  name,
142
104
  displayId: displayId(request.sessionId),
143
- mode: modeForPlan(plan),
144
105
  image: request.config.image,
145
106
  pid: process.pid,
146
107
  cwd: request.cwd,
147
- volumeName: volumeNameForPlan(plan),
148
- volumeHostPath: plan.kind === "git-volume" ? prepared.volume?.hostPath : undefined,
149
- seedBranch: seedBranch ?? null,
150
- seedSha: seedSha ?? null,
108
+ root: request.workspace.guestRoot,
151
109
  createdAt,
110
+ docker: { ...preparation.docker },
152
111
  };
153
112
  }
154
113
 
@@ -169,6 +128,8 @@ function sameBootRequest(a: BootRequest, b: BootRequest): boolean {
169
128
  return (
170
129
  a.sessionId === b.sessionId &&
171
130
  a.cwd === b.cwd &&
131
+ a.workspace.hostRoot === b.workspace.hostRoot &&
132
+ a.workspace.guestRoot === b.workspace.guestRoot &&
172
133
  JSON.stringify(a.config) === JSON.stringify(b.config)
173
134
  );
174
135
  }
@@ -180,7 +141,9 @@ function sameSessionRestore(
180
141
  if (!restored) return null;
181
142
  if (restored.version !== STATE_SCHEMA_VERSION) return null;
182
143
  if (restored.sessionId !== request.sessionId) return null;
183
- if (restored.cwd !== request.cwd) return null;
144
+ if (restored.cwd !== request.cwd ||
145
+ restored.root !== request.workspace.hostRoot ||
146
+ restored.guestRoot !== request.workspace.guestRoot) return null;
184
147
  return restored;
185
148
  }
186
149
 
@@ -192,10 +155,10 @@ export function createSandboxManager(deps: SandboxManagerDeps): SandboxManager {
192
155
  let state: RuntimeState = { status: "disabled", info: null };
193
156
  let lock: LockHandle | null = null;
194
157
  let runtime: RuntimeExecution | null = null;
158
+ let runtimeInvalid = false;
195
159
  let sandboxName: string | null = null;
196
160
  let lastRequest: BootRequest | null = null;
197
161
  let retainedState: PersistedSandboxState | null = null;
198
- let activePlan: StoragePlan | null = null;
199
162
  // activeUses includes reservations made before preflight. pendingUses marks
200
163
  // those reservations; only activeUses - pendingUses are callbacks currently
201
164
  // using a transport and therefore must drain before replacement/disposal.
@@ -240,6 +203,7 @@ export function createSandboxManager(deps: SandboxManagerDeps): SandboxManager {
240
203
  async function disposeRuntime(): Promise<void> {
241
204
  const current = runtime;
242
205
  runtime = null;
206
+ runtimeInvalid = false;
243
207
  if (!current) return;
244
208
  try {
245
209
  await current.transport.dispose();
@@ -285,6 +249,12 @@ export function createSandboxManager(deps: SandboxManagerDeps): SandboxManager {
285
249
  return request.config.sandboxName ?? sandboxNameFor(request.sessionId);
286
250
  }
287
251
 
252
+ function invalidatesRuntime(error: unknown): boolean {
253
+ if (!error || typeof error !== "object" || !("code" in error)) return false;
254
+ const code = (error as { code?: unknown }).code;
255
+ return code === "SANDBOX_DOWN";
256
+ }
257
+
288
258
  async function connectAndBuild(raw: unknown): Promise<RuntimeExecution> {
289
259
  const transport = deps.createTransport(raw);
290
260
  try {
@@ -342,7 +312,6 @@ export function createSandboxManager(deps: SandboxManagerDeps): SandboxManager {
342
312
  }
343
313
  lock = owner;
344
314
 
345
- let prepared: PreparedStorage | null = null;
346
315
  let partialSandbox = false;
347
316
  let currentRequestInfo: RuntimeState["info"] = null;
348
317
  let bootedRuntime: RuntimeExecution | null = null;
@@ -351,17 +320,7 @@ export function createSandboxManager(deps: SandboxManagerDeps): SandboxManager {
351
320
  if (request.config.pruneOnStart) {
352
321
  await deps.pruneOthers(request.sessionId);
353
322
  }
354
- const git = await deps.detectGit(request.cwd);
355
323
  const restored = sameSessionRestore(request);
356
- const plan = deps.buildStoragePlan({
357
- cwd: request.cwd,
358
- sessionId: request.sessionId,
359
- config: request.config,
360
- git,
361
- restored,
362
- });
363
- prepared = await deps.prepareStorage(plan, restored);
364
-
365
324
  const name = requestSandboxName(request);
366
325
  sandboxName = name;
367
326
  const inspected = await deps.inspectSandbox(name);
@@ -369,17 +328,23 @@ export function createSandboxManager(deps: SandboxManagerDeps): SandboxManager {
369
328
 
370
329
  if (!inspected) {
371
330
  partialSandbox = true;
372
- raw = await deps.createSandbox(request, prepared);
331
+ raw = await deps.createSandbox(request);
332
+ } else if (isLegacyManagedOwner(inspected, request)) {
333
+ // The current session owner lock guards removal. Legacy named volumes
334
+ // are intentionally neither inspected nor removed.
335
+ await deps.stopAndRemove(name, request.config.stopTimeoutMs);
336
+ partialSandbox = true;
337
+ raw = await deps.createSandbox(request);
373
338
  } else if (!isManagedOwner(inspected, request)) {
374
339
  throw new Error(`sandbox name conflict: ${name}`);
375
- } else if (!matchesPlan(inspected, request, plan)) {
340
+ } else if (!matchesWorkspace(inspected, request)) {
376
341
  // The labels prove this is ours, but its configuration is stale. Only
377
342
  // that proven ownership permits a replacement.
378
343
  await deps.stopAndRemove(name, request.config.stopTimeoutMs);
379
344
  // The old instance was removed; a failed replacement still needs its
380
345
  // own best-effort cleanup.
381
346
  partialSandbox = true;
382
- raw = await deps.createSandbox(request, prepared);
347
+ raw = await deps.createSandbox(request);
383
348
  } else if (isRunning(inspected)) {
384
349
  raw = await deps.connectSandbox(inspected);
385
350
  partialSandbox = true;
@@ -396,73 +361,28 @@ export function createSandboxManager(deps: SandboxManagerDeps): SandboxManager {
396
361
 
397
362
  bootedRuntime = await connectAndBuild(raw);
398
363
  runtime = bootedRuntime;
399
- await deps.probeAndBootstrap(bootedRuntime, request.config);
400
-
401
- let seed: SeedResult | null = null;
402
- if (
403
- prepared.plan.kind === "git-volume" &&
404
- prepared.createdVolume &&
405
- prepared.plan.seedRequired
406
- ) {
407
- seed = await deps.seed(bootedRuntime, prepared);
408
- }
364
+ runtimeInvalid = false;
365
+ const preparation = await deps.prepareRuntime(bootedRuntime, request.config);
409
366
 
410
367
  const createdAt = deps.now ? deps.now() : Date.now();
411
- const previous = restored;
412
- const seedBranch = prepared.createdVolume
413
- ? prepared.plan.kind === "git-volume"
414
- ? prepared.plan.branch
415
- : null
416
- : previous?.seedBranch ??
417
- (prepared.plan.kind === "git-volume" ? prepared.plan.branch : null);
418
- const seedSha = prepared.createdVolume
419
- ? seed?.headSha ??
420
- (prepared.plan.kind === "git-volume" ? prepared.plan.headSha : null)
421
- : previous?.seedSha ??
422
- (prepared.plan.kind === "git-volume" ? prepared.plan.headSha : null);
423
- currentRequestInfo = infoFor(
424
- request,
425
- prepared.plan,
426
- prepared,
427
- seedBranch,
428
- seedSha,
429
- createdAt,
430
- name,
431
- );
432
-
433
- // Bundle cleanup is deliberately before state publication: a successful
434
- // boot never publishes a state whose temporary host artifact leaked.
435
- if (prepared.bundle) await prepared.bundle.cleanup();
436
- prepared.bundle = null;
368
+ currentRequestInfo = infoFor(request, createdAt, name, preparation);
437
369
 
438
370
  const persisted: PersistedSandboxState = {
439
371
  version: STATE_SCHEMA_VERSION,
440
372
  sessionId: request.sessionId,
441
373
  sandboxName: name,
442
- mode: currentRequestInfo.mode,
443
374
  cwd: request.cwd,
375
+ root: request.workspace.hostRoot,
376
+ guestRoot: request.workspace.guestRoot,
444
377
  image: request.config.image,
445
- volumeName: currentRequestInfo.volumeName,
446
- volumeHostPath: currentRequestInfo.volumeHostPath,
447
- seedBranch: currentRequestInfo.seedBranch,
448
- seedSha: currentRequestInfo.seedSha,
449
378
  enabled: true,
450
379
  createdAt,
451
380
  };
452
381
  deps.persist(persisted);
453
382
  retainedState = persisted;
454
- activePlan = prepared.plan;
455
383
  state = { status: "active", info: currentRequestInfo };
456
384
  return state;
457
385
  } catch (error) {
458
- if (prepared?.bundle) {
459
- try {
460
- await prepared.bundle.cleanup();
461
- } catch {
462
- // The primary boot error is more useful, and no secret is exposed.
463
- }
464
- prepared.bundle = null;
465
- }
466
386
  await disposeRuntime();
467
387
  if (partialSandbox && sandboxName) {
468
388
  try {
@@ -473,7 +393,6 @@ export function createSandboxManager(deps: SandboxManagerDeps): SandboxManager {
473
393
  }
474
394
  runtime = null;
475
395
  bootedRuntime = null;
476
- activePlan = null;
477
396
  sandboxName = null;
478
397
  if (lock === owner) {
479
398
  lock = null;
@@ -489,7 +408,7 @@ export function createSandboxManager(deps: SandboxManagerDeps): SandboxManager {
489
408
  }
490
409
 
491
410
  async function wakeIfNeeded(): Promise<RuntimeExecution> {
492
- if (state.status !== "active" || !runtime || !lastRequest || !sandboxName || !activePlan) {
411
+ if (state.status !== "active" || !runtime || !lastRequest || !sandboxName) {
493
412
  throw unavailableError(state);
494
413
  }
495
414
 
@@ -504,22 +423,22 @@ export function createSandboxManager(deps: SandboxManagerDeps): SandboxManager {
504
423
  throw new Error(`sandbox ${sandboxName} has conflicting labels`);
505
424
  }
506
425
  // Revalidate the complete managed runtime identity before waking or
507
- // reconnecting. A same-name resource with changed mode/cwd/image/volume is
508
- // not safe to attach to.
509
- if (!matchesPlan(inspected, lastRequest, activePlan)) {
426
+ // reconnecting. A same-name resource with changed cwd/root/image is not
427
+ // safe to attach to.
428
+ if (!matchesWorkspace(inspected, lastRequest)) {
510
429
  throw new Error(`sandbox ${sandboxName} configuration changed`);
511
430
  }
512
431
 
513
- if (isRunning(inspected)) {
432
+ if (isRunning(inspected) && !runtimeInvalid) {
514
433
  // The boot-created transport remains authoritative while the sandbox is
515
434
  // running. Reconnecting here would replace a valid handle and dispose a
516
435
  // transport that may still have active callers.
517
436
  return runtime;
518
437
  }
519
438
 
520
- // A stopped/unknown sandbox requires a replacement transport. Do not stop,
521
- // start, or dispose the old one until every earlier callback has released
522
- // its runtime-use reservation.
439
+ // A stopped/unknown sandbox or invalid running handle requires a
440
+ // replacement transport. Do not start or dispose the old one until every
441
+ // earlier callback has released its runtime-use reservation.
523
442
  await waitForActiveUses();
524
443
  let raw: unknown;
525
444
  if (isStopped(inspected)) {
@@ -530,8 +449,40 @@ export function createSandboxManager(deps: SandboxManagerDeps): SandboxManager {
530
449
  }
531
450
 
532
451
  const replacement = await connectAndBuild(raw);
452
+ let preparation: RuntimePreparation;
453
+ try {
454
+ preparation = await deps.prepareRuntime(replacement, lastRequest.config);
455
+ } catch (error) {
456
+ try {
457
+ await replacement.transport.dispose();
458
+ } catch {
459
+ // Preserve the preparation error.
460
+ }
461
+ const previous = runtime;
462
+ runtime = null;
463
+ runtimeInvalid = false;
464
+ if (previous) {
465
+ try {
466
+ await previous.transport.dispose();
467
+ } catch {
468
+ // The preparation error remains authoritative.
469
+ }
470
+ }
471
+ const failedName = sandboxName;
472
+ sandboxName = null;
473
+ if (failedName) {
474
+ try {
475
+ await deps.stopAndRemove(failedName, lastRequest.config.stopTimeoutMs);
476
+ } catch {
477
+ throw new Error(`${redactMessage(error, lastRequest.config)}; failed to clean up the restarted sandbox`);
478
+ }
479
+ }
480
+ throw error;
481
+ }
533
482
  const previous = runtime;
534
483
  runtime = replacement;
484
+ runtimeInvalid = false;
485
+ if (state.info) state = { ...state, info: { ...state.info, docker: { ...preparation.docker } } };
535
486
  if (previous && previous !== replacement) {
536
487
  try {
537
488
  await previous.transport.dispose();
@@ -565,7 +516,6 @@ export function createSandboxManager(deps: SandboxManagerDeps): SandboxManager {
565
516
  }
566
517
 
567
518
  runtime = null;
568
- activePlan = null;
569
519
  sandboxName = null;
570
520
 
571
521
  if (persistDisabled && request && oldInfo) {
@@ -574,13 +524,10 @@ export function createSandboxManager(deps: SandboxManagerDeps): SandboxManager {
574
524
  version: STATE_SCHEMA_VERSION,
575
525
  sessionId: request.sessionId,
576
526
  sandboxName: oldInfo.name,
577
- mode: oldInfo.mode,
578
527
  cwd: oldInfo.cwd,
528
+ root: request.workspace.hostRoot,
529
+ guestRoot: request.workspace.guestRoot,
579
530
  image: oldInfo.image,
580
- volumeName: oldInfo.volumeName,
581
- volumeHostPath: oldInfo.volumeHostPath,
582
- seedBranch: oldInfo.seedBranch,
583
- seedSha: oldInfo.seedSha,
584
531
  enabled: false,
585
532
  createdAt,
586
533
  };
@@ -621,7 +568,7 @@ export function createSandboxManager(deps: SandboxManagerDeps): SandboxManager {
621
568
  getState(): RuntimeState {
622
569
  return {
623
570
  status: state.status,
624
- info: state.info ? { ...state.info } : null,
571
+ info: state.info ? { ...state.info, docker: { ...state.info.docker } } : null,
625
572
  ...(state.reason ? { reason: state.reason } : {}),
626
573
  };
627
574
  },
@@ -663,6 +610,9 @@ export function createSandboxManager(deps: SandboxManagerDeps): SandboxManager {
663
610
  }
664
611
  try {
665
612
  return await callback(current);
613
+ } catch (error) {
614
+ if (runtime === current && invalidatesRuntime(error)) runtimeInvalid = true;
615
+ throw error;
666
616
  } finally {
667
617
  releaseUse(true);
668
618
  }
@@ -22,7 +22,6 @@ import type {
22
22
  HostReadAccess,
23
23
  RuntimeState,
24
24
  SandboxGrepExecute,
25
- StorageMode,
26
25
  ToolOpsProvider,
27
26
  } from "./types.ts";
28
27
 
@@ -81,24 +80,16 @@ export function hostApprovalMessage(
81
80
  tool: string,
82
81
  input: Record<string, unknown>,
83
82
  cwd: string,
84
- mode?: StorageMode,
85
83
  ): string {
86
- const lines = [
84
+ return [
87
85
  `Tool: ${tool}`,
88
86
  `Working directory: ${cwd}`,
89
- ...(mode ? [`Storage mode: ${mode}`] : []),
90
87
  "",
91
88
  "Exact arguments:",
92
89
  JSON.stringify(sortForFingerprint(withoutExecutionTarget(input)), null, 2),
93
90
  "",
94
- "This operation will run outside the selected pi-microsandbox environment with the host process's permissions and environment.",
95
- ];
96
- if (mode === "git") {
97
- lines.push(
98
- "Warning: host-targeted execution bypasses the retained git volume and can mutate the host working tree.",
99
- );
100
- }
101
- return lines.join("\n");
91
+ "Warning: this operation runs on the host and bypasses VM, process, and network isolation.",
92
+ ].join("\n");
102
93
  }
103
94
 
104
95
  export interface RegisterToolsDeps {
@@ -122,9 +113,8 @@ function stateOf(deps: RegisterToolsDeps): RuntimeState {
122
113
  return deps.provider.getState();
123
114
  }
124
115
 
125
- function isHostMode(state: RuntimeState, config: Config): boolean {
126
- return state.status === "off" || state.status === "disabled" || state.status === "host-fallback" ||
127
- (state.status === "unavailable" && config.fallbackMode === "host");
116
+ function isHostMode(state: RuntimeState): boolean {
117
+ return state.status === "off" || state.status === "host-fallback";
128
118
  }
129
119
 
130
120
  function isSandboxActive(state: RuntimeState, provider: ToolOpsProvider): boolean {
@@ -214,7 +204,6 @@ export function registerSandboxTools(pi: ExtensionAPI, deps: RegisterToolsDeps):
214
204
  tool: string,
215
205
  id: string,
216
206
  input: Record<string, unknown>,
217
- mode: StorageMode | undefined,
218
207
  ): void => {
219
208
  const approved = approvedHostCalls.get(id);
220
209
  approvedHostCalls.delete(id);
@@ -222,8 +211,7 @@ export function registerSandboxTools(pi: ExtensionAPI, deps: RegisterToolsDeps):
222
211
  throw new Error("Host execution was not approved for this exact tool call.");
223
212
  }
224
213
  // Approval is deliberately consumed here rather than cached; a runtime
225
- // mode change must never extend a one-call host escape.
226
- void mode;
214
+ // state change must never extend a one-call host escape.
227
215
  };
228
216
 
229
217
  const executeHost = async (
@@ -234,13 +222,12 @@ export function registerSandboxTools(pi: ExtensionAPI, deps: RegisterToolsDeps):
234
222
  onUpdate: any,
235
223
  ctx: ExtensionContext,
236
224
  active: boolean,
237
- mode: StorageMode | undefined,
238
225
  ): Promise<any> => {
239
226
  if (active) {
240
227
  if (!deps.config.allowHostExecution) {
241
228
  throw new Error("Host execution is disabled by pi-microsandbox configuration.");
242
229
  }
243
- requireApprovedHostExecution(definition.name, id, input, mode);
230
+ requireApprovedHostExecution(definition.name, id, input);
244
231
  }
245
232
  return definition.execute(id, withoutExecutionTarget(input), signal, onUpdate, ctx);
246
233
  };
@@ -253,17 +240,17 @@ export function registerSandboxTools(pi: ExtensionAPI, deps: RegisterToolsDeps):
253
240
  execute: async (id, rawParams, signal, onUpdate, ctx) => {
254
241
  const params = rawParams as ToolParams;
255
242
  const state = stateOf(deps);
256
- const hostMode = isHostMode(state, deps.config);
243
+ const hostMode = isHostMode(state);
257
244
  const active = isSandboxActive(state, deps.provider);
258
245
  const targetHost = isHostTarget(params);
259
246
 
260
247
  if (!deps.config.routeTools.includes(name)) throw routeError(name);
261
248
  if (hostMode) {
262
- return executeHost(hostDefinition, id, params, signal, onUpdate, ctx, false, undefined);
249
+ return executeHost(hostDefinition, id, params, signal, onUpdate, ctx, false);
263
250
  }
264
251
  if (!active) throw unavailableError(state);
265
252
  if (targetHost) {
266
- return executeHost(hostDefinition, id, params, signal, onUpdate, ctx, true, state.info?.mode);
253
+ return executeHost(hostDefinition, id, params, signal, onUpdate, ctx, true);
267
254
  }
268
255
 
269
256
  if (name === "read" && deps.config.allowSkillReads) {
@@ -314,7 +301,7 @@ export function registerSandboxTools(pi: ExtensionAPI, deps: RegisterToolsDeps):
314
301
 
315
302
  pi.on("tool_call", async (event, ctx) => {
316
303
  const state = stateOf(deps);
317
- const hostMode = isHostMode(state, deps.config);
304
+ const hostMode = isHostMode(state);
318
305
  const active = isSandboxActive(state, deps.provider);
319
306
  const tool = event.toolName;
320
307
  const input = asRecord(event.input);
@@ -339,7 +326,7 @@ export function registerSandboxTools(pi: ExtensionAPI, deps: RegisterToolsDeps):
339
326
  }
340
327
  const approved = await ctx.ui.confirm(
341
328
  "Allow host execution?",
342
- hostApprovalMessage(tool, input, deps.cwd, state.info?.mode),
329
+ hostApprovalMessage(tool, input, deps.cwd),
343
330
  );
344
331
  if (!approved) {
345
332
  clearApproval(event.toolCallId);
@@ -375,7 +362,7 @@ export function registerSandboxTools(pi: ExtensionAPI, deps: RegisterToolsDeps):
375
362
 
376
363
  pi.on("user_bash", (): UserBashEventResult | undefined => {
377
364
  const state = stateOf(deps);
378
- if (isHostMode(state, deps.config)) return undefined;
365
+ if (isHostMode(state)) return undefined;
379
366
  if (!isSandboxActive(state, deps.provider)) {
380
367
  const error = unavailableError(state);
381
368
  const operations: BashOperations = {
@@ -327,22 +327,6 @@ export function createSdkTransport(sandbox: unknown): SandboxTransport {
327
327
  });
328
328
  });
329
329
 
330
- const copyFromHost = (hostPath: string, guestPath: string): Promise<void> =>
331
- call("copyFromHost", async () => {
332
- const copy = method<
333
- (hostPath: string, guestPath: string) => Promise<void>
334
- >(getFs(), "copyFromHost");
335
- await copy(hostPath, guestPath);
336
- });
337
-
338
- const copyToHost = (guestPath: string, hostPath: string): Promise<void> =>
339
- call("copyToHost", async () => {
340
- const copy = method<
341
- (guestPath: string, hostPath: string) => Promise<void>
342
- >(getFs(), "copyToHost");
343
- await copy(guestPath, hostPath);
344
- });
345
-
346
330
  const exec = (
347
331
  command: string,
348
332
  args: string[],
@@ -509,8 +493,6 @@ export function createSdkTransport(sandbox: unknown): SandboxTransport {
509
493
  exists,
510
494
  stat,
511
495
  list,
512
- copyFromHost,
513
- copyToHost,
514
496
  exec,
515
497
  execStream,
516
498
  dispose,