| 3717 | } |
| 3718 | |
| 3719 | export function buildRuntimeApiServer(options: BuildRuntimeApiServerOptions = {}): FastifyInstance { |
| 3720 | const ownsStore = !options.store; |
| 3721 | const store = |
| 3722 | options.store ?? |
| 3723 | new RuntimeStateStore({ |
| 3724 | dbPath: options.dbPath, |
| 3725 | workspaceRoot: options.workspaceRoot ?? defaultWorkspaceRoot() |
| 3726 | }); |
| 3727 | |
| 3728 | const app = Fastify({ |
| 3729 | logger: options.logger ?? false, |
| 3730 | bodyLimit: DEFAULT_BODY_LIMIT_BYTES, |
| 3731 | }); |
| 3732 | void app.register(websocket); |
| 3733 | const backgroundTasks = new Set<Promise<void>>(); |
| 3734 | const appSetupTasks = new Map<string, Promise<void>>(); |
| 3735 | const appEnsureRunningTasks = new Map<string, Promise<void>>(); |
| 3736 | // Serializes /api/v1/apps/install-archive against itself for the same |
| 3737 | // (workspaceId, appId). Without this, two concurrent installs both pass |
| 3738 | // the empty-appDir check, both extract on top of each other, and both |
| 3739 | // race-write app.runtime.yaml producing corrupt state. |
| 3740 | const appInstallTasks = new Map<string, Promise<unknown>>(); |
| 3741 | const appLifecycleExecutor = options.appLifecycleExecutor ?? new RuntimeAppLifecycleExecutor({ store }); |
| 3742 | const memoryService = options.memoryService ?? new FilesystemMemoryService({ |
| 3743 | workspaceRoot: store.workspaceRoot, |
| 3744 | resolveWorkspaceDir: (workspaceId) => store.workspaceDir(workspaceId), |
| 3745 | store, |
| 3746 | }); |
| 3747 | const runtimeConfigService = options.runtimeConfigService ?? new FileRuntimeConfigService(); |
| 3748 | const browserToolService = options.browserToolService ?? new DesktopBrowserToolService({ artifactStore: store }); |
| 3749 | const terminalSessionManager = |
| 3750 | options.terminalSessionManager === undefined |
| 3751 | ? new TerminalSessionManager({ |
| 3752 | store, |
| 3753 | logger: app.log, |
| 3754 | captureRuntimeException, |
| 3755 | }) |
| 3756 | : options.terminalSessionManager; |
| 3757 | const integrationContextFetchManager = createIntegrationContextFetchManager({ |
| 3758 | store, |
| 3759 | runFetch: options.integrationContextFetchRunner |
| 3760 | ?? fetchIntegrationContextForConnection, |
| 3761 | logger: { |
| 3762 | warn: (meta, message) => app.log.warn(meta, message), |
| 3763 | }, |
| 3764 | }); |
| 3765 | // Deferred holders: queue worker + composio MCP manager are both |
| 3766 | // constructed after the integration service, but onConnectionActive |
| 3767 | // needs to call into them. We pass closures that read the holders |
| 3768 | // at call time — by then the dependents are set. |
| 3769 | const queueWorkerHolder: { worker: { wake: () => void } | null } = { worker: null }; |
| 3770 | const composioMcpManagerHolder: { |
| 3771 | manager: { restart: (workspaceId: string) => Promise<unknown> } | null; |
| 3772 | } = { manager: null }; |
| 3773 | const runtimeAgentToolsHolder: { |
| 3774 | service: { queuePolishForCompletedBindings: (workspaceId: string) => unknown } | null; |
| 3775 | } = { service: null }; |
| 3776 | const integrationContextAutofetchWorkerHolder: { |