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'); // `opencode` is offered whenever the admin hasn't disabled it — unlike // `server` there is no env var to gate on, because availability is "is a // local `opencode serve` listening right now", which changes minute to // minute and is answered by /api/ai/opencode/models (503 when absent). // Advertising the class and letting that probe report the truth beats // hiding it based on a stale check at policy-fetch time. if (classAllowed('opencode')) classes.push('opencode'); 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 }); } }