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
+43
View File
@@ -37,6 +37,14 @@ export interface AiPolicy {
entitlement: AiEntitlement;
/** Public-model consent text version currently in force (§7.3). Unset until P2. */
publicConsentVersion: string | null;
/** Mirrors AiConsoleConfig.retrievalEnabled (docs/ADMIN-AI-POLICY-CONSOLE-SPEC.md §6). */
retrievalEnabled: boolean;
/** Admin-authored consent shown once per user before first BYOK/public use.
* Bumping the version re-prompts everyone (client tracks acceptance per version). */
consent: { version: string; text: string } | null;
/** Base-URL prefixes a BYOK profile's baseUrl must match. null = unrestricted
* (today's behavior). Advisory/client-side only — see spec §6.1. */
publicProviderAllowlist: string[] | null;
}
export const DEFAULT_AI_ENTITLEMENT: AiEntitlement = {
@@ -52,4 +60,39 @@ export const DEFAULT_AI_POLICY: AiPolicy = {
enabled: false,
entitlement: { ...DEFAULT_AI_ENTITLEMENT },
publicConsentVersion: null,
retrievalEnabled: true,
consent: null,
publicProviderAllowlist: null,
};
// Admin-authored console config (docs/AI-ASSISTANT-CONCEPT.md §6 /
// docs/ADMIN-AI-POLICY-CONSOLE-SPEC.md). Persisted via config-manager
// (CONFIG dir - operator-authored, not runtime state like entitlement.ts's
// seats/ledger). Read by /api/ai/policy (public) and written by
// /api/admin/ai/policy (admin-protected).
export interface AiConsoleConfig {
/** Per-class admin override. A class must be BOTH infra-available
* (server: AI_SERVER_BASE_URL set) AND not explicitly disabled here to
* reach users. Missing entries default to true - turning this feature
* on changes nothing until an admin touches it. */
classesEnabled: Partial<Record<AiClass, boolean>>;
/** null = every completion-capable model Ollama reports (today's
* behavior, unchanged). Non-null = only these model names selectable
* for the `server` class. */
serverModelAllowlist: string[] | null;
/** null = unrestricted BYOK base URLs (today's behavior, unchanged).
* Non-null = base URL must start with one of these prefixes. */
publicProviderAllowlist: string[] | null;
/** Master switch for the retrieval leg (mail-content → embeddings).
* Independent of classesEnabled.server. Defaults true. */
retrievalEnabled: boolean;
consent: { version: string; text: string } | null;
}
export const DEFAULT_AI_CONSOLE_CONFIG: AiConsoleConfig = {
classesEnabled: {},
serverModelAllowlist: null,
publicProviderAllowlist: null,
retrievalEnabled: true,
consent: null,
};