@claxedo/sandbox-manager 0.7.0 → 0.9.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.
@@ -1,7 +1,54 @@
1
- // src/drivers/vercel.ts
2
- import { Sandbox } from "@vercel/sandbox";
1
+ // src/constants.ts
2
+ var DEFAULT_WORKSPACE_RUNTIME_PORT = 2593;
3
+
4
+ // src/hosted-network-policy.ts
5
+ var SANDBOX_MODEL_PROVIDER_HOSTS = [
6
+ "api.anthropic.com",
7
+ "api.openai.com",
8
+ "generativelanguage.googleapis.com",
9
+ "openrouter.ai",
10
+ // Model catalog + engine config, fetched on runner startup.
11
+ "models.dev",
12
+ "opencode.ai"
13
+ ];
14
+ var SANDBOX_PACKAGE_REGISTRY_HOSTS = [
15
+ "registry.npmjs.org",
16
+ "nodejs.org",
17
+ "pypi.org",
18
+ "files.pythonhosted.org",
19
+ "crates.io",
20
+ "static.crates.io",
21
+ "index.crates.io",
22
+ "proxy.golang.org",
23
+ "sum.golang.org"
24
+ ];
25
+ var SANDBOX_BASELINE_EGRESS_HOSTS = [
26
+ ...SANDBOX_MODEL_PROVIDER_HOSTS,
27
+ ...SANDBOX_PACKAGE_REGISTRY_HOSTS
28
+ ];
29
+
30
+ // src/index.ts
31
+ function brokeredSecretPlaceholder(name) {
32
+ return `claxedo-broker:${name}`;
33
+ }
34
+ function brokeredPlaceholderEnv(secrets) {
35
+ return Object.fromEntries((secrets ?? []).map((secret) => [secret.name, brokeredSecretPlaceholder(secret.name)]));
36
+ }
3
37
 
4
38
  // src/runtime-env.ts
