@computesdk/archil 0.4.12 → 0.4.14
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 +91 -29
- package/dist/index.d.mts +18 -2
- package/dist/index.d.ts +18 -2
- package/dist/index.js +198 -17
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +198 -17
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -4
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
|
19
|
-
|
|
|
20
|
-
| `apiKey`
|
|
21
|
-
| `region`
|
|
22
|
-
| `baseUrl`
|
|
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 |
|
|
50
|
-
| ------------- | --------- |
|
|
51
|
-
| `create` | ✅
|
|
52
|
-
| `getById` | ✅
|
|
53
|
-
| `list` | ✅
|
|
54
|
-
| `destroy` | no-op
|
|
55
|
-
| `runCommand` | ✅
|
|
56
|
-
| `getInfo` | ✅
|
|
57
|
-
| `getUrl` | ❌
|
|
58
|
-
| `filesystem` | ✅
|
|
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
|
-
-
|
|
63
|
-
state between calls beyond what is written to the disk.
|
|
64
|
-
-
|
|
65
|
-
- `getUrl` is not supported — each exec runs in a fresh
|
|
66
|
-
so there is no long-lived process to expose a port on.
|
|
67
|
-
-
|
|
68
|
-
`/mnt/archil` disk mount internally. Each mutating operation
|
|
69
|
-
checks in the disk automatically.
|
|
70
|
-
|
|
71
|
-
|
|
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
|
|
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 =
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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}`);
|