164 lines
7.0 KiB
TypeScript
164 lines
7.0 KiB
TypeScript
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 } from "./workspaces/registry.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<Settings>;
|
|
readiness?: ReadinessManager;
|
|
hub?: SseHub;
|
|
workspaceRegistry?: WorkspaceRegistry;
|
|
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", "runtime"),
|
|
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 ?? new WorkspaceRegistry(config.workspaceRegistry);
|
|
const workspaceDiagnoser = deps?.workspaceDiagnoser
|
|
?? createProductionWorkspaceDiagnoser(config.workspaceDiagnosticTimeoutMs);
|
|
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<Settings> => {
|
|
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,
|
|
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 });
|
|
metaRoutes(app, { harnessDir: config.harnessDir, listModels });
|
|
workspaceRoutes(app, { registry: workspaceRegistry, config: config.workspaceRegistry, diagnose: workspaceDiagnoser });
|
|
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);
|
|
}
|