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.
166 lines
9.5 KiB
Markdown
166 lines
9.5 KiB
Markdown
# 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](../app/(main)/admin/_tabs/policy.tsx)) | None — this one's real and stays where it is. |
|
|
| Which classes (`local`/`server`/`public`) are reachable | [`lib/ai/types.ts`](../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`](../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`](../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`](../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`](../lib/ai/entitlement.ts), [`app/api/admin/ai/entitlement/route.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`](../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`](../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`](../lib/admin/paths.ts)'s `getConfigDir()` vs
|
|
`getStatePath()` distinction; this is config, seats/ledger stay in STATE
|
|
where `entitlement.ts` already puts them):
|
|
|
|
```ts
|
|
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`](../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) — returns
|
|
`AiConsoleConfig`.
|
|
- **`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 existing `auditLog()` convention in
|
|
[`app/api/admin/policy/route.ts`](../app/api/admin/policy/route.ts)).
|
|
- **`GET /api/ai/policy`** (existing, extend) — folds `classesEnabled` into
|
|
the `classes` computation (a class only appears if infra-available AND
|
|
admin-enabled), adds `retrievalEnabled` and the current `consent` block 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`](../app/(main)/admin/_tabs/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):
|
|
|
|
1. **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.
|
|
2. **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".
|
|
3. **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.
|
|
4. **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 .../entitlement` with
|
|
`revokeUsername`.
|
|
5. **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.
|
|
6. **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
|
|
|
|
1. **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
|
|
`public` attractive 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.
|
|
2. **Tier semantics.** `tier` (base/standard/pro) exists in
|
|
`AiEntitlementState` today 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.
|
|
3. **Where the master `aiAssistantEnabled` toggle 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).
|