@desplega.ai/agent-swarm 1.85.0 → 1.87.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.
Files changed (129) hide show
  1. package/README.md +1 -0
  2. package/openapi.json +72 -1
  3. package/package.json +10 -6
  4. package/src/be/db-queries/tracker.ts +21 -0
  5. package/src/be/db.ts +279 -14
  6. package/src/be/migrations/078_backfill_gpt_5_5_pricing.sql +15 -0
  7. package/src/be/migrations/079_task_followup_config.sql +1 -0
  8. package/src/be/modelsdev-cache.json +155618 -0
  9. package/src/be/modelsdev-cache.ts +46 -0
  10. package/src/be/seed-pricing.ts +7 -44
  11. package/src/cli.tsx +38 -2
  12. package/src/commands/codex-session-runner.ts +132 -0
  13. package/src/commands/context-preamble.ts +272 -0
  14. package/src/commands/credential-wait.ts +2 -2
  15. package/src/commands/e2b.ts +728 -0
  16. package/src/commands/provider-credentials.ts +10 -5
  17. package/src/commands/resume-session.ts +35 -78
  18. package/src/commands/runner.ts +128 -16
  19. package/src/e2b/dispatch.ts +429 -0
  20. package/src/e2b/env.ts +206 -0
  21. package/src/heartbeat/heartbeat.ts +145 -30
  22. package/src/heartbeat/templates.ts +11 -7
  23. package/src/http/session-data.ts +8 -1
  24. package/src/http/tasks.ts +152 -3
  25. package/src/jira/sync.ts +4 -4
  26. package/src/linear/sync.ts +6 -5
  27. package/src/prompts/base-prompt.ts +49 -3
  28. package/src/providers/claude-adapter.ts +76 -61
  29. package/src/providers/claude-managed-adapter.ts +61 -75
  30. package/src/providers/claude-managed-models.ts +18 -2
  31. package/src/providers/codex-adapter.ts +429 -112
  32. package/src/providers/codex-models.ts +9 -2
  33. package/src/providers/codex-oauth/auth-json.ts +18 -1
  34. package/src/providers/codex-oauth/flow.ts +24 -1
  35. package/src/providers/index.ts +28 -19
  36. package/src/providers/pricing-sources.md +7 -4
  37. package/src/providers/swarm-events-shared.ts +14 -0
  38. package/src/providers/types.ts +6 -0
  39. package/src/slack/HEURISTICS.md +5 -1
  40. package/src/slack/handlers.test.ts +35 -0
  41. package/src/slack/handlers.ts +79 -2
  42. package/src/tasks/worker-follow-up.ts +162 -2
  43. package/src/telemetry.ts +11 -1
  44. package/src/tests/base-prompt.test.ts +46 -8
  45. package/src/tests/claude-adapter.test.ts +5 -27
  46. package/src/tests/claude-managed-adapter.test.ts +42 -56
  47. package/src/tests/codex-adapter-otel.test.ts +4 -4
  48. package/src/tests/codex-adapter.test.ts +25 -37
  49. package/src/tests/codex-oauth.test.ts +149 -3
  50. package/src/tests/codex-pool.test.ts +14 -3
  51. package/src/tests/codex-swarm-events.test.ts +35 -0
  52. package/src/tests/context-window.test.ts +1 -0
  53. package/src/tests/credential-check.test.ts +48 -29
  54. package/src/tests/e2b-dispatch.test.ts +330 -0
  55. package/src/tests/entrypoint-config-env-export.test.ts +81 -0
  56. package/src/tests/follow-up-redelivery-guard.test.ts +165 -0
  57. package/src/tests/heartbeat-supersede-resume.test.ts +285 -0
  58. package/src/tests/heartbeat.test.ts +26 -16
  59. package/src/tests/migration-046-budgets.test.ts +6 -5
  60. package/src/tests/pricing-routes.test.ts +6 -5
  61. package/src/tests/prompt-template-remaining.test.ts +4 -0
  62. package/src/tests/provider-adapter.test.ts +10 -10
  63. package/src/tests/provider-command-format.test.ts +4 -4
  64. package/src/tests/resume-session.test.ts +42 -50
  65. package/src/tests/session-costs-codex-recompute.test.ts +25 -0
  66. package/src/tests/structured-output.test.ts +69 -0
  67. package/src/tests/task-completion-idempotency.test.ts +185 -2
  68. package/src/tests/task-supersede-resume.test.ts +722 -0
  69. package/src/tests/telemetry-init.test.ts +69 -0
  70. package/src/tests/vcs-tracking.test.ts +39 -0
  71. package/src/tools/send-task.ts +42 -10
  72. package/src/tools/store-progress.ts +2 -2
  73. package/src/tools/templates.ts +14 -2
  74. package/src/types.ts +46 -1
  75. package/src/utils/context-window.ts +1 -0
  76. package/src/workflows/executors/agent-task.ts +3 -0
  77. package/templates/schedules/daily-blocker-digest/config.json +13 -0
  78. package/templates/schedules/daily-blocker-digest/content.md +150 -0
  79. package/templates/schedules/daily-compounding-reflection/config.json +21 -0
  80. package/templates/schedules/daily-compounding-reflection/content.md +210 -0
  81. package/templates/schedules/daily-hn-briefing/config.json +13 -0
  82. package/templates/schedules/daily-hn-briefing/content.md +97 -0
  83. package/templates/schedules/daily-workflow-health-audit/config.json +13 -0
  84. package/templates/schedules/daily-workflow-health-audit/content.md +189 -0
  85. package/templates/schedules/gtm-weekly-review/config.json +13 -0
  86. package/templates/schedules/gtm-weekly-review/content.md +58 -0
  87. package/templates/schedules/weekly-dependabot-triage/config.json +13 -0
  88. package/templates/schedules/weekly-dependabot-triage/content.md +45 -0
  89. package/templates/schema.ts +26 -0
  90. package/templates/skills/agentmail-sending/config.json +13 -0
  91. package/templates/skills/agentmail-sending/content.md +48 -0
  92. package/templates/skills/artifacts/config.json +13 -0
  93. package/templates/skills/artifacts/content.md +87 -0
  94. package/templates/skills/browser-use-cloud/config.json +13 -0
  95. package/templates/skills/browser-use-cloud/content.md +155 -0
  96. package/templates/skills/desloppify/config.json +13 -0
  97. package/templates/skills/desloppify/content.md +201 -0
  98. package/templates/skills/exa-search/config.json +13 -0
  99. package/templates/skills/exa-search/content.md +106 -0
  100. package/templates/skills/jira-interaction/config.json +13 -0
  101. package/templates/skills/jira-interaction/content.md +252 -0
  102. package/templates/skills/kapso-whatsapp/config.json +13 -0
  103. package/templates/skills/kapso-whatsapp/content.md +369 -0
  104. package/templates/skills/kv-storage/config.json +13 -0
  105. package/templates/skills/kv-storage/content.md +111 -0
  106. package/templates/skills/linear-interaction/config.json +20 -0
  107. package/templates/skills/linear-interaction/content.md +230 -0
  108. package/templates/skills/pages/config.json +18 -0
  109. package/templates/skills/pages/content.md +85 -0
  110. package/templates/skills/profile-corruption-escalation/config.json +13 -0
  111. package/templates/skills/profile-corruption-escalation/content.md +105 -0
  112. package/templates/skills/scheduled-task-resilience/config.json +13 -0
  113. package/templates/skills/scheduled-task-resilience/content.md +95 -0
  114. package/templates/skills/sprite-cli/config.json +13 -0
  115. package/templates/skills/sprite-cli/content.md +133 -0
  116. package/templates/skills/turso-interaction/config.json +13 -0
  117. package/templates/skills/turso-interaction/content.md +192 -0
  118. package/templates/skills/workflow-iterate/config.json +18 -0
  119. package/templates/skills/workflow-iterate/content.md +399 -0
  120. package/templates/skills/workflow-structured-output/config.json +13 -0
  121. package/templates/skills/workflow-structured-output/content.md +101 -0
  122. package/templates/skills/x-api-interactions/config.json +13 -0
  123. package/templates/skills/x-api-interactions/content.md +109 -0
  124. package/templates/workflows/autopilot/config.json +13 -0
  125. package/templates/workflows/autopilot/content.md +58 -0
  126. package/templates/workflows/linear-drain-loop/config.json +21 -0
  127. package/templates/workflows/linear-drain-loop/content.md +72 -0
  128. package/templates/workflows/ralph-loop/config.json +13 -0
  129. package/templates/workflows/ralph-loop/content.md +75 -0
