@agentstep/agent-sdk 0.1.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 (105) hide show
  1. package/package.json +45 -0
  2. package/src/auth/middleware.ts +38 -0
  3. package/src/backends/claude/args.ts +88 -0
  4. package/src/backends/claude/index.ts +193 -0
  5. package/src/backends/claude/permission-hook.ts +152 -0
  6. package/src/backends/claude/tool-bridge.ts +211 -0
  7. package/src/backends/claude/translator.ts +209 -0
  8. package/src/backends/claude/wrapper-script.ts +45 -0
  9. package/src/backends/codex/args.ts +69 -0
  10. package/src/backends/codex/auth.ts +35 -0
  11. package/src/backends/codex/index.ts +57 -0
  12. package/src/backends/codex/setup.ts +37 -0
  13. package/src/backends/codex/translator.ts +223 -0
  14. package/src/backends/codex/wrapper-script.ts +26 -0
  15. package/src/backends/factory/args.ts +45 -0
  16. package/src/backends/factory/auth.ts +30 -0
  17. package/src/backends/factory/index.ts +56 -0
  18. package/src/backends/factory/setup.ts +34 -0
  19. package/src/backends/factory/translator.ts +139 -0
  20. package/src/backends/factory/wrapper-script.ts +33 -0
  21. package/src/backends/gemini/args.ts +44 -0
  22. package/src/backends/gemini/auth.ts +30 -0
  23. package/src/backends/gemini/index.ts +53 -0
  24. package/src/backends/gemini/setup.ts +34 -0
  25. package/src/backends/gemini/translator.ts +139 -0
  26. package/src/backends/gemini/wrapper-script.ts +26 -0
  27. package/src/backends/opencode/args.ts +53 -0
  28. package/src/backends/opencode/auth.ts +53 -0
  29. package/src/backends/opencode/index.ts +70 -0
  30. package/src/backends/opencode/mcp.ts +67 -0
  31. package/src/backends/opencode/setup.ts +54 -0
  32. package/src/backends/opencode/translator.ts +168 -0
  33. package/src/backends/opencode/wrapper-script.ts +46 -0
  34. package/src/backends/registry.ts +38 -0
  35. package/src/backends/shared/ndjson.ts +29 -0
  36. package/src/backends/shared/translator-types.ts +69 -0
  37. package/src/backends/shared/wrap-prompt.ts +17 -0
  38. package/src/backends/types.ts +85 -0
  39. package/src/config/index.ts +95 -0
  40. package/src/db/agents.ts +185 -0
  41. package/src/db/api_keys.ts +78 -0
  42. package/src/db/batch.ts +142 -0
  43. package/src/db/client.ts +81 -0
  44. package/src/db/environments.ts +127 -0
  45. package/src/db/events.ts +208 -0
  46. package/src/db/memory.ts +143 -0
  47. package/src/db/migrations.ts +295 -0
  48. package/src/db/proxy.ts +37 -0
  49. package/src/db/sessions.ts +295 -0
  50. package/src/db/vaults.ts +110 -0
  51. package/src/errors.ts +53 -0
  52. package/src/handlers/agents.ts +194 -0
  53. package/src/handlers/batch.ts +41 -0
  54. package/src/handlers/docs.ts +87 -0
  55. package/src/handlers/environments.ts +154 -0
  56. package/src/handlers/events.ts +234 -0
  57. package/src/handlers/index.ts +12 -0
  58. package/src/handlers/memory.ts +141 -0
  59. package/src/handlers/openapi.ts +14 -0
  60. package/src/handlers/sessions.ts +223 -0
  61. package/src/handlers/stream.ts +76 -0
  62. package/src/handlers/threads.ts +26 -0
  63. package/src/handlers/ui/app.js +984 -0
  64. package/src/handlers/ui/index.html +112 -0
  65. package/src/handlers/ui/style.css +164 -0
  66. package/src/handlers/ui.ts +1281 -0
  67. package/src/handlers/vaults.ts +99 -0
  68. package/src/http.ts +35 -0
  69. package/src/index.ts +104 -0
  70. package/src/init.ts +227 -0
  71. package/src/openapi/registry.ts +8 -0
  72. package/src/openapi/schemas.ts +625 -0
  73. package/src/openapi/spec.ts +691 -0
  74. package/src/providers/apple.ts +220 -0
  75. package/src/providers/daytona.ts +217 -0
  76. package/src/providers/docker.ts +264 -0
  77. package/src/providers/e2b.ts +203 -0
  78. package/src/providers/fly.ts +276 -0
  79. package/src/providers/modal.ts +222 -0
  80. package/src/providers/podman.ts +206 -0
  81. package/src/providers/registry.ts +28 -0
  82. package/src/providers/shared.ts +11 -0
  83. package/src/providers/sprites.ts +55 -0
  84. package/src/providers/types.ts +73 -0
  85. package/src/providers/vercel.ts +208 -0
  86. package/src/proxy/forward.ts +111 -0
  87. package/src/queue/index.ts +111 -0
  88. package/src/sessions/actor.ts +53 -0
  89. package/src/sessions/bus.ts +155 -0
  90. package/src/sessions/driver.ts +818 -0
  91. package/src/sessions/grader.ts +120 -0
  92. package/src/sessions/interrupt.ts +14 -0
  93. package/src/sessions/sweeper.ts +136 -0
  94. package/src/sessions/threads.ts +126 -0
  95. package/src/sessions/tools.ts +50 -0
  96. package/src/shutdown.ts +78 -0
  97. package/src/sprite/client.ts +294 -0
  98. package/src/sprite/exec.ts +161 -0
  99. package/src/sprite/lifecycle.ts +339 -0
  100. package/src/sprite/pool.ts +65 -0
  101. package/src/sprite/setup.ts +159 -0
  102. package/src/state.ts +61 -0
  103. package/src/types.ts +339 -0
  104. package/src/util/clock.ts +7 -0
  105. package/src/util/ids.ts +11 -0
