@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,17 @@
1
- // src/drivers/modal.ts
2
- import { ModalClient, Probe } from "modal";
3
-
4
1
  // src/runtime-env.ts
2
+ function workspaceRuntimeBootEnv(input) {
3
+ const env = {
4
+ ...workspaceRuntimeTargetEnv(input),
5
+ ...workspaceRuntimeSourceEnv(input),
6
+ ...input.env
7
+ };
8
+ if (input.runner) env.WORKSPACE_RUNTIME_RUNNER = input.runner;
9
+ if (input.controlEnv?.relayJwksUrl) env.WORKSPACE_RUNTIME_RELAY_JWKS_URL = input.controlEnv.relayJwksUrl;
10
+ if (input.controlEnv?.relayVerifyPem) env.WORKSPACE_RUNTIME_RELAY_HOST_VERIFY_PEM = input.controlEnv.relayVerifyPem;
11
+ if (input.controlEnv?.managementJwksUrl) env.WORKSPACE_RUNTIME_MANAGEMENT_JWKS_URL = input.controlEnv.managementJwksUrl;
12
+ if (input.controlEnv?.sessionAuthorityUrl) env.WORKSPACE_RUNTIME_SESSION_AUTHORITY_URL = input.controlEnv.sessionAuthorityUrl;
13
+ return env;
14
+ }
5
15
  function workspaceRuntimeTargetEnv(input) {
6
16
  return {
7
17
  WORKSPACE_RUNTIME_WORKSPACE_ID: input.workspaceId,
@@ -28,9 +38,6 @@ function shell(input) {
28
38
  // src/constants.ts
29
39
  var DEFAULT_WORKSPACE_RUNTIME_PORT = 2593;
30
40
 
31
- // src/image.ts
32
- import { Image, DaytonaNotFoundError } from "@daytona/sdk";
33
-
34
41
  // src/runtime-version.ts
35
42
  var DEFAULT_WORKSPACE_RUNTIME_VERSION = "0.5.2";
36
43
  function workspaceRuntimeVersion() {
@@ -58,6 +65,19 @@ function defaultSnapshotName(version = workspaceRuntimeVersion(), buildId, env =
58
65
  return `claxedo-workspace-runtime-${snapshotVersion(version)}${buildIdSuffix(buildId, env)}-v${SNAPSHOT_SCHEMA_VERSION}`;
59
66
  }
60
67
 
68
+ // ../claxedo-helpers/dist/guards.mjs
69
+ function isRecord(value) {
70
+ return value !== null && typeof value === "object" && !Array.isArray(value);
71
+ }
72
+ function asRecord(value) {
73
+ return isRecord(value) ? value : void 0;
74
+ }
75
+
76
+ // src/json.ts
77
+ function num(input) {
78
+ return typeof input === "number" ? input : void 0;
79
+ }
80
+
61
81
  // src/image.ts
62
82
  var SANDBOX_IMAGE_REPOSITORY = sandboxImageRepository();
63
83
  var SANDBOX_IMAGE = process.env.CLAXEDO_SANDBOX_IMAGE || defaultSandboxImage();
@@ -89,12 +109,42 @@ var SANDBOX_BASELINE_EGRESS_HOSTS = [
89
109
  ...SANDBOX_PACKAGE_REGISTRY_HOSTS
90
110
  ];
91
111
 
112
+ // ../sandbox-contract/dist/index.mjs
113
+ var sandboxDriverCredentialFields = {
114
+ exe: [{ key: "api_token", label: "API Token", secret: true }],
115
+ daytona: [{ key: "api_key", label: "API Key", secret: true }],
116
+ modal: [
117
+ { key: "token_id", label: "Token ID", secret: true },
118
+ { key: "token_secret", label: "Token Secret", secret: true }
119
+ ],
120
+ vercel: [
121
+ { key: "access_token", label: "Access Token", secret: true },
122
+ { key: "team_id", label: "Team ID" },
123
+ { key: "project_id", label: "Project ID" }
124
+ ],
125
+ cloudflare: [
126
+ { key: "api_token", label: "API Token", secret: true },
127
+ { key: "worker_url", label: "Worker URL" }
128
+ ],
129
+ box: [{ key: "api_key", label: "API Key", secret: true }],
130
+ docker: [{ key: "image", label: "Image" }]
131
+ };
132
+ var sandboxDriverLabels = {
133
+ exe: "exe.dev",
134
+ daytona: "Daytona",
135
+ modal: "Modal",
136
+ vercel: "Vercel",
137
+ cloudflare: "Cloudflare",
138
+ box: "Box",
139
+ docker: "Docker"
140
+ };
141
+
92
142
  // src/driver-catalog.ts
93
143
  var sandboxDriverCatalog = {
94
144
  exe: {
95
145
  id: "exe",
96
- label: "exe.dev",
97
- credentialFields: [{ key: "api_token", label: "API Token", secret: true }],
146
+ label: sandboxDriverLabels.exe,
147
+ credentialFields: sandboxDriverCredentialFields.exe,
98
148
  metadata: {
99
149
  driverRunsIn: ["worker", "node"],
100
150
  hostStopBehavior: "not-supported",
@@ -114,17 +164,15 @@ var sandboxDriverCatalog = {
114
164
  },
115
165
  daytona: {
116
166
  id: "daytona",
117
- label: "Daytona",
118
- credentialFields: [{ key: "api_key", label: "API Key", secret: true }],
167
+ label: sandboxDriverLabels.daytona,
168
+ credentialFields: sandboxDriverCredentialFields.daytona,
119
169
  metadata: {
120
170
  driverRunsIn: ["worker", "node"],
121
171
  hostStopBehavior: "suspends-host",
122
172
  hostResumeBehavior: "same-host",
123
173
  targetAccess: "relay",
124
174
  secretBrokering: "native",
125
- // Daytona is the only driver that can filter egress by NAME as well as
126
- // by address: `domainAllowList` (names) alongside `networkAllowList`
127
- // (CIDRs), with `networkBlockAll` as the deny-all floor.
175
+ // The driver translates names to domainAllowList, or addresses to networkAllowList.
128
176
  egressControl: "hosts-and-cidrs",
129
177
  persistence: {
130
178
  resume: "same-sandbox",
@@ -138,20 +186,15 @@ var sandboxDriverCatalog = {
138
186
  },
139
187
  modal: {
140
188
  id: "modal",
141
- label: "Modal",
142
- credentialFields: [
143
- { key: "token_id", label: "Token ID", secret: true },
144
- { key: "token_secret", label: "Token Secret", secret: true }
145
- ],
189
+ label: sandboxDriverLabels.modal,
190
+ credentialFields: sandboxDriverCredentialFields.modal,
146
191
  metadata: {
147
192
  driverRunsIn: ["node"],
148
193
  hostStopBehavior: "terminates-host",
149
194
  hostResumeBehavior: "replacement-host",
150
195
  targetAccess: "relay",
151
196
  secretBrokering: "none",
152
- // Modal can cut the network entirely (`blockNetwork`) but cannot express
153
- // an allowlist, and its driver throws on a host policy. A blackout is
154
- // not containment for a workspace that has to clone and reach a model.
197
+ // The driver does not install Modal's domain allowlist or alpha sidecar.
155
198
  egressControl: "none",
156
199
  persistence: {
157
200
  resume: "replacement-restore",
@@ -165,20 +208,15 @@ var sandboxDriverCatalog = {
165
208
  },
166
209
  vercel: {
167
210
  id: "vercel",
168
- label: "Vercel",
169
- credentialFields: [
170
- { key: "access_token", label: "Access Token", secret: true },
171
- { key: "team_id", label: "Team ID" },
172
- { key: "project_id", label: "Project ID" }
173
- ],
211
+ label: sandboxDriverLabels.vercel,
212
+ credentialFields: sandboxDriverCredentialFields.vercel,
174
213
  metadata: {
175
214
  driverRunsIn: ["node"],
176
215
  hostStopBehavior: "terminates-host",
177
216
  hostResumeBehavior: "replacement-host",
178
217
  targetAccess: "relay",
179
218
  secretBrokering: "native",
180
- // Vercel's sandbox firewall takes a hostname allow list (or "deny-all"),
181
- // never CIDRs.
219
+ // The driver translates hosts only; the SDK's subnet rules are not wired to net.cidrs.
182
220
  egressControl: "hosts",
183
221
  persistence: {
184
222
  resume: "replacement-restore",
@@ -192,23 +230,15 @@ var sandboxDriverCatalog = {
192
230
  },
193
231
  cloudflare: {
194
232
  id: "cloudflare",
195
- label: "Cloudflare",
196
- credentialFields: [
197
- { key: "api_token", label: "API Token", secret: true },
198
- { key: "worker_url", label: "Worker URL" }
199
- ],
233
+ label: sandboxDriverLabels.cloudflare,
234
+ credentialFields: sandboxDriverCredentialFields.cloudflare,
200
235
  metadata: {
201
236
  driverRunsIn: ["worker"],
202
237
  hostStopBehavior: "not-supported",
203
238
  hostResumeBehavior: "same-host",
204
239
  targetAccess: "relay",
205
- secretBrokering: "proxy",
206
- // The Cloudflare Sandbox Worker exposes no egress filter: its broker
207
- // (drivers/cloudflare-egress.ts) is an OPT-IN proxy the sandbox chooses
208
- // to route brokered-credential requests through, not a boundary. The
209
- // driver therefore drops `net` entirely — which is exactly why this
210
- // must be declared, so the manager refuses instead of provisioning a
211
- // sandbox the caller believes is contained.
240
+ secretBrokering: "native",
241
+ // Credential handlers do not restrict unrelated destinations.
212
242
  egressControl: "none",
213
243
  persistence: {
214
244
  resume: "replacement-restore",
@@ -222,8 +252,8 @@ var sandboxDriverCatalog = {
222
252
  },
223
253
  box: {
224
254
  id: "box",
225
- label: "Box",
226
- credentialFields: [{ key: "api_key", label: "API Key", secret: true }],
255
+ label: sandboxDriverLabels.box,
256
+ credentialFields: sandboxDriverCredentialFields.box,
227
257
  metadata: {
228
258
  driverRunsIn: ["node"],
229
259
  hostStopBehavior: "suspends-host",
@@ -243,8 +273,8 @@ var sandboxDriverCatalog = {
243
273
  },
244
274
  docker: {
245
275
  id: "docker",
246
- label: "Docker",
247
- credentialFields: [{ key: "image", label: "Image" }],
276
+ label: sandboxDriverLabels.docker,
277
+ credentialFields: sandboxDriverCredentialFields.docker,
248
278
  metadata: {
249
279
  driverRunsIn: ["local"],
250
280
  hostStopBehavior: "terminates-host",
@@ -264,6 +294,24 @@ var sandboxDriverCatalog = {
264
294
  }
265
295
  };
266
296
 
297
+ // src/drivers/transient-error.ts
298
+ function textOf(input) {
299
+ return typeof input === "string" ? input : "";
300
+ }
301
+ function driverErrorSignals(err) {
302
+ const shaped = isRecord(err) ? err : {};
303
+ const nested = isRecord(shaped.response) ? shaped.response : {};
304
+ return {
305
+ status: num(nested.status) ?? num(shaped.status),
306
+ text: `${textOf(shaped.code)} ${textOf(shaped.name)} ${textOf(shaped.message)}`.toLowerCase()
307
+ };
308
+ }
309
+ function isTransientDriverError(err, markers) {
310
+ const { status, text: text2 } = driverErrorSignals(err);
311
+ if (status !== void 0 && status >= 500) return true;
312
+ return markers.some((marker) => text2.includes(marker));
313
+ }
314
+
267
315
  // src/drivers/modal.ts
268
316
  var DEFAULT_APP_NAME = "claxedo";
269
317
  var DEFAULT_RUNTIME_COMMAND = "/usr/local/bin/workspace-runtime";
@@ -271,21 +319,28 @@ var DEFAULT_WORKSPACE_DIR = "/workspace";
271
319
  var DEFAULT_TIMEOUT_MS = 30 * 60 * 1e3;
272
320
  var DEFAULT_TUNNEL_TIMEOUT_MS = 3e4;
273
321
  var DEFAULT_OPERATION_TIMEOUT_MS = 6e4;
322
+ function isModalClient(value) {
323
+ const row = asRecord(value);
324
+ return typeof asRecord(row?.apps)?.fromName === "function" && typeof asRecord(row?.images)?.fromRegistry === "function" && typeof asRecord(row?.sandboxes)?.create === "function";
325
+ }
274
326
  function nameFor(workspaceId) {
275
327
  return `claxedo-${workspaceId}`;
276
328
  }
329
+ var TRANSIENT_MARKERS = ["timeout", "unavailable", "deadline"];
277
330
  function transientDriverError(err) {
278
- const shaped = err;
279
- const status = shaped.response?.status ?? shaped.status;
280
- if (typeof status === "number" && status >= 500) return true;
281
- const text = `${shaped.code ?? ""} ${shaped.name ?? ""} ${shaped.message ?? ""}`.toLowerCase();
282
- return text.includes("timeout") || text.includes("unavailable") || text.includes("deadline");
331
+ return isTransientDriverError(err, TRANSIENT_MARKERS);
283
332
  }
284
333
  function createModalSandboxDriver(options) {
285
- const client = options.client ?? new ModalClient({
286
- tokenId: options.tokenId,
287
- tokenSecret: options.tokenSecret
288
- });
334
+ let defaultClient;
335
+ function resolveClient() {
336
+ if (options.client) return options.client;
337
+ defaultClient ??= import("modal").then((sdk) => {
338
+ const client = new sdk.ModalClient({ tokenId: options.tokenId, tokenSecret: options.tokenSecret });
339
+ if (!isModalClient(client)) throw new Error("modal SDK does not expose the client surface this driver uses");
340
+ return client;
341
+ });
342
+ return defaultClient;
343
+ }
289
344
  const appName = options.appName ?? DEFAULT_APP_NAME;
290
345
  const runtimePort = options.runtimePort ?? DEFAULT_WORKSPACE_RUNTIME_PORT;
291
346
  const runtimeCommand = options.runtimeCommand ?? DEFAULT_RUNTIME_COMMAND;
@@ -293,29 +348,28 @@ function createModalSandboxDriver(options) {
293
348
  const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
294
349
  const tunnelTimeoutMs = options.tunnelTimeoutMs ?? DEFAULT_TUNNEL_TIMEOUT_MS;
295
350
  const operationTimeoutMs = options.operationTimeoutMs ?? DEFAULT_OPERATION_TIMEOUT_MS;
296
- const readinessProbe = options.readinessProbe ?? ((port) => Probe.withTcp(port, { intervalMs: 1e3 }));
351
+ async function readinessProbeFor(port) {
352
+ if (options.readinessProbe) return options.readinessProbe(port);
353
+ const { Probe } = await import("modal");
354
+ return Probe.withTcp(port, { intervalMs: 1e3 });
355
+ }
297
356
  function workspaceDirectory(input) {
298
357
  return input.workspaceRoot ?? workspaceDir;
299
358
  }
300
359
  function bootEnv(input, hostId) {
301
- const env = {
302
- ...workspaceRuntimeTargetEnv({
303
- workspaceId: input.workspaceId,
304
- hostId,
305
- directory: workspaceDirectory(input),
306
- port: runtimePort,
307
- host: "0.0.0.0"
308
- }),
309
- ...workspaceRuntimeSourceEnv({ source: input.source }),
310
- ...input.env
311
- };
312
- if (options.runner) env.WORKSPACE_RUNTIME_RUNNER = options.runner;
313
- if (options.controlEnv?.relayJwksUrl) env.WORKSPACE_RUNTIME_RELAY_JWKS_URL = options.controlEnv.relayJwksUrl;
314
- if (options.controlEnv?.relayVerifyPem) env.WORKSPACE_RUNTIME_RELAY_HOST_VERIFY_PEM = options.controlEnv.relayVerifyPem;
315
- if (options.controlEnv?.managementJwksUrl) env.WORKSPACE_RUNTIME_MANAGEMENT_JWKS_URL = options.controlEnv.managementJwksUrl;
316
- return env;
360
+ return workspaceRuntimeBootEnv({
361
+ workspaceId: input.workspaceId,
362
+ hostId,
363
+ directory: workspaceDirectory(input),
364
+ port: runtimePort,
365
+ host: "0.0.0.0",
366
+ source: input.source,
367
+ env: input.env,
368
+ runner: options.runner,
369
+ controlEnv: options.controlEnv
370
+ });
317
371
  }
318
- async function image(input) {
372
+ async function image(client, input) {
319
373
  if (input.bootSource?.kind === "driver-snapshot") return client.images.fromId(input.bootSource.snapshotId);
320
374
  if (input.bootSource?.kind === "image") return client.images.fromId(input.bootSource.image);
321
375
  if (input.snapshot) return client.images.fromId(input.snapshot);
@@ -353,9 +407,10 @@ function createModalSandboxDriver(options) {
353
407
  ...bootEnv(input, hostId),
354
408
  ...await options.env?.(input, { id: hostId })
355
409
  };
410
+ const client = await resolveClient();
356
411
  const sandbox = await client.sandboxes.create(
357
412
  await client.apps.fromName(appName, { createIfMissing: true }),
358
- await image(input),
413
+ await image(client, input),
359
414
  {
360
415
  name: nameFor(input.workspaceId),
361
416
  command: ["bash", "-lc", `mkdir -p ${shell(directory)}; cd ${shell(directory)}; exec ${runtimeCommand}`],
@@ -365,7 +420,7 @@ function createModalSandboxDriver(options) {
365
420
  timeoutMs,
366
421
  ...options.idleTimeoutMs !== void 0 ? { idleTimeoutMs: options.idleTimeoutMs } : {},
367
422
  tags: { ...input.labels, "claxedo.workspaceId": input.workspaceId },
368
- readinessProbe: readinessProbe(runtimePort),
423
+ readinessProbe: await readinessProbeFor(runtimePort),
369
424
  ...network(input)
370
425
  }
371
426
  ).catch((err) => {
@@ -376,7 +431,7 @@ function createModalSandboxDriver(options) {
376
431
  return readyTarget(input, sandbox, hostId);
377
432
  }
378
433
  async function sandboxById(target) {
379
- return client.sandboxes.fromId(target.sandboxId);
434
+ return (await resolveClient()).sandboxes.fromId(target.sandboxId);
380
435
  }
381
436
  return {
382
437
  id: "modal",
@@ -0,0 +1,11 @@
1
+ /** The status and lower-cased text a driver error carries, if it carries any. */
2
+ export declare function driverErrorSignals(err: unknown): {
3
+ status: number | undefined;
4
+ text: string;
5
+ };
6
+ /**
7
+ * A 5xx is transient for every driver; anything else is a per-driver judgment
8
+ * made from the error text, so each driver passes the markers it has actually
9
+ * seen its own SDK produce.
10
+ */
11
+ export declare function isTransientDriverError(err: unknown, markers: readonly string[]): boolean;
@@ -1,5 +1,6 @@
1
- import { type NetworkPolicy } from "@vercel/sandbox";
2
- import type { SandboxBrokeredSecret, SandboxDriver, SandboxDriverEnsureInput } from "..";
1
+ import type { NetworkPolicy } from "@vercel/sandbox";
2
+ import { type SandboxBrokeredSecret, type SandboxDriver, type SandboxDriverEnsureInput } from "..";
3
+ import { type WorkspaceRuntimeControlEnv } from "../runtime-env";
3
4
  type VercelSnapshotLike = {
4
5
  snapshotId: string;
5
6
  };
@@ -80,11 +81,7 @@ export type VercelSandboxDriverOptions = {
80
81
  runtimeCommand?: string;
81
82
  workspaceDir?: string;
82
83
  runner?: string;
83
- controlEnv?: {
84
- relayJwksUrl?: string;
85
- relayVerifyPem?: string;
86
- managementJwksUrl?: string;
87
- };
84
+ controlEnv?: WorkspaceRuntimeControlEnv;
88
85
  env?: (input: SandboxDriverEnsureInput, host: {
89
86
  id: string;
90
87
  }) => Record<string, string> | Promise<Record<string, string>>;
@@ -103,26 +100,26 @@ export type VercelSandboxDriverOptions = {
103
100
  * and https://vercel.com/docs/sandbox/concepts/firewall — credentials
104
101
  * brokering).
105
102
  *
106
- * The result is the UNION of `base` — the create-time allow-list — and the
103
+ * The result is the union of `base` — the create-time allow-list — and the
107
104
  * brokered hosts, never a replacement. Replacing it is wrong in both
108
- * directions. It BREAKS egress the caller was granted: `updateNetworkPolicy`
109
- * overwrites the whole policy, so a sandbox created with git and the npm
110
- * registry allowed loses both the instant a brokered secret is attached, and
111
- * the agent's next `npm install` fails mid-run. And it can silently WIDEN
112
- * egress: a brokered host that was not in the create-time list becomes
113
- * reachable, so a policy nobody approved is installed as a side effect of
114
- * attaching a credential. A union changes exactly one thing — it adds the
115
- * brokered hosts, which is the smallest edit that makes the credential usable.
105
+ * directions: it breaks egress the caller was granted, since
106
+ * `updateNetworkPolicy` overwrites the whole policy, so a sandbox created with
107
+ * git and the npm registry allowed loses both the instant a brokered secret is
108
+ * attached, and the agent's next `npm install` fails mid-run. It can also
109
+ * silently widen egress, since a brokered host that was not in the create-time
110
+ * list becomes reachable, installing a policy nobody approved as a side effect
111
+ * of attaching a credential. A union changes exactly one thing — it adds the
112
+ * brokered hosts, the smallest edit that makes the credential usable.
116
113
  *
117
114
  * `base` is the create-time `NetworkPolicy` exactly as passed to `create`, and
118
- * it is REQUIRED even though `undefined` is a legal value: `undefined` means
115
+ * it is required even though `undefined` is a legal value: `undefined` means
119
116
  * "no policy was requested", so the merge keeps egress unrestricted, expressed
120
117
  * as the `"*"` wildcard the record form needs for the header transforms to ride
121
- * alongside it. That is the opposite of what this function used to return with
122
- * no base at all (deny-all-except-brokered-hosts), so an optional parameter
123
- * would let an existing caller keep compiling while its egress silently WIDENS.
124
- * Spelling the base out forces that decision to be made once, visibly. A
125
- * `"deny-all"` base contributes nothing, leaving deny-all-except-brokered-hosts.
118
+ * alongside it. An optional parameter defaulting to
119
+ * deny-all-except-brokered-hosts would let an existing caller keep compiling
120
+ * while its egress silently widens. Spelling the base out forces that decision
121
+ * to be made once, visibly. A `"deny-all"` base contributes nothing, leaving
122
+ * deny-all-except-brokered-hosts.
126
123
  */
127
124
  export declare function vercelBrokeredNetworkPolicy(secrets: SandboxBrokeredSecret[], base: NetworkPolicy | undefined): NetworkPolicy;
128
125
  export declare function createVercelSandboxDriver(options: VercelSandboxDriverOptions): SandboxDriver;