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:
@@ -3,6 +3,7 @@ import { logger } from '@/lib/logger';
|
||||
import { readFileEnv } from '@/lib/read-file-env';
|
||||
import { CONFIG_ENV_MAP, DEFAULT_FEATURE_GATES, DEFAULT_POLICY, DEFAULT_THEME_POLICY, type SettingsPolicy } from './types';
|
||||
import { ensureConfigDir, getConfigPath, assertWritable } from './paths';
|
||||
import { DEFAULT_AI_CONSOLE_CONFIG, type AiConsoleConfig } from '@/lib/ai/types';
|
||||
|
||||
function parseEnvValue(value: string, type: string): unknown {
|
||||
switch (type) {
|
||||
@@ -26,6 +27,7 @@ function parseEnvValue(value: string, type: string): unknown {
|
||||
class ConfigManager {
|
||||
private adminConfig: Record<string, unknown> = {};
|
||||
private policyCache: SettingsPolicy = { ...DEFAULT_POLICY };
|
||||
private aiConsoleConfigCache: AiConsoleConfig = { ...DEFAULT_AI_CONSOLE_CONFIG };
|
||||
private loaded = false;
|
||||
|
||||
/** Load admin config and policy from disk. Called once at startup and on reload. */
|
||||
@@ -42,6 +44,12 @@ class ConfigManager {
|
||||
} else {
|
||||
this.policyCache = { ...DEFAULT_POLICY };
|
||||
}
|
||||
const aiConsoleConfig = await this.readJsonFile('ai-policy.json');
|
||||
this.aiConsoleConfigCache = {
|
||||
...DEFAULT_AI_CONSOLE_CONFIG,
|
||||
...aiConsoleConfig,
|
||||
classesEnabled: { ...DEFAULT_AI_CONSOLE_CONFIG.classesEnabled, ...(aiConsoleConfig?.classesEnabled as object | undefined) },
|
||||
} as AiConsoleConfig;
|
||||
this.loaded = true;
|
||||
logger.debug('ConfigManager loaded', { configKeys: Object.keys(this.adminConfig).length });
|
||||
}
|
||||
@@ -175,6 +183,27 @@ class ConfigManager {
|
||||
await this.writeJsonFile('policy.json', this.policyCache as unknown as Record<string, unknown>);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current AI console config (docs/ADMIN-AI-POLICY-CONSOLE-SPEC.md §6).
|
||||
*/
|
||||
getAiConsoleConfig(): AiConsoleConfig {
|
||||
return this.aiConsoleConfigCache;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the AI console config. Writes to disk.
|
||||
*/
|
||||
async setAiConsoleConfig(config: Partial<AiConsoleConfig>): Promise<AiConsoleConfig> {
|
||||
assertWritable('update AI console config');
|
||||
this.aiConsoleConfigCache = {
|
||||
...this.aiConsoleConfigCache,
|
||||
...config,
|
||||
classesEnabled: { ...this.aiConsoleConfigCache.classesEnabled, ...(config.classesEnabled || {}) },
|
||||
};
|
||||
await this.writeJsonFile('ai-policy.json', this.aiConsoleConfigCache as unknown as Record<string, unknown>);
|
||||
return this.aiConsoleConfigCache;
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrates deprecated feature gates forward. The standalone "All Mail" view
|
||||
* (`allMailViewEnabled`) was folded into the unified "All mail" entry, so an
|
||||
|
||||
Reference in New Issue
Block a user