import type { FastifyInstance } from "fastify"; import type { AppConfig } from "../config.js"; import type { Settings } from "../settings/settings-store.js"; import { listWorkspaces, type ListModelsFn } from "./meta.js"; import type { PrincipalContext } from "../auth/principal.js"; import { isPrincipalContext, requirePermission } from "../auth/authorization.js"; /** Merge stored settings over env/first-workspace defaults. */ export function effectiveSettings(cfg: AppConfig, stored: Settings): Settings { const workspaces = listWorkspaces(cfg.harnessDir); return { workspace: stored.workspace ?? workspaces[0]?.name, provider: cfg.defaults.provider ?? stored.provider, model: cfg.defaults.model ?? stored.model, thinking: cfg.defaults.thinking ?? stored.thinking, }; } export function settingsRoutes( app: FastifyInstance, deps: { cfg: AppConfig; listModels: ListModelsFn; getSettings: (principal: PrincipalContext) => Promise; }, ): void { app.get("/settings", async (req, reply) => { const principal = requirePermission(req, reply, "session.use"); if (!isPrincipalContext(principal)) return principal; try { return await deps.getSettings(principal); } catch { return reply.code(503).send({ error: "settings storage is unavailable" }); } }); app.put("/settings", async (req, reply) => { const principal = requirePermission(req, reply, "settings.manage"); if (!isPrincipalContext(principal)) return principal; const b = (req.body ?? {}) as Settings; if (b.model) { let available: { provider: string; id: string }[] = []; try { available = await deps.listModels(); } catch { available = []; } // Only validate when Pi gave us a non-empty list; otherwise allow (degraded). if (available.length > 0 && !available.some( (candidate) => candidate.provider === b.provider && candidate.id === b.model, )) { return reply.code(400).send({ error: `Unknown model: ${b.provider ?? "unknown"}/${b.model}`, }); } } try { // Retain this endpoint as a validating compatibility surface for older clients, but do // not write anonymous users' choices to shared server storage. return await deps.getSettings(principal); } catch { return reply.code(503).send({ error: "settings storage is unavailable" }); } }); }