import Fastify, { type FastifyInstance } from "fastify"; import cors from "@fastify/cors"; import { join } from "node:path"; import type { AppConfig } from "./config.js"; import { ThtRunner } from "./tht/tht-runner.js"; import { PiProcessManager } from "./pi/pi-process-manager.js"; import { SseHub } from "./sse/sse-hub.js"; import { authPreHandler } from "./auth/auth.js"; import { getPrincipal } from "./auth/auth.js"; import type { PrincipalContext } from "./auth/principal.js"; import { sessionRoutes } from "./routes/sessions.js"; import { sqlRoutes } from "./routes/sql.js"; import { metaRoutes, type ListModelsFn } from "./routes/meta.js"; import { settingsRoutes, effectiveSettings } from "./routes/settings.js"; import { createPiModelLister } from "./pi/list-models.js"; import { createPiManagement, type PiManagementService } from "./pi/management.js"; import { loadSettings, type Settings } from "./settings/settings-store.js"; import { ReadinessManager } from "./runtime/readiness-manager.js"; import { MaintenanceBarrier } from "./runtime/maintenance-gate.js"; import { WorkspaceRegistry, createWorkspaceRegistry, reconcileWorkspaceSnapshotRetention, workspaceRegistryRecoveryIdentity, workspaceRegistrySnapshotPath } from "./workspaces/registry.js"; import { WorkspaceAuthorGitService } from "./workspaces/author-git-service.js"; import { GitWorkspaceRepository } from "./workspaces/git-repository.js"; import { createProductionWorkspaceDiagnoser } from "./workspaces/diagnostics.js"; import { workspaceRoutes, type WorkspaceDiagnoser } from "./routes/workspaces.js"; import { piManagementRoutes } from "./routes/pi-management.js"; import { resolveRuntimeBindings, supportsSessionRuntime } from "./workspaces/bindings.js"; import type { WorkspaceDescriptor } from "./workspaces/schema.js"; export interface BuildAppDeps { thtRunner?: ThtRunner; mgr?: PiProcessManager; spawnFn?: () => any; listModels?: ListModelsFn; getSettings?: (principal?: PrincipalContext) => Settings | Promise; readiness?: ReadinessManager; hub?: SseHub; workspaceRegistry?: WorkspaceRegistry; workspaceRegistryRecoveryIdentity?: () => ReturnType; workspaceRegistrySnapshotPath?: (commit: string, id: string) => string; reconcileSnapshotRetention?: (referencedCommits: readonly string[]) => Promise; workspaceAuthorService?: WorkspaceAuthorGitService; workspaceDiagnoser?: WorkspaceDiagnoser; workspaceRuntimeSupport?: (workspace: WorkspaceDescriptor) => boolean; maintenanceBarrier?: MaintenanceBarrier; piManagement?: PiManagementService; } export function buildApp(config: AppConfig, deps?: BuildAppDeps): FastifyInstance { const app = Fastify({ logger: { level: "warn" }, disableRequestLogging: true }); // Allow any origin in dev/e2e; tighten in production via config if needed. app.register(cors, { origin: true, credentials: true, methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"], }); const tht = deps?.thtRunner ?? new ThtRunner({ thtBin: config.thtBin, harnessDir: config.harnessDir, configPath: process.env.THT_CONFIG ?? "config/tht.yaml", dataRoot: config.dataRoot, runtimeSnapshotRoot: join(config.workspaceRegistry.root, "snapshots"), secretRoots: config.workspaceRegistry.secretRoots, secretsFile: config.secretsFile, secretFiles: config.secretFiles, semanticRuntime: { internalQdrantUrl: config.internalQdrantUrl, internalEmbeddingUrl: config.internalEmbeddingUrl, internalEmbeddingModel: config.internalEmbeddingModel, internalEmbeddingDimensions: config.internalEmbeddingDimensions, }, }); const mgr = deps?.mgr ?? new PiProcessManager(config, deps?.spawnFn ? { spawnFn: deps.spawnFn } : undefined); const hub = deps?.hub ?? new SseHub(); const workspaceRegistry = deps?.workspaceRegistry ?? (() => { try { return createWorkspaceRegistry(config.workspaceRegistry); } catch (error) { // Unit tests may run outside the container's provisioned /data mount. if (config.workspaceRegistry.root !== "/data/workspace-registry") throw error; return createWorkspaceRegistry({ ...config.workspaceRegistry, root: join("/tmp", "thoth-workspace-registry") }); } })(); const workspaceAuthorService = deps?.workspaceAuthorService ?? new WorkspaceAuthorGitService(new GitWorkspaceRepository(config.workspaceRegistry)); const workspaceDiagnoser = deps?.workspaceDiagnoser ?? createProductionWorkspaceDiagnoser(config.workspaceDiagnosticTimeoutMs, undefined, { internalQdrantUrl: config.internalQdrantUrl, internalEmbeddingUrl: config.internalEmbeddingUrl, internalEmbeddingModel: config.internalEmbeddingModel, internalEmbeddingDimensions: config.internalEmbeddingDimensions, }); const workspaceRuntimeSupport = deps?.workspaceRuntimeSupport ?? ((workspace: WorkspaceDescriptor) => ( supportsSessionRuntime(resolveRuntimeBindings( workspace, process.env, config.workspaceRegistry.secretRoots, )) )); const readiness = deps?.readiness ?? new ReadinessManager( tht as ThtRunner, Math.round(config.ollamaEnsureTimeoutMs / 1000), ); const listModels = deps?.listModels ?? createPiModelLister(config, { warn: (detail) => app.log.warn( { component: "pi-model-list", detail }, "Pi enabled-model configuration warning", ), }); const runnerFor = (principal: PrincipalContext): any => { const candidate = tht as any; return typeof candidate.withPrincipal === "function" ? candidate.withPrincipal(principal) : candidate; }; const getSettings = async (principal: PrincipalContext): Promise => { if (deps?.getSettings) return await deps.getSettings(principal); return effectiveSettings(config, loadSettings(config)); }; const piManagement = deps?.piManagement ?? createPiManagement(config, { listModels }); const maintenanceBarrier = deps?.maintenanceBarrier ?? new MaintenanceBarrier(config.maintenanceFile); const authenticate = authPreHandler(config.authMode); app.addHook("preHandler", async (req, reply) => { // Process readiness is intentionally unauthenticated for local container/proxy probes. if (req.url === "/health" || req.url === "/health/dwh") return; if (isMaintenanceControl(req.url)) { if (!isLoopback(req.ip)) { return reply.code(403).send({ error: "loopback maintenance control required" }); } return; } return authenticate(req, reply); }); app.get("/health", async () => ({ status: "ok" })); app.get("/health/dwh", async () => tht.dbPing()); app.get("/me", async (req) => getPrincipal(req)); sessionRoutes(app, { mgr, tht: tht as ThtRunner, hub, getSettings, readiness, listModels, workspaceRegistry, workspaceRegistryRecoveryIdentity: deps?.workspaceRegistryRecoveryIdentity ?? (() => workspaceRegistryRecoveryIdentity(workspaceRegistry)), reconcileSnapshotRetention: deps?.reconcileSnapshotRetention ?? ((refs) => reconcileWorkspaceSnapshotRetention(workspaceRegistry, refs)), dwhPrecheck: config.dwhPrecheck, legacyWorkspaceMode: config.legacyWorkspaceMode, workspaceRuntimeSupport, maintenanceBarrier, }); app.post("/internal/maintenance/activate", async (req, reply) => { try { await maintenanceBarrier.activate(); return maintenanceBarrier.status(); } catch { return reply.code(500).send({ ...maintenanceBarrier.status(), code: "maintenance_durability_failed", error: "maintenance activation durability was not acknowledged", }); } }); app.post("/internal/maintenance/deactivate", async (req, reply) => { try { maintenanceBarrier.deactivate(); return maintenanceBarrier.status(); } catch { return reply.code(500).send({ ...maintenanceBarrier.status(), code: "maintenance_durability_failed", error: "maintenance deactivation durability was not acknowledged", }); } }); app.get("/internal/maintenance/status", async (req, reply) => { return maintenanceBarrier.status(); }); sqlRoutes(app, { tht: tht as ThtRunner, getSettings, workspaceRegistry, workspaceRegistryRecoveryIdentity: deps?.workspaceRegistryRecoveryIdentity ?? (() => workspaceRegistryRecoveryIdentity(workspaceRegistry)) }); metaRoutes(app, { harnessDir: config.harnessDir, listModels }); workspaceRoutes(app, { registry: workspaceRegistry, config: config.workspaceRegistry, diagnose: workspaceDiagnoser, authorService: workspaceAuthorService, recoveryIdentity: deps?.workspaceRegistryRecoveryIdentity ?? (() => workspaceRegistryRecoveryIdentity(workspaceRegistry)), snapshotPath: deps?.workspaceRegistrySnapshotPath ?? ((commit, id) => workspaceRegistrySnapshotPath(workspaceRegistry, commit, id)), }); settingsRoutes(app, { cfg: config, listModels, getSettings }); piManagementRoutes(app, { config, service: piManagement }); return app; } function isLoopback(ip: string): boolean { return ip === "127.0.0.1" || ip === "::1" || ip === "::ffff:127.0.0.1"; } function isMaintenanceControl(url: string): boolean { return /^\/internal\/maintenance\/(?:activate|deactivate|status)(?:\?|$)/.test(url); }