Documents the 7 real gaps (per-class enable, model/provider allow-lists, seats/usage UI, retrieval off-switch, consent) against the existing entitlement.ts/policy.tsx backend, proposed AiConsoleConfig schema, new endpoints, and a 6-section UI layout. Companion visual mockup presented separately. No application code changed — spec + mockup only, as instructed.
9.5 KiB
Admin AI Policy Console — Spec (docs/AI-ASSISTANT-CONCEPT.md §6)
Status: SPEC + MOCKUP ONLY — not implemented. Per explicit instruction, this is presented for approval before any of it is coded. Nothing in this document has a corresponding UI yet; the referenced existing files are the real, already-shipped backend this console would sit on top of.
1. Why this exists
Every AI policy lever that exists today is either hardcoded, env-only, or has a data endpoint with no UI:
| Lever | Today | Gap |
|---|---|---|
| AI Assistant on/off | FeatureGates.aiAssistantEnabled, toggle in the generic Policy tab (policy.tsx) |
None — this one's real and stays where it is. |
Which classes (local/server/public) are reachable |
lib/ai/types.ts: local+public hardcoded on, server auto-added only if AI_SERVER_BASE_URL is set (app/api/ai/policy/route.ts) |
No admin override. An admin cannot disable public (BYOK) org-wide, or disable server while keeping the env var set for staging. |
| Server-class model list | Every completion-capable model Ollama reports, unfiltered (app/api/ai/server/models/route.ts) |
No allow-list. If the shared Ollama host has a large/expensive model loaded, every user can select it. |
| BYOK (public) provider endpoints | Fully open — a user can point baseUrl at anything (lib/ai/local-settings.ts) |
No admin allow-list of approved providers/base URLs. Pure client trust today. |
Entitlement / seats (server class) |
Real enforcement + data endpoints exist (lib/ai/entitlement.ts, app/api/admin/ai/entitlement/route.ts) |
No UI. An admin today can only set seatsTotal via curl against the admin API. |
| Usage / metering ledger | Real, append-only, already recorded on every server-class call |
No UI. Same — curl only. |
| Retrieval (mail content → embeddings) | Always on when the server leg is reachable (lib/ai/retrieval/mail-embeddings.ts) |
No org-level off switch. A privacy-conscious admin cannot disable server-side mail-content augmentation independent of disabling the whole server class. |
| BYOK consent | Schema has publicConsentVersion: string | null, permanently null (lib/ai/types.ts) |
No admin-authored consent text or version bump flow. |
This console is the single screen that closes all seven gaps.
2. Scope boundary
In scope: a new admin tab that reads/writes the levers above. Not in scope (explicitly deferred, flag if wrong): per-user overrides (everything here is tenant-wide), model pricing, any billing/invoice integration beyond the existing metering ledger, S/MIME/theme consoles (separate features).
3. Data model additions
New persisted config, AiConsoleConfig, stored via the existing
config-manager convention (CONFIG dir, operator-authored — see
lib/admin/paths.ts's getConfigDir() vs
getStatePath() distinction; this is config, seats/ledger stay in STATE
where entitlement.ts already puts them):
export interface AiConsoleConfig {
/** Per-class admin override. A class must be BOTH infra-available
* (server: AI_SERVER_BASE_URL set) AND enabled here to reach users.
* Missing entries default to true (local/public) / false (server) —
* matches today's DEFAULT_AI_ENTITLEMENT.classes behavior exactly, so
* 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, checked
* client-side (advisory — see §6 open question on server-side
* enforcement) at profile-save time. */
publicProviderAllowlist: string[] | null;
/** Master switch for the retrieval leg (mail-content → embeddings).
* Independent of classesEnabled.server: an admin can allow chat but
* disable content augmentation. Defaults true (today's behavior). */
retrievalEnabled: boolean;
/** Admin-authored consent shown once per user before first BYOK/public
* use. Replaces the permanently-null publicConsentVersion. Bumping
* `version` re-prompts every user (their locally-stored acceptance is
* keyed by version — client-side change, not in this doc's scope). */
consent: { version: string; text: string } | null;
}
export const DEFAULT_AI_CONSOLE_CONFIG: AiConsoleConfig = {
classesEnabled: {},
serverModelAllowlist: null,
publicProviderAllowlist: null,
retrievalEnabled: true,
consent: null,
};
Entitlement (tier, seatsTotal, seat list, ledger) needs no new schema
— lib/ai/entitlement.ts already has everything
the console needs to read and write.
4. New/changed API endpoints
GET /api/admin/ai/policy(new, admin-protected) — returnsAiConsoleConfig.PUT /api/admin/ai/policy(new, admin-protected) — validates and persists partial updates, audit-logs each change (ai.console.classes_updated,ai.console.consent_updated, etc., following the existingauditLog()convention inapp/api/admin/policy/route.ts).GET /api/ai/policy(existing, extend) — foldsclassesEnabledinto theclassescomputation (a class only appears if infra-available AND admin-enabled), addsretrievalEnabledand the currentconsentblock to the response so the client can gate/prompt correctly.GET/PUT /api/admin/ai/entitlement(existing, unchanged) — the console UI simply gets a front-end for what already exists.
5. UI — new admin tab "AI"
New file app/(main)/admin/_tabs/ai-policy.tsx, registered alongside the
existing tabs (Policy, Themes, Plugins, …) in whatever wires up the sidebar
today. Follows the exact visual conventions already in
policy.tsx: bordered
rounded-lg sections with a bg-muted/30 header strip, the same toggle
switch markup, save button that only appears when dirty.
Six sections, top to bottom (see the companion mockup for the visual):
- Provider Classes — three cards (Local / Server / Public), each a toggle + one line of status. Server's card shows "Not configured (AI_SERVER_BASE_URL unset)" and disables its own toggle when infra isn't there, rather than letting an admin flip on something that 503s.
- Server: Model Allow-list — only visible/enabled when Server is on.
Multi-select pulled live from
/api/ai/server/models, defaulting to "all models" (today's behavior) with an explicit switch to "restrict to selected". - Public (BYOK): Provider Allow-list — same pattern, but base-URL
prefixes instead of model names (e.g.
https://api.openai.com,https://api.anthropic.com), free-text add/remove list, defaulting to unrestricted. - Entitlement & Seats — tier picker (base/standard/pro — cosmetic today,
no different enforcement per tier, flagged as such), seat total number
input, and a live table of assigned seats each with a "Revoke" button —
direct front-end for the existing
PUT .../entitlementwithrevokeUsername. - Usage — read-only table, last 200 rows of the metering ledger (timestamp, user, model, tokens, latency), plus a one-line rollup (calls today, total tokens this week) computed client-side from the same rows — no new aggregation endpoint needed for a first cut.
- Retrieval & Consent — one toggle (retrieval on/off) + a textarea for consent text with a version string input and a "Bump version (re-prompt all users)" button.
6. Open questions for approval
- Provider allow-list enforcement point. Spec above checks the BYOK
base-URL allow-list client-side only (at profile-save time in Settings).
True enforcement would require routing BYOK calls through this app's own
backend (losing the "no CORS problem, no server cost" property that made
publicattractive as client-direct in the first place — see vncmail-ai-assistant-rollout decision #3). Recommend: ship client-side only for now, document it as advisory, revisit if it needs to be a real boundary. - Tier semantics.
tier(base/standard/pro) exists inAiEntitlementStatetoday but nothing reads it to change behavior (seat count is the only real gate). This console would let an admin set it without it doing anything yet. Recommend: keep the picker (cheap, matches the schema, avoids a future migration) but label it "cosmetic — no tier-differentiated behavior yet" in the UI itself, not just this doc. - Where the master
aiAssistantEnabledtoggle lives. Recommend leaving it in the generic Policy tab (single source of truth, already shipped, already tested) and just linking to it from the top of this new AI tab ("AI Assistant is currently ON — change in Policy →") rather than duplicating the toggle in two places.
7. Explicitly not building yet
Per-user/per-group overrides, billing/invoice integration, server-side
enforcement of the BYOK allow-list (see 6.1), any change to how local
works (stays free/unmonitored/client-direct, per standing decision).