@@ -0,0 +1,206 @@
1
+ /**
2
+ * Podman container provider.
3
+ *
4
+ * Nearly identical to docker.ts — uses `podman` CLI instead of `docker`.
5
+ * Podman is a drop-in replacement for Docker with the same CLI surface,
6
+ * so the only difference is the binary name in spawn calls.
7
+ *
8
+ * Container lifecycle:
9
+ * create → podman create --name {name} node:22 sleep infinity + podman start
10
+ * exec → podman exec -i {name} with stdin piped + stdout captured
11
+ * delete → podman rm -f {name}
12
+ */
13
+ import { spawn } from "node:child_process";
14
+ import { Readable } from "node:stream";
15
+ import type { ContainerProvider, ExecOptions, ExecSession } from "./types";
16
+
17
+ const DEFAULT_IMAGE = process.env.PODMAN_IMAGE ?? "node:22";
18
+ const CLI = "podman";
19
+
20
+ // ---------------------------------------------------------------------------
21
+ // Helpers
22
+ // ---------------------------------------------------------------------------
23
+
24
+ function podmanRun(
25
+ args: string[],
26
+ opts?: { stdin?: string; timeoutMs?: number },
27
+ ): Promise<string> {
28
+ return new Promise((resolve, reject) => {
29
+ const proc = spawn(CLI, args, { stdio: ["pipe", "pipe", "pipe"] });
30
+
31
+ let stdout = "";
32
+ let stderr = "";
33
+ proc.stdout?.on("data", (buf: Buffer) => { stdout += buf.toString(); });
34
+ proc.stderr?.on("data", (buf: Buffer) => { stderr += buf.toString(); });
35
+
36
+ if (opts?.stdin) {
37
+ proc.stdin?.write(opts.stdin);
38
+ }
39
+ proc.stdin?.end();
40
+
41
+ const timer = opts?.timeoutMs
42
+ ? setTimeout(() => {
43
+ proc.kill("SIGKILL");
44
+ reject(new Error(`${CLI} command timed out after ${opts.timeoutMs}ms`));
45
+ }, opts.timeoutMs)
46
+ : null;
47
+
48
+ proc.on("close", (code) => {
49
+ if (timer) clearTimeout(timer);
50
+ if (code !== 0) {
51
+ reject(new Error(`${CLI} ${args[0]} failed (${code}): ${stderr.trim()}`));
52
+ } else {
53
+ resolve(stdout);
54
+ }
55
+ });
56
+ proc.on("error", (err) => {
57
+ if (timer) clearTimeout(timer);
58
+ reject(err);
59
+ });
60
+ });
61
+ }
62
+
63
+ function podmanExecOneShot(
64
+ containerName: string,
65
+ argv: string[],
66
+ stdin?: string,
67
+ timeoutMs?: number,
68
+ ): Promise<{ stdout: string; stderr: string; exit_code: number }> {
69
+ return new Promise((resolve, reject) => {
70
+ const proc = spawn(CLI, ["exec", "-i", containerName, ...argv], {
71
+ stdio: ["pipe", "pipe", "pipe"],
72
+ });
73
+
74
+ let stdout = "";
75
+ let stderr = "";
76
+ proc.stdout?.on("data", (buf: Buffer) => { stdout += buf.toString(); });
77
+ proc.stderr?.on("data", (buf: Buffer) => { stderr += buf.toString(); });
78
+
79
+ if (stdin) proc.stdin?.write(stdin);
80
+ proc.stdin?.end();
81
+
82
+ const timer = timeoutMs
83
+ ? setTimeout(() => {
84
+ proc.kill("SIGKILL");
85
+ reject(new Error(`${CLI} exec timed out after ${timeoutMs}ms`));
86
+ }, timeoutMs)
87
+ : null;
88
+
89
+ proc.on("close", (code) => {
90
+ if (timer) clearTimeout(timer);
91
+ resolve({ stdout, stderr, exit_code: code ?? 1 });
92
+ });
93
+ proc.on("error", (err) => {
94
+ if (timer) clearTimeout(timer);
95
+ reject(err);
96
+ });
97
+ });
98
+ }
99
+
100
+ function podmanExecStreaming(
101
+ containerName: string,
102
+ opts: ExecOptions,
103
+ ): ExecSession {
104
+ const proc = spawn(CLI, ["exec", "-i", containerName, ...opts.argv], {
105
+ stdio: ["pipe", "pipe", "pipe"],
106
+ });
107
+
108
+ if (opts.stdin) proc.stdin?.write(opts.stdin);
109
+ proc.stdin?.end();
110
+
111
+ const stdout = Readable.toWeb(proc.stdout!) as ReadableStream<Uint8Array>;
112
+
113
+ let exitResolve: (v: { code: number }) => void;
114
+ let exitReject: (e: unknown) => void;
115
+ const exit = new Promise<{ code: number }>((res, rej) => {
116
+ exitResolve = res;
117
+ exitReject = rej;
118
+ });
119
+
120
+ proc.on("close", (code) => exitResolve({ code: code ?? 0 }));
121
+ proc.on("error", (err) => exitReject(err));
122
+
123
+ let timer: NodeJS.Timeout | null = null;
124
+ if (opts.timeoutMs) {
125
+ timer = setTimeout(() => {
126
+ proc.kill("SIGKILL");
127
+ exitReject(new Error(`${CLI} exec timed out after ${opts.timeoutMs}ms`));
128
+ }, opts.timeoutMs);
129
+ }
130
+ exit.finally(() => { if (timer) clearTimeout(timer); });
131
+
132
+ if (opts.signal) {
133
+ if (opts.signal.aborted) {
134
+ proc.kill("SIGTERM");
135
+ } else {
136
+ opts.signal.addEventListener("abort", () => {
137
+ proc.kill("SIGTERM");
138
+ setTimeout(() => { if (!proc.killed) proc.kill("SIGKILL"); }, 3000);
139
+ });
140
+ }
141
+ }
142
+
143
+ return {
144
+ stdout,
145
+ exit,
146
+ async kill() {
147
+ proc.kill("SIGTERM");
148
+ setTimeout(() => { if (!proc.killed) proc.kill("SIGKILL"); }, 3000);
149
+ },
150
+ };
151
+ }
152
+
153
+ // ---------------------------------------------------------------------------
154
+ // Provider
155
+ // ---------------------------------------------------------------------------
156
+
157
+ export const podmanProvider: ContainerProvider = {
158
+ name: "podman",
159
+ stripControlChars: false,
160
+
161
+ async checkAvailability() {
162
+ try {
163
+ await podmanRun(["version", "--format", "{{.Server.Version}}"], { timeoutMs: 3000 });
164
+ return { available: true };
165
+ } catch (err) {
166
+ const msg = err instanceof Error ? err.message : String(err);
167
+ if (msg.includes("ENOENT")) {
168
+ return { available: false, message: "Podman CLI is not installed. Install it from https://podman.io/docs/installation" };
169
+ }
170
+ return { available: false, message: `Podman is not running or not accessible: ${msg}` };
171
+ }
172
+ },
173
+
174
+ async create({ name }) {
175
+ await podmanRun(["create", "--name", name, DEFAULT_IMAGE, "sleep", "infinity"]);
176
+ await podmanRun(["start", name]);
177
+ },
178
+
179
+ async delete(name) {
180
+ await podmanRun(["rm", "-f", name]).catch(() => {});
181
+ },
182
+
183
+ async list(opts) {
184
+ try {
185
+ const out = await podmanRun([
186
+ "ps",
187
+ "-a",
188
+ "--filter",
189
+ `name=${opts?.prefix ?? "ca-sess-"}`,
190
+ "--format",
191
+ "{{.Names}}",
192
+ ]);
193
+ return out.trim().split("\n").filter(Boolean).map((name) => ({ name }));
194
+ } catch {
195
+ return [];
196
+ }
197
+ },
198
+
199
+ async exec(name, argv, opts) {
200
+ return podmanExecOneShot(name, argv, opts?.stdin, opts?.timeoutMs);
201
+ },
202
+
203
+ startExec(name, opts) {
204
+ return Promise.resolve(podmanExecStreaming(name, opts));
205
+ },
206
+ };
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Container provider registry.
3
+ *
4
+ * Resolves a provider by name (from environment config). Defaults to
5
+ * sprites.dev for backward compatibility. Uses lazy dynamic imports
6
+ * so optional SDK-based providers (e2b, vercel) only load when selected.
7
+ */
8
+ import type { ContainerProvider, ProviderName } from "./types";
9
+
10
+ const PROVIDERS: Record<ProviderName, () => Promise<ContainerProvider>> = {
11
+ sprites: async () => (await import("./sprites")).spritesProvider,
12
+ docker: async () => (await import("./docker")).dockerProvider,
13
+ apple: async () => (await import("./apple")).appleProvider,
14
+ podman: async () => (await import("./podman")).podmanProvider,
15
+ e2b: async () => (await import("./e2b")).e2bProvider,
16
+ vercel: async () => (await import("./vercel")).vercelProvider,
17
+ daytona: async () => (await import("./daytona")).daytonaProvider,
18
+ fly: async () => (await import("./fly")).flyProvider,
19
+ modal: async () => (await import("./modal")).modalProvider,
20
+ };
21
+
22
+ export async function resolveContainerProvider(
23
+ providerName?: string | null,
24
+ ): Promise<ContainerProvider> {
25
+ const key = (providerName ?? "sprites") as ProviderName;
26
+ const loader = PROVIDERS[key];
27
+ return loader ? loader() : PROVIDERS.sprites();
28
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Shared utilities for container providers.
3
+ */
4
+
5
+ /**
6
+ * Shell-escape a string for safe embedding in a bash command.
7
+ * Wraps in single quotes and escapes any embedded single quotes.
8
+ */
9
+ export function shellEscape(s: string): string {
10
+ return `'${s.replace(/'/g, "'\\''")}'`;
11
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Sprites.dev container provider.
3
+ *
4
+ * Wraps the existing `lib/sprite/client.ts` + `lib/sprite/exec.ts`
5
+ * functions behind the `ContainerProvider` interface. No logic changes —
6
+ * just delegation.
7
+ */
8
+ import type { ContainerProvider } from "./types";
9
+ import {
10
+ createSprite,
11
+ deleteSprite,
12
+ listSprites,
13
+ httpExec,
14
+ } from "../sprite/client";
15
+ import { startExec } from "../sprite/exec";
16
+
17
+ export const spritesProvider: ContainerProvider = {
18
+ name: "sprites",
19
+ stripControlChars: true, // sprites.dev HTTP exec multiplexes stdout/stderr with control bytes
20
+
21
+ async checkAvailability() {
22
+ const { getConfig } = await import("../config/index");
23
+ const cfg = getConfig();
24
+ if (!cfg.spriteToken) {
25
+ return { available: false, message: "sprites.dev requires SPRITE_TOKEN to be set" };
26
+ }
27
+ return { available: true };
28
+ },
29
+
30
+ async create(opts) {
31
+ await createSprite(opts);
32
+ },
33
+
34
+ async delete(name) {
35
+ await deleteSprite(name);
36
+ },
37
+
38
+ async list(opts) {
39
+ const res = await listSprites({
40
+ prefix: opts?.prefix,
41
+ max_results: 100,
42
+ });
43
+ // Flatten to simple name array — sprites' paginated response is
44
+ // handled by the sprites-specific reconcileOrphans, not here.
45
+ return res.sprites.map((s) => ({ name: s.name }));
46
+ },
47
+
48
+ async exec(name, argv, opts) {
49
+ return httpExec(name, argv, opts);
50
+ },
51
+
52
+ startExec(name, opts) {
53
+ return startExec(name, opts);
54
+ },
55
+ };
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Container provider abstraction.
3
+ *
4
+ * Both sprites.dev and Docker implement this interface. The driver,
5
+ * lifecycle, and backend setup code call these methods instead of
6
+ * directly using sprites.dev's REST API or Docker's CLI. The provider
7
+ * is selected per-environment via `EnvironmentConfig.provider`.
8
+ */
9
+
10
+ export type ProviderName = "sprites" | "docker" | "apple" | "podman" | "e2b" | "vercel" | "daytona" | "fly" | "modal";
11
+
12
+ export interface ExecOptions {
13
+ argv: string[];
14
+ stdin?: string;
15
+ signal?: AbortSignal;
16
+ timeoutMs?: number;
17
+ }
18
+
19
+ export interface ExecResult {
20
+ code: number;
21
+ }
22
+
23
+ export interface ExecSession {
24
+ /** Raw streamed stdout bytes */
25
+ stdout: ReadableStream<Uint8Array>;
26
+ /** Resolves when the process exits */
27
+ exit: Promise<ExecResult>;
28
+ /** Best-effort kill — aborts the process */
29
+ kill(): Promise<void>;
30
+ }
31
+
32
+ export interface AvailabilityResult {
33
+ available: boolean;
34
+ message?: string;
35
+ }
36
+
37
+ export interface ContainerProvider {
38
+ name: ProviderName;
39
+
40
+ /** Pre-flight check: is this provider usable right now?
41
+ * Returns { available: true } or { available: false, message: "..." }.
42
+ * Optional — providers that don't implement it are assumed available. */
43
+ checkAvailability?(): Promise<AvailabilityResult>;
44
+
45
+ /** Create and start a new container */
46
+ create(opts: { name: string }): Promise<void>;
47
+ /** Force-remove a container (best-effort, does not throw on missing) */
48
+ delete(name: string): Promise<void>;
49
+ /** List containers matching an optional prefix (flat, no pagination) */
50
+ list(opts?: { prefix?: string }): Promise<Array<{ name: string }>>;
51
+
52
+ /**
53
+ * One-shot execution: run argv in a container and wait for exit.
54
+ * Used by backend setup (install wrapper, install CLI, etc.)
55
+ */
56
+ exec(
57
+ name: string,
58
+ argv: string[],
59
+ opts?: { stdin?: string; timeoutMs?: number },
60
+ ): Promise<{ stdout: string; stderr: string; exit_code: number }>;
61
+
62
+ /**
63
+ * Streaming execution: run argv in a container and stream stdout.
64
+ * Used by the turn driver for CLI output.
65
+ */
66
+ startExec(name: string, opts: ExecOptions): Promise<ExecSession>;
67
+
68
+ /**
69
+ * Whether to strip control chars from stdout. True for sprites.dev
70
+ * (HTTP multiplexing framing bytes), false for Docker.
71
+ */
72
+ stripControlChars: boolean;
73
+ }
@@ -0,0 +1,208 @@
1
+ /**
2
+ * Vercel Sandbox provider.
3
+ *
4
+ * Uses the `@vercel/sandbox` npm package to create and manage cloud sandboxes.
5
+ * The SDK is optional — if not installed, a clear error is thrown on first use.
6
+ *
7
+ * Container lifecycle:
8
+ * create → Sandbox.create({ runtime: 'node24' })
9
+ * exec → Write stdin to file, then sandbox.runCommand('bash', ['-c', ...])
10
+ * delete → sandbox.stop()
11
+ *
12
+ * Env vars: VERCEL_TOKEN, VERCEL_TEAM_ID, VERCEL_PROJECT_ID
13
+ */
14
+ import type { ContainerProvider, ExecOptions, ExecSession } from "./types";
15
+ import { shellEscape } from "./shared";
16
+
17
+ // Lazy-loaded SDK types
18
+ type VercelSandbox = {
19
+ writeFiles(files: Record<string, string>): Promise<void>;
20
+ runCommand(
21
+ cmd: string,
22
+ args?: string[],
23
+ opts?: Record<string, unknown>,
24
+ ): Promise<{ exitCode: number; stdout: string; stderr: string }>;
25
+ stop(): Promise<void>;
26
+ };
27
+
28
+ type VercelSandboxClass = {
29
+ create(opts: {
30
+ runtime?: string;
31
+ token?: string;
32
+ teamId?: string;
33
+ projectId?: string;
34
+ }): Promise<VercelSandbox>;
35
+ };
36
+
37
+ let SandboxClass: VercelSandboxClass | null = null;
38
+
39
+ async function loadSdk(): Promise<VercelSandboxClass> {
40
+ if (SandboxClass) return SandboxClass;
41
+ try {
42
+ // @ts-ignore — optional dependency, may not be installed
43
+ const mod = await import("@vercel/sandbox");
44
+ SandboxClass = (mod.Sandbox ?? mod.default?.Sandbox ?? mod.default) as VercelSandboxClass;
45
+ if (!SandboxClass?.create) {
46
+ throw new Error("Sandbox.create not found in @vercel/sandbox exports");
47
+ }
48
+ return SandboxClass;
49
+ } catch (err) {
50
+ if (
51
+ err instanceof Error &&
52
+ (err.message.includes("Cannot find module") ||
53
+ err.message.includes("MODULE_NOT_FOUND") ||
54
+ err.message.includes("ERR_MODULE_NOT_FOUND"))
55
+ ) {
56
+ throw new Error(
57
+ "Vercel provider requires the @vercel/sandbox package. Install it with: npm install @vercel/sandbox",
58
+ );
59
+ }
60
+ throw err;
61
+ }
62
+ }
63
+
64
+ // HMR-safe sandbox instance map
65
+ type GlobalWithVercel = typeof globalThis & { __caVercelSandboxes?: Map<string, VercelSandbox> };
66
+ const g = globalThis as GlobalWithVercel;
67
+ if (!g.__caVercelSandboxes) g.__caVercelSandboxes = new Map();
68
+ const sandboxes = g.__caVercelSandboxes;
69
+
70
+ // ---------------------------------------------------------------------------
71
+ // Provider
72
+ // ---------------------------------------------------------------------------
73
+
74
+ export const vercelProvider: ContainerProvider = {
75
+ name: "vercel",
76
+ stripControlChars: false,
77
+
78
+ async checkAvailability() {
79
+ try { await loadSdk(); } catch {
80
+ return { available: false, message: "Vercel Sandbox requires the @vercel/sandbox package. Install with: npm install @vercel/sandbox" };
81
+ }
82
+ if (!process.env.VERCEL_TOKEN) {
83
+ return { available: false, message: "Vercel Sandbox requires VERCEL_TOKEN to be set" };
84
+ }
85
+ return { available: true };
86
+ },
87
+
88
+ async create({ name }) {
89
+ const Sandbox = await loadSdk();
90
+ const token = process.env.VERCEL_TOKEN;
91
+ if (!token) throw new Error("VERCEL_TOKEN environment variable is required");
92
+
93
+ const runtime = process.env.VERCEL_SANDBOX_RUNTIME ?? "node24";
94
+ const sandbox = await Sandbox.create({
95
+ runtime,
96
+ token,
97
+ teamId: process.env.VERCEL_TEAM_ID,
98
+ projectId: process.env.VERCEL_PROJECT_ID,
99
+ });
100
+ sandboxes.set(name, sandbox);
101
+ },
102
+
103
+ async delete(name) {
104
+ const sandbox = sandboxes.get(name);
105
+ if (!sandbox) return;
106
+ try {
107
+ await sandbox.stop();
108
+ } catch {
109
+ // Best-effort — sandbox may already be gone
110
+ }
111
+ sandboxes.delete(name);
112
+ },
113
+
114
+ async list(opts) {
115
+ // NOTE: @vercel/sandbox does not expose a listing API to enumerate running
116
+ // sandboxes server-side. After a server restart the in-memory map is empty,
117
+ // so previously-created sandboxes will not appear here. This is an accepted
118
+ // limitation — callers must handle missing containers gracefully (e.g.
119
+ // re-create on demand).
120
+ const prefix = opts?.prefix ?? "ca-sess-";
121
+ return Array.from(sandboxes.keys())
122
+ .filter((n) => n.startsWith(prefix))
123
+ .map((name) => ({ name }));
124
+ },
125
+
126
+ async exec(name, argv, opts) {
127
+ const sandbox = sandboxes.get(name);
128
+ if (!sandbox) throw new Error(`Vercel sandbox not found: ${name}`);
129
+
130
+ const cmd = argv.map((a) => shellEscape(a)).join(" ");
131
+
132
+ // Write stdin to a unique temp file if provided, then run cmd reading from it
133
+ if (opts?.stdin) {
134
+ const stdinPath = `/tmp/_stdin_${Date.now()}_${Math.random().toString(36).slice(2)}`;
135
+ await sandbox.writeFiles({ [stdinPath]: opts.stdin });
136
+ const result = await sandbox.runCommand("bash", [
137
+ "-c",
138
+ `cat ${shellEscape(stdinPath)} | ${cmd} ; rm -f ${shellEscape(stdinPath)}`,
139
+ ]);
140
+ return {
141
+ stdout: result.stdout,
142
+ stderr: result.stderr,
143
+ exit_code: result.exitCode,
144
+ };
145
+ }
146
+
147
+ const result = await sandbox.runCommand("bash", ["-c", cmd]);
148
+ return {
149
+ stdout: result.stdout,
150
+ stderr: result.stderr,
151
+ exit_code: result.exitCode,
152
+ };
153
+ },
154
+
155
+ async startExec(name, opts) {
156
+ const sandbox = sandboxes.get(name);
157
+ if (!sandbox) throw new Error(`Vercel sandbox not found: ${name}`);
158
+
159
+ const cmd = opts.argv.map((a) => shellEscape(a)).join(" ");
160
+
161
+ // Write stdin to a unique temp file if provided
162
+ let stdinPath: string | undefined;
163
+ if (opts.stdin) {
164
+ stdinPath = `/tmp/_stdin_${Date.now()}_${Math.random().toString(36).slice(2)}`;
165
+ await sandbox.writeFiles({ [stdinPath]: opts.stdin });
166
+ }
167
+
168
+ const fullCmd = stdinPath
169
+ ? `cat ${shellEscape(stdinPath)} | ${cmd} ; rm -f ${shellEscape(stdinPath)}`
170
+ : cmd;
171
+
172
+ const encoder = new TextEncoder();
173
+ let streamController: ReadableStreamDefaultController<Uint8Array>;
174
+
175
+ const stdout = new ReadableStream<Uint8Array>({
176
+ start(controller) {
177
+ streamController = controller;
178
+ },
179
+ });
180
+
181
+ const resultPromise = sandbox.runCommand("bash", ["-c", fullCmd]);
182
+
183
+ const exit = resultPromise.then((result) => {
184
+ try {
185
+ streamController.enqueue(encoder.encode(result.stdout));
186
+ streamController.close();
187
+ } catch {
188
+ // Already closed
189
+ }
190
+ return { code: result.exitCode };
191
+ }).catch((err) => {
192
+ try {
193
+ streamController.error(err);
194
+ } catch {
195
+ // Already closed
196
+ }
197
+ throw err;
198
+ });
199
+
200
+ return {
201
+ stdout,
202
+ exit,
203
+ async kill() {
204
+ // Vercel sandbox doesn't support killing individual commands
205
+ },
206
+ };
207
+ },
208
+ };
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Forward a request to Anthropic's hosted Managed Agents API.
3
+ *
4
+ * Swaps the caller's local API key for the server's ANTHROPIC_API_KEY,
5
+ * adds the required `anthropic-beta` header, and pipes the response
6
+ * (including SSE streams) back to the client. Works for both JSON and
7
+ * streaming responses.
8
+ *
9
+ * This is the core of the "anthropic" proxy backend — no sprite, no CLI,
10
+ * no translator. Anthropic owns the resource IDs and handles all execution.
11
+ */
12
+ import { getConfig } from "../config";
13
+ import { ApiError } from "../errors";
14
+
15
+ const ANTHROPIC_BASE = "https://api.anthropic.com";
16
+ const BETA_HEADER = "managed-agents-2026-04-01";
17
+
18
+ /**
19
+ * Forward a request to Anthropic. The caller is responsible for
20
+ * authenticating the user against the local API key table (via routeWrap)
21
+ * before calling this.
22
+ *
23
+ * @param request The original incoming Request (used for method, signal,
24
+ * and body reading if opts.body is not provided)
25
+ * @param path The MA API path (e.g. "/v1/agents" or "/v1/sessions/sess_123/events")
26
+ * @param opts.body Pre-read request body string. Required for POST routes
27
+ * that already consumed the body to inspect it (e.g. to
28
+ * check `body.backend`). If not provided and method is
29
+ * not GET, the body is read from `request.text()`.
30
+ */
31
+ export async function forwardToAnthropic(
32
+ request: Request,
33
+ path: string,
34
+ opts?: { body?: string },
35
+ ): Promise<Response> {
36
+ const cfg = getConfig();
37
+ if (!cfg.anthropicApiKey) {
38
+ throw new ApiError(
39
+ 500,
40
+ "server_error",
41
+ "ANTHROPIC_API_KEY is required for the anthropic proxy backend",
42
+ );
43
+ }
44
+
45
+ const url = new URL(path, ANTHROPIC_BASE);
46
+ // Preserve query string from the original request
47
+ const origUrl = new URL(request.url);
48
+ url.search = origUrl.search;
49
+
50
+ const headers = new Headers();
51
+ headers.set("x-api-key", cfg.anthropicApiKey);
52
+ headers.set("anthropic-beta", BETA_HEADER);
53
+
54
+ // Forward select headers from the original request
55
+ const ct = request.headers.get("content-type");
56
+ if (ct) headers.set("content-type", ct);
57
+ const lastId = request.headers.get("last-event-id");
58
+ if (lastId) headers.set("last-event-id", lastId);
59
+ const idem =
60
+ request.headers.get("idempotency-key") ||
61
+ request.headers.get("Idempotency-Key");
62
+ if (idem) headers.set("idempotency-key", idem);
63
+ const accept = request.headers.get("accept");
64
+ if (accept) headers.set("accept", accept);
65
+
66
+ // Determine request body
67
+ let body: string | undefined;
68
+ if (request.method !== "GET" && request.method !== "HEAD") {
69
+ body = opts?.body ?? (await request.text());
70
+ }
71
+
72
+ const res = await fetch(url.toString(), {
73
+ method: request.method,
74
+ headers,
75
+ body,
76
+ signal: request.signal,
77
+ });
78
+
79
+ // Pipe response back with original status + headers, stripping hop-by-hop
80
+ const HOP_BY_HOP = new Set([
81
+ "connection",
82
+ "keep-alive",
83
+ "transfer-encoding",
84
+ "te",
85
+ "trailer",
86
+ "upgrade",
87
+ ]);
88
+ const responseHeaders = new Headers();
89
+ for (const [k, v] of res.headers) {
90
+ if (!HOP_BY_HOP.has(k.toLowerCase())) {
91
+ responseHeaders.set(k, v);
92
+ }
93
+ }
94
+
95
+ return new Response(res.body, {
96
+ status: res.status,
97
+ headers: responseHeaders,
98
+ });
99
+ }
100
+
101
+ /**
102
+ * Validate that the anthropic proxy can run with the current config.
103
+ * Used at agent-create time and as a belt-and-braces check in the proxy.
104
+ */
105
+ export function validateAnthropicProxy(): string | null {
106
+ const cfg = getConfig();
107
+ if (!cfg.anthropicApiKey) {
108
+ return "anthropic proxy backend requires ANTHROPIC_API_KEY to be set";
109
+ }
110
+ return null;
111
+ }