@computesdk/archil 0.4.12 → 0.4.13

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 CHANGED
@@ -2,10 +2,21 @@
2
2
 
3
3
  [Archil](https://archil.com) provider for [ComputeSDK](https://www.computesdk.com).
4
4
 
5
- `create` resolves a handle to an existing Archil disk id, and `runCommand`
6
- executes shell commands in a managed container with that disk attached via the
7
- control-plane `exec` endpoint. `destroy` is a no-op because disk lifecycle is
8
- managed by Archil. `getById` requires a disk id.
5
+ The provider supports Archil's two compute surfaces, selected by the
6
+ `execution` config option:
7
+
8
+ - **`exec`** (default): `create` resolves a handle to an existing Archil disk
9
+ id, and `runCommand` executes shell commands in a managed ephemeral
10
+ container with that disk attached via the control-plane `exec` endpoint.
11
+ `destroy` is a no-op because disk lifecycle is managed by Archil.
12
+ `getById` requires a disk id.
13
+ - **`persistent`**: `create` provisions a persistent Archil sandbox — a
14
+ long-running Linux VM with a dedicated disk — and waits until it is
15
+ `running`. `runCommand` uses the sandbox's interactive process API over a
16
+ short-lived WebSocket connection (a fresh connection URL is fetched per
17
+ command, so expiry is handled automatically). `destroy` deletes the
18
+ sandbox, `getById` fetches the sandbox and resumes it if paused, and
19
+ `getUrl` resolves the sandbox's published endpoints.
9
20
 
10
21
  ## Installation
11
22
 
@@ -15,11 +26,12 @@ npm install @computesdk/archil
15
26
 
16
27
  ## Configuration
17
28
 
18
- | Option | Env var | Required | Description |
19
- | --------- | ------------------ | -------- | ------------------------------------------ |
20
- | `apiKey` | `ARCHIL_API_KEY` | yes | Archil control-plane API key |
21
- | `region` | `ARCHIL_REGION` | yes | Archil region (e.g. `aws-us-east-1`) |
22
- | `baseUrl` | — | no | Override control-plane URL (for testing) |
29
+ | Option | Env var | Required | Description |
30
+ | ----------- | ------------------ | -------- | ----------------------------------------------- |
31
+ | `apiKey` | `ARCHIL_API_KEY` | yes | Archil control-plane API key |
32
+ | `region` | `ARCHIL_REGION` | yes | Archil region (e.g. `aws-us-east-1`) |
33
+ | `baseUrl` | — | no | Override control-plane URL (for testing) |
34
+ | `execution` | — | no | `"exec"` (default) or `"persistent"` |
23
35
 
24
36
  ## Usage
25
37
 
@@ -44,28 +56,78 @@ await provider.sandbox.destroy(sandbox.sandboxId);
44
56
 
45
57
  `create()` requires top-level `diskId` as the target disk id.
46
58
 
59
+ ### Persistent mode
60
+
61
+ ```ts
62
+ const provider = archil({ execution: 'persistent' });
63
+
64
+ const sandbox = await provider.sandbox.create({
65
+ name: 'ci-job',
66
+ baseImage: 'node:24-bookworm', // OCI image; `templateId` works too
67
+ vcpus: 4, // or `cpu` / `cpus`
68
+ memoryMiB: 8192, // or `memory`
69
+ env: { CI: '1' },
70
+ maxTtlSeconds: 3600, // per powered-on session
71
+ sandbox: { /* extra Archil fields, e.g. network policy */ },
72
+ });
73
+
74
+ // cwd/env/installs and background processes persist between calls.
75
+ await sandbox.runCommand('npm ci', { cwd: '/repo' });
76
+ await sandbox.runCommand('npm test', { cwd: '/repo' });
77
+
78
+ // Paused sandboxes are resumed automatically on getById/runCommand.
79
+ const resumed = await provider.sandbox.getById(sandbox.sandboxId);
80
+
81
+ await provider.sandbox.destroy(sandbox.sandboxId); // deletes the sandbox
82
+ ```
83
+
84
+ Sandbox `create` accepts generic `CreateSandboxOptions` fields (`vcpus`/`cpu`/`cpus`,
85
+ `memory`/`memoryMiB`, `image`/`templateId`, `envs`, `timeout` (ms → `maxTtlSeconds`))
86
+ plus the Archil-specific keys shown above.
87
+
88
+ The shared `ephemeral` create option overrides the configured mode per sandbox:
89
+ `ephemeral: true` resolves an exec-mode disk handle (requires `diskId`),
90
+ `ephemeral: false` provisions a persistent sandbox — matching the same flag on
91
+ other dual-mode providers like Upstash. `getById`/`list`/`destroy` follow the
92
+ configured `execution` mode.
93
+
94
+ ### Choosing a mode
95
+
96
+ | | `exec` | `persistent` |
97
+ | --------------------- | ------------------------------- | ------------------------------------------- |
98
+ | Compute | Ephemeral container per command | Persistent VM |
99
+ | `cwd`/`env`/installs | Lost between commands | Persist while running |
100
+ | Filesystem writes | Only under `/mnt/archil` | Anywhere on the VM |
101
+ | Background processes | Die with the container | Keep running |
102
+ | `create` input | Existing `diskId` | Sandbox spec (image, vcpus, memory, …) |
103
+ | Billing | Per `executeMs` | Per running wall-clock time |
104
+ | `getUrl` | ❌ | ✅ (published endpoints) |
105
+
47
106
  ## Supported operations
48
107
 
49
- | Method | Supported | Notes |
50
- | ------------- | --------- | ----------------------------------------------------------- |
51
- | `create` | ✅ | Resolves an existing disk from top-level `diskId`. |
52
- | `getById` | ✅ | Requires the disk id. |
53
- | `list` | ✅ | Lists all disks visible to the API key. |
54
- | `destroy` | no-op | Disk lifecycle is managed by Archil. |
55
- | `runCommand` | ✅ | Executes shell commands through Archil's HTTP `exec` endpoint. |
56
- | `getInfo` | ✅ | |
57
- | `getUrl` | ❌ | Each exec runs in a fresh ephemeral container — no port to expose. |
58
- | `filesystem` | ✅ | Maps paths into `/mnt/archil` and manages shared-mode checkout/checkin automatically. |
108
+ | Method | exec mode | persistent mode |
109
+ | ------------- | --------- | ------------ |
110
+ | `create` | ✅ Resolves an existing disk from top-level `diskId`. | ✅ Provisions a sandbox, waits for `running`. |
111
+ | `getById` | ✅ Requires the disk id. | ✅ Fetches the sandbox; resumes if paused/stopped. |
112
+ | `list` | ✅ Lists all disks visible to the API key. | ✅ Lists sandboxes. |
113
+ | `destroy` | no-op — disk lifecycle is managed by Archil. | ✅ Deletes the sandbox and its backing disk. |
114
+ | `runCommand` | ✅ Executes via the HTTP `exec` endpoint. | ✅ Executes via the process API (fresh connection URL per command). |
115
+ | `getInfo` | ✅ | ✅ |
116
+ | `getUrl` | ❌ Ephemeral containers have no port to expose. | ✅ Resolves the sandbox's endpoint for the port. |
117
+ | `filesystem` | ✅ Maps paths into `/mnt/archil`; manages shared-mode checkout/checkin. | ✅ Uses the sandbox file transfer API; paths are the VM's real paths. |
59
118
 
60
119
  ## Limitations
61
120
 
62
- - Each `exec` call provisions a fresh container — there is no persistent
63
- state between calls beyond what is written to the disk.
64
- - Responses are truncated to ~5 MB by the Archil control plane.
65
- - `getUrl` is not supported — each exec runs in a fresh ephemeral container,
66
- so there is no long-lived process to expose a port on.
67
- - Filesystem operations accept normal absolute paths and map them into the
68
- `/mnt/archil` disk mount internally. Each mutating operation checks out and
69
- checks in the disk automatically.
70
- - Raw `runCommand` strings are not rewritten. Commands that access the disk
71
- directly must use `/mnt/archil` themselves.
121
+ - **exec mode**: each `exec` call provisions a fresh container — there is no
122
+ persistent state between calls beyond what is written to the disk.
123
+ - **exec mode**: responses are truncated to ~5 MB by the Archil control plane.
124
+ - **exec mode**: `getUrl` is not supported — each exec runs in a fresh
125
+ ephemeral container, so there is no long-lived process to expose a port on.
126
+ - **exec mode**: filesystem operations accept normal absolute paths and map
127
+ them into the `/mnt/archil` disk mount internally. Each mutating operation
128
+ checks out and checks in the disk automatically. Raw `runCommand` strings
129
+ are not rewritten — commands that access the disk must use `/mnt/archil`
130
+ themselves.
131
+ - **persistent mode**: `pause`/`resume`/`fork` are available on the underlying
132
+ `disk`-SDK sandbox via `getInstance()` — the generic provider interface has
133
+ no `pause`/`resume` methods, so only `getById`/`runCommand` auto-resume.
package/dist/index.d.mts CHANGED
@@ -1,6 +1,12 @@
1
1
  import * as _computesdk_provider from '@computesdk/provider';
2
- import { Archil } from 'disk';
2
+ import { Archil, Sandbox } from 'disk';
3
3
 
4
+ /**
5
+ * Archil execution mode:
6
+ * - "exec": serverless per-command containers on a disk (default).
7
+ * - "persistent": a persistent sandbox VM run through the process API.
8
+ */
9
+ type ArchilExecutionMode = 'exec' | 'persistent';
4
10
  interface ArchilConfig {
5
11
  /** Archil API key. Falls back to ARCHIL_API_KEY env var. */
6
12
  apiKey?: string;
@@ -8,6 +14,13 @@ interface ArchilConfig {
8
14
  region?: string;
9
15
  /** Override the control-plane base URL (useful for testing). */
10
16
  baseUrl?: string;
17
+ /**
18
+ * Which Archil compute surface to use. Defaults to "exec". In
19
+ * "persistent" mode create() provisions a persistent sandbox VM instead of
20
+ * resolving a disk handle. Per-sandbox override: `ephemeral` on
21
+ * create() options (true -> exec handle, false -> persistent VM).
22
+ */
23
+ execution?: ArchilExecutionMode;
11
24
  }
12
25
  interface DiskResponse {
13
26
  id: string;
@@ -22,13 +35,16 @@ type DiskHandle = Pick<DiskResponse, 'id'>;
22
35
  interface ResolvedConfig {
23
36
  apiKey: string;
24
37
  baseUrl: string;
38
+ execution: ArchilExecutionMode;
25
39
  }
26
40
  interface ArchilSandbox {
27
41
  client: Archil;
28
42
  disk: DiskHandle | DiskResponse;
43
+ /** Set only in "persistent" execution mode. */
44
+ vm?: Sandbox;
29
45
  resolved: ResolvedConfig;
30
46
  createdAt: Date;
31
47
  }
32
48
  declare const archil: (config?: ArchilConfig) => _computesdk_provider.Provider<ArchilSandbox, any, any>;
33
49
 
34
- export { type ArchilConfig, type ArchilSandbox, archil };
50
+ export { type ArchilConfig, type ArchilExecutionMode, type ArchilSandbox, archil };
package/dist/index.d.ts CHANGED
@@ -1,6 +1,12 @@
1
1
  import * as _computesdk_provider from '@computesdk/provider';
2
- import { Archil } from 'disk';
2
+ import { Archil, Sandbox } from 'disk';
3
3
 
4
+ /**
5
+ * Archil execution mode:
6
+ * - "exec": serverless per-command containers on a disk (default).
7
+ * - "persistent": a persistent sandbox VM run through the process API.
8
+ */
9
+ type ArchilExecutionMode = 'exec' | 'persistent';
4
10
  interface ArchilConfig {
5
11
  /** Archil API key. Falls back to ARCHIL_API_KEY env var. */
6
12
  apiKey?: string;
@@ -8,6 +14,13 @@ interface ArchilConfig {
8
14
  region?: string;
9
15
  /** Override the control-plane base URL (useful for testing). */
10
16
  baseUrl?: string;
17
+ /**
18
+ * Which Archil compute surface to use. Defaults to "exec". In
19
+ * "persistent" mode create() provisions a persistent sandbox VM instead of
20
+ * resolving a disk handle. Per-sandbox override: `ephemeral` on
21
+ * create() options (true -> exec handle, false -> persistent VM).
22
+ */
23
+ execution?: ArchilExecutionMode;
11
24
  }
12
25
  interface DiskResponse {
13
26
  id: string;
@@ -22,13 +35,16 @@ type DiskHandle = Pick<DiskResponse, 'id'>;
22
35
  interface ResolvedConfig {
23
36
  apiKey: string;
24
37
  baseUrl: string;
38
+ execution: ArchilExecutionMode;
25
39
  }
26
40
  interface ArchilSandbox {
27
41
  client: Archil;
28
42
  disk: DiskHandle | DiskResponse;
43
+ /** Set only in "persistent" execution mode. */
44
+ vm?: Sandbox;
29
45
  resolved: ResolvedConfig;
30
46
  createdAt: Date;
31
47
  }
32
48
  declare const archil: (config?: ArchilConfig) => _computesdk_provider.Provider<ArchilSandbox, any, any>;
33
49
 
34
- export { type ArchilConfig, type ArchilSandbox, archil };
50
+ export { type ArchilConfig, type ArchilExecutionMode, type ArchilSandbox, archil };
package/dist/index.js CHANGED
@@ -62,7 +62,7 @@ function resolveConfig(config) {
62
62
  }
63
63
  baseUrl = regionToBaseUrl(region);
64
64
  }
65
- return { apiKey, baseUrl };
65
+ return { apiKey, baseUrl, execution: config.execution ?? "exec" };
66
66
  }
67
67
  function createClient(config, resolved) {
68
68
  const region = config.region ?? process.env.ARCHIL_REGION ?? "aws-us-east-1";
@@ -80,8 +80,11 @@ function resolveCreateDiskId(options) {
80
80
  function shellEscape(value) {
81
81
  return `'${value.replace(/'/g, `'\\''`)}'`;
82
82
  }
83
- function mapFilesystemPath(path) {
83
+ function mapFilesystemPath(sandbox, path) {
84
84
  const normalized = import_node_path.posix.normalize(path.startsWith("/") ? path : `/${path}`);
85
+ if (sandbox.vm) {
86
+ return normalized;
87
+ }
85
88
  if (normalized === "/") {
86
89
  return ARCHIL_MOUNT_ROOT;
87
90
  }
@@ -90,13 +93,17 @@ function mapFilesystemPath(path) {
90
93
  }
91
94
  return `${ARCHIL_MOUNT_ROOT}${normalized}`;
92
95
  }
93
- function withDiskWriteLock(command) {
96
+ function withDiskWriteLockCommand(command) {
94
97
  const mountRoot = shellEscape(ARCHIL_MOUNT_ROOT);
95
98
  return [
96
99
  `archil checkout --force --yes ${mountRoot}`,
97
100
  `{ ${command}; status=$?; archil checkin ${mountRoot}; checkin_status=$?; if [ $status -ne 0 ]; then exit $status; fi; exit $checkin_status; }`
98
101
  ].join(" && ");
99
102
  }
103
+ function withDiskWriteLock(sandbox, command) {
104
+ if (sandbox.vm) return command;
105
+ return withDiskWriteLockCommand(command);
106
+ }
100
107
  function execCommandBytes(command) {
101
108
  return Buffer.byteLength(wrapCommand(command), "utf8");
102
109
  }
@@ -123,7 +130,7 @@ function maxWriteChunkSize(parent, tempPath, diskPath) {
123
130
  let high = ARCHIL_MAX_EXEC_COMMAND_BYTES;
124
131
  while (low < high) {
125
132
  const candidate = Math.ceil((low + high) / 2);
126
- const command = withDiskWriteLock(
133
+ const command = withDiskWriteLockCommand(
127
134
  buildWriteChunkCommand(
128
135
  parent,
129
136
  tempPath,
@@ -166,6 +173,42 @@ function wrapCommand(command, options) {
166
173
  async function execOnDisk(sandbox, command) {
167
174
  return sandbox.client.disks.exec(sandbox.disk.id, command);
168
175
  }
176
+ function wrapSandboxCommand(command, options) {
177
+ let wrapped = command;
178
+ if (options?.cwd) {
179
+ wrapped = `cd ${shellEscape(options.cwd)} && ${wrapped}`;
180
+ }
181
+ if (options?.background) {
182
+ wrapped = `nohup sh -c ${shellEscape(wrapped)} > /dev/null 2>&1 &`;
183
+ }
184
+ return wrapped;
185
+ }
186
+ async function ensureVmRunning(vm) {
187
+ await vm.refresh();
188
+ if (vm.status === "running") return;
189
+ if (vm.status === "paused") {
190
+ await vm.resume();
191
+ } else if (vm.status === "stopped" || vm.status === "exited") {
192
+ await vm.start();
193
+ }
194
+ }
195
+ function toSandboxRequest(options) {
196
+ const request = {
197
+ ...options?.sandbox,
198
+ name: options?.name,
199
+ baseImage: options?.baseImage ?? options?.image ?? options?.templateId,
200
+ env: options?.env ?? options?.envs,
201
+ vcpuCount: options?.vcpus ?? options?.cpu ?? options?.cpus,
202
+ memSizeMiB: options?.memoryMiB ?? options?.memory
203
+ };
204
+ if (options?.maxTtlSeconds !== void 0) {
205
+ request.maxTtlSeconds = options.maxTtlSeconds;
206
+ } else if (options?.timeout !== void 0) {
207
+ request.maxTtlSeconds = Math.ceil(options.timeout / 1e3);
208
+ }
209
+ return request;
210
+ }
211
+ var sandboxModes = /* @__PURE__ */ new Map();
169
212
  var _provider = (0, import_provider.defineProvider)({
170
213
  name: "archil",
171
214
  methods: {
@@ -173,7 +216,25 @@ var _provider = (0, import_provider.defineProvider)({
173
216
  create: async (config, options) => {
174
217
  const resolved = resolveConfig(config);
175
218
  const client = createClient(config, resolved);
219
+ const execution = options?.ephemeral !== void 0 ? options.ephemeral ? "exec" : "persistent" : resolved.execution;
220
+ if (execution === "persistent") {
221
+ const vm = await client.sandboxes.create(toSandboxRequest(options), {
222
+ wait: true
223
+ });
224
+ sandboxModes.set(vm.id, "persistent");
225
+ return {
226
+ sandbox: {
227
+ client,
228
+ disk: { id: vm.id },
229
+ vm,
230
+ resolved,
231
+ createdAt: /* @__PURE__ */ new Date()
232
+ },
233
+ sandboxId: vm.id
234
+ };
235
+ }
176
236
  const diskId = resolveCreateDiskId(options);
237
+ sandboxModes.set(diskId, "exec");
177
238
  return {
178
239
  sandbox: {
179
240
  client,
@@ -188,6 +249,20 @@ var _provider = (0, import_provider.defineProvider)({
188
249
  const resolved = resolveConfig(config);
189
250
  const client = createClient(config, resolved);
190
251
  try {
252
+ if (resolved.execution === "persistent") {
253
+ const vm = await client.sandboxes.get(sandboxId);
254
+ await ensureVmRunning(vm);
255
+ return {
256
+ sandbox: {
257
+ client,
258
+ disk: { id: vm.id },
259
+ vm,
260
+ resolved,
261
+ createdAt: /* @__PURE__ */ new Date()
262
+ },
263
+ sandboxId: vm.id
264
+ };
265
+ }
191
266
  const disk = await client.disks.get(sandboxId);
192
267
  return {
193
268
  sandbox: { client, disk, resolved, createdAt: /* @__PURE__ */ new Date() },
@@ -200,17 +275,64 @@ var _provider = (0, import_provider.defineProvider)({
200
275
  list: async (config) => {
201
276
  const resolved = resolveConfig(config);
202
277
  const client = createClient(config, resolved);
278
+ if (resolved.execution === "persistent") {
279
+ const vms = await client.sandboxes.list();
280
+ return vms.map((vm) => ({
281
+ sandbox: {
282
+ client,
283
+ disk: { id: vm.id },
284
+ vm,
285
+ resolved,
286
+ createdAt: /* @__PURE__ */ new Date()
287
+ },
288
+ sandboxId: vm.id
289
+ }));
290
+ }
203
291
  const disks = await client.disks.list();
204
292
  return disks.map((disk) => ({
205
293
  sandbox: { client, disk, resolved, createdAt: /* @__PURE__ */ new Date() },
206
294
  sandboxId: disk.id
207
295
  }));
208
296
  },
209
- destroy: async (_config, _sandboxId) => {
297
+ destroy: async (config, sandboxId) => {
298
+ const resolved = resolveConfig(config);
299
+ const mode = sandboxModes.get(sandboxId) ?? resolved.execution;
300
+ if (mode === "persistent") {
301
+ const client = createClient(config, resolved);
302
+ const vm = await client.sandboxes.get(sandboxId);
303
+ await vm.delete();
304
+ }
305
+ sandboxModes.delete(sandboxId);
210
306
  },
211
307
  runCommand: async (sandbox, command, options) => {
212
308
  const startTime = Date.now();
213
309
  try {
310
+ if (sandbox.vm) {
311
+ await ensureVmRunning(sandbox.vm);
312
+ const wrapped = wrapSandboxCommand(command, options);
313
+ if (options?.background) {
314
+ const proc = await sandbox.vm.processes.start(wrapped, {
315
+ env: options?.env
316
+ });
317
+ await proc.disconnect();
318
+ return {
319
+ stdout: "",
320
+ stderr: "",
321
+ exitCode: 0,
322
+ durationMs: Date.now() - startTime
323
+ };
324
+ }
325
+ const result2 = await sandbox.vm.exec(wrapped, {
326
+ env: options?.env,
327
+ timeoutSeconds: options?.timeout !== void 0 ? Math.ceil(options.timeout / 1e3) : void 0
328
+ });
329
+ return {
330
+ stdout: result2.stdout,
331
+ stderr: result2.stderr,
332
+ exitCode: result2.exitCode ?? (result2.status === "completed" ? 0 : 1),
333
+ durationMs: Date.now() - startTime
334
+ };
335
+ }
214
336
  const result = await execOnDisk(sandbox, wrapCommand(command, options));
215
337
  return {
216
338
  stdout: result.stdout ?? "",
@@ -228,6 +350,24 @@ var _provider = (0, import_provider.defineProvider)({
228
350
  }
229
351
  },
230
352
  getInfo: async (sandbox) => {
353
+ if (sandbox.vm) {
354
+ const vm = sandbox.vm;
355
+ return {
356
+ id: vm.id,
357
+ provider: "archil",
358
+ status: vm.status === "running" ? "running" : vm.status === "failed" ? "error" : "stopped",
359
+ createdAt: vm.createdAt,
360
+ timeout: vm.maxTtlSeconds * 1e3,
361
+ metadata: {
362
+ name: vm.name,
363
+ archilStatus: vm.status,
364
+ vcpuCount: vm.vcpuCount,
365
+ memSizeMiB: vm.memSizeMiB,
366
+ baseImage: vm.baseImage,
367
+ platform: vm.platform
368
+ }
369
+ };
370
+ }
231
371
  const diskInfo = "status" in sandbox.disk ? sandbox.disk : void 0;
232
372
  return {
233
373
  id: sandbox.disk.id,
@@ -243,14 +383,39 @@ var _provider = (0, import_provider.defineProvider)({
243
383
  } : {}
244
384
  };
245
385
  },
246
- getUrl: async (_sandbox, options) => {
386
+ getUrl: async (sandbox, options) => {
387
+ if (sandbox.vm) {
388
+ await sandbox.vm.refresh();
389
+ const endpoint = sandbox.vm.endpoints?.find(
390
+ (e) => e.port === options.port
391
+ );
392
+ if (!endpoint) {
393
+ throw new Error(
394
+ `No endpoint exposed on port ${options.port} for Archil sandbox ${sandbox.vm.id}.`
395
+ );
396
+ }
397
+ return `${options.protocol ?? "https"}://${endpoint.hostname}`;
398
+ }
247
399
  throw new Error(
248
400
  `Archil exec runs each command in a fresh ephemeral container that exits when the command returns, so there is no long-lived process to expose port ${options.port} on. getUrl is not supported.`
249
401
  );
250
402
  },
251
403
  filesystem: {
252
404
  readFile: async (sandbox, path, runCommand) => {
253
- const diskPath = mapFilesystemPath(path);
405
+ if (sandbox.vm) {
406
+ const chunks2 = [];
407
+ try {
408
+ await sandbox.vm.files.downloadFile(path, (chunk) => {
409
+ chunks2.push(Buffer.from(chunk));
410
+ });
411
+ } catch (error) {
412
+ throw new Error(
413
+ `Failed to read ${path}: ${error instanceof Error ? error.message : String(error)}`
414
+ );
415
+ }
416
+ return Buffer.concat(chunks2).toString("utf8");
417
+ }
418
+ const diskPath = mapFilesystemPath(sandbox, path);
254
419
  const sizeResult = await runCommand(
255
420
  sandbox,
256
421
  `wc -c < ${shellEscape(diskPath)}`
@@ -301,7 +466,20 @@ var _provider = (0, import_provider.defineProvider)({
301
466
  return Buffer.concat(chunks).toString("utf8");
302
467
  },
303
468
  writeFile: async (sandbox, path, content, runCommand) => {
304
- const diskPath = mapFilesystemPath(path);
469
+ if (sandbox.vm) {
470
+ try {
471
+ await sandbox.vm.files.uploadFile(
472
+ Buffer.from(content, "utf8"),
473
+ path
474
+ );
475
+ } catch (error) {
476
+ throw new Error(
477
+ `Failed to write ${path}: ${error instanceof Error ? error.message : String(error)}`
478
+ );
479
+ }
480
+ return;
481
+ }
482
+ const diskPath = mapFilesystemPath(sandbox, path);
305
483
  const parent = import_node_path.posix.dirname(diskPath);
306
484
  const encoded = Buffer.from(content, "utf8").toString("base64");
307
485
  const tempPath = `${diskPath}.computesdk-write-${(0, import_node_crypto.randomUUID)()}`;
@@ -312,6 +490,7 @@ var _provider = (0, import_provider.defineProvider)({
312
490
  const result = await runCommand(
313
491
  sandbox,
314
492
  withDiskWriteLock(
493
+ sandbox,
315
494
  [
316
495
  `mkdir -p ${shellEscape(parent)}`,
317
496
  `: > ${shellEscape(tempPath)}`,
@@ -332,6 +511,7 @@ var _provider = (0, import_provider.defineProvider)({
332
511
  const result = await runCommand(
333
512
  sandbox,
334
513
  withDiskWriteLock(
514
+ sandbox,
335
515
  buildWriteChunkCommand(
336
516
  parent,
337
517
  tempPath,
@@ -351,7 +531,7 @@ var _provider = (0, import_provider.defineProvider)({
351
531
  try {
352
532
  await runCommand(
353
533
  sandbox,
354
- withDiskWriteLock(`rm -f ${shellEscape(tempPath)}`)
534
+ withDiskWriteLock(sandbox, `rm -f ${shellEscape(tempPath)}`)
355
535
  );
356
536
  } catch {
357
537
  }
@@ -362,20 +542,21 @@ var _provider = (0, import_provider.defineProvider)({
362
542
  }
363
543
  },
364
544
  mkdir: async (sandbox, path, runCommand) => {
365
- const diskPath = mapFilesystemPath(path);
545
+ const diskPath = mapFilesystemPath(sandbox, path);
366
546
  const result = await runCommand(
367
547
  sandbox,
368
- withDiskWriteLock(`mkdir -p ${shellEscape(diskPath)}`)
548
+ withDiskWriteLock(sandbox, `mkdir -p ${shellEscape(diskPath)}`)
369
549
  );
370
550
  if (result.exitCode !== 0) {
371
551
  throw new Error(`Failed to create directory ${path}: ${result.stderr}`);
372
552
  }
373
553
  },
374
554
  readdir: async (sandbox, path, runCommand) => {
375
- const diskPath = mapFilesystemPath(path);
555
+ const diskPath = mapFilesystemPath(sandbox, path);
376
556
  const result = await runCommand(
377
557
  sandbox,
378
- `find ${shellEscape(diskPath)} -mindepth 1 -maxdepth 1 -printf '%y\\t%s\\t%T@\\t%f\\n'`
558
+ `find ${shellEscape(diskPath)} -mindepth 1 -maxdepth 1 -printf '%y %s %T@ %f
559
+ '`
379
560
  );
380
561
  if (result.exitCode !== 0) {
381
562
  throw new Error(`Failed to list directory ${path}: ${result.stderr}`);
@@ -395,18 +576,18 @@ var _provider = (0, import_provider.defineProvider)({
395
576
  return entries;
396
577
  },
397
578
  exists: async (sandbox, path, runCommand) => {
398
- const diskPath = mapFilesystemPath(path);
579
+ const diskPath = mapFilesystemPath(sandbox, path);
399
580
  const result = await runCommand(sandbox, `test -e ${shellEscape(diskPath)}`);
400
581
  return result.exitCode === 0;
401
582
  },
402
583
  remove: async (sandbox, path, runCommand) => {
403
- const diskPath = mapFilesystemPath(path);
404
- if (diskPath === ARCHIL_MOUNT_ROOT) {
584
+ const diskPath = mapFilesystemPath(sandbox, path);
585
+ if (!sandbox.vm && diskPath === ARCHIL_MOUNT_ROOT) {
405
586
  throw new Error("Refusing to remove the Archil disk mount root.");
406
587
  }
407
588
  const result = await runCommand(
408
589
  sandbox,
409
- withDiskWriteLock(`rm -rf ${shellEscape(diskPath)}`)
590
+ withDiskWriteLock(sandbox, `rm -rf ${shellEscape(diskPath)}`)
410
591
  );
411
592
  if (result.exitCode !== 0) {
412
593
  throw new Error(`Failed to remove ${path}: ${result.stderr}`);