39
+ function workspaceRuntimeBootEnv(input) {
40
+ const env = {
41
+ ...workspaceRuntimeTargetEnv(input),
42
+ ...workspaceRuntimeSourceEnv(input),
43
+ ...input.env
44
+ };
45
+ if (input.runner) env.WORKSPACE_RUNTIME_RUNNER = input.runner;
46
+ if (input.controlEnv?.relayJwksUrl) env.WORKSPACE_RUNTIME_RELAY_JWKS_URL = input.controlEnv.relayJwksUrl;
47
+ if (input.controlEnv?.relayVerifyPem) env.WORKSPACE_RUNTIME_RELAY_HOST_VERIFY_PEM = input.controlEnv.relayVerifyPem;
48
+ if (input.controlEnv?.managementJwksUrl) env.WORKSPACE_RUNTIME_MANAGEMENT_JWKS_URL = input.controlEnv.managementJwksUrl;
49
+ if (input.controlEnv?.sessionAuthorityUrl) env.WORKSPACE_RUNTIME_SESSION_AUTHORITY_URL = input.controlEnv.sessionAuthorityUrl;
50
+ return env;
51
+ }
5
52
  function workspaceRuntimeTargetEnv(input) {
6
53
  return {
7
54
  WORKSPACE_RUNTIME_WORKSPACE_ID: input.workspaceId,
@@ -31,44 +78,45 @@ function shell(input) {
31
78
  return `'${input.replace(/'/g, `'"'"'`)}'`;
32
79
  }
33
80
 
34
- // src/constants.ts
35
- var DEFAULT_WORKSPACE_RUNTIME_PORT = 2593;
36
-
37
81
  // src/defaults.ts
38
82
  var RUNTIME_DIR = "/opt/workspace-runtime";
39
83
 
40
- // src/hosted-network-policy.ts
41
- var SANDBOX_MODEL_PROVIDER_HOSTS = [
42
- "api.anthropic.com",
43
- "api.openai.com",
44
- "generativelanguage.googleapis.com",
45
- "openrouter.ai",
46
- // Model catalog + engine config, fetched on runner startup.
47
- "models.dev",
48
- "opencode.ai"
49
- ];
50
- var SANDBOX_PACKAGE_REGISTRY_HOSTS = [
51
- "registry.npmjs.org",
52
- "nodejs.org",
53
- "pypi.org",
54
- "files.pythonhosted.org",
55
- "crates.io",
56
- "static.crates.io",
57
- "index.crates.io",
58
- "proxy.golang.org",
59
- "sum.golang.org"
60
- ];
61
- var SANDBOX_BASELINE_EGRESS_HOSTS = [
62
- ...SANDBOX_MODEL_PROVIDER_HOSTS,
63
- ...SANDBOX_PACKAGE_REGISTRY_HOSTS
64
- ];
84
+ // ../sandbox-contract/dist/index.mjs
85
+ var sandboxDriverCredentialFields = {
86
+ exe: [{ key: "api_token", label: "API Token", secret: true }],
87
+ daytona: [{ key: "api_key", label: "API Key", secret: true }],
88
+ modal: [
89
+ { key: "token_id", label: "Token ID", secret: true },
90
+ { key: "token_secret", label: "Token Secret", secret: true }
91
+ ],
92
+ vercel: [
93
+ { key: "access_token", label: "Access Token", secret: true },
94
+ { key: "team_id", label: "Team ID" },
95
+ { key: "project_id", label: "Project ID" }
96
+ ],
97
+ cloudflare: [
98
+ { key: "api_token", label: "API Token", secret: true },
99
+ { key: "worker_url", label: "Worker URL" }
100
+ ],
101
+ box: [{ key: "api_key", label: "API Key", secret: true }],
102
+ docker: [{ key: "image", label: "Image" }]
103
+ };
104
+ var sandboxDriverLabels = {
105
+ exe: "exe.dev",
106
+ daytona: "Daytona",
107
+ modal: "Modal",
108
+ vercel: "Vercel",
109
+ cloudflare: "Cloudflare",
110
+ box: "Box",
111
+ docker: "Docker"
112
+ };
65
113
 
66
114
  // src/driver-catalog.ts
67
115
  var sandboxDriverCatalog = {
68
116
  exe: {
69
117
  id: "exe",
70
- label: "exe.dev",
71
- credentialFields: [{ key: "api_token", label: "API Token", secret: true }],
118
+ label: sandboxDriverLabels.exe,
119
+ credentialFields: sandboxDriverCredentialFields.exe,
72
120
  metadata: {
73
121
  driverRunsIn: ["worker", "node"],
74
122
  hostStopBehavior: "not-supported",
@@ -88,17 +136,15 @@ var sandboxDriverCatalog = {
88
136
  },
89
137
  daytona: {
90
138
  id: "daytona",
91
- label: "Daytona",
92
- credentialFields: [{ key: "api_key", label: "API Key", secret: true }],
139
+ label: sandboxDriverLabels.daytona,
140
+ credentialFields: sandboxDriverCredentialFields.daytona,
93
141
  metadata: {
94
142
  driverRunsIn: ["worker", "node"],
95
143
  hostStopBehavior: "suspends-host",
96
144
  hostResumeBehavior: "same-host",
97
145
  targetAccess: "relay",
98
146
  secretBrokering: "native",
99
- // Daytona is the only driver that can filter egress by NAME as well as
100
- // by address: `domainAllowList` (names) alongside `networkAllowList`
101
- // (CIDRs), with `networkBlockAll` as the deny-all floor.
147
+ // The driver translates names to domainAllowList, or addresses to networkAllowList.
102
148
  egressControl: "hosts-and-cidrs",
103
149
  persistence: {
104
150
  resume: "same-sandbox",
@@ -112,20 +158,15 @@ var sandboxDriverCatalog = {
112
158
  },
113
159
  modal: {
114
160
  id: "modal",
115
- label: "Modal",
116
- credentialFields: [
117
- { key: "token_id", label: "Token ID", secret: true },
118
- { key: "token_secret", label: "Token Secret", secret: true }
119
- ],
161
+ label: sandboxDriverLabels.modal,
162
+ credentialFields: sandboxDriverCredentialFields.modal,
120
163
  metadata: {
121
164
  driverRunsIn: ["node"],
122
165
  hostStopBehavior: "terminates-host",
123
166
  hostResumeBehavior: "replacement-host",
124
167
  targetAccess: "relay",
125
168
  secretBrokering: "none",
126
- // Modal can cut the network entirely (`blockNetwork`) but cannot express
127
- // an allowlist, and its driver throws on a host policy. A blackout is
128
- // not containment for a workspace that has to clone and reach a model.
169
+ // The driver does not install Modal's domain allowlist or alpha sidecar.
129
170
  egressControl: "none",
130
171
  persistence: {
131
172
  resume: "replacement-restore",
@@ -139,20 +180,15 @@ var sandboxDriverCatalog = {
139
180
  },
140
181
  vercel: {
141
182
  id: "vercel",
142
- label: "Vercel",
143
- credentialFields: [
144
- { key: "access_token", label: "Access Token", secret: true },
145
- { key: "team_id", label: "Team ID" },
146
- { key: "project_id", label: "Project ID" }
147
- ],
183
+ label: sandboxDriverLabels.vercel,
184
+ credentialFields: sandboxDriverCredentialFields.vercel,
148
185
  metadata: {
149
186
  driverRunsIn: ["node"],
150
187
  hostStopBehavior: "terminates-host",
151
188
  hostResumeBehavior: "replacement-host",
152
189
  targetAccess: "relay",
153
190
  secretBrokering: "native",
154
- // Vercel's sandbox firewall takes a hostname allow list (or "deny-all"),
155
- // never CIDRs.
191
+ // The driver translates hosts only; the SDK's subnet rules are not wired to net.cidrs.
156
192
  egressControl: "hosts",
157
193
  persistence: {
158
194
  resume: "replacement-restore",
@@ -166,23 +202,15 @@ var sandboxDriverCatalog = {
166
202
  },
167
203
  cloudflare: {
168
204
  id: "cloudflare",
169
- label: "Cloudflare",
170
- credentialFields: [
171
- { key: "api_token", label: "API Token", secret: true },
172
- { key: "worker_url", label: "Worker URL" }
173
- ],
205
+ label: sandboxDriverLabels.cloudflare,
206
+ credentialFields: sandboxDriverCredentialFields.cloudflare,
174
207
  metadata: {
175
208
  driverRunsIn: ["worker"],
176
209
  hostStopBehavior: "not-supported",
177
210
  hostResumeBehavior: "same-host",
178
211
  targetAccess: "relay",
179
- secretBrokering: "proxy",
180
- // The Cloudflare Sandbox Worker exposes no egress filter: its broker
181
- // (drivers/cloudflare-egress.ts) is an OPT-IN proxy the sandbox chooses
182
- // to route brokered-credential requests through, not a boundary. The
183
- // driver therefore drops `net` entirely — which is exactly why this
184
- // must be declared, so the manager refuses instead of provisioning a
185
- // sandbox the caller believes is contained.
212
+ secretBrokering: "native",
213
+ // Credential handlers do not restrict unrelated destinations.
186
214
  egressControl: "none",
187
215
  persistence: {
188
216
  resume: "replacement-restore",
@@ -196,8 +224,8 @@ var sandboxDriverCatalog = {
196
224
  },
197
225
  box: {
198
226
  id: "box",
199
- label: "Box",
200
- credentialFields: [{ key: "api_key", label: "API Key", secret: true }],
227
+ label: sandboxDriverLabels.box,
228
+ credentialFields: sandboxDriverCredentialFields.box,
201
229
  metadata: {
202
230
  driverRunsIn: ["node"],
203
231
  hostStopBehavior: "suspends-host",
@@ -217,8 +245,8 @@ var sandboxDriverCatalog = {
217
245
  },
218
246
  docker: {
219
247
  id: "docker",
220
- label: "Docker",
221
- credentialFields: [{ key: "image", label: "Image" }],
248
+ label: sandboxDriverLabels.docker,
249
+ credentialFields: sandboxDriverCredentialFields.docker,
222
250
  metadata: {
223
251
  driverRunsIn: ["local"],
224
252
  hostStopBehavior: "terminates-host",
@@ -238,6 +266,37 @@ var sandboxDriverCatalog = {
238
266
  }
239
267
  };
240
268
 
269
+ // ../claxedo-helpers/dist/guards.mjs
270
+ function isRecord(value) {
271
+ return value !== null && typeof value === "object" && !Array.isArray(value);
272
+ }
273
+ function asRecord(value) {
274
+ return isRecord(value) ? value : void 0;
275
+ }
276
+
277
+ // src/json.ts
278
+ function num(input) {
279
+ return typeof input === "number" ? input : void 0;
280
+ }
281
+
282
+ // src/drivers/transient-error.ts
283
+ function textOf(input) {
284
+ return typeof input === "string" ? input : "";
285
+ }
286
+ function driverErrorSignals(err) {
287
+ const shaped = isRecord(err) ? err : {};
288
+ const nested = isRecord(shaped.response) ? shaped.response : {};
289
+ return {
290
+ status: num(nested.status) ?? num(shaped.status),
291
+ text: `${textOf(shaped.code)} ${textOf(shaped.name)} ${textOf(shaped.message)}`.toLowerCase()
292
+ };
293
+ }
294
+ function isTransientDriverError(err, markers) {
295
+ const { status, text } = driverErrorSignals(err);
296
+ if (status !== void 0 && status >= 500) return true;
297
+ return markers.some((marker) => text.includes(marker));
298
+ }
299
+
241
300
  // src/drivers/vercel.ts
242
301
  var DEFAULT_RUNTIME = "node22";
243
302
  var DEFAULT_RUNTIME_COMMAND = "/usr/local/bin/workspace-runtime";
@@ -252,15 +311,16 @@ function runtimePackage(version) {
252
311
  var SNAPSHOT_CACHE_TTL_MS = 60 * 60 * 1e3;
253
312
  var snapshotCache = /* @__PURE__ */ new Map();
254
313
  var snapshotCacheState = /* @__PURE__ */ new WeakMap();
314
+ function isVercelSandboxFactory(value) {
315
+ const row = asRecord(value);
316
+ return typeof row?.create === "function" && typeof row.get === "function";
317
+ }
255
318
  function nameFor(workspaceId) {
256
319
  return `claxedo-${workspaceId}`;
257
320
  }
321
+ var TRANSIENT_MARKERS = ["timeout", "unavailable", "deadline", "pending"];
258
322
  function transientDriverError(err) {
259
- const shaped = err;
260
- const status = shaped.response?.status ?? shaped.status;
261
- if (typeof status === "number" && status >= 500) return true;
262
- const text = `${shaped.code ?? ""} ${shaped.name ?? ""} ${shaped.message ?? ""}`.toLowerCase();
263
- return text.includes("timeout") || text.includes("unavailable") || text.includes("deadline") || text.includes("pending");
323
+ return isTransientDriverError(err, TRANSIENT_MARKERS);
264
324
  }
265
325
  function network(input) {
266
326
  if (!input.net || input.net.mode === "allow-all") return void 0;
@@ -277,9 +337,20 @@ function vercelBrokeredNetworkPolicy(secrets, base) {
277
337
  `vercel brokered secret "${secret.name}" requires a header \u2014 the firewall injects the value as an HTTP header on egress`
278
338
  );
279
339
  }
340
+ if (secret.hosts.length === 0) {
341
+ throw new Error(`vercel brokered secret "${secret.name}" requires at least one host in its egress allowlist`);
342
+ }
343
+ const value = secret.scheme ? `${secret.scheme} ${secret.value}` : secret.value;
344
+ const methods = [...secret.methods ?? []];
345
+ const pathPrefixes = [...secret.pathPrefixes ?? []];
280
346
  for (const host of secret.hosts) {
281
- ;
282
- (allow[host] ??= []).push({ transform: [{ headers: { [secret.header]: secret.value } }] });
347
+ const rules = allow[host] ??= [];
348
+ for (const prefix of methods.length ? pathPrefixes : []) {
349
+ rules.push({
350
+ match: { path: { startsWith: prefix }, method: methods },
351
+ transform: [{ headers: { [secret.header]: value } }]
352
+ });
353
+ }
283
354
  }
284
355
  }
285
356
  const subnets = typeof base === "object" && base?.subnets ? { subnets: base.subnets } : {};
@@ -292,7 +363,18 @@ function baseAllowEntries(base) {
292
363
  return Object.entries(base.allow ?? {});
293
364
  }
294
365
  function createVercelSandboxDriver(options) {
295
- const sandboxFactory = options.sandbox ?? Sandbox;
366
+ let defaultFactory;
367
+ function resolveFactory() {
368
+ if (options.sandbox) return options.sandbox;
369
+ defaultFactory ??= import("@vercel/sandbox").then((sdk) => {
370
+ const factory = sdk.Sandbox;
371
+ if (!isVercelSandboxFactory(factory)) {
372
+ throw new Error("@vercel/sandbox does not expose the factory surface this driver uses");
373
+ }
374
+ return factory;
375
+ });
376
+ return defaultFactory;
377
+ }
296
378
  const runtime = options.runtime ?? DEFAULT_RUNTIME;
297
379
  const runtimePort = options.runtimePort ?? DEFAULT_WORKSPACE_RUNTIME_PORT;
298
380
  const runtimeCommand = options.runtimeCommand ?? DEFAULT_RUNTIME_COMMAND;
@@ -319,22 +401,17 @@ function createVercelSandboxDriver(options) {
319
401
  };
320
402
  }
321
403
  function bootEnv(input, hostId) {
322
- const env = {
323
- ...workspaceRuntimeTargetEnv({
324
- workspaceId: input.workspaceId,
325
- hostId,
326
- directory: workspaceDirectory(input),
327
- port: runtimePort,
328
- host: "0.0.0.0"
329
- }),
330
- ...workspaceRuntimeSourceEnv({ source: input.source }),
331
- ...input.env
332
- };
333
- if (options.runner) env.WORKSPACE_RUNTIME_RUNNER = options.runner;
334
- if (options.controlEnv?.relayJwksUrl) env.WORKSPACE_RUNTIME_RELAY_JWKS_URL = options.controlEnv.relayJwksUrl;
335
- if (options.controlEnv?.relayVerifyPem) env.WORKSPACE_RUNTIME_RELAY_HOST_VERIFY_PEM = options.controlEnv.relayVerifyPem;
336
- if (options.controlEnv?.managementJwksUrl) env.WORKSPACE_RUNTIME_MANAGEMENT_JWKS_URL = options.controlEnv.managementJwksUrl;
337
- return env;
404
+ return workspaceRuntimeBootEnv({
405
+ workspaceId: input.workspaceId,
406
+ hostId,
407
+ directory: workspaceDirectory(input),
408
+ port: runtimePort,
409
+ host: "0.0.0.0",
410
+ source: input.source,
411
+ env: input.env,
412
+ runner: options.runner,
413
+ controlEnv: options.controlEnv
414
+ });
338
415
  }
339
416
  async function runSetup(sandbox, command) {
340
417
  await sandbox.runCommand("bash", ["-c", command], {
@@ -355,7 +432,7 @@ function createVercelSandboxDriver(options) {
355
432
  const running = cacheState.builds.get(cacheKey);
356
433
  if (running) return running;
357
434
  const build = (async () => {
358
- const builder = await sandboxFactory.create({
435
+ const builder = await (await resolveFactory()).create({
359
436
  ...credentials(),
360
437
  runtime,
361
438
  ports: [runtimePort],
@@ -389,14 +466,8 @@ function createVercelSandboxDriver(options) {
389
466
  `npm install ${runtimePackage(expectedRuntimeVersion)} --min-release-age=2`,
390
467
  `node -e "const fs=require('fs');const actual=JSON.parse(fs.readFileSync('${RUNTIME_DIR}/node_modules/@claxedo/workspace-runtime/package.json','utf8')).version;if(actual!=='${expectedRuntimeVersion}'){throw new Error('workspace-runtime snapshot package version mismatch: expected ${expectedRuntimeVersion}, got '+actual)}"`,
391
468
  "test -x node_modules/.bin/workspace-runtime",
392
- "test -x node_modules/.bin/claude-agent-acp",
393
- "test -x node_modules/.bin/codex-acp",
394
469
  `sudo ln -sf ${RUNTIME_DIR}/node_modules/.bin/workspace-runtime /usr/local/bin/workspace-runtime`,
395
- `sudo ln -sf ${RUNTIME_DIR}/node_modules/.bin/claude-agent-acp /usr/local/bin/claude-agent-acp`,
396
- `sudo ln -sf ${RUNTIME_DIR}/node_modules/.bin/codex-acp /usr/local/bin/codex-acp`,
397
- "command -v workspace-runtime",
398
- "command -v claude-agent-acp",
399
- "command -v codex-acp"
470
+ "command -v workspace-runtime"
400
471
  ].join(" && "));
401
472
  const snapshot = await builder.snapshot({
402
473
  expiration: 0,
@@ -450,10 +521,13 @@ function createVercelSandboxDriver(options) {
450
521
  const hostId = input.hostId ?? nameFor(input.workspaceId);
451
522
  const env = {
452
523
  ...bootEnv(input, hostId),
453
- ...await options.env?.(input, { id: hostId })
524
+ ...await options.env?.(input, { id: hostId }),
525
+ // Last, so no caller-supplied variable of the same name can stand in for
526
+ // a placeholder the firewall transform overwrites.
527
+ ...brokeredPlaceholderEnv(input.secrets)
454
528
  };
455
529
  const createPolicy = network(input);
456
- const sandbox = await sandboxFactory.create({
530
+ const sandbox = await (await resolveFactory()).create({
457
531
  ...credentials(),
458
532
  source: await source(input),
459
533
  ports: [runtimePort],
@@ -474,7 +548,7 @@ function createVercelSandboxDriver(options) {
474
548
  return readyTarget(input, sandbox, hostId);
475
549
  }
476
550
  async function sandboxById(target) {
477
- return sandboxFactory.get({
551
+ return (await resolveFactory()).get({
478
552
  ...credentials(),
479
553
  sandboxId: target.sandboxId
480
554
  });
@@ -39,8 +39,8 @@
39
39
  import type { SandboxNetworkPolicy, SandboxSource } from ".";
40
40
  /**
41
41
  * Model-provider endpoints. Derived from the harness→provider groups in
42
- * claxedo-server `network/types.ts` (`PROVIDER_TO_GROUP`): claude-acp and
43
- * claude-sdk → anthropic, codex-acp/codex-app-server/cursor-acp → openai,
42
+ * claxedo-server `network/types.ts` (`PROVIDER_TO_GROUP`): claude-sdk →
43
+ * anthropic, codex-app-server/cursor-sdk → openai,
44
44
  * google → google_ai; plus the aggregator and catalog endpoints the engine
45
45
  * itself fetches at startup.
46
46
  */
@@ -25,16 +25,16 @@ var SANDBOX_BASELINE_EGRESS_HOSTS = [
25
25
  ];
26
26
  function hostOf(value) {
27
27
  const raw = value?.trim();
28
- if (!raw) return;
28
+ if (!raw) return void 0;
29
29
  try {
30
30
  const host = new URL(raw).hostname.trim().toLowerCase();
31
31
  return host ? host : void 0;
32
32
  } catch {
33
- return;
33
+ return void 0;
34
34
  }
35
35
  }
36
36
  function sandboxSourceHost(source) {
37
- if (!source || source.kind !== "git") return;
37
+ if (!source || source.kind !== "git") return void 0;
38
38
  return hostOf(source.repoUrl);
39
39
  }
40
40
  function hostedSandboxNetworkPolicy(input) {
@@ -0,0 +1,34 @@
1
+ // src/runtime-version.ts
2
+ var DEFAULT_WORKSPACE_RUNTIME_VERSION = "0.5.2";
3
+ function workspaceRuntimeVersion() {
4
+ return process.env.WORKSPACE_RUNTIME_VERSION || DEFAULT_WORKSPACE_RUNTIME_VERSION;
5
+ }
6
+
7
+ // src/image-name.ts
8
+ var SNAPSHOT_SCHEMA_VERSION = 8;
9
+ var DEFAULT_SANDBOX_IMAGE_REPOSITORY = "ghcr.io/kyashrathore/claxedo-sandbox";
10
+ function snapshotVersion(value) {
11
+ return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
12
+ }
13
+ function sandboxImageRepository(env = process.env) {
14
+ const repository = env.CLAXEDO_SANDBOX_IMAGE_REPOSITORY?.trim();
15
+ return repository ? repository : DEFAULT_SANDBOX_IMAGE_REPOSITORY;
16
+ }
17
+ function buildIdSuffix(buildId, env) {
18
+ const id = (buildId ?? env.CLAXEDO_SANDBOX_BUILD_ID)?.trim();
19
+ return id ? `-${snapshotVersion(id)}` : "";
20
+ }
21
+ function defaultSandboxImage(version = workspaceRuntimeVersion(), buildId, env = process.env) {
22
+ return `${sandboxImageRepository(env)}:workspace-runtime-${snapshotVersion(version)}${buildIdSuffix(buildId, env)}-v${SNAPSHOT_SCHEMA_VERSION}`;
23
+ }
24
+ function defaultSnapshotName(version = workspaceRuntimeVersion(), buildId, env = process.env) {
25
+ return `claxedo-workspace-runtime-${snapshotVersion(version)}${buildIdSuffix(buildId, env)}-v${SNAPSHOT_SCHEMA_VERSION}`;
26
+ }
27
+ export {
28
+ DEFAULT_SANDBOX_IMAGE_REPOSITORY,
29
+ SNAPSHOT_SCHEMA_VERSION,
30
+ defaultSandboxImage,
31
+ defaultSnapshotName,
32
+ sandboxImageRepository,
33
+ snapshotVersion
34
+ };
package/dist/image.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Daytona, Image } from "@daytona/sdk";
1
+ import type { Daytona, Image } from "@daytona/sdk";
2
2
  export { RUNTIME_DIR, WORKSPACE_RUNTIME_PORT, WORKSPACE_DIR } from "./defaults";
3
3
  export { SNAPSHOT_SCHEMA_VERSION, snapshotVersion, defaultSandboxImage, defaultSnapshotName, } from "./image-name";
4
4
  /**
@@ -19,5 +19,5 @@ export type SandboxImageOptions = {
19
19
  export declare const SANDBOX_IMAGE_REPOSITORY: string;
20
20
  export declare const SANDBOX_IMAGE: string;
21
21
  export declare const SNAPSHOT_NAME: string;
22
- export declare function buildSandboxImage(): Image;
22
+ export declare function buildSandboxImage(): Promise<Image>;
23
23
  export declare function ensureSnapshot(daytona: Daytona, options?: SandboxImageOptions): Promise<string>;
package/dist/image.mjs CHANGED
@@ -1,6 +1,3 @@
1
- // src/image.ts
2
- import { Image, DaytonaNotFoundError } from "@daytona/sdk";
3
-
4
1
  // src/constants.ts
5
2
  var DEFAULT_WORKSPACE_RUNTIME_PORT = 2593;
6
3
 
@@ -36,6 +33,19 @@ function defaultSnapshotName(version = workspaceRuntimeVersion(), buildId, env =
36
33
  return `claxedo-workspace-runtime-${snapshotVersion(version)}${buildIdSuffix(buildId, env)}-v${SNAPSHOT_SCHEMA_VERSION}`;
37
34
  }
38
35
 
36
+ // ../claxedo-helpers/dist/guards.mjs
37
+ function isRecord(value) {
38
+ return value !== null && typeof value === "object" && !Array.isArray(value);
39
+ }
40
+ function asRecord(value) {
41
+ return isRecord(value) ? value : void 0;
42
+ }
43
+
44
+ // src/json.ts
45
+ function text(input) {
46
+ return typeof input === "string" && input.length > 0 ? input : void 0;
47
+ }
48
+
39
49
  // src/image.ts
40
50
  function defaultImageLogSink(message, context) {
41
51
  if (context) console.info(message, context);
@@ -44,16 +54,26 @@ function defaultImageLogSink(message, context) {
44
54
  var SANDBOX_IMAGE_REPOSITORY = sandboxImageRepository();
45
55
  var SANDBOX_IMAGE = process.env.CLAXEDO_SANDBOX_IMAGE || defaultSandboxImage();
46
56
  var SNAPSHOT_NAME = process.env.CLAXEDO_SNAPSHOT_NAME || defaultSnapshotName();
47
- function buildSandboxImage() {
57
+ async function buildSandboxImage() {
58
+ const { Image } = await import("@daytona/sdk");
48
59
  return Image.base(SANDBOX_IMAGE);
49
60
  }
50
61
  function snapshotUsable(state) {
51
62
  return state === "ready" || state === "active";
52
63
  }
64
+ function snapshotView(input) {
65
+ const row = asRecord(input) ?? {};
66
+ return { id: text(row.id), state: text(row.state), errorReason: text(row.errorReason) };
67
+ }
68
+ async function activateSnapshot(snapshots, id) {
69
+ const activate = asRecord(snapshots)?.activate;
70
+ if (typeof activate !== "function") throw new Error("Daytona SDK does not support snapshot activation");
71
+ await activate.call(snapshots, { id });
72
+ }
53
73
  async function createSnapshot(daytona, log) {
54
74
  log("Creating sandbox snapshot (this may take a few minutes)...", { name: SNAPSHOT_NAME });
55
75
  await daytona.snapshot.create(
56
- { name: SNAPSHOT_NAME, image: buildSandboxImage() },
76
+ { name: SNAPSHOT_NAME, image: await buildSandboxImage() },
57
77
  {
58
78
  timeout: 120,
59
79
  onLogs: (chunk) => log("[snapshot]", { text: chunk })
@@ -64,14 +84,15 @@ async function createSnapshot(daytona, log) {
64
84
  async function ensureSnapshot(daytona, options = {}) {
65
85
  const log = options.log ?? defaultImageLogSink;
66
86
  try {
67
- const existing = await daytona.snapshot.get(SNAPSHOT_NAME);
87
+ const existing = snapshotView(await daytona.snapshot.get(SNAPSHOT_NAME));
68
88
  if (existing.state === "inactive") {
69
89
  log("Snapshot is inactive, reactivating...", { name: SNAPSHOT_NAME, id: existing.id });
70
- await daytona.snapshot.activate({ id: existing.id });
90
+ if (!existing.id) throw new Error("Daytona snapshot is inactive but reports no id to activate");
91
+ await activateSnapshot(daytona.snapshot, existing.id);
71
92
  for (let i = 0; i < 120; i++) {
72
- const snap = await daytona.snapshot.get(SNAPSHOT_NAME);
93
+ const snap = snapshotView(await daytona.snapshot.get(SNAPSHOT_NAME));
73
94
  if (snapshotUsable(snap.state)) return SNAPSHOT_NAME;
74
- if (snap.state === "error") throw new Error(`Snapshot activation failed: ${snap.errorReason}`);
95
+ if (snap.state === "error") throw new Error(`Snapshot activation failed: ${snap.errorReason ?? "unknown"}`);
75
96
  await new Promise((resolve) => setTimeout(resolve, 2e3));
76
97
  }
77
98
  throw new Error("Snapshot activation timed out");
@@ -79,9 +100,9 @@ async function ensureSnapshot(daytona, options = {}) {
79
100
  if (existing.state === "building") {
80
101
  log("Snapshot is building, waiting (up to 10 min)...", { name: SNAPSHOT_NAME });
81
102
  for (let i = 0; i < 300; i++) {
82
- const snap = await daytona.snapshot.get(SNAPSHOT_NAME);
103
+ const snap = snapshotView(await daytona.snapshot.get(SNAPSHOT_NAME));
83
104
  if (snapshotUsable(snap.state)) return SNAPSHOT_NAME;
84
- if (snap.state === "error") throw new Error(`Snapshot build failed: ${snap.errorReason}`);
105
+ if (snap.state === "error") throw new Error(`Snapshot build failed: ${snap.errorReason ?? "unknown"}`);
85
106
  if (i % 15 === 0) log("Still waiting for snapshot...", { state: snap.state, elapsed: `${i * 2}s` });
86
107
  await new Promise((resolve) => setTimeout(resolve, 2e3));
87
108
  }
@@ -89,6 +110,7 @@ async function ensureSnapshot(daytona, options = {}) {
89
110
  }
90
111
  return SNAPSHOT_NAME;
91
112
  } catch (err) {
113
+ const { DaytonaNotFoundError } = await import("@daytona/sdk");
92
114
  if (!(err instanceof DaytonaNotFoundError)) throw err;
93
115
  }
94
116
  return createSnapshot(daytona, log);