feat(admin): build the AI Policy console (§6) — approved, spec now implemented

New admin tab "AI" (app/(main)/admin/_tabs/ai-policy.tsx): provider-class
toggles, server model allow-list, BYOK provider allow-list, seats/usage
(front-end for the already-real lib/ai/entitlement.ts), retrieval on/off,
consent text + version bump.

Real backend, not cosmetic: AiConsoleConfig persisted via config-manager
(lib/ai/types.ts, ai-policy.json in the CONFIG dir). New GET/PUT
/api/admin/ai/policy. Enforcement wired at every real chokepoint, not just
the picker: /api/ai/server/chat checks classesEnabled.server and the model
allow-list, /api/ai/retrieve checks retrievalEnabled, /api/ai/server/models
filters by allow-list. GET /api/ai/policy folds classesEnabled into the
classes list clients see.

Resolved the spec's 3 open questions as recommended: BYOK allow-list stays
client-side/advisory (wired into ai-assistant-settings.tsx's addProfile),
tier picker stays cosmetic, master aiAssistantEnabled toggle stays in the
existing Policy tab (this tab links to it instead of duplicating it).

Defaults preserve today's behavior exactly (classesEnabled/allowlists all
start empty/null) — turning this on changes nothing until an admin touches it.
This commit is contained in:
Bernd Rodler
2026-08-06 08:48:30 +02:00
parent 61651b1ed1
commit 30e5059b94
12 changed files with 591 additions and 5 deletions
+12 -1
View File
@@ -1,5 +1,6 @@
import { NextRequest, NextResponse } from 'next/server';
import { getStalwartCredentials } from '@/lib/stalwart/credentials';
import { configManager } from '@/lib/admin/config-manager';
export const runtime = 'nodejs';
@@ -37,7 +38,17 @@ export async function GET(request: NextRequest) {
// lists them in the same /api/tags response, but calling /api/chat with
// one fails outright. `capabilities` absent (older Ollama) fails open
// rather than hiding every model on an upgrade.
const chatModels = (body.models ?? []).filter((m) => !m.capabilities || m.capabilities.includes('completion'));
let chatModels = (body.models ?? []).filter((m) => !m.capabilities || m.capabilities.includes('completion'));
// Admin allow-list (docs/ADMIN-AI-POLICY-CONSOLE-SPEC.md §6). null = every
// completion-capable model (today's behavior, unchanged).
await configManager.ensureLoaded();
const allowlist = configManager.getAiConsoleConfig().serverModelAllowlist;
if (allowlist) {
const allowed = new Set(allowlist);
chatModels = chatModels.filter((m) => allowed.has(m.name));
}
return NextResponse.json({ models: chatModels.map((m) => m.name).filter(Boolean) });
} catch (cause) {
return NextResponse.json(