pi-microsandbox 0.2.0 → 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.
- package/README.md +58 -45
- package/SECURITY.md +12 -8
- package/docs/commands.md +18 -15
- package/docs/configuration.md +15 -8
- package/docs/development.md +14 -10
- package/docs/getting-started.md +16 -4
- package/docs/images.md +7 -6
- package/docs/safety.md +34 -22
- package/docs/storage.md +54 -55
- package/docs/troubleshooting.md +32 -11
- package/extensions/pi-msb/command.ts +12 -189
- package/extensions/pi-msb/config.ts +126 -38
- package/extensions/pi-msb/control.ts +42 -291
- package/extensions/pi-msb/index.ts +2 -2
- package/extensions/pi-msb/labels.ts +46 -231
- package/extensions/pi-msb/locks.ts +4 -6
- package/extensions/pi-msb/prune.ts +16 -12
- package/extensions/pi-msb/sandbox-manager.ts +39 -138
- package/extensions/pi-msb/tools.ts +13 -26
- package/extensions/pi-msb/transport.ts +0 -18
- package/extensions/pi-msb/types.ts +24 -108
- package/extensions/pi-msb/workspace.ts +126 -0
- package/native/flock/prebuilds/darwin-arm64/flock.node +0 -0
- package/package.json +1 -1
- package/extensions/pi-msb/git.ts +0 -256
- package/extensions/pi-msb/storage.ts +0 -332
|
@@ -4,34 +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,
|
|
13
11
|
type RuntimePreparation,
|
|
14
12
|
type RuntimeState,
|
|
15
13
|
type SandboxManager,
|
|
16
14
|
type SandboxTransport,
|
|
17
|
-
type SeedResult,
|
|
18
|
-
type StorageMode,
|
|
19
|
-
type StoragePlan,
|
|
20
15
|
type ToolOperations,
|
|
21
16
|
} from "./types.ts";
|
|
22
17
|
|
|
23
|
-
/**
|
|
24
|
-
* Inputs to the storage planner. Kept local because the shared contract only
|
|
25
|
-
* describes the values crossing the manager boundary.
|
|
26
|
-
*/
|
|
27
|
-
export interface StoragePlanInput {
|
|
28
|
-
cwd: string;
|
|
29
|
-
sessionId: string;
|
|
30
|
-
config: Config;
|
|
31
|
-
git: GitRepoInfo;
|
|
32
|
-
restored?: PersistedSandboxState | null;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
18
|
/** The SDK adapter deliberately owns the concrete shape of an inspected handle. */
|
|
36
19
|
export interface InspectedSandbox {
|
|
37
20
|
name: string;
|
|
@@ -43,21 +26,14 @@ export interface InspectedSandbox {
|
|
|
43
26
|
export interface SandboxManagerDeps {
|
|
44
27
|
acquireOwnerLock(request: BootRequest): Promise<LockHandle | null>;
|
|
45
28
|
pruneOthers(currentSessionId: string): Promise<PruneReport>;
|
|
46
|
-
detectGit(cwd: string): Promise<GitRepoInfo>;
|
|
47
|
-
buildStoragePlan(input: StoragePlanInput): StoragePlan;
|
|
48
|
-
prepareStorage(
|
|
49
|
-
plan: StoragePlan,
|
|
50
|
-
restored: PersistedSandboxState | null,
|
|
51
|
-
): Promise<PreparedStorage>;
|
|
52
29
|
inspectSandbox(name: string): Promise<InspectedSandbox | null>;
|
|
53
30
|
connectSandbox(value: InspectedSandbox): Promise<unknown>;
|
|
54
31
|
startSandbox(value: InspectedSandbox): Promise<unknown>;
|
|
55
|
-
createSandbox(request: BootRequest
|
|
32
|
+
createSandbox(request: BootRequest): Promise<unknown>;
|
|
56
33
|
stopAndRemove(name: string, timeoutMs: number): Promise<void>;
|
|
57
34
|
createTransport(raw: unknown): SandboxTransport;
|
|
58
35
|
createOperations(transport: SandboxTransport): ToolOperations;
|
|
59
36
|
prepareRuntime(runtime: RuntimeExecution, config: Config): Promise<RuntimePreparation>;
|
|
60
|
-
seed(runtime: RuntimeExecution, prepared: PreparedStorage): Promise<SeedResult>;
|
|
61
37
|
persist(state: PersistedSandboxState): void;
|
|
62
38
|
now?: () => number;
|
|
63
39
|
}
|
|
@@ -72,18 +48,6 @@ const STOPPED_STATUSES = new Set([
|
|
|
72
48
|
"created",
|
|
73
49
|
]);
|
|
74
50
|
|
|
75
|
-
function modeForPlan(plan: StoragePlan): StorageMode {
|
|
76
|
-
return plan.kind === "git-volume"
|
|
77
|
-
? "git"
|
|
78
|
-
: plan.kind === "direct-mount"
|
|
79
|
-
? "direct"
|
|
80
|
-
: "none";
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function volumeNameForPlan(plan: StoragePlan): string | undefined {
|
|
84
|
-
return plan.kind === "git-volume" ? plan.volumeName : undefined;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
51
|
function statusOf(value: InspectedSandbox): string | undefined {
|
|
88
52
|
if (typeof value.status === "string") return value.status.toLowerCase();
|
|
89
53
|
if (value.running === true) return "running";
|
|
@@ -112,29 +76,25 @@ function isManagedOwner(value: InspectedSandbox, request: BootRequest): boolean
|
|
|
112
76
|
);
|
|
113
77
|
}
|
|
114
78
|
|
|
115
|
-
function
|
|
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 {
|
|
116
88
|
const labels = labelsOf(value);
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
if (labels["pi-msb.mode"] !== expectedMode) return false;
|
|
123
|
-
if (labels["pi-msb.cwd"] !== request.cwd) return false;
|
|
124
|
-
if (labels["pi-msb.image"] !== request.config.image) return false;
|
|
125
|
-
|
|
126
|
-
const actualVolume = labels["pi-msb.volume"];
|
|
127
|
-
return expectedVolume === undefined
|
|
128
|
-
? actualVolume === undefined
|
|
129
|
-
: 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;
|
|
130
94
|
}
|
|
131
95
|
|
|
132
96
|
function infoFor(
|
|
133
97
|
request: BootRequest,
|
|
134
|
-
plan: StoragePlan,
|
|
135
|
-
prepared: PreparedStorage,
|
|
136
|
-
seedBranch: string | null | undefined,
|
|
137
|
-
seedSha: string | null | undefined,
|
|
138
98
|
createdAt: number,
|
|
139
99
|
name: string,
|
|
140
100
|
preparation: RuntimePreparation,
|
|
@@ -142,14 +102,10 @@ function infoFor(
|
|
|
142
102
|
return {
|
|
143
103
|
name,
|
|
144
104
|
displayId: displayId(request.sessionId),
|
|
145
|
-
mode: modeForPlan(plan),
|
|
146
105
|
image: request.config.image,
|
|
147
106
|
pid: process.pid,
|
|
148
107
|
cwd: request.cwd,
|
|
149
|
-
|
|
150
|
-
volumeHostPath: plan.kind === "git-volume" ? prepared.volume?.hostPath : undefined,
|
|
151
|
-
seedBranch: seedBranch ?? null,
|
|
152
|
-
seedSha: seedSha ?? null,
|
|
108
|
+
root: request.workspace.guestRoot,
|
|
153
109
|
createdAt,
|
|
154
110
|
docker: { ...preparation.docker },
|
|
155
111
|
};
|
|
@@ -172,6 +128,8 @@ function sameBootRequest(a: BootRequest, b: BootRequest): boolean {
|
|
|
172
128
|
return (
|
|
173
129
|
a.sessionId === b.sessionId &&
|
|
174
130
|
a.cwd === b.cwd &&
|
|
131
|
+
a.workspace.hostRoot === b.workspace.hostRoot &&
|
|
132
|
+
a.workspace.guestRoot === b.workspace.guestRoot &&
|
|
175
133
|
JSON.stringify(a.config) === JSON.stringify(b.config)
|
|
176
134
|
);
|
|
177
135
|
}
|
|
@@ -183,7 +141,9 @@ function sameSessionRestore(
|
|
|
183
141
|
if (!restored) return null;
|
|
184
142
|
if (restored.version !== STATE_SCHEMA_VERSION) return null;
|
|
185
143
|
if (restored.sessionId !== request.sessionId) return null;
|
|
186
|
-
if (restored.cwd !== request.cwd
|
|
144
|
+
if (restored.cwd !== request.cwd ||
|
|
145
|
+
restored.root !== request.workspace.hostRoot ||
|
|
146
|
+
restored.guestRoot !== request.workspace.guestRoot) return null;
|
|
187
147
|
return restored;
|
|
188
148
|
}
|
|
189
149
|
|
|
@@ -199,7 +159,6 @@ export function createSandboxManager(deps: SandboxManagerDeps): SandboxManager {
|
|
|
199
159
|
let sandboxName: string | null = null;
|
|
200
160
|
let lastRequest: BootRequest | null = null;
|
|
201
161
|
let retainedState: PersistedSandboxState | null = null;
|
|
202
|
-
let activePlan: StoragePlan | null = null;
|
|
203
162
|
// activeUses includes reservations made before preflight. pendingUses marks
|
|
204
163
|
// those reservations; only activeUses - pendingUses are callbacks currently
|
|
205
164
|
// using a transport and therefore must drain before replacement/disposal.
|
|
@@ -353,7 +312,6 @@ export function createSandboxManager(deps: SandboxManagerDeps): SandboxManager {
|
|
|
353
312
|
}
|
|
354
313
|
lock = owner;
|
|
355
314
|
|
|
356
|
-
let prepared: PreparedStorage | null = null;
|
|
357
315
|
let partialSandbox = false;
|
|
358
316
|
let currentRequestInfo: RuntimeState["info"] = null;
|
|
359
317
|
let bootedRuntime: RuntimeExecution | null = null;
|
|
@@ -362,17 +320,7 @@ export function createSandboxManager(deps: SandboxManagerDeps): SandboxManager {
|
|
|
362
320
|
if (request.config.pruneOnStart) {
|
|
363
321
|
await deps.pruneOthers(request.sessionId);
|
|
364
322
|
}
|
|
365
|
-
const git = await deps.detectGit(request.cwd);
|
|
366
323
|
const restored = sameSessionRestore(request);
|
|
367
|
-
const plan = deps.buildStoragePlan({
|
|
368
|
-
cwd: request.cwd,
|
|
369
|
-
sessionId: request.sessionId,
|
|
370
|
-
config: request.config,
|
|
371
|
-
git,
|
|
372
|
-
restored,
|
|
373
|
-
});
|
|
374
|
-
prepared = await deps.prepareStorage(plan, restored);
|
|
375
|
-
|
|
376
324
|
const name = requestSandboxName(request);
|
|
377
325
|
sandboxName = name;
|
|
378
326
|
const inspected = await deps.inspectSandbox(name);
|
|
@@ -380,17 +328,23 @@ export function createSandboxManager(deps: SandboxManagerDeps): SandboxManager {
|
|
|
380
328
|
|
|
381
329
|
if (!inspected) {
|
|
382
330
|
partialSandbox = true;
|
|
383
|
-
raw = await deps.createSandbox(request
|
|
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);
|
|
384
338
|
} else if (!isManagedOwner(inspected, request)) {
|
|
385
339
|
throw new Error(`sandbox name conflict: ${name}`);
|
|
386
|
-
} else if (!
|
|
340
|
+
} else if (!matchesWorkspace(inspected, request)) {
|
|
387
341
|
// The labels prove this is ours, but its configuration is stale. Only
|
|
388
342
|
// that proven ownership permits a replacement.
|
|
389
343
|
await deps.stopAndRemove(name, request.config.stopTimeoutMs);
|
|
390
344
|
// The old instance was removed; a failed replacement still needs its
|
|
391
345
|
// own best-effort cleanup.
|
|
392
346
|
partialSandbox = true;
|
|
393
|
-
raw = await deps.createSandbox(request
|
|
347
|
+
raw = await deps.createSandbox(request);
|
|
394
348
|
} else if (isRunning(inspected)) {
|
|
395
349
|
raw = await deps.connectSandbox(inspected);
|
|
396
350
|
partialSandbox = true;
|
|
@@ -410,72 +364,25 @@ export function createSandboxManager(deps: SandboxManagerDeps): SandboxManager {
|
|
|
410
364
|
runtimeInvalid = false;
|
|
411
365
|
const preparation = await deps.prepareRuntime(bootedRuntime, request.config);
|
|
412
366
|
|
|
413
|
-
let seed: SeedResult | null = null;
|
|
414
|
-
if (
|
|
415
|
-
prepared.plan.kind === "git-volume" &&
|
|
416
|
-
prepared.createdVolume &&
|
|
417
|
-
prepared.plan.seedRequired
|
|
418
|
-
) {
|
|
419
|
-
seed = await deps.seed(bootedRuntime, prepared);
|
|
420
|
-
}
|
|
421
|
-
|
|
422
367
|
const createdAt = deps.now ? deps.now() : Date.now();
|
|
423
|
-
|
|
424
|
-
const seedBranch = prepared.createdVolume
|
|
425
|
-
? prepared.plan.kind === "git-volume"
|
|
426
|
-
? prepared.plan.branch
|
|
427
|
-
: null
|
|
428
|
-
: previous?.seedBranch ??
|
|
429
|
-
(prepared.plan.kind === "git-volume" ? prepared.plan.branch : null);
|
|
430
|
-
const seedSha = prepared.createdVolume
|
|
431
|
-
? seed?.headSha ??
|
|
432
|
-
(prepared.plan.kind === "git-volume" ? prepared.plan.headSha : null)
|
|
433
|
-
: previous?.seedSha ??
|
|
434
|
-
(prepared.plan.kind === "git-volume" ? prepared.plan.headSha : null);
|
|
435
|
-
currentRequestInfo = infoFor(
|
|
436
|
-
request,
|
|
437
|
-
prepared.plan,
|
|
438
|
-
prepared,
|
|
439
|
-
seedBranch,
|
|
440
|
-
seedSha,
|
|
441
|
-
createdAt,
|
|
442
|
-
name,
|
|
443
|
-
preparation,
|
|
444
|
-
);
|
|
445
|
-
|
|
446
|
-
// Bundle cleanup is deliberately before state publication: a successful
|
|
447
|
-
// boot never publishes a state whose temporary host artifact leaked.
|
|
448
|
-
if (prepared.bundle) await prepared.bundle.cleanup();
|
|
449
|
-
prepared.bundle = null;
|
|
368
|
+
currentRequestInfo = infoFor(request, createdAt, name, preparation);
|
|
450
369
|
|
|
451
370
|
const persisted: PersistedSandboxState = {
|
|
452
371
|
version: STATE_SCHEMA_VERSION,
|
|
453
372
|
sessionId: request.sessionId,
|
|
454
373
|
sandboxName: name,
|
|
455
|
-
mode: currentRequestInfo.mode,
|
|
456
374
|
cwd: request.cwd,
|
|
375
|
+
root: request.workspace.hostRoot,
|
|
376
|
+
guestRoot: request.workspace.guestRoot,
|
|
457
377
|
image: request.config.image,
|
|
458
|
-
volumeName: currentRequestInfo.volumeName,
|
|
459
|
-
volumeHostPath: currentRequestInfo.volumeHostPath,
|
|
460
|
-
seedBranch: currentRequestInfo.seedBranch,
|
|
461
|
-
seedSha: currentRequestInfo.seedSha,
|
|
462
378
|
enabled: true,
|
|
463
379
|
createdAt,
|
|
464
380
|
};
|
|
465
381
|
deps.persist(persisted);
|
|
466
382
|
retainedState = persisted;
|
|
467
|
-
activePlan = prepared.plan;
|
|
468
383
|
state = { status: "active", info: currentRequestInfo };
|
|
469
384
|
return state;
|
|
470
385
|
} catch (error) {
|
|
471
|
-
if (prepared?.bundle) {
|
|
472
|
-
try {
|
|
473
|
-
await prepared.bundle.cleanup();
|
|
474
|
-
} catch {
|
|
475
|
-
// The primary boot error is more useful, and no secret is exposed.
|
|
476
|
-
}
|
|
477
|
-
prepared.bundle = null;
|
|
478
|
-
}
|
|
479
386
|
await disposeRuntime();
|
|
480
387
|
if (partialSandbox && sandboxName) {
|
|
481
388
|
try {
|
|
@@ -486,7 +393,6 @@ export function createSandboxManager(deps: SandboxManagerDeps): SandboxManager {
|
|
|
486
393
|
}
|
|
487
394
|
runtime = null;
|
|
488
395
|
bootedRuntime = null;
|
|
489
|
-
activePlan = null;
|
|
490
396
|
sandboxName = null;
|
|
491
397
|
if (lock === owner) {
|
|
492
398
|
lock = null;
|
|
@@ -502,7 +408,7 @@ export function createSandboxManager(deps: SandboxManagerDeps): SandboxManager {
|
|
|
502
408
|
}
|
|
503
409
|
|
|
504
410
|
async function wakeIfNeeded(): Promise<RuntimeExecution> {
|
|
505
|
-
if (state.status !== "active" || !runtime || !lastRequest || !sandboxName
|
|
411
|
+
if (state.status !== "active" || !runtime || !lastRequest || !sandboxName) {
|
|
506
412
|
throw unavailableError(state);
|
|
507
413
|
}
|
|
508
414
|
|
|
@@ -517,9 +423,9 @@ export function createSandboxManager(deps: SandboxManagerDeps): SandboxManager {
|
|
|
517
423
|
throw new Error(`sandbox ${sandboxName} has conflicting labels`);
|
|
518
424
|
}
|
|
519
425
|
// Revalidate the complete managed runtime identity before waking or
|
|
520
|
-
// reconnecting. A same-name resource with changed
|
|
521
|
-
//
|
|
522
|
-
if (!
|
|
426
|
+
// reconnecting. A same-name resource with changed cwd/root/image is not
|
|
427
|
+
// safe to attach to.
|
|
428
|
+
if (!matchesWorkspace(inspected, lastRequest)) {
|
|
523
429
|
throw new Error(`sandbox ${sandboxName} configuration changed`);
|
|
524
430
|
}
|
|
525
431
|
|
|
@@ -564,7 +470,6 @@ export function createSandboxManager(deps: SandboxManagerDeps): SandboxManager {
|
|
|
564
470
|
}
|
|
565
471
|
const failedName = sandboxName;
|
|
566
472
|
sandboxName = null;
|
|
567
|
-
activePlan = null;
|
|
568
473
|
if (failedName) {
|
|
569
474
|
try {
|
|
570
475
|
await deps.stopAndRemove(failedName, lastRequest.config.stopTimeoutMs);
|
|
@@ -611,7 +516,6 @@ export function createSandboxManager(deps: SandboxManagerDeps): SandboxManager {
|
|
|
611
516
|
}
|
|
612
517
|
|
|
613
518
|
runtime = null;
|
|
614
|
-
activePlan = null;
|
|
615
519
|
sandboxName = null;
|
|
616
520
|
|
|
617
521
|
if (persistDisabled && request && oldInfo) {
|
|
@@ -620,13 +524,10 @@ export function createSandboxManager(deps: SandboxManagerDeps): SandboxManager {
|
|
|
620
524
|
version: STATE_SCHEMA_VERSION,
|
|
621
525
|
sessionId: request.sessionId,
|
|
622
526
|
sandboxName: oldInfo.name,
|
|
623
|
-
mode: oldInfo.mode,
|
|
624
527
|
cwd: oldInfo.cwd,
|
|
528
|
+
root: request.workspace.hostRoot,
|
|
529
|
+
guestRoot: request.workspace.guestRoot,
|
|
625
530
|
image: oldInfo.image,
|
|
626
|
-
volumeName: oldInfo.volumeName,
|
|
627
|
-
volumeHostPath: oldInfo.volumeHostPath,
|
|
628
|
-
seedBranch: oldInfo.seedBranch,
|
|
629
|
-
seedSha: oldInfo.seedSha,
|
|
630
531
|
enabled: false,
|
|
631
532
|
createdAt,
|
|
632
533
|
};
|
|
@@ -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
|
-
|
|
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
|
-
"
|
|
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
|
|
126
|
-
return state.status === "off" || state.status === "
|
|
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
|
-
//
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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,
|