Files
SRCmail/app/api/ai/policy/route.ts
T
Bernd Rodler 30e5059b94 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.
2026-08-06 08:48:30 +02:00

49 lines
2.3 KiB
TypeScript

import { NextResponse } from 'next/server';
import { configManager } from '@/lib/admin/config-manager';
import { logger } from '@/lib/logger';
import { DEFAULT_AI_ENTITLEMENT, type AiPolicy } from '@/lib/ai/types';
/**
* GET /api/ai/policy - AI Assistant policy (NOT admin-protected - users read this)
*
* `enabled` mirrors the admin FeatureGates toggle. `entitlement.classes`
* reflects real configuration, not a hardcoded guess: `server` only appears
* when AI_SERVER_BASE_URL is actually set (app/api/ai/server/* would 503
* otherwise) - this is enforcement point 1 (docs §10), cosmetic-only, the
* client hiding what it can't use; the real gate is checkAndAssignSeat() on
* every /api/ai/server/chat call, not this list.
*/
export async function GET() {
try {
await configManager.ensureLoaded();
const policy = configManager.getPolicy();
const consoleConfig = configManager.getAiConsoleConfig();
// A class must be BOTH infra-available AND not explicitly disabled by
// the admin console (docs/ADMIN-AI-POLICY-CONSOLE-SPEC.md §6) to reach
// users. Missing classesEnabled entries default to allowed, so this
// changes nothing until an admin actually touches the console.
const classAllowed = (cls: (typeof DEFAULT_AI_ENTITLEMENT.classes)[number]) => consoleConfig.classesEnabled[cls] !== false;
const classes: typeof DEFAULT_AI_ENTITLEMENT.classes = [];
if (classAllowed('local')) classes.push('local');
if (classAllowed('public')) classes.push('public');
if (process.env.AI_SERVER_BASE_URL && classAllowed('server')) classes.push('server');
const aiPolicy: AiPolicy = {
enabled: policy.features.aiAssistantEnabled,
entitlement: { ...DEFAULT_AI_ENTITLEMENT, classes },
publicConsentVersion: consoleConfig.consent?.version ?? null,
retrievalEnabled: consoleConfig.retrievalEnabled,
consent: consoleConfig.consent,
publicProviderAllowlist: consoleConfig.publicProviderAllowlist,
};
return NextResponse.json(aiPolicy, {
headers: { 'Cache-Control': 'no-store' },
});
} catch (error) {
logger.error('AI policy read error', { error: error instanceof Error ? error.message : 'Unknown error' });
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
}
}