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.
@@ -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
- probeAndBootstrap(runtime: RuntimeExecution, config: Config): Promise<void>;
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
- await deps.probeAndBootstrap(bootedRuntime, request.config);
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 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.
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-microsandbox",
3
- "version": "0.1.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 run package-smoke && npm audit --omit=dev",
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
  }