History

Replace hand-rolled contracts/client with Elysia + Eden Treaty

Migrate executor server from raw Bun.serve() to Elysia with typed route schemas, enabling Eden Treaty to infer types directly from the server definition. This eliminates the need for separate contracts and client packages — the server is the contract. - Rewrite executor server index.ts using Elysia typed routes - Replace assistant adapter with 8-line Eden Treaty client - Delete executor/packages/contracts and executor/packages/client - Delete dead web app api.ts (migrated to Convex) - Remove tsconfig path aliases for deleted packages

Rhys Sullivan committed 9 months ago

Commit 80503a41 parent 7641ff914 files · +602 −1120Browse files

Unified · Split

14 files
  1. assistant/bun.lock +72 −0
  2. assistant/packages/agent-executor-adapter/package.json +4 −0
  3. assistant/packages/agent-executor-adapter/src/index.test.ts +87 −0
  4. assistant/packages/agent-executor-adapter/src/index.ts +5 −55
  5. assistant/packages/agent-executor-adapter/tsconfig.json +14 −0
  6. executor/apps/server/package.json +2 −0
  7. executor/apps/server/src/index.ts +384 −388
  8. executor/apps/web/src/lib/api.ts +0 −221
  9. executor/bun.lock +33 −16
  10. executor/packages/client/package.json +0 −11
  11. executor/packages/client/src/index.ts +0 −258
  12. executor/packages/contracts/package.json +0 −8
  13. executor/packages/contracts/src/index.ts +0 −154
  14. executor/tsconfig.json +1 −9
