pi-microsandbox 0.1.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.
@@ -0,0 +1,436 @@
1
+ /** Shared pi-microsandbox contracts. */
2
+ import { createHash } from "node:crypto";
3
+ import type {
4
+ BashOperations,
5
+ EditOperations,
6
+ FindOperations,
7
+ GrepOperations,
8
+ LsOperations,
9
+ ReadOperations,
10
+ WriteOperations,
11
+ } from "@earendil-works/pi-coding-agent";
12
+
13
+ // --------------------------------------------------------------- identity ----
14
+
15
+ export const STATE_SCHEMA_VERSION = 1;
16
+ export const LOCKFILE_VERSION = 1;
17
+
18
+ export function resourceId(sessionId: string): string {
19
+ return createHash("sha256").update(sessionId).digest("hex").slice(0, 20);
20
+ }
21
+ export function displayId(sessionId: string): string {
22
+ return resourceId(sessionId).slice(0, 6);
23
+ }
24
+ export function sandboxNameFor(sessionId: string): string {
25
+ return `pi-msb-${resourceId(sessionId)}`;
26
+ }
27
+ export function volumeNameFor(sessionId: string): string {
28
+ return `pi-msb-vol-${resourceId(sessionId)}`;
29
+ }
30
+
31
+ // ---------------------------------------------------------------- config ----
32
+
33
+ export type StorageMode = "git" | "direct" | "none";
34
+ export type ConfigStorageMode = "auto" | StorageMode;
35
+ export type NetworkMode = "default" | "open" | "allowlist" | "deny";
36
+ export type FallbackMode = "block" | "host";
37
+ export type BootstrapTools = "auto" | boolean;
38
+ export type PullPolicy = "always" | "if-missing" | "never";
39
+ export type MountType = "dir" | "file" | "named" | "tmpfs";
40
+
41
+ export interface NetworkConfig {
42
+ mode: NetworkMode;
43
+ allowHosts: string[];
44
+ allowDns: boolean;
45
+ publishPorts: string[];
46
+ }
47
+ export interface SecretConfig {
48
+ env: string;
49
+ value: string;
50
+ allowHosts: string[];
51
+ }
52
+ export interface MountConfig {
53
+ type: MountType;
54
+ hostPath?: string;
55
+ guestPath?: string;
56
+ readonly: boolean;
57
+ options: string[];
58
+ }
59
+ export interface Config {
60
+ image: string;
61
+ pullPolicy: PullPolicy;
62
+ bootstrapTools: BootstrapTools;
63
+ cpus: number;
64
+ memoryMiB: number;
65
+ idleTimeoutSec: number;
66
+ stopTimeoutMs: number;
67
+ detached: boolean;
68
+ replace: boolean;
69
+ replaceTimeoutMs: number;
70
+ sandboxName: string | null;
71
+ mode: ConfigStorageMode;
72
+ cloneBranch: "current" | string;
73
+ cloneDepth: number | "unlimited";
74
+ shallowArchive: boolean;
75
+ volumeQuotaMiB: number;
76
+ network: NetworkConfig;
77
+ secrets: SecretConfig[];
78
+ mounts: MountConfig[];
79
+ blockThirdParty: boolean;
80
+ routeTools: string[];
81
+ passThroughTools: string[];
82
+ allowHostExecution: boolean;
83
+ allowSkillReads: boolean;
84
+ fallbackMode: FallbackMode;
85
+ exposeSessionEnvironment: boolean;
86
+ hostEnv: string[];
87
+ autoStart: boolean;
88
+ pruneOnStart: boolean;
89
+ showFooter: boolean;
90
+ lockDir: string;
91
+ hostRoAllowlist: string[];
92
+ }
93
+ export type DeepPartial<T> = {
94
+ [K in keyof T]?: T[K] extends Array<infer U>
95
+ ? Array<DeepPartial<U>> | Array<U>
96
+ : T[K] extends object
97
+ ? DeepPartial<T[K]>
98
+ : T[K];
99
+ };
100
+ export type DeepReadonly<T> = T extends (...args: any[]) => unknown
101
+ ? T
102
+ : T extends readonly (infer U)[]
103
+ ? readonly DeepReadonly<U>[]
104
+ : T extends object
105
+ ? { readonly [K in keyof T]: DeepReadonly<T[K]> }
106
+ : T;
107
+ export type ConfigLayerName = "defaults" | "global" | "project" | "env" | "cli";
108
+ export interface ParsedConfigLayer {
109
+ name: ConfigLayerName;
110
+ value: DeepPartial<Config>;
111
+ warnings: string[];
112
+ source?: string;
113
+ }
114
+ export type ConfigLayerInput = Omit<ParsedConfigLayer, "value"> & {
115
+ value: DeepPartial<Config> | DeepReadonly<Config>;
116
+ };
117
+ export interface ResolvedConfig {
118
+ config: Config;
119
+ provenance: Record<string, ConfigLayerName>;
120
+ warnings: string[];
121
+ }
122
+ export interface MergeResult {
123
+ value: DeepPartial<Config>;
124
+ provenance: Record<string, ConfigLayerName>;
125
+ warnings: string[];
126
+ }
127
+
128
+ // ---------------------------------------------------------- labels/state ----
129
+
130
+ export const LABEL_KEYS = {
131
+ managed: "pi-msb.managed",
132
+ schema: "pi-msb.schema",
133
+ session: "pi-msb.session",
134
+ mode: "pi-msb.mode",
135
+ cwd: "pi-msb.cwd",
136
+ pid: "pi-msb.pid",
137
+ volume: "pi-msb.volume",
138
+ image: "pi-msb.image",
139
+ keep: "pi-msb.keep",
140
+ seedBranch: "pi-msb.seed-branch",
141
+ seedSha: "pi-msb.seed-sha",
142
+ } as const;
143
+
144
+ export interface SandboxLabelInput {
145
+ sessionId: string;
146
+ mode: StorageMode;
147
+ cwd: string;
148
+ pid: number;
149
+ image: string;
150
+ volumeName?: string;
151
+ seedBranch?: string | null;
152
+ seedSha?: string | null;
153
+ }
154
+ export interface VolumeLabelInput {
155
+ sessionId: string;
156
+ cwd: string;
157
+ seedBranch?: string | null;
158
+ seedSha?: string | null;
159
+ }
160
+ export interface ManagedSandboxRecord {
161
+ name: string;
162
+ status?: string;
163
+ labels: Record<string, string>;
164
+ createdAt?: number;
165
+ }
166
+ export interface ValidatedManagedSandbox extends ManagedSandboxRecord {
167
+ sessionId: string;
168
+ mode: StorageMode;
169
+ cwd: string;
170
+ volumeName?: string;
171
+ }
172
+ export interface VolumeRecord {
173
+ name: string;
174
+ hostPath: string;
175
+ labels: Record<string, string>;
176
+ kind?: string;
177
+ usedBytes?: number;
178
+ createdAt?: number;
179
+ }
180
+ export interface PersistedSandboxState {
181
+ version: typeof STATE_SCHEMA_VERSION;
182
+ sessionId: string;
183
+ sandboxName: string;
184
+ mode: StorageMode;
185
+ cwd: string;
186
+ image: string;
187
+ volumeName?: string;
188
+ volumeHostPath?: string;
189
+ seedBranch?: string | null;
190
+ seedSha?: string | null;
191
+ enabled: boolean;
192
+ createdAt: number;
193
+ }
194
+
195
+ // ----------------------------------------------------------------- locks ----
196
+
197
+ export interface LockInfo {
198
+ version: typeof LOCKFILE_VERSION;
199
+ sessionId: string;
200
+ sandboxName: string;
201
+ volumeName?: string;
202
+ mode: StorageMode;
203
+ cwd: string;
204
+ pid: number;
205
+ createdAt: number;
206
+ }
207
+ export interface LockHandle {
208
+ readonly path: string;
209
+ release(): Promise<void>;
210
+ }
211
+ export interface LocksPort {
212
+ tryAcquire(sessionId: string): Promise<LockHandle | null>;
213
+ }
214
+
215
+ // ------------------------------------------------------------- git/storage ----
216
+
217
+ export interface GitRepoInfo {
218
+ isGitRepo: boolean;
219
+ /** Canonical host source for direct binds; guest paths remain lexical. */
220
+ hostCwd?: string;
221
+ /** Canonical host source used only for Git reads and seed bundle creation. */
222
+ repoRoot: string | null;
223
+ /** Lexical guest namespace where the retained volume must be mounted. */
224
+ guestRepoRoot?: string | null;
225
+ branch: string | null;
226
+ headSha: string | null;
227
+ unborn: boolean;
228
+ isLinkedWorktree: boolean;
229
+ }
230
+ export interface GitSeedBundle {
231
+ hostPath: string;
232
+ branch: string | null;
233
+ headSha: string;
234
+ cleanup(): Promise<void>;
235
+ }
236
+ export interface GitVolumePlan {
237
+ kind: "git-volume";
238
+ sessionId: string;
239
+ volumeName: string;
240
+ volumeQuotaMiB: number;
241
+ repoRoot: string;
242
+ mountGuestPath: string;
243
+ workdir: string;
244
+ branch: string | null;
245
+ headSha: string | null;
246
+ unborn: boolean;
247
+ depth: number | "unlimited";
248
+ seedRequired: boolean;
249
+ }
250
+ export type StoragePlan =
251
+ | GitVolumePlan
252
+ | { kind: "direct-mount"; hostPath: string; guestPath: string; workdir: string }
253
+ | { kind: "none"; guestPath: string; workdir: string };
254
+ export interface PreparedStorage {
255
+ plan: StoragePlan;
256
+ volume?: VolumeRecord;
257
+ bundle?: GitSeedBundle | null;
258
+ createdVolume: boolean;
259
+ }
260
+ export interface SeedResult { headSha: string | null }
261
+
262
+ // ------------------------------------------------------------- transport ----
263
+
264
+ export type TransportErrorCode =
265
+ | "NOT_FOUND"
266
+ | "ACCESS"
267
+ | "TIMEOUT"
268
+ | "ABORTED"
269
+ | "SANDBOX_DOWN"
270
+ | "INVALID"
271
+ | "IO"
272
+ | "UNKNOWN";
273
+ export interface TransportError extends Error {
274
+ readonly code: TransportErrorCode;
275
+ readonly cause?: unknown;
276
+ }
277
+ export type EntryKind = "file" | "directory" | "other";
278
+ export interface FsEntry { name: string; kind: EntryKind }
279
+ export interface StatResult {
280
+ kind: EntryKind;
281
+ size: number;
282
+ mode: number;
283
+ readonly: boolean;
284
+ modifiedAt: number | null;
285
+ }
286
+ export interface TransportExecResult {
287
+ stdout: Buffer;
288
+ stderr: Buffer;
289
+ exitCode: number;
290
+ }
291
+ export interface ExecOptions { cwd?: string; timeoutMs?: number }
292
+ export interface ExecStreamOptions extends ExecOptions {
293
+ signal?: AbortSignal;
294
+ onStdout?: (data: Buffer) => void;
295
+ onStderr?: (data: Buffer) => void;
296
+ }
297
+ export interface SandboxTransport {
298
+ readFile(path: string): Promise<Buffer>;
299
+ writeFile(path: string, data: string | Buffer): Promise<void>;
300
+ exists(path: string): Promise<boolean>;
301
+ stat(path: string): Promise<StatResult>;
302
+ list(path: string): Promise<FsEntry[]>;
303
+ copyFromHost(hostPath: string, guestPath: string): Promise<void>;
304
+ copyToHost(guestPath: string, hostPath: string): Promise<void>;
305
+ exec(command: string, args: string[], options?: ExecOptions): Promise<TransportExecResult>;
306
+ execStream(command: string, args: string[], options?: ExecStreamOptions): Promise<{ exitCode: number }>;
307
+ dispose(): Promise<void>;
308
+ }
309
+
310
+ // --------------------------------------------------------------- tool ops ----
311
+
312
+ export interface ToolOperations {
313
+ read: ReadOperations;
314
+ write: WriteOperations;
315
+ edit: EditOperations;
316
+ bash: BashOperations;
317
+ ls: LsOperations;
318
+ find: FindOperations;
319
+ grep: GrepOperations;
320
+ }
321
+ export interface RuntimeExecution {
322
+ transport: SandboxTransport;
323
+ operations: ToolOperations;
324
+ }
325
+ export interface ToolOpsProvider {
326
+ isActive(): boolean;
327
+ getState(): RuntimeState;
328
+ withRuntime<T>(callback: (runtime: RuntimeExecution) => Promise<T>): Promise<T>;
329
+ }
330
+ export interface GrepFormattingHelpers {
331
+ DEFAULT_MAX_BYTES: number;
332
+ DEFAULT_MAX_LINES: number;
333
+ truncateHead: (content: string, options?: unknown) => any;
334
+ truncateLine: (line: string, maxChars?: number) => { text: string; wasTruncated: boolean };
335
+ formatSize: (bytes: number) => string;
336
+ }
337
+ export type SandboxGrepExecute = (
338
+ id: string,
339
+ params: Record<string, any>,
340
+ signal?: AbortSignal,
341
+ onUpdate?: (value: any) => void,
342
+ ) => Promise<any>;
343
+
344
+ // ----------------------------------------------------------- host access ----
345
+
346
+ export type ExecutionTarget = "sandbox" | "host";
347
+ export interface DiscoveredSkillPath { filePath: string; baseDir: string }
348
+ export interface HostReadAccess {
349
+ updateSkills(skills: readonly DiscoveredSkillPath[]): void;
350
+ allowGeneratedFile(path: string): Promise<void>;
351
+ resolve(requestedPath: string, cwd: string): Promise<string | undefined>;
352
+ clear(): void;
353
+ }
354
+
355
+ // -------------------------------------------------------- manager/runtime ----
356
+
357
+ export type RuntimeStatus =
358
+ | "booting"
359
+ | "active"
360
+ | "stopping"
361
+ | "unavailable"
362
+ | "off"
363
+ | "host-fallback"
364
+ | "disabled";
365
+ export interface SandboxInfo {
366
+ name: string;
367
+ displayId: string;
368
+ mode: StorageMode;
369
+ image: string;
370
+ pid: number;
371
+ cwd: string;
372
+ volumeName?: string;
373
+ volumeHostPath?: string;
374
+ seedBranch?: string | null;
375
+ seedSha?: string | null;
376
+ createdAt: number;
377
+ }
378
+ export interface RuntimeState {
379
+ status: RuntimeStatus;
380
+ info: SandboxInfo | null;
381
+ reason?: string;
382
+ }
383
+ export interface BootRequest {
384
+ sessionId: string;
385
+ cwd: string;
386
+ config: Config;
387
+ restored: PersistedSandboxState | null;
388
+ }
389
+ export interface SandboxManager extends ToolOpsProvider {
390
+ boot(request: BootRequest): Promise<RuntimeState>;
391
+ shutdown(): Promise<void>;
392
+ setEnabled(enabled: boolean): Promise<RuntimeState>;
393
+ }
394
+
395
+ // ------------------------------------------------------------------ prune ----
396
+
397
+ export interface PruneReport {
398
+ inspected: number;
399
+ removed: string[];
400
+ kept: string[];
401
+ errors: string[];
402
+ }
403
+
404
+ // --------------------------------------------------------------- host exec ----
405
+
406
+ export type ExecFn = (
407
+ command: string,
408
+ args: string[],
409
+ options?: { cwd?: string; timeout?: number; env?: NodeJS.ProcessEnv },
410
+ ) => Promise<{ stdout: string; stderr: string; code: number; killed?: boolean }>;
411
+
412
+ // ------------------------------------------------------- command/control ----
413
+
414
+ export interface ExportResult { source: string; destination: string }
415
+ export interface MsbControl {
416
+ getState(): RuntimeState;
417
+ setEnabled(enabled: boolean): Promise<void>;
418
+ reload(): Promise<void>;
419
+ pruneNow(): Promise<PruneReport>;
420
+ listVolumes(): Promise<VolumeRecord[]>;
421
+ describeVolume(name: string): Promise<{
422
+ volume: VolumeRecord;
423
+ branch?: string;
424
+ lastCommit?: string;
425
+ dirtyCount?: number;
426
+ mounted: boolean;
427
+ }>;
428
+ removeVolume(name: string): Promise<void>;
429
+ exportPaths(paths: string[], destination?: string): Promise<ExportResult[]>;
430
+ getLogs(tailLines?: number): Promise<string>;
431
+ getEffectiveConfig(): ResolvedConfig;
432
+ getEffectiveConfigToml(): string;
433
+ setOverride(dottedSnakeKey: string, value: unknown): Promise<void>;
434
+ unsetOverride(dottedSnakeKey: string): Promise<void>;
435
+ resetOverrides(): Promise<void>;
436
+ }
package/package.json ADDED
@@ -0,0 +1,74 @@
1
+ {
2
+ "name": "pi-microsandbox",
3
+ "version": "0.1.0",
4
+ "description": "Microsandbox-backed isolation for Pi coding tools",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/hcohe/pi-microsandbox.git"
10
+ },
11
+ "homepage": "https://github.com/hcohe/pi-microsandbox#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/hcohe/pi-microsandbox/issues"
14
+ },
15
+ "publishConfig": {
16
+ "access": "public",
17
+ "registry": "https://registry.npmjs.org/"
18
+ },
19
+ "packageManager": "npm@11.17.0",
20
+ "keywords": [
21
+ "pi-package",
22
+ "microsandbox",
23
+ "sandbox"
24
+ ],
25
+ "engines": {
26
+ "node": ">=22.19.0"
27
+ },
28
+ "os": [
29
+ "darwin",
30
+ "linux"
31
+ ],
32
+ "files": [
33
+ "SECURITY.md",
34
+ "docs/*.md",
35
+ "extensions/pi-msb/*.ts",
36
+ "!extensions/pi-msb/*.test.ts"
37
+ ],
38
+ "pi": {
39
+ "extensions": [
40
+ "./extensions/pi-msb/index.ts"
41
+ ]
42
+ },
43
+ "scripts": {
44
+ "typecheck": "tsc --noEmit",
45
+ "test": "node --experimental-strip-types --import ./test/setup.ts --test extensions/pi-msb/*.test.ts",
46
+ "smoke": "pi --no-extensions -e . --offline --list-models __pi_msb_smoke__",
47
+ "check": "npm run typecheck && npm test && npm run smoke",
48
+ "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"
51
+ },
52
+ "dependencies": {
53
+ "fs-ext": "2.1.1",
54
+ "microsandbox": "0.6.16"
55
+ },
56
+ "peerDependencies": {
57
+ "@earendil-works/pi-coding-agent": "*",
58
+ "@earendil-works/pi-tui": "*",
59
+ "typebox": "*"
60
+ },
61
+ "devDependencies": {
62
+ "@earendil-works/pi-coding-agent": "0.84.4",
63
+ "@earendil-works/pi-tui": "0.84.4",
64
+ "@types/fs-ext": "^2.0.3",
65
+ "@types/node": "^22.10.0",
66
+ "typebox": "1.3.23",
67
+ "typescript": "^7.0.2"
68
+ },
69
+ "allowScripts": {
70
+ "fs-ext@2.1.1": true,
71
+ "@google/genai": false,
72
+ "protobufjs": false
73
+ }
74
+ }