@torkbot/sandbox 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,176 +1,85 @@
1
+ import { builtInRootfsIdentity, builtInRootfsPath, } from "./artifacts.js";
1
2
  import { HostControlTransport } from "./control.js";
2
3
  import { HostProcessSandboxVm } from "./host-process.js";
3
- import { createSandboxHostFileSystemTools } from "./host-filesystem-tools.js";
4
- import { isSandboxWritableFileSystem } from "./vfs.js";
5
- export { HostControlTransport } from "./control.js";
6
- export function projectKernel(options = {}) {
7
- return {
8
- kind: "project-kernel",
9
- ...options,
10
- };
11
- }
12
- export function projectInit() {
13
- return {
14
- kind: "project-init",
15
- crate: "sandbox-init",
16
- };
17
- }
18
- export function prebuiltRootfs(path, options) {
19
- return {
20
- kind: "prebuilt-rootfs",
21
- path,
22
- readonly: options.readonly ?? true,
23
- format: options.format,
24
- };
25
- }
26
- export function scratchFs() {
27
- return {
28
- kind: "scratch-fs",
29
- };
30
- }
31
- export function linuxOverlayFs(input) {
32
- return {
33
- kind: "linux-overlay-fs",
34
- lower: input.lower,
35
- upper: input.upper,
36
- };
37
- }
38
- export function virtualFsMount(path, fileSystem) {
4
+ import { createMemoryFileSystem } from "./memory-fs.js";
5
+ import { isSandboxPosixFileSystem, isSandboxWritableFileSystem, } from "./vfs.js";
6
+ const networkPolicyHandler = Symbol("networkPolicyHandler");
7
+ export const rootfs = {
8
+ builtIn(name) {
9
+ return {
10
+ kind: "built-in-rootfs",
11
+ name,
12
+ };
13
+ },
14
+ cow(options) {
15
+ return {
16
+ kind: "cow-rootfs",
17
+ base: options.base,
18
+ writable: options.writable,
19
+ };
20
+ },
21
+ };
22
+ function virtualFs(fileSystem) {
39
23
  return {
40
24
  kind: "virtual-fs",
41
- path,
42
25
  fileSystem,
43
26
  };
44
27
  }
45
- export function mount(path, fileSystem) {
46
- return virtualFsMount(path, fileSystem);
47
- }
48
- export function binding(path, fileSystem) {
49
- return {
50
- kind: "filesystem-binding",
51
- path,
52
- fileSystem,
53
- };
54
- }
55
- export function acceptTcp(input) {
56
- return {
57
- action: "accept",
58
- protocol: "tcp",
59
- cidr: input.cidr,
60
- ports: input.ports,
61
- };
62
- }
63
- export function acceptUdp(input) {
64
- return {
65
- action: "accept",
66
- protocol: "udp",
67
- cidr: input.cidr,
68
- ports: input.ports,
69
- };
70
- }
71
- export function acceptPublicInternet(input = {}) {
72
- return {
73
- action: "accept",
74
- scope: "public-internet",
75
- ports: input.ports,
76
- };
77
- }
78
- export async function spawnSandbox(options) {
79
- validateSandboxOptions(options);
80
- const hostOptions = toHostSpawnOptions(options, []);
81
- const hostVm = await HostProcessSandboxVm.spawn(options, hostOptions, new Map());
82
- return new HostBackedSandboxVm(hostVm, options);
83
- }
84
- export function createSandbox(options) {
85
- validateSandboxOptions(options);
86
- return new ConfiguredSandboxBuilder(options);
28
+ export const fs = {
29
+ memory: createMemoryFileSystem,
30
+ virtual: virtualFs,
31
+ };
32
+ export const network = {
33
+ policy(onConnectionRequest) {
34
+ return {
35
+ kind: "network-policy",
36
+ [networkPolicyHandler]: onConnectionRequest,
37
+ };
38
+ },
39
+ };
40
+ export function defineSandbox(options) {
41
+ validateSandboxDefinitionOptions(options);
42
+ return new DefinedSandbox(options);
87
43
  }
88
- class ConfiguredSandboxBuilder {
89
- http;
44
+ class DefinedSandbox {
90
45
  #options;
91
- #requestHeaderHooks = new Set();
92
- #nextRequestHeaderHookId = 1;
93
- #runStarted = false;
94
- #vm = null;
95
- #closed = false;
96
46
  constructor(options) {
97
47
  this.#options = options;
98
- this.http = {
99
- onRequest: (selector, hook) => {
100
- this.#assertOpen();
101
- if (this.#runStarted) {
102
- throw new Error("sandbox has already been run");
103
- }
104
- const registration = {
105
- id: `http-request-headers-${this.#nextRequestHeaderHookId++}`,
106
- selector,
107
- hook,
108
- active: true,
109
- };
110
- this.#requestHeaderHooks.add(registration);
111
- return new ConfiguredSandboxHttpHook(this, registration);
112
- },
113
- };
114
- }
115
- async run() {
116
- this.#assertOpen();
117
- if (this.#runStarted) {
118
- throw new Error("sandbox has already been run");
119
- }
120
- this.#runStarted = true;
121
- const registrations = Array.from(this.#requestHeaderHooks);
122
- const hostOptions = toHostSpawnOptions(this.#options, registrations);
123
- const hostVm = await HostProcessSandboxVm.spawn(this.#options, hostOptions, new Map(registrations.map((registration) => [registration.id, registration])));
124
- this.#vm = new HostBackedSandboxVm(hostVm, this.#options);
125
- return this.#vm;
126
- }
127
- async [Symbol.asyncDispose]() {
128
- if (this.#closed) {
129
- return;
130
- }
131
- this.#closed = true;
132
- await this.#vm?.close();
133
- this.#requestHeaderHooks.clear();
134
48
  }
135
- async removeHook(registration) {
136
- registration.active = false;
137
- this.#requestHeaderHooks.delete(registration);
138
- }
139
- #assertOpen() {
140
- if (this.#closed) {
141
- throw new Error("sandbox is closed");
49
+ async boot(options = {}) {
50
+ validateSandboxBootOptions(options);
51
+ const networkPolicy = this.#options.network === undefined
52
+ ? undefined
53
+ : createNetworkPolicyHookRegistration(this.#options.network);
54
+ const launchOptions = await toInternalSandboxOptions(this.#options, options, networkPolicy?.network);
55
+ try {
56
+ validateInternalSandboxOptions(launchOptions);
57
+ const hostOptions = toHostSpawnOptions(launchOptions, networkPolicy?.hooks ?? []);
58
+ const hostVm = await HostProcessSandboxVm.spawn(launchOptions, hostOptions, new Map((networkPolicy?.hooks ?? []).map((hook) => [hook.id, hook])));
59
+ return new HostBackedSandboxVm(hostVm, launchOptions);
142
60
  }
143
- }
144
- }
145
- class ConfiguredSandboxHttpHook {
146
- #sandbox;
147
- #registration;
148
- #disposed = false;
149
- constructor(sandbox, registration) {
150
- this.#sandbox = sandbox;
151
- this.#registration = registration;
152
- }
153
- async [Symbol.asyncDispose]() {
154
- if (this.#disposed) {
155
- return;
61
+ catch (error) {
62
+ throw error;
156
63
  }
157
- this.#disposed = true;
158
- await this.#sandbox.removeHook(this.#registration);
159
64
  }
160
65
  }
161
66
  class HostBackedSandboxVm {
162
- mounts;
163
67
  control;
164
68
  diagnostics;
69
+ #exec;
70
+ #rootExec;
71
+ #options;
165
72
  #hostVm;
166
73
  #closed = false;
167
74
  constructor(hostVm, options) {
168
75
  this.#hostVm = hostVm;
169
- this.mounts = new ConfiguredSandboxMounts(options.mounts ?? [], options.bindings ?? []);
76
+ this.#options = options;
170
77
  this.control = new HostControlTransport({
171
78
  connected: hostVm.hasControlSocket,
172
79
  channel: hostVm,
173
80
  });
81
+ this.#exec = new ControlBackedSandboxExec(this.control, options.cwd);
82
+ this.#rootExec = new ControlBackedSandboxExec(this.control, "/");
174
83
  if (hostVm.terminateHostForTest !== undefined) {
175
84
  this.diagnostics = {
176
85
  terminateHostForTest: () => hostVm.terminateHostForTest?.() ?? Promise.resolve(),
@@ -182,65 +91,80 @@ class HostBackedSandboxVm {
182
91
  return;
183
92
  }
184
93
  this.#closed = true;
185
- await this.control.close();
186
- await this.#hostVm.close();
187
- }
188
- async [Symbol.asyncDispose]() {
189
- await this.close();
190
- }
191
- }
192
- class ConfiguredSandboxMounts {
193
- #mounts = new Map();
194
- #virtualMounts = new Map();
195
- #hostTools = new Map();
196
- constructor(mounts, bindings) {
197
- for (const mount of mounts) {
198
- this.#mounts.set(mount.path, mount.fileSystem);
199
- this.#virtualMounts.set(mount.path, mount.fileSystem);
200
- this.#hostTools.set(mount.path, createSandboxHostFileSystemTools(mount.fileSystem));
94
+ let syncError;
95
+ if (this.#options.rootfs.storage !== undefined) {
96
+ try {
97
+ const result = await this.#rootExec.exec("/bin/sync");
98
+ if (result.exitCode !== 0) {
99
+ throw new Error(`sandbox close sync failed with exit code ${result.exitCode}: ${result.stderr}`);
100
+ }
101
+ }
102
+ catch (error) {
103
+ syncError = error;
104
+ }
201
105
  }
202
- for (const binding of bindings) {
203
- this.#hostTools.set(binding.path, createSandboxHostFileSystemTools(binding.fileSystem));
106
+ try {
107
+ await this.control.close();
108
+ await this.#hostVm.close();
204
109
  }
205
- }
206
- get(path) {
207
- const mount = this.#mounts.get(path);
208
- if (mount === undefined) {
209
- throw new Error(`sandbox mount not found: ${path}`);
110
+ finally {
210
111
  }
211
- return mount;
212
- }
213
- virtualFs(path) {
214
- const mount = this.#virtualMounts.get(path);
215
- if (mount === undefined) {
216
- throw new Error(`virtualFs mount not found: ${path}`);
112
+ if (syncError !== undefined) {
113
+ throw syncError;
217
114
  }
218
- return mount;
219
115
  }
220
- host(path) {
221
- const tools = this.#hostTools.get(path);
222
- if (tools === undefined) {
223
- throw new Error(`host filesystem tools not found: ${path}`);
224
- }
225
- return tools;
116
+ async [Symbol.asyncDispose]() {
117
+ await this.close();
118
+ }
119
+ async exec(command, args = [], options = {}) {
120
+ return await this.#exec.exec(command, args, options);
121
+ }
122
+ }
123
+ class ControlBackedSandboxExec {
124
+ #control;
125
+ #cwd;
126
+ constructor(control, cwd) {
127
+ this.#control = control;
128
+ this.#cwd = cwd;
129
+ }
130
+ async exec(command, args = [], options = {}) {
131
+ const cwd = options.cwd ?? this.#cwd;
132
+ const env = cwd === undefined
133
+ ? options.env
134
+ : {
135
+ ...options.env,
136
+ SANDBOX_EXEC_CWD: cwd,
137
+ PWD: cwd,
138
+ };
139
+ const argv = cwd === undefined
140
+ ? [command, ...args]
141
+ : ["/bin/sh", "-lc", "cd \"$SANDBOX_EXEC_CWD\" && exec \"$@\"", "sandbox-exec", command, ...args];
142
+ const result = await this.#control.exec({
143
+ argv,
144
+ env,
145
+ });
146
+ return {
147
+ exitCode: result.exitCode,
148
+ stdout: result.stdout,
149
+ stderr: result.stderr,
150
+ };
226
151
  }
227
152
  }
228
153
  function toHostSpawnOptions(options, requestHeaderHooks) {
229
- const rootfs = lowerNativeRootfs(options.rootfs);
230
154
  if ((requestHeaderHooks.length > 0 || options.network?.http !== undefined)
231
155
  && options.network?.outbound === undefined) {
232
- throw new Error("invalid spawnSandbox options: network.outbound is required when HTTP interception is configured");
156
+ throw new Error("invalid sandbox options: network.outbound is required when HTTP interception is configured");
233
157
  }
234
158
  const network = options.network === undefined && requestHeaderHooks.length === 0
235
159
  ? undefined
236
160
  : {
237
161
  outbound: options.network?.outbound,
238
162
  http: requestHeaderHooks.length === 0
239
- && options.network?.http?.certificateAuthority === undefined
163
+ && options.network?.http?.caCertificatePem === undefined
240
164
  ? undefined
241
165
  : {
242
- caCertificatePem: options.network?.http?.certificateAuthority?.certificatePem,
243
- caPrivateKeyPem: options.network?.http?.certificateAuthority?.privateKeyPem,
166
+ caCertificatePem: options.network?.http?.caCertificatePem,
167
+ caPrivateKeyPem: options.network?.http?.caPrivateKeyPem,
244
168
  requestHeaderHooks: requestHeaderHooks.map((hook) => ({
245
169
  id: hook.id,
246
170
  origin: hook.selector.origin,
@@ -248,26 +172,22 @@ function toHostSpawnOptions(options, requestHeaderHooks) {
248
172
  },
249
173
  };
250
174
  return {
251
- name: options.name,
252
- cpu: options.cpu,
253
- memory: options.memory,
254
175
  kernel: {
255
- format: options.kernel.format,
176
+ format: undefined,
256
177
  },
257
- init: {
258
- crateName: options.init.crate,
178
+ cpu: {
179
+ vcpus: options.resources?.cpus,
180
+ },
181
+ memory: {
182
+ mib: options.resources?.memoryMiB,
259
183
  },
260
- rootfs: {
261
- path: rootfs.path,
262
- readonly: rootfs.readonly,
263
- format: rootfs.format,
184
+ init: {
185
+ crateName: "sandbox-init",
264
186
  },
265
- rootfsOverlay: options.rootfs.kind === "linux-overlay-fs"
266
- ? { mode: "writable" }
267
- : undefined,
187
+ rootfs: options.rootfs,
268
188
  mounts: options.mounts?.map((mount) => {
269
189
  return {
270
- kind: mount.kind,
190
+ kind: "virtual-fs",
271
191
  path: mount.path,
272
192
  writable: isSandboxWritableFileSystem(mount.fileSystem),
273
193
  };
@@ -275,56 +195,187 @@ function toHostSpawnOptions(options, requestHeaderHooks) {
275
195
  network,
276
196
  };
277
197
  }
278
- function lowerNativeRootfs(rootfs) {
279
- if (rootfs.kind === "prebuilt-rootfs") {
280
- return rootfs;
198
+ async function toInternalSandboxOptions(config, boot, network) {
199
+ const rootfs = await lowerRootfs(config.rootfs);
200
+ return {
201
+ resources: config.resources,
202
+ rootfs,
203
+ cwd: boot.cwd,
204
+ mounts: Object.entries(boot.mounts ?? {}).map(([path, source]) => {
205
+ return {
206
+ path,
207
+ fileSystem: source.fileSystem,
208
+ };
209
+ }),
210
+ network,
211
+ };
212
+ }
213
+ function createNetworkPolicyHookRegistration(policy) {
214
+ const hook = async (request) => {
215
+ const grants = [];
216
+ const connection = {
217
+ transport: "tcp",
218
+ host: request.url.hostname,
219
+ ip: request.destination.upstreamIp,
220
+ port: request.destination.upstreamPort,
221
+ allow() {
222
+ grants.push({ kind: "http" });
223
+ return {};
224
+ },
225
+ allowHttp(middleware) {
226
+ grants.push({ kind: "http", middleware });
227
+ return {};
228
+ },
229
+ };
230
+ await policy[networkPolicyHandler](connection);
231
+ if (grants.length === 0) {
232
+ throw new Error(`network connection denied: ${request.url.origin}`);
233
+ }
234
+ for (const grant of grants) {
235
+ if (grant.kind === "http") {
236
+ await grant.middleware?.(request);
237
+ }
238
+ }
239
+ };
240
+ const hooks = [
241
+ {
242
+ id: "network-policy-http",
243
+ selector: { origin: "http://*" },
244
+ hook,
245
+ active: true,
246
+ },
247
+ {
248
+ id: "network-policy-https",
249
+ selector: { origin: "https://*" },
250
+ hook,
251
+ active: true,
252
+ },
253
+ ];
254
+ return {
255
+ hooks,
256
+ network: {
257
+ outbound: {
258
+ policy: "deny",
259
+ rules: [
260
+ { action: "accept", scope: "public-internet", ports: [] },
261
+ { action: "accept", protocol: "udp", cidr: "10.0.2.1/32", ports: [53] },
262
+ ],
263
+ },
264
+ },
265
+ };
266
+ }
267
+ async function lowerRootfs(rootfs) {
268
+ switch (rootfs.kind) {
269
+ case "built-in-rootfs":
270
+ return {
271
+ path: builtInRootfsPath(rootfs.name),
272
+ readonly: true,
273
+ format: "erofs",
274
+ };
275
+ case "cow-rootfs":
276
+ return {
277
+ path: builtInRootfsPath(rootfs.base.name, "ext4"),
278
+ readonly: false,
279
+ format: "ext4",
280
+ storage: {
281
+ kind: "cow-block-store",
282
+ blockSize: rootfs.writable.blockSize,
283
+ blockStore: rootfs.writable,
284
+ context: {
285
+ base: builtInRootfsIdentity(rootfs.base.name, "ext4"),
286
+ },
287
+ },
288
+ };
289
+ }
290
+ }
291
+ function validateRootfs(rootfs) {
292
+ switch (rootfs.kind) {
293
+ case "built-in-rootfs":
294
+ validateBuiltInRootfsName(rootfs.name);
295
+ return;
296
+ case "cow-rootfs":
297
+ if (rootfs.base.kind !== "built-in-rootfs") {
298
+ throw new Error("invalid sandbox definition: rootfs.cow base must be created with rootfs.builtIn(...)");
299
+ }
300
+ validateBuiltInRootfsName(rootfs.base.name);
301
+ validateBlockStore(rootfs.writable);
302
+ return;
303
+ default:
304
+ throw new Error("invalid sandbox definition: rootfs must be created with rootfs.builtIn(...) or rootfs.cow(...)");
281
305
  }
282
- if (rootfs.lower.kind !== "prebuilt-rootfs") {
283
- throw new Error(`rootfs ${rootfs.kind} lower ${rootfs.lower.kind} is not implemented yet`);
306
+ }
307
+ function validateBlockStore(blockStore) {
308
+ if (!Number.isInteger(blockStore.blockSize) || blockStore.blockSize <= 0) {
309
+ throw new Error("invalid sandbox definition: rootfs COW block size must be a positive integer");
284
310
  }
285
- if (rootfs.upper.kind !== "scratch-fs") {
286
- throw new Error(`rootfs ${rootfs.kind} upper ${rootfs.upper.kind} is not implemented yet`);
311
+ if (blockStore.blockSize % 512 !== 0) {
312
+ throw new Error("invalid sandbox definition: rootfs COW block size must be a multiple of 512 bytes");
313
+ }
314
+ if (typeof blockStore.list !== "function") {
315
+ throw new Error("invalid sandbox definition: rootfs COW block store must provide list()");
316
+ }
317
+ if (typeof blockStore.read !== "function") {
318
+ throw new Error("invalid sandbox definition: rootfs COW block store must provide read()");
319
+ }
320
+ if (typeof blockStore.write !== "function") {
321
+ throw new Error("invalid sandbox definition: rootfs COW block store must provide write()");
322
+ }
323
+ }
324
+ function validateSandboxDefinitionOptions(options) {
325
+ validateRootfs(options.rootfs);
326
+ if (options.resources?.cpus !== undefined && (!Number.isInteger(options.resources.cpus) || options.resources.cpus <= 0)) {
327
+ throw new Error("invalid sandbox definition: resources.cpus must be a positive integer");
328
+ }
329
+ if (options.resources?.cpus !== undefined && options.resources.cpus > 255) {
330
+ throw new Error("invalid sandbox definition: resources.cpus must be less than or equal to 255");
331
+ }
332
+ if (options.resources?.memoryMiB !== undefined
333
+ && (!Number.isInteger(options.resources.memoryMiB) || options.resources.memoryMiB <= 0)) {
334
+ throw new Error("invalid sandbox definition: resources.memoryMiB must be a positive integer");
335
+ }
336
+ if (options.network !== undefined && options.network.kind !== "network-policy") {
337
+ throw new Error("invalid sandbox definition: network must be created with network.policy(...)");
338
+ }
339
+ }
340
+ function validateBuiltInRootfsName(name) {
341
+ if (name !== "alpine:3.20") {
342
+ throw new Error(`unsupported built-in rootfs: ${name}`);
287
343
  }
288
- return {
289
- ...rootfs.lower,
290
- readonly: true,
291
- };
292
344
  }
293
- function validateSandboxOptions(options) {
294
- if (options.cpu?.vcpus !== undefined && (!Number.isInteger(options.cpu.vcpus) || options.cpu.vcpus <= 0)) {
295
- throw new Error("invalid spawnSandbox options: cpu.vcpus must be greater than zero");
345
+ function validateSandboxBootOptions(options) {
346
+ const mountPaths = new Set();
347
+ for (const [path, source] of Object.entries(options.mounts ?? {})) {
348
+ validateGuestPath(path, "mount.path");
349
+ if (mountPaths.has(path)) {
350
+ throw new Error(`invalid sandbox boot options: duplicate mount path: ${path}`);
351
+ }
352
+ if (isSandboxWritableFileSystem(source.fileSystem)
353
+ && !isSandboxPosixFileSystem(source.fileSystem)) {
354
+ throw new Error(`invalid sandbox boot options: writable mount must implement the POSIX filesystem interface: ${path}`);
355
+ }
356
+ mountPaths.add(path);
296
357
  }
297
- if (options.cpu?.vcpus !== undefined && options.cpu.vcpus > 255) {
298
- throw new Error("invalid spawnSandbox options: cpu.vcpus must be less than or equal to 255");
358
+ if (options.cwd !== undefined && !options.cwd.startsWith("/")) {
359
+ throw new Error("invalid sandbox boot options: cwd must be absolute");
299
360
  }
300
- if (options.memory?.mib !== undefined && (!Number.isInteger(options.memory.mib) || options.memory.mib <= 0)) {
301
- throw new Error("invalid spawnSandbox options: memory.mib must be greater than zero");
361
+ }
362
+ function validateInternalSandboxOptions(options) {
363
+ if (options.rootfs.path.length === 0) {
364
+ throw new Error("invalid sandbox options: rootfs.path must not be empty");
302
365
  }
303
- if (options.init.crate !== "sandbox-init") {
304
- throw new Error(`invalid spawnSandbox options: unsupported init crate: ${options.init.crate}`);
366
+ if (options.rootfs.format !== "erofs" && options.rootfs.format !== "ext4") {
367
+ throw new Error("invalid sandbox options: rootfs.format must be erofs or ext4");
305
368
  }
306
- validateRootfsConfig(options.rootfs, "rootfs");
307
369
  const mountPaths = new Set();
308
370
  for (const mount of options.mounts ?? []) {
309
371
  validateGuestPath(mount.path, "mount.path");
310
372
  if (mountPaths.has(mount.path)) {
311
- throw new Error(`invalid spawnSandbox options: duplicate mount path: ${mount.path}`);
373
+ throw new Error(`invalid sandbox options: duplicate mount path: ${mount.path}`);
312
374
  }
313
375
  mountPaths.add(mount.path);
314
376
  }
315
- const bindingPaths = new Set();
316
- for (const binding of options.bindings ?? []) {
317
- validateGuestPath(binding.path, "binding.path");
318
- if (mountPaths.has(binding.path)) {
319
- throw new Error(`invalid spawnSandbox options: binding path conflicts with mount path: ${binding.path}`);
320
- }
321
- if (bindingPaths.has(binding.path)) {
322
- throw new Error(`invalid spawnSandbox options: duplicate binding path: ${binding.path}`);
323
- }
324
- bindingPaths.add(binding.path);
325
- }
326
377
  if (options.network?.outbound?.policy !== undefined && options.network.outbound.policy !== "deny") {
327
- throw new Error("invalid spawnSandbox options: network.outbound.policy must be deny");
378
+ throw new Error("invalid sandbox options: network.outbound.policy must be deny");
328
379
  }
329
380
  for (const rule of options.network?.outbound?.rules ?? []) {
330
381
  if ("cidr" in rule) {
@@ -333,71 +384,50 @@ function validateSandboxOptions(options) {
333
384
  validateOutboundPorts(rule.ports);
334
385
  }
335
386
  }
336
- function validateRootfsConfig(rootfs, field) {
337
- if (rootfs.kind === "prebuilt-rootfs") {
338
- if (rootfs.path.length === 0) {
339
- throw new Error(`invalid spawnSandbox options: ${field}.path must not be empty`);
340
- }
341
- if (rootfs.format === "directory") {
342
- const prefix = field === "rootfs" ? "" : `${field} `;
343
- throw new Error(`invalid spawnSandbox options: ${prefix}directory rootfs is not supported for sandboxed VM launch; use an EROFS rootfs`);
344
- }
345
- return;
346
- }
347
- if (rootfs.kind === "linux-overlay-fs") {
348
- validateRootfsConfig(rootfs.lower, `${field}.lower`);
349
- validateRootfsConfig(rootfs.upper, `${field}.upper`);
350
- return;
351
- }
352
- if (rootfs.kind === "scratch-fs") {
353
- return;
354
- }
355
- throw new Error(`invalid spawnSandbox options: unsupported ${field} kind`);
356
- }
357
387
  function validateGuestPath(path, field) {
358
388
  if (!path.startsWith("/")) {
359
- throw new Error(`invalid spawnSandbox options: ${field} must be absolute`);
389
+ throw new Error(`invalid sandbox options: ${field} must be absolute`);
360
390
  }
361
391
  if (path === "/") {
362
- throw new Error(`invalid spawnSandbox options: ${field} must not be root`);
392
+ throw new Error(`invalid sandbox options: ${field} must not be root`);
363
393
  }
364
394
  if (path.includes("\0")) {
365
- throw new Error(`invalid spawnSandbox options: ${field} must not contain NUL bytes`);
395
+ throw new Error(`invalid sandbox options: ${field} must not contain NUL bytes`);
366
396
  }
367
397
  if (path.split("/").some((component) => component === "." || component === "..")) {
368
- throw new Error(`invalid spawnSandbox options: ${field} must not contain '.' or '..' components`);
398
+ throw new Error(`invalid sandbox options: ${field} must not contain '.' or '..' components`);
369
399
  }
370
400
  }
371
401
  function validateOutboundPorts(ports) {
372
402
  for (const port of ports ?? []) {
373
403
  if (!Number.isInteger(port) || port < 1 || port > 65_535) {
374
- throw new Error(`invalid spawnSandbox options: invalid outbound network port: ${port}`);
404
+ throw new Error(`invalid sandbox options: invalid outbound network port: ${port}`);
375
405
  }
376
406
  }
377
407
  }
378
408
  function validateCidr(range) {
379
409
  const [address, prefixText, extra] = range.split("/");
380
410
  if (address === undefined || prefixText === undefined || extra !== undefined) {
381
- throw new Error(`invalid spawnSandbox options: invalid CIDR range: ${range}`);
411
+ throw new Error(`invalid sandbox options: invalid CIDR range: ${range}`);
382
412
  }
383
413
  const prefix = Number(prefixText);
384
414
  if (!Number.isInteger(prefix)) {
385
- throw new Error(`invalid spawnSandbox options: invalid CIDR prefix: ${range}`);
415
+ throw new Error(`invalid sandbox options: invalid CIDR prefix: ${range}`);
386
416
  }
387
417
  if (address.includes(":")) {
388
418
  if (prefix < 0 || prefix > 128) {
389
- throw new Error(`invalid spawnSandbox options: invalid CIDR prefix: ${range}`);
419
+ throw new Error(`invalid sandbox options: invalid CIDR prefix: ${range}`);
390
420
  }
391
421
  if (parseIpv6Address(address) === null) {
392
- throw new Error(`invalid spawnSandbox options: invalid CIDR address: ${range}`);
422
+ throw new Error(`invalid sandbox options: invalid CIDR address: ${range}`);
393
423
  }
394
- throw new Error(`invalid spawnSandbox options: IPv6 outbound CIDR ranges are not supported yet: ${range}`);
424
+ throw new Error(`invalid sandbox options: IPv6 outbound CIDR ranges are not supported yet: ${range}`);
395
425
  }
396
426
  if (prefix < 0 || prefix > 32) {
397
- throw new Error(`invalid spawnSandbox options: invalid CIDR prefix: ${range}`);
427
+ throw new Error(`invalid sandbox options: invalid CIDR prefix: ${range}`);
398
428
  }
399
429
  if (parseIpv4Address(address) === null) {
400
- throw new Error(`invalid spawnSandbox options: invalid CIDR address: ${range}`);
430
+ throw new Error(`invalid sandbox options: invalid CIDR address: ${range}`);
401
431
  }
402
432
  }
403
433
  function parseIpv4Address(address) {