@claxedo/sandbox-manager 0.5.2 → 0.7.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 +50 -1
- package/dist/checkpoint-manager.d.ts +47 -0
- package/dist/checkpoint-manager.mjs +116 -0
- package/dist/daytona-allow-list.d.ts +11 -0
- package/dist/daytona-allow-list.mjs +5 -1
- package/dist/driver-catalog.d.ts +9 -6
- package/dist/driver-catalog.mjs +150 -17
- package/dist/drivers/box.mjs +205 -10
- package/dist/drivers/cloudflare.d.ts +15 -1
- package/dist/drivers/cloudflare.mjs +301 -2
- package/dist/drivers/daytona.d.ts +33 -0
- package/dist/drivers/daytona.mjs +310 -2
- package/dist/drivers/docker.mjs +205 -10
- package/dist/drivers/exe.d.ts +26 -0
- package/dist/drivers/exe.mjs +469 -0
- package/dist/drivers/fetch-bridge.mjs +10 -1
- package/dist/drivers/modal.mjs +205 -10
- package/dist/drivers/vercel.d.ts +23 -4
- package/dist/drivers/vercel.mjs +225 -19
- package/dist/hosted-network-policy.d.ts +87 -0
- package/dist/hosted-network-policy.mjs +61 -0
- package/dist/image.d.ts +16 -1
- package/dist/image.mjs +14 -21
- package/dist/index.d.ts +220 -1
- package/dist/index.mjs +341 -20
- package/dist/lease-policy.d.ts +1 -2
- package/dist/lease-policy.mjs +203 -54
- package/dist/lease-types.d.ts +6 -1
- package/dist/runtime-version.mjs +1 -9
- package/dist/stores/memory.mjs +37 -2
- package/package.json +16 -2
package/README.md
CHANGED
|
@@ -99,7 +99,7 @@ Per-provider mechanism (from each provider's official docs):
|
|
|
99
99
|
| Driver | `secretBrokering` | Mechanism |
|
|
100
100
|
| --- | --- | --- |
|
|
101
101
|
| Daytona | `native` | `daytona.secret.create({name, value, hosts})`; the sandbox env holds an opaque `dtn_secret_…` placeholder and an egress proxy substitutes the real value only for allowlisted hosts ([docs](https://www.daytona.io/docs/en/secrets/)). |
|
|
102
|
-
| Vercel | `native` | Firewall header-transform (`updateNetworkPolicy`): the value is spliced onto egress to the allowlisted hosts as `header
|
|
102
|
+
| Vercel | `native` | Firewall header-transform (`updateNetworkPolicy`): the value is spliced onto egress to the allowlisted hosts as `header`, so the sandbox makes an unauthenticated request ([docs](https://vercel.com/docs/sandbox/concepts/firewall)). `updateNetworkPolicy` replaces the whole policy, so the driver sends the **union** of the create-time allow-list and the brokered hosts — attaching a credential must not revoke egress the caller was already granted (a mid-run `npm install` would start failing), nor widen it to a brokered host the create-time policy never approved. |
|
|
103
103
|
| Cloudflare | `proxy` | Implemented via the official [Worker-proxy pattern](https://developers.cloudflare.com/sandbox/guides/proxy-requests/): the driver sends the value to the sandbox Worker (API-token-gated, never into the container); the Worker stores it in KV, mints a short-lived per-sandbox JWT, and sets `CLAXEDO_EGRESS_PROXY_URL`/`CLAXEDO_EGRESS_TOKEN`/`CLAXEDO_EGRESS_HOSTS` in the container. The sandbox routes brokered-host egress through the Worker, which validates the JWT and injects the credential as `header` — the raw value never enters the sandbox. The reusable core is `@claxedo/sandbox-manager/drivers/cloudflare-egress` (mint/verify + `handleEgressRequest`). |
|
|
104
104
|
| Modal | `none` | Modal [Secrets](https://modal.com/docs/guide/secrets) are an encrypted by-reference store, but exposed as **readable env vars** inside the sandbox — they cannot satisfy the never-readable contract. |
|
|
105
105
|
| Docker, fetch | `none` | Plaintext env only. |
|
|
@@ -120,6 +120,55 @@ never logged, and never captured in a driver snapshot.
|
|
|
120
120
|
> improves storage hygiene for *readable* credentials but does not hide them
|
|
121
121
|
> from the sandbox.
|
|
122
122
|
|
|
123
|
+
## Egress containment
|
|
124
|
+
|
|
125
|
+
`SandboxManagerInput.net` restricts what the sandbox can reach on the network.
|
|
126
|
+
It is a *separate* control from brokered secrets, with a **different** posture,
|
|
127
|
+
and the difference matters:
|
|
128
|
+
|
|
129
|
+
- **Brokered secrets fail closed.** A driver that cannot broker refuses to
|
|
130
|
+
provision (`error: "secret_brokering_unsupported"`).
|
|
131
|
+
- **Egress does not.** Only `daytona` (names + CIDRs) and `vercel` (names) can
|
|
132
|
+
enforce an allowlist. Every other driver declares
|
|
133
|
+
`metadata.egressControl: "none"`, and for those the manager **withholds** the
|
|
134
|
+
policy and provisions anyway — the sandbox runs with unrestricted egress.
|
|
135
|
+
|
|
136
|
+
Withholding rather than passing is deliberate: `exe`, `docker`, `modal` and
|
|
137
|
+
`box` throw when handed a restricted policy, and `cloudflare` and the fetch
|
|
138
|
+
bridge accept one and silently ignore it. Withholding at the manager means the
|
|
139
|
+
throwing drivers never see a policy (their throws stay as their own last line of
|
|
140
|
+
defence) and the silently-dropping ones stop pretending.
|
|
141
|
+
|
|
142
|
+
The gap is never silent. `createSandboxManager` warns at composition, and again
|
|
143
|
+
each time a policy is withheld:
|
|
144
|
+
|
|
145
|
+
```text
|
|
146
|
+
[sandbox-manager] SANDBOX EGRESS IS UNRESTRICTED: driver "cloudflare" declares
|
|
147
|
+
egressControl: "none", so workspace ws_1 can reach ANY host on the internet …
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Pass `onEgressUnenforced` to route that into telemetry instead of `console.warn`
|
|
151
|
+
— it receives a structured `SandboxEgressUnenforcedEvent`. Overriding the sink
|
|
152
|
+
replaces the console warning, so only do it if the replacement is as visible.
|
|
153
|
+
|
|
154
|
+
**Reuse and resume reapply the policy.** A restricted policy only reaches a
|
|
155
|
+
provider as *creation* parameters, so a sandbox handed back rather than created
|
|
156
|
+
— reuse in `ensureHost`, resume in `resumeHost` — would otherwise run on the
|
|
157
|
+
policy in force when it was first created, possibly a wider one from an earlier
|
|
158
|
+
caller. Daytona's allow-list is mutable post-create
|
|
159
|
+
(`Sandbox.updateNetworkSettings`), so `daytona` reapplies the requested policy
|
|
160
|
+
on both paths. A client too old to expose that call makes the driver **refuse
|
|
161
|
+
the reuse**: a policy reported as applied but not in force is worse than no
|
|
162
|
+
sandbox. Requesting no containment leaves the existing policy alone rather than
|
|
163
|
+
clearing it — "not requested" is not "please unrestrict".
|
|
164
|
+
|
|
165
|
+
`driver.metadata.egressControl` is the machine-readable source of truth, and
|
|
166
|
+
`sandboxEgressDisposition(control, net)` is the pure predicate the manager uses,
|
|
167
|
+
so you can ask the same question before composing. One exception stays fail
|
|
168
|
+
closed: a hosts-only driver handed an address-only policy is refused
|
|
169
|
+
(`sandbox_egress_policy_unenforceable`) rather than degraded, because that
|
|
170
|
+
driver *does* enforce egress — it just cannot express that encoding.
|
|
171
|
+
|
|
123
172
|
> SDK conformance: the Daytona secret API and Vercel network-policy transform
|
|
124
173
|
> shapes follow the providers' official docs and are validated structurally
|
|
125
174
|
> against the pinned SDK types; like all provider calls in this package they
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { SandboxCheckpointReference, SandboxEnsureResult, SandboxLease, SandboxLeaseStore, SandboxManagerInput, SandboxSnapshotManagerResult, SandboxTargetResult } from ".";
|
|
2
|
+
export type SandboxCheckpointRuntime = {
|
|
3
|
+
freeze: (policy: "drain" | "interrupt") => Promise<void>;
|
|
4
|
+
flush: () => Promise<void>;
|
|
5
|
+
scrub: () => Promise<void>;
|
|
6
|
+
resume: () => Promise<void>;
|
|
7
|
+
reconcile: (input: {
|
|
8
|
+
epoch: number;
|
|
9
|
+
checkpointId: string;
|
|
10
|
+
}) => Promise<void>;
|
|
11
|
+
};
|
|
12
|
+
export type SandboxCheckpointCaptureInput = {
|
|
13
|
+
runtime: SandboxCheckpointRuntime;
|
|
14
|
+
policy?: "drain" | "interrupt";
|
|
15
|
+
retentionExpiresAt?: number;
|
|
16
|
+
};
|
|
17
|
+
export type SandboxCheckpointRestoreInput = {
|
|
18
|
+
runtime: SandboxCheckpointRuntime;
|
|
19
|
+
checkpointId?: string;
|
|
20
|
+
ensure?: Omit<SandboxManagerInput, "homeRegion">;
|
|
21
|
+
};
|
|
22
|
+
export type SandboxCheckpointResult = {
|
|
23
|
+
status: "ready";
|
|
24
|
+
lease: SandboxLease;
|
|
25
|
+
checkpoint: SandboxCheckpointReference;
|
|
26
|
+
} | {
|
|
27
|
+
status: "provisioning";
|
|
28
|
+
lease: SandboxLease;
|
|
29
|
+
checkpoint: SandboxCheckpointReference;
|
|
30
|
+
retryAfterMs: number;
|
|
31
|
+
};
|
|
32
|
+
export declare function captureSandboxCheckpoint(input: {
|
|
33
|
+
workspaceId: string;
|
|
34
|
+
request: SandboxCheckpointCaptureInput;
|
|
35
|
+
leaseStore: SandboxLeaseStore;
|
|
36
|
+
target: (workspaceId: string) => Promise<SandboxTargetResult>;
|
|
37
|
+
snapshot: (workspaceId: string) => Promise<SandboxSnapshotManagerResult>;
|
|
38
|
+
now?: () => number;
|
|
39
|
+
checkpointId?: () => string;
|
|
40
|
+
}): Promise<SandboxCheckpointResult>;
|
|
41
|
+
export declare function restoreSandboxCheckpoint(input: {
|
|
42
|
+
workspaceId: string;
|
|
43
|
+
request: SandboxCheckpointRestoreInput;
|
|
44
|
+
leaseStore: SandboxLeaseStore;
|
|
45
|
+
ensure: (workspaceId: string, input: SandboxManagerInput) => Promise<SandboxEnsureResult>;
|
|
46
|
+
now?: () => number;
|
|
47
|
+
}): Promise<SandboxCheckpointResult>;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// src/checkpoint-manager.ts
|
|
2
|
+
async function captureSandboxCheckpoint(input) {
|
|
3
|
+
const lease = await requireLease(input.leaseStore, input.workspaceId);
|
|
4
|
+
if (lease.status !== "ready") throw new Error("workspace_checkpoint_lease_not_ready");
|
|
5
|
+
const persistence = lease.persistence;
|
|
6
|
+
if (!persistence || persistence.capture === "none") throw new Error("workspace_checkpoint_unsupported");
|
|
7
|
+
const target = await input.target(input.workspaceId);
|
|
8
|
+
if (target.status !== "ready") throw new Error(target.reason);
|
|
9
|
+
await input.request.runtime.freeze(input.request.policy ?? "drain");
|
|
10
|
+
let capturedSource = false;
|
|
11
|
+
try {
|
|
12
|
+
await input.request.runtime.flush();
|
|
13
|
+
await input.request.runtime.scrub();
|
|
14
|
+
const captured = persistence.capture === "same-resource" ? { ok: true, snapshotId: target.driverResourceId ?? target.sandboxId } : await input.snapshot(input.workspaceId);
|
|
15
|
+
if (!captured.ok) throw new Error(captured.reason);
|
|
16
|
+
capturedSource = true;
|
|
17
|
+
const capturedAt = (input.now ?? Date.now)();
|
|
18
|
+
const checkpoint = {
|
|
19
|
+
id: input.checkpointId?.() ?? `cp_${lease.epoch}_${crypto.randomUUID()}`,
|
|
20
|
+
providerReference: captured.snapshotId,
|
|
21
|
+
sourceEpoch: lease.epoch,
|
|
22
|
+
capturedAt,
|
|
23
|
+
metadata: {
|
|
24
|
+
scope: persistence.capture,
|
|
25
|
+
sourceBehavior: requireCaptureSource(persistence.captureSource),
|
|
26
|
+
...input.request.retentionExpiresAt === void 0 ? {} : { retentionExpiresAt: input.request.retentionExpiresAt },
|
|
27
|
+
restoreMount: requireRestoreMount(persistence.restoreMount)
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
const updated = await input.leaseStore.update(input.workspaceId, lease.epoch, {
|
|
31
|
+
checkpoint,
|
|
32
|
+
restore: null,
|
|
33
|
+
...persistence.captureSource === "stopped" || persistence.captureSource === "deleted" ? { status: "stopped" } : {}
|
|
34
|
+
});
|
|
35
|
+
if (!updated) throw new Error("workspace_checkpoint_epoch_fenced");
|
|
36
|
+
return { status: "ready", lease: updated, checkpoint };
|
|
37
|
+
} finally {
|
|
38
|
+
if (!capturedSource || persistence.captureSource === "preserved") {
|
|
39
|
+
await input.request.runtime.resume();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
async function restoreSandboxCheckpoint(input) {
|
|
44
|
+
const lease = await requireLease(input.leaseStore, input.workspaceId);
|
|
45
|
+
const checkpoint = lease.checkpoint;
|
|
46
|
+
if (!checkpoint || input.request.checkpointId && checkpoint.id !== input.request.checkpointId) {
|
|
47
|
+
throw new Error("workspace_checkpoint_not_found");
|
|
48
|
+
}
|
|
49
|
+
if (lease.restore?.state === "ready" && lease.restore.checkpointId === checkpoint.id) {
|
|
50
|
+
return { status: "ready", lease, checkpoint };
|
|
51
|
+
}
|
|
52
|
+
const requestedAt = lease.restore?.checkpointId === checkpoint.id ? lease.restore.requestedAt : (input.now ?? Date.now)();
|
|
53
|
+
const restoring = lease.restore?.checkpointId === checkpoint.id && lease.restore.state !== "pending" ? lease.restore : {
|
|
54
|
+
checkpointId: checkpoint.id,
|
|
55
|
+
sourceEpoch: checkpoint.sourceEpoch,
|
|
56
|
+
state: "restoring",
|
|
57
|
+
requestedAt,
|
|
58
|
+
startedAt: (input.now ?? Date.now)()
|
|
59
|
+
};
|
|
60
|
+
const staged = lease.status === "ready" && lease.epoch === checkpoint.sourceEpoch ? await input.leaseStore.update(input.workspaceId, lease.epoch, { status: "stopped", restore: restoring }) : await input.leaseStore.update(input.workspaceId, lease.epoch, { restore: restoring });
|
|
61
|
+
if (!staged) throw new Error("workspace_restore_epoch_fenced");
|
|
62
|
+
const result = await input.ensure(input.workspaceId, {
|
|
63
|
+
homeRegion: staged.homeRegion,
|
|
64
|
+
...input.request.ensure,
|
|
65
|
+
bootSource: checkpoint.metadata.restoreMount === "same-resource" ? { kind: "default" } : { kind: "driver-snapshot", snapshotId: checkpoint.providerReference }
|
|
66
|
+
});
|
|
67
|
+
const current = await requireLease(input.leaseStore, input.workspaceId);
|
|
68
|
+
if (result.status === "provisioning") {
|
|
69
|
+
return { status: "provisioning", lease: current, checkpoint, retryAfterMs: result.retryAfterMs };
|
|
70
|
+
}
|
|
71
|
+
if (result.status !== "ready") {
|
|
72
|
+
const failedAt = (input.now ?? Date.now)();
|
|
73
|
+
await input.leaseStore.update(input.workspaceId, current.epoch, {
|
|
74
|
+
restore: {
|
|
75
|
+
checkpointId: checkpoint.id,
|
|
76
|
+
sourceEpoch: checkpoint.sourceEpoch,
|
|
77
|
+
state: "failed",
|
|
78
|
+
requestedAt,
|
|
79
|
+
startedAt: "startedAt" in restoring ? restoring.startedAt : void 0,
|
|
80
|
+
failedAt,
|
|
81
|
+
error: result.error ?? "workspace_restore_unavailable"
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
throw new Error(result.error ?? "workspace_restore_unavailable");
|
|
85
|
+
}
|
|
86
|
+
await input.request.runtime.reconcile({ epoch: result.epoch, checkpointId: checkpoint.id });
|
|
87
|
+
const ready = await input.leaseStore.update(input.workspaceId, result.epoch, {
|
|
88
|
+
restore: {
|
|
89
|
+
checkpointId: checkpoint.id,
|
|
90
|
+
sourceEpoch: checkpoint.sourceEpoch,
|
|
91
|
+
state: "ready",
|
|
92
|
+
requestedAt,
|
|
93
|
+
startedAt: "startedAt" in restoring && restoring.startedAt !== void 0 ? restoring.startedAt : (input.now ?? Date.now)(),
|
|
94
|
+
completedAt: (input.now ?? Date.now)()
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
if (!ready) throw new Error("workspace_restore_epoch_fenced");
|
|
98
|
+
return { status: "ready", lease: ready, checkpoint };
|
|
99
|
+
}
|
|
100
|
+
async function requireLease(store, workspaceId) {
|
|
101
|
+
const lease = await store.get(workspaceId);
|
|
102
|
+
if (!lease) throw new Error("workspace_checkpoint_lease_missing");
|
|
103
|
+
return lease;
|
|
104
|
+
}
|
|
105
|
+
function requireCaptureSource(value) {
|
|
106
|
+
if (value === "preserved" || value === "stopped" || value === "deleted") return value;
|
|
107
|
+
throw new Error("workspace_checkpoint_capabilities_invalid");
|
|
108
|
+
}
|
|
109
|
+
function requireRestoreMount(value) {
|
|
110
|
+
if (value === "same-resource" || value === "copy-on-write" || value === "new-resource") return value;
|
|
111
|
+
throw new Error("workspace_checkpoint_capabilities_invalid");
|
|
112
|
+
}
|
|
113
|
+
export {
|
|
114
|
+
captureSandboxCheckpoint,
|
|
115
|
+
restoreSandboxCheckpoint
|
|
116
|
+
};
|
|
@@ -3,3 +3,14 @@
|
|
|
3
3
|
* Daytona currently accepts at most 10 entries.
|
|
4
4
|
*/
|
|
5
5
|
export declare function formatDaytonaAllowList(cidrs: string[]): string;
|
|
6
|
+
/**
|
|
7
|
+
* Format hostnames for Daytona SDK `domainAllowList` parameter.
|
|
8
|
+
*
|
|
9
|
+
* Deliberately does NOT truncate, unlike the CIDR formatter above. Silently
|
|
10
|
+
* dropping entries from an egress allowlist is the worst of both worlds: the
|
|
11
|
+
* sandbox stays contained, but it loses reachability the caller believed it
|
|
12
|
+
* granted and nothing anywhere says so. Daytona documents no cap on
|
|
13
|
+
* `domainAllowList`; if one exists, an over-long list fails the create call
|
|
14
|
+
* loudly, which is the outcome we want.
|
|
15
|
+
*/
|
|
16
|
+
export declare function formatDaytonaDomainAllowList(hosts: string[]): string;
|
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
function formatDaytonaAllowList(cidrs) {
|
|
3
3
|
return cidrs.slice(0, 10).join(",");
|
|
4
4
|
}
|
|
5
|
+
function formatDaytonaDomainAllowList(hosts) {
|
|
6
|
+
return [...new Set(hosts)].join(",");
|
|
7
|
+
}
|
|
5
8
|
export {
|
|
6
|
-
formatDaytonaAllowList
|
|
9
|
+
formatDaytonaAllowList,
|
|
10
|
+
formatDaytonaDomainAllowList
|
|
7
11
|
};
|
package/dist/driver-catalog.d.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
import type
|
|
2
|
-
export declare const sandboxDriverIds: readonly ["daytona", "modal", "vercel", "cloudflare", "box", "docker"];
|
|
1
|
+
import { validateSandboxPersistenceCapabilities, type SandboxDriverMetadata } from "./index";
|
|
2
|
+
export declare const sandboxDriverIds: readonly ["exe", "daytona", "modal", "vercel", "cloudflare", "box", "docker"];
|
|
3
3
|
export type SandboxDriverID = (typeof sandboxDriverIds)[number];
|
|
4
4
|
export type SandboxDriverAuth = {
|
|
5
|
+
exe?: {
|
|
6
|
+
api_token?: string;
|
|
7
|
+
};
|
|
5
8
|
daytona?: {
|
|
6
9
|
api_key?: string;
|
|
7
10
|
};
|
|
@@ -39,17 +42,18 @@ export type SandboxDriverCatalogEntry = {
|
|
|
39
42
|
secret?: boolean;
|
|
40
43
|
}>;
|
|
41
44
|
};
|
|
45
|
+
export { validateSandboxPersistenceCapabilities };
|
|
42
46
|
export declare const sandboxDriverCatalog: Record<SandboxDriverID, SandboxDriverCatalogEntry>;
|
|
43
47
|
export declare function isSandboxDriverID(input: string | undefined): input is SandboxDriverID;
|
|
44
|
-
export declare function sandboxDriverId(input: string | undefined, cfg?: SandboxDriverConfig, env?: SandboxDriverEnv): "box" | "cloudflare" | "daytona" | "docker" | "modal" | "vercel" | undefined;
|
|
48
|
+
export declare function sandboxDriverId(input: string | undefined, cfg?: SandboxDriverConfig, env?: SandboxDriverEnv): "box" | "cloudflare" | "daytona" | "docker" | "exe" | "modal" | "vercel" | undefined;
|
|
45
49
|
export declare function defaultSandboxDriverID(cfg?: SandboxDriverConfig, env?: SandboxDriverEnv): SandboxDriverID;
|
|
46
50
|
export declare function sandboxDriverAuth<T extends SandboxDriverID>(cfg: SandboxDriverConfig | undefined, id: T, env?: SandboxDriverEnv): SandboxDriverAuth[T] | undefined;
|
|
47
51
|
export declare function hasSandboxDriverAuth(cfg: SandboxDriverConfig | undefined, id: SandboxDriverID, env?: SandboxDriverEnv): boolean;
|
|
48
52
|
export declare function dockerSandboxDriverEnabled(env?: SandboxDriverEnv): boolean;
|
|
49
53
|
export declare function listSandboxDrivers(cfg?: SandboxDriverConfig, env?: SandboxDriverEnv, managedDriverIds?: ReadonlySet<string>): {
|
|
50
|
-
default_driver: "box" | "cloudflare" | "daytona" | "docker" | "modal" | "vercel";
|
|
54
|
+
default_driver: "box" | "cloudflare" | "daytona" | "docker" | "exe" | "modal" | "vercel";
|
|
51
55
|
drivers: {
|
|
52
|
-
id: "box" | "cloudflare" | "daytona" | "docker" | "modal" | "vercel";
|
|
56
|
+
id: "box" | "cloudflare" | "daytona" | "docker" | "exe" | "modal" | "vercel";
|
|
53
57
|
label: string;
|
|
54
58
|
fields: {
|
|
55
59
|
key: string;
|
|
@@ -62,4 +66,3 @@ export declare function listSandboxDrivers(cfg?: SandboxDriverConfig, env?: Sand
|
|
|
62
66
|
}[];
|
|
63
67
|
};
|
|
64
68
|
type SandboxDriverEnv = Record<string, string | undefined>;
|
|
65
|
-
export {};
|
package/dist/driver-catalog.mjs
CHANGED
|
@@ -1,15 +1,53 @@
|
|
|
1
|
+
// src/hosted-network-policy.ts
|
|
2
|
+
var SANDBOX_MODEL_PROVIDER_HOSTS = [
|
|
3
|
+
"api.anthropic.com",
|
|
4
|
+
"api.openai.com",
|
|
5
|
+
"generativelanguage.googleapis.com",
|
|
6
|
+
"openrouter.ai",
|
|
7
|
+
// Model catalog + engine config, fetched on runner startup.
|
|
8
|
+
"models.dev",
|
|
9
|
+
"opencode.ai"
|
|
10
|
+
];
|
|
11
|
+
var SANDBOX_PACKAGE_REGISTRY_HOSTS = [
|
|
12
|
+
"registry.npmjs.org",
|
|
13
|
+
"nodejs.org",
|
|
14
|
+
"pypi.org",
|
|
15
|
+
"files.pythonhosted.org",
|
|
16
|
+
"crates.io",
|
|
17
|
+
"static.crates.io",
|
|
18
|
+
"index.crates.io",
|
|
19
|
+
"proxy.golang.org",
|
|
20
|
+
"sum.golang.org"
|
|
21
|
+
];
|
|
22
|
+
var SANDBOX_BASELINE_EGRESS_HOSTS = [
|
|
23
|
+
...SANDBOX_MODEL_PROVIDER_HOSTS,
|
|
24
|
+
...SANDBOX_PACKAGE_REGISTRY_HOSTS
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
// src/index.ts
|
|
28
|
+
function validateSandboxPersistenceCapabilities(input) {
|
|
29
|
+
if (input.resume === "replacement-restore" && input.capture === "none") {
|
|
30
|
+
return { valid: false, reason: "replacement restore requires a capture source" };
|
|
31
|
+
}
|
|
32
|
+
if (input.capture === "none" && input.captureSource !== "not-applicable") {
|
|
33
|
+
return { valid: false, reason: "capture source behavior requires capture support" };
|
|
34
|
+
}
|
|
35
|
+
if (input.capture !== "none" && input.captureSource === "not-applicable") {
|
|
36
|
+
return { valid: false, reason: "capture support requires source behavior" };
|
|
37
|
+
}
|
|
38
|
+
if (input.capture === "same-resource" && input.restoreMount !== "same-resource") {
|
|
39
|
+
return { valid: false, reason: "same-resource capture requires same-resource restore" };
|
|
40
|
+
}
|
|
41
|
+
if (input.restoreMount === "copy-on-write" && input.capture !== "directories") {
|
|
42
|
+
return { valid: false, reason: "copy-on-write restore requires directory capture" };
|
|
43
|
+
}
|
|
44
|
+
return { valid: true };
|
|
45
|
+
}
|
|
46
|
+
|
|
1
47
|
// src/runtime-version.ts
|
|
2
48
|
var DEFAULT_WORKSPACE_RUNTIME_VERSION = "0.5.2";
|
|
3
|
-
var versionCache;
|
|
4
49
|
function workspaceRuntimeVersion() {
|
|
5
|
-
|
|
6
|
-
const envVersion = process.env.WORKSPACE_RUNTIME_VERSION;
|
|
7
|
-
if (envVersion) {
|
|
8
|
-
versionCache = envVersion;
|
|
9
|
-
return versionCache;
|
|
10
|
-
}
|
|
11
|
-
versionCache = DEFAULT_WORKSPACE_RUNTIME_VERSION;
|
|
12
|
-
return versionCache;
|
|
50
|
+
return process.env.WORKSPACE_RUNTIME_VERSION || DEFAULT_WORKSPACE_RUNTIME_VERSION;
|
|
13
51
|
}
|
|
14
52
|
|
|
15
53
|
// src/image-name.ts
|
|
@@ -31,8 +69,29 @@ function defaultSandboxImage(version = workspaceRuntimeVersion(), buildId, env =
|
|
|
31
69
|
}
|
|
32
70
|
|
|
33
71
|
// src/driver-catalog.ts
|
|
34
|
-
var sandboxDriverIds = ["daytona", "modal", "vercel", "cloudflare", "box", "docker"];
|
|
72
|
+
var sandboxDriverIds = ["exe", "daytona", "modal", "vercel", "cloudflare", "box", "docker"];
|
|
35
73
|
var sandboxDriverCatalog = {
|
|
74
|
+
exe: {
|
|
75
|
+
id: "exe",
|
|
76
|
+
label: "exe.dev",
|
|
77
|
+
credentialFields: [{ key: "api_token", label: "API Token", secret: true }],
|
|
78
|
+
metadata: {
|
|
79
|
+
driverRunsIn: ["worker", "node"],
|
|
80
|
+
hostStopBehavior: "not-supported",
|
|
81
|
+
hostResumeBehavior: "same-host",
|
|
82
|
+
targetAccess: "relay",
|
|
83
|
+
secretBrokering: "none",
|
|
84
|
+
egressControl: "none",
|
|
85
|
+
persistence: {
|
|
86
|
+
resume: "same-sandbox",
|
|
87
|
+
capture: "none",
|
|
88
|
+
clone: true,
|
|
89
|
+
captureSource: "not-applicable",
|
|
90
|
+
retention: "provider-managed",
|
|
91
|
+
restoreMount: "same-resource"
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
},
|
|
36
95
|
daytona: {
|
|
37
96
|
id: "daytona",
|
|
38
97
|
label: "Daytona",
|
|
@@ -42,7 +101,19 @@ var sandboxDriverCatalog = {
|
|
|
42
101
|
hostStopBehavior: "suspends-host",
|
|
43
102
|
hostResumeBehavior: "same-host",
|
|
44
103
|
targetAccess: "relay",
|
|
45
|
-
secretBrokering: "native"
|
|
104
|
+
secretBrokering: "native",
|
|
105
|
+
// Daytona is the only driver that can filter egress by NAME as well as
|
|
106
|
+
// by address: `domainAllowList` (names) alongside `networkAllowList`
|
|
107
|
+
// (CIDRs), with `networkBlockAll` as the deny-all floor.
|
|
108
|
+
egressControl: "hosts-and-cidrs",
|
|
109
|
+
persistence: {
|
|
110
|
+
resume: "same-sandbox",
|
|
111
|
+
capture: "filesystem",
|
|
112
|
+
clone: false,
|
|
113
|
+
captureSource: "preserved",
|
|
114
|
+
retention: "provider-managed",
|
|
115
|
+
restoreMount: "new-resource"
|
|
116
|
+
}
|
|
46
117
|
}
|
|
47
118
|
},
|
|
48
119
|
modal: {
|
|
@@ -57,7 +128,19 @@ var sandboxDriverCatalog = {
|
|
|
57
128
|
hostStopBehavior: "terminates-host",
|
|
58
129
|
hostResumeBehavior: "replacement-host",
|
|
59
130
|
targetAccess: "relay",
|
|
60
|
-
secretBrokering: "none"
|
|
131
|
+
secretBrokering: "none",
|
|
132
|
+
// Modal can cut the network entirely (`blockNetwork`) but cannot express
|
|
133
|
+
// an allowlist, and its driver throws on a host policy. A blackout is
|
|
134
|
+
// not containment for a workspace that has to clone and reach a model.
|
|
135
|
+
egressControl: "none",
|
|
136
|
+
persistence: {
|
|
137
|
+
resume: "replacement-restore",
|
|
138
|
+
capture: "filesystem",
|
|
139
|
+
clone: false,
|
|
140
|
+
captureSource: "preserved",
|
|
141
|
+
retention: "provider-managed",
|
|
142
|
+
restoreMount: "new-resource"
|
|
143
|
+
}
|
|
61
144
|
}
|
|
62
145
|
},
|
|
63
146
|
vercel: {
|
|
@@ -73,7 +156,18 @@ var sandboxDriverCatalog = {
|
|
|
73
156
|
hostStopBehavior: "terminates-host",
|
|
74
157
|
hostResumeBehavior: "replacement-host",
|
|
75
158
|
targetAccess: "relay",
|
|
76
|
-
secretBrokering: "native"
|
|
159
|
+
secretBrokering: "native",
|
|
160
|
+
// Vercel's sandbox firewall takes a hostname allow list (or "deny-all"),
|
|
161
|
+
// never CIDRs.
|
|
162
|
+
egressControl: "hosts",
|
|
163
|
+
persistence: {
|
|
164
|
+
resume: "replacement-restore",
|
|
165
|
+
capture: "filesystem",
|
|
166
|
+
clone: false,
|
|
167
|
+
captureSource: "stopped",
|
|
168
|
+
retention: "explicit",
|
|
169
|
+
restoreMount: "new-resource"
|
|
170
|
+
}
|
|
77
171
|
}
|
|
78
172
|
},
|
|
79
173
|
cloudflare: {
|
|
@@ -88,7 +182,22 @@ var sandboxDriverCatalog = {
|
|
|
88
182
|
hostStopBehavior: "not-supported",
|
|
89
183
|
hostResumeBehavior: "same-host",
|
|
90
184
|
targetAccess: "relay",
|
|
91
|
-
secretBrokering: "proxy"
|
|
185
|
+
secretBrokering: "proxy",
|
|
186
|
+
// The Cloudflare Sandbox Worker exposes no egress filter: its broker
|
|
187
|
+
// (drivers/cloudflare-egress.ts) is an OPT-IN proxy the sandbox chooses
|
|
188
|
+
// to route brokered-credential requests through, not a boundary. The
|
|
189
|
+
// driver therefore drops `net` entirely — which is exactly why this
|
|
190
|
+
// must be declared, so the manager refuses instead of provisioning a
|
|
191
|
+
// sandbox the caller believes is contained.
|
|
192
|
+
egressControl: "none",
|
|
193
|
+
persistence: {
|
|
194
|
+
resume: "replacement-restore",
|
|
195
|
+
capture: "directories",
|
|
196
|
+
clone: false,
|
|
197
|
+
captureSource: "preserved",
|
|
198
|
+
retention: "provider-managed",
|
|
199
|
+
restoreMount: "copy-on-write"
|
|
200
|
+
}
|
|
92
201
|
}
|
|
93
202
|
},
|
|
94
203
|
box: {
|
|
@@ -100,7 +209,16 @@ var sandboxDriverCatalog = {
|
|
|
100
209
|
hostStopBehavior: "suspends-host",
|
|
101
210
|
hostResumeBehavior: "same-host",
|
|
102
211
|
targetAccess: "relay",
|
|
103
|
-
secretBrokering: "none"
|
|
212
|
+
secretBrokering: "none",
|
|
213
|
+
egressControl: "none",
|
|
214
|
+
persistence: {
|
|
215
|
+
resume: "same-sandbox",
|
|
216
|
+
capture: "none",
|
|
217
|
+
clone: false,
|
|
218
|
+
captureSource: "not-applicable",
|
|
219
|
+
retention: "provider-managed",
|
|
220
|
+
restoreMount: "same-resource"
|
|
221
|
+
}
|
|
104
222
|
}
|
|
105
223
|
},
|
|
106
224
|
docker: {
|
|
@@ -112,7 +230,16 @@ var sandboxDriverCatalog = {
|
|
|
112
230
|
hostStopBehavior: "terminates-host",
|
|
113
231
|
hostResumeBehavior: "same-host",
|
|
114
232
|
targetAccess: "loopback",
|
|
115
|
-
secretBrokering: "none"
|
|
233
|
+
secretBrokering: "none",
|
|
234
|
+
egressControl: "none",
|
|
235
|
+
persistence: {
|
|
236
|
+
resume: "replacement-restore",
|
|
237
|
+
capture: "same-resource",
|
|
238
|
+
clone: false,
|
|
239
|
+
captureSource: "preserved",
|
|
240
|
+
retention: "provider-managed",
|
|
241
|
+
restoreMount: "same-resource"
|
|
242
|
+
}
|
|
116
243
|
}
|
|
117
244
|
}
|
|
118
245
|
};
|
|
@@ -129,6 +256,7 @@ function defaultSandboxDriverID(cfg, env = process.env) {
|
|
|
129
256
|
}
|
|
130
257
|
function sandboxDriverAuth(cfg, id, env = process.env) {
|
|
131
258
|
if (id === "daytona") return authDaytona(cfg);
|
|
259
|
+
if (id === "exe") return authExe(cfg, env);
|
|
132
260
|
if (id === "modal") return authModal(cfg, env);
|
|
133
261
|
if (id === "vercel") return authVercel(cfg, env);
|
|
134
262
|
if (id === "cloudflare") return authCloudflare(cfg, env);
|
|
@@ -176,6 +304,10 @@ function authDaytona(cfg) {
|
|
|
176
304
|
const api_key = clean(cfg?.auth?.daytona?.api_key);
|
|
177
305
|
return api_key ? { api_key } : void 0;
|
|
178
306
|
}
|
|
307
|
+
function authExe(cfg, env) {
|
|
308
|
+
const api_token = clean(cfg?.auth?.exe?.api_token) ?? clean(env.EXE_DEV_API_TOKEN);
|
|
309
|
+
return api_token ? { api_token } : void 0;
|
|
310
|
+
}
|
|
179
311
|
function authModal(cfg, env) {
|
|
180
312
|
const token_id = clean(cfg?.auth?.modal?.token_id) ?? clean(env.MODAL_TOKEN_ID);
|
|
181
313
|
const token_secret = clean(cfg?.auth?.modal?.token_secret) ?? clean(env.MODAL_TOKEN_SECRET);
|
|
@@ -211,5 +343,6 @@ export {
|
|
|
211
343
|
sandboxDriverAuth,
|
|
212
344
|
sandboxDriverCatalog,
|
|
213
345
|
sandboxDriverId,
|
|
214
|
-
sandboxDriverIds
|
|
346
|
+
sandboxDriverIds,
|
|
347
|
+
validateSandboxPersistenceCapabilities
|
|
215
348
|
};
|