@nimbus-sh/core 0.6.0 → 0.8.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.
Files changed (96) hide show
  1. package/README.md +47 -26
  2. package/dist/_shared/byte-stream.d.ts +5 -4
  3. package/dist/_shared/byte-stream.d.ts.map +1 -1
  4. package/dist/_shared/real-node-imports.d.ts +7 -0
  5. package/dist/_shared/real-node-imports.d.ts.map +1 -1
  6. package/dist/_shared/real-node-imports.js +8 -0
  7. package/dist/_shared/tarball.d.ts +24 -5
  8. package/dist/_shared/tarball.d.ts.map +1 -1
  9. package/dist/_shared/tarball.js +42 -5
  10. package/dist/_shared/vfs-write-ledger.d.ts.map +1 -1
  11. package/dist/_shared/vfs-write-ledger.js +22 -16
  12. package/dist/runtime/comment-strip.d.ts +32 -29
  13. package/dist/runtime/comment-strip.d.ts.map +1 -1
  14. package/dist/runtime/comment-strip.js +273 -51
  15. package/dist/runtime/cpython-runner.d.ts +1 -0
  16. package/dist/runtime/cpython-runner.d.ts.map +1 -1
  17. package/dist/runtime/cpython-runner.js +1 -1
  18. package/dist/runtime/esbuild-service.d.ts +38 -42
  19. package/dist/runtime/esbuild-service.d.ts.map +1 -1
  20. package/dist/runtime/esbuild-service.js +500 -388
  21. package/dist/runtime/javascript-ast.d.ts.map +1 -1
  22. package/dist/runtime/javascript-ast.js +45 -7
  23. package/dist/runtime/opentui-wasm-backend.d.ts.map +1 -1
  24. package/dist/runtime/opentui-wasm-backend.js +18 -16
  25. package/dist/runtime/port-registry.d.ts +4 -2
  26. package/dist/runtime/port-registry.d.ts.map +1 -1
  27. package/dist/runtime/port-registry.js +60 -8
  28. package/dist/runtime/ruby-runner.d.ts +1 -0
  29. package/dist/runtime/ruby-runner.d.ts.map +1 -1
  30. package/dist/runtime/ruby-runner.js +1 -0
  31. package/dist/shell/stdin-adapter.d.ts +11 -0
  32. package/dist/shell/stdin-adapter.d.ts.map +1 -0
  33. package/dist/shell/stdin-adapter.js +59 -0
  34. package/dist/shell/unix-commands.d.ts +2 -1
  35. package/dist/shell/unix-commands.d.ts.map +1 -1
  36. package/dist/shell/unix-commands.js +1078 -50
  37. package/dist/substrate/lifo/commands/io/dd.js +1 -1
  38. package/dist/substrate/lifo/commands/net/curl.d.ts.map +1 -1
  39. package/dist/substrate/lifo/commands/net/curl.js +270 -54
  40. package/dist/substrate/lifo/commands/system/npm.d.ts +18 -1
  41. package/dist/substrate/lifo/commands/system/npm.d.ts.map +1 -1
  42. package/dist/substrate/lifo/commands/system/npm.js +25 -43
  43. package/dist/substrate/lifo/commands/text/head.d.ts.map +1 -1
  44. package/dist/substrate/lifo/commands/text/head.js +2 -1
  45. package/dist/substrate/lifo/commands/text/sed.d.ts.map +1 -1
  46. package/dist/substrate/lifo/commands/text/sed.js +499 -117
  47. package/dist/substrate/lifo/commands/types.d.ts +8 -4
  48. package/dist/substrate/lifo/commands/types.d.ts.map +1 -1
  49. package/dist/substrate/lifo/node-compat/zlib.d.ts +37 -18
  50. package/dist/substrate/lifo/node-compat/zlib.d.ts.map +1 -1
  51. package/dist/substrate/lifo/node-compat/zlib.js +122 -23
  52. package/dist/substrate/lifo/shell/interpreter.d.ts +9 -1
  53. package/dist/substrate/lifo/shell/interpreter.d.ts.map +1 -1
  54. package/dist/substrate/lifo/shell/interpreter.js +79 -29
  55. package/dist/substrate/lifo/shell/pipe.d.ts +16 -0
  56. package/dist/substrate/lifo/shell/pipe.d.ts.map +1 -1
  57. package/dist/substrate/lifo/shell/pipe.js +62 -18
  58. package/dist/substrate/lifo/shell/terminal-stdin.d.ts +20 -3
  59. package/dist/substrate/lifo/shell/terminal-stdin.d.ts.map +1 -1
  60. package/dist/substrate/lifo/shell/terminal-stdin.js +60 -24
  61. package/dist/vfs/sqlite-vfs.d.ts +19 -0
  62. package/dist/vfs/sqlite-vfs.d.ts.map +1 -1
  63. package/dist/vfs/sqlite-vfs.js +106 -23
  64. package/dist/workspace/nimbus-workspace.d.ts +22 -1
  65. package/dist/workspace/nimbus-workspace.d.ts.map +1 -1
  66. package/dist/workspace/nimbus-workspace.js +19 -3
  67. package/dist/workspace/supervisor-op.d.ts +107 -0
  68. package/dist/workspace/supervisor-op.d.ts.map +1 -0
  69. package/dist/workspace/supervisor-op.js +224 -0
  70. package/package.json +2 -2
  71. package/src/_shared/byte-stream.ts +5 -4
  72. package/src/_shared/real-node-imports.ts +8 -0
  73. package/src/_shared/tarball.ts +60 -5
  74. package/src/_shared/vfs-write-ledger.ts +22 -16
  75. package/src/runtime/comment-strip.ts +218 -52
  76. package/src/runtime/cpython-runner.ts +2 -1
  77. package/src/runtime/esbuild-service.ts +572 -372
  78. package/src/runtime/javascript-ast.ts +40 -9
  79. package/src/runtime/opentui-wasm-backend.ts +19 -17
  80. package/src/runtime/port-registry.ts +66 -8
  81. package/src/runtime/ruby-runner.ts +2 -0
  82. package/src/shell/stdin-adapter.ts +58 -0
  83. package/src/shell/unix-commands.ts +1054 -50
  84. package/src/substrate/lifo/commands/io/dd.ts +2 -2
  85. package/src/substrate/lifo/commands/net/curl.ts +310 -58
  86. package/src/substrate/lifo/commands/system/npm.ts +55 -38
  87. package/src/substrate/lifo/commands/text/head.ts +2 -1
  88. package/src/substrate/lifo/commands/text/sed.ts +519 -109
  89. package/src/substrate/lifo/commands/types.ts +8 -5
  90. package/src/substrate/lifo/node-compat/zlib.ts +155 -26
  91. package/src/substrate/lifo/shell/interpreter.ts +74 -27
  92. package/src/substrate/lifo/shell/pipe.ts +65 -24
  93. package/src/substrate/lifo/shell/terminal-stdin.ts +66 -30
  94. package/src/vfs/sqlite-vfs.ts +104 -23
  95. package/src/workspace/nimbus-workspace.ts +30 -3
  96. package/src/workspace/supervisor-op.ts +309 -0
