| @@ -12,6 +12,7 @@ import { · 11 unmodified lines |
| 12 | 12 | createRunExecutor, |
| 13 | 13 | createSourceToolRegistry, |
| 14 | 14 | defaultExecuteToolExposureMode, |
| 15 | + invokeRuntimeToolCallResult, |
| 15 | 16 | makeGraphqlToolProvider, |
| 16 | 17 | makeMcpToolProvider, |
| 17 | 18 | makeOpenApiToolProvider, |
| @@ -106,6 +107,7 @@ type ControlPlaneRuntime = { · 88 unmodified lines |
| 106 | 107 | fetchOpenApiDocument: typeof fetchOpenApiDocument; |
| 107 | 108 | handleControlPlane: (request: Request) => Promise<Response>; |
| 108 | 109 | handleMcp: (request: Request, workspaceId: string) => Promise<Response>; |
| 110 | + handleRuntimeToolCall: (request: Request) => Promise<Response>; |
| 109 | 111 | dispose: () => Promise<void>; |
| 110 | 112 | }; |
| 111 | 113 | |
| @@ -119,6 +121,36 @@ type ControlPlanePrincipal = { · 7 unmodified lines |
| 119 | 121 | workspaceId: string; |
| 120 | 122 | }; |
| 121 | 123 | |
| 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 | + |
| 122 | 154 | const normalizeIdPart = (value: string): string => |
| 123 | 155 | value.replace(/[^a-zA-Z0-9_-]/g, "_"); |
| 124 | 156 | |
| @@ -144,6 +176,7 @@ const resolveStateRootDir = (): string => · 19 unmodified lines |
| 144 | 176 | defaultControlPlaneStateRootDir; |
| 145 | 177 | |
| 146 | 178 | const openApiSyncRetryDelayMs = 300; |
| 179 | +const runtimeToolCallRetentionMs = 15 * 60 * 1000; |
| 147 | 180 | |
| 148 | 181 | const formatCause = (cause: unknown): string => { |
| 149 | 182 | if (cause && typeof cause === "object") { |
| @@ -181,6 +214,54 @@ const formatCause = (cause: unknown): string => { · 31 unmodified lines |
| 181 | 214 | } |
| 182 | 215 | }; |
| 183 | 216 | |
| 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 | + |
| 184 | 265 | const ensurePrincipalProvisioned = ( |
| 185 | 266 | persistence: SqlControlPlanePersistence, |
| 186 | 267 | principal: ControlPlanePrincipal, |
| @@ -297,10 +378,12 @@ const createControlPlaneRuntime = async (): Promise<ControlPlaneRuntime> => { · 110 unmodified lines |
| 297 | 378 | requireApprovals: requireToolApprovals, |
| 298 | 379 | }, |
| 299 | 380 | ); |
| 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>>(); |
| 304 | 387 | |
| 305 | 388 | const sourceCatalog = makeSourceCatalogService(sourceStore); |
| 306 | 389 | const sourceManager = makeSourceManagerService(toolArtifactStore); |
| @@ -424,8 +507,27 @@ const createControlPlaneRuntime = async (): Promise<ControlPlaneRuntime> => { · 117 unmodified lines |
| 424 | 507 | PmActorLive(persistence.rows), |
| 425 | 508 | ); |
| 426 | 509 | |
| 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); |
| 429 | 531 | if (existing) { |
| 430 | 532 | return existing; |
| 431 | 533 | } |
| @@ -442,14 +544,25 @@ const createControlPlaneRuntime = async (): Promise<ControlPlaneRuntime> => { · 10 unmodified lines |
| 442 | 544 | runtimeAdapters, |
| 443 | 545 | toolRegistry, |
| 444 | 546 | }); |
| 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 | + }); |
| 446 | 554 | const next = createPmMcpHandler(runExecutor.executeRun, { |
| 447 | 555 | toolRegistry, |
| 448 | 556 | defaultToolExposureMode, |
| 449 | 557 | }); |
| 450 | 558 | |
| 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; |
| 453 | 566 | }; |
| 454 | 567 | |
| 455 | 568 | return { |
| @@ -459,10 +572,67 @@ const createControlPlaneRuntime = async (): Promise<ControlPlaneRuntime> => { · 3 unmodified lines |
| 459 | 572 | fetchOpenApiDocument, |
| 460 | 573 | handleControlPlane: controlPlaneWebHandler.handler, |
| 461 | 574 | 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 }); |
| 464 | 627 | }, |
| 465 | 628 | dispose: async () => { |
| 629 | + for (const timer of runCleanupTimers.values()) { |
| 630 | + clearTimeout(timer); |
| 631 | + } |
| 632 | + runCleanupTimers.clear(); |
| 633 | + workspaceByRunId.clear(); |
| 634 | + mcpSessions.clear(); |
| 635 | + |
| 466 | 636 | await controlPlaneWebHandler.dispose(); |
| 467 | 637 | await persistence.close(); |
| 468 | 638 | }, |