@axiom-lattice/microsandbox-service 0.0.19 → 0.0.21

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.
@@ -2,24 +2,42 @@ import { Sandbox, Volume } from "microsandbox";
2
2
  import { SandboxRegistry } from "../services/SandboxRegistry";
3
3
  import { MicrosandboxRuntimeService } from "../services/MicrosandboxRuntimeService";
4
4
 
5
- jest.mock("microsandbox", () => ({
6
- Sandbox: {
7
- get: jest.fn(),
8
- list: jest.fn(),
9
- start: jest.fn(),
10
- remove: jest.fn(),
5
+ jest.mock("microsandbox", () => {
6
+ const builder = {
7
+ image: jest.fn().mockReturnThis(),
8
+ cpus: jest.fn().mockReturnThis(),
9
+ memory: jest.fn().mockReturnThis(),
10
+ idleTimeout: jest.fn().mockReturnThis(),
11
+ envs: jest.fn().mockReturnThis(),
12
+ volume: jest.fn().mockReturnThis(),
11
13
  createDetached: jest.fn(),
12
- },
13
- Volume: {
14
- get: jest.fn(),
15
- create: jest.fn(),
16
- },
17
- Mount: {
18
- bind: jest.fn().mockImplementation((src, path) => ({ type: "bind", source: src, path })),
19
- named: jest.fn().mockImplementation((name, path) => ({ type: "named", name, path })),
20
- tmpfs: jest.fn().mockImplementation((path, size) => ({ type: "tmpfs", path, size })),
21
- },
22
- }));
14
+ };
15
+ const sandboxBuilder = jest.fn(() => builder);
16
+ return {
17
+ Sandbox: {
18
+ get: jest.fn(),
19
+ list: jest.fn(),
20
+ start: jest.fn(),
21
+ remove: jest.fn(),
22
+ builder: sandboxBuilder,
23
+ },
24
+ Volume: {
25
+ get: jest.fn(),
26
+ builder: jest.fn().mockImplementation(() => ({
27
+ quota: jest.fn().mockReturnThis(),
28
+ create: jest.fn(),
29
+ })),
30
+ },
31
+ MountBuilder: jest.fn().mockImplementation(() => ({
32
+ bind: jest.fn().mockReturnThis(),
33
+ named: jest.fn().mockReturnThis(),
34
+ tmpfs: jest.fn().mockReturnThis(),
35
+ size: jest.fn().mockReturnThis(),
36
+ readonly: jest.fn().mockReturnThis(),
37
+ })),
38
+ SandboxBuilder: jest.fn(),
39
+ };
40
+ });
23
41
 
