@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.
- package/README.md +10 -14
- package/dist/driver-catalog.d.ts +3 -53
- package/dist/driver-catalog.mjs +137 -128
- package/dist/drivers/box.mjs +143 -68
- package/dist/drivers/cloudflare.d.ts +4 -7
- package/dist/drivers/cloudflare.mjs +198 -108
- package/dist/drivers/daytona.d.ts +49 -13
- package/dist/drivers/daytona.mjs +225 -83
- package/dist/drivers/docker.mjs +118 -67
- package/dist/drivers/exe.mjs +74 -51
- package/dist/drivers/fetch-bridge.mjs +19 -4
- package/dist/drivers/modal.d.ts +2 -5
- package/dist/drivers/modal.mjs +130 -75
- package/dist/drivers/transient-error.d.ts +11 -0
- package/dist/drivers/vercel.d.ts +19 -22
- package/dist/drivers/vercel.mjs +178 -104
- package/dist/hosted-network-policy.d.ts +2 -2
- package/dist/hosted-network-policy.mjs +3 -3
- package/dist/image-name.mjs +34 -0
- package/dist/image.d.ts +2 -2
- package/dist/image.mjs +33 -11
- package/dist/index.d.ts +62 -17
- package/dist/index.mjs +68 -22
- package/dist/json.d.ts +13 -0
- package/dist/lease-policy.d.ts +6 -2
- package/dist/lease-policy.mjs +50 -40
- package/dist/lease-types.d.ts +11 -2
- package/dist/runtime-env.d.ts +12 -1
- package/dist/runtime-env.mjs +14 -0
- package/dist/service-url-exposure.d.ts +1 -2
- package/dist/service-url-exposure.mjs +1 -17
- package/dist/stores/memory.mjs +2 -2
- package/package.json +57 -55
- package/dist/drivers/cloudflare-egress.d.ts +0 -40
- package/dist/drivers/cloudflare-egress.mjs +0 -113
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { DEFAULT_WORKSPACE_RUNTIME_PORT } from "./constants";
|
|
2
|
-
import type { SandboxDriverID } from "
|
|
2
|
+
import type { SandboxDriverID, SandboxSecretBrokering } from "@claxedo/sandbox-contract";
|
|
3
|
+
export type { SandboxSecretBrokering } from "@claxedo/sandbox-contract";
|
|
3
4
|
import { type SandboxCheckpointCaptureInput, type SandboxCheckpointRestoreInput, type SandboxCheckpointResult } from "./checkpoint-manager";
|
|
4
5
|
export { DEFAULT_WORKSPACE_RUNTIME_PORT };
|
|
5
6
|
export * from "./checkpoint-manager";
|
|
@@ -72,25 +73,16 @@ export type SandboxDriverMetadata = {
|
|
|
72
73
|
* How the driver can honor a `SandboxBrokeredSecret` — a credential the
|
|
73
74
|
* sandbox may USE for outbound requests but must NEVER be able to READ.
|
|
74
75
|
*
|
|
75
|
-
* - `"native"` — the provider brokers it
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
* driver
|
|
79
|
-
* - `"proxy"` — brokering is achievable but only via a host-operated egress
|
|
80
|
-
* proxy the driver routes through (Cloudflare's official Worker-proxy
|
|
81
|
-
* pattern: the sandbox gets a short-lived JWT, egress hits the Worker,
|
|
82
|
-
* the Worker injects the real credential — the raw value never enters the
|
|
83
|
-
* sandbox). Not automatic; requires the host's proxy to be wired.
|
|
76
|
+
* - `"native"` — the provider brokers it on egress to the allowlisted hosts
|
|
77
|
+
* with no extra infrastructure of ours: Daytona secret placeholders,
|
|
78
|
+
* Vercel firewall header transforms, Cloudflare outbound handlers reading
|
|
79
|
+
* the value from KV. The driver installs it during `ensureHost`.
|
|
84
80
|
* - `"none"` — no way to keep the value out of sandbox processes. The
|
|
85
81
|
* provider may still have an encrypted secret STORE (e.g. Modal secrets),
|
|
86
|
-
* but
|
|
82
|
+
* but it is exposed as a readable env var, which cannot satisfy the
|
|
87
83
|
* never-readable contract.
|
|
88
|
-
*
|
|
89
|
-
* The manager fails closed unless the driver actually injects brokered
|
|
90
|
-
* secrets (today: `"native"` only). `"proxy"` records an achievable ceiling
|
|
91
|
-
* that is not yet wired and therefore also fails closed.
|
|
92
84
|
*/
|
|
93
|
-
secretBrokering:
|
|
85
|
+
secretBrokering: SandboxSecretBrokering;
|
|
94
86
|
/**
|
|
95
87
|
* How the driver can enforce a RESTRICTED `SandboxNetworkPolicy` — i.e.
|
|
96
88
|
* whether the sandbox's outbound network can actually be contained.
|
|
@@ -196,7 +188,7 @@ export type SandboxEgressUnenforcedEvent = {
|
|
|
196
188
|
*
|
|
197
189
|
* Guarantees enforced by the manager: brokered secrets are never written to
|
|
198
190
|
* labels, never logged, and never captured in a driver snapshot. A driver
|
|
199
|
-
* whose `metadata.secretBrokering
|
|
191
|
+
* whose `metadata.secretBrokering` is not `"native"` cannot honor them and the
|
|
200
192
|
* manager refuses to provision (fail-closed).
|
|
201
193
|
*/
|
|
202
194
|
export type SandboxBrokeredSecret = {
|
|
@@ -213,7 +205,49 @@ export type SandboxBrokeredSecret = {
|
|
|
213
205
|
* the sandbox to attach itself (Daytona).
|
|
214
206
|
*/
|
|
215
207
|
header?: string;
|
|
208
|
+
/**
|
|
209
|
+
* The authentication scheme `header` carries, when it has one (`Bearer`).
|
|
210
|
+
*
|
|
211
|
+
* A driver that WRITES the whole header composes `"<scheme> <value>"`; one
|
|
212
|
+
* that SUBSTITUTES a placeholder the sandbox already wrote after the scheme
|
|
213
|
+
* ignores it, because the scheme is in the request before the value is.
|
|
214
|
+
*/
|
|
215
|
+
scheme?: string;
|
|
216
|
+
/**
|
|
217
|
+
* The methods and path prefixes the credential may be attached to, on top of
|
|
218
|
+
* `hosts`. A vendor host serves far more than the routes a turn needs —
|
|
219
|
+
* api.anthropic.com also answers the organization-admin API — and everything
|
|
220
|
+
* sharing the sandbox reaches the same host.
|
|
221
|
+
*
|
|
222
|
+
* Optional so a producer that has not been taught to state a policy still
|
|
223
|
+
* type-checks, and fails closed instead: a driver that can express them
|
|
224
|
+
* attaches the credential only within them, so an absent or empty policy
|
|
225
|
+
* names a host and nothing else, and nothing is spendable at a host alone. A
|
|
226
|
+
* driver whose provider cannot express them documents that it drops them,
|
|
227
|
+
* and the host allowlist is all the containment there is.
|
|
228
|
+
*/
|
|
229
|
+
methods?: readonly string[];
|
|
230
|
+
pathPrefixes?: readonly string[];
|
|
216
231
|
};
|
|
232
|
+
/**
|
|
233
|
+
* What the sandbox presents so a header-injecting driver recognizes the request
|
|
234
|
+
* as one asking for that secret.
|
|
235
|
+
*
|
|
236
|
+
* Carries no authority: the value is attached at the edge, keyed by the host
|
|
237
|
+
* and this string, and the string itself is never accepted by a vendor. The
|
|
238
|
+
* Cloudflare sandbox Worker is deployed on its own and cannot depend on this
|
|
239
|
+
* package, so it matches on its own copy of the prefix; a driver minting
|
|
240
|
+
* anything else sends every brokered request upstream bare, which is what
|
|
241
|
+
* `brokered-placeholder.test.ts` exists to catch.
|
|
242
|
+
*/
|
|
243
|
+
export declare function brokeredSecretPlaceholder(name: string): string;
|
|
244
|
+
/**
|
|
245
|
+
* The environment a header-injecting driver must add so the sandbox can present
|
|
246
|
+
* each secret. Daytona is absent from this: its own mount fills the same
|
|
247
|
+
* variables with the placeholder it substitutes, and an env entry would shadow
|
|
248
|
+
* it with a string Daytona does not know.
|
|
249
|
+
*/
|
|
250
|
+
export declare function brokeredPlaceholderEnv(secrets: readonly SandboxBrokeredSecret[] | undefined): Record<string, string>;
|
|
217
251
|
export type SandboxExposure = {
|
|
218
252
|
kind: "loopback";
|
|
219
253
|
} | {
|
|
@@ -290,6 +324,8 @@ export type SandboxNetworkPolicy = {
|
|
|
290
324
|
hosts?: string[];
|
|
291
325
|
cidrs?: string[];
|
|
292
326
|
};
|
|
327
|
+
/** Brokered destinations are the minimum egress needed to use the credential. */
|
|
328
|
+
export declare function sandboxNetworkPolicyWithBrokeredHosts(policy: SandboxNetworkPolicy, secrets: readonly SandboxBrokeredSecret[] | undefined): SandboxNetworkPolicy;
|
|
293
329
|
export type SandboxBootSource = {
|
|
294
330
|
kind: "image";
|
|
295
331
|
image: string;
|
|
@@ -431,6 +467,14 @@ export type SandboxRegisterInput = {
|
|
|
431
467
|
now?: number;
|
|
432
468
|
};
|
|
433
469
|
export type SandboxHeartbeatInput = SandboxRegisterInput;
|
|
470
|
+
/**
|
|
471
|
+
* What a provisioning cycle is actually doing, for honest progress UI.
|
|
472
|
+
* `restore` = booting a replacement restored from the workspace's snapshot
|
|
473
|
+
* (files come back), `resume` = restarting the same paused resource,
|
|
474
|
+
* `cold-start` = a fresh sandbox with no prior state to bring back (first
|
|
475
|
+
* boot, or nothing snapshot-able survived).
|
|
476
|
+
*/
|
|
477
|
+
export type SandboxBootMode = "restore" | "resume" | "cold-start";
|
|
434
478
|
export type SandboxEnsureResult = ({
|
|
435
479
|
status: "ready";
|
|
436
480
|
} & SandboxTarget & {
|
|
@@ -441,6 +485,7 @@ export type SandboxEnsureResult = ({
|
|
|
441
485
|
retryAfterMs: number;
|
|
442
486
|
epoch: number;
|
|
443
487
|
homeRegion: SandboxRegion;
|
|
488
|
+
bootMode?: SandboxBootMode;
|
|
444
489
|
} | {
|
|
445
490
|
status: "unavailable";
|
|
446
491
|
retryAfterMs?: number;
|
package/dist/index.mjs
CHANGED
|
@@ -141,16 +141,16 @@ var SANDBOX_BASELINE_EGRESS_HOSTS = [
|
|
|
141
141
|
];
|
|
142
142
|
function hostOf(value) {
|
|
143
143
|
const raw = value?.trim();
|
|
144
|
-
if (!raw) return;
|
|
144
|
+
if (!raw) return void 0;
|
|
145
145
|
try {
|
|
146
146
|
const host = new URL(raw).hostname.trim().toLowerCase();
|
|
147
147
|
return host ? host : void 0;
|
|
148
148
|
} catch {
|
|
149
|
-
return;
|
|
149
|
+
return void 0;
|
|
150
150
|
}
|
|
151
151
|
}
|
|
152
152
|
function sandboxSourceHost(source) {
|
|
153
|
-
if (!source || source.kind !== "git") return;
|
|
153
|
+
if (!source || source.kind !== "git") return void 0;
|
|
154
154
|
return hostOf(source.repoUrl);
|
|
155
155
|
}
|
|
156
156
|
function hostedSandboxNetworkPolicy(input) {
|
|
@@ -206,6 +206,12 @@ var compositionEgressWarnings = /* @__PURE__ */ new Set();
|
|
|
206
206
|
function defaultEgressUnenforcedSink(event) {
|
|
207
207
|
console.warn(event.message);
|
|
208
208
|
}
|
|
209
|
+
function brokeredSecretPlaceholder(name) {
|
|
210
|
+
return `claxedo-broker:${name}`;
|
|
211
|
+
}
|
|
212
|
+
function brokeredPlaceholderEnv(secrets) {
|
|
213
|
+
return Object.fromEntries((secrets ?? []).map((secret) => [secret.name, brokeredSecretPlaceholder(secret.name)]));
|
|
214
|
+
}
|
|
209
215
|
function applySandboxLeasePatch(current, patch, updatedAt) {
|
|
210
216
|
return {
|
|
211
217
|
...current,
|
|
@@ -226,6 +232,17 @@ function applySandboxLeasePatch(current, patch, updatedAt) {
|
|
|
226
232
|
updatedAt
|
|
227
233
|
};
|
|
228
234
|
}
|
|
235
|
+
function sandboxNetworkPolicyWithBrokeredHosts(policy, secrets) {
|
|
236
|
+
if (policy.mode !== "restricted" || !secrets?.length) return policy;
|
|
237
|
+
const hosts = [.../* @__PURE__ */ new Set([
|
|
238
|
+
...policy.hosts ?? [],
|
|
239
|
+
...secrets.flatMap((secret) => secret.hosts)
|
|
240
|
+
])].toSorted();
|
|
241
|
+
return {
|
|
242
|
+
...policy,
|
|
243
|
+
...hosts.length ? { hosts } : {}
|
|
244
|
+
};
|
|
245
|
+
}
|
|
229
246
|
function isSandboxListingUnsupported(err) {
|
|
230
247
|
return !!err && typeof err === "object" && err.listingUnsupported === true;
|
|
231
248
|
}
|
|
@@ -254,14 +271,17 @@ function ensureHostInput(input) {
|
|
|
254
271
|
workspaceRuntimePort: input.managerInput?.workspaceRuntimePort ?? DEFAULT_WORKSPACE_RUNTIME_PORT,
|
|
255
272
|
env: input.managerInput?.env ?? {},
|
|
256
273
|
// Brokered secrets ride their own channel — NEVER merged into labels or env.
|
|
257
|
-
|
|
274
|
+
// Presence of the key, not its length, is the signal: `[]` is the caller
|
|
275
|
+
// withdrawing every brokered secret, and dropping it would reach the driver
|
|
276
|
+
// as "preserve what you have".
|
|
277
|
+
...input.managerInput?.secrets ? { secrets: input.managerInput.secrets } : {},
|
|
258
278
|
source: input.managerInput?.source,
|
|
259
279
|
exposure: input.managerInput?.exposure ?? (input.driver.metadata.targetAccess === "loopback" ? { kind: "loopback" } : { kind: "relay" }),
|
|
260
280
|
// Withheld, never downgraded: `undefined` here is the same shape a caller
|
|
261
281
|
// that asked for no containment produces, so the throwing drivers take
|
|
262
282
|
// their allow-all path and their throws stay intact for anyone who hands
|
|
263
283
|
// them a policy directly.
|
|
264
|
-
...egress.action === "enforce" && input.managerInput?.net ? { net: input.managerInput.net } : {},
|
|
284
|
+
...egress.action === "enforce" && input.managerInput?.net ? { net: sandboxNetworkPolicyWithBrokeredHosts(input.managerInput.net, input.managerInput.secrets) } : {},
|
|
265
285
|
snapshot: input.managerInput?.snapshot
|
|
266
286
|
};
|
|
267
287
|
}
|
|
@@ -306,17 +326,27 @@ function createSandboxManager(options) {
|
|
|
306
326
|
});
|
|
307
327
|
}
|
|
308
328
|
}
|
|
309
|
-
function
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
lifecycleOperations.set(workspaceId,
|
|
329
|
+
function lifecycleQueue(current) {
|
|
330
|
+
return current?.promise.catch(() => void 0) ?? Promise.resolve();
|
|
331
|
+
}
|
|
332
|
+
function lifecycleSettle(workspaceId, entry, operation) {
|
|
333
|
+
lifecycleOperations.set(workspaceId, entry);
|
|
314
334
|
return operation.finally(() => {
|
|
315
|
-
if (lifecycleOperations.get(workspaceId)
|
|
316
|
-
lifecycleOperations.delete(workspaceId);
|
|
317
|
-
}
|
|
335
|
+
if (lifecycleOperations.get(workspaceId) === entry) lifecycleOperations.delete(workspaceId);
|
|
318
336
|
});
|
|
319
337
|
}
|
|
338
|
+
function checkpointLifecycle(workspaceId, kind, run) {
|
|
339
|
+
const current = lifecycleOperations.get(workspaceId);
|
|
340
|
+
if (current?.kind === kind) return current.promise;
|
|
341
|
+
const operation = lifecycleQueue(current).then(run);
|
|
342
|
+
return lifecycleSettle(workspaceId, { kind, promise: operation }, operation);
|
|
343
|
+
}
|
|
344
|
+
function mutationLifecycle(workspaceId, kind, run) {
|
|
345
|
+
const current = lifecycleOperations.get(workspaceId);
|
|
346
|
+
if (current?.kind === kind) return current.promise;
|
|
347
|
+
const operation = lifecycleQueue(current).then(run);
|
|
348
|
+
return lifecycleSettle(workspaceId, { kind, promise: operation }, operation);
|
|
349
|
+
}
|
|
320
350
|
async function leaseTarget(lease) {
|
|
321
351
|
if (lease.status !== "ready" || !lease.sandboxId || !lease.url || !lease.hostId) {
|
|
322
352
|
return { status: "unavailable", reason: "runtime_lease_not_ready" };
|
|
@@ -337,6 +367,12 @@ function createSandboxManager(options) {
|
|
|
337
367
|
homeRegion: lease.homeRegion
|
|
338
368
|
};
|
|
339
369
|
}
|
|
370
|
+
function leaseBootMode(lease) {
|
|
371
|
+
const restoring = Boolean(lease.checkpoint) && lease.status !== "ready" && options.driver.metadata.persistence.resume === "replacement-restore";
|
|
372
|
+
if (restoring) return "restore";
|
|
373
|
+
if (lease.sandboxId && options.driver.resumeHost) return "resume";
|
|
374
|
+
return "cold-start";
|
|
375
|
+
}
|
|
340
376
|
async function provision(workspaceId, lease, homeRegion, managerInput) {
|
|
341
377
|
try {
|
|
342
378
|
const ensure = ensureHostInput({
|
|
@@ -355,7 +391,8 @@ function createSandboxManager(options) {
|
|
|
355
391
|
homeRegion
|
|
356
392
|
};
|
|
357
393
|
}
|
|
358
|
-
const
|
|
394
|
+
const resuming = Boolean(lease.sandboxId && ensure.bootSource?.kind === "default" && options.driver.resumeHost);
|
|
395
|
+
const target = resuming ? await options.driver.resumeHost({ lease, ensure }) : await options.driver.ensureHost(ensure);
|
|
359
396
|
if ("provisioning" in target) {
|
|
360
397
|
await options.leaseStore.update(workspaceId, lease.epoch, {
|
|
361
398
|
status: "acquiring",
|
|
@@ -365,7 +402,11 @@ function createSandboxManager(options) {
|
|
|
365
402
|
status: "provisioning",
|
|
366
403
|
retryAfterMs: target.retryAfterMs,
|
|
367
404
|
epoch: lease.epoch,
|
|
368
|
-
homeRegion
|
|
405
|
+
homeRegion,
|
|
406
|
+
// Honest progress for the connect UI: which of the three boot paths
|
|
407
|
+
// this cycle is on. Derived from the same inputs that CHOSE the path
|
|
408
|
+
// above, so it cannot drift from what actually ran.
|
|
409
|
+
bootMode: resuming ? "resume" : ensure.bootSource?.kind === "driver-snapshot" ? "restore" : "cold-start"
|
|
369
410
|
};
|
|
370
411
|
}
|
|
371
412
|
const updated = await options.leaseStore.update(workspaceId, lease.epoch, {
|
|
@@ -428,7 +469,8 @@ function createSandboxManager(options) {
|
|
|
428
469
|
status: "provisioning",
|
|
429
470
|
retryAfterMs: existing.nextRetryAt - now(),
|
|
430
471
|
epoch: existing.epoch,
|
|
431
|
-
homeRegion: existing.homeRegion
|
|
472
|
+
homeRegion: existing.homeRegion,
|
|
473
|
+
bootMode: leaseBootMode(existing)
|
|
432
474
|
};
|
|
433
475
|
}
|
|
434
476
|
return {
|
|
@@ -464,7 +506,8 @@ function createSandboxManager(options) {
|
|
|
464
506
|
status: "provisioning",
|
|
465
507
|
retryAfterMs: acquired.retryAfterMs,
|
|
466
508
|
epoch: acquired.lease.epoch,
|
|
467
|
-
homeRegion: acquired.lease.homeRegion
|
|
509
|
+
homeRegion: acquired.lease.homeRegion,
|
|
510
|
+
bootMode: leaseBootMode(acquired.lease)
|
|
468
511
|
};
|
|
469
512
|
}
|
|
470
513
|
return provision(workspaceId, acquired.lease, input.homeRegion, input);
|
|
@@ -493,7 +536,7 @@ function createSandboxManager(options) {
|
|
|
493
536
|
return { ok: true, ...await options.driver.snapshot(target) };
|
|
494
537
|
},
|
|
495
538
|
async checkpoint(workspaceId, input) {
|
|
496
|
-
return await
|
|
539
|
+
return await checkpointLifecycle(workspaceId, "checkpoint", () => captureSandboxCheckpoint({
|
|
497
540
|
workspaceId,
|
|
498
541
|
request: input,
|
|
499
542
|
leaseStore: options.leaseStore,
|
|
@@ -503,7 +546,7 @@ function createSandboxManager(options) {
|
|
|
503
546
|
}));
|
|
504
547
|
},
|
|
505
548
|
async restore(workspaceId, input) {
|
|
506
|
-
return await
|
|
549
|
+
return await checkpointLifecycle(workspaceId, "restore", () => restoreSandboxCheckpoint({
|
|
507
550
|
workspaceId,
|
|
508
551
|
request: input,
|
|
509
552
|
leaseStore: options.leaseStore,
|
|
@@ -512,7 +555,7 @@ function createSandboxManager(options) {
|
|
|
512
555
|
}));
|
|
513
556
|
},
|
|
514
557
|
async stop(workspaceId) {
|
|
515
|
-
return await
|
|
558
|
+
return await mutationLifecycle(workspaceId, "stop", async () => {
|
|
516
559
|
const lease = await options.leaseStore.get(workspaceId);
|
|
517
560
|
if (lease?.status === "stopped") return { ok: true, status: "stopped" };
|
|
518
561
|
const target = await this.target(workspaceId);
|
|
@@ -527,7 +570,7 @@ function createSandboxManager(options) {
|
|
|
527
570
|
});
|
|
528
571
|
},
|
|
529
572
|
async destroy(workspaceId) {
|
|
530
|
-
return await
|
|
573
|
+
return await mutationLifecycle(workspaceId, "destroy", async () => {
|
|
531
574
|
const lease = await options.leaseStore.get(workspaceId);
|
|
532
575
|
if (lease?.status === "destroyed") return { ok: true, status: "destroyed" };
|
|
533
576
|
const target = await this.target(workspaceId);
|
|
@@ -617,7 +660,7 @@ function createSandboxManager(options) {
|
|
|
617
660
|
const expectedEpoch = input.epoch ?? current.epoch;
|
|
618
661
|
if (current.epoch !== expectedEpoch) return { ok: false, reason: "runtime_lease_epoch_mismatch" };
|
|
619
662
|
const timestamp = input.now ?? now();
|
|
620
|
-
const status = input.status === "unhealthy" || input.ok
|
|
663
|
+
const status = input.status === "unhealthy" || !input.ok ? "unavailable" : input.ok ? "ready" : current.status;
|
|
621
664
|
const updated = await options.leaseStore.update(workspaceId, expectedEpoch, {
|
|
622
665
|
status,
|
|
623
666
|
...input.sandboxId ? { sandboxId: input.sandboxId } : {},
|
|
@@ -638,12 +681,15 @@ export {
|
|
|
638
681
|
SANDBOX_MODEL_PROVIDER_HOSTS,
|
|
639
682
|
SANDBOX_PACKAGE_REGISTRY_HOSTS,
|
|
640
683
|
applySandboxLeasePatch,
|
|
684
|
+
brokeredPlaceholderEnv,
|
|
685
|
+
brokeredSecretPlaceholder,
|
|
641
686
|
captureSandboxCheckpoint,
|
|
642
687
|
createSandboxManager,
|
|
643
688
|
hostedSandboxNetworkPolicy,
|
|
644
689
|
isSandboxListingUnsupported,
|
|
645
690
|
restoreSandboxCheckpoint,
|
|
646
691
|
sandboxEgressDisposition,
|
|
692
|
+
sandboxNetworkPolicyWithBrokeredHosts,
|
|
647
693
|
sandboxSourceHost,
|
|
648
694
|
validateSandboxPersistenceCapabilities
|
|
649
695
|
};
|
package/dist/json.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading untrusted JSON: driver API responses, CLI output, stored records.
|
|
3
|
+
*
|
|
4
|
+
* Drivers all face the same problem — a provider hands back `any`, and the code
|
|
5
|
+
* needs a few named fields out of it. Narrowing through these keeps the check
|
|
6
|
+
* and the type in the same place, instead of each site asserting a shape it
|
|
7
|
+
* never verified. The object check is `@claxedo/helpers/guards`, re-exported
|
|
8
|
+
* under the names the drivers use.
|
|
9
|
+
*/
|
|
10
|
+
export { asRecord as record, isRecord } from "@claxedo/helpers/guards";
|
|
11
|
+
/** A non-empty string, or undefined — the shape almost every provider field wants. */
|
|
12
|
+
export declare function text(input: unknown): string | undefined;
|
|
13
|
+
export declare function num(input: unknown): number | undefined;
|
package/dist/lease-policy.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { SandboxLeaseRow } from "./lease-types";
|
|
2
|
-
import {
|
|
2
|
+
import type { SandboxDriverID } from "@claxedo/sandbox-contract";
|
|
3
3
|
export type SandboxDecision = {
|
|
4
4
|
action: "skip";
|
|
5
5
|
reason: string;
|
|
@@ -44,5 +44,9 @@ export type SandboxDriverPlacement = {
|
|
|
44
44
|
export declare const DEFAULT_WORKSPACE_HOST_DECISION_CONFIG: SandboxDecisionConfig;
|
|
45
45
|
export declare function sandboxDriverPlacement(driver: SandboxDriverID): SandboxDriverPlacement;
|
|
46
46
|
export declare function decideSandboxStart(lease: SandboxLeaseRow, placement: SandboxDriverPlacement, now: number, config?: SandboxDecisionConfig): SandboxDecision;
|
|
47
|
-
|
|
47
|
+
/**
|
|
48
|
+
* Health-failure policy is placement-independent: retry budget and backoff are
|
|
49
|
+
* the same question for every driver, so this deliberately takes no placement.
|
|
50
|
+
*/
|
|
51
|
+
export declare function decideSandboxHealthFailure(lease: SandboxLeaseRow, now: number, config?: SandboxDecisionConfig): SandboxDecision;
|
|
48
52
|
export declare function nextSandboxRetryAt(retryCount: number, now: number, config?: SandboxDecisionConfig): number | null;
|
package/dist/lease-policy.mjs
CHANGED
|
@@ -24,12 +24,42 @@ var SANDBOX_BASELINE_EGRESS_HOSTS = [
|
|
|
24
24
|
...SANDBOX_PACKAGE_REGISTRY_HOSTS
|
|
25
25
|
];
|
|
26
26
|
|
|
27
|
+
// ../sandbox-contract/dist/index.mjs
|
|
28
|
+
var sandboxDriverCredentialFields = {
|
|
29
|
+
exe: [{ key: "api_token", label: "API Token", secret: true }],
|
|
30
|
+
daytona: [{ key: "api_key", label: "API Key", secret: true }],
|
|
31
|
+
modal: [
|
|
32
|
+
{ key: "token_id", label: "Token ID", secret: true },
|
|
33
|
+
{ key: "token_secret", label: "Token Secret", secret: true }
|
|
34
|
+
],
|
|
35
|
+
vercel: [
|
|
36
|
+
{ key: "access_token", label: "Access Token", secret: true },
|
|
37
|
+
{ key: "team_id", label: "Team ID" },
|
|
38
|
+
{ key: "project_id", label: "Project ID" }
|
|
39
|
+
],
|
|
40
|
+
cloudflare: [
|
|
41
|
+
{ key: "api_token", label: "API Token", secret: true },
|
|
42
|
+
{ key: "worker_url", label: "Worker URL" }
|
|
43
|
+
],
|
|
44
|
+
box: [{ key: "api_key", label: "API Key", secret: true }],
|
|
45
|
+
docker: [{ key: "image", label: "Image" }]
|
|
46
|
+
};
|
|
47
|
+
var sandboxDriverLabels = {
|
|
48
|
+
exe: "exe.dev",
|
|
49
|
+
daytona: "Daytona",
|
|
50
|
+
modal: "Modal",
|
|
51
|
+
vercel: "Vercel",
|
|
52
|
+
cloudflare: "Cloudflare",
|
|
53
|
+
box: "Box",
|
|
54
|
+
docker: "Docker"
|
|
55
|
+
};
|
|
56
|
+
|
|
27
57
|
// src/driver-catalog.ts
|
|
28
58
|
var sandboxDriverCatalog = {
|
|
29
59
|
exe: {
|
|
30
60
|
id: "exe",
|
|
31
|
-
label:
|
|
32
|
-
credentialFields:
|
|
61
|
+
label: sandboxDriverLabels.exe,
|
|
62
|
+
credentialFields: sandboxDriverCredentialFields.exe,
|
|
33
63
|
metadata: {
|
|
34
64
|
driverRunsIn: ["worker", "node"],
|
|
35
65
|
hostStopBehavior: "not-supported",
|
|
@@ -49,17 +79,15 @@ var sandboxDriverCatalog = {
|
|
|
49
79
|
},
|
|
50
80
|
daytona: {
|
|
51
81
|
id: "daytona",
|
|
52
|
-
label:
|
|
53
|
-
credentialFields:
|
|
82
|
+
label: sandboxDriverLabels.daytona,
|
|
83
|
+
credentialFields: sandboxDriverCredentialFields.daytona,
|
|
54
84
|
metadata: {
|
|
55
85
|
driverRunsIn: ["worker", "node"],
|
|
56
86
|
hostStopBehavior: "suspends-host",
|
|
57
87
|
hostResumeBehavior: "same-host",
|
|
58
88
|
targetAccess: "relay",
|
|
59
89
|
secretBrokering: "native",
|
|
60
|
-
//
|
|
61
|
-
// by address: `domainAllowList` (names) alongside `networkAllowList`
|
|
62
|
-
// (CIDRs), with `networkBlockAll` as the deny-all floor.
|
|
90
|
+
// The driver translates names to domainAllowList, or addresses to networkAllowList.
|
|
63
91
|
egressControl: "hosts-and-cidrs",
|
|
64
92
|
persistence: {
|
|
65
93
|
resume: "same-sandbox",
|
|
@@ -73,20 +101,15 @@ var sandboxDriverCatalog = {
|
|
|
73
101
|
},
|
|
74
102
|
modal: {
|
|
75
103
|
id: "modal",
|
|
76
|
-
label:
|
|
77
|
-
credentialFields:
|
|
78
|
-
{ key: "token_id", label: "Token ID", secret: true },
|
|
79
|
-
{ key: "token_secret", label: "Token Secret", secret: true }
|
|
80
|
-
],
|
|
104
|
+
label: sandboxDriverLabels.modal,
|
|
105
|
+
credentialFields: sandboxDriverCredentialFields.modal,
|
|
81
106
|
metadata: {
|
|
82
107
|
driverRunsIn: ["node"],
|
|
83
108
|
hostStopBehavior: "terminates-host",
|
|
84
109
|
hostResumeBehavior: "replacement-host",
|
|
85
110
|
targetAccess: "relay",
|
|
86
111
|
secretBrokering: "none",
|
|
87
|
-
//
|
|
88
|
-
// an allowlist, and its driver throws on a host policy. A blackout is
|
|
89
|
-
// not containment for a workspace that has to clone and reach a model.
|
|
112
|
+
// The driver does not install Modal's domain allowlist or alpha sidecar.
|
|
90
113
|
egressControl: "none",
|
|
91
114
|
persistence: {
|
|
92
115
|
resume: "replacement-restore",
|
|
@@ -100,20 +123,15 @@ var sandboxDriverCatalog = {
|
|
|
100
123
|
},
|
|
101
124
|
vercel: {
|
|
102
125
|
id: "vercel",
|
|
103
|
-
label:
|
|
104
|
-
credentialFields:
|
|
105
|
-
{ key: "access_token", label: "Access Token", secret: true },
|
|
106
|
-
{ key: "team_id", label: "Team ID" },
|
|
107
|
-
{ key: "project_id", label: "Project ID" }
|
|
108
|
-
],
|
|
126
|
+
label: sandboxDriverLabels.vercel,
|
|
127
|
+
credentialFields: sandboxDriverCredentialFields.vercel,
|
|
109
128
|
metadata: {
|
|
110
129
|
driverRunsIn: ["node"],
|
|
111
130
|
hostStopBehavior: "terminates-host",
|
|
112
131
|
hostResumeBehavior: "replacement-host",
|
|
113
132
|
targetAccess: "relay",
|
|
114
133
|
secretBrokering: "native",
|
|
115
|
-
//
|
|
116
|
-
// never CIDRs.
|
|
134
|
+
// The driver translates hosts only; the SDK's subnet rules are not wired to net.cidrs.
|
|
117
135
|
egressControl: "hosts",
|
|
118
136
|
persistence: {
|
|
119
137
|
resume: "replacement-restore",
|
|
@@ -127,23 +145,15 @@ var sandboxDriverCatalog = {
|
|
|
127
145
|
},
|
|
128
146
|
cloudflare: {
|
|
129
147
|
id: "cloudflare",
|
|
130
|
-
label:
|
|
131
|
-
credentialFields:
|
|
132
|
-
{ key: "api_token", label: "API Token", secret: true },
|
|
133
|
-
{ key: "worker_url", label: "Worker URL" }
|
|
134
|
-
],
|
|
148
|
+
label: sandboxDriverLabels.cloudflare,
|
|
149
|
+
credentialFields: sandboxDriverCredentialFields.cloudflare,
|
|
135
150
|
metadata: {
|
|
136
151
|
driverRunsIn: ["worker"],
|
|
137
152
|
hostStopBehavior: "not-supported",
|
|
138
153
|
hostResumeBehavior: "same-host",
|
|
139
154
|
targetAccess: "relay",
|
|
140
|
-
secretBrokering: "
|
|
141
|
-
//
|
|
142
|
-
// (drivers/cloudflare-egress.ts) is an OPT-IN proxy the sandbox chooses
|
|
143
|
-
// to route brokered-credential requests through, not a boundary. The
|
|
144
|
-
// driver therefore drops `net` entirely — which is exactly why this
|
|
145
|
-
// must be declared, so the manager refuses instead of provisioning a
|
|
146
|
-
// sandbox the caller believes is contained.
|
|
155
|
+
secretBrokering: "native",
|
|
156
|
+
// Credential handlers do not restrict unrelated destinations.
|
|
147
157
|
egressControl: "none",
|
|
148
158
|
persistence: {
|
|
149
159
|
resume: "replacement-restore",
|
|
@@ -157,8 +167,8 @@ var sandboxDriverCatalog = {
|
|
|
157
167
|
},
|
|
158
168
|
box: {
|
|
159
169
|
id: "box",
|
|
160
|
-
label:
|
|
161
|
-
credentialFields:
|
|
170
|
+
label: sandboxDriverLabels.box,
|
|
171
|
+
credentialFields: sandboxDriverCredentialFields.box,
|
|
162
172
|
metadata: {
|
|
163
173
|
driverRunsIn: ["node"],
|
|
164
174
|
hostStopBehavior: "suspends-host",
|
|
@@ -178,8 +188,8 @@ var sandboxDriverCatalog = {
|
|
|
178
188
|
},
|
|
179
189
|
docker: {
|
|
180
190
|
id: "docker",
|
|
181
|
-
label:
|
|
182
|
-
credentialFields:
|
|
191
|
+
label: sandboxDriverLabels.docker,
|
|
192
|
+
credentialFields: sandboxDriverCredentialFields.docker,
|
|
183
193
|
metadata: {
|
|
184
194
|
driverRunsIn: ["local"],
|
|
185
195
|
hostStopBehavior: "terminates-host",
|
|
@@ -236,7 +246,7 @@ function decideSandboxStart(lease, placement, now, config = DEFAULT_WORKSPACE_HO
|
|
|
236
246
|
}
|
|
237
247
|
return decideStartMethod(lease, placement);
|
|
238
248
|
}
|
|
239
|
-
function decideSandboxHealthFailure(lease,
|
|
249
|
+
function decideSandboxHealthFailure(lease, now, config = DEFAULT_WORKSPACE_HOST_DECISION_CONFIG) {
|
|
240
250
|
if (lease.status !== "ready" && lease.status !== "unhealthy") {
|
|
241
251
|
return { action: "skip", reason: `status is ${lease.status}, health check not applicable` };
|
|
242
252
|
}
|
package/dist/lease-types.d.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { SandboxDriverID } from "./driver-catalog";
|
|
2
1
|
import type { SandboxCheckpointReference, SandboxPersistenceCapabilities, SandboxRestoreStatus } from "./index";
|
|
3
2
|
export type SandboxLeaseRowStatus = "pending" | "acquiring" | "starting" | "ready" | "unhealthy" | "backoff" | "stopping" | "stopped" | "destroyed" | "failed";
|
|
4
3
|
export type SandboxComputeClass = "small" | "medium" | "large" | "gpu";
|
|
@@ -8,7 +7,17 @@ export type SandboxLeaseRow = {
|
|
|
8
7
|
home_region?: string;
|
|
9
8
|
epoch: number;
|
|
10
9
|
status: SandboxLeaseRowStatus;
|
|
11
|
-
|
|
10
|
+
/**
|
|
11
|
+
* The id the driver was registered with, which is any string: the manager
|
|
12
|
+
* dispatches on whatever a composition registers (`stores/d1.test.ts`
|
|
13
|
+
* provisions one called `"test-provider"`), and every other type this value
|
|
14
|
+
* flows between — `SandboxLease["driver"]`, `SandboxLeaseAcquireInput["driver"]`,
|
|
15
|
+
* `SandboxTarget["driver"]["id"]` — is already `string`. Typing the row as the
|
|
16
|
+
* closed `SandboxDriverID` made it the odd one out and forced every store to
|
|
17
|
+
* assert into it. Narrow with `isSandboxDriverID` where a CATALOG entry is
|
|
18
|
+
* needed, which is the only place the closed set is the right question.
|
|
19
|
+
*/
|
|
20
|
+
driver: string;
|
|
12
21
|
driver_resource_id: string | null;
|
|
13
22
|
driver_snapshot_id: string | null;
|
|
14
23
|
sandbox_id: string | null;
|
package/dist/runtime-env.d.ts
CHANGED
|
@@ -1,4 +1,15 @@
|
|
|
1
|
-
import type { SandboxDriverID } from "
|
|
1
|
+
import type { SandboxDriverID } from "@claxedo/sandbox-contract";
|
|
2
|
+
export type WorkspaceRuntimeControlEnv = {
|
|
3
|
+
relayJwksUrl?: string;
|
|
4
|
+
relayVerifyPem?: string;
|
|
5
|
+
managementJwksUrl?: string;
|
|
6
|
+
sessionAuthorityUrl?: string;
|
|
7
|
+
};
|
|
8
|
+
export declare function workspaceRuntimeBootEnv(input: Parameters<typeof workspaceRuntimeTargetEnv>[0] & Parameters<typeof workspaceRuntimeSourceEnv>[0] & {
|
|
9
|
+
env?: Record<string, string>;
|
|
10
|
+
runner?: string;
|
|
11
|
+
controlEnv?: WorkspaceRuntimeControlEnv;
|
|
12
|
+
}): Record<string, string>;
|
|
2
13
|
export declare function workspaceRuntimeTargetEnv(input: {
|
|
3
14
|
workspaceId: string;
|
|
4
15
|
hostId?: string;
|
package/dist/runtime-env.mjs
CHANGED
|
@@ -1,4 +1,17 @@
|
|
|
1
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
|
+
}
|
|
2
15
|
function workspaceRuntimeTargetEnv(input) {
|
|
3
16
|
return {
|
|
4
17
|
WORKSPACE_RUNTIME_WORKSPACE_ID: input.workspaceId,
|
|
@@ -27,6 +40,7 @@ function workspaceRuntimeServiceExposureEnv(input) {
|
|
|
27
40
|
};
|
|
28
41
|
}
|
|
29
42
|
export {
|
|
43
|
+
workspaceRuntimeBootEnv,
|
|
30
44
|
workspaceRuntimeServiceExposureEnv,
|
|
31
45
|
workspaceRuntimeSourceEnv,
|
|
32
46
|
workspaceRuntimeTargetEnv
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { SandboxDriverID } from "
|
|
1
|
+
import type { SandboxDriverID } from "@claxedo/sandbox-contract";
|
|
2
2
|
export type SandboxServiceUrlExposure = {
|
|
3
3
|
source: "driver-service-url";
|
|
4
4
|
access: "private" | "public" | "driver-authenticated" | "unknown";
|
|
@@ -7,4 +7,3 @@ export type SandboxServiceUrlExposure = {
|
|
|
7
7
|
note?: string;
|
|
8
8
|
};
|
|
9
9
|
export declare function sandboxServiceUrlExposure(driver: SandboxDriverID): SandboxServiceUrlExposure;
|
|
10
|
-
export declare function sandboxServiceUrlExposureEnv(driver: SandboxDriverID): Record<string, string>;
|
|
@@ -1,15 +1,3 @@
|
|
|
1
|
-
// src/runtime-env.ts
|
|
2
|
-
function workspaceRuntimeServiceExposureEnv(input) {
|
|
3
|
-
return {
|
|
4
|
-
...input.driver === "docker" ? { WORKSPACE_RUNTIME_HOST: "0.0.0.0" } : {},
|
|
5
|
-
WORKSPACE_RUNTIME_SERVICE_EXPOSURE_SOURCE: input.source,
|
|
6
|
-
WORKSPACE_RUNTIME_SERVICE_EXPOSURE_ACCESS: input.access,
|
|
7
|
-
WORKSPACE_RUNTIME_SERVICE_EXPOSURE_DRIVER: input.driver,
|
|
8
|
-
...input.fallbackAccess ? { WORKSPACE_RUNTIME_SERVICE_EXPOSURE_FALLBACK_ACCESS: input.fallbackAccess } : {},
|
|
9
|
-
...input.note ? { WORKSPACE_RUNTIME_SERVICE_EXPOSURE_NOTE: input.note } : {}
|
|
10
|
-
};
|
|
11
|
-
}
|
|
12
|
-
|
|
13
1
|
// src/service-url-exposure.ts
|
|
14
2
|
function sandboxServiceUrlExposure(driver) {
|
|
15
3
|
if (driver === "docker") {
|
|
@@ -35,10 +23,6 @@ function sandboxServiceUrlExposure(driver) {
|
|
|
35
23
|
driver
|
|
36
24
|
};
|
|
37
25
|
}
|
|
38
|
-
function sandboxServiceUrlExposureEnv(driver) {
|
|
39
|
-
return workspaceRuntimeServiceExposureEnv(sandboxServiceUrlExposure(driver));
|
|
40
|
-
}
|
|
41
26
|
export {
|
|
42
|
-
sandboxServiceUrlExposure
|
|
43
|
-
sandboxServiceUrlExposureEnv
|
|
27
|
+
sandboxServiceUrlExposure
|
|
44
28
|
};
|