pi-microsandbox 0.1.0 → 0.2.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 +21 -16
- package/docs/commands.md +3 -2
- package/docs/configuration.md +35 -4
- package/docs/development.md +96 -8
- package/docs/getting-started.md +25 -21
- package/docs/images.md +16 -8
- package/docs/safety.md +25 -5
- package/docs/storage.md +14 -0
- package/docs/troubleshooting.md +24 -0
- package/extensions/pi-msb/command.ts +5 -0
- package/extensions/pi-msb/config.ts +65 -6
- package/extensions/pi-msb/control.ts +95 -14
- package/extensions/pi-msb/flock.ts +94 -0
- package/extensions/pi-msb/locks.ts +5 -84
- package/extensions/pi-msb/sandbox-manager.ts +56 -7
- package/extensions/pi-msb/types.ts +18 -0
- package/native/flock/prebuilds/darwin-arm64/flock.node +0 -0
- package/native/flock/prebuilds/linux-arm64-gnu/flock.node +0 -0
- package/native/flock/prebuilds/linux-x64-gnu/flock.node +0 -0
- package/package.json +10 -7
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
type PreparedStorage,
|
|
11
11
|
type PruneReport,
|
|
12
12
|
type RuntimeExecution,
|
|
13
|
+
type RuntimePreparation,
|
|
13
14
|
type RuntimeState,
|
|
14
15
|
type SandboxManager,
|
|
15
16
|
type SandboxTransport,
|
|
@@ -55,7 +56,7 @@ export interface SandboxManagerDeps {
|
|
|
55
56
|
stopAndRemove(name: string, timeoutMs: number): Promise<void>;
|
|
56
57
|
createTransport(raw: unknown): SandboxTransport;
|
|
57
58
|
createOperations(transport: SandboxTransport): ToolOperations;
|
|
58
|
-
|
|
59
|
+
prepareRuntime(runtime: RuntimeExecution, config: Config): Promise<RuntimePreparation>;
|
|
59
60
|
seed(runtime: RuntimeExecution, prepared: PreparedStorage): Promise<SeedResult>;
|
|
60
61
|
persist(state: PersistedSandboxState): void;
|
|
61
62
|
now?: () => number;
|
|
@@ -136,6 +137,7 @@ function infoFor(
|
|
|
136
137
|
seedSha: string | null | undefined,
|
|
137
138
|
createdAt: number,
|
|
138
139
|
name: string,
|
|
140
|
+
preparation: RuntimePreparation,
|
|
139
141
|
): NonNullable<RuntimeState["info"]> {
|
|
140
142
|
return {
|
|
141
143
|
name,
|
|
@@ -149,6 +151,7 @@ function infoFor(
|
|
|
149
151
|
seedBranch: seedBranch ?? null,
|
|
150
152
|
seedSha: seedSha ?? null,
|
|
151
153
|
createdAt,
|
|
154
|
+
docker: { ...preparation.docker },
|
|
152
155
|
};
|
|
153
156
|
}
|
|
154
157
|
|
|
@@ -192,6 +195,7 @@ export function createSandboxManager(deps: SandboxManagerDeps): SandboxManager {
|
|
|
192
195
|
let state: RuntimeState = { status: "disabled", info: null };
|
|
193
196
|
let lock: LockHandle | null = null;
|
|
194
197
|
let runtime: RuntimeExecution | null = null;
|
|
198
|
+
let runtimeInvalid = false;
|
|
195
199
|
let sandboxName: string | null = null;
|
|
196
200
|
let lastRequest: BootRequest | null = null;
|
|
197
201
|
let retainedState: PersistedSandboxState | null = null;
|
|
@@ -240,6 +244,7 @@ export function createSandboxManager(deps: SandboxManagerDeps): SandboxManager {
|
|
|
240
244
|
async function disposeRuntime(): Promise<void> {
|
|
241
245
|
const current = runtime;
|
|
242
246
|
runtime = null;
|
|
247
|
+
runtimeInvalid = false;
|
|
243
248
|
if (!current) return;
|
|
244
249
|
try {
|
|
245
250
|
await current.transport.dispose();
|
|
@@ -285,6 +290,12 @@ export function createSandboxManager(deps: SandboxManagerDeps): SandboxManager {
|
|
|
285
290
|
return request.config.sandboxName ?? sandboxNameFor(request.sessionId);
|
|
286
291
|
}
|
|
287
292
|
|
|
293
|
+
function invalidatesRuntime(error: unknown): boolean {
|
|
294
|
+
if (!error || typeof error !== "object" || !("code" in error)) return false;
|
|
295
|
+
const code = (error as { code?: unknown }).code;
|
|
296
|
+
return code === "SANDBOX_DOWN";
|
|
297
|
+
}
|
|
298
|
+
|
|
288
299
|
async function connectAndBuild(raw: unknown): Promise<RuntimeExecution> {
|
|
289
300
|
const transport = deps.createTransport(raw);
|
|
290
301
|
try {
|
|
@@ -396,7 +407,8 @@ export function createSandboxManager(deps: SandboxManagerDeps): SandboxManager {
|
|
|
396
407
|
|
|
397
408
|
bootedRuntime = await connectAndBuild(raw);
|
|
398
409
|
runtime = bootedRuntime;
|
|
399
|
-
|
|
410
|
+
runtimeInvalid = false;
|
|
411
|
+
const preparation = await deps.prepareRuntime(bootedRuntime, request.config);
|
|
400
412
|
|
|
401
413
|
let seed: SeedResult | null = null;
|
|
402
414
|
if (
|
|
@@ -428,6 +440,7 @@ export function createSandboxManager(deps: SandboxManagerDeps): SandboxManager {
|
|
|
428
440
|
seedSha,
|
|
429
441
|
createdAt,
|
|
430
442
|
name,
|
|
443
|
+
preparation,
|
|
431
444
|
);
|
|
432
445
|
|
|
433
446
|
// Bundle cleanup is deliberately before state publication: a successful
|
|
@@ -510,16 +523,16 @@ export function createSandboxManager(deps: SandboxManagerDeps): SandboxManager {
|
|
|
510
523
|
throw new Error(`sandbox ${sandboxName} configuration changed`);
|
|
511
524
|
}
|
|
512
525
|
|
|
513
|
-
if (isRunning(inspected)) {
|
|
526
|
+
if (isRunning(inspected) && !runtimeInvalid) {
|
|
514
527
|
// The boot-created transport remains authoritative while the sandbox is
|
|
515
528
|
// running. Reconnecting here would replace a valid handle and dispose a
|
|
516
529
|
// transport that may still have active callers.
|
|
517
530
|
return runtime;
|
|
518
531
|
}
|
|
519
532
|
|
|
520
|
-
// A stopped/unknown sandbox
|
|
521
|
-
// start
|
|
522
|
-
// its runtime-use reservation.
|
|
533
|
+
// A stopped/unknown sandbox or invalid running handle requires a
|
|
534
|
+
// replacement transport. Do not start or dispose the old one until every
|
|
535
|
+
// earlier callback has released its runtime-use reservation.
|
|
523
536
|
await waitForActiveUses();
|
|
524
537
|
let raw: unknown;
|
|
525
538
|
if (isStopped(inspected)) {
|
|
@@ -530,8 +543,41 @@ export function createSandboxManager(deps: SandboxManagerDeps): SandboxManager {
|
|
|
530
543
|
}
|
|
531
544
|
|
|
532
545
|
const replacement = await connectAndBuild(raw);
|
|
546
|
+
let preparation: RuntimePreparation;
|
|
547
|
+
try {
|
|
548
|
+
preparation = await deps.prepareRuntime(replacement, lastRequest.config);
|
|
549
|
+
} catch (error) {
|
|
550
|
+
try {
|
|
551
|
+
await replacement.transport.dispose();
|
|
552
|
+
} catch {
|
|
553
|
+
// Preserve the preparation error.
|
|
554
|
+
}
|
|
555
|
+
const previous = runtime;
|
|
556
|
+
runtime = null;
|
|
557
|
+
runtimeInvalid = false;
|
|
558
|
+
if (previous) {
|
|
559
|
+
try {
|
|
560
|
+
await previous.transport.dispose();
|
|
561
|
+
} catch {
|
|
562
|
+
// The preparation error remains authoritative.
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
const failedName = sandboxName;
|
|
566
|
+
sandboxName = null;
|
|
567
|
+
activePlan = null;
|
|
568
|
+
if (failedName) {
|
|
569
|
+
try {
|
|
570
|
+
await deps.stopAndRemove(failedName, lastRequest.config.stopTimeoutMs);
|
|
571
|
+
} catch {
|
|
572
|
+
throw new Error(`${redactMessage(error, lastRequest.config)}; failed to clean up the restarted sandbox`);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
throw error;
|
|
576
|
+
}
|
|
533
577
|
const previous = runtime;
|
|
534
578
|
runtime = replacement;
|
|
579
|
+
runtimeInvalid = false;
|
|
580
|
+
if (state.info) state = { ...state, info: { ...state.info, docker: { ...preparation.docker } } };
|
|
535
581
|
if (previous && previous !== replacement) {
|
|
536
582
|
try {
|
|
537
583
|
await previous.transport.dispose();
|
|
@@ -621,7 +667,7 @@ export function createSandboxManager(deps: SandboxManagerDeps): SandboxManager {
|
|
|
621
667
|
getState(): RuntimeState {
|
|
622
668
|
return {
|
|
623
669
|
status: state.status,
|
|
624
|
-
info: state.info ? { ...state.info } : null,
|
|
670
|
+
info: state.info ? { ...state.info, docker: { ...state.info.docker } } : null,
|
|
625
671
|
...(state.reason ? { reason: state.reason } : {}),
|
|
626
672
|
};
|
|
627
673
|
},
|
|
@@ -663,6 +709,9 @@ export function createSandboxManager(deps: SandboxManagerDeps): SandboxManager {
|
|
|
663
709
|
}
|
|
664
710
|
try {
|
|
665
711
|
return await callback(current);
|
|
712
|
+
} catch (error) {
|
|
713
|
+
if (runtime === current && invalidatesRuntime(error)) runtimeInvalid = true;
|
|
714
|
+
throw error;
|
|
666
715
|
} finally {
|
|
667
716
|
releaseUse(true);
|
|
668
717
|
}
|
|
@@ -37,7 +37,12 @@ export type FallbackMode = "block" | "host";
|
|
|
37
37
|
export type BootstrapTools = "auto" | boolean;
|
|
38
38
|
export type PullPolicy = "always" | "if-missing" | "never";
|
|
39
39
|
export type MountType = "dir" | "file" | "named" | "tmpfs";
|
|
40
|
+
export type DockerMode = "auto" | "require" | "disabled";
|
|
40
41
|
|
|
42
|
+
export interface DockerConfig {
|
|
43
|
+
mode: DockerMode;
|
|
44
|
+
startupTimeoutMs: number;
|
|
45
|
+
}
|
|
41
46
|
export interface NetworkConfig {
|
|
42
47
|
mode: NetworkMode;
|
|
43
48
|
allowHosts: string[];
|
|
@@ -74,6 +79,7 @@ export interface Config {
|
|
|
74
79
|
shallowArchive: boolean;
|
|
75
80
|
volumeQuotaMiB: number;
|
|
76
81
|
network: NetworkConfig;
|
|
82
|
+
docker: DockerConfig;
|
|
77
83
|
secrets: SecretConfig[];
|
|
78
84
|
mounts: MountConfig[];
|
|
79
85
|
blockThirdParty: boolean;
|
|
@@ -362,6 +368,17 @@ export type RuntimeStatus =
|
|
|
362
368
|
| "off"
|
|
363
369
|
| "host-fallback"
|
|
364
370
|
| "disabled";
|
|
371
|
+
export type DockerReadiness = "ready" | "missing" | "unavailable" | "disabled";
|
|
372
|
+
export interface DockerCapabilityStatus {
|
|
373
|
+
mode: DockerMode;
|
|
374
|
+
readiness: DockerReadiness;
|
|
375
|
+
version?: string;
|
|
376
|
+
storageDriver?: string;
|
|
377
|
+
reason?: string;
|
|
378
|
+
}
|
|
379
|
+
export interface RuntimePreparation {
|
|
380
|
+
docker: DockerCapabilityStatus;
|
|
381
|
+
}
|
|
365
382
|
export interface SandboxInfo {
|
|
366
383
|
name: string;
|
|
367
384
|
displayId: string;
|
|
@@ -374,6 +391,7 @@ export interface SandboxInfo {
|
|
|
374
391
|
seedBranch?: string | null;
|
|
375
392
|
seedSha?: string | null;
|
|
376
393
|
createdAt: number;
|
|
394
|
+
docker: DockerCapabilityStatus;
|
|
377
395
|
}
|
|
378
396
|
export interface RuntimeState {
|
|
379
397
|
status: RuntimeStatus;
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-microsandbox",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Microsandbox-backed isolation for Pi coding tools",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -33,7 +33,10 @@
|
|
|
33
33
|
"SECURITY.md",
|
|
34
34
|
"docs/*.md",
|
|
35
35
|
"extensions/pi-msb/*.ts",
|
|
36
|
-
"!extensions/pi-msb/*.test.ts"
|
|
36
|
+
"!extensions/pi-msb/*.test.ts",
|
|
37
|
+
"native/flock/prebuilds/darwin-arm64/flock.node",
|
|
38
|
+
"native/flock/prebuilds/linux-x64-gnu/flock.node",
|
|
39
|
+
"native/flock/prebuilds/linux-arm64-gnu/flock.node"
|
|
37
40
|
],
|
|
38
41
|
"pi": {
|
|
39
42
|
"extensions": [
|
|
@@ -41,16 +44,17 @@
|
|
|
41
44
|
]
|
|
42
45
|
},
|
|
43
46
|
"scripts": {
|
|
47
|
+
"build:flock": "node ./scripts/build-flock-addon.mjs",
|
|
48
|
+
"smoke:flock": "node ./scripts/smoke-flock-addon.mjs",
|
|
49
|
+
"assemble:package": "node ./scripts/assemble-package.mjs",
|
|
44
50
|
"typecheck": "tsc --noEmit",
|
|
45
51
|
"test": "node --experimental-strip-types --import ./test/setup.ts --test extensions/pi-msb/*.test.ts",
|
|
46
52
|
"smoke": "pi --no-extensions -e . --offline --list-models __pi_msb_smoke__",
|
|
47
53
|
"check": "npm run typecheck && npm test && npm run smoke",
|
|
48
54
|
"package-smoke": "./scripts/package-smoke.sh",
|
|
49
|
-
"release-check": "npm run check && npm
|
|
50
|
-
"prepublishOnly": "npm run release-check"
|
|
55
|
+
"release-check": "npm run check && npm audit --omit=dev"
|
|
51
56
|
},
|
|
52
57
|
"dependencies": {
|
|
53
|
-
"fs-ext": "2.1.1",
|
|
54
58
|
"microsandbox": "0.6.16"
|
|
55
59
|
},
|
|
56
60
|
"peerDependencies": {
|
|
@@ -61,13 +65,12 @@
|
|
|
61
65
|
"devDependencies": {
|
|
62
66
|
"@earendil-works/pi-coding-agent": "0.84.4",
|
|
63
67
|
"@earendil-works/pi-tui": "0.84.4",
|
|
64
|
-
"@types/fs-ext": "^2.0.3",
|
|
65
68
|
"@types/node": "^22.10.0",
|
|
69
|
+
"node-gyp": "12.1.0",
|
|
66
70
|
"typebox": "1.3.23",
|
|
67
71
|
"typescript": "^7.0.2"
|
|
68
72
|
},
|
|
69
73
|
"allowScripts": {
|
|
70
|
-
"fs-ext@2.1.1": true,
|
|
71
74
|
"@google/genai": false,
|
|
72
75
|
"protobufjs": false
|
|
73
76
|
}
|