@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/docker.d.ts
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
|
-
import type { SandboxDriver, SandboxDriverEnsureInput } from "
|
|
1
|
+
import type { SandboxDriver, SandboxDriverEnsureInput } from "../contract";
|
|
2
2
|
type SandboxEnv = Record<string, string | undefined>;
|
|
3
3
|
export type DockerCommandResult = {
|
|
4
4
|
stdout?: string | Buffer;
|
|
5
5
|
stderr?: string | Buffer;
|
|
6
6
|
};
|
|
7
|
-
|
|
7
|
+
/** `env` joins the docker CLI's own environment; a `--env NAME` argument with no value reads NAME from it. */
|
|
8
|
+
export type DockerCommandRunner = (args: string[], timeoutMs?: number, env?: Record<string, string>) => Promise<DockerCommandResult>;
|
|
8
9
|
export type DockerHealthFetch = (input: string, init?: RequestInit) => Promise<Response>;
|
|
9
10
|
export type DockerSandboxDriverOptions = {
|
|
10
11
|
image: string;
|
|
11
12
|
runtimePort?: number;
|
|
12
13
|
runtimeCommand?: string;
|
|
13
14
|
workspaceDir?: string;
|
|
14
|
-
|
|
15
|
+
nativeHarness?: string;
|
|
15
16
|
env?: (input: SandboxDriverEnsureInput, host: {
|
|
16
17
|
id: string;
|
|
17
18
|
}) => Record<string, string> | Promise<Record<string, string>>;
|
package/dist/drivers/docker.mjs
CHANGED
|
@@ -5,7 +5,61 @@ import os from "node:os";
|
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { promisify } from "node:util";
|
|
7
7
|
|
|
8
|
+
// ../claxedo-helpers/dist/string.mjs
|
|
9
|
+
function isString(value) {
|
|
10
|
+
return typeof value === "string";
|
|
11
|
+
}
|
|
12
|
+
function trimToUndefined(value) {
|
|
13
|
+
if (!isString(value))
|
|
14
|
+
return;
|
|
15
|
+
const trimmed = value.trim();
|
|
16
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
17
|
+
}
|
|
18
|
+
|
|
8
19
|
// src/runtime-env.ts
|
|
20
|
+
var WORKSPACE_RUNTIME_IDENTITY_ENV_KEYS = [
|
|
21
|
+
"WORKSPACE_RUNTIME_WORKSPACE_ID",
|
|
22
|
+
"WORKSPACE_RUNTIME_HOST_ID",
|
|
23
|
+
"WORKSPACE_RUNTIME_RELAY_WORKSPACE_IDS"
|
|
24
|
+
];
|
|
25
|
+
function workspaceRuntimeIdentityEnvConflicts(env) {
|
|
26
|
+
if (!env) return [];
|
|
27
|
+
return WORKSPACE_RUNTIME_IDENTITY_ENV_KEYS.filter((key) => env[key] !== void 0);
|
|
28
|
+
}
|
|
29
|
+
function assertWorkspaceRuntimeIdentityEnv(env) {
|
|
30
|
+
const conflicts = workspaceRuntimeIdentityEnvConflicts(env);
|
|
31
|
+
if (conflicts.length === 0) return;
|
|
32
|
+
throw new Error(
|
|
33
|
+
`sandbox env cannot set runtime identity ${conflicts.join(", ")}: the driver's placement owns the identity its target reports and the lease records`
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
var WORKSPACE_RUNTIME_PUBLIC_ENV_KEYS = /* @__PURE__ */ new Set([
|
|
37
|
+
...WORKSPACE_RUNTIME_IDENTITY_ENV_KEYS,
|
|
38
|
+
"WORKSPACE_RUNTIME_DIRECTORY",
|
|
39
|
+
"WORKSPACE_RUNTIME_PORT",
|
|
40
|
+
"WORKSPACE_RUNTIME_HOST",
|
|
41
|
+
"WORKSPACE_RUNTIME_SOURCE_KIND",
|
|
42
|
+
"WORKSPACE_RUNTIME_GIT_REPO_URL",
|
|
43
|
+
"WORKSPACE_RUNTIME_GIT_BRANCH",
|
|
44
|
+
"WORKSPACE_RUNTIME_NATIVE_HARNESS",
|
|
45
|
+
"WORKSPACE_RUNTIME_RELAY_JWKS_URL",
|
|
46
|
+
"WORKSPACE_RUNTIME_MANAGEMENT_JWKS_URL",
|
|
47
|
+
"WORKSPACE_RUNTIME_SESSION_AUTHORITY_URL"
|
|
48
|
+
]);
|
|
49
|
+
function workspaceRuntimeBootEnv(input) {
|
|
50
|
+
assertWorkspaceRuntimeIdentityEnv(input.env);
|
|
51
|
+
const env = {
|
|
52
|
+
...workspaceRuntimeTargetEnv(input),
|
|
53
|
+
...workspaceRuntimeSourceEnv(input),
|
|
54
|
+
...input.env
|
|
55
|
+
};
|
|
56
|
+
if (input.nativeHarness) env.WORKSPACE_RUNTIME_NATIVE_HARNESS = input.nativeHarness;
|
|
57
|
+
if (input.controlEnv?.relayJwksUrl) env.WORKSPACE_RUNTIME_RELAY_JWKS_URL = input.controlEnv.relayJwksUrl;
|
|
58
|
+
if (input.controlEnv?.relayVerifyPem) env.WORKSPACE_RUNTIME_RELAY_HOST_VERIFY_PEM = input.controlEnv.relayVerifyPem;
|
|
59
|
+
if (input.controlEnv?.managementJwksUrl) env.WORKSPACE_RUNTIME_MANAGEMENT_JWKS_URL = input.controlEnv.managementJwksUrl;
|
|
60
|
+
if (input.controlEnv?.sessionAuthorityUrl) env.WORKSPACE_RUNTIME_SESSION_AUTHORITY_URL = input.controlEnv.sessionAuthorityUrl;
|
|
61
|
+
return env;
|
|
62
|
+
}
|
|
9
63
|
function workspaceRuntimeTargetEnv(input) {
|
|
10
64
|
return {
|
|
11
65
|
WORKSPACE_RUNTIME_WORKSPACE_ID: input.workspaceId,
|
|
@@ -32,9 +86,6 @@ function shell(input) {
|
|
|
32
86
|
// src/constants.ts
|
|
33
87
|
var DEFAULT_WORKSPACE_RUNTIME_PORT = 2593;
|
|
34
88
|
|
|
35
|
-
// src/image.ts
|
|
36
|
-
import { Image, DaytonaNotFoundError } from "@daytona/sdk";
|
|
37
|
-
|
|
38
89
|
// src/runtime-version.ts
|
|
39
90
|
var DEFAULT_WORKSPACE_RUNTIME_VERSION = "0.5.2";
|
|
40
91
|
function workspaceRuntimeVersion() {
|
|
@@ -47,6 +98,12 @@ var DEFAULT_SANDBOX_IMAGE_REPOSITORY = "ghcr.io/kyashrathore/claxedo-sandbox";
|
|
|
47
98
|
function snapshotVersion(value) {
|
|
48
99
|
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
49
100
|
}
|
|
101
|
+
function assertSandboxImageReference(image) {
|
|
102
|
+
if (!image || image !== image.trim() || image.startsWith("-") || /\s/.test(image)) {
|
|
103
|
+
throw new Error(`sandbox image reference is not a legal image identifier: ${JSON.stringify(image)}`);
|
|
104
|
+
}
|
|
105
|
+
return image;
|
|
106
|
+
}
|
|
50
107
|
function sandboxImageRepository(env = process.env) {
|
|
51
108
|
const repository = env.CLAXEDO_SANDBOX_IMAGE_REPOSITORY?.trim();
|
|
52
109
|
return repository ? repository : DEFAULT_SANDBOX_IMAGE_REPOSITORY;
|
|
@@ -55,107 +112,87 @@ function buildIdSuffix(buildId, env) {
|
|
|
55
112
|
const id = (buildId ?? env.CLAXEDO_SANDBOX_BUILD_ID)?.trim();
|
|
56
113
|
return id ? `-${snapshotVersion(id)}` : "";
|
|
57
114
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
return `claxedo-workspace-runtime-${snapshotVersion(version)}${buildIdSuffix(buildId, env)}-v${SNAPSHOT_SCHEMA_VERSION}`;
|
|
115
|
+
var AGENT_PLUGINS_TAG = "-agent-plugins";
|
|
116
|
+
function defaultSandboxImage(version = workspaceRuntimeVersion(), buildId, env = process.env, options = {}) {
|
|
117
|
+
const feature = options.agentPlugins ? AGENT_PLUGINS_TAG : "";
|
|
118
|
+
return `${sandboxImageRepository(env)}:workspace-runtime-${snapshotVersion(version)}${buildIdSuffix(buildId, env)}${feature}-v${SNAPSHOT_SCHEMA_VERSION}`;
|
|
63
119
|
}
|
|
64
120
|
|
|
65
121
|
// src/image.ts
|
|
66
122
|
var SANDBOX_IMAGE_REPOSITORY = sandboxImageRepository();
|
|
67
123
|
var SANDBOX_IMAGE = process.env.CLAXEDO_SANDBOX_IMAGE || defaultSandboxImage();
|
|
68
|
-
var SNAPSHOT_NAME = process.env.CLAXEDO_SNAPSHOT_NAME || defaultSnapshotName();
|
|
69
124
|
|
|
70
|
-
// src/
|
|
71
|
-
var
|
|
72
|
-
|
|
73
|
-
"
|
|
74
|
-
"
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
"
|
|
87
|
-
"
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
125
|
+
// src/driver-billing.ts
|
|
126
|
+
var HOUR_SECONDS = 3600;
|
|
127
|
+
var boatBilling = {
|
|
128
|
+
source: { url: "https://docs.boat.dev/pricing", readOn: "2026-10-12" },
|
|
129
|
+
meter: "Boat bills machine time per second while a sandbox runs, at an hourly rate per size; a stopped sandbox costs nothing.",
|
|
130
|
+
rates: {
|
|
131
|
+
small: { size: "small", vcpu: 2, memoryGb: 4, usdPerHour: 0.018 },
|
|
132
|
+
default: { size: "default", vcpu: 4, memoryGb: 8, usdPerHour: 0.036 },
|
|
133
|
+
large: { size: "large", vcpu: 8, memoryGb: 16, usdPerHour: 0.072 }
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
var CLOUDFLARE_USD_PER_GIB_SECOND_MEMORY = 25e-7;
|
|
137
|
+
var CLOUDFLARE_USD_PER_GB_SECOND_DISK = 7e-8;
|
|
138
|
+
var CLOUDFLARE_USD_PER_VCPU_SECOND_ACTIVE = 2e-5;
|
|
139
|
+
var CLOUDFLARE_STANDARD_1 = { size: "standard-1", vcpu: 0.5, memoryGb: 4, diskGb: 8 };
|
|
140
|
+
var cloudflareBilling = {
|
|
141
|
+
source: { url: "https://developers.cloudflare.com/containers/pricing/", readOn: "2026-10-12" },
|
|
142
|
+
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.",
|
|
143
|
+
rates: {
|
|
144
|
+
default: {
|
|
145
|
+
size: CLOUDFLARE_STANDARD_1.size,
|
|
146
|
+
vcpu: CLOUDFLARE_STANDARD_1.vcpu,
|
|
147
|
+
memoryGb: CLOUDFLARE_STANDARD_1.memoryGb,
|
|
148
|
+
usdPerHour: (CLOUDFLARE_STANDARD_1.memoryGb * CLOUDFLARE_USD_PER_GIB_SECOND_MEMORY + CLOUDFLARE_STANDARD_1.diskGb * CLOUDFLARE_USD_PER_GB_SECOND_DISK) * HOUR_SECONDS
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
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`
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
// ../sandbox-contract/dist/index.mjs
|
|
155
|
+
var sandboxDriverCredentialFields = {
|
|
156
|
+
modal: [
|
|
157
|
+
{ key: "token_id", label: "Token ID", secret: true },
|
|
158
|
+
{ key: "token_secret", label: "Token Secret", secret: true }
|
|
159
|
+
],
|
|
160
|
+
vercel: [
|
|
161
|
+
{ key: "access_token", label: "Access Token", secret: true },
|
|
162
|
+
{ key: "team_id", label: "Team ID" },
|
|
163
|
+
{ key: "project_id", label: "Project ID" }
|
|
164
|
+
],
|
|
165
|
+
cloudflare: [
|
|
166
|
+
{ key: "api_token", label: "API Token", secret: true },
|
|
167
|
+
{ key: "worker_url", label: "Worker URL" }
|
|
168
|
+
],
|
|
169
|
+
boat: [{ key: "api_key", label: "API Key", secret: true }],
|
|
170
|
+
docker: [{ key: "image", label: "Image" }]
|
|
171
|
+
};
|
|
172
|
+
var sandboxDriverLabels = {
|
|
173
|
+
modal: "Modal",
|
|
174
|
+
vercel: "Vercel",
|
|
175
|
+
cloudflare: "Cloudflare",
|
|
176
|
+
boat: "Boat",
|
|
177
|
+
docker: "Docker"
|
|
178
|
+
};
|
|
95
179
|
|
|
96
180
|
// src/driver-catalog.ts
|
|
97
181
|
var sandboxDriverCatalog = {
|
|
98
|
-
exe: {
|
|
99
|
-
id: "exe",
|
|
100
|
-
label: "exe.dev",
|
|
101
|
-
credentialFields: [{ key: "api_token", label: "API Token", secret: true }],
|
|
102
|
-
metadata: {
|
|
103
|
-
driverRunsIn: ["worker", "node"],
|
|
104
|
-
hostStopBehavior: "not-supported",
|
|
105
|
-
hostResumeBehavior: "same-host",
|
|
106
|
-
targetAccess: "relay",
|
|
107
|
-
secretBrokering: "none",
|
|
108
|
-
egressControl: "none",
|
|
109
|
-
persistence: {
|
|
110
|
-
resume: "same-sandbox",
|
|
111
|
-
capture: "none",
|
|
112
|
-
clone: true,
|
|
113
|
-
captureSource: "not-applicable",
|
|
114
|
-
retention: "provider-managed",
|
|
115
|
-
restoreMount: "same-resource"
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
},
|
|
119
|
-
daytona: {
|
|
120
|
-
id: "daytona",
|
|
121
|
-
label: "Daytona",
|
|
122
|
-
credentialFields: [{ key: "api_key", label: "API Key", secret: true }],
|
|
123
|
-
metadata: {
|
|
124
|
-
driverRunsIn: ["worker", "node"],
|
|
125
|
-
hostStopBehavior: "suspends-host",
|
|
126
|
-
hostResumeBehavior: "same-host",
|
|
127
|
-
targetAccess: "relay",
|
|
128
|
-
secretBrokering: "native",
|
|
129
|
-
// Daytona is the only driver that can filter egress by NAME as well as
|
|
130
|
-
// by address: `domainAllowList` (names) alongside `networkAllowList`
|
|
131
|
-
// (CIDRs), with `networkBlockAll` as the deny-all floor.
|
|
132
|
-
egressControl: "hosts-and-cidrs",
|
|
133
|
-
persistence: {
|
|
134
|
-
resume: "same-sandbox",
|
|
135
|
-
capture: "filesystem",
|
|
136
|
-
clone: false,
|
|
137
|
-
captureSource: "preserved",
|
|
138
|
-
retention: "provider-managed",
|
|
139
|
-
restoreMount: "new-resource"
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
},
|
|
143
182
|
modal: {
|
|
144
183
|
id: "modal",
|
|
145
|
-
label:
|
|
146
|
-
credentialFields:
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
184
|
+
label: sandboxDriverLabels.modal,
|
|
185
|
+
credentialFields: sandboxDriverCredentialFields.modal,
|
|
186
|
+
runtimeNetwork: { controlPlane: "direct", relay: "configured" },
|
|
187
|
+
runtimeEnv: "callback",
|
|
188
|
+
preparedImage: true,
|
|
150
189
|
metadata: {
|
|
151
190
|
driverRunsIn: ["node"],
|
|
152
191
|
hostStopBehavior: "terminates-host",
|
|
153
192
|
hostResumeBehavior: "replacement-host",
|
|
154
193
|
targetAccess: "relay",
|
|
155
194
|
secretBrokering: "none",
|
|
156
|
-
//
|
|
157
|
-
// an allowlist, and its driver throws on a host policy. A blackout is
|
|
158
|
-
// not containment for a workspace that has to clone and reach a model.
|
|
195
|
+
// The driver does not install Modal's domain allowlist or alpha sidecar.
|
|
159
196
|
egressControl: "none",
|
|
160
197
|
persistence: {
|
|
161
198
|
resume: "replacement-restore",
|
|
@@ -169,20 +206,18 @@ var sandboxDriverCatalog = {
|
|
|
169
206
|
},
|
|
170
207
|
vercel: {
|
|
171
208
|
id: "vercel",
|
|
172
|
-
label:
|
|
173
|
-
credentialFields:
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
],
|
|
209
|
+
label: sandboxDriverLabels.vercel,
|
|
210
|
+
credentialFields: sandboxDriverCredentialFields.vercel,
|
|
211
|
+
runtimeNetwork: { controlPlane: "direct", relay: "configured" },
|
|
212
|
+
runtimeEnv: "callback",
|
|
213
|
+
preparedImage: false,
|
|
178
214
|
metadata: {
|
|
179
215
|
driverRunsIn: ["node"],
|
|
180
216
|
hostStopBehavior: "terminates-host",
|
|
181
217
|
hostResumeBehavior: "replacement-host",
|
|
182
218
|
targetAccess: "relay",
|
|
183
219
|
secretBrokering: "native",
|
|
184
|
-
//
|
|
185
|
-
// never CIDRs.
|
|
220
|
+
// The driver translates hosts only; the SDK's subnet rules are not wired to net.cidrs.
|
|
186
221
|
egressControl: "hosts",
|
|
187
222
|
persistence: {
|
|
188
223
|
resume: "replacement-restore",
|
|
@@ -196,40 +231,42 @@ var sandboxDriverCatalog = {
|
|
|
196
231
|
},
|
|
197
232
|
cloudflare: {
|
|
198
233
|
id: "cloudflare",
|
|
199
|
-
label:
|
|
200
|
-
credentialFields:
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
234
|
+
label: sandboxDriverLabels.cloudflare,
|
|
235
|
+
credentialFields: sandboxDriverCredentialFields.cloudflare,
|
|
236
|
+
runtimeNetwork: { controlPlane: "direct", relay: "configured" },
|
|
237
|
+
runtimeEnv: "callback",
|
|
238
|
+
preparedImage: false,
|
|
239
|
+
billing: cloudflareBilling,
|
|
204
240
|
metadata: {
|
|
205
241
|
driverRunsIn: ["worker"],
|
|
206
|
-
hostStopBehavior: "
|
|
242
|
+
hostStopBehavior: "terminates-host",
|
|
207
243
|
hostResumeBehavior: "same-host",
|
|
244
|
+
hostWatchesIdle: true,
|
|
208
245
|
targetAccess: "relay",
|
|
209
|
-
secretBrokering: "
|
|
210
|
-
//
|
|
211
|
-
// (drivers/cloudflare-egress.ts) is an OPT-IN proxy the sandbox chooses
|
|
212
|
-
// to route brokered-credential requests through, not a boundary. The
|
|
213
|
-
// driver therefore drops `net` entirely — which is exactly why this
|
|
214
|
-
// must be declared, so the manager refuses instead of provisioning a
|
|
215
|
-
// sandbox the caller believes is contained.
|
|
246
|
+
secretBrokering: "native",
|
|
247
|
+
// Credential handlers do not restrict unrelated destinations.
|
|
216
248
|
egressControl: "none",
|
|
249
|
+
// A container snapshot holds the whole root filesystem; the platform keeps it 30 days past its last restore and offers no deletion.
|
|
217
250
|
persistence: {
|
|
218
251
|
resume: "replacement-restore",
|
|
219
|
-
capture: "
|
|
252
|
+
capture: "filesystem",
|
|
220
253
|
clone: false,
|
|
221
254
|
captureSource: "preserved",
|
|
222
255
|
retention: "provider-managed",
|
|
223
|
-
restoreMount: "
|
|
256
|
+
restoreMount: "new-resource"
|
|
224
257
|
}
|
|
225
258
|
}
|
|
226
259
|
},
|
|
227
|
-
|
|
228
|
-
id: "
|
|
229
|
-
label:
|
|
230
|
-
credentialFields:
|
|
260
|
+
boat: {
|
|
261
|
+
id: "boat",
|
|
262
|
+
label: sandboxDriverLabels.boat,
|
|
263
|
+
credentialFields: sandboxDriverCredentialFields.boat,
|
|
264
|
+
runtimeNetwork: { controlPlane: "direct", relay: "configured" },
|
|
265
|
+
runtimeEnv: "callback",
|
|
266
|
+
preparedImage: true,
|
|
267
|
+
billing: boatBilling,
|
|
231
268
|
metadata: {
|
|
232
|
-
driverRunsIn: ["node"],
|
|
269
|
+
driverRunsIn: ["worker", "node"],
|
|
233
270
|
hostStopBehavior: "suspends-host",
|
|
234
271
|
hostResumeBehavior: "same-host",
|
|
235
272
|
targetAccess: "relay",
|
|
@@ -242,13 +279,17 @@ var sandboxDriverCatalog = {
|
|
|
242
279
|
captureSource: "not-applicable",
|
|
243
280
|
retention: "provider-managed",
|
|
244
281
|
restoreMount: "same-resource"
|
|
245
|
-
}
|
|
282
|
+
},
|
|
283
|
+
machineClasses: ["small", "default", "large"]
|
|
246
284
|
}
|
|
247
285
|
},
|
|
248
286
|
docker: {
|
|
249
287
|
id: "docker",
|
|
250
|
-
label:
|
|
251
|
-
credentialFields:
|
|
288
|
+
label: sandboxDriverLabels.docker,
|
|
289
|
+
credentialFields: sandboxDriverCredentialFields.docker,
|
|
290
|
+
runtimeNetwork: { controlPlane: "docker-host", relay: "configured" },
|
|
291
|
+
runtimeEnv: "callback",
|
|
292
|
+
preparedImage: true,
|
|
252
293
|
metadata: {
|
|
253
294
|
driverRunsIn: ["local"],
|
|
254
295
|
hostStopBehavior: "terminates-host",
|
|
@@ -268,6 +309,48 @@ var sandboxDriverCatalog = {
|
|
|
268
309
|
}
|
|
269
310
|
};
|
|
270
311
|
|
|
312
|
+
// ../claxedo-helpers/dist/guards.mjs
|
|
313
|
+
function isRecord(value) {
|
|
314
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// src/json.ts
|
|
318
|
+
function num(input) {
|
|
319
|
+
return typeof input === "number" ? input : void 0;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// src/drivers/transient-error.ts
|
|
323
|
+
function textOf(input) {
|
|
324
|
+
return typeof input === "string" ? input : "";
|
|
325
|
+
}
|
|
326
|
+
function driverErrorSignals(err) {
|
|
327
|
+
const shaped = isRecord(err) ? err : {};
|
|
328
|
+
const nested = isRecord(shaped.response) ? shaped.response : {};
|
|
329
|
+
return {
|
|
330
|
+
// `response.status` is the axios shape; `statusCode` is what SDK-native
|
|
331
|
+
// error classes expose instead.
|
|
332
|
+
status: num(nested.status) ?? num(shaped.status) ?? num(shaped.statusCode),
|
|
333
|
+
text: `${textOf(shaped.code)} ${textOf(shaped.name)} ${textOf(shaped.message)}`.toLowerCase()
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
function isTransientDriverError(err, markers) {
|
|
337
|
+
const { status, text } = driverErrorSignals(err);
|
|
338
|
+
if (status !== void 0 && status >= 500) return true;
|
|
339
|
+
return markers.some((marker) => text.includes(marker));
|
|
340
|
+
}
|
|
341
|
+
var TRANSIENT_DRIVER_RETRY_MS = 2e3;
|
|
342
|
+
|
|
343
|
+
// ../claxedo-helpers/dist/deadline.mjs
|
|
344
|
+
async function withDeadlineSignal(timeoutMs, work, parent) {
|
|
345
|
+
const deadline = new AbortController();
|
|
346
|
+
const timer = setTimeout(() => deadline.abort(new DOMException("The operation timed out.", "TimeoutError")), timeoutMs);
|
|
347
|
+
try {
|
|
348
|
+
return await work(parent ? AbortSignal.any([parent, deadline.signal]) : deadline.signal);
|
|
349
|
+
} finally {
|
|
350
|
+
clearTimeout(timer);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
271
354
|
// src/drivers/docker.ts
|
|
272
355
|
var execFileAsync = promisify(execFile);
|
|
273
356
|
var DEFAULT_OPERATION_TIMEOUT_MS = 6e4;
|
|
@@ -278,13 +361,13 @@ var DEFAULT_WORKSPACE_DIR = "/workspace";
|
|
|
278
361
|
var DOCKER_LABEL = "claxedo";
|
|
279
362
|
var AUTH_STAGE_DIR = "/tmp/claxedo-auth";
|
|
280
363
|
function dockerSandboxSyncLocalAuth(env = process.env) {
|
|
281
|
-
return
|
|
364
|
+
return enabledFlag(env.CLAXEDO_DOCKER_SANDBOX_SYNC_LOCAL_AUTH);
|
|
282
365
|
}
|
|
283
366
|
function dockerSandboxAuthHome(env = process.env) {
|
|
284
|
-
return
|
|
367
|
+
return trimToUndefined(env.CLAXEDO_DOCKER_SANDBOX_AUTH_HOME) ?? trimToUndefined(env.HOME) ?? os.homedir();
|
|
285
368
|
}
|
|
286
369
|
function dockerSandboxImageFromEnv(env = process.env) {
|
|
287
|
-
return
|
|
370
|
+
return trimToUndefined(env.CLAXEDO_DOCKER_SANDBOX_IMAGE) ?? trimToUndefined(env.CLAXEDO_SANDBOX_IMAGE) ?? SANDBOX_IMAGE;
|
|
288
371
|
}
|
|
289
372
|
function parseDockerPort(input) {
|
|
290
373
|
const line = input.trim().split(/\r?\n/)[0]?.trim();
|
|
@@ -315,21 +398,18 @@ function dockerLocalAuthCandidates(home) {
|
|
|
315
398
|
}
|
|
316
399
|
];
|
|
317
400
|
}
|
|
318
|
-
function
|
|
319
|
-
|
|
320
|
-
return txt ? txt : void 0;
|
|
321
|
-
}
|
|
322
|
-
function disabled(input) {
|
|
323
|
-
return ["0", "false", "no", "off"].includes(input?.trim().toLowerCase() ?? "");
|
|
401
|
+
function enabledFlag(input) {
|
|
402
|
+
return ["1", "true", "yes", "on"].includes(input?.trim().toLowerCase() ?? "");
|
|
324
403
|
}
|
|
325
404
|
function nameFor(workspaceId) {
|
|
326
405
|
const cleanId = workspaceId.toLowerCase().replace(/[^a-z0-9_.-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
327
406
|
return `claxedo-${cleanId || "workspace"}-${Date.now().toString(36)}`.slice(0, 63);
|
|
328
407
|
}
|
|
329
|
-
async function docker(args, timeoutMs = DEFAULT_OPERATION_TIMEOUT_MS) {
|
|
408
|
+
async function docker(args, timeoutMs = DEFAULT_OPERATION_TIMEOUT_MS, env) {
|
|
330
409
|
return execFileAsync("docker", args, {
|
|
331
410
|
maxBuffer: 20 * 1024 * 1024,
|
|
332
|
-
timeout: timeoutMs
|
|
411
|
+
timeout: timeoutMs,
|
|
412
|
+
...env ? { env: { ...process.env, ...env } } : {}
|
|
333
413
|
});
|
|
334
414
|
}
|
|
335
415
|
function sleep(ms) {
|
|
@@ -340,22 +420,25 @@ async function stageLocalAgentAuth(containerId, input) {
|
|
|
340
420
|
const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "claxedo-docker-auth-"));
|
|
341
421
|
try {
|
|
342
422
|
const stagedRoot = path.join(tmp, "root");
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
423
|
+
let copied = false;
|
|
424
|
+
for (const item of dockerLocalAuthCandidates(input.authHome)) {
|
|
425
|
+
const stat = await fs.stat(item.source).catch((error) => {
|
|
426
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") return void 0;
|
|
427
|
+
throw error;
|
|
428
|
+
});
|
|
429
|
+
if (!stat) continue;
|
|
430
|
+
if (item.kind === "file" && !stat.isFile()) continue;
|
|
431
|
+
if (item.kind === "dir" && !stat.isDirectory()) continue;
|
|
348
432
|
const target = path.join(stagedRoot, item.target.replace(/^\/root\/?/, ""));
|
|
349
433
|
await fs.mkdir(path.dirname(target), { recursive: true });
|
|
350
434
|
if (item.kind === "dir") await fs.cp(item.source, target, { recursive: true });
|
|
351
435
|
else await fs.copyFile(item.source, target);
|
|
352
|
-
|
|
353
|
-
}
|
|
354
|
-
if (!copied
|
|
436
|
+
copied = true;
|
|
437
|
+
}
|
|
438
|
+
if (!copied) return;
|
|
355
439
|
await input.docker(["cp", stagedRoot, `${containerId}:${AUTH_STAGE_DIR}`], input.timeoutMs);
|
|
356
440
|
} finally {
|
|
357
|
-
await fs.rm(tmp, { recursive: true, force: true })
|
|
358
|
-
});
|
|
441
|
+
await fs.rm(tmp, { recursive: true, force: true });
|
|
359
442
|
}
|
|
360
443
|
}
|
|
361
444
|
function authHydrationScript() {
|
|
@@ -377,16 +460,15 @@ function bootScript(directory, runtimeCommand) {
|
|
|
377
460
|
`exec ${runtimeCommand}`
|
|
378
461
|
].join(" && ");
|
|
379
462
|
}
|
|
380
|
-
function
|
|
381
|
-
return Object.
|
|
463
|
+
function envNameArgs(input) {
|
|
464
|
+
return Object.keys(input).flatMap((key) => ["--env", key]);
|
|
382
465
|
}
|
|
383
466
|
function labelArgs(input) {
|
|
384
467
|
return Object.entries(input).flatMap(([key, value]) => ["--label", `${key}=${value}`]);
|
|
385
468
|
}
|
|
469
|
+
var TRANSIENT_MARKERS = ["timeout", "temporarily unavailable", "connection refused"];
|
|
386
470
|
function transientDriverError(err) {
|
|
387
|
-
|
|
388
|
-
const text = `${shaped.code ?? ""} ${shaped.message ?? ""}`.toLowerCase();
|
|
389
|
-
return text.includes("timeout") || text.includes("temporarily unavailable") || text.includes("connection refused");
|
|
471
|
+
return isTransientDriverError(err, TRANSIENT_MARKERS);
|
|
390
472
|
}
|
|
391
473
|
function createDockerSandboxDriver(options) {
|
|
392
474
|
const dockerCommand = options.docker ?? docker;
|
|
@@ -408,22 +490,21 @@ function createDockerSandboxDriver(options) {
|
|
|
408
490
|
if (input.bootSource?.kind === "driver-snapshot") {
|
|
409
491
|
throw new Error("Docker SandboxDriver does not support driver snapshots; use image boot");
|
|
410
492
|
}
|
|
411
|
-
return
|
|
493
|
+
return assertSandboxImageReference(
|
|
494
|
+
input.bootSource?.kind === "image" ? input.bootSource.image : input.snapshot ?? options.image
|
|
495
|
+
);
|
|
412
496
|
}
|
|
413
497
|
function bootEnv(input, hostId) {
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
};
|
|
425
|
-
if (options.runner) env.WORKSPACE_RUNTIME_RUNNER = options.runner;
|
|
426
|
-
return env;
|
|
498
|
+
return workspaceRuntimeBootEnv({
|
|
499
|
+
workspaceId: input.workspaceId,
|
|
500
|
+
hostId,
|
|
501
|
+
directory: workspaceDirectory(input),
|
|
502
|
+
port: runtimePort(input),
|
|
503
|
+
host: "0.0.0.0",
|
|
504
|
+
source: input.source,
|
|
505
|
+
env: input.env,
|
|
506
|
+
nativeHarness: options.nativeHarness
|
|
507
|
+
});
|
|
427
508
|
}
|
|
428
509
|
async function readyTarget(input, sandboxId, hostId) {
|
|
429
510
|
const out = await dockerCommand(["port", sandboxId, `${runtimePort(input)}/tcp`], timeoutMs);
|
|
@@ -448,9 +529,7 @@ function createDockerSandboxDriver(options) {
|
|
|
448
529
|
let error = "workspace runtime not ready";
|
|
449
530
|
while (Date.now() < until) {
|
|
450
531
|
try {
|
|
451
|
-
const res = await healthFetch(`${url}/global/health`, {
|
|
452
|
-
signal: AbortSignal.timeout(Math.min(2e3, Math.max(1, healthTimeoutMs)))
|
|
453
|
-
});
|
|
532
|
+
const res = await withDeadlineSignal(Math.min(2e3, Math.max(1, healthTimeoutMs)), (signal) => healthFetch(`${url}/global/health`, { signal }));
|
|
454
533
|
if (res.ok) return;
|
|
455
534
|
error = `${res.status} ${res.statusText}`;
|
|
456
535
|
} catch (err) {
|
|
@@ -480,17 +559,17 @@ function createDockerSandboxDriver(options) {
|
|
|
480
559
|
...labelArgs(input.labels),
|
|
481
560
|
"-p",
|
|
482
561
|
`127.0.0.1::${runtimePort(input)}`,
|
|
483
|
-
...
|
|
562
|
+
...envNameArgs(env),
|
|
484
563
|
"--entrypoint",
|
|
485
564
|
"sh",
|
|
486
565
|
image(input),
|
|
487
566
|
"-lc",
|
|
488
567
|
bootScript(workspaceDirectory(input), runtimeCommand)
|
|
489
|
-
], 12e4).catch((err) => {
|
|
568
|
+
], 12e4, env).catch((err) => {
|
|
490
569
|
if (transientDriverError(err)) return void 0;
|
|
491
570
|
throw err;
|
|
492
571
|
});
|
|
493
|
-
if (!created) return { provisioning: true, retryAfterMs:
|
|
572
|
+
if (!created) return { provisioning: true, retryAfterMs: TRANSIENT_DRIVER_RETRY_MS };
|
|
494
573
|
const sandboxId = String(created.stdout ?? "").trim() || hostId;
|
|
495
574
|
await stageLocalAgentAuth(sandboxId, { authHome, syncLocalAuth, docker: dockerCommand, timeoutMs });
|
|
496
575
|
await dockerCommand(["start", sandboxId], timeoutMs);
|
|
@@ -499,7 +578,7 @@ function createDockerSandboxDriver(options) {
|
|
|
499
578
|
async function resumeHost(input) {
|
|
500
579
|
const sandboxId = input.lease.sandboxId ?? input.lease.sandbox_id;
|
|
501
580
|
if (!sandboxId) return ensureHost(input.ensure);
|
|
502
|
-
await dockerCommand(["inspect", "-f", "{{.Id}}", sandboxId], timeoutMs)
|
|
581
|
+
await dockerCommand(["inspect", "-f", "{{.Id}}", sandboxId], timeoutMs);
|
|
503
582
|
await dockerCommand(["start", sandboxId], timeoutMs).catch((err) => {
|
|
504
583
|
if (transientDriverError(err)) return void 0;
|
|
505
584
|
throw err;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { SandboxDriver } from "../contract";
|
|
2
|
+
import { localBrokeringTestDriverCatalogEntry } from "../driver-catalog";
|
|
3
|
+
export type LocalBrokeringDriverOptions = {
|
|
4
|
+
root: string;
|
|
5
|
+
executable: string;
|
|
6
|
+
args: string[];
|
|
7
|
+
allowedOrigins: readonly string[];
|
|
8
|
+
directOrigins: readonly string[];
|
|
9
|
+
upstreams?: Readonly<Record<string, string>>;
|
|
10
|
+
inheritedEnv?: Record<string, string>;
|
|
11
|
+
};
|
|
12
|
+
export declare function createLocalBrokeringSandboxDriver(options: LocalBrokeringDriverOptions): SandboxDriver & {
|
|
13
|
+
catalogEntry: typeof localBrokeringTestDriverCatalogEntry;
|
|
14
|
+
};
|