@@ -0,0 +1,309 @@
1
+ import type { SqliteVFS } from '../vfs/sqlite-vfs.js';
2
+ import { CRED_SESSION_USER, type VfsCred } from '../runtime/os-contracts.js';
3
+ import { SqliteRuntimeFsBridge } from '../runtime/sqlite-runtime-fs-bridge.js';
4
+ import { getSymlinkRegistry } from '../vfs/symlink-registry.js';
5
+ import type { SessionProcessSupervisor } from '../runtime/session-process-supervisor.js';
6
+
7
+ /** Identity comes from the supervisor binding, never from facet arguments. */
8
+ export interface SupervisorOpEnvelope {
9
+ readonly op: SupervisorOpName;
10
+ readonly args?: readonly unknown[];
11
+ readonly pid?: number;
12
+ readonly writerId?: string;
13
+ readonly mutationOwner?: string;
14
+ readonly stream?: ReadableStream<Uint8Array>;
15
+ }
16
+
17
+ export type SupervisorOpHandler = (envelope: SupervisorOpEnvelope, tools: SupervisorOpTools) => unknown;
18
+
19
+ export interface SupervisorOpDeps {
20
+ readonly vfs: SqliteVFS;
21
+ /** Absent a process table, operations use the unprivileged session user. */
22
+ readonly processes?: SessionProcessSupervisor;
23
+ readonly output?: (stream: 'stdout' | 'stderr', pid: number, data: string) => void;
24
+ /**
25
+ * The host's `_rpc*` surface for ops beyond the native set — an in-process
26
+ * workspace's dispatch record, or the session itself for
27
+ * `sessionSupervisorOps`. A native op never consults it.
28
+ */
29
+ readonly host?: SupervisorOpHost;
30
+ /**
31
+ * A pid-keyed bridge cache to serve the native ops from. Supplied by the
32
+ * session so `supervisorBridge` hands callers the same bridges the handler
33
+ * uses; in-process workspaces let the handler build its own.
34
+ */
35
+ readonly bridge?: SupervisorOpBridgeStore;
36
+ readonly extend?: Partial<Record<SupervisorOpName, SupervisorOpHandler>>;
37
+ }
38
+
39
+ function stringArg(envelope: SupervisorOpEnvelope, index: number): string {
40
+ const value = envelope.args?.[index];
41
+ if (typeof value !== 'string') {
42
+ throw new Error(`supervisor op ${envelope.op}: argument ${index} must be a string`);
43
+ }
44
+ return value;
45
+ }
46
+
47
+ function numberArg(envelope: SupervisorOpEnvelope, index: number): number {
48
+ const value = envelope.args?.[index];
49
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
50
+ throw new Error(`supervisor op ${envelope.op}: argument ${index} must be a number`);
51
+ }
52
+ return value;
53
+ }
54
+
55
+ function contentArg(envelope: SupervisorOpEnvelope, index: number): string | Uint8Array {
56
+ const value = envelope.args?.[index];
57
+ if (typeof value === 'string' || value instanceof Uint8Array) return value;
58
+ if (value instanceof ArrayBuffer) return new Uint8Array(value);
59
+ throw new Error(`supervisor op ${envelope.op}: argument ${index} must be bytes or text`);
60
+ }
61
+
62
+ function credFor(deps: SupervisorOpDeps, pid: number | undefined): VfsCred {
63
+ if (pid === undefined) return CRED_SESSION_USER;
64
+ if (!Number.isInteger(pid) || pid <= 0) {
65
+ throw new Error('supervisor op: filesystem operation requires a valid process pid');
66
+ }
67
+ return deps.processes ? deps.processes.cred(pid) : CRED_SESSION_USER;
68
+ }
69
+
70
+ /**
71
+ * One slot in an op's argument plan: a number takes `envelope.args[n]`, a
72
+ * name takes the envelope's identity field (`pid`, `writerId`, `stream`,
73
+ * `mutationOwner`). The envelope is always the shape — a host never
74
+ * re-parses it.
75
+ */
76
+ export type SupervisorOpArg = number | 'pid' | 'writerId' | 'stream' | 'mutationOwner';
77
+
78
+ export interface SupervisorOpRoute {
79
+ /** The host method this op dispatches to. */
80
+ readonly method: string;
81
+ /** Positional plan for the host call — envelope fields, not raw args. */
82
+ readonly args: readonly SupervisorOpArg[];
83
+ }
84
+
85
+ /**
86
+ * The embedder's dispatch surface — the `_rpc*` methods SUPERVISOR_OP_ROUTES
87
+ * names. The session satisfies it with its own class; an in-process
88
+ * workspace supplies its host object.
89
+ */
90
+ export interface SupervisorOpHost {
91
+ readonly [method: string]: unknown;
92
+ }
93
+
94
+ /**
95
+ * The canonical supervisor op set — every operation the supervisor RPC
96
+ * serves. Three consumers key on these names:
97
+ *
98
+ * - `sessionSupervisorOp` (worker): the DO's host — `extend` overrides for
99
+ * hosted accounting plus the non-filesystem ops it answers itself.
100
+ * - `createSupervisorOpHandler`: an in-process workspace — filesystem ops
101
+ * run against the VFS directly; every other op dispatches to
102
+ * `deps.host` through SUPERVISOR_OP_ROUTES, the embedder's `_rpc*`
103
+ * surface.
104
+ * - `supervisor-host-dispatch`: the test — derives every case's delegate
105
+ * and expected arguments from SUPERVISOR_OP_ROUTES, not a copied list.
106
+ *
107
+ * An op absent here is not served, on any host.
108
+ */
109
+ export const SUPERVISOR_OPS = [
110
+ 'readFile', 'readFileBytes', 'writeFile', 'stat', 'lstat',
111
+ 'hasLegacySymlinkUnder', 'utimes', 'chmod', 'access', 'chown', 'setUmask',
112
+ 'readdir', 'exists', 'mkdir', 'rmdir', 'rename', 'unlink', 'readlink',
113
+ 'symlink', 'fsAcquire', 'fsRevision', 'fsList', 'wsOpen', 'wsPoll',
114
+ 'wsSend', 'wsClose', 'fsOpen', 'fsRead', 'fsWrite', 'fsClose',
115
+ 'fsReadRange', 'fsReadRangeUncached', 'fsReadBatch', 'fsWriteRange',
116
+ 'fsAppend', 'fsAppendAck', 'fsTruncate', 'writeBatch', 'writeBatchStream',
117
+ 'putRegistryEntries', 'stdout', 'stderr', 'prefetch', 'registerPort',
118
+ 'unregisterPort', 'reportExit', 'routeLoopback', 'transform', 'cpSpawn',
119
+ 'cpStdinWrite', 'cpStdinEnd', 'cpReadStdin', 'cpReadOutput',
120
+ 'cpDrainOutput', 'cpKill', 'cpWait', 'cpDispatchInline',
121
+ ] as const;
122
+
123
+ export type SupervisorOpName = (typeof SUPERVISOR_OPS)[number];
124
+
125
+ /**
126
+ * What the shared handler hands a host override: the pid-keyed bridge and
127
+ * the deps it was built with, so an override that wraps a filesystem op
128
+ * (read-allocation accounting, stream-drain timing) reuses the same bridge
129
+ * the default handler would have used instead of caching its own.
130
+ */
131
+ export interface SupervisorOpTools {
132
+ readonly bridge: (pid?: number) => SqliteRuntimeFsBridge;
133
+ readonly vfs: SqliteVFS;
134
+ readonly cred: (pid?: number) => VfsCred;
135
+ readonly output?: (stream: 'stdout' | 'stderr', pid: number, data: string) => void;
136
+ }
137
+
138
+ /** The host-side argument plan per op — how an envelope becomes an _rpc* call. */
139
+ export const SUPERVISOR_OP_ROUTES: Readonly<Record<SupervisorOpName, SupervisorOpRoute>> = {
140
+ readFile: { method: '_rpcReadFile', args: [0,'pid'] },
141
+ readFileBytes: { method: '_rpcReadFileBytes', args: [0,'pid'] },
142
+ writeFile: { method: '_rpcWriteFile', args: [0,1,'pid'] },
143
+ stat: { method: '_rpcStat', args: [0,'pid'] },
144
+ lstat: { method: '_rpcLstat', args: [0,'pid'] },
145
+ hasLegacySymlinkUnder: { method: '_rpcHasLegacySymlinkUnder', args: [0,'pid'] },
146
+ utimes: { method: '_rpcUtimes', args: [0,1,2,'pid'] },
147
+ chmod: { method: '_rpcChmod', args: [0,1,'pid'] },
148
+ access: { method: '_rpcAccess', args: [0,1,'pid'] },
149
+ chown: { method: '_rpcChown', args: [0,1,2,'pid',3] },
150
+ setUmask: { method: '_rpcSetUmask', args: [0,'pid'] },
151
+ readdir: { method: '_rpcReaddir', args: [0,'pid'] },
152
+ exists: { method: '_rpcExists', args: [0,'pid'] },
153
+ mkdir: { method: '_rpcMkdir', args: [0,'pid'] },
154
+ rmdir: { method: '_rpcRmdir', args: [0,'pid'] },
155
+ rename: { method: '_rpcRename', args: [0,1,'pid'] },
156
+ unlink: { method: '_rpcUnlink', args: [0,'pid'] },
157
+ readlink: { method: '_rpcReadlink', args: [0,'pid'] },
158
+ symlink: { method: '_rpcSymlink', args: [0,1,'pid'] },
159
+ fsAcquire: { method: '_rpcFsAcquire', args: [0,1,'pid'] },
160
+ fsRevision: { method: '_rpcFsRevision', args: [0,'pid'] },
161
+ fsList: { method: '_rpcFsList', args: [0,1,'pid'] },
162
+ wsOpen: { method: '_rpcWsOpen', args: [0,1,'pid'] },
163
+ wsPoll: { method: '_rpcWsPoll', args: [0,1,'pid'] },
164
+ wsSend: { method: '_rpcWsSend', args: [0,1,2,'pid'] },
165
+ wsClose: { method: '_rpcWsClose', args: [0,1,2,'pid'] },
166
+ fsOpen: { method: '_rpcFsOpen', args: [0,1,'pid'] },
167
+ fsRead: { method: '_rpcFsRead', args: [0,1,2,'pid'] },
168
+ fsWrite: { method: '_rpcFsWrite', args: [0,1,2,'pid'] },
169
+ fsClose: { method: '_rpcFsClose', args: [0,'pid'] },
170
+ fsReadRange: { method: '_rpcFsReadRange', args: [0,1,2,'pid'] },
171
+ fsReadRangeUncached: { method: '_rpcFsReadRangeUncached', args: [0,1,2,'pid'] },
172
+ fsReadBatch: { method: '_rpcFsReadBatch', args: [0,'pid'] },
173
+ fsWriteRange: { method: '_rpcFsWriteRange', args: [0,1,2,'pid'] },
174
+ fsAppend: { method: '_rpcFsAppend', args: [0,'writerId',1,2,3,'pid'] },
175
+ fsAppendAck: { method: '_rpcFsAppendAck', args: ['writerId',0,1,'pid'] },
176
+ fsTruncate: { method: '_rpcFsTruncate', args: [0,1,'pid'] },
177
+ writeBatch: { method: '_rpcWriteBatch', args: [0,'pid'] },
178
+ writeBatchStream: { method: '_rpcWriteBatchStream', args: ['stream','mutationOwner','pid'] },
179
+ putRegistryEntries: { method: '_rpcPutRegistryEntries', args: [0] },
180
+ stdout: { method: '_rpcStdout', args: ['pid',0] },
181
+ stderr: { method: '_rpcStderr', args: ['pid',0] },
182
+ prefetch: { method: '_rpcPrefetch', args: [0,1] },
183
+ registerPort: { method: '_rpcRegisterPort', args: ['pid',0] },
184
+ unregisterPort: { method: '_rpcUnregisterPort', args: [0] },
185
+ reportExit: { method: '_rpcReportExit', args: ['pid',0,1] },
186
+ routeLoopback: { method: '_rpcRouteLoopback', args: [0,1] },
187
+ transform: { method: '_rpcTransform', args: [0,1] },
188
+ cpSpawn: { method: '_rpcCpSpawn', args: [0] },
189
+ cpStdinWrite: { method: '_rpcCpStdinWrite', args: [0,1] },
190
+ cpStdinEnd: { method: '_rpcCpStdinEnd', args: [0] },
191
+ cpReadStdin: { method: '_rpcCpReadStdin', args: [0,1] },
192
+ cpReadOutput: { method: '_rpcCpReadOutput', args: [0,1,2,3] },
193
+ cpDrainOutput: { method: '_rpcCpDrainOutput', args: [0] },
194
+ cpKill: { method: '_rpcCpKill', args: [0,1] },
195
+ cpWait: { method: '_rpcCpWait', args: [0,1] },
196
+ cpDispatchInline: { method: '_rpcCpDispatchInline', args: [0,1] },
197
+ } as const;
198
+
199
+ /**
200
+ * The ops `createSupervisorOpHandler` serves natively — one pid-keyed
201
+ * filesystem bridge, plus the output stream. A session's `extend` overrides
202
+ * never cover these by accident: `sessionSupervisorOps` builds its delegate
203
+ * set from this name list, not a hand-copied table.
204
+ */
205
+ export const SUPERVISOR_NATIVE_OPS: ReadonlySet<string> = new Set([
206
+ 'readFile', 'readFileBytes', 'stat', 'lstat', 'exists', 'readdir',
207
+ 'readlink', 'fsReadRange', 'fsReadRangeUncached', 'fsRevision',
208
+ 'hasLegacySymlinkUnder', 'writeFile', 'mkdir', 'rmdir', 'unlink',
209
+ 'rename', 'symlink', 'chmod', 'utimes', 'fsTruncate',
210
+ 'writeBatchStream', 'stdout', 'stderr',
211
+ ]);
212
+
213
+ /** The pid-keyed bridge cache behind the native filesystem ops. */
214
+ export interface SupervisorOpBridgeStore {
215
+ readonly bridge: (pid?: number) => SqliteRuntimeFsBridge;
216
+ /** Drop a pid's bridge — a process exit ends its credential's validity. */
217
+ readonly forget: (pid: number) => void;
218
+ }
219
+
220
+ /**
221
+ * Exported so the session's `supervisorBridge` — used by RPC bodies the
222
+ * envelope delegates back to (fsOpen, fsAppend, writeBatch, …) — is the
223
+ * same cache the handler's native ops serve from, never a second one.
224
+ */
225
+ export function createSupervisorBridgeStore(
226
+ deps: Pick<SupervisorOpDeps, 'vfs' | 'processes'>,
227
+ ): SupervisorOpBridgeStore {
228
+ const bridges = new Map<number, SqliteRuntimeFsBridge>();
229
+ return {
230
+ bridge: (pid: number | undefined): SqliteRuntimeFsBridge => {
231
+ const key = pid ?? 0;
232
+ const credentialed = deps.vfs.as(credFor(deps, pid));
233
+ const held = bridges.get(key);
234
+ if (held) {
235
+ held.updateCredential(credentialed);
236
+ return held;
237
+ }
238
+ const built = new SqliteRuntimeFsBridge(credentialed, deps.vfs);
239
+ bridges.set(key, built);
240
+ return built;
241
+ },
242
+ forget: (pid: number): void => { bridges.delete(pid); },
243
+ };
244
+ }
245
+
246
+ /** One dispatch method lets any host serve its workspace to process facets. */
247
+ export function createSupervisorOpHandler(
248
+ deps: SupervisorOpDeps,
249
+ ): (envelope: SupervisorOpEnvelope) => Promise<unknown> {
250
+ const bridgeFor = deps.bridge?.bridge ?? createSupervisorBridgeStore(deps).bridge;
251
+ const tools: SupervisorOpTools = {
252
+ bridge: bridgeFor,
253
+ vfs: deps.vfs,
254
+ cred: (pid) => credFor(deps, pid),
255
+ output: deps.output,
256
+ };
257
+ const ops: Partial<Record<SupervisorOpName, SupervisorOpHandler>> = {
258
+ readFile: async (e) => {
259
+ const bytes = await bridgeFor(e.pid).readFile(stringArg(e, 0));
260
+ return bytes === null ? null : new TextDecoder().decode(bytes);
261
+ },
262
+ readFileBytes: (e) => bridgeFor(e.pid).readFile(stringArg(e, 0)),
263
+ stat: (e) => bridgeFor(e.pid).stat(stringArg(e, 0)),
264
+ lstat: (e) => bridgeFor(e.pid).stat(stringArg(e, 0), { followSymlinks: false }),
265
+ exists: async (e) => (await bridgeFor(e.pid).stat(stringArg(e, 0))) !== null,
266
+ readdir: (e) => bridgeFor(e.pid).readdir(stringArg(e, 0)),
267
+ readlink: (e) => bridgeFor(e.pid).readlink(stringArg(e, 0)),
268
+ fsReadRange: (e) => bridgeFor(e.pid).readRange(stringArg(e, 0), numberArg(e, 1), numberArg(e, 2)),
269
+ fsReadRangeUncached: (e) => bridgeFor(e.pid).readRange(stringArg(e, 0), numberArg(e, 1), numberArg(e, 2), { cached: false }),
270
+ fsRevision: (e) => bridgeFor(e.pid).revision(e.args?.[0] === undefined ? undefined : stringArg(e, 0)),
271
+ hasLegacySymlinkUnder: (e) => getSymlinkRegistry(deps.vfs).hasAtOrBelow(stringArg(e, 0)),
272
+ writeFile: (e) => bridgeFor(e.pid).writeFile(stringArg(e, 0), contentArg(e, 1)),
273
+ mkdir: (e) => bridgeFor(e.pid).mkdir(stringArg(e, 0), { recursive: true }),
274
+ rmdir: (e) => bridgeFor(e.pid).rmdir(stringArg(e, 0)),
275
+ unlink: (e) => bridgeFor(e.pid).unlink(stringArg(e, 0)),
276
+ rename: (e) => bridgeFor(e.pid).rename(stringArg(e, 0), stringArg(e, 1)),
277
+ symlink: (e) => bridgeFor(e.pid).symlink(stringArg(e, 0), stringArg(e, 1)),
278
+ chmod: (e) => bridgeFor(e.pid).chmod(stringArg(e, 0), numberArg(e, 1)),
279
+ utimes: (e) => bridgeFor(e.pid).utimes(stringArg(e, 0), numberArg(e, 1), numberArg(e, 2)),
280
+ fsTruncate: (e) => bridgeFor(e.pid).truncate(stringArg(e, 0), numberArg(e, 1)),
281
+ writeBatchStream: (e) => {
282
+ if (!e.stream) throw new Error('supervisor op writeBatchStream: no stream');
283
+ return deps.vfs.as(credFor(deps, e.pid)).writeStream(e.stream, { mutationOwner: e.mutationOwner });
284
+ },
285
+ stdout: (e) => { deps.output?.('stdout', e.pid ?? 0, stringArg(e, 0)); },
286
+ stderr: (e) => { deps.output?.('stderr', e.pid ?? 0, stringArg(e, 0)); },
287
+ };
288
+ const extend = deps.extend ?? {};
289
+ return async (envelope) => {
290
+ if (!envelope || typeof envelope.op !== 'string') {
291
+ throw new Error('supervisor op: envelope names no operation');
292
+ }
293
+ // Priority: the embedder's own handler → the native filesystem op → the
294
+ // canonical route table onto the host's _rpc* methods. An op in none of
295
+ // these is not served by this host.
296
+ const handler = Object.hasOwn(extend, envelope.op) ? extend[envelope.op]
297
+ : Object.hasOwn(ops, envelope.op) ? ops[envelope.op as SupervisorOpName] : undefined;
298
+ if (handler) return handler(envelope, tools);
299
+ const route = Object.hasOwn(SUPERVISOR_OP_ROUTES, envelope.op)
300
+ ? SUPERVISOR_OP_ROUTES[envelope.op as SupervisorOpName] : undefined;
301
+ if (!route) throw new Error(`supervisor op: '${envelope.op}' is not served by this host`);
302
+ const host = deps.host;
303
+ if (!host) throw new Error(`supervisor op: '${envelope.op}' needs a host that this workspace does not have`);
304
+ const method = host[route.method];
305
+ if (typeof method !== 'function') throw new Error(`supervisor op: missing host method ${route.method}`);
306
+ const args = route.args.map((slot) => typeof slot === 'number' ? envelope.args?.[slot] : envelope[slot]);
307
+ return Reflect.apply(method, host, args);
308
+ };
309
+ }