@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/dist/drivers/modal.mjs
CHANGED
|
@@ -33,16 +33,8 @@ import { Image, DaytonaNotFoundError } from "@daytona/sdk";
|
|
|
33
33
|
|
|
34
34
|
// src/runtime-version.ts
|
|
35
35
|
var DEFAULT_WORKSPACE_RUNTIME_VERSION = "0.5.2";
|
|
36
|
-
var versionCache;
|
|
37
36
|
function workspaceRuntimeVersion() {
|
|
38
|
-
|
|
39
|
-
const envVersion = process.env.WORKSPACE_RUNTIME_VERSION;
|
|
40
|
-
if (envVersion) {
|
|
41
|
-
versionCache = envVersion;
|
|
42
|
-
return versionCache;
|
|
43
|
-
}
|
|
44
|
-
versionCache = DEFAULT_WORKSPACE_RUNTIME_VERSION;
|
|
45
|
-
return versionCache;
|
|
37
|
+
return process.env.WORKSPACE_RUNTIME_VERSION || DEFAULT_WORKSPACE_RUNTIME_VERSION;
|
|
46
38
|
}
|
|
47
39
|
|
|
48
40
|
// src/image-name.ts
|
|
@@ -71,6 +63,207 @@ var SANDBOX_IMAGE_REPOSITORY = sandboxImageRepository();
|
|
|
71
63
|
var SANDBOX_IMAGE = process.env.CLAXEDO_SANDBOX_IMAGE || defaultSandboxImage();
|
|
72
64
|
var SNAPSHOT_NAME = process.env.CLAXEDO_SNAPSHOT_NAME || defaultSnapshotName();
|
|
73
65
|
|
|
66
|
+
// src/hosted-network-policy.ts
|
|
67
|
+
var SANDBOX_MODEL_PROVIDER_HOSTS = [
|
|
68
|
+
"api.anthropic.com",
|
|
69
|
+
"api.openai.com",
|
|
70
|
+
"generativelanguage.googleapis.com",
|
|
71
|
+
"openrouter.ai",
|
|
72
|
+
// Model catalog + engine config, fetched on runner startup.
|
|
73
|
+
"models.dev",
|
|
74
|
+
"opencode.ai"
|
|
75
|
+
];
|
|
76
|
+
var SANDBOX_PACKAGE_REGISTRY_HOSTS = [
|
|
77
|
+
"registry.npmjs.org",
|
|
78
|
+
"nodejs.org",
|
|
79
|
+
"pypi.org",
|
|
80
|
+
"files.pythonhosted.org",
|
|
81
|
+
"crates.io",
|
|
82
|
+
"static.crates.io",
|
|
83
|
+
"index.crates.io",
|
|
84
|
+
"proxy.golang.org",
|
|
85
|
+
"sum.golang.org"
|
|
86
|
+
];
|
|
87
|
+
var SANDBOX_BASELINE_EGRESS_HOSTS = [
|
|
88
|
+
...SANDBOX_MODEL_PROVIDER_HOSTS,
|
|
89
|
+
...SANDBOX_PACKAGE_REGISTRY_HOSTS
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
// src/driver-catalog.ts
|
|
93
|
+
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
|
+
modal: {
|
|
140
|
+
id: "modal",
|
|
141
|
+
label: "Modal",
|
|
142
|
+
credentialFields: [
|
|
143
|
+
{ key: "token_id", label: "Token ID", secret: true },
|
|
144
|
+
{ key: "token_secret", label: "Token Secret", secret: true }
|
|
145
|
+
],
|
|
146
|
+
metadata: {
|
|
147
|
+
driverRunsIn: ["node"],
|
|
148
|
+
hostStopBehavior: "terminates-host",
|
|
149
|
+
hostResumeBehavior: "replacement-host",
|
|
150
|
+
targetAccess: "relay",
|
|
151
|
+
secretBrokering: "none",
|
|
152
|
+
// Modal can cut the network entirely (`blockNetwork`) but cannot express
|
|
153
|
+
// an allowlist, and its driver throws on a host policy. A blackout is
|
|
154
|
+
// not containment for a workspace that has to clone and reach a model.
|
|
155
|
+
egressControl: "none",
|
|
156
|
+
persistence: {
|
|
157
|
+
resume: "replacement-restore",
|
|
158
|
+
capture: "filesystem",
|
|
159
|
+
clone: false,
|
|
160
|
+
captureSource: "preserved",
|
|
161
|
+
retention: "provider-managed",
|
|
162
|
+
restoreMount: "new-resource"
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
vercel: {
|
|
167
|
+
id: "vercel",
|
|
168
|
+
label: "Vercel",
|
|
169
|
+
credentialFields: [
|
|
170
|
+
{ key: "access_token", label: "Access Token", secret: true },
|
|
171
|
+
{ key: "team_id", label: "Team ID" },
|
|
172
|
+
{ key: "project_id", label: "Project ID" }
|
|
173
|
+
],
|
|
174
|
+
metadata: {
|
|
175
|
+
driverRunsIn: ["node"],
|
|
176
|
+
hostStopBehavior: "terminates-host",
|
|
177
|
+
hostResumeBehavior: "replacement-host",
|
|
178
|
+
targetAccess: "relay",
|
|
179
|
+
secretBrokering: "native",
|
|
180
|
+
// Vercel's sandbox firewall takes a hostname allow list (or "deny-all"),
|
|
181
|
+
// never CIDRs.
|
|
182
|
+
egressControl: "hosts",
|
|
183
|
+
persistence: {
|
|
184
|
+
resume: "replacement-restore",
|
|
185
|
+
capture: "filesystem",
|
|
186
|
+
clone: false,
|
|
187
|
+
captureSource: "stopped",
|
|
188
|
+
retention: "explicit",
|
|
189
|
+
restoreMount: "new-resource"
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
cloudflare: {
|
|
194
|
+
id: "cloudflare",
|
|
195
|
+
label: "Cloudflare",
|
|
196
|
+
credentialFields: [
|
|
197
|
+
{ key: "api_token", label: "API Token", secret: true },
|
|
198
|
+
{ key: "worker_url", label: "Worker URL" }
|
|
199
|
+
],
|
|
200
|
+
metadata: {
|
|
201
|
+
driverRunsIn: ["worker"],
|
|
202
|
+
hostStopBehavior: "not-supported",
|
|
203
|
+
hostResumeBehavior: "same-host",
|
|
204
|
+
targetAccess: "relay",
|
|
205
|
+
secretBrokering: "proxy",
|
|
206
|
+
// The Cloudflare Sandbox Worker exposes no egress filter: its broker
|
|
207
|
+
// (drivers/cloudflare-egress.ts) is an OPT-IN proxy the sandbox chooses
|
|
208
|
+
// to route brokered-credential requests through, not a boundary. The
|
|
209
|
+
// driver therefore drops `net` entirely — which is exactly why this
|
|
210
|
+
// must be declared, so the manager refuses instead of provisioning a
|
|
211
|
+
// sandbox the caller believes is contained.
|
|
212
|
+
egressControl: "none",
|
|
213
|
+
persistence: {
|
|
214
|
+
resume: "replacement-restore",
|
|
215
|
+
capture: "directories",
|
|
216
|
+
clone: false,
|
|
217
|
+
captureSource: "preserved",
|
|
218
|
+
retention: "provider-managed",
|
|
219
|
+
restoreMount: "copy-on-write"
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
},
|
|
223
|
+
box: {
|
|
224
|
+
id: "box",
|
|
225
|
+
label: "Box",
|
|
226
|
+
credentialFields: [{ key: "api_key", label: "API Key", secret: true }],
|
|
227
|
+
metadata: {
|
|
228
|
+
driverRunsIn: ["node"],
|
|
229
|
+
hostStopBehavior: "suspends-host",
|
|
230
|
+
hostResumeBehavior: "same-host",
|
|
231
|
+
targetAccess: "relay",
|
|
232
|
+
secretBrokering: "none",
|
|
233
|
+
egressControl: "none",
|
|
234
|
+
persistence: {
|
|
235
|
+
resume: "same-sandbox",
|
|
236
|
+
capture: "none",
|
|
237
|
+
clone: false,
|
|
238
|
+
captureSource: "not-applicable",
|
|
239
|
+
retention: "provider-managed",
|
|
240
|
+
restoreMount: "same-resource"
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
},
|
|
244
|
+
docker: {
|
|
245
|
+
id: "docker",
|
|
246
|
+
label: "Docker",
|
|
247
|
+
credentialFields: [{ key: "image", label: "Image" }],
|
|
248
|
+
metadata: {
|
|
249
|
+
driverRunsIn: ["local"],
|
|
250
|
+
hostStopBehavior: "terminates-host",
|
|
251
|
+
hostResumeBehavior: "same-host",
|
|
252
|
+
targetAccess: "loopback",
|
|
253
|
+
secretBrokering: "none",
|
|
254
|
+
egressControl: "none",
|
|
255
|
+
persistence: {
|
|
256
|
+
resume: "replacement-restore",
|
|
257
|
+
capture: "same-resource",
|
|
258
|
+
clone: false,
|
|
259
|
+
captureSource: "preserved",
|
|
260
|
+
retention: "provider-managed",
|
|
261
|
+
restoreMount: "same-resource"
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
|
|
74
267
|
// src/drivers/modal.ts
|
|
75
268
|
var DEFAULT_APP_NAME = "claxedo";
|
|
76
269
|
var DEFAULT_RUNTIME_COMMAND = "/usr/local/bin/workspace-runtime";
|
|
@@ -192,7 +385,9 @@ function createModalSandboxDriver(options) {
|
|
|
192
385
|
hostStopBehavior: "terminates-host",
|
|
193
386
|
hostResumeBehavior: "replacement-host",
|
|
194
387
|
targetAccess: "relay",
|
|
195
|
-
secretBrokering: "none"
|
|
388
|
+
secretBrokering: "none",
|
|
389
|
+
egressControl: "none",
|
|
390
|
+
persistence: sandboxDriverCatalog.modal.metadata.persistence
|
|
196
391
|
},
|
|
197
392
|
ensureHost,
|
|
198
393
|
resumeHost: (input) => ensureHost(input.ensure),
|
package/dist/drivers/vercel.d.ts
CHANGED
|
@@ -101,10 +101,29 @@ export type VercelSandboxDriverOptions = {
|
|
|
101
101
|
* a header on outbound requests to the allowlisted hosts, so the raw value
|
|
102
102
|
* never enters the sandbox (see @claxedo/sandbox-manager SandboxBrokeredSecret
|
|
103
103
|
* and https://vercel.com/docs/sandbox/concepts/firewall — credentials
|
|
104
|
-
* brokering).
|
|
105
|
-
*
|
|
106
|
-
*
|
|
104
|
+
* brokering).
|
|
105
|
+
*
|
|
106
|
+
* The result is the UNION of `base` — the create-time allow-list — and the
|
|
107
|
+
* brokered hosts, never a replacement. Replacing it is wrong in both
|
|
108
|
+
* directions. It BREAKS egress the caller was granted: `updateNetworkPolicy`
|
|
109
|
+
* overwrites the whole policy, so a sandbox created with git and the npm
|
|
110
|
+
* registry allowed loses both the instant a brokered secret is attached, and
|
|
111
|
+
* the agent's next `npm install` fails mid-run. And it can silently WIDEN
|
|
112
|
+
* egress: a brokered host that was not in the create-time list becomes
|
|
113
|
+
* reachable, so a policy nobody approved is installed as a side effect of
|
|
114
|
+
* attaching a credential. A union changes exactly one thing — it adds the
|
|
115
|
+
* brokered hosts, which is the smallest edit that makes the credential usable.
|
|
116
|
+
*
|
|
117
|
+
* `base` is the create-time `NetworkPolicy` exactly as passed to `create`, and
|
|
118
|
+
* it is REQUIRED even though `undefined` is a legal value: `undefined` means
|
|
119
|
+
* "no policy was requested", so the merge keeps egress unrestricted, expressed
|
|
120
|
+
* as the `"*"` wildcard the record form needs for the header transforms to ride
|
|
121
|
+
* alongside it. That is the opposite of what this function used to return with
|
|
122
|
+
* no base at all (deny-all-except-brokered-hosts), so an optional parameter
|
|
123
|
+
* would let an existing caller keep compiling while its egress silently WIDENS.
|
|
124
|
+
* Spelling the base out forces that decision to be made once, visibly. A
|
|
125
|
+
* `"deny-all"` base contributes nothing, leaving deny-all-except-brokered-hosts.
|
|
107
126
|
*/
|
|
108
|
-
export declare function vercelBrokeredNetworkPolicy(secrets: SandboxBrokeredSecret[]): NetworkPolicy;
|
|
127
|
+
export declare function vercelBrokeredNetworkPolicy(secrets: SandboxBrokeredSecret[], base: NetworkPolicy | undefined): NetworkPolicy;
|
|
109
128
|
export declare function createVercelSandboxDriver(options: VercelSandboxDriverOptions): SandboxDriver;
|
|
110
129
|
export {};
|
package/dist/drivers/vercel.mjs
CHANGED
|
@@ -22,16 +22,8 @@ function workspaceRuntimeSourceEnv(input) {
|
|
|
22
22
|
|
|
23
23
|
// src/runtime-version.ts
|
|
24
24
|
var DEFAULT_WORKSPACE_RUNTIME_VERSION = "0.5.2";
|
|
25
|
-
var versionCache;
|
|
26
25
|
function workspaceRuntimeVersion() {
|
|
27
|
-
|
|
28
|
-
const envVersion = process.env.WORKSPACE_RUNTIME_VERSION;
|
|
29
|
-
if (envVersion) {
|
|
30
|
-
versionCache = envVersion;
|
|
31
|
-
return versionCache;
|
|
32
|
-
}
|
|
33
|
-
versionCache = DEFAULT_WORKSPACE_RUNTIME_VERSION;
|
|
34
|
-
return versionCache;
|
|
26
|
+
return process.env.WORKSPACE_RUNTIME_VERSION || DEFAULT_WORKSPACE_RUNTIME_VERSION;
|
|
35
27
|
}
|
|
36
28
|
|
|
37
29
|
// src/command.ts
|
|
@@ -45,6 +37,207 @@ var DEFAULT_WORKSPACE_RUNTIME_PORT = 2593;
|
|
|
45
37
|
// src/defaults.ts
|
|
46
38
|
var RUNTIME_DIR = "/opt/workspace-runtime";
|
|
47
39
|
|
|
40
|
+
// src/hosted-network-policy.ts
|
|
41
|
+
var SANDBOX_MODEL_PROVIDER_HOSTS = [
|
|
42
|
+
"api.anthropic.com",
|
|
43
|
+
"api.openai.com",
|
|
44
|
+
"generativelanguage.googleapis.com",
|
|
45
|
+
"openrouter.ai",
|
|
46
|
+
// Model catalog + engine config, fetched on runner startup.
|
|
47
|
+
"models.dev",
|
|
48
|
+
"opencode.ai"
|
|
49
|
+
];
|
|
50
|
+
var SANDBOX_PACKAGE_REGISTRY_HOSTS = [
|
|
51
|
+
"registry.npmjs.org",
|
|
52
|
+
"nodejs.org",
|
|
53
|
+
"pypi.org",
|
|
54
|
+
"files.pythonhosted.org",
|
|
55
|
+
"crates.io",
|
|
56
|
+
"static.crates.io",
|
|
57
|
+
"index.crates.io",
|
|
58
|
+
"proxy.golang.org",
|
|
59
|
+
"sum.golang.org"
|
|
60
|
+
];
|
|
61
|
+
var SANDBOX_BASELINE_EGRESS_HOSTS = [
|
|
62
|
+
...SANDBOX_MODEL_PROVIDER_HOSTS,
|
|
63
|
+
...SANDBOX_PACKAGE_REGISTRY_HOSTS
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
// src/driver-catalog.ts
|
|
67
|
+
var sandboxDriverCatalog = {
|
|
68
|
+
exe: {
|
|
69
|
+
id: "exe",
|
|
70
|
+
label: "exe.dev",
|
|
71
|
+
credentialFields: [{ key: "api_token", label: "API Token", secret: true }],
|
|
72
|
+
metadata: {
|
|
73
|
+
driverRunsIn: ["worker", "node"],
|
|
74
|
+
hostStopBehavior: "not-supported",
|
|
75
|
+
hostResumeBehavior: "same-host",
|
|
76
|
+
targetAccess: "relay",
|
|
77
|
+
secretBrokering: "none",
|
|
78
|
+
egressControl: "none",
|
|
79
|
+
persistence: {
|
|
80
|
+
resume: "same-sandbox",
|
|
81
|
+
capture: "none",
|
|
82
|
+
clone: true,
|
|
83
|
+
captureSource: "not-applicable",
|
|
84
|
+
retention: "provider-managed",
|
|
85
|
+
restoreMount: "same-resource"
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
daytona: {
|
|
90
|
+
id: "daytona",
|
|
91
|
+
label: "Daytona",
|
|
92
|
+
credentialFields: [{ key: "api_key", label: "API Key", secret: true }],
|
|
93
|
+
metadata: {
|
|
94
|
+
driverRunsIn: ["worker", "node"],
|
|
95
|
+
hostStopBehavior: "suspends-host",
|
|
96
|
+
hostResumeBehavior: "same-host",
|
|
97
|
+
targetAccess: "relay",
|
|
98
|
+
secretBrokering: "native",
|
|
99
|
+
// Daytona is the only driver that can filter egress by NAME as well as
|
|
100
|
+
// by address: `domainAllowList` (names) alongside `networkAllowList`
|
|
101
|
+
// (CIDRs), with `networkBlockAll` as the deny-all floor.
|
|
102
|
+
egressControl: "hosts-and-cidrs",
|
|
103
|
+
persistence: {
|
|
104
|
+
resume: "same-sandbox",
|
|
105
|
+
capture: "filesystem",
|
|
106
|
+
clone: false,
|
|
107
|
+
captureSource: "preserved",
|
|
108
|
+
retention: "provider-managed",
|
|
109
|
+
restoreMount: "new-resource"
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
modal: {
|
|
114
|
+
id: "modal",
|
|
115
|
+
label: "Modal",
|
|
116
|
+
credentialFields: [
|
|
117
|
+
{ key: "token_id", label: "Token ID", secret: true },
|
|
118
|
+
{ key: "token_secret", label: "Token Secret", secret: true }
|
|
119
|
+
],
|
|
120
|
+
metadata: {
|
|
121
|
+
driverRunsIn: ["node"],
|
|
122
|
+
hostStopBehavior: "terminates-host",
|
|
123
|
+
hostResumeBehavior: "replacement-host",
|
|
124
|
+
targetAccess: "relay",
|
|
125
|
+
secretBrokering: "none",
|
|
126
|
+
// Modal can cut the network entirely (`blockNetwork`) but cannot express
|
|
127
|
+
// an allowlist, and its driver throws on a host policy. A blackout is
|
|
128
|
+
// not containment for a workspace that has to clone and reach a model.
|
|
129
|
+
egressControl: "none",
|
|
130
|
+
persistence: {
|
|
131
|
+
resume: "replacement-restore",
|
|
132
|
+
capture: "filesystem",
|
|
133
|
+
clone: false,
|
|
134
|
+
captureSource: "preserved",
|
|
135
|
+
retention: "provider-managed",
|
|
136
|
+
restoreMount: "new-resource"
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
vercel: {
|
|
141
|
+
id: "vercel",
|
|
142
|
+
label: "Vercel",
|
|
143
|
+
credentialFields: [
|
|
144
|
+
{ key: "access_token", label: "Access Token", secret: true },
|
|
145
|
+
{ key: "team_id", label: "Team ID" },
|
|
146
|
+
{ key: "project_id", label: "Project ID" }
|
|
147
|
+
],
|
|
148
|
+
metadata: {
|
|
149
|
+
driverRunsIn: ["node"],
|
|
150
|
+
hostStopBehavior: "terminates-host",
|
|
151
|
+
hostResumeBehavior: "replacement-host",
|
|
152
|
+
targetAccess: "relay",
|
|
153
|
+
secretBrokering: "native",
|
|
154
|
+
// Vercel's sandbox firewall takes a hostname allow list (or "deny-all"),
|
|
155
|
+
// never CIDRs.
|
|
156
|
+
egressControl: "hosts",
|
|
157
|
+
persistence: {
|
|
158
|
+
resume: "replacement-restore",
|
|
159
|
+
capture: "filesystem",
|
|
160
|
+
clone: false,
|
|
161
|
+
captureSource: "stopped",
|
|
162
|
+
retention: "explicit",
|
|
163
|
+
restoreMount: "new-resource"
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
cloudflare: {
|
|
168
|
+
id: "cloudflare",
|
|
169
|
+
label: "Cloudflare",
|
|
170
|
+
credentialFields: [
|
|
171
|
+
{ key: "api_token", label: "API Token", secret: true },
|
|
172
|
+
{ key: "worker_url", label: "Worker URL" }
|
|
173
|
+
],
|
|
174
|
+
metadata: {
|
|
175
|
+
driverRunsIn: ["worker"],
|
|
176
|
+
hostStopBehavior: "not-supported",
|
|
177
|
+
hostResumeBehavior: "same-host",
|
|
178
|
+
targetAccess: "relay",
|
|
179
|
+
secretBrokering: "proxy",
|
|
180
|
+
// The Cloudflare Sandbox Worker exposes no egress filter: its broker
|
|
181
|
+
// (drivers/cloudflare-egress.ts) is an OPT-IN proxy the sandbox chooses
|
|
182
|
+
// to route brokered-credential requests through, not a boundary. The
|
|
183
|
+
// driver therefore drops `net` entirely — which is exactly why this
|
|
184
|
+
// must be declared, so the manager refuses instead of provisioning a
|
|
185
|
+
// sandbox the caller believes is contained.
|
|
186
|
+
egressControl: "none",
|
|
187
|
+
persistence: {
|
|
188
|
+
resume: "replacement-restore",
|
|
189
|
+
capture: "directories",
|
|
190
|
+
clone: false,
|
|
191
|
+
captureSource: "preserved",
|
|
192
|
+
retention: "provider-managed",
|
|
193
|
+
restoreMount: "copy-on-write"
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
box: {
|
|
198
|
+
id: "box",
|
|
199
|
+
label: "Box",
|
|
200
|
+
credentialFields: [{ key: "api_key", label: "API Key", secret: true }],
|
|
201
|
+
metadata: {
|
|
202
|
+
driverRunsIn: ["node"],
|
|
203
|
+
hostStopBehavior: "suspends-host",
|
|
204
|
+
hostResumeBehavior: "same-host",
|
|
205
|
+
targetAccess: "relay",
|
|
206
|
+
secretBrokering: "none",
|
|
207
|
+
egressControl: "none",
|
|
208
|
+
persistence: {
|
|
209
|
+
resume: "same-sandbox",
|
|
210
|
+
capture: "none",
|
|
211
|
+
clone: false,
|
|
212
|
+
captureSource: "not-applicable",
|
|
213
|
+
retention: "provider-managed",
|
|
214
|
+
restoreMount: "same-resource"
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
docker: {
|
|
219
|
+
id: "docker",
|
|
220
|
+
label: "Docker",
|
|
221
|
+
credentialFields: [{ key: "image", label: "Image" }],
|
|
222
|
+
metadata: {
|
|
223
|
+
driverRunsIn: ["local"],
|
|
224
|
+
hostStopBehavior: "terminates-host",
|
|
225
|
+
hostResumeBehavior: "same-host",
|
|
226
|
+
targetAccess: "loopback",
|
|
227
|
+
secretBrokering: "none",
|
|
228
|
+
egressControl: "none",
|
|
229
|
+
persistence: {
|
|
230
|
+
resume: "replacement-restore",
|
|
231
|
+
capture: "same-resource",
|
|
232
|
+
clone: false,
|
|
233
|
+
captureSource: "preserved",
|
|
234
|
+
retention: "provider-managed",
|
|
235
|
+
restoreMount: "same-resource"
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
|
|
48
241
|
// src/drivers/vercel.ts
|
|
49
242
|
var DEFAULT_RUNTIME = "node22";
|
|
50
243
|
var DEFAULT_RUNTIME_COMMAND = "/usr/local/bin/workspace-runtime";
|
|
@@ -53,8 +246,9 @@ var DEFAULT_TIMEOUT_MS = 30 * 60 * 1e3;
|
|
|
53
246
|
var DEFAULT_SNAPSHOT_BUILD_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
54
247
|
var DEFAULT_OPERATION_TIMEOUT_MS = 6e4;
|
|
55
248
|
var DEFAULT_KEEP_ALIVE_MS = 5 * 60 * 1e3;
|
|
56
|
-
|
|
57
|
-
|
|
249
|
+
function runtimePackage(version) {
|
|
250
|
+
return `@claxedo/workspace-runtime@${version}`;
|
|
251
|
+
}
|
|
58
252
|
var SNAPSHOT_CACHE_TTL_MS = 60 * 60 * 1e3;
|
|
59
253
|
var snapshotCache = /* @__PURE__ */ new Map();
|
|
60
254
|
var snapshotCacheState = /* @__PURE__ */ new WeakMap();
|
|
@@ -74,8 +268,9 @@ function network(input) {
|
|
|
74
268
|
if (hosts.length === 0) return "deny-all";
|
|
75
269
|
return { allow: hosts };
|
|
76
270
|
}
|
|
77
|
-
function vercelBrokeredNetworkPolicy(secrets) {
|
|
271
|
+
function vercelBrokeredNetworkPolicy(secrets, base) {
|
|
78
272
|
const allow = {};
|
|
273
|
+
for (const [host, rules] of baseAllowEntries(base)) allow[host] = [...rules];
|
|
79
274
|
for (const secret of secrets) {
|
|
80
275
|
if (!secret.header) {
|
|
81
276
|
throw new Error(
|
|
@@ -87,7 +282,14 @@ function vercelBrokeredNetworkPolicy(secrets) {
|
|
|
87
282
|
(allow[host] ??= []).push({ transform: [{ headers: { [secret.header]: secret.value } }] });
|
|
88
283
|
}
|
|
89
284
|
}
|
|
90
|
-
|
|
285
|
+
const subnets = typeof base === "object" && base?.subnets ? { subnets: base.subnets } : {};
|
|
286
|
+
return { allow, ...subnets };
|
|
287
|
+
}
|
|
288
|
+
function baseAllowEntries(base) {
|
|
289
|
+
if (base === void 0 || base === "allow-all") return [["*", []]];
|
|
290
|
+
if (base === "deny-all") return [];
|
|
291
|
+
if (Array.isArray(base.allow)) return base.allow.map((host) => [host, []]);
|
|
292
|
+
return Object.entries(base.allow ?? {});
|
|
91
293
|
}
|
|
92
294
|
function createVercelSandboxDriver(options) {
|
|
93
295
|
const sandboxFactory = options.sandbox ?? Sandbox;
|
|
@@ -141,7 +343,8 @@ function createVercelSandboxDriver(options) {
|
|
|
141
343
|
}
|
|
142
344
|
async function ensureSnapshot() {
|
|
143
345
|
if (options.baseSnapshotId) return options.baseSnapshotId;
|
|
144
|
-
const
|
|
346
|
+
const expectedRuntimeVersion = workspaceRuntimeVersion();
|
|
347
|
+
const cacheKey = `${options.teamId}:${options.projectId}:${runtime}:${expectedRuntimeVersion}`;
|
|
145
348
|
const hit = cache.get(cacheKey);
|
|
146
349
|
const expiresAt = cacheState.expires.get(cacheKey);
|
|
147
350
|
if (hit && expiresAt === void 0) {
|
|
@@ -183,8 +386,8 @@ function createVercelSandboxDriver(options) {
|
|
|
183
386
|
`sudo chown $(whoami) ${shell(RUNTIME_DIR)}`,
|
|
184
387
|
`cd ${shell(RUNTIME_DIR)}`,
|
|
185
388
|
"npm init -y",
|
|
186
|
-
`npm install ${
|
|
187
|
-
`node -e "const fs=require('fs');const actual=JSON.parse(fs.readFileSync('${RUNTIME_DIR}/node_modules/@claxedo/workspace-runtime/package.json','utf8')).version;if(actual!=='${
|
|
389
|
+
`npm install ${runtimePackage(expectedRuntimeVersion)} --min-release-age=2`,
|
|
390
|
+
`node -e "const fs=require('fs');const actual=JSON.parse(fs.readFileSync('${RUNTIME_DIR}/node_modules/@claxedo/workspace-runtime/package.json','utf8')).version;if(actual!=='${expectedRuntimeVersion}'){throw new Error('workspace-runtime snapshot package version mismatch: expected ${expectedRuntimeVersion}, got '+actual)}"`,
|
|
188
391
|
"test -x node_modules/.bin/workspace-runtime",
|
|
189
392
|
"test -x node_modules/.bin/claude-agent-acp",
|
|
190
393
|
"test -x node_modules/.bin/codex-acp",
|
|
@@ -249,20 +452,21 @@ function createVercelSandboxDriver(options) {
|
|
|
249
452
|
...bootEnv(input, hostId),
|
|
250
453
|
...await options.env?.(input, { id: hostId })
|
|
251
454
|
};
|
|
455
|
+
const createPolicy = network(input);
|
|
252
456
|
const sandbox = await sandboxFactory.create({
|
|
253
457
|
...credentials(),
|
|
254
458
|
source: await source(input),
|
|
255
459
|
ports: [runtimePort],
|
|
256
460
|
timeout: timeoutMs,
|
|
257
461
|
env,
|
|
258
|
-
...
|
|
462
|
+
...createPolicy ? { networkPolicy: createPolicy } : {}
|
|
259
463
|
}).catch((err) => {
|
|
260
464
|
if (transientDriverError(err)) return void 0;
|
|
261
465
|
throw err;
|
|
262
466
|
});
|
|
263
467
|
if (!sandbox) return { provisioning: true, retryAfterMs: 2e3 };
|
|
264
468
|
if (input.secrets?.length) {
|
|
265
|
-
await sandbox.updateNetworkPolicy(vercelBrokeredNetworkPolicy(input.secrets), {
|
|
469
|
+
await sandbox.updateNetworkPolicy(vercelBrokeredNetworkPolicy(input.secrets, createPolicy), {
|
|
266
470
|
signal: AbortSignal.timeout(operationTimeoutMs)
|
|
267
471
|
});
|
|
268
472
|
}
|
|
@@ -282,7 +486,9 @@ function createVercelSandboxDriver(options) {
|
|
|
282
486
|
hostStopBehavior: "terminates-host",
|
|
283
487
|
hostResumeBehavior: "replacement-host",
|
|
284
488
|
targetAccess: "relay",
|
|
285
|
-
secretBrokering: "native"
|
|
489
|
+
secretBrokering: "native",
|
|
490
|
+
egressControl: "hosts",
|
|
491
|
+
persistence: sandboxDriverCatalog.vercel.metadata.persistence
|
|
286
492
|
},
|
|
287
493
|
ensureHost,
|
|
288
494
|
resumeHost: (input) => ensureHost(input.ensure),
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The egress allowlist a HOSTED, multi-tenant sandbox is provisioned with.
|
|
3
|
+
*
|
|
4
|
+
* A hosted sandbox runs agent-authored code on the operator's infrastructure,
|
|
5
|
+
* next to other tenants' sandboxes, holding a checkout of the user's private
|
|
6
|
+
* repository and (via `SandboxBrokeredSecret`) the ability to spend the
|
|
7
|
+
* operator's model credits. The default for that shape of workload is
|
|
8
|
+
* restrictive, not allow-all: the interesting attack is not "the agent
|
|
9
|
+
* downloads a package", it is "the agent POSTs the repository to a host the
|
|
10
|
+
* attacker controls". An allowlist is what makes that fail.
|
|
11
|
+
*
|
|
12
|
+
* Every entry below has to earn its place, so the list is assembled from three
|
|
13
|
+
* sources of truth rather than from taste:
|
|
14
|
+
*
|
|
15
|
+
* 1. DERIVED PER REQUEST — the control plane the sandbox reports back to and
|
|
16
|
+
* the git host it clones from. These differ per deployment and per
|
|
17
|
+
* workspace, so they are inputs, not constants.
|
|
18
|
+
* 2. MODEL PROVIDERS — the endpoints an agent harness must reach to do any
|
|
19
|
+
* work at all. Taken from the harness→provider mapping the credential
|
|
20
|
+
* layer already maintains (`PROVIDER_TO_GROUP` /`DEFAULT_ALLOWLIST` in
|
|
21
|
+
* claxedo-server's `network/types.ts`), narrowed to the API hostnames.
|
|
22
|
+
* 3. PACKAGE REGISTRIES — an agent that cannot install a dependency cannot
|
|
23
|
+
* edit a real repository. Same source, again narrowed.
|
|
24
|
+
*
|
|
25
|
+
* What is deliberately NOT here, despite being in that same default list:
|
|
26
|
+
* general-purpose object storage and CDN wildcards (`*.workers.dev`,
|
|
27
|
+
* `r2.cloudflarestorage.com`, `storage.googleapis.com`,
|
|
28
|
+
* `*.blob.core.windows.net`, `*.amazonaws.com`). Every one of those is a
|
|
29
|
+
* bucket an attacker can create in their own account, so allowlisting them
|
|
30
|
+
* hands back exactly the exfiltration channel the policy exists to close. A
|
|
31
|
+
* deployment that genuinely needs one adds it through `extraHosts` as an
|
|
32
|
+
* explicit, auditable decision.
|
|
33
|
+
*
|
|
34
|
+
* This module is data + string handling only, so it runs unchanged inside the
|
|
35
|
+
* hosted Worker (no DNS, no node builtins). That matters: name-based policy is
|
|
36
|
+
* the only kind the Worker can compute, which is why `egressControl` treats
|
|
37
|
+
* name filtering as the capability that qualifies a driver for hosted use.
|
|
38
|
+
*/
|
|
39
|
+
import type { SandboxNetworkPolicy, SandboxSource } from ".";
|
|
40
|
+
/**
|
|
41
|
+
* Model-provider endpoints. Derived from the harness→provider groups in
|
|
42
|
+
* claxedo-server `network/types.ts` (`PROVIDER_TO_GROUP`): claude-acp and
|
|
43
|
+
* claude-sdk → anthropic, codex-acp/codex-app-server/cursor-acp → openai,
|
|
44
|
+
* google → google_ai; plus the aggregator and catalog endpoints the engine
|
|
45
|
+
* itself fetches at startup.
|
|
46
|
+
*/
|
|
47
|
+
export declare const SANDBOX_MODEL_PROVIDER_HOSTS: readonly ["api.anthropic.com", "api.openai.com", "generativelanguage.googleapis.com", "openrouter.ai", "models.dev", "opencode.ai"];
|
|
48
|
+
/**
|
|
49
|
+
* Package registries. Narrowed from the npm / pypi / rust / golang groups of
|
|
50
|
+
* `DEFAULT_ALLOWLIST` to the hostnames a package install actually contacts —
|
|
51
|
+
* the wildcard CDN entries in those groups are excluded on purpose (see the
|
|
52
|
+
* module comment).
|
|
53
|
+
*/
|
|
54
|
+
export declare const SANDBOX_PACKAGE_REGISTRY_HOSTS: readonly ["registry.npmjs.org", "nodejs.org", "pypi.org", "files.pythonhosted.org", "crates.io", "static.crates.io", "index.crates.io", "proxy.golang.org", "sum.golang.org"];
|
|
55
|
+
/** The deployment-independent floor: providers + registries. */
|
|
56
|
+
export declare const SANDBOX_BASELINE_EGRESS_HOSTS: readonly ["api.anthropic.com", "api.openai.com", "generativelanguage.googleapis.com", "openrouter.ai", "models.dev", "opencode.ai", "registry.npmjs.org", "nodejs.org", "pypi.org", "files.pythonhosted.org", "crates.io", "static.crates.io", "index.crates.io", "proxy.golang.org", "sum.golang.org"];
|
|
57
|
+
/**
|
|
58
|
+
* The git host this workspace clones from.
|
|
59
|
+
*
|
|
60
|
+
* Scoped to the ONE repository host in the request rather than to a forge
|
|
61
|
+
* allowlist, matching how `authenticatedGitHubCloneSource` scopes the clone
|
|
62
|
+
* credential it brokers (`hosts: ["github.com"]`) — the credential and the
|
|
63
|
+
* network policy should describe the same reachable surface.
|
|
64
|
+
*/
|
|
65
|
+
export declare function sandboxSourceHost(source: SandboxSource | undefined): string | undefined;
|
|
66
|
+
export type HostedSandboxNetworkPolicyInput = {
|
|
67
|
+
/**
|
|
68
|
+
* Control-plane and relay endpoints the runtime reports back to: the relay
|
|
69
|
+
* it tunnels through and fetches `WORKSPACE_RUNTIME_RELAY_JWKS_URL` from,
|
|
70
|
+
* and the control-plane origin serving `WORKSPACE_RUNTIME_MANAGEMENT_JWKS_URL`
|
|
71
|
+
* plus register/heartbeat. URLs or bare hostnames; blanks are ignored.
|
|
72
|
+
*/
|
|
73
|
+
controlPlane: Array<string | undefined>;
|
|
74
|
+
/** Workspace source — its git host is allowed, nothing else on that forge. */
|
|
75
|
+
source?: SandboxSource;
|
|
76
|
+
/** Operator escape hatch for deployment-specific hosts. */
|
|
77
|
+
extraHosts?: readonly string[];
|
|
78
|
+
};
|
|
79
|
+
/**
|
|
80
|
+
* Build the restricted policy for a hosted sandbox.
|
|
81
|
+
*
|
|
82
|
+
* Always `mode: "restricted"`. There is no input that makes this return
|
|
83
|
+
* allow-all — a hosted sandbox with an empty or unresolvable control-plane URL
|
|
84
|
+
* is a misconfiguration, and answering it with "then allow everything" is how
|
|
85
|
+
* the gap this closes came to exist in the first place.
|
|
86
|
+
*/
|
|
87
|
+
export declare function hostedSandboxNetworkPolicy(input: HostedSandboxNetworkPolicyInput): SandboxNetworkPolicy;
|