History

callback

Rhys Sullivan committed 8 months ago

Commit 8ec79cf1 parent 354b0694 files · +202 −11Browse files

Unified · Split

4 files
  1. apps/web/app/v1/runtime/tool-call/route.ts +9 −0
  2. apps/web/lib/control-plane/server.ts +181 −11
  3. apps/web/lib/env/server.ts +5 −0
  4. turbo.json +7 −0
apps/web/app/v1/runtime/tool-call/route.ts+9−0
@@ -0,0 +1,9 @@
1+import { getControlPlaneRuntime } from "../../../../lib/control-plane/server";
2+
3+
4+const handler = async (request: Request): Promise<Response> => {
5+ const controlPlaneRuntime = await getControlPlaneRuntime();
6+ return controlPlaneRuntime.handleRuntimeToolCall(request);
7+};
8+
9+export const POST = handler;
apps/web/lib/control-plane/server.ts+181−11
@@ -12,6 +12,7 @@ import { · 11 unmodified lines
1212 createRunExecutor,
1313 createSourceToolRegistry,
1414 defaultExecuteToolExposureMode,
15+ invokeRuntimeToolCallResult,
1516 makeGraphqlToolProvider,
1617 makeMcpToolProvider,
1718 makeOpenApiToolProvider,
@@ -106,6 +107,7 @@ type ControlPlaneRuntime = { · 88 unmodified lines
106107 fetchOpenApiDocument: typeof fetchOpenApiDocument;
107108 handleControlPlane: (request: Request) => Promise<Response>;
108109 handleMcp: (request: Request, workspaceId: string) => Promise<Response>;
110+ handleRuntimeToolCall: (request: Request) => Promise<Response>;
109111 dispose: () => Promise<void>;
110112 };
111113
@@ -119,6 +121,36 @@ type ControlPlanePrincipal = { · 7 unmodified lines
119121 workspaceId: string;
120122 };
121123
124+type RuntimeToolCallRequest = {
125+ runId: string;
126+ callId: string;
127+ toolPath: string;
128+ input?: Record<string, unknown>;
129+};
130+
131+type RuntimeToolCallResult =
132+ | {
133+ ok: true;
134+ value: unknown;
135+ }
136+ | {
137+ ok: false;
138+ kind: "pending";
139+ approvalId: string;
140+ retryAfterMs: number;
141+ error?: string;
142+ }
143+ | {
144+ ok: false;
145+ kind: "denied";
146+ error: string;
147+ }
148+ | {
149+ ok: false;
150+ kind: "failed";
151+ error: string;
152+ };
153+
122154 const normalizeIdPart = (value: string): string =>
123155 value.replace(/[^a-zA-Z0-9_-]/g, "_");
124156
@@ -144,6 +176,7 @@ const resolveStateRootDir = (): string => · 19 unmodified lines
144176 defaultControlPlaneStateRootDir;
145177
146178 const openApiSyncRetryDelayMs = 300;
179+const runtimeToolCallRetentionMs = 15 * 60 * 1000;
147180
148181 const formatCause = (cause: unknown): string => {
149182 if (cause && typeof cause === "object") {
@@ -181,6 +214,54 @@ const formatCause = (cause: unknown): string => { · 31 unmodified lines
181214 }
182215 };
183216
217+const toFailedRuntimeToolCallResult = (error: string): RuntimeToolCallResult => ({
218+ ok: false,
219+ kind: "failed",
220+ error,
221+});
222+
223+const runtimeToolCallErrorResponse = (status: number, error: string): Response =>
224+ Response.json(toFailedRuntimeToolCallResult(error), { status });
225+
226+const normalizeRuntimeToolCallInput = (
227+ value: unknown,
228+): Record<string, unknown> | undefined => {
229+ if (value === undefined) {
230+ return undefined;
231+ }
232+
233+ if (value && typeof value === "object" && !Array.isArray(value)) {
234+ return value as Record<string, unknown>;
235+ }
236+
237+ return {};
238+};
239+
240+const parseRuntimeToolCallRequest = async (
241+ request: Request,
242+): Promise<RuntimeToolCallRequest | null> => {
243+ const payload = await request.json().catch(() => null);
244+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
245+ return null;
246+ }
247+
248+ const record = payload as Record<string, unknown>;
249+ if (
250+ typeof record.runId !== "string"
251+ || typeof record.callId !== "string"
252+ || typeof record.toolPath !== "string"
253+ ) {
254+ return null;
255+ }
256+
257+ return {
258+ runId: record.runId,
259+ callId: record.callId,
260+ toolPath: record.toolPath,
261+ input: normalizeRuntimeToolCallInput(record.input),
262+ };
263+};
264+
184265 const ensurePrincipalProvisioned = (
185266 persistence: SqlControlPlanePersistence,
186267 principal: ControlPlanePrincipal,
@@ -297,10 +378,12 @@ const createControlPlaneRuntime = async (): Promise<ControlPlaneRuntime> => { · 110 unmodified lines
297378 requireApprovals: requireToolApprovals,
298379 },
299380 );
300− const mcpHandlers = new Map<
301− string,
302− (request: Request) => Promise<Response>
303− >();
381+ const mcpSessions = new Map<string, {
382+ handler: (request: Request) => Promise<Response>;
383+ toolRegistry: ReturnType<typeof createSourceToolRegistry>;
384+ }>();
385+ const workspaceByRunId = new Map<string, string>();
386+ const runCleanupTimers = new Map<string, ReturnType<typeof setTimeout>>();
304387
305388 const sourceCatalog = makeSourceCatalogService(sourceStore);
306389 const sourceManager = makeSourceManagerService(toolArtifactStore);
@@ -424,8 +507,27 @@ const createControlPlaneRuntime = async (): Promise<ControlPlaneRuntime> => { · 117 unmodified lines
424507 PmActorLive(persistence.rows),
425508 );
426509
427− const resolveMcpHandler = (workspaceId: string): ((request: Request) => Promise<Response>) => {
428− const existing = mcpHandlers.get(workspaceId);
510+ const rememberRunWorkspace = (runId: string, workspaceId: string): void => {
511+ workspaceByRunId.set(runId, workspaceId);
512+
513+ const existingTimer = runCleanupTimers.get(runId);
514+ if (existingTimer) {
515+ clearTimeout(existingTimer);
516+ }
517+
518+ const timer = setTimeout(() => {
519+ workspaceByRunId.delete(runId);
520+ runCleanupTimers.delete(runId);
521+ }, runtimeToolCallRetentionMs);
522+
523+ runCleanupTimers.set(runId, timer);
524+ };
525+
526+ const resolveMcpSession = (workspaceId: string): {
527+ handler: (request: Request) => Promise<Response>;
528+ toolRegistry: ReturnType<typeof createSourceToolRegistry>;
529+ } => {
530+ const existing = mcpSessions.get(workspaceId);
429531 if (existing) {
430532 return existing;
431533 }
@@ -442,14 +544,25 @@ const createControlPlaneRuntime = async (): Promise<ControlPlaneRuntime> => { · 10 unmodified lines
442544 runtimeAdapters,
443545 toolRegistry,
444546 });
445− const runExecutor = createRunExecutor(executeRuntimeRun);
547+ const runExecutor = createRunExecutor(executeRuntimeRun, {
548+ makeRunId: () => {
549+ const runId = `run_${crypto.randomUUID()}`;
550+ rememberRunWorkspace(runId, workspaceId);
551+ return runId;
552+ },
553+ });
446554 const next = createPmMcpHandler(runExecutor.executeRun, {
447555 toolRegistry,
448556 defaultToolExposureMode,
449557 });
450558
451− mcpHandlers.set(workspaceId, next);
452− return next;
559+ const session = {
560+ handler: next,
561+ toolRegistry,
562+ };
563+
564+ mcpSessions.set(workspaceId, session);
565+ return session;
453566 };
454567
455568 return {
@@ -459,10 +572,67 @@ const createControlPlaneRuntime = async (): Promise<ControlPlaneRuntime> => { · 3 unmodified lines
459572 fetchOpenApiDocument,
460573 handleControlPlane: controlPlaneWebHandler.handler,
461574 handleMcp: async (request, workspaceId) => {
462− const handler = resolveMcpHandler(workspaceId);
463− return handler(request);
575+ const session = resolveMcpSession(workspaceId);
576+ return session.handler(request);
577+ },
578+ handleRuntimeToolCall: async (request) => {
579+ if (request.method.toUpperCase() !== "POST") {
580+ return runtimeToolCallErrorResponse(405, "Method not allowed. Expected POST");
581+ }
582+
583+ const expectedSecret = webServerEnvironment.cloudflareSandboxCallbackSecret;
584+ if (!expectedSecret) {
585+ return runtimeToolCallErrorResponse(503, "Runtime callback secret is not configured");
586+ }
587+
588+ const providedSecret = request.headers.get("x-internal-secret")?.trim();
589+ if (!providedSecret) {
590+ return runtimeToolCallErrorResponse(401, "Runtime callback authentication is required");
591+ }
592+
593+ if (providedSecret !== expectedSecret) {
594+ return runtimeToolCallErrorResponse(403, "Runtime callback authentication failed");
595+ }
596+
597+ const input = await parseRuntimeToolCallRequest(request);
598+ if (!input) {
599+ return runtimeToolCallErrorResponse(400, "Runtime callback request body is invalid");
600+ }
601+
602+ const workspaceId = workspaceByRunId.get(input.runId);
603+ if (!workspaceId) {
604+ return runtimeToolCallErrorResponse(404, `Unknown runtime callback run id: ${input.runId}`);
605+ }
606+
607+ const session = resolveMcpSession(workspaceId);
608+
609+ const result = await Effect.runPromise(
610+ invokeRuntimeToolCallResult(session.toolRegistry, input).pipe(
611+ Effect.catchTag("RuntimeAdapterError", (error) =>
612+ Effect.succeed<RuntimeToolCallResult>(
613+ toFailedRuntimeToolCallResult(
614+ error.details ? `${error.message}: ${error.details}` : error.message,
615+ ),
616+ )
617+ ),
618+ Effect.catchAll((cause) =>
619+ Effect.succeed<RuntimeToolCallResult>(
620+ toFailedRuntimeToolCallResult(formatCause(cause)),
621+ )
622+ ),
623+ ),
624+ );
625+
626+ return Response.json(result, { status: 200 });
464627 },
465628 dispose: async () => {
629+ for (const timer of runCleanupTimers.values()) {
630+ clearTimeout(timer);
631+ }
632+ runCleanupTimers.clear();
633+ workspaceByRunId.clear();
634+ mcpSessions.clear();
635+
466636 await controlPlaneWebHandler.dispose();
467637 await persistence.close();
468638 },
apps/web/lib/env/server.ts+5−0
@@ -11,6 +11,10 @@ const webServerEnvConfig = configSchema("WebServerEnvironment", { · 10 unmodified lines
1111 pmRuntimeKind: server({ env: "PM_RUNTIME_KIND", optional: true }),
1212 pmRequireToolApprovals: server({ env: "PM_REQUIRE_TOOL_APPROVALS", optional: true }),
1313 pmToolExposureMode: server({ env: "PM_TOOL_EXPOSURE_MODE", optional: true }),
14+ cloudflareSandboxCallbackSecret: server({
15+ env: "CLOUDFLARE_SANDBOX_CALLBACK_SECRET",
16+ optional: true,
17+ }),
1418 executorPublicOrigin: server({ env: "EXECUTOR_PUBLIC_ORIGIN", optional: true }),
1519 vercelProjectProductionUrl: server({ env: "VERCEL_PROJECT_PRODUCTION_URL", optional: true }),
1620 vercelUrl: server({ env: "VERCEL_URL", optional: true }),
@@ -36,6 +40,7 @@ export const webServerEnvironment = { · 19 unmodified lines
3640 pmRuntimeKind: trim(env.pmRuntimeKind),
3741 pmRequireToolApprovals: isTruthy(env.pmRequireToolApprovals),
3842 pmToolExposureMode: trim(env.pmToolExposureMode),
43+ cloudflareSandboxCallbackSecret: trim(env.cloudflareSandboxCallbackSecret),
3944 executorPublicOrigin: trim(env.executorPublicOrigin),
4045 vercelProjectProductionUrl: trim(env.vercelProjectProductionUrl),
4146 vercelUrl: trim(env.vercelUrl),
turbo.json+7−0
@@ -11,8 +11,15 @@ · 10 unmodified lines
1111 "build": {
1212 "dependsOn": ["^build"],
1313 "env": [
14+ "CLOUDFLARE_SANDBOX_AUTH_TOKEN",
15+ "CLOUDFLARE_SANDBOX_CALLBACK_SECRET",
16+ "CLOUDFLARE_SANDBOX_CALLBACK_URL",
17+ "CLOUDFLARE_SANDBOX_RUN_URL",
1418 "CONVEX_URL",
19+ "CONTROL_PLANE_POSTGRES_MAX_CONNECTIONS",
1520 "DATABASE_URL",
21+ "MCP_AUTHORIZATION_SERVER",
22+ "PM_RUNTIME_KIND",
1623 "VITE_CONVEX_URL",
1724 "WORKOS_API_KEY",
1825 "WORKOS_CLIENT_ID",