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:
@@ -117,9 +117,18 @@ export function AiAssistantSettings() {
|
||||
const [newProfileBaseUrl, setNewProfileBaseUrl] = useState('https://openrouter.ai/api/v1');
|
||||
const [newProfileModel, setNewProfileModel] = useState('');
|
||||
const [newProfileKey, setNewProfileKey] = useState('');
|
||||
const [profileError, setProfileError] = useState<string | null>(null);
|
||||
|
||||
const addProfile = useCallback(() => {
|
||||
if (!newProfileName || !newProfileBaseUrl || !newProfileModel || !newProfileKey) return;
|
||||
setProfileError(null);
|
||||
// Admin allow-list (docs/ADMIN-AI-POLICY-CONSOLE-SPEC.md §6.1) — advisory,
|
||||
// client-side only, checked here at save time.
|
||||
const allowlist = policy.publicProviderAllowlist;
|
||||
if (allowlist && !allowlist.some((prefix) => newProfileBaseUrl.startsWith(prefix))) {
|
||||
setProfileError(`This base URL isn't on the admin-approved list (${allowlist.join(', ')}).`);
|
||||
return;
|
||||
}
|
||||
const profile = createProfile(newProfileName, newProfileBaseUrl, newProfileModel);
|
||||
setAiApiKey(profile.id, newProfileKey);
|
||||
update('publicProfiles', [...settings.publicProfiles, profile]);
|
||||
@@ -128,7 +137,7 @@ export function AiAssistantSettings() {
|
||||
setNewProfileBaseUrl('https://openrouter.ai/api/v1');
|
||||
setNewProfileModel('');
|
||||
setNewProfileKey('');
|
||||
}, [newProfileName, newProfileBaseUrl, newProfileModel, newProfileKey, settings.publicProfiles, settings.activeProfileId, update]);
|
||||
}, [newProfileName, newProfileBaseUrl, newProfileModel, newProfileKey, settings.publicProfiles, settings.activeProfileId, update, policy.publicProviderAllowlist]);
|
||||
|
||||
const removeProfile = useCallback(
|
||||
(id: string) => {
|
||||
@@ -376,6 +385,7 @@ export function AiAssistantSettings() {
|
||||
Add
|
||||
</Button>
|
||||
</div>
|
||||
{profileError && <p className="text-xs text-destructive">{profileError}</p>}
|
||||
</div>
|
||||
</SettingItem>
|
||||
<SettingItem
|
||||
|
||||
Reference in New Issue
Block a user