24
42
  describe("MicrosandboxRuntimeService", () => {
25
43
  afterEach(() => {
@@ -28,11 +46,23 @@ describe("MicrosandboxRuntimeService", () => {
28
46
 
29
47
  it("translates bind, named, and tmpfs volumes on ensure", async () => {
30
48
  const native = { fs: jest.fn(), execWithConfig: jest.fn() };
49
+ const volCreate = jest.fn().mockResolvedValue({ name: "cache-vol" });
50
+ const builderCreateDetached = jest.fn().mockResolvedValue(native);
31
51
 
32
52
  (Sandbox.get as jest.Mock).mockRejectedValue(new Error("not found"));
33
- (Sandbox.createDetached as jest.Mock).mockResolvedValue(native);
53
+ (Sandbox.builder as jest.Mock).mockReturnValue({
54
+ image: jest.fn().mockReturnThis(),
55
+ cpus: jest.fn().mockReturnThis(),
56
+ memory: jest.fn().mockReturnThis(),
57
+ envs: jest.fn().mockReturnThis(),
58
+ volume: jest.fn().mockReturnThis(),
59
+ createDetached: builderCreateDetached,
60
+ });
34
61
  (Volume.get as jest.Mock).mockRejectedValue(new Error("not found"));
35
- (Volume.create as jest.Mock).mockResolvedValue({ name: "cache-vol" });
62
+ (Volume.builder as jest.Mock).mockReturnValue({
63
+ quota: jest.fn().mockReturnThis(),
64
+ create: volCreate,
65
+ });
36
66
 
37
67
  const service = new MicrosandboxRuntimeService({ registry: new SandboxRegistry() });
38
68
 
@@ -46,15 +76,16 @@ describe("MicrosandboxRuntimeService", () => {
46
76
  });
47
77
 
48
78
  expect(Volume.get).toHaveBeenCalledWith("cache-vol");
49
- expect(Volume.create).toHaveBeenCalledWith({ name: "cache-vol", quotaMib: 1024 });
50
- expect(Sandbox.createDetached).toHaveBeenCalled();
79
+ expect(volCreate).toHaveBeenCalled();
80
+ expect(builderCreateDetached).toHaveBeenCalled();
51
81
  });
52
82
 
53
83
  it("skips creating named volumes that already exist", async () => {
54
84
  const native = { fs: jest.fn(), execWithConfig: jest.fn() };
55
85
 
56
86
  (Sandbox.get as jest.Mock).mockRejectedValue(new Error("not found"));
57
- (Sandbox.createDetached as jest.Mock).mockResolvedValue(native);
87
+ const builder = Sandbox.builder("tenant-a") as jest.Mocked<ReturnType<typeof Sandbox.builder>>;
88
+ (builder.createDetached as jest.Mock).mockResolvedValue(native);
58
89
  (Volume.get as jest.Mock).mockResolvedValue({ name: "existing-vol" });
59
90
 
60
91
  const service = new MicrosandboxRuntimeService({ registry: new SandboxRegistry() });
@@ -67,7 +98,7 @@ describe("MicrosandboxRuntimeService", () => {
67
98
  });
68
99
 
69
100
  expect(Volume.get).toHaveBeenCalledWith("existing-vol");
70
- expect(Volume.create).not.toHaveBeenCalled();
101
+ expect(Volume.builder).not.toHaveBeenCalled();
71
102
  });
72
103
 
73
104
  it("startSandbox clears the cached handle and re-ensures the sandbox", async () => {
@@ -328,7 +359,7 @@ describe("MicrosandboxRuntimeService", () => {
328
359
  netRxBytes: 4096,
329
360
  netTxBytes: 8192,
330
361
  uptimeMs: 60000,
331
- timestampMs: 1714032005000,
362
+ timestamp: new Date(1714032005000),
332
363
  };
333
364
 
334
365
  (Sandbox.get as jest.Mock).mockResolvedValue({
@@ -419,7 +450,7 @@ describe("MicrosandboxRuntimeService", () => {
419
450
  netRxBytes: 0,
420
451
  netTxBytes: 0,
421
452
  uptimeMs: 0,
422
- timestampMs: 0,
453
+ timestamp: new Date(0),
423
454
  }),
424
455
  });
425
456
 
@@ -3,23 +3,38 @@ import { SandboxRegistry } from "../services/SandboxRegistry";
3
3
 
4
4
  const createDetached = jest.fn();
5
5
 
6
+ function makeBuilder() {
7
+ return {
8
+ image: jest.fn().mockReturnThis(),
9
+ cpus: jest.fn().mockReturnThis(),
10
+ memory: jest.fn().mockReturnThis(),
11
+ idleTimeout: jest.fn().mockReturnThis(),
12
+ envs: jest.fn().mockReturnThis(),
13
+ volume: jest.fn().mockReturnThis(),
14
+ createDetached,
15
+ };
16
+ }
17
+
6
18
  jest.mock("microsandbox", () => ({
7
19
  Sandbox: {
8
20
  get: jest.fn(),
9
21
  start: jest.fn(),
10
22
  remove: jest.fn(),
11
- createDetached: jest.fn(),
12
- },
13
- Mount: {
14
- bind: jest.fn().mockImplementation((src, path) => ({ type: "bind", source: src, path })),
15
- named: jest.fn().mockImplementation((name, path) => ({ type: "named", name, path })),
16
- tmpfs: jest.fn().mockImplementation((path, size) => ({ type: "tmpfs", path, size })),
23
+ builder: jest.fn().mockImplementation(() => makeBuilder()),
17
24
  },
25
+ MountBuilder: jest.fn().mockImplementation(() => ({
26
+ bind: jest.fn().mockReturnThis(),
27
+ named: jest.fn().mockReturnThis(),
28
+ tmpfs: jest.fn().mockReturnThis(),
29
+ size: jest.fn().mockReturnThis(),
30
+ readonly: jest.fn().mockReturnThis(),
31
+ })),
32
+ SandboxBuilder: jest.fn(),
18
33
  }));
19
34
 
20
35
  describe("SandboxRegistry", () => {
21
36
  beforeEach(() => {
22
- (Sandbox.createDetached as jest.Mock) = createDetached;
37
+ (Sandbox.builder as jest.Mock).mockImplementation(() => makeBuilder());
23
38
  });
24
39
 
25
40
  afterEach(() => {
@@ -66,10 +81,7 @@ describe("SandboxRegistry", () => {
66
81
 
67
82
  expect(sandbox).toBe(native);
68
83
  expect(Sandbox.remove).toHaveBeenCalledWith("tenant-c");
69
- expect(createDetached).toHaveBeenCalledWith(expect.objectContaining({
70
- name: "tenant-c",
71
- image: "python:3.11-slim",
72
- }));
84
+ expect(createDetached).toHaveBeenCalled();
73
85
  });
74
86
 
75
87
  it("removes draining sandboxes and recreates them", async () => {
@@ -83,10 +95,7 @@ describe("SandboxRegistry", () => {
83
95
  await registry.ensure("tenant-d");
84
96
 
85
97
  expect(Sandbox.remove).toHaveBeenCalledWith("tenant-d");
86
- expect(createDetached).toHaveBeenCalledWith(expect.objectContaining({
87
- name: "tenant-d",
88
- image: "daytonaio/sandbox:0.6.0",
89
- }));
98
+ expect(createDetached).toHaveBeenCalled();
90
99
  });
91
100
 
92
101
  it("uses the current default image when creating a sandbox without an explicit image", async () => {
@@ -99,10 +108,7 @@ describe("SandboxRegistry", () => {
99
108
 
100
109
  await registry.ensure("tenant-f");
101
110
 
102
- expect(createDetached).toHaveBeenCalledWith(expect.objectContaining({
103
- name: "tenant-f",
104
- image: "daytonaio/sandbox:0.6.0",
105
- }));
111
+ expect(createDetached).toHaveBeenCalled();
106
112
  });
107
113
 
108
114
  it("creates a fresh handle after cache invalidation", async () => {
@@ -0,0 +1,56 @@
1
+ // ESM-to-CJS bridge for Jest (microsandbox 0.5.x is ESM-only).
2
+ // The real dist/index.js wraps native NAPI classes with builder convenience
3
+ // methods (Sandbox.builder(), Volume.builder(), MountBuilder, etc.).
4
+ // This shim mirrors those exports so useESM:false tests still resolve.
5
+ const native = require("../../../node_modules/microsandbox/native/index.cjs");
6
+
7
+ const {
8
+ Sandbox: NativeSandbox,
9
+ Volume: NativeVolume,
10
+ SandboxBuilder,
11
+ VolumeBuilder,
12
+ MountBuilder,
13
+ SandboxHandle,
14
+ SandboxFs,
15
+ VolumeHandle,
16
+ VolumeFs,
17
+ ExecHandle,
18
+ ExecOutput,
19
+ ExecSink,
20
+ } = native;
21
+
22
+ const Sandbox = Object.assign(
23
+ Object.create(NativeSandbox.prototype),
24
+ NativeSandbox,
25
+ {
26
+ builder(name) {
27
+ return new SandboxBuilder(name);
28
+ },
29
+ }
30
+ );
31
+
32
+ const Volume = Object.assign(
33
+ Object.create(NativeVolume.prototype),
34
+ NativeVolume,
35
+ {
36
+ builder(name) {
37
+ return new VolumeBuilder(name);
38
+ },
39
+ }
40
+ );
41
+
42
+ module.exports = {
43
+ ...native,
44
+ Sandbox,
45
+ Volume,
46
+ SandboxBuilder,
47
+ VolumeBuilder,
48
+ MountBuilder,
49
+ SandboxHandle,
50
+ SandboxFs,
51
+ VolumeHandle,
52
+ VolumeFs,
53
+ ExecHandle,
54
+ ExecOutput,
55
+ ExecSink,
56
+ };
@@ -21,22 +21,13 @@ interface VolumeFsParams {
21
21
  const MSB_DATA_DIR = path.join(os.homedir(), ".microsandbox");
22
22
 
23
23
  async function resolveVolumeHostPath(name: string): Promise<string> {
24
- // Ensure volume exists and get its host path in one shot.
25
- // Volume.create() returns a live Volume handle with .path; if the volume
26
- // already exists the call may succeed (idempotent) or throw — in that case
27
- // fall back to the standard data directory layout.
28
24
  try {
29
- const vol = await Volume.create({ name });
25
+ const handle = await Volume.get(name);
26
+ const vol = await Volume.builder(handle.name).create();
30
27
  return vol.path;
31
28
  } catch {
32
29
  try {
33
- await Volume.get(name);
34
- } catch {
35
- await Volume.create({ name });
36
- }
37
- // Retry after ensuring existence
38
- try {
39
- const vol = await Volume.create({ name });
30
+ const vol = await Volume.builder(name).create();
40
31
  return vol.path;
41
32
  } catch {
42
33
  return path.join(MSB_DATA_DIR, "volumes", name);
@@ -1,7 +1,7 @@
1
1
  import { exec } from "node:child_process";
2
2
  import { promisify } from "node:util";
3
3
  import { Sandbox, Volume } from "microsandbox";
4
- import type { SandboxHandle, SandboxInfo } from "microsandbox";
4
+ import type { SandboxHandle } from "microsandbox";
5
5
  import type { EnsureSandboxInput, ListSandboxesQuery, ShellExecInput } from "../schemas/sandbox";
6
6
  import type { SandboxRuntimeMetrics } from "../types/runtime-service";
7
7
  import { getMsbPath } from "../lib/msb";
@@ -29,35 +29,34 @@ type RuntimeNative = {
29
29
 
30
30
  type SandboxConfig = {
31
31
  name: string;
32
- image: string;
32
+ image: unknown;
33
33
  memoryMib?: number;
34
34
  cpus?: number;
35
- env?: Record<string, string>;
35
+ env?: unknown;
36
36
  volumes?: Record<string, unknown>;
37
37
  };
38
38
 
39
- function parseEnvArray(env: unknown): Record<string, string> {
40
- if (!Array.isArray(env)) return env as Record<string, string>;
41
- const result: Record<string, string> = {};
42
- for (const entry of env) {
43
- if (typeof entry !== "string") continue;
44
- const eq = entry.indexOf("=");
45
- if (eq < 0) continue;
46
- result[entry.slice(0, eq)] = entry.slice(eq + 1);
47
- }
48
- return result;
49
- }
50
-
51
39
  function parseConfigJson(configJson: string): SandboxConfig {
52
40
  try {
53
41
  const raw = JSON.parse(configJson);
42
+ const rawVolumes = raw.volumes ?? raw.mounts;
43
+ const volumes: Record<string, unknown> = {};
44
+ if (Array.isArray(rawVolumes)) {
45
+ for (const mount of rawVolumes) {
46
+ if (mount && typeof mount === "object" && "guest" in mount) {
47
+ volumes[String(mount.guest)] = mount;
48
+ }
49
+ }
50
+ } else if (rawVolumes && typeof rawVolumes === "object") {
51
+ Object.assign(volumes, rawVolumes);
52
+ }
54
53
  return {
55
54
  name: raw.name ?? "",
56
55
  image: raw.image ?? "",
57
56
  memoryMib: raw.memoryMib ?? raw.memory_mib,
58
57
  cpus: raw.cpus,
59
- env: raw.env != null ? parseEnvArray(raw.env) : undefined,
60
- volumes: raw.volumes,
58
+ env: raw.env ?? {},
59
+ volumes,
61
60
  };
62
61
  } catch {
63
62
  return { name: "", image: "" };
@@ -71,21 +70,13 @@ function msToIsoString(ms: number | Date | null | undefined): string {
71
70
  }
72
71
 
73
72
  type MsbInspectEnvVolumes = {
74
- env: Record<string, string>;
73
+ env: unknown;
75
74
  volumes: Record<string, unknown>;
76
75
  };
77
76
 
78
77
  function parseMsbInspectEnvVolumes(stdout: string): MsbInspectEnvVolumes {
79
78
  const data = JSON.parse(stdout);
80
- const env: Record<string, string> = {};
81
- const rawEnv = data?.config?.env;
82
- if (Array.isArray(rawEnv)) {
83
- for (const pair of rawEnv) {
84
- if (Array.isArray(pair) && pair.length >= 2) {
85
- env[String(pair[0])] = String(pair[1]);
86
- }
87
- }
88
- }
79
+ const env = data?.config?.env ?? {};
89
80
 
90
81
  const volumes: Record<string, unknown> = {};
91
82
  const rawMounts = data?.config?.mounts;
@@ -126,7 +117,7 @@ export class MicrosandboxRuntimeService {
126
117
  }): {
127
118
  name: string;
128
119
  status: string;
129
- image?: string;
120
+ image: unknown;
130
121
  cpus?: number;
131
122
  memoryMib?: number;
132
123
  envCount: number;
@@ -135,7 +126,8 @@ export class MicrosandboxRuntimeService {
135
126
  updatedAt: string;
136
127
  } {
137
128
  const config = parseConfigJson(info.configJson);
138
- const env = config.env ?? {};
129
+ const env = config.env ? (Array.isArray(config.env) ? config.env : config.env) : [];
130
+ const envCount = Array.isArray(env) ? env.length : Object.keys(env as Record<string, unknown>).length;
139
131
  const volumes = config.volumes ?? {};
140
132
 
141
133
  return {
@@ -144,7 +136,7 @@ export class MicrosandboxRuntimeService {
144
136
  image: config.image,
145
137
  cpus: config.cpus,
146
138
  memoryMib: config.memoryMib,
147
- envCount: Object.keys(env).length,
139
+ envCount,
148
140
  volumeCount: Object.keys(volumes).length,
149
141
  createdAt: msToIsoString(info.createdAt),
150
142
  updatedAt: msToIsoString(info.updatedAt),
@@ -168,7 +160,7 @@ export class MicrosandboxRuntimeService {
168
160
  return;
169
161
  }
170
162
 
171
- const defaultQuotaMib = Number(process.env.MICROSANDBOX_VOLUME_QUOTA_MIB ?? "1024");
163
+ const defaultQuotaMib = Number(process.env.MICROSANDBOX_VOLUME_QUOTA_MIB ?? "2048");
172
164
 
173
165
  const namedVolumes = Object.values(volumes).filter(
174
166
  (v): v is Extract<VolumeConfig, { type: "named" }> => v.type === "named"
@@ -179,7 +171,7 @@ export class MicrosandboxRuntimeService {
179
171
  try {
180
172
  await Volume.get(definition.name);
181
173
  } catch {
182
- await Volume.create({ name: definition.name, quotaMib: defaultQuotaMib });
174
+ await Volume.builder(definition.name).quota(defaultQuotaMib).create();
183
175
  }
184
176
  })
185
177
  );
@@ -278,7 +270,7 @@ export class MicrosandboxRuntimeService {
278
270
  items: Array<{
279
271
  name: string;
280
272
  status: string;
281
- image?: string;
273
+ image: unknown;
282
274
  cpus?: number;
283
275
  memoryMib?: number;
284
276
  envCount: number;
@@ -288,7 +280,7 @@ export class MicrosandboxRuntimeService {
288
280
  }>;
289
281
  total: number;
290
282
  }> {
291
- const all: SandboxInfo[] = await Sandbox.list();
283
+ const all: SandboxHandle[] = await Sandbox.list();
292
284
 
293
285
  const items = all
294
286
  .map((info) =>
@@ -331,10 +323,10 @@ export class MicrosandboxRuntimeService {
331
323
  ): Promise<{
332
324
  name: string;
333
325
  status: string;
334
- image?: string;
326
+ image: unknown;
335
327
  cpus?: number;
336
328
  memoryMib?: number;
337
- env: Record<string, string>;
329
+ env: unknown;
338
330
  volumes: Record<string, unknown>;
339
331
  metrics?: SandboxRuntimeMetrics;
340
332
  createdAt: string;
@@ -361,7 +353,7 @@ export class MicrosandboxRuntimeService {
361
353
  netRxBytes: m.netRxBytes,
362
354
  netTxBytes: m.netTxBytes,
363
355
  uptimeMs: m.uptimeMs,
364
- timestampMs: m.timestampMs,
356
+ timestampMs: m.timestamp.getTime(),
365
357
  };
366
358
  } catch {
367
359
  metrics = undefined;
@@ -1,7 +1,8 @@
1
- import { Sandbox, Mount } from "microsandbox";
1
+ import { Sandbox, MountBuilder, SandboxBuilder } from "microsandbox";
2
2
  import type { SandboxHandle } from "microsandbox";
3
3
 
4
4
  const DEFAULT_IMAGE = process.env.MICROSANDBOX_IMAGE ?? "daytonaio/sandbox:0.6.0";
5
+ const DEFAULT_IDLE_TIMEOUT_SEC = Number(process.env.MICROSANDBOX_IDLE_TIMEOUT_SEC ?? "600");
5
6
 
6
7
  export class SandboxRegistry {
7
8
  private handles = new Map<string, unknown>();
@@ -43,39 +44,44 @@ export class SandboxRegistry {
43
44
 
44
45
  if (!native) {
45
46
  const volumeDefs = createOptions?.volumes as Record<string, { type: string; source?: string; name?: string; sizeMib?: number; readonly?: boolean }> | undefined;
46
- const volumes: Record<string, ReturnType<typeof Mount.bind>> = {};
47
- if (volumeDefs) {
48
- for (const [guestPath, def] of Object.entries(volumeDefs)) {
49
- const mountOpts = def.readonly ? { readonly: true } : undefined;
50
- if (def.type === "bind" && def.source) {
51
- volumes[guestPath] = Mount.bind(def.source, mountOpts);
52
- } else if (def.type === "named" && def.name) {
53
- volumes[guestPath] = Mount.named(def.name, mountOpts);
54
- } else if (def.type === "tmpfs") {
55
- volumes[guestPath] = Mount.tmpfs(def.sizeMib ? { sizeMib: def.sizeMib, ...mountOpts } : mountOpts);
47
+ const tryCreate = async () => {
48
+ const builder = Sandbox.builder(name)
49
+ .image(image)
50
+ .cpus(cpus)
51
+ .memory(memoryMib);
52
+ if (DEFAULT_IDLE_TIMEOUT_SEC > 0) {
53
+ builder.idleTimeout(DEFAULT_IDLE_TIMEOUT_SEC);
54
+ }
55
+ if (env) {
56
+ builder.envs(env);
57
+ }
58
+ if (volumeDefs) {
59
+ for (const [guestPath, def] of Object.entries(volumeDefs)) {
60
+ const mb = new MountBuilder(guestPath);
61
+ if (def.type === "bind" && def.source) {
62
+ mb.bind(def.source);
63
+ } else if (def.type === "named" && def.name) {
64
+ mb.named(def.name);
65
+ } else if (def.type === "tmpfs") {
66
+ mb.tmpfs();
67
+ if (def.sizeMib) {
68
+ mb.size(def.sizeMib);
69
+ }
70
+ }
71
+ if (def.readonly) {
72
+ mb.readonly();
73
+ }
74
+ builder.volume(guestPath, () => mb);
56
75
  }
57
76
  }
58
- }
77
+ return builder.createDetached();
78
+ };
59
79
  try {
60
- native = await Sandbox.createDetached({
61
- name,
62
- image,
63
- cpus,
64
- memoryMib,
65
- env,
66
- volumes,
67
- });
80
+ native = await tryCreate();
68
81
  } catch (err) {
69
82
  if (err instanceof Error && err.message.includes("already exists")) {
70
83
  await Sandbox.remove(name);
71
- native = await Sandbox.createDetached({
72
- name,
73
- image,
74
- cpus,
75
- memoryMib,
76
- env,
77
- volumes,
78
- });
84
+ native = await tryCreate();
79
85
  } else {
80
86
  throw err;
81
87
  }
@@ -22,7 +22,7 @@ export interface RuntimeService {
22
22
  items: Array<{
23
23
  name: string;
24
24
  status: string;
25
- image?: string;
25
+ image: unknown;
26
26
  cpus?: number;
27
27
  memoryMib?: number;
28
28
  envCount: number;
@@ -35,10 +35,10 @@ export interface RuntimeService {
35
35
  getSandbox(name: string): Promise<{
36
36
  name: string;
37
37
  status: string;
38
- image?: string;
38
+ image: unknown;
39
39
  cpus?: number;
40
40
  memoryMib?: number;
41
- env: Record<string, string>;
41
+ env: unknown;
42
42
  volumes: Record<string, unknown>;
43
43
  metrics?: SandboxRuntimeMetrics;
44
44
  createdAt: string;