@@ -0,0 +1,429 @@
1
+ import { DEFAULT_E2B_API_BASE, type EnvMap, redactWithEnv } from "./env";
2
+
3
+ export type E2BRole = "api" | "worker";
4
+
5
+ export type E2BSandboxInfo = {
6
+ templateID: string;
7
+ sandboxID: string;
8
+ clientID?: string;
9
+ envdVersion?: string;
10
+ alias?: string;
11
+ envdAccessToken?: string;
12
+ trafficAccessToken?: string;
13
+ domain?: string | null;
14
+ startedAt?: string;
15
+ endAt?: string;
16
+ metadata?: Record<string, string>;
17
+ };
18
+
19
+ export type E2BCommandResult = {
20
+ exitCode: number;
21
+ stdout: string;
22
+ stderr: string;
23
+ };
24
+
25
+ export type BuildTemplateOptions = {
26
+ role: E2BRole;
27
+ name: string;
28
+ dockerfile: string;
29
+ cwd: string;
30
+ cpuCount: number;
31
+ memoryMb: number;
32
+ noCache: boolean;
33
+ e2bEnv: EnvMap;
34
+ dryRun?: boolean;
35
+ };
36
+
37
+ export type DeleteTemplateOptions = {
38
+ name: string;
39
+ e2bEnv: EnvMap;
40
+ dryRun?: boolean;
41
+ };
42
+
43
+ export type TemplateVisibilityOptions = {
44
+ name: string;
45
+ e2bEnv: EnvMap;
46
+ public: boolean;
47
+ dryRun?: boolean;
48
+ };
49
+
50
+ export type BuildImageTemplateOptions = {
51
+ role: E2BRole;
52
+ name: string;
53
+ image: string;
54
+ cpuCount: number;
55
+ memoryMb: number;
56
+ noCache: boolean;
57
+ e2bEnv: EnvMap;
58
+ dryRun?: boolean;
59
+ };
60
+
61
+ export type CreateSandboxOptions = {
62
+ apiKey: string;
63
+ apiBase?: string;
64
+ template: string;
65
+ timeoutSec: number;
66
+ envVars: EnvMap;
67
+ metadata: Record<string, string>;
68
+ allowInternetAccess?: boolean;
69
+ };
70
+
71
+ export type StartDetachedOptions = {
72
+ sandbox: E2BSandboxInfo;
73
+ apiKey: string;
74
+ apiBase?: string;
75
+ e2bEnv?: EnvMap;
76
+ env: EnvMap;
77
+ command: string;
78
+ role: E2BRole;
79
+ user?: string;
80
+ cwd?: string;
81
+ };
82
+
83
+ type E2BSdkConnectionOptions = {
84
+ apiKey: string;
85
+ apiUrl?: string;
86
+ domain?: string;
87
+ sandboxUrl?: string;
88
+ };
89
+
90
+ type E2BTemplateVisibilityResponse = {
91
+ names: string[];
92
+ };
93
+
94
+ function e2bHeaders(apiKey: string): Record<string, string> {
95
+ return {
96
+ "Content-Type": "application/json",
97
+ "X-API-Key": apiKey,
98
+ };
99
+ }
100
+
101
+ export function buildDetachedShell(command: string, logPath: string, pidPath: string): string {
102
+ return [
103
+ "set -e",
104
+ `nohup ${command} >${logPath} 2>&1 </dev/null & pid=$!`,
105
+ "sleep 2",
106
+ `if ! kill -0 "$pid" 2>/dev/null; then cat ${logPath} >&2; exit 1; fi`,
107
+ `echo "$pid" > ${pidPath}`,
108
+ 'echo "$pid"',
109
+ ].join("; ");
110
+ }
111
+
112
+ export function e2bSdkConnectionOptions(
113
+ apiKey: string,
114
+ env: EnvMap,
115
+ apiBase?: string,
116
+ ): E2BSdkConnectionOptions {
117
+ const options: E2BSdkConnectionOptions = { apiKey };
118
+ const resolvedApiUrl = apiBase || env.E2B_API_URL;
119
+ if (resolvedApiUrl) options.apiUrl = resolvedApiUrl;
120
+ if (env.E2B_DOMAIN) options.domain = env.E2B_DOMAIN;
121
+ if (env.E2B_SANDBOX_URL) options.sandboxUrl = env.E2B_SANDBOX_URL;
122
+ return options;
123
+ }
124
+
125
+ function sandboxDomainFromUrl(rawUrl: string): string | undefined {
126
+ try {
127
+ const url = new URL(rawUrl);
128
+ const host = url.host;
129
+ return host.startsWith("sandbox.") ? host.slice("sandbox.".length) : host;
130
+ } catch {
131
+ const host = rawUrl.replace(/^https?:\/\//, "").replace(/\/.*$/, "");
132
+ if (!host) return undefined;
133
+ return host.startsWith("sandbox.") ? host.slice("sandbox.".length) : host;
134
+ }
135
+ }
136
+
137
+ function configuredSandboxDomain(env: EnvMap): string | undefined {
138
+ if (env.E2B_DOMAIN) return env.E2B_DOMAIN;
139
+ if (env.E2B_SANDBOX_URL) return sandboxDomainFromUrl(env.E2B_SANDBOX_URL);
140
+ return undefined;
141
+ }
142
+
143
+ export function sandboxPortHost(sandbox: E2BSandboxInfo, port: number, env: EnvMap = {}): string {
144
+ const domain = sandbox.domain || configuredSandboxDomain(env) || "e2b.app";
145
+ if (domain.includes(sandbox.sandboxID)) {
146
+ return `${port}-${domain}`;
147
+ }
148
+ return `${port}-${sandbox.sandboxID}.${domain}`;
149
+ }
150
+
151
+ export function sandboxPortUrl(sandbox: E2BSandboxInfo, port: number, env: EnvMap = {}): string {
152
+ return `https://${sandboxPortHost(sandbox, port, env)}`;
153
+ }
154
+
155
+ async function readResponseBody(response: Response): Promise<string> {
156
+ const text = await response.text();
157
+ return text.trim();
158
+ }
159
+
160
+ export async function e2bFetchJson<T>(
161
+ path: string,
162
+ apiKey: string,
163
+ init: RequestInit = {},
164
+ apiBase = DEFAULT_E2B_API_BASE,
165
+ ): Promise<T> {
166
+ const response = await fetch(`${apiBase}${path}`, {
167
+ ...init,
168
+ headers: {
169
+ ...e2bHeaders(apiKey),
170
+ ...(init.headers ?? {}),
171
+ },
172
+ });
173
+
174
+ if (!response.ok) {
175
+ const body = await readResponseBody(response);
176
+ throw new Error(`E2B API ${response.status} ${response.statusText}: ${body}`);
177
+ }
178
+
179
+ if (response.status === 204) {
180
+ return undefined as T;
181
+ }
182
+
183
+ return (await response.json()) as T;
184
+ }
185
+
186
+ export async function createSandbox(opts: CreateSandboxOptions): Promise<E2BSandboxInfo> {
187
+ return e2bFetchJson<E2BSandboxInfo>(
188
+ "/sandboxes",
189
+ opts.apiKey,
190
+ {
191
+ method: "POST",
192
+ body: JSON.stringify({
193
+ templateID: opts.template,
194
+ timeout: opts.timeoutSec,
195
+ secure: true,
196
+ allow_internet_access: opts.allowInternetAccess ?? true,
197
+ metadata: opts.metadata,
198
+ envVars: opts.envVars,
199
+ }),
200
+ },
201
+ opts.apiBase,
202
+ );
203
+ }
204
+
205
+ export async function killSandbox(
206
+ sandboxId: string,
207
+ apiKey: string,
208
+ apiBase = DEFAULT_E2B_API_BASE,
209
+ ): Promise<void> {
210
+ await e2bFetchJson<void>(
211
+ `/sandboxes/${encodeURIComponent(sandboxId)}`,
212
+ apiKey,
213
+ { method: "DELETE" },
214
+ apiBase,
215
+ );
216
+ }
217
+
218
+ export async function listSandboxes(
219
+ apiKey: string,
220
+ apiBase = DEFAULT_E2B_API_BASE,
221
+ ): Promise<E2BSandboxInfo[]> {
222
+ return e2bFetchJson<E2BSandboxInfo[]>("/sandboxes", apiKey, {}, apiBase);
223
+ }
224
+
225
+ export async function startDetachedProcess(opts: StartDetachedOptions): Promise<string> {
226
+ const logPath = `/tmp/agent-swarm-e2b-${opts.role}.log`;
227
+ const pidPath = `/tmp/agent-swarm-e2b-${opts.role}.pid`;
228
+ const shell = buildDetachedShell(opts.command, logPath, pidPath);
229
+
230
+ const { Sandbox } = await import("e2b");
231
+ const sandbox = await Sandbox.connect(
232
+ opts.sandbox.sandboxID,
233
+ e2bSdkConnectionOptions(opts.apiKey, opts.e2bEnv ?? {}, opts.apiBase),
234
+ );
235
+ const result = await sandbox.commands.run(shell, {
236
+ user: opts.user ?? "root",
237
+ cwd: opts.cwd ?? "/",
238
+ envs: opts.env,
239
+ timeoutMs: 30_000,
240
+ });
241
+
242
+ if (result.exitCode !== 0) {
243
+ throw new Error(`E2B start command failed: ${redactWithEnv(result.stderr, opts.env)}`);
244
+ }
245
+ return result.stdout.trim();
246
+ }
247
+
248
+ export async function waitForAgentRegistration(
249
+ apiUrl: string,
250
+ agentId: string,
251
+ apiKey: string,
252
+ timeoutMs: number,
253
+ ): Promise<void> {
254
+ const baseUrl = apiUrl.replace(/\/+$/, "");
255
+ const url = `${baseUrl}/api/agents/${encodeURIComponent(agentId)}`;
256
+ const started = Date.now();
257
+ let lastError = "";
258
+
259
+ while (Date.now() - started < timeoutMs) {
260
+ try {
261
+ const response = await fetch(url, {
262
+ headers: {
263
+ Authorization: `Bearer ${apiKey}`,
264
+ },
265
+ });
266
+ if (response.ok) return;
267
+ lastError = `${response.status} ${response.statusText}`;
268
+ } catch (err) {
269
+ lastError = err instanceof Error ? err.message : String(err);
270
+ }
271
+ await Bun.sleep(1_000);
272
+ }
273
+
274
+ throw new Error(
275
+ `Timed out waiting for worker ${agentId} to register at ${url}${
276
+ lastError ? ` (${lastError})` : ""
277
+ }`,
278
+ );
279
+ }
280
+
281
+ export async function waitForHttpOk(url: string, timeoutMs: number): Promise<void> {
282
+ const started = Date.now();
283
+ let lastError = "";
284
+ while (Date.now() - started < timeoutMs) {
285
+ try {
286
+ const response = await fetch(url);
287
+ if (response.ok) return;
288
+ lastError = `${response.status} ${response.statusText}`;
289
+ } catch (err) {
290
+ lastError = err instanceof Error ? err.message : String(err);
291
+ }
292
+ await Bun.sleep(1_000);
293
+ }
294
+ throw new Error(`Timed out waiting for ${url}${lastError ? ` (${lastError})` : ""}`);
295
+ }
296
+
297
+ export function buildTemplateArgs(opts: BuildTemplateOptions): string[] {
298
+ const args = [
299
+ "template",
300
+ "create",
301
+ "-p",
302
+ opts.cwd,
303
+ "-d",
304
+ opts.dockerfile,
305
+ "-c",
306
+ "sleep infinity",
307
+ "--ready-cmd",
308
+ "sleep 0",
309
+ "--cpu-count",
310
+ String(opts.cpuCount),
311
+ "--memory-mb",
312
+ String(opts.memoryMb),
313
+ ];
314
+
315
+ if (opts.noCache) {
316
+ args.push("--no-cache");
317
+ }
318
+
319
+ args.push(opts.name);
320
+ return args;
321
+ }
322
+
323
+ export async function runE2BCommand(args: string[], env: EnvMap): Promise<E2BCommandResult> {
324
+ const child = Bun.spawn(["e2b", ...args], {
325
+ env: { ...process.env, ...env },
326
+ stdout: "pipe",
327
+ stderr: "pipe",
328
+ });
329
+ const [stdout, stderr, exitCode] = await Promise.all([
330
+ new Response(child.stdout).text(),
331
+ new Response(child.stderr).text(),
332
+ child.exited,
333
+ ]);
334
+ return { stdout: redactWithEnv(stdout, env), stderr: redactWithEnv(stderr, env), exitCode };
335
+ }
336
+
337
+ export async function buildTemplate(opts: BuildTemplateOptions): Promise<E2BCommandResult> {
338
+ const args = buildTemplateArgs(opts);
339
+ if (opts.dryRun) {
340
+ return { exitCode: 0, stdout: `e2b ${args.join(" ")}\n`, stderr: "" };
341
+ }
342
+ return runE2BCommand(args, opts.e2bEnv);
343
+ }
344
+
345
+ export async function buildImageTemplate(
346
+ opts: BuildImageTemplateOptions,
347
+ ): Promise<E2BCommandResult> {
348
+ if (opts.dryRun) {
349
+ return {
350
+ exitCode: 0,
351
+ stdout: [
352
+ `e2b-sdk template build --from-image ${opts.image}`,
353
+ ` --name ${opts.name}`,
354
+ ` --start-cmd "sleep infinity"`,
355
+ ` --ready-cmd "sleep 0"`,
356
+ ` --cpu-count ${opts.cpuCount}`,
357
+ ` --memory-mb ${opts.memoryMb}`,
358
+ opts.noCache ? ` --no-cache` : "",
359
+ ]
360
+ .filter(Boolean)
361
+ .join("\n")
362
+ .concat("\n"),
363
+ stderr: "",
364
+ };
365
+ }
366
+
367
+ const apiKey = opts.e2bEnv.E2B_API_KEY;
368
+ if (!apiKey) {
369
+ throw new Error("Missing E2B_API_KEY");
370
+ }
371
+
372
+ const { Template } = await import("e2b");
373
+ const template = Template().fromImage(opts.image).setStartCmd("sleep infinity", "sleep 0");
374
+ const buildInfo = await Template.build(template, opts.name, {
375
+ ...e2bSdkConnectionOptions(apiKey, opts.e2bEnv),
376
+ cpuCount: opts.cpuCount,
377
+ memoryMB: opts.memoryMb,
378
+ skipCache: opts.noCache,
379
+ });
380
+
381
+ return {
382
+ exitCode: 0,
383
+ stdout: `Built E2B ${opts.role} template ${buildInfo.name} (${buildInfo.templateId}, build ${buildInfo.buildId})\n`,
384
+ stderr: "",
385
+ };
386
+ }
387
+
388
+ export async function deleteTemplate(opts: DeleteTemplateOptions): Promise<E2BCommandResult> {
389
+ const args = ["template", "delete", opts.name, "-y"];
390
+ if (opts.dryRun) {
391
+ return { exitCode: 0, stdout: `e2b ${args.join(" ")}\n`, stderr: "" };
392
+ }
393
+ return runE2BCommand(args, opts.e2bEnv);
394
+ }
395
+
396
+ export async function setTemplateVisibility(
397
+ opts: TemplateVisibilityOptions,
398
+ ): Promise<E2BCommandResult> {
399
+ const path = `/v2/templates/${encodeURIComponent(opts.name)}`;
400
+ if (opts.dryRun) {
401
+ return {
402
+ exitCode: 0,
403
+ stdout: `PATCH ${path} {"public":${opts.public}}\n`,
404
+ stderr: "",
405
+ };
406
+ }
407
+
408
+ const apiKey = opts.e2bEnv.E2B_API_KEY;
409
+ if (!apiKey) {
410
+ throw new Error("Missing E2B_API_KEY");
411
+ }
412
+
413
+ const result = await e2bFetchJson<E2BTemplateVisibilityResponse>(
414
+ path,
415
+ apiKey,
416
+ {
417
+ method: "PATCH",
418
+ body: JSON.stringify({ public: opts.public }),
419
+ },
420
+ opts.e2bEnv.E2B_API_URL || DEFAULT_E2B_API_BASE,
421
+ );
422
+ const names = result.names.length > 0 ? ` (${result.names.join(", ")})` : "";
423
+ const visibility = opts.public ? "public" : "private";
424
+ return {
425
+ exitCode: 0,
426
+ stdout: `Set E2B template ${opts.name} visibility to ${visibility}${names}\n`,
427
+ stderr: "",
428
+ };
429
+ }
package/src/e2b/env.ts ADDED
@@ -0,0 +1,206 @@
1
+ import { resolve } from "node:path";
2
+ import { getApiKey } from "../utils/api-key";
3
+ import { isSensitiveKey, scrubSecrets } from "../utils/secret-scrubber";
4
+
5
+ export type EnvMap = Record<string, string>;
6
+
7
+ export const DEFAULT_E2B_API_BASE = "https://api.e2b.app";
8
+ export const DEFAULT_E2B_ENVD_PORT = 49_983;
9
+
10
+ export const DEFAULT_E2B_TEMPLATE_NAMES = {
11
+ api: "agent-swarm-api",
12
+ worker: "agent-swarm-worker",
13
+ } as const;
14
+
15
+ export const DEFAULT_E2B_FORWARD_KEYS = [
16
+ "AGENT_SWARM_API_KEY",
17
+ "API_KEY",
18
+ "SECRETS_ENCRYPTION_KEY",
19
+ "ANTHROPIC_API_KEY",
20
+ "CLAUDE_CODE_OAUTH_TOKEN",
21
+ "OPENAI_API_KEY",
22
+ "OPENROUTER_API_KEY",
23
+ "GITHUB_TOKEN",
24
+ "GITLAB_TOKEN",
25
+ "LINEAR_API_KEY",
26
+ "SLACK_BOT_TOKEN",
27
+ "SLACK_SIGNING_SECRET",
28
+ "BUSINESS_USE_API_KEY",
29
+ "SENTRY_AUTH_TOKEN",
30
+ "OTEL_EXPORTER_OTLP_ENDPOINT",
31
+ "OTEL_EXPORTER_OTLP_HEADERS",
32
+ "SIGNOZ_INGESTION_KEY",
33
+ "DEVIN_API_KEY",
34
+ "DEVIN_ORG_ID",
35
+ "MANAGED_AGENT_ID",
36
+ "MANAGED_ENVIRONMENT_ID",
37
+ "MANAGED_AGENT_MODEL",
38
+ "MANAGED_GITHUB_TOKEN",
39
+ "ARCHIL_MOUNT_TOKEN",
40
+ "ARCHIL_REGION",
41
+ "ARCHIL_SHARED_DISK_NAME",
42
+ "ARCHIL_PERSONAL_DISK_NAME",
43
+ "HARNESS_PROVIDER",
44
+ "MODEL_OVERRIDE",
45
+ "STARTUP_SCRIPT_STRICT",
46
+ ] as const;
47
+
48
+ export type SwarmRole = "api" | "worker";
49
+
50
+ function decodeDoubleQuotedValue(value: string): string {
51
+ return value
52
+ .replace(/\\n/g, "\n")
53
+ .replace(/\\r/g, "\r")
54
+ .replace(/\\t/g, "\t")
55
+ .replace(/\\"/g, '"')
56
+ .replace(/\\\\/g, "\\");
57
+ }
58
+
59
+ function parseQuotedValue(value: string, quote: '"' | "'"): string | null {
60
+ let escaped = false;
61
+ for (let i = 1; i < value.length; i++) {
62
+ const char = value[i];
63
+ if (quote === '"' && escaped) {
64
+ escaped = false;
65
+ continue;
66
+ }
67
+ if (quote === '"' && char === "\\") {
68
+ escaped = true;
69
+ continue;
70
+ }
71
+ if (char !== quote) continue;
72
+
73
+ const rest = value.slice(i + 1).trim();
74
+ if (rest && !rest.startsWith("#")) return null;
75
+
76
+ const inner = value.slice(1, i);
77
+ return quote === '"' ? decodeDoubleQuotedValue(inner) : inner;
78
+ }
79
+ return null;
80
+ }
81
+
82
+ export function parseDotenv(source: string): EnvMap {
83
+ const out: EnvMap = {};
84
+
85
+ for (const rawLine of source.split(/\r?\n/)) {
86
+ let line = rawLine.trim();
87
+ if (!line || line.startsWith("#")) continue;
88
+ if (line.startsWith("export ")) {
89
+ line = line.slice("export ".length).trimStart();
90
+ }
91
+
92
+ const eq = line.indexOf("=");
93
+ if (eq <= 0) continue;
94
+
95
+ const key = line.slice(0, eq).trim();
96
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) continue;
97
+
98
+ let value = line.slice(eq + 1).trim();
99
+ const quote = value[0];
100
+ const quoted = quote === '"' || quote === "'" ? parseQuotedValue(value, quote) : null;
101
+ if (quoted !== null) {
102
+ value = quoted;
103
+ } else {
104
+ value = value.replace(/\s+#.*$/, "").trim();
105
+ }
106
+
107
+ out[key] = value;
108
+ }
109
+
110
+ return out;
111
+ }
112
+
113
+ export async function readDotenvFile(path: string): Promise<EnvMap> {
114
+ const file = Bun.file(path);
115
+ if (!(await file.exists())) {
116
+ throw new Error(`Env file not found: ${path}`);
117
+ }
118
+ return parseDotenv(await file.text());
119
+ }
120
+
121
+ export async function maybeReadDotenvFile(path: string): Promise<EnvMap> {
122
+ const file = Bun.file(path);
123
+ if (!(await file.exists())) return {};
124
+ return parseDotenv(await file.text());
125
+ }
126
+
127
+ export function parseKeyValue(raw: string, label: string): [string, string] {
128
+ const eq = raw.indexOf("=");
129
+ if (eq <= 0) {
130
+ throw new Error(`${label} must be KEY=VALUE`);
131
+ }
132
+ const key = raw.slice(0, eq);
133
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
134
+ throw new Error(`${label} has invalid env key: ${key}`);
135
+ }
136
+ return [key, raw.slice(eq + 1)];
137
+ }
138
+
139
+ export function splitKeys(values: string[]): string[] {
140
+ return values
141
+ .flatMap((value) => value.split(","))
142
+ .map((value) => value.trim())
143
+ .filter(Boolean);
144
+ }
145
+
146
+ export function selectEnv(source: NodeJS.ProcessEnv | EnvMap, keys: readonly string[]): EnvMap {
147
+ const out: EnvMap = {};
148
+ for (const key of keys) {
149
+ const value = source[key];
150
+ if (typeof value === "string" && value.length > 0) {
151
+ out[key] = value;
152
+ }
153
+ }
154
+ return out;
155
+ }
156
+
157
+ export function resolveSwarmApiKey(env: EnvMap, explicit?: string): string {
158
+ const apiKey = explicit || env.AGENT_SWARM_API_KEY || env.API_KEY || getApiKey();
159
+ if (!apiKey) {
160
+ throw new Error(
161
+ "Missing swarm API key. Pass --api-key or set AGENT_SWARM_API_KEY/API_KEY for E2B sandboxes.",
162
+ );
163
+ }
164
+ return apiKey;
165
+ }
166
+
167
+ export function redactWithEnv(text: string, env: EnvMap): string {
168
+ let out = text;
169
+ const entries = Object.entries(env)
170
+ .filter(([key, value]) => isSensitiveKey(key) && value.length >= 8)
171
+ .sort((a, b) => b[1].length - a[1].length);
172
+
173
+ for (const [key, value] of entries) {
174
+ out = out.split(value).join(`[REDACTED:${key}]`);
175
+ }
176
+
177
+ return scrubSecrets(out);
178
+ }
179
+
180
+ export function redactObjectWithEnv<T>(value: T, env: EnvMap, seen = new WeakSet<object>()): T {
181
+ if (value === null || value === undefined) return value;
182
+ if (typeof value === "string") return redactWithEnv(value, env) as T;
183
+ if (typeof value !== "object") return value;
184
+
185
+ if (seen.has(value)) return "[Circular]" as T;
186
+ seen.add(value);
187
+
188
+ if (Array.isArray(value)) {
189
+ return value.map((item) => redactObjectWithEnv(item, env, seen)) as T;
190
+ }
191
+
192
+ const out: Record<string, unknown> = {};
193
+ for (const [key, child] of Object.entries(value as Record<string, unknown>)) {
194
+ const normalized = key.replace(/([a-z])([A-Z])/g, "$1_$2").toUpperCase();
195
+ if (isSensitiveKey(normalized) || /TOKEN|SECRET|PASSWORD|PRIVATE_KEY/.test(normalized)) {
196
+ out[key] = `[REDACTED:${key}]`;
197
+ continue;
198
+ }
199
+ out[key] = redactObjectWithEnv(child, env, seen);
200
+ }
201
+ return out as T;
202
+ }
203
+
204
+ export function absolutePath(path: string, cwd = process.cwd()): string {
205
+ return resolve(cwd, path);
206
+ }