@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
|
@@ -0,0 +1,1029 @@
|
|
|
1
|
+
// src/drivers/local-brokering.ts
|
|
2
|
+
import { execFileSync, spawn } from "node:child_process";
|
|
3
|
+
import { closeSync, mkdtempSync, openSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
|
|
4
|
+
import fs from "node:fs/promises";
|
|
5
|
+
import { createServer } from "node:http";
|
|
6
|
+
import { createServer as createHttpsServer } from "node:https";
|
|
7
|
+
import net from "node:net";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
|
|
10
|
+
// ../agent-runtime-contract/dist/index.mjs
|
|
11
|
+
function isRecord(value) {
|
|
12
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
13
|
+
}
|
|
14
|
+
function asRecord(value) {
|
|
15
|
+
return isRecord(value) ? value : void 0;
|
|
16
|
+
}
|
|
17
|
+
var CLAXEDO_TOOL_NAMES = [
|
|
18
|
+
"task_list",
|
|
19
|
+
"task_get",
|
|
20
|
+
"task_create",
|
|
21
|
+
"task_edit",
|
|
22
|
+
"task_start",
|
|
23
|
+
"session_create",
|
|
24
|
+
"sessions_list",
|
|
25
|
+
"session_get",
|
|
26
|
+
"session_transcript",
|
|
27
|
+
"session_send",
|
|
28
|
+
"session_cancel_turn",
|
|
29
|
+
"session_handoff",
|
|
30
|
+
"session_rename",
|
|
31
|
+
"session_delete",
|
|
32
|
+
"session_changes",
|
|
33
|
+
"documents_list",
|
|
34
|
+
"documents_open",
|
|
35
|
+
"subagent_capabilities",
|
|
36
|
+
"create_subagent",
|
|
37
|
+
"subagent_status",
|
|
38
|
+
"subagent_list",
|
|
39
|
+
"subagent_cancel",
|
|
40
|
+
"sessions_board",
|
|
41
|
+
"permission_reply",
|
|
42
|
+
"question_reply",
|
|
43
|
+
"question_reject",
|
|
44
|
+
"wait_for_attention",
|
|
45
|
+
"workspaces_list",
|
|
46
|
+
"workspace_status",
|
|
47
|
+
"workspace_checkpoint",
|
|
48
|
+
"workspace_restore",
|
|
49
|
+
"workspace_lifecycle",
|
|
50
|
+
"app_plugin_create",
|
|
51
|
+
"app_plugin_check",
|
|
52
|
+
"app_plugin_add",
|
|
53
|
+
"app_plugin_guide",
|
|
54
|
+
"ui_show",
|
|
55
|
+
"ui_guide"
|
|
56
|
+
];
|
|
57
|
+
var CLAXEDO_TOOL_NAME_SET = new Set(CLAXEDO_TOOL_NAMES);
|
|
58
|
+
var CREDENTIAL_BROKER_ERRORS = {
|
|
59
|
+
binding_route_required: {
|
|
60
|
+
fault: "harness",
|
|
61
|
+
message: "The credential broker serves binding paths only"
|
|
62
|
+
},
|
|
63
|
+
runtime_token_required: {
|
|
64
|
+
fault: "harness",
|
|
65
|
+
message: "The request carried no runtime token, or two that disagree"
|
|
66
|
+
},
|
|
67
|
+
runtime_token_invalid: {
|
|
68
|
+
fault: "credential",
|
|
69
|
+
message: "The runtime token could not be verified"
|
|
70
|
+
},
|
|
71
|
+
binding_not_permitted: {
|
|
72
|
+
fault: "harness",
|
|
73
|
+
message: "This runtime token does not name that binding"
|
|
74
|
+
},
|
|
75
|
+
binding_unavailable: {
|
|
76
|
+
fault: "credential",
|
|
77
|
+
message: "The binding names no account this runtime can spend"
|
|
78
|
+
},
|
|
79
|
+
credential_unavailable: {
|
|
80
|
+
fault: "credential",
|
|
81
|
+
message: "The account behind this binding has no readable value"
|
|
82
|
+
},
|
|
83
|
+
binding_destination_invalid: {
|
|
84
|
+
fault: "harness",
|
|
85
|
+
message: "The binding's destination is not a usable vendor origin"
|
|
86
|
+
},
|
|
87
|
+
binding_injection_invalid: {
|
|
88
|
+
fault: "harness",
|
|
89
|
+
message: "The binding names a header the broker cannot set"
|
|
90
|
+
},
|
|
91
|
+
request_outside_policy: {
|
|
92
|
+
fault: "harness",
|
|
93
|
+
message: "The request is outside the routes this binding allows"
|
|
94
|
+
},
|
|
95
|
+
upstream_unavailable: {
|
|
96
|
+
fault: "model",
|
|
97
|
+
message: "The vendor could not be reached"
|
|
98
|
+
},
|
|
99
|
+
upstream_redirect_refused: {
|
|
100
|
+
fault: "model",
|
|
101
|
+
message: "The vendor answered with a redirect, which the broker does not follow"
|
|
102
|
+
},
|
|
103
|
+
broker_authority_unavailable: {
|
|
104
|
+
fault: "harness",
|
|
105
|
+
message: "The credential authority could not be reached"
|
|
106
|
+
},
|
|
107
|
+
loopback_required: {
|
|
108
|
+
fault: "harness",
|
|
109
|
+
message: "The credential broker answers loopback callers only"
|
|
110
|
+
},
|
|
111
|
+
runtime_token_rate_limited: {
|
|
112
|
+
fault: "harness",
|
|
113
|
+
message: "Too many requests without a verifiable runtime token from this client"
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
var codeInProse = new RegExp(`\\b(${Object.keys(CREDENTIAL_BROKER_ERRORS).join("|")})\\b`);
|
|
117
|
+
var AGENT_PRESENTATION_EVENT_TYPE_REGISTRY = {
|
|
118
|
+
"message.updated": true,
|
|
119
|
+
"message.removed": true,
|
|
120
|
+
"message.part.updated": true,
|
|
121
|
+
"message.part.removed": true,
|
|
122
|
+
"message.part.delta": true,
|
|
123
|
+
"message.part.retracted": true,
|
|
124
|
+
"message.completed": true,
|
|
125
|
+
"permission.asked": true,
|
|
126
|
+
"permission.replied": true,
|
|
127
|
+
"permission.expired": true,
|
|
128
|
+
"question.asked": true,
|
|
129
|
+
"question.replied": true,
|
|
130
|
+
"question.rejected": true,
|
|
131
|
+
"question.expired": true,
|
|
132
|
+
"todo.updated": true,
|
|
133
|
+
"session.status": true,
|
|
134
|
+
"session.idle": true,
|
|
135
|
+
"session.error": true,
|
|
136
|
+
"session.updated": true,
|
|
137
|
+
"session.diff": true,
|
|
138
|
+
"session.compacted": true,
|
|
139
|
+
"session.agent": true,
|
|
140
|
+
"session.config": true,
|
|
141
|
+
"session.commands": true,
|
|
142
|
+
"session.usage": true,
|
|
143
|
+
"subagent.updated": true,
|
|
144
|
+
"goal.updated": true,
|
|
145
|
+
"goal.cleared": true,
|
|
146
|
+
"runtime.diagnostic": true,
|
|
147
|
+
"server.connected": true,
|
|
148
|
+
"server.heartbeat": true,
|
|
149
|
+
"session.deleted": true,
|
|
150
|
+
"harness.health": true,
|
|
151
|
+
"session.background-work": true
|
|
152
|
+
};
|
|
153
|
+
function isAgentPresentationEventType(value) {
|
|
154
|
+
return Object.hasOwn(AGENT_PRESENTATION_EVENT_TYPE_REGISTRY, value);
|
|
155
|
+
}
|
|
156
|
+
var AGENT_PRESENTATION_EVENT_TYPES = Object.keys(AGENT_PRESENTATION_EVENT_TYPE_REGISTRY).filter(isAgentPresentationEventType);
|
|
157
|
+
var mode = (id, name, description, level) => ({
|
|
158
|
+
id,
|
|
159
|
+
name,
|
|
160
|
+
description,
|
|
161
|
+
...level ? { level } : {}
|
|
162
|
+
});
|
|
163
|
+
var CLAUDE_PERMISSION_MODES = {
|
|
164
|
+
modes: [
|
|
165
|
+
mode("default", "Default", "Standard behavior, prompts for dangerous operations", "ask"),
|
|
166
|
+
mode("acceptEdits", "Accept edits", "Auto-accept file edit operations"),
|
|
167
|
+
mode("auto", "Auto", "Use a model classifier to approve/deny permission prompts", "auto"),
|
|
168
|
+
mode("plan", "Plan", "Planning mode, no actual tool execution"),
|
|
169
|
+
mode("dontAsk", "Don't ask", "Don't prompt for permissions, deny if not pre-approved"),
|
|
170
|
+
mode("bypassPermissions", "Bypass permissions", "Bypass all permission checks", "full")
|
|
171
|
+
],
|
|
172
|
+
appliesFrom: "immediate",
|
|
173
|
+
defaultModeId: "auto"
|
|
174
|
+
};
|
|
175
|
+
var CURSOR_PERMISSION_MODES = {
|
|
176
|
+
modes: [
|
|
177
|
+
mode("review", "Review each call", "Run tool calls inside Cursor's sandbox and review them", "ask"),
|
|
178
|
+
mode("auto-review", "Auto-review", "Let Cursor's classifier approve tool calls inside the sandbox", "auto"),
|
|
179
|
+
mode("unsandboxed", "Unsandboxed", "Run tool calls directly, with no sandbox", "full")
|
|
180
|
+
],
|
|
181
|
+
appliesFrom: "next-turn"
|
|
182
|
+
};
|
|
183
|
+
var CODEX_PERMISSION_MODES = {
|
|
184
|
+
modes: [
|
|
185
|
+
mode("read-only", "Read only", "Never asks; the sandbox permits reads only, so writes fail", "ask"),
|
|
186
|
+
mode("workspace-write", "Workspace write", "Runs commands inside the workspace without asking; escalates outside it", "auto"),
|
|
187
|
+
mode("untrusted", "Untrusted", "Only trusted commands run without asking; anything else escalates"),
|
|
188
|
+
mode("full-access", "Full access", "Never asks; full filesystem and network access", "full")
|
|
189
|
+
],
|
|
190
|
+
appliesFrom: "next-turn",
|
|
191
|
+
defaultModeId: "workspace-write"
|
|
192
|
+
};
|
|
193
|
+
var PI_PERMISSION_MODES = {
|
|
194
|
+
modes: [
|
|
195
|
+
mode("ask", "Ask", "Ask before each write, edit, command and MCP tool call", "ask"),
|
|
196
|
+
mode("full", "Full access", "Run every tool call without asking", "full")
|
|
197
|
+
],
|
|
198
|
+
appliesFrom: "next-turn",
|
|
199
|
+
defaultModeId: "full"
|
|
200
|
+
};
|
|
201
|
+
var AGENT_HARNESS_DEFINITIONS = [
|
|
202
|
+
{
|
|
203
|
+
key: "claude",
|
|
204
|
+
id: "claude",
|
|
205
|
+
access: "native",
|
|
206
|
+
label: "Claude",
|
|
207
|
+
authEnv: "ANTHROPIC_API_KEY",
|
|
208
|
+
authSlot: "anthropic",
|
|
209
|
+
cloudHost: "workspace-runtime"
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
key: "codex",
|
|
213
|
+
id: "codex",
|
|
214
|
+
access: "native",
|
|
215
|
+
label: "Codex",
|
|
216
|
+
authEnv: "OPENAI_API_KEY",
|
|
217
|
+
authSlot: "openai",
|
|
218
|
+
cloudHost: "workspace-runtime"
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
key: "cursor",
|
|
222
|
+
id: "cursor",
|
|
223
|
+
access: "native",
|
|
224
|
+
label: "Cursor",
|
|
225
|
+
authEnv: "CURSOR_API_KEY",
|
|
226
|
+
authSlot: "cursor",
|
|
227
|
+
cloudHost: "workspace-runtime"
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
key: "pi",
|
|
231
|
+
id: "pi",
|
|
232
|
+
access: "native",
|
|
233
|
+
label: "Pi",
|
|
234
|
+
authEnv: null,
|
|
235
|
+
authSlot: null,
|
|
236
|
+
cloudHost: "session-host"
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
key: "opencode",
|
|
240
|
+
id: "opencode",
|
|
241
|
+
access: "native",
|
|
242
|
+
label: "OpenCode",
|
|
243
|
+
authEnv: null,
|
|
244
|
+
authSlot: null,
|
|
245
|
+
cloudHost: "workspace-runtime"
|
|
246
|
+
}
|
|
247
|
+
];
|
|
248
|
+
var AGENT_HARNESS_KEYS = AGENT_HARNESS_DEFINITIONS.map((item) => item.key);
|
|
249
|
+
var HARNESS_IDS = ["claude", "codex", "cursor"];
|
|
250
|
+
var HARNESS_TABLE = {
|
|
251
|
+
claude: {
|
|
252
|
+
label: "Claude Code",
|
|
253
|
+
vendor: "Anthropic",
|
|
254
|
+
providerIds: ["claude-sdk", "claude-acp", "anthropic"],
|
|
255
|
+
connectProvider: "claude-sdk",
|
|
256
|
+
vendorProvider: "anthropic",
|
|
257
|
+
machineLoginServes: ["claude-sdk", "claude-acp"],
|
|
258
|
+
permissionModes: CLAUDE_PERMISSION_MODES,
|
|
259
|
+
defaultModelId: "default"
|
|
260
|
+
},
|
|
261
|
+
codex: {
|
|
262
|
+
label: "Codex",
|
|
263
|
+
vendor: "OpenAI",
|
|
264
|
+
providerIds: ["codex-app-server", "openai"],
|
|
265
|
+
connectProvider: "codex-app-server",
|
|
266
|
+
vendorProvider: "openai",
|
|
267
|
+
machineLoginServes: ["codex-app-server", "openai"],
|
|
268
|
+
permissionModes: CODEX_PERMISSION_MODES
|
|
269
|
+
},
|
|
270
|
+
cursor: {
|
|
271
|
+
label: "Cursor",
|
|
272
|
+
vendor: "Cursor",
|
|
273
|
+
providerIds: ["cursor-sdk", "cursor-acp", "cursor"],
|
|
274
|
+
connectProvider: "cursor-sdk",
|
|
275
|
+
vendorProvider: "cursor",
|
|
276
|
+
machineLoginServes: ["cursor-acp"],
|
|
277
|
+
permissionModes: CURSOR_PERMISSION_MODES,
|
|
278
|
+
defaultModelId: "auto"
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
function harnessBindingIds(harness) {
|
|
282
|
+
const record2 = HARNESS_TABLE[harness];
|
|
283
|
+
return record2.providerIds.filter((id) => id !== record2.vendorProvider);
|
|
284
|
+
}
|
|
285
|
+
function vendorCredentialProviderIds(vendorProvider) {
|
|
286
|
+
const bindings = HARNESS_IDS.filter((harness) => HARNESS_TABLE[harness].vendorProvider === vendorProvider).flatMap((harness) => harnessBindingIds(harness));
|
|
287
|
+
return [.../* @__PURE__ */ new Set([vendorProvider, ...bindings])];
|
|
288
|
+
}
|
|
289
|
+
var credentialProviders = {
|
|
290
|
+
"openai-codex": ["codex-app-server"],
|
|
291
|
+
anthropic: vendorCredentialProviderIds("anthropic"),
|
|
292
|
+
openai: ["openai"],
|
|
293
|
+
openrouter: ["openrouter"],
|
|
294
|
+
google: ["google"],
|
|
295
|
+
groq: ["groq"],
|
|
296
|
+
xai: ["xai"]
|
|
297
|
+
};
|
|
298
|
+
var DEFAULT_RECOVERY_BUDGETS = {
|
|
299
|
+
ackMs: 2e3,
|
|
300
|
+
providerQueryMs: 5e3,
|
|
301
|
+
gracefulCancelMs: 1e4,
|
|
302
|
+
termGraceMs: 3e3,
|
|
303
|
+
killVerifyMs: 2e3,
|
|
304
|
+
reconcileMs: 3e4,
|
|
305
|
+
drainMs: 3e4
|
|
306
|
+
};
|
|
307
|
+
var RECOVERY_OPERATION_RETENTION_MS = DEFAULT_RECOVERY_BUDGETS.reconcileMs * 10;
|
|
308
|
+
var MCP_APP_MAX_BYTES = 2 * 1024 * 1024;
|
|
309
|
+
function fieldsNamed(value, keys) {
|
|
310
|
+
return Object.fromEntries(keys.flatMap((key) => Object.hasOwn(value, key) ? [[key, value[key]]] : []));
|
|
311
|
+
}
|
|
312
|
+
var metadataNamed = (...keys) => (metadata) => fieldsNamed(metadata, keys);
|
|
313
|
+
var DIFF_COUNTS = ["file", "filePath", "relativePath", "type", "movePath", "additions", "deletions"];
|
|
314
|
+
function diffCounts(value) {
|
|
315
|
+
const record2 = asRecord(value);
|
|
316
|
+
return record2 && fieldsNamed(record2, DIFF_COUNTS);
|
|
317
|
+
}
|
|
318
|
+
var HEADERS = {
|
|
319
|
+
read: { input: ["filePath", "offset", "limit"], metadata: metadataNamed("loaded") },
|
|
320
|
+
list: { input: ["path"] },
|
|
321
|
+
glob: { input: ["path", "pattern"] },
|
|
322
|
+
grep: { input: ["path", "pattern", "include"] },
|
|
323
|
+
webfetch: { input: ["url"] },
|
|
324
|
+
websearch: { input: ["query"], metadata: metadataNamed("provider") },
|
|
325
|
+
bash: { input: ["command", "description"], metadata: metadataNamed("command", "exitCode") },
|
|
326
|
+
edit: {
|
|
327
|
+
input: ["filePath", "path"],
|
|
328
|
+
metadata: (metadata) => {
|
|
329
|
+
const filediff = diffCounts(metadata.filediff);
|
|
330
|
+
return filediff ? { filediff } : {};
|
|
331
|
+
}
|
|
332
|
+
},
|
|
333
|
+
write: { input: ["filePath", "path"] },
|
|
334
|
+
apply_patch: {
|
|
335
|
+
input: [],
|
|
336
|
+
metadata: (metadata) => Array.isArray(metadata.files) ? { files: metadata.files.flatMap((file) => diffCounts(file) ?? []) } : {}
|
|
337
|
+
},
|
|
338
|
+
todowrite: { input: [] },
|
|
339
|
+
skill: { input: ["name", "skill"] },
|
|
340
|
+
enterplanmode: { input: [] }
|
|
341
|
+
};
|
|
342
|
+
var TURN_PAGE_BYTE_CAP = 256 * 1024;
|
|
343
|
+
var encoder = new TextEncoder();
|
|
344
|
+
var AGENT_RUNTIME_EVENT_TYPE_REGISTRY = {
|
|
345
|
+
"text-delta": true,
|
|
346
|
+
"thinking-delta": true,
|
|
347
|
+
"tool-start": true,
|
|
348
|
+
"tool-input": true,
|
|
349
|
+
"tool-status": true,
|
|
350
|
+
"tool-content": true,
|
|
351
|
+
"tool-output": true,
|
|
352
|
+
"tool-error": true,
|
|
353
|
+
"file-diff": true,
|
|
354
|
+
"step-start": true,
|
|
355
|
+
"input-incorporated": true,
|
|
356
|
+
"permission-request": true,
|
|
357
|
+
question: true,
|
|
358
|
+
"question-answered": true,
|
|
359
|
+
"proposed-plan-delta": true,
|
|
360
|
+
"proposed-plan-complete": true,
|
|
361
|
+
"todo-update": true,
|
|
362
|
+
"session-status": true,
|
|
363
|
+
"session-compaction": true,
|
|
364
|
+
"harness-notice": true,
|
|
365
|
+
"agent-message": true,
|
|
366
|
+
"session-retry": true,
|
|
367
|
+
"response-start": true,
|
|
368
|
+
"response-retracted": true,
|
|
369
|
+
"conversation-reset": true,
|
|
370
|
+
"auth-status": true,
|
|
371
|
+
"rate-limit": true,
|
|
372
|
+
"mcp-server-status": true,
|
|
373
|
+
"goal-updated": true,
|
|
374
|
+
"goal-cleared": true,
|
|
375
|
+
"subagent-updated": true,
|
|
376
|
+
finish: true,
|
|
377
|
+
cancelled: true,
|
|
378
|
+
error: true,
|
|
379
|
+
"image-delta": true,
|
|
380
|
+
"audio-delta": true,
|
|
381
|
+
"resource-link-delta": true,
|
|
382
|
+
"thinking-audio-delta": true,
|
|
383
|
+
"thinking-resource-link-delta": true,
|
|
384
|
+
"resource-delta": true,
|
|
385
|
+
"tool-location": true,
|
|
386
|
+
"tool-terminal": true,
|
|
387
|
+
"available-commands-update": true,
|
|
388
|
+
"session-agent": true,
|
|
389
|
+
"config-update": true,
|
|
390
|
+
"session-info": true,
|
|
391
|
+
"session-title": true,
|
|
392
|
+
"background-work": true,
|
|
393
|
+
usage: true,
|
|
394
|
+
diagnostic: true
|
|
395
|
+
};
|
|
396
|
+
function isAgentRuntimeEventType(value) {
|
|
397
|
+
return Object.hasOwn(AGENT_RUNTIME_EVENT_TYPE_REGISTRY, value);
|
|
398
|
+
}
|
|
399
|
+
var AGENT_RUNTIME_EVENT_TYPES = Object.keys(AGENT_RUNTIME_EVENT_TYPE_REGISTRY).filter(isAgentRuntimeEventType);
|
|
400
|
+
var D1_BOOKMARK_COOKIE_PREFIX = "__Host-claxedo.d1-";
|
|
401
|
+
var CONTROL_COOKIE = `${D1_BOOKMARK_COOKIE_PREFIX}control=`;
|
|
402
|
+
var EVENT_STREAM_HEARTBEAT_MS = 1e4;
|
|
403
|
+
var EVENT_STREAM_STALL_TIMEOUT_MS = 4 * EVENT_STREAM_HEARTBEAT_MS;
|
|
404
|
+
|
|
405
|
+
// ../egress-broker/dist/token.js
|
|
406
|
+
var MAX_TOKEN_LIFETIME_MS = 60 * 60 * 1e3;
|
|
407
|
+
|
|
408
|
+
// ../egress-broker/dist/native-substitution.js
|
|
409
|
+
function registeredValue(secret) {
|
|
410
|
+
return secret.scheme ? `${secret.scheme} ${secret.value}` : secret.value;
|
|
411
|
+
}
|
|
412
|
+
function substituteNativeSecrets(target, method, incoming, secrets) {
|
|
413
|
+
const headers = new Headers(incoming);
|
|
414
|
+
const presented = [...headers].filter(([, value]) => value.includes("claxedo-broker:"));
|
|
415
|
+
for (const [, value] of presented) {
|
|
416
|
+
if (!secrets.some((secret) => value.includes(`claxedo-broker:${secret.name}`))) {
|
|
417
|
+
throw new Error("brokered_secret_withdrawn");
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
for (const secret of secrets) {
|
|
421
|
+
const slot = secret.header;
|
|
422
|
+
if (!slot)
|
|
423
|
+
continue;
|
|
424
|
+
const placeholder = `claxedo-broker:${secret.name}`;
|
|
425
|
+
const sent = headers.get(slot);
|
|
426
|
+
if (sent !== placeholder && sent !== `${secret.scheme ?? "Bearer"} ${placeholder}`)
|
|
427
|
+
continue;
|
|
428
|
+
if (!secret.hosts.includes(target.host) || secret.methods && !secret.methods.includes(method) || secret.pathPrefixes && !secret.pathPrefixes.some((prefix) => target.pathname === prefix || target.pathname.startsWith(prefix.endsWith("/") ? prefix : `${prefix}/`))) {
|
|
429
|
+
throw new Error("brokered_secret_destination_refused");
|
|
430
|
+
}
|
|
431
|
+
headers.set(slot, registeredValue(secret));
|
|
432
|
+
}
|
|
433
|
+
if ([...headers].some(([, value]) => value.includes("claxedo-broker:"))) {
|
|
434
|
+
throw new Error("brokered_secret_slot_refused");
|
|
435
|
+
}
|
|
436
|
+
return headers;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// ../egress-broker/dist/mount.js
|
|
440
|
+
var PREFIX = "/bindings/";
|
|
441
|
+
var BROKER_ROUTE_PATTERN = `${PREFIX}*`;
|
|
442
|
+
|
|
443
|
+
// src/driver-billing.ts
|
|
444
|
+
var HOUR_SECONDS = 3600;
|
|
445
|
+
var boatBilling = {
|
|
446
|
+
source: { url: "https://docs.boat.dev/pricing", readOn: "2026-10-12" },
|
|
447
|
+
meter: "Boat bills machine time per second while a sandbox runs, at an hourly rate per size; a stopped sandbox costs nothing.",
|
|
448
|
+
rates: {
|
|
449
|
+
small: { size: "small", vcpu: 2, memoryGb: 4, usdPerHour: 0.018 },
|
|
450
|
+
default: { size: "default", vcpu: 4, memoryGb: 8, usdPerHour: 0.036 },
|
|
451
|
+
large: { size: "large", vcpu: 8, memoryGb: 16, usdPerHour: 0.072 }
|
|
452
|
+
}
|
|
453
|
+
};
|
|
454
|
+
var CLOUDFLARE_USD_PER_GIB_SECOND_MEMORY = 25e-7;
|
|
455
|
+
var CLOUDFLARE_USD_PER_GB_SECOND_DISK = 7e-8;
|
|
456
|
+
var CLOUDFLARE_USD_PER_VCPU_SECOND_ACTIVE = 2e-5;
|
|
457
|
+
var CLOUDFLARE_STANDARD_1 = { size: "standard-1", vcpu: 0.5, memoryGb: 4, diskGb: 8 };
|
|
458
|
+
var cloudflareBilling = {
|
|
459
|
+
source: { url: "https://developers.cloudflare.com/containers/pricing/", readOn: "2026-10-12" },
|
|
460
|
+
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.",
|
|
461
|
+
rates: {
|
|
462
|
+
default: {
|
|
463
|
+
size: CLOUDFLARE_STANDARD_1.size,
|
|
464
|
+
vcpu: CLOUDFLARE_STANDARD_1.vcpu,
|
|
465
|
+
memoryGb: CLOUDFLARE_STANDARD_1.memoryGb,
|
|
466
|
+
usdPerHour: (CLOUDFLARE_STANDARD_1.memoryGb * CLOUDFLARE_USD_PER_GIB_SECOND_MEMORY + CLOUDFLARE_STANDARD_1.diskGb * CLOUDFLARE_USD_PER_GB_SECOND_DISK) * HOUR_SECONDS
|
|
467
|
+
}
|
|
468
|
+
},
|
|
469
|
+
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`
|
|
470
|
+
};
|
|
471
|
+
|
|
472
|
+
// ../sandbox-contract/dist/index.mjs
|
|
473
|
+
var sandboxDriverCredentialFields = {
|
|
474
|
+
modal: [
|
|
475
|
+
{ key: "token_id", label: "Token ID", secret: true },
|
|
476
|
+
{ key: "token_secret", label: "Token Secret", secret: true }
|
|
477
|
+
],
|
|
478
|
+
vercel: [
|
|
479
|
+
{ key: "access_token", label: "Access Token", secret: true },
|
|
480
|
+
{ key: "team_id", label: "Team ID" },
|
|
481
|
+
{ key: "project_id", label: "Project ID" }
|
|
482
|
+
],
|
|
483
|
+
cloudflare: [
|
|
484
|
+
{ key: "api_token", label: "API Token", secret: true },
|
|
485
|
+
{ key: "worker_url", label: "Worker URL" }
|
|
486
|
+
],
|
|
487
|
+
boat: [{ key: "api_key", label: "API Key", secret: true }],
|
|
488
|
+
docker: [{ key: "image", label: "Image" }]
|
|
489
|
+
};
|
|
490
|
+
var sandboxDriverLabels = {
|
|
491
|
+
modal: "Modal",
|
|
492
|
+
vercel: "Vercel",
|
|
493
|
+
cloudflare: "Cloudflare",
|
|
494
|
+
boat: "Boat",
|
|
495
|
+
docker: "Docker"
|
|
496
|
+
};
|
|
497
|
+
|
|
498
|
+
// src/driver-catalog.ts
|
|
499
|
+
var localBrokeringTestDriverCatalogEntry = {
|
|
500
|
+
id: "local-brokering-test",
|
|
501
|
+
label: "Local brokering test driver",
|
|
502
|
+
credentialFields: [],
|
|
503
|
+
runtimeNetwork: { controlPlane: "direct", relay: "local" },
|
|
504
|
+
runtimeEnv: "ensure",
|
|
505
|
+
preparedImage: false,
|
|
506
|
+
metadata: {
|
|
507
|
+
driverRunsIn: ["local"],
|
|
508
|
+
hostStopBehavior: "terminates-host",
|
|
509
|
+
hostResumeBehavior: "same-host",
|
|
510
|
+
targetAccess: "loopback",
|
|
511
|
+
secretBrokering: "native",
|
|
512
|
+
egressControl: "hosts",
|
|
513
|
+
persistence: {
|
|
514
|
+
resume: "same-sandbox",
|
|
515
|
+
capture: "none",
|
|
516
|
+
clone: false,
|
|
517
|
+
captureSource: "not-applicable",
|
|
518
|
+
retention: "provider-managed",
|
|
519
|
+
restoreMount: "same-resource"
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
};
|
|
523
|
+
var sandboxDriverCatalog = {
|
|
524
|
+
modal: {
|
|
525
|
+
id: "modal",
|
|
526
|
+
label: sandboxDriverLabels.modal,
|
|
527
|
+
credentialFields: sandboxDriverCredentialFields.modal,
|
|
528
|
+
runtimeNetwork: { controlPlane: "direct", relay: "configured" },
|
|
529
|
+
runtimeEnv: "callback",
|
|
530
|
+
preparedImage: true,
|
|
531
|
+
metadata: {
|
|
532
|
+
driverRunsIn: ["node"],
|
|
533
|
+
hostStopBehavior: "terminates-host",
|
|
534
|
+
hostResumeBehavior: "replacement-host",
|
|
535
|
+
targetAccess: "relay",
|
|
536
|
+
secretBrokering: "none",
|
|
537
|
+
// The driver does not install Modal's domain allowlist or alpha sidecar.
|
|
538
|
+
egressControl: "none",
|
|
539
|
+
persistence: {
|
|
540
|
+
resume: "replacement-restore",
|
|
541
|
+
capture: "filesystem",
|
|
542
|
+
clone: false,
|
|
543
|
+
captureSource: "preserved",
|
|
544
|
+
retention: "provider-managed",
|
|
545
|
+
restoreMount: "new-resource"
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
},
|
|
549
|
+
vercel: {
|
|
550
|
+
id: "vercel",
|
|
551
|
+
label: sandboxDriverLabels.vercel,
|
|
552
|
+
credentialFields: sandboxDriverCredentialFields.vercel,
|
|
553
|
+
runtimeNetwork: { controlPlane: "direct", relay: "configured" },
|
|
554
|
+
runtimeEnv: "callback",
|
|
555
|
+
preparedImage: false,
|
|
556
|
+
metadata: {
|
|
557
|
+
driverRunsIn: ["node"],
|
|
558
|
+
hostStopBehavior: "terminates-host",
|
|
559
|
+
hostResumeBehavior: "replacement-host",
|
|
560
|
+
targetAccess: "relay",
|
|
561
|
+
secretBrokering: "native",
|
|
562
|
+
// The driver translates hosts only; the SDK's subnet rules are not wired to net.cidrs.
|
|
563
|
+
egressControl: "hosts",
|
|
564
|
+
persistence: {
|
|
565
|
+
resume: "replacement-restore",
|
|
566
|
+
capture: "filesystem",
|
|
567
|
+
clone: false,
|
|
568
|
+
captureSource: "stopped",
|
|
569
|
+
retention: "explicit",
|
|
570
|
+
restoreMount: "new-resource"
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
},
|
|
574
|
+
cloudflare: {
|
|
575
|
+
id: "cloudflare",
|
|
576
|
+
label: sandboxDriverLabels.cloudflare,
|
|
577
|
+
credentialFields: sandboxDriverCredentialFields.cloudflare,
|
|
578
|
+
runtimeNetwork: { controlPlane: "direct", relay: "configured" },
|
|
579
|
+
runtimeEnv: "callback",
|
|
580
|
+
preparedImage: false,
|
|
581
|
+
billing: cloudflareBilling,
|
|
582
|
+
metadata: {
|
|
583
|
+
driverRunsIn: ["worker"],
|
|
584
|
+
hostStopBehavior: "terminates-host",
|
|
585
|
+
hostResumeBehavior: "same-host",
|
|
586
|
+
hostWatchesIdle: true,
|
|
587
|
+
targetAccess: "relay",
|
|
588
|
+
secretBrokering: "native",
|
|
589
|
+
// Credential handlers do not restrict unrelated destinations.
|
|
590
|
+
egressControl: "none",
|
|
591
|
+
// A container snapshot holds the whole root filesystem; the platform keeps it 30 days past its last restore and offers no deletion.
|
|
592
|
+
persistence: {
|
|
593
|
+
resume: "replacement-restore",
|
|
594
|
+
capture: "filesystem",
|
|
595
|
+
clone: false,
|
|
596
|
+
captureSource: "preserved",
|
|
597
|
+
retention: "provider-managed",
|
|
598
|
+
restoreMount: "new-resource"
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
},
|
|
602
|
+
boat: {
|
|
603
|
+
id: "boat",
|
|
604
|
+
label: sandboxDriverLabels.boat,
|
|
605
|
+
credentialFields: sandboxDriverCredentialFields.boat,
|
|
606
|
+
runtimeNetwork: { controlPlane: "direct", relay: "configured" },
|
|
607
|
+
runtimeEnv: "callback",
|
|
608
|
+
preparedImage: true,
|
|
609
|
+
billing: boatBilling,
|
|
610
|
+
metadata: {
|
|
611
|
+
driverRunsIn: ["worker", "node"],
|
|
612
|
+
hostStopBehavior: "suspends-host",
|
|
613
|
+
hostResumeBehavior: "same-host",
|
|
614
|
+
targetAccess: "relay",
|
|
615
|
+
secretBrokering: "none",
|
|
616
|
+
egressControl: "none",
|
|
617
|
+
persistence: {
|
|
618
|
+
resume: "same-sandbox",
|
|
619
|
+
capture: "none",
|
|
620
|
+
clone: false,
|
|
621
|
+
captureSource: "not-applicable",
|
|
622
|
+
retention: "provider-managed",
|
|
623
|
+
restoreMount: "same-resource"
|
|
624
|
+
},
|
|
625
|
+
machineClasses: ["small", "default", "large"]
|
|
626
|
+
}
|
|
627
|
+
},
|
|
628
|
+
docker: {
|
|
629
|
+
id: "docker",
|
|
630
|
+
label: sandboxDriverLabels.docker,
|
|
631
|
+
credentialFields: sandboxDriverCredentialFields.docker,
|
|
632
|
+
runtimeNetwork: { controlPlane: "docker-host", relay: "configured" },
|
|
633
|
+
runtimeEnv: "callback",
|
|
634
|
+
preparedImage: true,
|
|
635
|
+
metadata: {
|
|
636
|
+
driverRunsIn: ["local"],
|
|
637
|
+
hostStopBehavior: "terminates-host",
|
|
638
|
+
hostResumeBehavior: "same-host",
|
|
639
|
+
targetAccess: "loopback",
|
|
640
|
+
secretBrokering: "none",
|
|
641
|
+
egressControl: "none",
|
|
642
|
+
persistence: {
|
|
643
|
+
resume: "replacement-restore",
|
|
644
|
+
capture: "same-resource",
|
|
645
|
+
clone: false,
|
|
646
|
+
captureSource: "preserved",
|
|
647
|
+
retention: "provider-managed",
|
|
648
|
+
restoreMount: "same-resource"
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
};
|
|
653
|
+
|
|
654
|
+
// src/runtime-env.ts
|
|
655
|
+
var WORKSPACE_RUNTIME_IDENTITY_ENV_KEYS = [
|
|
656
|
+
"WORKSPACE_RUNTIME_WORKSPACE_ID",
|
|
657
|
+
"WORKSPACE_RUNTIME_HOST_ID",
|
|
658
|
+
"WORKSPACE_RUNTIME_RELAY_WORKSPACE_IDS"
|
|
659
|
+
];
|
|
660
|
+
function workspaceRuntimeIdentityEnvConflicts(env) {
|
|
661
|
+
if (!env) return [];
|
|
662
|
+
return WORKSPACE_RUNTIME_IDENTITY_ENV_KEYS.filter((key) => env[key] !== void 0);
|
|
663
|
+
}
|
|
664
|
+
function assertWorkspaceRuntimeIdentityEnv(env) {
|
|
665
|
+
const conflicts = workspaceRuntimeIdentityEnvConflicts(env);
|
|
666
|
+
if (conflicts.length === 0) return;
|
|
667
|
+
throw new Error(
|
|
668
|
+
`sandbox env cannot set runtime identity ${conflicts.join(", ")}: the driver's placement owns the identity its target reports and the lease records`
|
|
669
|
+
);
|
|
670
|
+
}
|
|
671
|
+
var WORKSPACE_RUNTIME_PUBLIC_ENV_KEYS = /* @__PURE__ */ new Set([
|
|
672
|
+
...WORKSPACE_RUNTIME_IDENTITY_ENV_KEYS,
|
|
673
|
+
"WORKSPACE_RUNTIME_DIRECTORY",
|
|
674
|
+
"WORKSPACE_RUNTIME_PORT",
|
|
675
|
+
"WORKSPACE_RUNTIME_HOST",
|
|
676
|
+
"WORKSPACE_RUNTIME_SOURCE_KIND",
|
|
677
|
+
"WORKSPACE_RUNTIME_GIT_REPO_URL",
|
|
678
|
+
"WORKSPACE_RUNTIME_GIT_BRANCH",
|
|
679
|
+
"WORKSPACE_RUNTIME_NATIVE_HARNESS",
|
|
680
|
+
"WORKSPACE_RUNTIME_RELAY_JWKS_URL",
|
|
681
|
+
"WORKSPACE_RUNTIME_MANAGEMENT_JWKS_URL",
|
|
682
|
+
"WORKSPACE_RUNTIME_SESSION_AUTHORITY_URL"
|
|
683
|
+
]);
|
|
684
|
+
function workspaceRuntimeBootEnv(input) {
|
|
685
|
+
assertWorkspaceRuntimeIdentityEnv(input.env);
|
|
686
|
+
const env = {
|
|
687
|
+
...workspaceRuntimeTargetEnv(input),
|
|
688
|
+
...workspaceRuntimeSourceEnv(input),
|
|
689
|
+
...input.env
|
|
690
|
+
};
|
|
691
|
+
if (input.nativeHarness) env.WORKSPACE_RUNTIME_NATIVE_HARNESS = input.nativeHarness;
|
|
692
|
+
if (input.controlEnv?.relayJwksUrl) env.WORKSPACE_RUNTIME_RELAY_JWKS_URL = input.controlEnv.relayJwksUrl;
|
|
693
|
+
if (input.controlEnv?.relayVerifyPem) env.WORKSPACE_RUNTIME_RELAY_HOST_VERIFY_PEM = input.controlEnv.relayVerifyPem;
|
|
694
|
+
if (input.controlEnv?.managementJwksUrl) env.WORKSPACE_RUNTIME_MANAGEMENT_JWKS_URL = input.controlEnv.managementJwksUrl;
|
|
695
|
+
if (input.controlEnv?.sessionAuthorityUrl) env.WORKSPACE_RUNTIME_SESSION_AUTHORITY_URL = input.controlEnv.sessionAuthorityUrl;
|
|
696
|
+
return env;
|
|
697
|
+
}
|
|
698
|
+
function workspaceRuntimeTargetEnv(input) {
|
|
699
|
+
return {
|
|
700
|
+
WORKSPACE_RUNTIME_WORKSPACE_ID: input.workspaceId,
|
|
701
|
+
...input.hostId ? { WORKSPACE_RUNTIME_HOST_ID: input.hostId } : {},
|
|
702
|
+
WORKSPACE_RUNTIME_DIRECTORY: input.directory,
|
|
703
|
+
WORKSPACE_RUNTIME_PORT: String(input.port),
|
|
704
|
+
...input.host ? { WORKSPACE_RUNTIME_HOST: input.host } : {}
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
function workspaceRuntimeSourceEnv(input) {
|
|
708
|
+
if (!input.source || input.source.kind === "empty") return { WORKSPACE_RUNTIME_SOURCE_KIND: "empty" };
|
|
709
|
+
return {
|
|
710
|
+
WORKSPACE_RUNTIME_SOURCE_KIND: "git",
|
|
711
|
+
WORKSPACE_RUNTIME_GIT_REPO_URL: input.source.repoUrl,
|
|
712
|
+
...input.source.branch ? { WORKSPACE_RUNTIME_GIT_BRANCH: input.source.branch } : {}
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// ../claxedo-helpers/dist/deadline.mjs
|
|
717
|
+
async function withDeadlineSignal(timeoutMs, work, parent) {
|
|
718
|
+
const deadline = new AbortController();
|
|
719
|
+
const timer = setTimeout(() => deadline.abort(new DOMException("The operation timed out.", "TimeoutError")), timeoutMs);
|
|
720
|
+
try {
|
|
721
|
+
return await work(parent ? AbortSignal.any([parent, deadline.signal]) : deadline.signal);
|
|
722
|
+
} finally {
|
|
723
|
+
clearTimeout(timer);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// src/drivers/local-brokering.ts
|
|
728
|
+
function createTestAuthority(root) {
|
|
729
|
+
const privateDirectory = realpathSync(mkdtempSync(path.join(root, "local-broker-ca-")));
|
|
730
|
+
const certificate = path.join(root, `${path.basename(privateDirectory)}.pem`);
|
|
731
|
+
const key = path.join(privateDirectory, "ca.key");
|
|
732
|
+
execFileSync("openssl", ["req", "-x509", "-newkey", "rsa:2048", "-nodes", "-keyout", key, "-out", certificate, "-days", "1", "-subj", "/CN=Claxedo local broker test CA", "-addext", "basicConstraints=critical,CA:TRUE"], { stdio: "ignore" });
|
|
733
|
+
return { certificate, privateDirectory, key, leaves: /* @__PURE__ */ new Map() };
|
|
734
|
+
}
|
|
735
|
+
function certificateForHost(authority, hostname) {
|
|
736
|
+
const existing = authority.leaves.get(hostname);
|
|
737
|
+
if (existing) return existing;
|
|
738
|
+
const directory = mkdtempSync(path.join(authority.privateDirectory, "leaf-"));
|
|
739
|
+
const keyFile = path.join(directory, "leaf.key");
|
|
740
|
+
const requestFile = path.join(directory, "leaf.csr");
|
|
741
|
+
const certificateFile = path.join(directory, "leaf.pem");
|
|
742
|
+
const extensionFile = path.join(directory, "leaf.ext");
|
|
743
|
+
writeFileSync(extensionFile, `subjectAltName=DNS:${hostname}
|
|
744
|
+
`);
|
|
745
|
+
execFileSync("openssl", ["req", "-new", "-newkey", "rsa:2048", "-nodes", "-keyout", keyFile, "-out", requestFile, "-subj", `/CN=${hostname}`], { stdio: "ignore" });
|
|
746
|
+
execFileSync("openssl", ["x509", "-req", "-in", requestFile, "-CA", authority.certificate, "-CAkey", authority.key, "-CAcreateserial", "-out", certificateFile, "-days", "1", "-extfile", extensionFile], { stdio: "ignore" });
|
|
747
|
+
const leaf = { key: readFileSync(keyFile), cert: readFileSync(certificateFile) };
|
|
748
|
+
authority.leaves.set(hostname, leaf);
|
|
749
|
+
return leaf;
|
|
750
|
+
}
|
|
751
|
+
function listen(server) {
|
|
752
|
+
return new Promise((resolve, reject) => {
|
|
753
|
+
server.once("error", reject);
|
|
754
|
+
server.listen(0, "127.0.0.1", () => {
|
|
755
|
+
server.off("error", reject);
|
|
756
|
+
const address = server.address();
|
|
757
|
+
if (!address || typeof address === "string") return reject(new Error("local broker has no TCP port"));
|
|
758
|
+
resolve(address.port);
|
|
759
|
+
});
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
async function reserveLocalRuntimePort() {
|
|
763
|
+
const server = net.createServer();
|
|
764
|
+
const port = await new Promise((resolve, reject) => {
|
|
765
|
+
server.once("error", reject);
|
|
766
|
+
server.listen(0, "127.0.0.1", () => {
|
|
767
|
+
const address = server.address();
|
|
768
|
+
if (!address || typeof address === "string") reject(new Error("local runtime has no TCP port"));
|
|
769
|
+
else resolve(address.port);
|
|
770
|
+
});
|
|
771
|
+
});
|
|
772
|
+
await new Promise((resolve) => server.close(() => resolve()));
|
|
773
|
+
return port;
|
|
774
|
+
}
|
|
775
|
+
async function waitForLocalRuntimeHealth(url, child, logFile) {
|
|
776
|
+
const lastLines = async () => (await fs.readFile(logFile, "utf8")).trimEnd().split("\n").slice(-40).join("\n");
|
|
777
|
+
const until = Date.now() + 2e4;
|
|
778
|
+
while (Date.now() < until) {
|
|
779
|
+
if (child.exitCode !== null || child.signalCode !== null) {
|
|
780
|
+
const lines2 = await lastLines();
|
|
781
|
+
throw new Error(`local sandbox runtime exited: ${child.exitCode ?? child.signalCode}${lines2 ? `
|
|
782
|
+
${lines2}` : ""}`);
|
|
783
|
+
}
|
|
784
|
+
try {
|
|
785
|
+
if ((await withDeadlineSignal(1e3, (signal) => fetch(`${url}/global/health`, { signal }))).ok) return;
|
|
786
|
+
} catch {
|
|
787
|
+
}
|
|
788
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
789
|
+
}
|
|
790
|
+
const lines = await lastLines();
|
|
791
|
+
throw new Error(`local sandbox runtime did not become healthy${lines ? `
|
|
792
|
+
${lines}` : ""}`);
|
|
793
|
+
}
|
|
794
|
+
async function closeHost(host) {
|
|
795
|
+
const child = host.child;
|
|
796
|
+
if (child && child.exitCode === null) {
|
|
797
|
+
child.kill("SIGTERM");
|
|
798
|
+
await new Promise((resolve) => child.once("exit", () => resolve()));
|
|
799
|
+
}
|
|
800
|
+
for (const tunnel of host.tunnels) tunnel.destroy();
|
|
801
|
+
host.proxy.closeAllConnections();
|
|
802
|
+
await new Promise((resolve) => host.proxy.close(() => resolve()));
|
|
803
|
+
await fs.rm(host.directory, { recursive: true, force: true });
|
|
804
|
+
}
|
|
805
|
+
function createLocalBrokeringSandboxDriver(options) {
|
|
806
|
+
if (process.platform !== "darwin") throw new Error("local brokering test driver requires macOS sandbox-exec network policy");
|
|
807
|
+
const authority = createTestAuthority(options.root);
|
|
808
|
+
const hosts = /* @__PURE__ */ new Map();
|
|
809
|
+
const allowedOrigins = new Set(options.allowedOrigins.map((origin) => new URL(origin).origin));
|
|
810
|
+
const upstreams = new Map(Object.entries(options.upstreams ?? {}).map(([origin, upstream]) => {
|
|
811
|
+
const parsed = new URL(origin);
|
|
812
|
+
const destination = new URL(upstream);
|
|
813
|
+
if (parsed.protocol !== "https:" || parsed.origin !== origin || parsed.port || destination.protocol !== "http:" || !["localhost", "127.0.0.1", "[::1]"].includes(destination.hostname)) {
|
|
814
|
+
throw new Error("local broker upstreams require exact HTTPS origins and loopback HTTP destinations");
|
|
815
|
+
}
|
|
816
|
+
return [parsed.origin, destination.origin];
|
|
817
|
+
}));
|
|
818
|
+
const directOrigins = options.directOrigins.map((origin) => new URL(origin));
|
|
819
|
+
const directDestinations = directOrigins.map((url) => {
|
|
820
|
+
const loopback = ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname) || url.hostname.endsWith(".localhost");
|
|
821
|
+
if (!(loopback && ["http:", "https:"].includes(url.protocol))) {
|
|
822
|
+
throw new Error("local brokering direct origins must use loopback HTTP");
|
|
823
|
+
}
|
|
824
|
+
return `localhost:${url.port || (url.protocol === "https:" ? "443" : "80")}`;
|
|
825
|
+
});
|
|
826
|
+
async function ensureHost(input) {
|
|
827
|
+
const existing = hosts.get(input.workspaceId);
|
|
828
|
+
if (existing) {
|
|
829
|
+
const held = new Set(existing.secrets.map((secret) => secret.name));
|
|
830
|
+
if ((input.secrets ?? []).some((secret) => !held.has(secret.name))) {
|
|
831
|
+
await stop(existing.target);
|
|
832
|
+
return ensureHost(input);
|
|
833
|
+
}
|
|
834
|
+
existing.secrets = [...input.secrets ?? []];
|
|
835
|
+
return existing.target;
|
|
836
|
+
}
|
|
837
|
+
const directory = await fs.mkdtemp(path.join(options.root, "local-brokered-"));
|
|
838
|
+
const workspace = path.resolve(input.workspaceRoot ?? path.join(directory, "workspace"));
|
|
839
|
+
const relative = path.relative(options.root, workspace);
|
|
840
|
+
if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
841
|
+
await fs.rm(directory, { recursive: true, force: true });
|
|
842
|
+
throw new Error("local sandbox workspace must stay under its private test root");
|
|
843
|
+
}
|
|
844
|
+
const home = path.join(directory, "home");
|
|
845
|
+
await Promise.all([fs.mkdir(workspace, { recursive: true }), fs.mkdir(home)]);
|
|
846
|
+
const trustedCertificates = path.join(directory, "trusted-cas.pem");
|
|
847
|
+
await fs.writeFile(trustedCertificates, Buffer.concat([
|
|
848
|
+
readFileSync(authority.certificate),
|
|
849
|
+
...options.inheritedEnv?.NODE_EXTRA_CA_CERTS ? [readFileSync(options.inheritedEnv.NODE_EXTRA_CA_CERTS)] : []
|
|
850
|
+
]), { mode: 384 });
|
|
851
|
+
const runtimePort = await reserveLocalRuntimePort();
|
|
852
|
+
const hostId = input.hostId ?? `local-${input.workspaceId}`;
|
|
853
|
+
const target = {
|
|
854
|
+
workspaceId: input.workspaceId,
|
|
855
|
+
sandboxId: `${input.workspaceId}-${input.epoch}`,
|
|
856
|
+
url: `http://127.0.0.1:${runtimePort}`,
|
|
857
|
+
hostId,
|
|
858
|
+
driverResourceId: `${input.workspaceId}-${input.epoch}`,
|
|
859
|
+
labels: input.labels,
|
|
860
|
+
driver: { id: "local-brokering-test", resourceId: `${input.workspaceId}-${input.epoch}` }
|
|
861
|
+
};
|
|
862
|
+
async function forward(request, response, targetUrl, destination) {
|
|
863
|
+
try {
|
|
864
|
+
const method = request.method ?? "GET";
|
|
865
|
+
const incoming = new Headers();
|
|
866
|
+
for (const [name, values] of Object.entries(request.headers)) {
|
|
867
|
+
if (Array.isArray(values)) for (const value of values) incoming.append(name, value);
|
|
868
|
+
else if (values !== void 0) incoming.append(name, values);
|
|
869
|
+
}
|
|
870
|
+
const headers = substituteNativeSecrets(targetUrl, method, incoming, host.secrets);
|
|
871
|
+
for (const name of ["host", "connection", "proxy-connection", "content-length", "transfer-encoding"]) headers.delete(name);
|
|
872
|
+
const chunks = [];
|
|
873
|
+
if (method !== "GET" && method !== "HEAD") for await (const chunk of request) chunks.push(Buffer.from(chunk));
|
|
874
|
+
const upstream = await fetch(destination, {
|
|
875
|
+
method,
|
|
876
|
+
headers,
|
|
877
|
+
body: chunks.length ? new Uint8Array(Buffer.concat(chunks)) : void 0,
|
|
878
|
+
redirect: "manual"
|
|
879
|
+
});
|
|
880
|
+
response.writeHead(upstream.status, Object.fromEntries(upstream.headers));
|
|
881
|
+
response.end(Buffer.from(await upstream.arrayBuffer()));
|
|
882
|
+
} catch {
|
|
883
|
+
response.writeHead(403).end("local sandbox egress refused");
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
const proxy = createServer((request, response) => {
|
|
887
|
+
try {
|
|
888
|
+
const targetUrl = new URL(request.url ?? "");
|
|
889
|
+
if (!allowedOrigins.has(targetUrl.origin)) throw new Error("destination_refused");
|
|
890
|
+
void forward(request, response, targetUrl, targetUrl);
|
|
891
|
+
} catch {
|
|
892
|
+
response.writeHead(403).end("local sandbox egress refused");
|
|
893
|
+
}
|
|
894
|
+
});
|
|
895
|
+
proxy.on("connect", (request, socket, head) => {
|
|
896
|
+
const match = /^([a-z0-9.-]+):443$/i.exec(request.url ?? "");
|
|
897
|
+
const hostname = match?.[1]?.toLowerCase();
|
|
898
|
+
const origin = hostname ? `https://${hostname}` : void 0;
|
|
899
|
+
const destination = origin && upstreams.get(origin);
|
|
900
|
+
if (!hostname || !destination || !host.secrets.some((secret) => secret.hosts.includes(hostname))) {
|
|
901
|
+
socket.end("HTTP/1.1 403 Forbidden\r\ncontent-length: 0\r\n\r\n");
|
|
902
|
+
return;
|
|
903
|
+
}
|
|
904
|
+
const leaf = certificateForHost(authority, hostname);
|
|
905
|
+
const secure = createHttpsServer(leaf, (innerRequest, innerResponse) => {
|
|
906
|
+
const hostHeader = innerRequest.headers.host?.toLowerCase();
|
|
907
|
+
if (hostHeader !== hostname && hostHeader !== `${hostname}:443`) {
|
|
908
|
+
innerResponse.writeHead(403).end("local sandbox egress refused");
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
const url = new URL(innerRequest.url ?? "", origin);
|
|
912
|
+
if (url.origin !== origin) {
|
|
913
|
+
innerResponse.writeHead(403).end("local sandbox egress refused");
|
|
914
|
+
return;
|
|
915
|
+
}
|
|
916
|
+
void forward(innerRequest, innerResponse, url, new URL(`${url.pathname}${url.search}`, destination));
|
|
917
|
+
});
|
|
918
|
+
secure.listen(0, "127.0.0.1", () => {
|
|
919
|
+
const address = secure.address();
|
|
920
|
+
if (!address || typeof address === "string") {
|
|
921
|
+
socket.destroy(new Error("local TLS listener has no port"));
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
924
|
+
const bridge = net.connect(address.port, "127.0.0.1", () => {
|
|
925
|
+
socket.write("HTTP/1.1 200 Connection Established\r\n\r\n", () => {
|
|
926
|
+
if (head.length) bridge.write(head);
|
|
927
|
+
socket.pipe(bridge).pipe(socket);
|
|
928
|
+
socket.resume();
|
|
929
|
+
});
|
|
930
|
+
});
|
|
931
|
+
bridge.on("error", () => socket.destroy());
|
|
932
|
+
socket.on("error", () => bridge.destroy());
|
|
933
|
+
socket.on("close", () => {
|
|
934
|
+
bridge.destroy();
|
|
935
|
+
secure.close();
|
|
936
|
+
host.tunnels.delete(socket);
|
|
937
|
+
});
|
|
938
|
+
host.tunnels.add(socket);
|
|
939
|
+
});
|
|
940
|
+
});
|
|
941
|
+
proxy.on("upgrade", (_request, socket) => socket.end("HTTP/1.1 403 Forbidden\r\ncontent-length: 0\r\n\r\n"));
|
|
942
|
+
const host = { directory, target, secrets: [...input.secrets ?? []], proxy, child: void 0, tunnels: /* @__PURE__ */ new Set() };
|
|
943
|
+
try {
|
|
944
|
+
const proxyPort = await listen(proxy);
|
|
945
|
+
const proxyUrl = `http://127.0.0.1:${proxyPort}`;
|
|
946
|
+
const unproxied = [...directOrigins.map((url) => url.host), `127.0.0.1:${runtimePort}`, `localhost:${runtimePort}`].join(",");
|
|
947
|
+
const env = {
|
|
948
|
+
...options.inheritedEnv,
|
|
949
|
+
...workspaceRuntimeBootEnv({
|
|
950
|
+
workspaceId: input.workspaceId,
|
|
951
|
+
hostId,
|
|
952
|
+
directory: workspace,
|
|
953
|
+
port: runtimePort,
|
|
954
|
+
source: input.source,
|
|
955
|
+
env: input.env
|
|
956
|
+
}),
|
|
957
|
+
...Object.fromEntries(host.secrets.map((secret) => [secret.name, `claxedo-broker:${secret.name}`])),
|
|
958
|
+
HOME: home,
|
|
959
|
+
TMPDIR: directory,
|
|
960
|
+
HTTP_PROXY: proxyUrl,
|
|
961
|
+
HTTPS_PROXY: proxyUrl,
|
|
962
|
+
ALL_PROXY: proxyUrl,
|
|
963
|
+
http_proxy: proxyUrl,
|
|
964
|
+
https_proxy: proxyUrl,
|
|
965
|
+
all_proxy: proxyUrl,
|
|
966
|
+
NO_PROXY: unproxied,
|
|
967
|
+
no_proxy: unproxied,
|
|
968
|
+
NODE_USE_ENV_PROXY: "1",
|
|
969
|
+
NODE_EXTRA_CA_CERTS: trustedCertificates
|
|
970
|
+
};
|
|
971
|
+
for (const secret of host.secrets) {
|
|
972
|
+
if (Object.values(env).some((value) => value.includes(secret.value))) throw new Error("brokered secret entered sandbox environment");
|
|
973
|
+
}
|
|
974
|
+
const networkPolicy = `(version 1) (allow default) (deny network-outbound) (deny file-read* (subpath ${JSON.stringify(authority.privateDirectory)})) (allow process-exec (literal "/bin/ps") (with no-sandbox)) ${[`localhost:${proxyPort}`, `localhost:${runtimePort}`, ...directDestinations].map((destination) => `(allow network-outbound (remote tcp ${JSON.stringify(destination)}))`).join(" ")}`;
|
|
975
|
+
const logFile = path.join(directory, "runtime.log");
|
|
976
|
+
const log = openSync(logFile, "w", 384);
|
|
977
|
+
let child;
|
|
978
|
+
try {
|
|
979
|
+
child = spawn("/usr/bin/sandbox-exec", ["-p", networkPolicy, options.executable, ...options.args], {
|
|
980
|
+
cwd: workspace,
|
|
981
|
+
env,
|
|
982
|
+
stdio: ["ignore", log, log]
|
|
983
|
+
});
|
|
984
|
+
} finally {
|
|
985
|
+
closeSync(log);
|
|
986
|
+
}
|
|
987
|
+
host.child = child;
|
|
988
|
+
if (!child.pid) throw new Error("local sandbox runtime did not start");
|
|
989
|
+
hosts.set(input.workspaceId, host);
|
|
990
|
+
await waitForLocalRuntimeHealth(target.url, child, logFile);
|
|
991
|
+
const targets = path.join(options.root, "local-broker-targets");
|
|
992
|
+
await fs.mkdir(targets, { recursive: true });
|
|
993
|
+
await fs.writeFile(path.join(targets, `${input.workspaceId}.json`), JSON.stringify({ url: target.url, directory: workspace, home, pid: child.pid, secretNames: host.secrets.map((secret) => secret.name) }));
|
|
994
|
+
return target;
|
|
995
|
+
} catch (error) {
|
|
996
|
+
if (host.child) await closeHost(host);
|
|
997
|
+
else {
|
|
998
|
+
proxy.closeAllConnections();
|
|
999
|
+
await new Promise((resolve) => proxy.close(() => resolve()));
|
|
1000
|
+
await fs.rm(directory, { recursive: true, force: true });
|
|
1001
|
+
}
|
|
1002
|
+
hosts.delete(input.workspaceId);
|
|
1003
|
+
throw error;
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
async function stop(target) {
|
|
1007
|
+
const host = hosts.get(target.workspaceId ?? "");
|
|
1008
|
+
if (!host) return;
|
|
1009
|
+
hosts.delete(target.workspaceId ?? "");
|
|
1010
|
+
await closeHost(host);
|
|
1011
|
+
await fs.rm(path.join(options.root, "local-broker-targets", `${target.workspaceId}.json`), { force: true });
|
|
1012
|
+
}
|
|
1013
|
+
return {
|
|
1014
|
+
id: "local-brokering-test",
|
|
1015
|
+
metadata: localBrokeringTestDriverCatalogEntry.metadata,
|
|
1016
|
+
catalogEntry: localBrokeringTestDriverCatalogEntry,
|
|
1017
|
+
ensureHost,
|
|
1018
|
+
resumeHost: ({ ensure }) => ensureHost(ensure),
|
|
1019
|
+
inspect: async (target) => hosts.get(target.workspaceId ?? "")?.target ? "serving" : "gone",
|
|
1020
|
+
touch: async () => {
|
|
1021
|
+
},
|
|
1022
|
+
stop,
|
|
1023
|
+
suspend: stop,
|
|
1024
|
+
destroy: stop
|
|
1025
|
+
};
|
|
1026
|
+
}
|
|
1027
|
+
export {
|
|
1028
|
+
createLocalBrokeringSandboxDriver
|
|
1029
|
+
};
|