@claxedo/sandbox-manager 0.7.0 → 0.10.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 +245 -42
- package/dist/checkpoint-manager.d.ts +29 -4
- package/dist/checkpoint-manager.mjs +133 -24
- package/dist/command.d.ts +9 -2
- package/dist/command.mjs +8 -8
- package/dist/contract.d.ts +687 -0
- package/dist/driver-billing.d.ts +29 -0
- package/dist/driver-catalog.d.ts +43 -58
- package/dist/driver-catalog.mjs +192 -229
- package/dist/drivers/boat-client.d.ts +95 -0
- package/dist/drivers/boat-named-snapshots.d.ts +31 -0
- package/dist/drivers/boat-template.d.ts +49 -0
- package/dist/drivers/boat-template.mjs +1196 -0
- package/dist/drivers/boat.d.ts +64 -0
- package/dist/drivers/boat.mjs +990 -0
- package/dist/drivers/cloudflare.d.ts +5 -8
- package/dist/drivers/cloudflare.mjs +364 -213
- package/dist/drivers/docker.d.ts +4 -3
- package/dist/drivers/docker.mjs +244 -165
- package/dist/drivers/local-brokering.d.ts +14 -0
- package/dist/drivers/local-brokering.mjs +1029 -0
- package/dist/drivers/modal.d.ts +4 -7
- package/dist/drivers/modal.mjs +220 -155
- package/dist/drivers/transient-error.d.ts +13 -0
- package/dist/drivers/vercel.d.ts +21 -23
- package/dist/drivers/vercel.mjs +279 -169
- package/dist/egress-policy.d.ts +134 -0
- package/dist/egress-warnings.d.ts +22 -0
- package/dist/garbage-collect.d.ts +14 -0
- package/dist/hosted-network-policy.d.ts +11 -4
- package/dist/hosted-network-policy.mjs +221 -4
- package/dist/image-name.d.ts +12 -2
- package/dist/image-name.mjs +43 -0
- package/dist/image.d.ts +1 -20
- package/dist/image.mjs +11 -66
- package/dist/index.d.ts +8 -533
- package/dist/index.mjs +1420 -315
- package/dist/json.d.ts +13 -0
- package/dist/lease-failure.d.ts +173 -0
- package/dist/lease-lifecycle.d.ts +65 -0
- package/dist/lease-retry-wait.d.ts +8 -0
- package/dist/lease-start.d.ts +21 -0
- package/dist/lease-types.d.ts +15 -5
- package/dist/manager.d.ts +2 -0
- package/dist/runtime-env.d.ts +15 -2
- package/dist/runtime-env.mjs +50 -1
- package/dist/runtime-snapshot.d.ts +49 -0
- package/dist/runtime-version.d.ts +9 -0
- package/dist/runtime-version.mjs +2 -0
- package/dist/service-url-exposure.d.ts +1 -3
- package/dist/service-url-exposure.mjs +1 -26
- package/dist/start-fresh.d.ts +17 -0
- package/dist/start-telemetry.d.ts +37 -0
- package/dist/stores/memory.d.ts +1 -1
- package/dist/stores/memory.mjs +65 -54
- package/package.json +67 -81
- package/dist/daytona-allow-list.d.ts +0 -16
- package/dist/daytona-allow-list.mjs +0 -11
- package/dist/drivers/box.d.ts +0 -52
- package/dist/drivers/box.mjs +0 -499
- package/dist/drivers/cloudflare-egress.d.ts +0 -40
- package/dist/drivers/cloudflare-egress.mjs +0 -113
- package/dist/drivers/daytona.d.ts +0 -127
- package/dist/drivers/daytona.mjs +0 -541
- package/dist/drivers/exe.d.ts +0 -26
- package/dist/drivers/exe.mjs +0 -469
- package/dist/drivers/fetch-bridge.d.ts +0 -10
- package/dist/drivers/fetch-bridge.mjs +0 -111
- package/dist/lease-policy.d.ts +0 -48
- package/dist/lease-policy.mjs +0 -280
package/dist/drivers/modal.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { SandboxDriver, SandboxDriverEnsureInput } from "
|
|
1
|
+
import type { SandboxDriver, SandboxDriverEnsureInput } from "../contract";
|
|
2
|
+
import { type WorkspaceRuntimeControlEnv } from "../runtime-env";
|
|
2
3
|
type ModalAppLike = unknown;
|
|
3
4
|
type ModalImageLike = {
|
|
4
5
|
imageId?: string;
|
|
@@ -48,12 +49,8 @@ export type ModalSandboxDriverOptions = {
|
|
|
48
49
|
runtimePort?: number;
|
|
49
50
|
runtimeCommand?: string;
|
|
50
51
|
workspaceDir?: string;
|
|
51
|
-
|
|
52
|
-
controlEnv?:
|
|
53
|
-
relayJwksUrl?: string;
|
|
54
|
-
relayVerifyPem?: string;
|
|
55
|
-
managementJwksUrl?: string;
|
|
56
|
-
};
|
|
52
|
+
nativeHarness?: string;
|
|
53
|
+
controlEnv?: WorkspaceRuntimeControlEnv;
|
|
57
54
|
env?: (input: SandboxDriverEnsureInput, host: {
|
|
58
55
|
id: string;
|
|
59
56
|
}) => Record<string, string> | Promise<Record<string, string>>;
|
package/dist/drivers/modal.mjs
CHANGED
|
@@ -1,7 +1,47 @@
|
|
|
1
|
-
// src/drivers/modal.ts
|
|
2
|
-
import { ModalClient, Probe } from "modal";
|
|
3
|
-
|
|
4
1
|
// src/runtime-env.ts
|
|
2
|
+
var WORKSPACE_RUNTIME_IDENTITY_ENV_KEYS = [
|
|
3
|
+
"WORKSPACE_RUNTIME_WORKSPACE_ID",
|
|
4
|
+
"WORKSPACE_RUNTIME_HOST_ID",
|
|
5
|
+
"WORKSPACE_RUNTIME_RELAY_WORKSPACE_IDS"
|
|
6
|
+
];
|
|
7
|
+
function workspaceRuntimeIdentityEnvConflicts(env) {
|
|
8
|
+
if (!env) return [];
|
|
9
|
+
return WORKSPACE_RUNTIME_IDENTITY_ENV_KEYS.filter((key) => env[key] !== void 0);
|
|
10
|
+
}
|
|
11
|
+
function assertWorkspaceRuntimeIdentityEnv(env) {
|
|
12
|
+
const conflicts = workspaceRuntimeIdentityEnvConflicts(env);
|
|
13
|
+
if (conflicts.length === 0) return;
|
|
14
|
+
throw new Error(
|
|
15
|
+
`sandbox env cannot set runtime identity ${conflicts.join(", ")}: the driver's placement owns the identity its target reports and the lease records`
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
var WORKSPACE_RUNTIME_PUBLIC_ENV_KEYS = /* @__PURE__ */ new Set([
|
|
19
|
+
...WORKSPACE_RUNTIME_IDENTITY_ENV_KEYS,
|
|
20
|
+
"WORKSPACE_RUNTIME_DIRECTORY",
|
|
21
|
+
"WORKSPACE_RUNTIME_PORT",
|
|
22
|
+
"WORKSPACE_RUNTIME_HOST",
|
|
23
|
+
"WORKSPACE_RUNTIME_SOURCE_KIND",
|
|
24
|
+
"WORKSPACE_RUNTIME_GIT_REPO_URL",
|
|
25
|
+
"WORKSPACE_RUNTIME_GIT_BRANCH",
|
|
26
|
+
"WORKSPACE_RUNTIME_NATIVE_HARNESS",
|
|
27
|
+
"WORKSPACE_RUNTIME_RELAY_JWKS_URL",
|
|
28
|
+
"WORKSPACE_RUNTIME_MANAGEMENT_JWKS_URL",
|
|
29
|
+
"WORKSPACE_RUNTIME_SESSION_AUTHORITY_URL"
|
|
30
|
+
]);
|
|
31
|
+
function workspaceRuntimeBootEnv(input) {
|
|
32
|
+
assertWorkspaceRuntimeIdentityEnv(input.env);
|
|
33
|
+
const env = {
|
|
34
|
+
...workspaceRuntimeTargetEnv(input),
|
|
35
|
+
...workspaceRuntimeSourceEnv(input),
|
|
36
|
+
...input.env
|
|
37
|
+
};
|
|
38
|
+
if (input.nativeHarness) env.WORKSPACE_RUNTIME_NATIVE_HARNESS = input.nativeHarness;
|
|
39
|
+
if (input.controlEnv?.relayJwksUrl) env.WORKSPACE_RUNTIME_RELAY_JWKS_URL = input.controlEnv.relayJwksUrl;
|
|
40
|
+
if (input.controlEnv?.relayVerifyPem) env.WORKSPACE_RUNTIME_RELAY_HOST_VERIFY_PEM = input.controlEnv.relayVerifyPem;
|
|
41
|
+
if (input.controlEnv?.managementJwksUrl) env.WORKSPACE_RUNTIME_MANAGEMENT_JWKS_URL = input.controlEnv.managementJwksUrl;
|
|
42
|
+
if (input.controlEnv?.sessionAuthorityUrl) env.WORKSPACE_RUNTIME_SESSION_AUTHORITY_URL = input.controlEnv.sessionAuthorityUrl;
|
|
43
|
+
return env;
|
|
44
|
+
}
|
|
5
45
|
function workspaceRuntimeTargetEnv(input) {
|
|
6
46
|
return {
|
|
7
47
|
WORKSPACE_RUNTIME_WORKSPACE_ID: input.workspaceId,
|
|
@@ -28,9 +68,6 @@ function shell(input) {
|
|
|
28
68
|
// src/constants.ts
|
|
29
69
|
var DEFAULT_WORKSPACE_RUNTIME_PORT = 2593;
|
|
30
70
|
|
|
31
|
-
// src/image.ts
|
|
32
|
-
import { Image, DaytonaNotFoundError } from "@daytona/sdk";
|
|
33
|
-
|
|
34
71
|
// src/runtime-version.ts
|
|
35
72
|
var DEFAULT_WORKSPACE_RUNTIME_VERSION = "0.5.2";
|
|
36
73
|
function workspaceRuntimeVersion() {
|
|
@@ -51,107 +88,87 @@ function buildIdSuffix(buildId, env) {
|
|
|
51
88
|
const id = (buildId ?? env.CLAXEDO_SANDBOX_BUILD_ID)?.trim();
|
|
52
89
|
return id ? `-${snapshotVersion(id)}` : "";
|
|
53
90
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
return `claxedo-workspace-runtime-${snapshotVersion(version)}${buildIdSuffix(buildId, env)}-v${SNAPSHOT_SCHEMA_VERSION}`;
|
|
91
|
+
var AGENT_PLUGINS_TAG = "-agent-plugins";
|
|
92
|
+
function defaultSandboxImage(version = workspaceRuntimeVersion(), buildId, env = process.env, options = {}) {
|
|
93
|
+
const feature = options.agentPlugins ? AGENT_PLUGINS_TAG : "";
|
|
94
|
+
return `${sandboxImageRepository(env)}:workspace-runtime-${snapshotVersion(version)}${buildIdSuffix(buildId, env)}${feature}-v${SNAPSHOT_SCHEMA_VERSION}`;
|
|
59
95
|
}
|
|
60
96
|
|
|
61
97
|
// src/image.ts
|
|
62
98
|
var SANDBOX_IMAGE_REPOSITORY = sandboxImageRepository();
|
|
63
99
|
var SANDBOX_IMAGE = process.env.CLAXEDO_SANDBOX_IMAGE || defaultSandboxImage();
|
|
64
|
-
var SNAPSHOT_NAME = process.env.CLAXEDO_SNAPSHOT_NAME || defaultSnapshotName();
|
|
65
100
|
|
|
66
|
-
// src/
|
|
67
|
-
var
|
|
68
|
-
|
|
69
|
-
"
|
|
70
|
-
"
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
"
|
|
83
|
-
"
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
101
|
+
// src/driver-billing.ts
|
|
102
|
+
var HOUR_SECONDS = 3600;
|
|
103
|
+
var boatBilling = {
|
|
104
|
+
source: { url: "https://docs.boat.dev/pricing", readOn: "2026-10-12" },
|
|
105
|
+
meter: "Boat bills machine time per second while a sandbox runs, at an hourly rate per size; a stopped sandbox costs nothing.",
|
|
106
|
+
rates: {
|
|
107
|
+
small: { size: "small", vcpu: 2, memoryGb: 4, usdPerHour: 0.018 },
|
|
108
|
+
default: { size: "default", vcpu: 4, memoryGb: 8, usdPerHour: 0.036 },
|
|
109
|
+
large: { size: "large", vcpu: 8, memoryGb: 16, usdPerHour: 0.072 }
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
var CLOUDFLARE_USD_PER_GIB_SECOND_MEMORY = 25e-7;
|
|
113
|
+
var CLOUDFLARE_USD_PER_GB_SECOND_DISK = 7e-8;
|
|
114
|
+
var CLOUDFLARE_USD_PER_VCPU_SECOND_ACTIVE = 2e-5;
|
|
115
|
+
var CLOUDFLARE_STANDARD_1 = { size: "standard-1", vcpu: 0.5, memoryGb: 4, diskGb: 8 };
|
|
116
|
+
var cloudflareBilling = {
|
|
117
|
+
source: { url: "https://developers.cloudflare.com/containers/pricing/", readOn: "2026-10-12" },
|
|
118
|
+
meter: "Cloudflare bills a container for every 10 ms it runs: memory at $0.0000025 per GiB-second and disk at $0.00000007 per GB-second for the whole run, plus CPU at $0.00002 per vCPU-second of active use.",
|
|
119
|
+
rates: {
|
|
120
|
+
default: {
|
|
121
|
+
size: CLOUDFLARE_STANDARD_1.size,
|
|
122
|
+
vcpu: CLOUDFLARE_STANDARD_1.vcpu,
|
|
123
|
+
memoryGb: CLOUDFLARE_STANDARD_1.memoryGb,
|
|
124
|
+
usdPerHour: (CLOUDFLARE_STANDARD_1.memoryGb * CLOUDFLARE_USD_PER_GIB_SECOND_MEMORY + CLOUDFLARE_STANDARD_1.diskGb * CLOUDFLARE_USD_PER_GB_SECOND_DISK) * HOUR_SECONDS
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
excludes: `active CPU, billed at $${CLOUDFLARE_USD_PER_VCPU_SECOND_ACTIVE} per vCPU-second and not measured by Claxedo, and the monthly allowance a Workers Paid plan includes`
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
// ../sandbox-contract/dist/index.mjs
|
|
131
|
+
var sandboxDriverCredentialFields = {
|
|
132
|
+
modal: [
|
|
133
|
+
{ key: "token_id", label: "Token ID", secret: true },
|
|
134
|
+
{ key: "token_secret", label: "Token Secret", secret: true }
|
|
135
|
+
],
|
|
136
|
+
vercel: [
|
|
137
|
+
{ key: "access_token", label: "Access Token", secret: true },
|
|
138
|
+
{ key: "team_id", label: "Team ID" },
|
|
139
|
+
{ key: "project_id", label: "Project ID" }
|
|
140
|
+
],
|
|
141
|
+
cloudflare: [
|
|
142
|
+
{ key: "api_token", label: "API Token", secret: true },
|
|
143
|
+
{ key: "worker_url", label: "Worker URL" }
|
|
144
|
+
],
|
|
145
|
+
boat: [{ key: "api_key", label: "API Key", secret: true }],
|
|
146
|
+
docker: [{ key: "image", label: "Image" }]
|
|
147
|
+
};
|
|
148
|
+
var sandboxDriverLabels = {
|
|
149
|
+
modal: "Modal",
|
|
150
|
+
vercel: "Vercel",
|
|
151
|
+
cloudflare: "Cloudflare",
|
|
152
|
+
boat: "Boat",
|
|
153
|
+
docker: "Docker"
|
|
154
|
+
};
|
|
91
155
|
|
|
92
156
|
// src/driver-catalog.ts
|
|
93
157
|
var sandboxDriverCatalog = {
|
|
94
|
-
exe: {
|
|
95
|
-
id: "exe",
|
|
96
|
-
label: "exe.dev",
|
|
97
|
-
credentialFields: [{ key: "api_token", label: "API Token", secret: true }],
|
|
98
|
-
metadata: {
|
|
99
|
-
driverRunsIn: ["worker", "node"],
|
|
100
|
-
hostStopBehavior: "not-supported",
|
|
101
|
-
hostResumeBehavior: "same-host",
|
|
102
|
-
targetAccess: "relay",
|
|
103
|
-
secretBrokering: "none",
|
|
104
|
-
egressControl: "none",
|
|
105
|
-
persistence: {
|
|
106
|
-
resume: "same-sandbox",
|
|
107
|
-
capture: "none",
|
|
108
|
-
clone: true,
|
|
109
|
-
captureSource: "not-applicable",
|
|
110
|
-
retention: "provider-managed",
|
|
111
|
-
restoreMount: "same-resource"
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
},
|
|
115
|
-
daytona: {
|
|
116
|
-
id: "daytona",
|
|
117
|
-
label: "Daytona",
|
|
118
|
-
credentialFields: [{ key: "api_key", label: "API Key", secret: true }],
|
|
119
|
-
metadata: {
|
|
120
|
-
driverRunsIn: ["worker", "node"],
|
|
121
|
-
hostStopBehavior: "suspends-host",
|
|
122
|
-
hostResumeBehavior: "same-host",
|
|
123
|
-
targetAccess: "relay",
|
|
124
|
-
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.
|
|
128
|
-
egressControl: "hosts-and-cidrs",
|
|
129
|
-
persistence: {
|
|
130
|
-
resume: "same-sandbox",
|
|
131
|
-
capture: "filesystem",
|
|
132
|
-
clone: false,
|
|
133
|
-
captureSource: "preserved",
|
|
134
|
-
retention: "provider-managed",
|
|
135
|
-
restoreMount: "new-resource"
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
},
|
|
139
158
|
modal: {
|
|
140
159
|
id: "modal",
|
|
141
|
-
label:
|
|
142
|
-
credentialFields:
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
160
|
+
label: sandboxDriverLabels.modal,
|
|
161
|
+
credentialFields: sandboxDriverCredentialFields.modal,
|
|
162
|
+
runtimeNetwork: { controlPlane: "direct", relay: "configured" },
|
|
163
|
+
runtimeEnv: "callback",
|
|
164
|
+
preparedImage: true,
|
|
146
165
|
metadata: {
|
|
147
166
|
driverRunsIn: ["node"],
|
|
148
167
|
hostStopBehavior: "terminates-host",
|
|
149
168
|
hostResumeBehavior: "replacement-host",
|
|
150
169
|
targetAccess: "relay",
|
|
151
170
|
secretBrokering: "none",
|
|
152
|
-
//
|
|
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.
|
|
171
|
+
// The driver does not install Modal's domain allowlist or alpha sidecar.
|
|
155
172
|
egressControl: "none",
|
|
156
173
|
persistence: {
|
|
157
174
|
resume: "replacement-restore",
|
|
@@ -165,20 +182,18 @@ var sandboxDriverCatalog = {
|
|
|
165
182
|
},
|
|
166
183
|
vercel: {
|
|
167
184
|
id: "vercel",
|
|
168
|
-
label:
|
|
169
|
-
credentialFields:
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
],
|
|
185
|
+
label: sandboxDriverLabels.vercel,
|
|
186
|
+
credentialFields: sandboxDriverCredentialFields.vercel,
|
|
187
|
+
runtimeNetwork: { controlPlane: "direct", relay: "configured" },
|
|
188
|
+
runtimeEnv: "callback",
|
|
189
|
+
preparedImage: false,
|
|
174
190
|
metadata: {
|
|
175
191
|
driverRunsIn: ["node"],
|
|
176
192
|
hostStopBehavior: "terminates-host",
|
|
177
193
|
hostResumeBehavior: "replacement-host",
|
|
178
194
|
targetAccess: "relay",
|
|
179
195
|
secretBrokering: "native",
|
|
180
|
-
//
|
|
181
|
-
// never CIDRs.
|
|
196
|
+
// The driver translates hosts only; the SDK's subnet rules are not wired to net.cidrs.
|
|
182
197
|
egressControl: "hosts",
|
|
183
198
|
persistence: {
|
|
184
199
|
resume: "replacement-restore",
|
|
@@ -192,40 +207,42 @@ var sandboxDriverCatalog = {
|
|
|
192
207
|
},
|
|
193
208
|
cloudflare: {
|
|
194
209
|
id: "cloudflare",
|
|
195
|
-
label:
|
|
196
|
-
credentialFields:
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
210
|
+
label: sandboxDriverLabels.cloudflare,
|
|
211
|
+
credentialFields: sandboxDriverCredentialFields.cloudflare,
|
|
212
|
+
runtimeNetwork: { controlPlane: "direct", relay: "configured" },
|
|
213
|
+
runtimeEnv: "callback",
|
|
214
|
+
preparedImage: false,
|
|
215
|
+
billing: cloudflareBilling,
|
|
200
216
|
metadata: {
|
|
201
217
|
driverRunsIn: ["worker"],
|
|
202
|
-
hostStopBehavior: "
|
|
218
|
+
hostStopBehavior: "terminates-host",
|
|
203
219
|
hostResumeBehavior: "same-host",
|
|
220
|
+
hostWatchesIdle: true,
|
|
204
221
|
targetAccess: "relay",
|
|
205
|
-
secretBrokering: "
|
|
206
|
-
//
|
|
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.
|
|
222
|
+
secretBrokering: "native",
|
|
223
|
+
// Credential handlers do not restrict unrelated destinations.
|
|
212
224
|
egressControl: "none",
|
|
225
|
+
// A container snapshot holds the whole root filesystem; the platform keeps it 30 days past its last restore and offers no deletion.
|
|
213
226
|
persistence: {
|
|
214
227
|
resume: "replacement-restore",
|
|
215
|
-
capture: "
|
|
228
|
+
capture: "filesystem",
|
|
216
229
|
clone: false,
|
|
217
230
|
captureSource: "preserved",
|
|
218
231
|
retention: "provider-managed",
|
|
219
|
-
restoreMount: "
|
|
232
|
+
restoreMount: "new-resource"
|
|
220
233
|
}
|
|
221
234
|
}
|
|
222
235
|
},
|
|
223
|
-
|
|
224
|
-
id: "
|
|
225
|
-
label:
|
|
226
|
-
credentialFields:
|
|
236
|
+
boat: {
|
|
237
|
+
id: "boat",
|
|
238
|
+
label: sandboxDriverLabels.boat,
|
|
239
|
+
credentialFields: sandboxDriverCredentialFields.boat,
|
|
240
|
+
runtimeNetwork: { controlPlane: "direct", relay: "configured" },
|
|
241
|
+
runtimeEnv: "callback",
|
|
242
|
+
preparedImage: true,
|
|
243
|
+
billing: boatBilling,
|
|
227
244
|
metadata: {
|
|
228
|
-
driverRunsIn: ["node"],
|
|
245
|
+
driverRunsIn: ["worker", "node"],
|
|
229
246
|
hostStopBehavior: "suspends-host",
|
|
230
247
|
hostResumeBehavior: "same-host",
|
|
231
248
|
targetAccess: "relay",
|
|
@@ -238,13 +255,17 @@ var sandboxDriverCatalog = {
|
|
|
238
255
|
captureSource: "not-applicable",
|
|
239
256
|
retention: "provider-managed",
|
|
240
257
|
restoreMount: "same-resource"
|
|
241
|
-
}
|
|
258
|
+
},
|
|
259
|
+
machineClasses: ["small", "default", "large"]
|
|
242
260
|
}
|
|
243
261
|
},
|
|
244
262
|
docker: {
|
|
245
263
|
id: "docker",
|
|
246
|
-
label:
|
|
247
|
-
credentialFields:
|
|
264
|
+
label: sandboxDriverLabels.docker,
|
|
265
|
+
credentialFields: sandboxDriverCredentialFields.docker,
|
|
266
|
+
runtimeNetwork: { controlPlane: "docker-host", relay: "configured" },
|
|
267
|
+
runtimeEnv: "callback",
|
|
268
|
+
preparedImage: true,
|
|
248
269
|
metadata: {
|
|
249
270
|
driverRunsIn: ["local"],
|
|
250
271
|
hostStopBehavior: "terminates-host",
|
|
@@ -264,6 +285,40 @@ var sandboxDriverCatalog = {
|
|
|
264
285
|
}
|
|
265
286
|
};
|
|
266
287
|
|
|
288
|
+
// ../claxedo-helpers/dist/guards.mjs
|
|
289
|
+
function isRecord(value) {
|
|
290
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
291
|
+
}
|
|
292
|
+
function asRecord(value) {
|
|
293
|
+
return isRecord(value) ? value : void 0;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// src/json.ts
|
|
297
|
+
function num(input) {
|
|
298
|
+
return typeof input === "number" ? input : void 0;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// src/drivers/transient-error.ts
|
|
302
|
+
function textOf(input) {
|
|
303
|
+
return typeof input === "string" ? input : "";
|
|
304
|
+
}
|
|
305
|
+
function driverErrorSignals(err) {
|
|
306
|
+
const shaped = isRecord(err) ? err : {};
|
|
307
|
+
const nested = isRecord(shaped.response) ? shaped.response : {};
|
|
308
|
+
return {
|
|
309
|
+
// `response.status` is the axios shape; `statusCode` is what SDK-native
|
|
310
|
+
// error classes expose instead.
|
|
311
|
+
status: num(nested.status) ?? num(shaped.status) ?? num(shaped.statusCode),
|
|
312
|
+
text: `${textOf(shaped.code)} ${textOf(shaped.name)} ${textOf(shaped.message)}`.toLowerCase()
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
function isTransientDriverError(err, markers) {
|
|
316
|
+
const { status, text } = driverErrorSignals(err);
|
|
317
|
+
if (status !== void 0 && status >= 500) return true;
|
|
318
|
+
return markers.some((marker) => text.includes(marker));
|
|
319
|
+
}
|
|
320
|
+
var TRANSIENT_DRIVER_RETRY_MS = 2e3;
|
|
321
|
+
|
|
267
322
|
// src/drivers/modal.ts
|
|
268
323
|
var DEFAULT_APP_NAME = "claxedo";
|
|
269
324
|
var DEFAULT_RUNTIME_COMMAND = "/usr/local/bin/workspace-runtime";
|
|
@@ -271,21 +326,28 @@ var DEFAULT_WORKSPACE_DIR = "/workspace";
|
|
|
271
326
|
var DEFAULT_TIMEOUT_MS = 30 * 60 * 1e3;
|
|
272
327
|
var DEFAULT_TUNNEL_TIMEOUT_MS = 3e4;
|
|
273
328
|
var DEFAULT_OPERATION_TIMEOUT_MS = 6e4;
|
|
329
|
+
function isModalClient(value) {
|
|
330
|
+
const row = asRecord(value);
|
|
331
|
+
return typeof asRecord(row?.apps)?.fromName === "function" && typeof asRecord(row?.images)?.fromRegistry === "function" && typeof asRecord(row?.sandboxes)?.create === "function";
|
|
332
|
+
}
|
|
274
333
|
function nameFor(workspaceId) {
|
|
275
334
|
return `claxedo-${workspaceId}`;
|
|
276
335
|
}
|
|
336
|
+
var TRANSIENT_MARKERS = ["timeout", "unavailable", "deadline"];
|
|
277
337
|
function transientDriverError(err) {
|
|
278
|
-
|
|
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");
|
|
338
|
+
return isTransientDriverError(err, TRANSIENT_MARKERS);
|
|
283
339
|
}
|
|
284
340
|
function createModalSandboxDriver(options) {
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
341
|
+
let defaultClient;
|
|
342
|
+
function resolveClient() {
|
|
343
|
+
if (options.client) return options.client;
|
|
344
|
+
defaultClient ??= import("modal").then((sdk) => {
|
|
345
|
+
const client = new sdk.ModalClient({ tokenId: options.tokenId, tokenSecret: options.tokenSecret });
|
|
346
|
+
if (!isModalClient(client)) throw new Error("modal SDK does not expose the client surface this driver uses");
|
|
347
|
+
return client;
|
|
348
|
+
});
|
|
349
|
+
return defaultClient;
|
|
350
|
+
}
|
|
289
351
|
const appName = options.appName ?? DEFAULT_APP_NAME;
|
|
290
352
|
const runtimePort = options.runtimePort ?? DEFAULT_WORKSPACE_RUNTIME_PORT;
|
|
291
353
|
const runtimeCommand = options.runtimeCommand ?? DEFAULT_RUNTIME_COMMAND;
|
|
@@ -293,29 +355,28 @@ function createModalSandboxDriver(options) {
|
|
|
293
355
|
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
294
356
|
const tunnelTimeoutMs = options.tunnelTimeoutMs ?? DEFAULT_TUNNEL_TIMEOUT_MS;
|
|
295
357
|
const operationTimeoutMs = options.operationTimeoutMs ?? DEFAULT_OPERATION_TIMEOUT_MS;
|
|
296
|
-
|
|
358
|
+
async function readinessProbeFor(port) {
|
|
359
|
+
if (options.readinessProbe) return options.readinessProbe(port);
|
|
360
|
+
const { Probe } = await import("modal");
|
|
361
|
+
return Probe.withTcp(port, { intervalMs: 1e3 });
|
|
362
|
+
}
|
|
297
363
|
function workspaceDirectory(input) {
|
|
298
364
|
return input.workspaceRoot ?? workspaceDir;
|
|
299
365
|
}
|
|
300
366
|
function bootEnv(input, hostId) {
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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;
|
|
367
|
+
return workspaceRuntimeBootEnv({
|
|
368
|
+
workspaceId: input.workspaceId,
|
|
369
|
+
hostId,
|
|
370
|
+
directory: workspaceDirectory(input),
|
|
371
|
+
port: runtimePort,
|
|
372
|
+
host: "0.0.0.0",
|
|
373
|
+
source: input.source,
|
|
374
|
+
env: input.env,
|
|
375
|
+
nativeHarness: options.nativeHarness,
|
|
376
|
+
controlEnv: options.controlEnv
|
|
377
|
+
});
|
|
317
378
|
}
|
|
318
|
-
async function image(input) {
|
|
379
|
+
async function image(client, input) {
|
|
319
380
|
if (input.bootSource?.kind === "driver-snapshot") return client.images.fromId(input.bootSource.snapshotId);
|
|
320
381
|
if (input.bootSource?.kind === "image") return client.images.fromId(input.bootSource.image);
|
|
321
382
|
if (input.snapshot) return client.images.fromId(input.snapshot);
|
|
@@ -329,10 +390,13 @@ function createModalSandboxDriver(options) {
|
|
|
329
390
|
return { blockNetwork: true };
|
|
330
391
|
}
|
|
331
392
|
async function readyTarget(input, sandbox, hostId) {
|
|
332
|
-
await sandbox.setTags(input.labels)
|
|
333
|
-
const tunnels = await sandbox.tunnels(tunnelTimeoutMs).catch(() =>
|
|
393
|
+
await sandbox.setTags(input.labels);
|
|
394
|
+
const tunnels = await sandbox.tunnels(tunnelTimeoutMs).catch((error) => {
|
|
395
|
+
if (error instanceof Error && error.name === "SandboxTimeoutError") return void 0;
|
|
396
|
+
throw error;
|
|
397
|
+
});
|
|
334
398
|
const tunnel = tunnels?.[runtimePort];
|
|
335
|
-
if (!tunnel?.url) return { provisioning: true, retryAfterMs:
|
|
399
|
+
if (!tunnel?.url) return { provisioning: true, retryAfterMs: 0 };
|
|
336
400
|
return {
|
|
337
401
|
workspaceId: input.workspaceId,
|
|
338
402
|
sandboxId: sandbox.sandboxId,
|
|
@@ -353,9 +417,10 @@ function createModalSandboxDriver(options) {
|
|
|
353
417
|
...bootEnv(input, hostId),
|
|
354
418
|
...await options.env?.(input, { id: hostId })
|
|
355
419
|
};
|
|
420
|
+
const client = await resolveClient();
|
|
356
421
|
const sandbox = await client.sandboxes.create(
|
|
357
422
|
await client.apps.fromName(appName, { createIfMissing: true }),
|
|
358
|
-
await image(input),
|
|
423
|
+
await image(client, input),
|
|
359
424
|
{
|
|
360
425
|
name: nameFor(input.workspaceId),
|
|
361
426
|
command: ["bash", "-lc", `mkdir -p ${shell(directory)}; cd ${shell(directory)}; exec ${runtimeCommand}`],
|
|
@@ -365,18 +430,18 @@ function createModalSandboxDriver(options) {
|
|
|
365
430
|
timeoutMs,
|
|
366
431
|
...options.idleTimeoutMs !== void 0 ? { idleTimeoutMs: options.idleTimeoutMs } : {},
|
|
367
432
|
tags: { ...input.labels, "claxedo.workspaceId": input.workspaceId },
|
|
368
|
-
readinessProbe:
|
|
433
|
+
readinessProbe: await readinessProbeFor(runtimePort),
|
|
369
434
|
...network(input)
|
|
370
435
|
}
|
|
371
436
|
).catch((err) => {
|
|
372
437
|
if (transientDriverError(err)) return void 0;
|
|
373
438
|
throw err;
|
|
374
439
|
});
|
|
375
|
-
if (!sandbox) return { provisioning: true, retryAfterMs:
|
|
440
|
+
if (!sandbox) return { provisioning: true, retryAfterMs: TRANSIENT_DRIVER_RETRY_MS };
|
|
376
441
|
return readyTarget(input, sandbox, hostId);
|
|
377
442
|
}
|
|
378
443
|
async function sandboxById(target) {
|
|
379
|
-
return
|
|
444
|
+
return (await resolveClient()).sandboxes.fromId(target.sandboxId);
|
|
380
445
|
}
|
|
381
446
|
return {
|
|
382
447
|
id: "modal",
|
|
@@ -0,0 +1,13 @@
|
|
|
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;
|
|
12
|
+
/** How long a driver waits before asking a provider again after a failure `isTransientDriverError` admits, so a provider in trouble is not asked at once. */
|
|
13
|
+
export declare const TRANSIENT_DRIVER_RETRY_MS = 2000;
|
package/dist/drivers/vercel.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
1
|
+
import type { NetworkPolicy } from "@vercel/sandbox";
|
|
2
|
+
import { type SandboxDriver, type SandboxDriverEnsureInput } from "../contract";
|
|
3
|
+
import { type SandboxBrokeredSecret } from "../egress-policy";
|
|
4
|
+
import { type WorkspaceRuntimeControlEnv } from "../runtime-env";
|
|
3
5
|
type VercelSnapshotLike = {
|
|
4
6
|
snapshotId: string;
|
|
5
7
|
};
|
|
@@ -79,12 +81,8 @@ export type VercelSandboxDriverOptions = {
|
|
|
79
81
|
runtimePort?: number;
|
|
80
82
|
runtimeCommand?: string;
|
|
81
83
|
workspaceDir?: string;
|
|
82
|
-
|
|
83
|
-
controlEnv?:
|
|
84
|
-
relayJwksUrl?: string;
|
|
85
|
-
relayVerifyPem?: string;
|
|
86
|
-
managementJwksUrl?: string;
|
|
87
|
-
};
|
|
84
|
+
nativeHarness?: string;
|
|
85
|
+
controlEnv?: WorkspaceRuntimeControlEnv;
|
|
88
86
|
env?: (input: SandboxDriverEnsureInput, host: {
|
|
89
87
|
id: string;
|
|
90
88
|
}) => Record<string, string> | Promise<Record<string, string>>;
|
|
@@ -103,26 +101,26 @@ export type VercelSandboxDriverOptions = {
|
|
|
103
101
|
* and https://vercel.com/docs/sandbox/concepts/firewall — credentials
|
|
104
102
|
* brokering).
|
|
105
103
|
*
|
|
106
|
-
* The result is the
|
|
104
|
+
* The result is the union of `base` — the create-time allow-list — and the
|
|
107
105
|
* brokered hosts, never a replacement. Replacing it is wrong in both
|
|
108
|
-
* directions
|
|
109
|
-
* overwrites the whole policy, so a sandbox created with
|
|
110
|
-
* registry allowed loses both the instant a brokered secret is
|
|
111
|
-
* the agent's next `npm install` fails mid-run.
|
|
112
|
-
* egress
|
|
113
|
-
* reachable,
|
|
114
|
-
* attaching a credential. A union changes exactly one thing — it adds the
|
|
115
|
-
* brokered hosts,
|
|
106
|
+
* directions: it breaks egress the caller was granted, since
|
|
107
|
+
* `updateNetworkPolicy` overwrites the whole policy, so a sandbox created with
|
|
108
|
+
* git and the npm registry allowed loses both the instant a brokered secret is
|
|
109
|
+
* attached, and the agent's next `npm install` fails mid-run. It can also
|
|
110
|
+
* silently widen egress, since a brokered host that was not in the create-time
|
|
111
|
+
* list becomes reachable, installing a policy nobody approved as a side effect
|
|
112
|
+
* of attaching a credential. A union changes exactly one thing — it adds the
|
|
113
|
+
* brokered hosts, the smallest edit that makes the credential usable.
|
|
116
114
|
*
|
|
117
115
|
* `base` is the create-time `NetworkPolicy` exactly as passed to `create`, and
|
|
118
|
-
* it is
|
|
116
|
+
* it is required even though `undefined` is a legal value: `undefined` means
|
|
119
117
|
* "no policy was requested", so the merge keeps egress unrestricted, expressed
|
|
120
118
|
* as the `"*"` wildcard the record form needs for the header transforms to ride
|
|
121
|
-
* alongside it.
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
119
|
+
* alongside it. An optional parameter defaulting to
|
|
120
|
+
* deny-all-except-brokered-hosts would let an existing caller keep compiling
|
|
121
|
+
* while its egress silently widens. Spelling the base out forces that decision
|
|
122
|
+
* to be made once, visibly. A `"deny-all"` base contributes nothing, leaving
|
|
123
|
+
* deny-all-except-brokered-hosts.
|
|
126
124
|
*/
|
|
127
125
|
export declare function vercelBrokeredNetworkPolicy(secrets: SandboxBrokeredSecret[], base: NetworkPolicy | undefined): NetworkPolicy;
|
|
128
126
|
export declare function createVercelSandboxDriver(options: VercelSandboxDriverOptions): SandboxDriver;
|