assistant/bun.lock+72−0
@@ -0,0 +1,72 @@
1+{
2+ "lockfileVersion": 1,
3+ "configVersion": 1,
4+ "workspaces": {
5+ "": {
6+ "name": "assistant-monorepo",
7+ "devDependencies": {
8+ "@types/bun": "latest",
9+ "typescript": "latest",
10+ },
11+ },
12+ "packages/agent-executor-adapter": {
13+ "name": "@assistant/agent-executor-adapter",
14+ "dependencies": {
15+ "@elysiajs/eden": "^1.4.6",
16+ "elysia": "^1.4.22",
17+ },
18+ "devDependencies": {
19+ "@types/bun": "latest",
20+ },
21+ },
22+ },
23+ "packages": {
24+ "@assistant/agent-executor-adapter": ["@assistant/agent-executor-adapter@workspace:packages/agent-executor-adapter"],
25+
26+ "@borewit/text-codec": ["@borewit/text-codec@0.2.1", "", {}, "sha512-k7vvKPbf7J2fZ5klGRD9AeKfUvojuZIQ3BT5u7Jfv+puwXkUBUT5PVyMDfJZpy30CBDXGMgw7fguK/lpOMBvgw=="],
27+
28+ "@elysiajs/eden": ["@elysiajs/eden@1.4.6", "", { "peerDependencies": { "elysia": ">=1.4.19" } }, "sha512-Tsa4NwXEWg/u73vWiYZQ3L5/ecgZSxqiEjYwpS+4qBKXeTZqZKl2hcgHJSVBL+InEDMi35Xugct7qyAXE5oM4Q=="],
29+
30+ "@sinclair/typebox": ["@sinclair/typebox@0.34.48", "", {}, "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA=="],
31+
32+ "@tokenizer/inflate": ["@tokenizer/inflate@0.4.1", "", { "dependencies": { "debug": "^4.4.3", "token-types": "^6.1.1" } }, "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA=="],
33+
34+ "@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="],
35+
36+ "@types/bun": ["@types/bun@1.3.8", "", { "dependencies": { "bun-types": "1.3.8" } }, "sha512-3LvWJ2q5GerAXYxO2mffLTqOzEu5qnhEAlh48Vnu8WQfnmSwbgagjGZV6BoHKJztENYEDn6QmVd949W4uESRJA=="],
37+
38+ "@types/node": ["@types/node@25.2.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-BkmoP5/FhRYek5izySdkOneRyXYN35I860MFAGupTdebyE66uZaR+bXLHq8k4DirE5DwQi3NuhvRU1jqTVwUrQ=="],
39+
40+ "bun-types": ["bun-types@1.3.8", "", { "dependencies": { "@types/node": "*" } }, "sha512-fL99nxdOWvV4LqjmC+8Q9kW3M4QTtTR1eePs94v5ctGqU8OeceWrSUaRw3JYb7tU3FkMIAjkueehrHPPPGKi5Q=="],
41+
42+ "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="],
43+
44+ "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
45+
46+ "elysia": ["elysia@1.4.22", "", { "dependencies": { "cookie": "^1.1.1", "exact-mirror": "^0.2.6", "fast-decode-uri-component": "^1.0.1", "memoirist": "^0.4.0" }, "peerDependencies": { "@sinclair/typebox": ">= 0.34.0 < 1", "@types/bun": ">= 1.2.0", "file-type": ">= 20.0.0", "openapi-types": ">= 12.0.0", "typescript": ">= 5.0.0" }, "optionalPeers": ["@types/bun", "typescript"] }, "sha512-Q90VCb1RVFxnFaRV0FDoSylESQQLWgLHFmWciQJdX9h3b2cSasji9KWEUvaJuy/L9ciAGg4RAhUVfsXHg5K2RQ=="],
47+
48+ "exact-mirror": ["exact-mirror@0.2.6", "", { "peerDependencies": { "@sinclair/typebox": "^0.34.15" }, "optionalPeers": ["@sinclair/typebox"] }, "sha512-7s059UIx9/tnOKSySzUk5cPGkoILhTE4p6ncf6uIPaQ+9aRBQzQjc9+q85l51+oZ+P6aBxh084pD0CzBQPcFUA=="],
49+
50+ "fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="],
51+
52+ "file-type": ["file-type@21.3.0", "", { "dependencies": { "@tokenizer/inflate": "^0.4.1", "strtok3": "^10.3.4", "token-types": "^6.1.1", "uint8array-extras": "^1.4.0" } }, "sha512-8kPJMIGz1Yt/aPEwOsrR97ZyZaD1Iqm8PClb1nYFclUCkBi0Ma5IsYNQzvSFS9ib51lWyIw5mIT9rWzI/xjpzA=="],
53+
54+ "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
55+
56+ "memoirist": ["memoirist@0.4.0", "", {}, "sha512-zxTgA0mSYELa66DimuNQDvyLq36AwDlTuVRbnQtB+VuTcKWm5Qc4z3WkSpgsFWHNhexqkIooqpv4hdcqrX5Nmg=="],
57+
58+ "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
59+
60+ "openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="],
61+
62+ "strtok3": ["strtok3@10.3.4", "", { "dependencies": { "@tokenizer/token": "^0.3.0" } }, "sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg=="],
63+
64+ "token-types": ["token-types@6.1.2", "", { "dependencies": { "@borewit/text-codec": "^0.2.1", "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww=="],
65+
66+ "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
67+
68+ "uint8array-extras": ["uint8array-extras@1.5.0", "", {}, "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A=="],
69+
70+ "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
71+ }
72+}
assistant/packages/agent-executor-adapter/package.json+4−0
@@ -8,6 +8,10 @@ · 7 unmodified lines
88 "scripts": {
99 "test": "bun test src/"
1010 },
11+ "dependencies": {
12+ "@elysiajs/eden": "^1.4.6",
13+ "elysia": "^1.4.22"
14+ },
1115 "devDependencies": {
1216 "@types/bun": "latest"
1317 }
assistant/packages/agent-executor-adapter/src/index.test.ts+87−0
@@ -0,0 +1,87 @@
1+import { expect, test } from "bun:test";
2+import { Elysia, t } from "elysia";
3+import { treaty } from "@elysiajs/eden";
4+
5+// Minimal Elysia server that mimics the executor's task creation route
6+function createTestServer() {
7+ return new Elysia()
8+ .get("/api/health", () => ({ ok: true as const, tools: 5 }))
9+ .post("/api/tasks", ({ body }) => ({
10+ taskId: "task_test_1",
11+ status: "queued" as const,
12+ }), {
13+ body: t.Object({
14+ code: t.String(),
15+ timeoutMs: t.Optional(t.Number()),
16+ runtimeId: t.Optional(t.String()),
17+ metadata: t.Optional(t.Record(t.String(), t.Unknown())),
18+ workspaceId: t.String(),
19+ actorId: t.String(),
20+ clientId: t.Optional(t.String()),
21+ }),
22+ })
23+ .post("/api/auth/anonymous/bootstrap", ({ body }) => ({
24+ sessionId: body.sessionId ?? "sess_test",
25+ workspaceId: "ws_anon",
26+ actorId: "actor_anon",
27+ clientId: "client_anon",
28+ createdAt: Date.now(),
29+ lastSeenAt: Date.now(),
30+ }), {
31+ body: t.Object({
32+ sessionId: t.Optional(t.String()),
33+ }),
34+ })
35+ .listen(0);
36+}
37+
38+test("Eden Treaty client can create a task with full type safety", async () => {
39+ const server = createTestServer();
40+ const client = treaty<typeof server>(`http://127.0.0.1:${server.server!.port}`);
41+
42+ try {
43+ const { data, error } = await client.api.tasks.post({
44+ code: "console.log('hello')",
45+ workspaceId: "ws_test",
46+ actorId: "actor_test",
47+ });
48+
49+ expect(error).toBeNull();
50+ expect(data!.taskId).toBe("task_test_1");
51+ expect(data!.status).toBe("queued");
52+ } finally {
53+ server.stop(true);
54+ }
55+});
56+
57+test("Eden Treaty client can check health", async () => {
58+ const server = createTestServer();
59+ const client = treaty<typeof server>(`http://127.0.0.1:${server.server!.port}`);
60+
61+ try {
62+ const { data, error } = await client.api.health.get();
63+
64+ expect(error).toBeNull();
65+ expect(data!.ok).toBe(true);
66+ expect(data!.tools).toBe(5);
67+ } finally {
68+ server.stop(true);
69+ }
70+});
71+
72+test("Eden Treaty client can bootstrap anonymous context", async () => {
73+ const server = createTestServer();
74+ const client = treaty<typeof server>(`http://127.0.0.1:${server.server!.port}`);
75+
76+ try {
77+ const { data, error } = await client.api.auth.anonymous.bootstrap.post({
78+ sessionId: "my-session",
79+ });
80+
81+ expect(error).toBeNull();
82+ expect(data!.workspaceId).toBe("ws_anon");
83+ expect(data!.sessionId).toBe("my-session");
84+ } finally {
85+ server.stop(true);
86+ }
87+});
assistant/packages/agent-executor-adapter/src/index.ts+5−55
@@ -1,58 +1,8 @@
1−export interface RunCodeRequest {
2− code: string;
3− timeoutMs?: number;
4− runtimeId?: string;
5−}
6−
7−export interface RunCodeResponse {
8− taskId: string;
9−}
10−
11−export async function runCode(
12− executorBaseUrl: string,
13− request: RunCodeRequest,
14−): Promise<RunCodeResponse> {
15− const response = await fetch(`${executorBaseUrl.replace(/\/$/, "")}/api/tasks`, {
16− method: "POST",
17− headers: { "content-type": "application/json" },
18− body: JSON.stringify(request),
19− });
20−
21− if (!response.ok) {
22− throw new Error(`Failed to create executor task: HTTP ${response.status}`);
23− }
1+import { treaty } from "@elysiajs/eden";
2+import type { App } from "@executor/server/src/index";
243
25− const payload = (await response.json()) as { taskId: string };
26− return { taskId: payload.taskId };
4+export function createExecutorClient(baseUrl: string) {
5+ return treaty<App>(baseUrl);
276 }
287
29−export function subscribeToTaskEvents(
30− executorBaseUrl: string,
31− taskId: string,
32− onMessage: (eventType: string, data: unknown) => void,
33−): EventSource {
34− const streamUrl = `${executorBaseUrl.replace(/\/$/, "")}/api/tasks/${encodeURIComponent(taskId)}/events`;
35− const source = new EventSource(streamUrl);
36−
37− source.onmessage = (event) => {
38− let data: unknown = event.data;
39− try {
40− data = JSON.parse(event.data);
41− } catch {
42− // Keep raw message if JSON parse fails.
43− }
44− onMessage("message", data);
45− };
46−
47− source.addEventListener("task", (event) => {
48− const message = event as MessageEvent<string>;
49− onMessage("task", JSON.parse(message.data));
50− });
51−
52− source.addEventListener("approval", (event) => {
53− const message = event as MessageEvent<string>;
54− onMessage("approval", JSON.parse(message.data));
55− });
56−
57− return source;
58−}
8+export type ExecutorClient = ReturnType<typeof createExecutorClient>;
assistant/packages/agent-executor-adapter/tsconfig.json+14−0
@@ -0,0 +1,14 @@
1+{
2+ "compilerOptions": {
3+ "target": "ESNext",
4+ "module": "ESNext",
5+ "moduleResolution": "bundler",
6+ "strict": true,
7+ "esModuleInterop": true,
8+ "skipLibCheck": true,
9+ "paths": {
10+ "@executor/server/*": ["../../../executor/apps/server/*"]
11+ }
12+ },
13+ "include": ["src"]
14+}
executor/apps/server/package.json+2−0
@@ -4,9 +4,11 @@ · 3 unmodified lines
44 "type": "module",
55 "dependencies": {
66 "@apidevtools/swagger-parser": "^12.1.0",
7+ "@elysiajs/eden": "^1.4.6",
78 "@modelcontextprotocol/sdk": "^1.26.0",
89 "@vercel/sandbox": "latest",
910 "convex": "latest",
11+ "elysia": "^1.4.22",
1012 "zod": "^4.3.6"
1113 },
1214 "scripts": {
executor/apps/server/src/index.ts+384−388
@@ -1,3 +1,4 @@
1+import { Elysia, t } from "elysia";
12 import { ExecutorDatabase } from "./database";
23 import { TaskEventHub } from "./events";
34 import { handleMcpRequest } from "./mcp-server";
@@ -9,15 +10,13 @@ import { loadExternalTools, parseToolSourcesFromEnv } from "./tool-sources"; · 5 unmodified lines
910 import { DEFAULT_TOOLS } from "./tools";
1011 import type {
1112 ApprovalStatus,
12− CredentialScope,
13− CreateTaskInput,
14− PendingApprovalRecord,
15− PolicyDecision,
1613 TaskStatus,
1714 ToolCallRequest,
1815 RuntimeOutputEvent,
1916 } from "./types";
2017
18+// ── Bootstrap ──
19+
2120 const port = Number(Bun.env.PORT ?? "4001");
2221 const autoFunnelEnabled = Bun.env.EXECUTOR_AUTO_TAILSCALE_FUNNEL !== "0";
2322 const explicitInternalBaseUrl = Bun.env.EXECUTOR_INTERNAL_BASE_URL ?? Bun.env.EXECUTOR_PUBLIC_BASE_URL;
@@ -65,51 +64,9 @@ const service = new ExecutorService(new ExecutorDatabase(), new TaskEventHub(), · 41 unmodified lines
6564 autoExecuteTasks: Bun.env.EXECUTOR_SERVER_AUTO_EXECUTE === "1",
6665 });
6766
68−const jsonHeaders = {
69− "content-type": "application/json; charset=utf-8",
70− "access-control-allow-origin": "*",
71− "access-control-allow-methods": "GET,POST,DELETE,OPTIONS",
72− "access-control-allow-headers": "content-type",
73−};
74−
75−function json(data: unknown, status = 200): Response {
76− return new Response(JSON.stringify(data), {
77− status,
78− headers: jsonHeaders,
79− });
80−}
81−
82−function error(status: number, message: string): Response {
83− return json({ error: message }, status);
84−}
85−
86−function isInternalAuthorized(request: Request): boolean {
87− if (!internalToken) {
88− return true;
89− }
90−
91− const header = request.headers.get("authorization");
92− if (!header || !header.startsWith("Bearer ")) {
93− return false;
94− }
95−
96− return header.slice("Bearer ".length) === internalToken;
97−}
98−
99−async function parseBody<T>(request: Request): Promise<T | null> {
100− try {
101− return (await request.json()) as T;
102− } catch {
103− return null;
104− }
105−}
106−
107−async function createTaskEventsResponse(taskId: string): Promise<Response> {
108− const task = await service.getTask(taskId);
109− if (!task) {
110− return error(404, "Task not found");
111− }
67+// ── SSE helper ──
11268
69+function createTaskEventsResponse(taskId: string): Response | Promise<Response> {
11370 const encoder = new TextEncoder();
11471 let unsubscribe: (() => void) | undefined;
11572 let keepalive: ReturnType<typeof setInterval> | undefined;
@@ -117,6 +74,12 @@ async function createTaskEventsResponse(taskId: string): Promise<Response> { · 1 unmodified line
11774 const stream = new ReadableStream<Uint8Array>({
11875 start(controller) {
11976 void (async () => {
77+ const task = await service.getTask(taskId);
78+ if (!task) {
79+ controller.close();
80+ return;
81+ }
82+
12083 const replay = await service.listTaskEvents(taskId);
12184 for (const event of replay) {
12285 const frame = `event: ${event.eventName}\ndata: ${JSON.stringify(event)}\n\n`;
@@ -179,359 +142,392 @@ async function createTaskEventsResponse(taskId: string): Promise<Response> { · 56 unmodified lines
179142 });
180143 }
181144
182−const server = Bun.serve({
183− port,
184− routes: {
185− "/mcp": {
186− POST: async (request) => await handleMcpRequest(service, request),
187− GET: async (request) => await handleMcpRequest(service, request),
188− DELETE: async (request) => await handleMcpRequest(service, request),
189− },
190− "/api/health": {
191− GET: () => json({ ok: true, tools: service.getBaseToolCount() }),
192− },
193− "/api/auth/anonymous/bootstrap": {
194− POST: async (request) => {
195− const body = await parseBody<{ sessionId?: string }>(request);
196− const context = await service.bootstrapAnonymousContext(body?.sessionId);
197− return json(context, 201);
198− },
199− },
200− "/api/runtime-targets": {
201− GET: () => json(service.listRuntimes()),
202− },
203− "/api/tools": {
204− GET: async (request) => {
205− const query = new URL(request.url).searchParams;
206− const workspaceId = query.get("workspaceId");
207− const actorId = query.get("actorId") ?? undefined;
208− const clientId = query.get("clientId") ?? undefined;
209−
210− if (!workspaceId) {
211− return error(400, "workspaceId is required");
212− }
213−
214− return json(await service.listTools({ workspaceId, actorId, clientId }));
215− },
216− },
217− "/api/tool-sources": {
218− GET: async (request) => {
219− const workspaceId = new URL(request.url).searchParams.get("workspaceId");
220− if (!workspaceId) {
221− return error(400, "workspaceId is required");
222− }
223− return json(await service.listToolSources(workspaceId));
224− },
225− POST: async (request) => {
226− const body = await parseBody<{
227− id?: string;
228− workspaceId?: string;
229− name?: string;
230− type?: "mcp" | "openapi" | "graphql";
231− config?: Record<string, unknown>;
232− enabled?: boolean;
233− }>(request);
234−
235− if (!body || !body.workspaceId || !body.name || !body.type || !body.config) {
236− return error(400, "workspaceId, name, type, and config are required");
237− }
238−
239− if (body.type !== "mcp" && body.type !== "openapi" && body.type !== "graphql") {
240− return error(400, "type must be 'mcp', 'openapi', or 'graphql'");
241− }
242−
243− try {
244− const source = await service.upsertToolSource({
245− id: body.id,
246− workspaceId: body.workspaceId,
247− name: body.name,
248− type: body.type,
249− config: body.config,
250− enabled: body.enabled,
251− });
252− return json(source, body.id ? 200 : 201);
253− } catch (cause) {
254− return error(400, cause instanceof Error ? cause.message : String(cause));
255− }
256− },
257− },
258− "/api/tool-sources/:sourceId": {
259− DELETE: async (request) => {
260− const workspaceId = new URL(request.url).searchParams.get("workspaceId");
261− if (!workspaceId) {
262− return error(400, "workspaceId is required");
263− }
264−
265− const deleted = await service.deleteToolSource(workspaceId, request.params.sourceId);
266− if (!deleted) {
267− return error(404, "Tool source not found");
268− }
269− return json({ ok: true });
270− },
271− },
272− "/api/tasks": {
273− GET: async (request) => {
274− const workspaceId = new URL(request.url).searchParams.get("workspaceId");
275− if (!workspaceId) {
276− return error(400, "workspaceId is required");
277− }
278− return json(await service.listTasks(workspaceId));
279− },
280− POST: async (request) => {
281− const body = await parseBody<CreateTaskInput>(request);
282− if (!body) {
283− return error(400, "Invalid JSON body");
284− }
285−
286− try {
287− const created = await service.createTask(body);
288− return json({ taskId: created.task.id, status: created.task.status }, 201);
289− } catch (cause) {
290− return error(400, cause instanceof Error ? cause.message : String(cause));
291− }
292− },
293− },
294− "/api/tasks/:taskId": {
295− GET: async (request) => {
296− const workspaceId = new URL(request.url).searchParams.get("workspaceId");
297− if (!workspaceId) {
298− return error(400, "workspaceId is required");
299− }
300−
301− const task = await service.getTask(request.params.taskId, workspaceId);
302− if (!task) {
303− return error(404, "Task not found");
304− }
305− return json(task);
306− },
307− },
308− "/api/tasks/:taskId/events": {
309− GET: async (request) => {
310− const workspaceId = new URL(request.url).searchParams.get("workspaceId");
311− if (!workspaceId) {
312− return error(400, "workspaceId is required");
313− }
314−
315− const task = await service.getTask(request.params.taskId, workspaceId);
316− if (!task) {
317− return error(404, "Task not found");
318− }
319− return await createTaskEventsResponse(request.params.taskId);
320− },
321− },
322− "/api/approvals": {
323− GET: async (request) => {
324− const query = new URL(request.url).searchParams;
325− const workspaceId = query.get("workspaceId");
326− if (!workspaceId) {
327− return error(400, "workspaceId is required");
328− }
329− const status = query.get("status") as ApprovalStatus | null;
330−
331− if (status === "pending") {
332− return json((await service.listPendingApprovals(workspaceId)) satisfies PendingApprovalRecord[]);
333− }
334−
335− if (status && status !== "approved" && status !== "denied") {
336− return error(400, "Invalid approval status");
337− }
145+// ── Internal auth guard ──
338146
339− return json(await service.listApprovals(workspaceId, status ?? undefined));
340− },
341− },
342− "/api/approvals/:approvalId": {
343− POST: async (request) => {
344− const body = await parseBody<{
345− workspaceId?: string;
346− decision?: "approved" | "denied";
347− reviewerId?: string;
348− reason?: string;
349− }>(request);
350−
351− if (!body || !body.workspaceId || (body.decision !== "approved" && body.decision !== "denied")) {
352− return error(400, "workspaceId and decision are required");
353− }
354−
355− const resolved = await service.resolveApproval(
356− body.workspaceId,
357− request.params.approvalId,
358− body.decision,
359− body.reviewerId,
360− body.reason,
361− );
362−
363− if (!resolved) {
364− return error(404, "Approval not found or already resolved");
365− }
366−
367− return json(resolved);
368− },
369− },
370− "/api/policies": {
371− GET: async (request) => {
372− const query = new URL(request.url).searchParams;
373− const workspaceId = query.get("workspaceId");
374− if (!workspaceId) {
375− return error(400, "workspaceId is required");
376− }
377− return json(await service.listAccessPolicies(workspaceId));
378− },
379− POST: async (request) => {
380− const body = await parseBody<{
381− id?: string;
382− workspaceId?: string;
383− actorId?: string;
384− clientId?: string;
385− toolPathPattern?: string;
386− decision?: PolicyDecision;
387− priority?: number;
388− }>(request);
389−
390− if (!body || !body.workspaceId || !body.toolPathPattern || !body.decision) {
391− return error(400, "workspaceId, toolPathPattern, and decision are required");
392− }
393−
394− if (body.decision !== "allow" && body.decision !== "require_approval" && body.decision !== "deny") {
395− return error(400, "Invalid decision");
396− }
397−
398− return json(await service.upsertAccessPolicy({
399− id: body.id,
400− workspaceId: body.workspaceId,
401− actorId: body.actorId,
402− clientId: body.clientId,
403− toolPathPattern: body.toolPathPattern,
404− decision: body.decision,
405− priority: body.priority,
406− }), body.id ? 200 : 201);
407− },
408− },
409− "/api/credentials": {
410− GET: async (request) => {
411− const query = new URL(request.url).searchParams;
412− const workspaceId = query.get("workspaceId");
413− if (!workspaceId) {
414− return error(400, "workspaceId is required");
415− }
416− return json(await service.listCredentials(workspaceId));
417− },
418− POST: async (request) => {
419− const body = await parseBody<{
420− id?: string;
421− workspaceId?: string;
422− sourceKey?: string;
423− scope?: CredentialScope;
424− actorId?: string;
425− secretJson?: Record<string, unknown>;
426− }>(request);
427−
428− if (!body || !body.workspaceId || !body.sourceKey || !body.scope || !body.secretJson) {
429− return error(400, "workspaceId, sourceKey, scope, and secretJson are required");
430− }
431−
432− if (body.scope !== "workspace" && body.scope !== "actor") {
433− return error(400, "scope must be 'workspace' or 'actor'");
434− }
435−
436− if (body.scope === "actor" && (!body.actorId || body.actorId.trim().length === 0)) {
437− return error(400, "actorId is required for actor-scoped credential");
438− }
147+function isInternalAuthorized(request: Request): boolean {
148+ if (!internalToken) return true;
149+ const header = request.headers.get("authorization");
150+ if (!header || !header.startsWith("Bearer ")) return false;
151+ return header.slice("Bearer ".length) === internalToken;
152+}
439153
440− const credential = await service.upsertCredential({
441− id: body.id,
442− workspaceId: body.workspaceId,
443− sourceKey: body.sourceKey,
444− scope: body.scope,
445− actorId: body.actorId,
446− secretJson: body.secretJson,
447− });
154+// ── Elysia app ──
155+
156+const app = new Elysia()
157+ // CORS
158+ .onRequest(({ set }) => {
159+ set.headers["access-control-allow-origin"] = "*";
160+ set.headers["access-control-allow-methods"] = "GET,POST,DELETE,OPTIONS";
161+ set.headers["access-control-allow-headers"] = "content-type,authorization";
162+ })
163+ .options("/*", () => new Response(null, { status: 204 }))
164+
165+ // ── MCP (raw protocol passthrough) ──
166+ .post("/mcp", async ({ request }) => await handleMcpRequest(service, request))
167+ .get("/mcp", async ({ request }) => await handleMcpRequest(service, request))
168+ .delete("/mcp", async ({ request }) => await handleMcpRequest(service, request))
169+
170+ // ── Health ──
171+ .get("/api/health", () => ({
172+ ok: true as const,
173+ tools: service.getBaseToolCount(),
174+ }))
175+
176+ // ── Auth ──
177+ .post("/api/auth/anonymous/bootstrap", async ({ body }) => {
178+ return await service.bootstrapAnonymousContext(body.sessionId);
179+ }, {
180+ body: t.Object({
181+ sessionId: t.Optional(t.String()),
182+ }),
183+ })
184+
185+ // ── Runtime Targets ──
186+ .get("/api/runtime-targets", () => service.listRuntimes())
187+
188+ // ── Tools ──
189+ .get("/api/tools", async ({ query, set }) => {
190+ if (!query.workspaceId) {
191+ set.status = 400;
192+ return { error: "workspaceId is required" };
193+ }
194+ return await service.listTools({
195+ workspaceId: query.workspaceId,
196+ actorId: query.actorId,
197+ clientId: query.clientId,
198+ });
199+ }, {
200+ query: t.Object({
201+ workspaceId: t.String(),
202+ actorId: t.Optional(t.String()),
203+ clientId: t.Optional(t.String()),
204+ }),
205+ })
206+
207+ // ── Tool Sources ──
208+ .get("/api/tool-sources", async ({ query, set }) => {
209+ if (!query.workspaceId) {
210+ set.status = 400;
211+ return { error: "workspaceId is required" };
212+ }
213+ return await service.listToolSources(query.workspaceId);
214+ }, {
215+ query: t.Object({
216+ workspaceId: t.String(),
217+ }),
218+ })
219+
220+ .post("/api/tool-sources", async ({ body, set }) => {
221+ if (!body.workspaceId || !body.name || !body.type || !body.config) {
222+ set.status = 400;
223+ return { error: "workspaceId, name, type, and config are required" };
224+ }
225+ try {
226+ return await service.upsertToolSource({
227+ id: body.id,
228+ workspaceId: body.workspaceId,
229+ name: body.name,
230+ type: body.type,
231+ config: body.config,
232+ enabled: body.enabled,
233+ });
234+ } catch (cause) {
235+ set.status = 400;
236+ return { error: cause instanceof Error ? cause.message : String(cause) };
237+ }
238+ }, {
239+ body: t.Object({
240+ id: t.Optional(t.String()),
241+ workspaceId: t.String(),
242+ name: t.String(),
243+ type: t.Union([t.Literal("mcp"), t.Literal("openapi"), t.Literal("graphql")]),
244+ config: t.Record(t.String(), t.Unknown()),
245+ enabled: t.Optional(t.Boolean()),
246+ }),
247+ })
248+
249+ .delete("/api/tool-sources/:sourceId", async ({ params, query, set }) => {
250+ if (!query.workspaceId) {
251+ set.status = 400;
252+ return { error: "workspaceId is required" };
253+ }
254+ const deleted = await service.deleteToolSource(query.workspaceId, params.sourceId);
255+ if (!deleted) {
256+ set.status = 404;
257+ return { error: "Tool source not found" };
258+ }
259+ return { ok: true as const };
260+ }, {
261+ params: t.Object({ sourceId: t.String() }),
262+ query: t.Object({ workspaceId: t.String() }),
263+ })
264+
265+ // ── Tasks ──
266+ .get("/api/tasks", async ({ query, set }) => {
267+ if (!query.workspaceId) {
268+ set.status = 400;
269+ return { error: "workspaceId is required" };
270+ }
271+ return await service.listTasks(query.workspaceId);
272+ }, {
273+ query: t.Object({ workspaceId: t.String() }),
274+ })
275+
276+ .post("/api/tasks", async ({ body, set }) => {
277+ try {
278+ const created = await service.createTask(body);
279+ return { taskId: created.task.id, status: created.task.status };
280+ } catch (cause) {
281+ set.status = 400;
282+ return { error: cause instanceof Error ? cause.message : String(cause) };
283+ }
284+ }, {
285+ body: t.Object({
286+ code: t.String(),
287+ timeoutMs: t.Optional(t.Number()),
288+ runtimeId: t.Optional(t.String()),
289+ metadata: t.Optional(t.Record(t.String(), t.Unknown())),
290+ workspaceId: t.String(),
291+ actorId: t.String(),
292+ clientId: t.Optional(t.String()),
293+ }),
294+ })
295+
296+ .get("/api/tasks/:taskId", async ({ params, query, set }) => {
297+ if (!query.workspaceId) {
298+ set.status = 400;
299+ return { error: "workspaceId is required" };
300+ }
301+ const task = await service.getTask(params.taskId, query.workspaceId);
302+ if (!task) {
303+ set.status = 404;
304+ return { error: "Task not found" };
305+ }
306+ return task;
307+ }, {
308+ params: t.Object({ taskId: t.String() }),
309+ query: t.Object({ workspaceId: t.String() }),
310+ })
311+
312+ .get("/api/tasks/:taskId/events", async ({ params, query, set }) => {
313+ if (!query.workspaceId) {
314+ set.status = 400;
315+ return { error: "workspaceId is required" };
316+ }
317+ const task = await service.getTask(params.taskId, query.workspaceId);
318+ if (!task) {
319+ set.status = 404;
320+ return { error: "Task not found" };
321+ }
322+ return createTaskEventsResponse(params.taskId);
323+ }, {
324+ params: t.Object({ taskId: t.String() }),
325+ query: t.Object({ workspaceId: t.String() }),
326+ })
327+
328+ // ── Approvals ──
329+ .get("/api/approvals", async ({ query, set }) => {
330+ if (!query.workspaceId) {
331+ set.status = 400;
332+ return { error: "workspaceId is required" };
333+ }
334+ const status = query.status as ApprovalStatus | undefined;
448335
449− return json({
450− id: credential.id,
451− workspaceId: credential.workspaceId,
452− sourceKey: credential.sourceKey,
453− scope: credential.scope,
454− actorId: credential.actorId,
455− hasSecret: true,
456− }, body.id ? 200 : 201);
457− },
458− },
459− "/internal/runs/:runId/tool-call": {
460− POST: async (request) => {
461− if (!isInternalAuthorized(request)) {
462− return error(401, "Unauthorized internal call");
463− }
336+ if (status === "pending") {
337+ return await service.listPendingApprovals(query.workspaceId);
338+ }
464339
465− const body = await parseBody<{
466− callId?: string;
467− toolPath?: string;
468− input?: unknown;
469− }>(request);
340+ if (status && status !== "approved" && status !== "denied") {
341+ set.status = 400;
342+ return { error: "Invalid approval status" };
343+ }
470344
471− if (!body || !body.callId || !body.toolPath) {
472− return error(400, "callId and toolPath are required");
473− }
345+ return await service.listApprovals(query.workspaceId, status ?? undefined);
346+ }, {
347+ query: t.Object({
348+ workspaceId: t.String(),
349+ status: t.Optional(t.String()),
350+ }),
351+ })
352+
353+ .post("/api/approvals/:approvalId", async ({ params, body, set }) => {
354+ if (!body.workspaceId || (body.decision !== "approved" && body.decision !== "denied")) {
355+ set.status = 400;
356+ return { error: "workspaceId and decision are required" };
357+ }
474358
475− const call: ToolCallRequest = {
476− runId: request.params.runId,
477− callId: body.callId,
478− toolPath: body.toolPath,
479− input: body.input,
480− };
359+ const resolved = await service.resolveApproval(
360+ body.workspaceId,
361+ params.approvalId,
362+ body.decision,
363+ body.reviewerId,
364+ body.reason,
365+ );
366+
367+ if (!resolved) {
368+ set.status = 404;
369+ return { error: "Approval not found or already resolved" };
370+ }
371+ return resolved;
372+ }, {
373+ params: t.Object({ approvalId: t.String() }),
374+ body: t.Object({
375+ workspaceId: t.String(),
376+ decision: t.Union([t.Literal("approved"), t.Literal("denied")]),
377+ reviewerId: t.Optional(t.String()),
378+ reason: t.Optional(t.String()),
379+ }),
380+ })
381+
382+ // ── Policies ──
383+ .get("/api/policies", async ({ query, set }) => {
384+ if (!query.workspaceId) {
385+ set.status = 400;
386+ return { error: "workspaceId is required" };
387+ }
388+ return await service.listAccessPolicies(query.workspaceId);
389+ }, {
390+ query: t.Object({ workspaceId: t.String() }),
391+ })
392+
393+ .post("/api/policies", async ({ body, set }) => {
394+ if (!body.workspaceId || !body.toolPathPattern || !body.decision) {
395+ set.status = 400;
396+ return { error: "workspaceId, toolPathPattern, and decision are required" };
397+ }
398+ return await service.upsertAccessPolicy({
399+ id: body.id,
400+ workspaceId: body.workspaceId,
401+ actorId: body.actorId,
402+ clientId: body.clientId,
403+ toolPathPattern: body.toolPathPattern,
404+ decision: body.decision,
405+ priority: body.priority,
406+ });
407+ }, {
408+ body: t.Object({
409+ id: t.Optional(t.String()),
410+ workspaceId: t.String(),
411+ actorId: t.Optional(t.String()),
412+ clientId: t.Optional(t.String()),
413+ toolPathPattern: t.String(),
414+ decision: t.Union([t.Literal("allow"), t.Literal("require_approval"), t.Literal("deny")]),
415+ priority: t.Optional(t.Number()),
416+ }),
417+ })
418+
419+ // ── Credentials ──
420+ .get("/api/credentials", async ({ query, set }) => {
421+ if (!query.workspaceId) {
422+ set.status = 400;
423+ return { error: "workspaceId is required" };
424+ }
425+ return await service.listCredentials(query.workspaceId);
426+ }, {
427+ query: t.Object({ workspaceId: t.String() }),
428+ })
429+
430+ .post("/api/credentials", async ({ body, set }) => {
431+ if (!body.workspaceId || !body.sourceKey || !body.scope || !body.secretJson) {
432+ set.status = 400;
433+ return { error: "workspaceId, sourceKey, scope, and secretJson are required" };
434+ }
435+ if (body.scope === "actor" && (!body.actorId || body.actorId.trim().length === 0)) {
436+ set.status = 400;
437+ return { error: "actorId is required for actor-scoped credential" };
438+ }
481439
482− return json(await service.handleExternalToolCall(call));
483− },
484− },
485− "/internal/runs/:runId/output": {
486− POST: async (request) => {
487− if (!isInternalAuthorized(request)) {
488− return error(401, "Unauthorized internal call");
489− }
440+ const credential = await service.upsertCredential({
441+ id: body.id,
442+ workspaceId: body.workspaceId,
443+ sourceKey: body.sourceKey,
444+ scope: body.scope,
445+ actorId: body.actorId,
446+ secretJson: body.secretJson,
447+ });
448+
449+ return {
450+ id: credential.id,
451+ workspaceId: credential.workspaceId,
452+ sourceKey: credential.sourceKey,
453+ scope: credential.scope,
454+ actorId: credential.actorId,
455+ hasSecret: true as const,
456+ };
457+ }, {
458+ body: t.Object({
459+ id: t.Optional(t.String()),
460+ workspaceId: t.String(),
461+ sourceKey: t.String(),
462+ scope: t.Union([t.Literal("workspace"), t.Literal("actor")]),
463+ actorId: t.Optional(t.String()),
464+ secretJson: t.Record(t.String(), t.Unknown()),
465+ }),
466+ })
467+
468+ // ── Internal endpoints (bearer auth protected) ──
469+ .post("/internal/runs/:runId/tool-call", async ({ params, body, request, set }) => {
470+ if (!isInternalAuthorized(request)) {
471+ set.status = 401;
472+ return { error: "Unauthorized internal call" };
473+ }
474+ if (!body.callId || !body.toolPath) {
475+ set.status = 400;
476+ return { error: "callId and toolPath are required" };
477+ }
490478
491− const body = await parseBody<{
492− stream?: "stdout" | "stderr";
493− line?: string;
494− timestamp?: number;
495− }>(request);
479+ const call: ToolCallRequest = {
480+ runId: params.runId,
481+ callId: body.callId,
482+ toolPath: body.toolPath,
483+ input: body.input,
484+ };
485+
486+ return await service.handleExternalToolCall(call);
487+ }, {
488+ params: t.Object({ runId: t.String() }),
489+ body: t.Object({
490+ callId: t.String(),
491+ toolPath: t.String(),
492+ input: t.Optional(t.Unknown()),
493+ }),
494+ })
495+
496+ .post("/internal/runs/:runId/output", async ({ params, body, request, set }) => {
497+ if (!isInternalAuthorized(request)) {
498+ set.status = 401;
499+ return { error: "Unauthorized internal call" };
500+ }
496501
497− if (!body || (body.stream !== "stdout" && body.stream !== "stderr") || typeof body.line !== "string") {
498− return error(400, "stream and line are required");
499− }
502+ const event: RuntimeOutputEvent = {
503+ runId: params.runId,
504+ stream: body.stream,
505+ line: body.line,
506+ timestamp: typeof body.timestamp === "number" ? body.timestamp : Date.now(),
507+ };
508+
509+ const task = await service.getTask(event.runId);
510+ if (!task) {
511+ set.status = 404;
512+ return { error: `Run not found: ${event.runId}` };
513+ }
500514
501− const event: RuntimeOutputEvent = {
502− runId: request.params.runId,
503− stream: body.stream,
504− line: body.line,
505− timestamp: typeof body.timestamp === "number" ? body.timestamp : Date.now(),
506− };
515+ await service.appendRuntimeOutput(event);
516+ return { ok: true as const };
517+ }, {
518+ params: t.Object({ runId: t.String() }),
519+ body: t.Object({
520+ stream: t.Union([t.Literal("stdout"), t.Literal("stderr")]),
521+ line: t.String(),
522+ timestamp: t.Optional(t.Number()),
523+ }),
524+ })
507525
508− const task = await service.getTask(event.runId);
509− if (!task) {
510− return error(404, `Run not found: ${event.runId}`);
511− }
526+ .listen(port);
512527
513− await service.appendRuntimeOutput(event);
514− return json({ ok: true });
515− },
516− },
517− },
518− fetch(request) {
519− if (request.method === "OPTIONS") {
520− return new Response(null, { status: 204, headers: jsonHeaders });
521− }
522− return new Response("Not found", { status: 404 });
523− },
524− error(cause) {
525− console.error("executor server error", cause);
526− return error(500, "Internal server error");
527− },
528− development: {
529− hmr: true,
530− console: true,
531− },
532−});
528+export type App = typeof app;
533529
534−console.log(`executor server listening on http://localhost:${server.port}`);
530+console.log(`executor server listening on http://localhost:${app.server!.port}`);
535531 console.log(`[executor] internal callback base: ${internalBaseUrl} (${internalBaseSource})`);
536532 console.log(`[executor] internal callback auth token enabled: yes`);
537533 console.log(`[executor] tools loaded: ${tools.length} (${externalTools.length} external)`);
executor/apps/web/src/lib/api.ts+0−221
@@ -1,221 +0,0 @@
1−// Thin API client that calls through the Next.js rewrite proxy (/api/* -> backend)
2−
3−import type {
4− AnonymousContext,
5− TaskRecord,
6− CreateTaskRequest,
7− CreateTaskResponse,
8− ApprovalRecord,
9− PendingApprovalRecord,
10− ResolveApprovalRequest,
11− RuntimeTargetDescriptor,
12− ToolDescriptor,
13− ToolSourceRecord,
14− AccessPolicyRecord,
15− CredentialDescriptor,
16−} from "./types";
17−
18−const BASE = "";
19−
20−async function json<T>(res: Response): Promise<T> {
21− if (!res.ok) {
22− const text = await res.text();
23− throw new Error(`${res.status} ${res.statusText}: ${text}`);
24− }
25− return res.json() as Promise<T>;
26−}
27−
28−// ── Auth ──
29−
30−export async function bootstrapAnonymousContext(
31− sessionId?: string,
32−): Promise<AnonymousContext> {
33− const res = await fetch(`${BASE}/api/auth/anonymous/bootstrap`, {
34− method: "POST",
35− headers: { "content-type": "application/json" },
36− body: JSON.stringify({ sessionId }),
37− });
38− return json<AnonymousContext>(res);
39−}
40−
41−// ── Tasks ──
42−
43−export async function createTask(
44− request: CreateTaskRequest,
45−): Promise<CreateTaskResponse> {
46− const res = await fetch(`${BASE}/api/tasks`, {
47− method: "POST",
48− headers: { "content-type": "application/json" },
49− body: JSON.stringify(request),
50− });
51− return json<CreateTaskResponse>(res);
52−}
53−
54−export async function listTasks(workspaceId: string): Promise<TaskRecord[]> {
55− const res = await fetch(
56− `${BASE}/api/tasks?workspaceId=${encodeURIComponent(workspaceId)}`,
57− );
58− return json<TaskRecord[]>(res);
59−}
60−
61−export async function getTask(
62− taskId: string,
63− workspaceId: string,
64−): Promise<TaskRecord> {
65− const res = await fetch(
66− `${BASE}/api/tasks/${encodeURIComponent(taskId)}?workspaceId=${encodeURIComponent(workspaceId)}`,
67− );
68− return json<TaskRecord>(res);
69−}
70−
71−// ── Approvals ──
72−
73−export async function listPendingApprovals(
74− workspaceId: string,
75−): Promise<PendingApprovalRecord[]> {
76− const res = await fetch(
77− `${BASE}/api/approvals?workspaceId=${encodeURIComponent(workspaceId)}&status=pending`,
78− );
79− return json<PendingApprovalRecord[]>(res);
80−}
81−
82−export async function listApprovals(
83− workspaceId: string,
84− status?: string,
85−): Promise<ApprovalRecord[]> {
86− const params = new URLSearchParams({ workspaceId });
87− if (status) params.set("status", status);
88− const res = await fetch(`${BASE}/api/approvals?${params.toString()}`);
89− return json<ApprovalRecord[]>(res);
90−}
91−
92−export async function resolveApproval(
93− approvalId: string,
94− request: ResolveApprovalRequest,
95−): Promise<{ approval: ApprovalRecord; task: TaskRecord }> {
96− const res = await fetch(
97− `${BASE}/api/approvals/${encodeURIComponent(approvalId)}`,
98− {
99− method: "POST",
100− headers: { "content-type": "application/json" },
101− body: JSON.stringify(request),
102− },
103− );
104− return json<{ approval: ApprovalRecord; task: TaskRecord }>(res);
105−}
106−
107−// ── Runtimes ──
108−
109−export async function listRuntimeTargets(): Promise<
110− RuntimeTargetDescriptor[]
111−> {
112− const res = await fetch(`${BASE}/api/runtime-targets`);
113− return json<RuntimeTargetDescriptor[]>(res);
114−}
115−
116−// ── Tools ──
117−
118−export async function listToolsForContext(context: {
119− workspaceId: string;
120− actorId?: string;
121− clientId?: string;
122−}): Promise<ToolDescriptor[]> {
123− const params = new URLSearchParams({ workspaceId: context.workspaceId });
124− if (context.actorId) params.set("actorId", context.actorId);
125− if (context.clientId) params.set("clientId", context.clientId);
126− const res = await fetch(`${BASE}/api/tools?${params.toString()}`);
127− return json<ToolDescriptor[]>(res);
128−}
129−
130−// ── Tool Sources ──
131−
132−export async function listToolSources(
133− workspaceId: string,
134−): Promise<ToolSourceRecord[]> {
135− const res = await fetch(
136− `${BASE}/api/tool-sources?workspaceId=${encodeURIComponent(workspaceId)}`,
137− );
138− return json<ToolSourceRecord[]>(res);
139−}
140−
141−export async function upsertToolSource(request: {
142− id?: string;
143− workspaceId: string;
144− name: string;
145− type: "mcp" | "openapi" | "graphql";
146− config: Record<string, unknown>;
147− enabled?: boolean;
148−}): Promise<ToolSourceRecord> {
149− const res = await fetch(`${BASE}/api/tool-sources`, {
150− method: "POST",
151− headers: { "content-type": "application/json" },
152− body: JSON.stringify(request),
153− });
154− return json<ToolSourceRecord>(res);
155−}
156−
157−export async function deleteToolSource(
158− workspaceId: string,
159− sourceId: string,
160−): Promise<{ ok: boolean }> {
161− const res = await fetch(
162− `${BASE}/api/tool-sources/${encodeURIComponent(sourceId)}?workspaceId=${encodeURIComponent(workspaceId)}`,
163− { method: "DELETE" },
164− );
165− return json<{ ok: boolean }>(res);
166−}
167−
168−// ── Policies ──
169−
170−export async function listPolicies(
171− workspaceId: string,
172−): Promise<AccessPolicyRecord[]> {
173− const res = await fetch(
174− `${BASE}/api/policies?workspaceId=${encodeURIComponent(workspaceId)}`,
175− );
176− return json<AccessPolicyRecord[]>(res);
177−}
178−
179−export async function upsertPolicy(request: {
180− id?: string;
181− workspaceId: string;
182− actorId?: string;
183− clientId?: string;
184− toolPathPattern: string;
185− decision: string;
186− priority?: number;
187−}): Promise<AccessPolicyRecord> {
188− const res = await fetch(`${BASE}/api/policies`, {
189− method: "POST",
190− headers: { "content-type": "application/json" },
191− body: JSON.stringify(request),
192− });
193− return json<AccessPolicyRecord>(res);
194−}
195−
196−// ── Credentials ──
197−
198−export async function listCredentials(
199− workspaceId: string,
200−): Promise<CredentialDescriptor[]> {
201− const res = await fetch(
202− `${BASE}/api/credentials?workspaceId=${encodeURIComponent(workspaceId)}`,
203− );
204− return json<CredentialDescriptor[]>(res);
205−}
206−
207−export async function upsertCredential(request: {
208− id?: string;
209− workspaceId: string;
210− sourceKey: string;
211− scope: string;
212− actorId?: string;
213− secretJson: Record<string, unknown>;
214−}): Promise<CredentialDescriptor> {
215− const res = await fetch(`${BASE}/api/credentials`, {
216− method: "POST",
217− headers: { "content-type": "application/json" },
218− body: JSON.stringify(request),
219− });
220− return json<CredentialDescriptor>(res);
221−}
executor/bun.lock+33−16
@@ -17,9 +17,11 @@ · 16 unmodified lines
1717 "name": "@executor/server",
1818 "dependencies": {
1919 "@apidevtools/swagger-parser": "^12.1.0",
20+ "@elysiajs/eden": "^1.4.6",
2021 "@modelcontextprotocol/sdk": "^1.26.0",
2122 "@vercel/sandbox": "latest",
2223 "convex": "latest",
24+ "elysia": "^1.4.22",
2325 "zod": "^4.3.6",
2426 },
2527 "devDependencies": {
@@ -59,15 +61,6 @@ · 33 unmodified lines
5961 "typescript": "^5",
6062 },
6163 },
62− "packages/client": {
63− "name": "@executor/client",
64− "dependencies": {
65− "@executor/contracts": "workspace:*",
66− },
67− },
68− "packages/contracts": {
69− "name": "@executor/contracts",
70− },
7164 },
7265 "trustedDependencies": [
7366 "sharp",
@@ -142,10 +135,14 @@ · 68 unmodified lines
142135
143136 "@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
144137
138+ "@borewit/text-codec": ["@borewit/text-codec@0.2.1", "", {}, "sha512-k7vvKPbf7J2fZ5klGRD9AeKfUvojuZIQ3BT5u7Jfv+puwXkUBUT5PVyMDfJZpy30CBDXGMgw7fguK/lpOMBvgw=="],
139+
145140 "@dotenvx/dotenvx": ["@dotenvx/dotenvx@1.52.0", "", { "dependencies": { "commander": "^11.1.0", "dotenv": "^17.2.1", "eciesjs": "^0.4.10", "execa": "^5.1.1", "fdir": "^6.2.0", "ignore": "^5.3.0", "object-treeify": "1.1.33", "picomatch": "^4.0.2", "which": "^4.0.0" }, "bin": { "dotenvx": "src/cli/dotenvx.js" } }, "sha512-CaQcc8JvtzQhUSm9877b6V4Tb7HCotkcyud9X2YwdqtQKwgljkMRwU96fVYKnzN3V0Hj74oP7Es+vZ0mS+Aa1w=="],
146141
147142 "@ecies/ciphers": ["@ecies/ciphers@0.2.5", "", { "peerDependencies": { "@noble/ciphers": "^1.0.0" } }, "sha512-GalEZH4JgOMHYYcYmVqnFirFsjZHeoGMDt9IxEnM9F7GRUUyUksJ7Ou53L83WHJq3RWKD3AcBpo0iQh0oMpf8A=="],
148143
144+ "@elysiajs/eden": ["@elysiajs/eden@1.4.6", "", { "peerDependencies": { "elysia": ">=1.4.19" } }, "sha512-Tsa4NwXEWg/u73vWiYZQ3L5/ecgZSxqiEjYwpS+4qBKXeTZqZKl2hcgHJSVBL+InEDMi35Xugct7qyAXE5oM4Q=="],
145+
149146 "@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="],
150147
151148 "@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="],
@@ -222,10 +219,6 @@ · 70 unmodified lines
222219
223220 "@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="],
224221
225− "@executor/client": ["@executor/client@workspace:packages/client"],
226−
227− "@executor/contracts": ["@executor/contracts@workspace:packages/contracts"],
228−
229222 "@executor/server": ["@executor/server@workspace:apps/server"],
230223
231224 "@executor/web": ["@executor/web@workspace:apps/web"],
@@ -496,6 +489,8 @@ · 264 unmodified lines
496489
497490 "@sec-ant/readable-stream": ["@sec-ant/readable-stream@0.4.1", "", {}, "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg=="],
498491
492+ "@sinclair/typebox": ["@sinclair/typebox@0.34.48", "", {}, "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA=="],
493+
499494 "@sindresorhus/merge-streams": ["@sindresorhus/merge-streams@4.0.0", "", {}, "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ=="],
500495
501496 "@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="],
@@ -530,6 +525,10 @@ · 28 unmodified lines
530525
531526 "@tailwindcss/postcss": ["@tailwindcss/postcss@4.1.18", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.1.18", "@tailwindcss/oxide": "4.1.18", "postcss": "^8.4.41", "tailwindcss": "4.1.18" } }, "sha512-Ce0GFnzAOuPyfV5SxjXGn0CubwGcuDB0zcdaPuCSzAa/2vII24JTkH+I6jcbXLb1ctjZMZZI6OjDaLPJQL1S0g=="],
532527
528+ "@tokenizer/inflate": ["@tokenizer/inflate@0.4.1", "", { "dependencies": { "debug": "^4.4.3", "token-types": "^6.1.1" } }, "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA=="],
529+
530+ "@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="],
531+
533532 "@ts-morph/common": ["@ts-morph/common@0.27.0", "", { "dependencies": { "fast-glob": "^3.3.3", "minimatch": "^10.0.1", "path-browserify": "^1.0.1" } }, "sha512-Wf29UqxWDpc+i61k3oIOzcUfQt79PIT9y/MWfAGlrkjg6lBC1hwDECLXPVJAhWjiGbfBCxZd65F/LIZF3+jeJQ=="],
534533
535534 "@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
@@ -742,7 +741,7 @@ · 206 unmodified lines
742741
743742 "convex-test": ["convex-test@0.0.41", "", { "peerDependencies": { "convex": "^1.16.4" } }, "sha512-GPHeYFOi70n7UtW0eCEQFVhzl/+m8PvbWkDCbKpHLybI1MrScf4sVpGeM0cC2qmtxiduxa2nLPbehPalhh9oyQ=="],
744743
745− "cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
744+ "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="],
746745
747746 "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="],
748747
@@ -806,6 +805,8 @@ · 57 unmodified lines
806805
807806 "electron-to-chromium": ["electron-to-chromium@1.5.286", "", {}, "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A=="],
808807
808+ "elysia": ["elysia@1.4.22", "", { "dependencies": { "cookie": "^1.1.1", "exact-mirror": "^0.2.6", "fast-decode-uri-component": "^1.0.1", "memoirist": "^0.4.0" }, "peerDependencies": { "@sinclair/typebox": ">= 0.34.0 < 1", "@types/bun": ">= 1.2.0", "file-type": ">= 20.0.0", "openapi-types": ">= 12.0.0", "typescript": ">= 5.0.0" }, "optionalPeers": ["@types/bun", "typescript"] }, "sha512-Q90VCb1RVFxnFaRV0FDoSylESQQLWgLHFmWciQJdX9h3b2cSasji9KWEUvaJuy/L9ciAGg4RAhUVfsXHg5K2RQ=="],
809+
809810 "emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
810811
811812 "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
@@ -882,12 +883,16 @@ · 70 unmodified lines
882883
883884 "eventsource-parser": ["eventsource-parser@3.0.6", "", {}, "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg=="],
884885
886+ "exact-mirror": ["exact-mirror@0.2.6", "", { "peerDependencies": { "@sinclair/typebox": "^0.34.15" }, "optionalPeers": ["@sinclair/typebox"] }, "sha512-7s059UIx9/tnOKSySzUk5cPGkoILhTE4p6ncf6uIPaQ+9aRBQzQjc9+q85l51+oZ+P6aBxh084pD0CzBQPcFUA=="],
887+
885888 "execa": ["execa@9.6.1", "", { "dependencies": { "@sindresorhus/merge-streams": "^4.0.0", "cross-spawn": "^7.0.6", "figures": "^6.1.0", "get-stream": "^9.0.0", "human-signals": "^8.0.1", "is-plain-obj": "^4.1.0", "is-stream": "^4.0.1", "npm-run-path": "^6.0.0", "pretty-ms": "^9.2.0", "signal-exit": "^4.1.0", "strip-final-newline": "^4.0.0", "yoctocolors": "^2.1.1" } }, "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA=="],
886889
887890 "express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="],
888891
889892 "express-rate-limit": ["express-rate-limit@8.2.1", "", { "dependencies": { "ip-address": "10.0.1" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g=="],
890893
894+ "fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="],
895+
891896 "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
892897
893898 "fast-fifo": ["fast-fifo@1.3.2", "", {}, "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ=="],
@@ -910,6 +915,8 @@ · 16 unmodified lines
910915
911916 "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="],
912917
918+ "file-type": ["file-type@21.3.0", "", { "dependencies": { "@tokenizer/inflate": "^0.4.1", "strtok3": "^10.3.4", "token-types": "^6.1.1", "uint8array-extras": "^1.4.0" } }, "sha512-8kPJMIGz1Yt/aPEwOsrR97ZyZaD1Iqm8PClb1nYFclUCkBi0Ma5IsYNQzvSFS9ib51lWyIw5mIT9rWzI/xjpzA=="],
919+
913920 "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
914921
915922 "finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="],
@@ -1004,6 +1011,8 @@ · 88 unmodified lines
10041011
10051012 "iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="],
10061013
1014+ "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
1015+
10071016 "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
10081017
10091018 "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
@@ -1188,6 +1197,8 @@ · 178 unmodified lines
11881197
11891198 "media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="],
11901199
1200+ "memoirist": ["memoirist@0.4.0", "", {}, "sha512-zxTgA0mSYELa66DimuNQDvyLq36AwDlTuVRbnQtB+VuTcKWm5Qc4z3WkSpgsFWHNhexqkIooqpv4hdcqrX5Nmg=="],
1201+
11911202 "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="],
11921203
11931204 "merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="],
@@ -1466,6 +1477,8 @@ · 272 unmodified lines
14661477
14671478 "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
14681479
1480+ "strtok3": ["strtok3@10.3.4", "", { "dependencies": { "@tokenizer/token": "^0.3.0" } }, "sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg=="],
1481+
14691482 "styled-jsx": ["styled-jsx@5.1.6", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" } }, "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA=="],
14701483
14711484 "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
@@ -1498,6 +1511,8 @@ · 26 unmodified lines
14981511
14991512 "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
15001513
1514+ "token-types": ["token-types@6.1.2", "", { "dependencies": { "@borewit/text-codec": "^0.2.1", "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww=="],
1515+
15011516 "tough-cookie": ["tough-cookie@6.0.0", "", { "dependencies": { "tldts": "^7.0.5" } }, "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w=="],
15021517
15031518 "ts-api-utils": ["ts-api-utils@2.4.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA=="],
@@ -1528,6 +1543,8 @@ · 24 unmodified lines
15281543
15291544 "typescript-eslint": ["typescript-eslint@8.54.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.54.0", "@typescript-eslint/parser": "8.54.0", "@typescript-eslint/typescript-estree": "8.54.0", "@typescript-eslint/utils": "8.54.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-CKsJ+g53QpsNPqbzUsfKVgd3Lny4yKZ1pP4qN3jdMOg/sisIDLGyDMezycquXLE5JsEU0wp3dGNdzig0/fmSVQ=="],
15301545
1546+ "uint8array-extras": ["uint8array-extras@1.5.0", "", {}, "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A=="],
1547+
15311548 "unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="],
15321549
15331550 "undici": ["undici@7.21.0", "", {}, "sha512-Hn2tCQpoDt1wv23a68Ctc8Cr/BHpUSfaPYrkajTXOS9IKpxVRx/X5m1K2YkbK2ipgZgxXSgsUinl3x+2YdSSfg=="],
@@ -1660,6 +1677,8 @@ · 126 unmodified lines
16601677
16611678 "eslint-plugin-react/resolve": ["resolve@2.0.0-next.5", "", { "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA=="],
16621679
1680+ "express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
1681+
16631682 "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
16641683
16651684 "is-bun-module/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
@@ -1670,8 +1689,6 @@ · 4 unmodified lines
16701689
16711690 "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
16721691
1673− "msw/cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="],
1674−
16751692 "next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="],
16761693
16771694 "npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="],
executor/packages/client/package.json+0−11
@@ -1,11 +0,0 @@
1−{
2− "name": "@executor/client",
3− "private": true,
4− "type": "module",
5− "dependencies": {
6− "@executor/contracts": "workspace:*"
7− },
8− "exports": {
9− ".": "./src/index.ts"
10− }
11−}
executor/packages/client/src/index.ts+0−258
@@ -1,258 +0,0 @@
1−import type {
2− AccessPolicyRecord,
3− ApprovalRecord,
4− AnonymousContext,
5− CreateTaskRequest,
6− CreateTaskResponse,
7− CredentialDescriptor,
8− CredentialScope,
9− PendingApprovalRecord,
10− PolicyDecision,
11− ResolveApprovalRequest,
12− RuntimeTargetDescriptor,
13− TaskEventRecord,
14− TaskRecord,
15− ToolSourceRecord,
16− ToolDescriptor,
17−} from "@executor/contracts";
18−
19−function withBase(baseUrl: string, path: string): string {
20− return `${baseUrl.replace(/\/$/, "")}${path}`;
21−}
22−
23−async function parseJson<T>(response: Response): Promise<T> {
24− if (!response.ok) {
25− const text = await response.text();
26− throw new Error(`Request failed: ${response.status} ${response.statusText} - ${text}`);
27− }
28− return (await response.json()) as T;
29−}
30−
31−export async function createTask(
32− baseUrl: string,
33− request: CreateTaskRequest,
34−): Promise<CreateTaskResponse> {
35− const response = await fetch(withBase(baseUrl, "/api/tasks"), {
36− method: "POST",
37− headers: { "content-type": "application/json" },
38− body: JSON.stringify(request),
39− });
40− return parseJson<CreateTaskResponse>(response);
41−}
42−
43−export async function listTasks(baseUrl: string): Promise<TaskRecord[]> {
44− throw new Error("listTasks now requires workspaceId. Use listTasksForWorkspace.");
45−}
46−
47−export async function listTasksForWorkspace(baseUrl: string, workspaceId: string): Promise<TaskRecord[]> {
48− const response = await fetch(
49− withBase(baseUrl, `/api/tasks?workspaceId=${encodeURIComponent(workspaceId)}`),
50− );
51− return parseJson<TaskRecord[]>(response);
52−}
53−
54−export async function getTask(baseUrl: string, taskId: string, workspaceId: string): Promise<TaskRecord> {
55− const response = await fetch(
56− withBase(
57− baseUrl,
58− `/api/tasks/${encodeURIComponent(taskId)}?workspaceId=${encodeURIComponent(workspaceId)}`,
59− ),
60− );
61− return parseJson<TaskRecord>(response);
62−}
63−
64−export async function listApprovals(
65− baseUrl: string,
66− workspaceId: string,
67− status?: ApprovalRecord["status"],
68−): Promise<ApprovalRecord[]> {
69− const search = status
70− ? `?workspaceId=${encodeURIComponent(workspaceId)}&status=${encodeURIComponent(status)}`
71− : `?workspaceId=${encodeURIComponent(workspaceId)}`;
72− const response = await fetch(withBase(baseUrl, `/api/approvals${search}`));
73− return parseJson<ApprovalRecord[]>(response);
74−}
75−
76−export async function listPendingApprovals(
77− baseUrl: string,
78− workspaceId: string,
79−): Promise<PendingApprovalRecord[]> {
80− const response = await fetch(
81− withBase(baseUrl, `/api/approvals?workspaceId=${encodeURIComponent(workspaceId)}&status=pending`),
82− );
83− return parseJson<PendingApprovalRecord[]>(response);
84−}
85−
86−export async function resolveApproval(
87− baseUrl: string,
88− approvalId: string,
89− request: ResolveApprovalRequest,
90−): Promise<{ approval: ApprovalRecord; task: TaskRecord }> {
91− const response = await fetch(withBase(baseUrl, `/api/approvals/${encodeURIComponent(approvalId)}`), {
92− method: "POST",
93− headers: { "content-type": "application/json" },
94− body: JSON.stringify(request),
95− });
96− return parseJson<{ approval: ApprovalRecord; task: TaskRecord }>(response);
97−}
98−
99−export async function listRuntimeTargets(baseUrl: string): Promise<RuntimeTargetDescriptor[]> {
100− const response = await fetch(withBase(baseUrl, "/api/runtime-targets"));
101− return parseJson<RuntimeTargetDescriptor[]>(response);
102−}
103−
104−export async function listTools(baseUrl: string): Promise<ToolDescriptor[]> {
105− throw new Error("listTools now requires workspace context. Use listToolsForContext.");
106−}
107−
108−export async function listToolsForContext(
109− baseUrl: string,
110− context: { workspaceId: string; actorId?: string; clientId?: string },
111−): Promise<ToolDescriptor[]> {
112− const params = new URLSearchParams({ workspaceId: context.workspaceId });
113− if (context.actorId) params.set("actorId", context.actorId);
114− if (context.clientId) params.set("clientId", context.clientId);
115− const response = await fetch(withBase(baseUrl, `/api/tools?${params.toString()}`));
116− return parseJson<ToolDescriptor[]>(response);
117−}
118−
119−export async function listToolSources(
120− baseUrl: string,
121− workspaceId: string,
122−): Promise<ToolSourceRecord[]> {
123− const response = await fetch(
124− withBase(baseUrl, `/api/tool-sources?workspaceId=${encodeURIComponent(workspaceId)}`),
125− );
126− return parseJson<ToolSourceRecord[]>(response);
127−}
128−
129−export async function upsertToolSource(
130− baseUrl: string,
131− request: {
132− id?: string;
133− workspaceId: string;
134− name: string;
135− type: "mcp" | "openapi" | "graphql";
136− config: Record<string, unknown>;
137− enabled?: boolean;
138− },
139−): Promise<ToolSourceRecord> {
140− const response = await fetch(withBase(baseUrl, "/api/tool-sources"), {
141− method: "POST",
142− headers: { "content-type": "application/json" },
143− body: JSON.stringify(request),
144− });
145− return parseJson<ToolSourceRecord>(response);
146−}
147−
148−export async function deleteToolSource(
149− baseUrl: string,
150− workspaceId: string,
151− sourceId: string,
152−): Promise<{ ok: boolean }> {
153− const response = await fetch(
154− withBase(
155− baseUrl,
156− `/api/tool-sources/${encodeURIComponent(sourceId)}?workspaceId=${encodeURIComponent(workspaceId)}`,
157− ),
158− { method: "DELETE" },
159− );
160− return parseJson<{ ok: boolean }>(response);
161−}
162−
163−export async function bootstrapAnonymousContext(
164− baseUrl: string,
165− sessionId?: string,
166−): Promise<AnonymousContext> {
167− const response = await fetch(withBase(baseUrl, "/api/auth/anonymous/bootstrap"), {
168− method: "POST",
169− headers: { "content-type": "application/json" },
170− body: JSON.stringify({ sessionId }),
171− });
172− return parseJson<AnonymousContext>(response);
173−}
174−
175−export async function listPolicies(
176− baseUrl: string,
177− workspaceId: string,
178−): Promise<AccessPolicyRecord[]> {
179− const response = await fetch(
180− withBase(baseUrl, `/api/policies?workspaceId=${encodeURIComponent(workspaceId)}`),
181− );
182− return parseJson<AccessPolicyRecord[]>(response);
183−}
184−
185−export async function upsertPolicy(
186− baseUrl: string,
187− request: {
188− id?: string;
189− workspaceId: string;
190− actorId?: string;
191− clientId?: string;
192− toolPathPattern: string;
193− decision: PolicyDecision;
194− priority?: number;
195− },
196−): Promise<AccessPolicyRecord> {
197− const response = await fetch(withBase(baseUrl, "/api/policies"), {
198− method: "POST",
199− headers: { "content-type": "application/json" },
200− body: JSON.stringify(request),
201− });
202− return parseJson<AccessPolicyRecord>(response);
203−}
204−
205−export async function listCredentials(
206− baseUrl: string,
207− workspaceId: string,
208−): Promise<CredentialDescriptor[]> {
209− const response = await fetch(
210− withBase(baseUrl, `/api/credentials?workspaceId=${encodeURIComponent(workspaceId)}`),
211− );
212− return parseJson<CredentialDescriptor[]>(response);
213−}
214−
215−export async function upsertCredential(
216− baseUrl: string,
217− request: {
218− id?: string;
219− workspaceId: string;
220− sourceKey: string;
221− scope: CredentialScope;
222− actorId?: string;
223− secretJson: Record<string, unknown>;
224− },
225−): Promise<CredentialDescriptor> {
226− const response = await fetch(withBase(baseUrl, "/api/credentials"), {
227− method: "POST",
228− headers: { "content-type": "application/json" },
229− body: JSON.stringify(request),
230− });
231− return parseJson<CredentialDescriptor>(response);
232−}
233−
234−export function subscribeToTaskEvents(
235− baseUrl: string,
236− taskId: string,
237− workspaceId: string,
238− onEvent: (eventName: TaskEventRecord["eventName"], event: TaskEventRecord) => void,
239−): EventSource {
240− const source = new EventSource(
241− withBase(
242− baseUrl,
243− `/api/tasks/${encodeURIComponent(taskId)}/events?workspaceId=${encodeURIComponent(workspaceId)}`,
244− ),
245− );
246−
247− source.addEventListener("task", (event) => {
248− const message = event as MessageEvent<string>;
249− onEvent("task", JSON.parse(message.data) as TaskEventRecord);
250− });
251−
252− source.addEventListener("approval", (event) => {
253− const message = event as MessageEvent<string>;
254− onEvent("approval", JSON.parse(message.data) as TaskEventRecord);
255− });
256−
257− return source;
258−}
executor/packages/contracts/package.json+0−8
@@ -1,8 +0,0 @@
1−{
2− "name": "@executor/contracts",
3− "private": true,
4− "type": "module",
5− "exports": {
6− ".": "./src/index.ts"
7− }
8−}
executor/packages/contracts/src/index.ts+0−154
@@ -1,154 +0,0 @@
1−export type TaskStatus =
2− | "queued"
3− | "running"
4− | "completed"
5− | "failed"
6− | "timed_out"
7− | "denied";
8−
9−export type ApprovalStatus = "pending" | "approved" | "denied";
10−export type ApprovalDecision = "approved" | "denied";
11−
12−export interface CreateTaskRequest {
13− code: string;
14− runtimeId?: string;
15− timeoutMs?: number;
16− metadata?: Record<string, unknown>;
17− workspaceId: string;
18− actorId: string;
19− clientId?: string;
20−}
21−
22−export interface CreateTaskResponse {
23− taskId: string;
24− status: TaskStatus;
25−}
26−
27−export interface TaskRecord {
28− id: string;
29− code: string;
30− runtimeId: string;
31− timeoutMs: number;
32− metadata: Record<string, unknown>;
33− workspaceId: string;
34− actorId?: string;
35− clientId?: string;
36− status: TaskStatus;
37− createdAt: number;
38− updatedAt: number;
39− startedAt?: number;
40− completedAt?: number;
41− stdout?: string;
42− stderr?: string;
43− exitCode?: number;
44− error?: string;
45−}
46−
47−export interface ApprovalRecord {
48− id: string;
49− taskId: string;
50− toolPath: string;
51− input: unknown;
52− status: ApprovalStatus;
53− createdAt: number;
54− resolvedAt?: number;
55− reviewerId?: string;
56− reason?: string;
57−}
58−
59−export interface PendingApprovalRecord extends ApprovalRecord {
60− task: Pick<TaskRecord, "id" | "status" | "runtimeId" | "timeoutMs" | "createdAt">;
61−}
62−
63−export interface ResolveApprovalRequest {
64− workspaceId: string;
65− decision: ApprovalDecision;
66− reviewerId?: string;
67− reason?: string;
68−}
69−
70−export interface AnonymousContext {
71− sessionId: string;
72− workspaceId: string;
73− actorId: string;
74− clientId: string;
75− createdAt: number;
76− lastSeenAt: number;
77−}
78−
79−export interface RuntimeTargetDescriptor {
80− id: string;
81− label: string;
82− description: string;
83−}
84−
85−export interface ToolDescriptor {
86− path: string;
87− description: string;
88− approval: "auto" | "required";
89− source?: string;
90− argsType?: string;
91− returnsType?: string;
92−}
93−
94−export type PolicyDecision = "allow" | "require_approval" | "deny";
95−
96−export interface AccessPolicyRecord {
97− id: string;
98− workspaceId: string;
99− actorId?: string;
100− clientId?: string;
101− toolPathPattern: string;
102− decision: PolicyDecision;
103− priority: number;
104− createdAt: number;
105− updatedAt: number;
106−}
107−
108−export type CredentialScope = "workspace" | "actor";
109−
110−export interface CredentialDescriptor {
111− id: string;
112− workspaceId: string;
113− sourceKey: string;
114− scope: CredentialScope;
115− actorId?: string;
116− hasSecret: boolean;
117−}
118−
119−export type ToolSourceType = "mcp" | "openapi" | "graphql";
120−
121−export interface ToolSourceRecord {
122− id: string;
123− workspaceId: string;
124− name: string;
125− type: ToolSourceType;
126− config: Record<string, unknown>;
127− enabled: boolean;
128− createdAt: number;
129− updatedAt: number;
130−}
131−
132−export interface TaskEventRecord {
133− id: number;
134− taskId: string;
135− eventName: "task" | "approval";
136− type: string;
137− payload: Record<string, unknown>;
138− createdAt: number;
139−}
140−
141−export interface SandboxExecutionRequest {
142− taskId: string;
143− code: string;
144− timeoutMs: number;
145−}
146−
147−export interface SandboxExecutionResult {
148− status: "completed" | "failed" | "timed_out" | "denied";
149− stdout: string;
150− stderr: string;
151− exitCode?: number;
152− error?: string;
153− durationMs: number;
154−}
executor/tsconfig.json+1−9
@@ -13,15 +13,7 @@ · 12 unmodified lines
1313 "ESNext",
1414 "DOM"
1515 ],
16− "baseUrl": ".",
17− "paths": {
18− "@executor/contracts": [
19− "packages/contracts/src/index.ts"
20− ],
21− "@executor/client": [
22− "packages/client/src/index.ts"
23− ]
24− }
16+ "baseUrl": "."
2517 },
2618 "include": [
2719 "apps/**/*.ts",