From 58313f7e31b28097ca1665d932d16bd5cc47ef3f Mon Sep 17 00:00:00 2001 From: xcaliber Date: Wed, 11 Mar 2026 14:45:37 +0000 Subject: [PATCH] Changeset 0.28.0.1 (#173) --- VERSION | 2 +- docs/ICD-API.md | 2621 ----------------- docs/ICD-AUDIT.md | 265 -- docs/ICD/README.md | 155 + docs/ICD/admin.md | 198 ++ docs/ICD/auth.md | 175 ++ docs/ICD/channels.md | 711 +++++ docs/ICD/enums.md | 250 ++ docs/ICD/extensions.md | 34 + docs/ICD/knowledge.md | 167 ++ docs/ICD/memory.md | 48 + docs/ICD/models.md | 88 + docs/ICD/notes.md | 105 + docs/ICD/notifications.md | 44 + docs/ICD/personas.md | 204 ++ docs/ICD/profile.md | 55 + docs/ICD/projects.md | 87 + docs/ICD/providers.md | 221 ++ docs/ICD/surfaces.md | 118 + docs/ICD/tasks.md | 290 ++ docs/ICD/teams.md | 177 ++ docs/ICD/utility.md | 146 + docs/ICD/websocket.md | 219 ++ docs/ICD/workflows.md | 472 +++ docs/ICD/workspaces.md | 137 + docs/{ => archive}/AUDIT-0.22.0-PREFLIGHT.md | 0 docs/{ => archive}/DESIGN-0.24.0.md | 0 docs/{ => archive}/DESIGN-0.26.0.md | 0 docs/{ => archive}/DESIGN-0.27.0.md | 0 docs/{ => archive}/DESIGN-CM6.md | 0 docs/{ => archive}/v0171-sqlite-backend.md | 0 .../database/migrations/sqlite/002_teams.sql | 3 + .../migrations/sqlite/016_v023_multiuser.sql | 23 - .../database/migrations/sqlite/017_unread.sql | 3 - .../sqlite/018_auth_abstraction.sql | 8 - .../migrations/sqlite/019_auth_providers.sql | 13 - .../migrations/sqlite/020_permissions.sql | 25 - .../migrations/sqlite/021_sessions.sql | 20 - .../migrations/sqlite/023_workflows.sql | 56 - .../sqlite/024_workflow_instances.sql | 13 - .../sqlite/025_workflow_assignments.sql | 21 - .../database/migrations/sqlite/026_tasks.sql | 63 - .../migrations/sqlite/027_webhooks.sql | 5 - server/handlers/workflow_assignments.go | 50 +- server/handlers/workflow_entry.go | 10 +- server/handlers/workflow_instance_test.go | 844 ++++++ server/handlers/workflow_instances.go | 8 +- server/handlers/workflow_test.go | 2 +- server/handlers/workflows.go | 12 +- server/main.go | 4 +- server/models/workflow.go | 3 +- server/store/postgres/workflows.go | 61 +- server/store/sqlite/routing_policy.go | 4 +- server/store/sqlite/sessions.go | 8 +- server/store/sqlite/tasks.go | 10 +- server/store/sqlite/workflows.go | 76 +- server/tools/workflow.go | 11 +- 57 files changed, 5139 insertions(+), 3206 deletions(-) delete mode 100644 docs/ICD-API.md delete mode 100644 docs/ICD-AUDIT.md create mode 100644 docs/ICD/README.md create mode 100644 docs/ICD/admin.md create mode 100644 docs/ICD/auth.md create mode 100644 docs/ICD/channels.md create mode 100644 docs/ICD/enums.md create mode 100644 docs/ICD/extensions.md create mode 100644 docs/ICD/knowledge.md create mode 100644 docs/ICD/memory.md create mode 100644 docs/ICD/models.md create mode 100644 docs/ICD/notes.md create mode 100644 docs/ICD/notifications.md create mode 100644 docs/ICD/personas.md create mode 100644 docs/ICD/profile.md create mode 100644 docs/ICD/projects.md create mode 100644 docs/ICD/providers.md create mode 100644 docs/ICD/surfaces.md create mode 100644 docs/ICD/tasks.md create mode 100644 docs/ICD/teams.md create mode 100644 docs/ICD/utility.md create mode 100644 docs/ICD/websocket.md create mode 100644 docs/ICD/workflows.md create mode 100644 docs/ICD/workspaces.md rename docs/{ => archive}/AUDIT-0.22.0-PREFLIGHT.md (100%) rename docs/{ => archive}/DESIGN-0.24.0.md (100%) rename docs/{ => archive}/DESIGN-0.26.0.md (100%) rename docs/{ => archive}/DESIGN-0.27.0.md (100%) rename docs/{ => archive}/DESIGN-CM6.md (100%) rename docs/{ => archive}/v0171-sqlite-backend.md (100%) delete mode 100644 server/database/migrations/sqlite/016_v023_multiuser.sql delete mode 100644 server/database/migrations/sqlite/017_unread.sql delete mode 100644 server/database/migrations/sqlite/018_auth_abstraction.sql delete mode 100644 server/database/migrations/sqlite/019_auth_providers.sql delete mode 100644 server/database/migrations/sqlite/020_permissions.sql delete mode 100644 server/database/migrations/sqlite/021_sessions.sql delete mode 100644 server/database/migrations/sqlite/023_workflows.sql delete mode 100644 server/database/migrations/sqlite/024_workflow_instances.sql delete mode 100644 server/database/migrations/sqlite/025_workflow_assignments.sql delete mode 100644 server/database/migrations/sqlite/026_tasks.sql delete mode 100644 server/database/migrations/sqlite/027_webhooks.sql create mode 100644 server/handlers/workflow_instance_test.go diff --git a/VERSION b/VERSION index 6ca8061..022a033 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.27.5 \ No newline at end of file +0.28.0 \ No newline at end of file diff --git a/docs/ICD-API.md b/docs/ICD-API.md deleted file mode 100644 index e417a89..0000000 --- a/docs/ICD-API.md +++ /dev/null @@ -1,2621 +0,0 @@ -# ICD-API — Chat Switchboard Backend API Contract - -**Version:** 0.23.0 -**Updated:** 2026-03-04 -**Audience:** Frontend developers, integrators, API consumers - -> **Organization principle:** Each section is one domain — the complete story. -> Every endpoint that touches Knowledge Bases is in §5, every endpoint that -> touches Personas is in §4, etc. Admin and user endpoints live together in -> their domain section. Cross-cutting platform admin (user management, global -> settings, audit) lives in §16. - ---- - -## Table of Contents - -1. [Conventions](#1-conventions) -2. [Auth](#2-auth) -3. [Channels & Conversations](#3-channels--conversations) -4. [Personas](#4-personas) -5. [Knowledge Bases](#5-knowledge-bases) -6. [Notes](#6-notes) -7. [Workspaces & Git](#7-workspaces--git) -8. [Projects](#8-projects) -9. [Memory](#9-memory) -10. [Providers & Routing](#10-providers--routing) -11. [Models & Preferences](#11-models--preferences) -12. [Notifications](#12-notifications) -13. [Extensions](#13-extensions) -14. [User Profile & Settings](#14-user-profile--settings) -15. [Teams & Access Control](#15-teams--access-control) -16. [Platform Administration](#16-platform-administration) -17. [Export & Utility](#17-export--utility) -17b. [Files & Storage](#17b-files--storage) -18. [WebSocket Protocol](#18-websocket-protocol) -19. [Appendix: Enums](#19-appendix-enums) - ---- - -## 1. Conventions - -### Base URL - -``` -{scheme}://{host}{BASE_PATH}/api/v1 -``` - -`BASE_PATH` is empty by default. In path-routed K8s deployments (e.g. -`/staging/`), all routes shift accordingly. The frontend reads `BASE_PATH` -from a `` tag injected by the Go template engine. - -### Authentication - -JWT bearer tokens. Every request to a `protected` or `admin` route requires: - -``` -Authorization: Bearer {access_token} -``` - -**Access token:** HS256 JWT, 15-minute TTL. Claims: `user_id`, `email`, -`role`, `exp`, `iat`, `jti`. - -**Refresh token:** Opaque, DB-stored, 7-day TTL. Used to obtain new -access tokens without re-login. - -**WebSocket fallback:** `?token={access_token}` query parameter when -the `Authorization` header isn't available. - -**Cookie sync:** On every token save/refresh, the frontend writes -`sb_token` as a cookie. Go template page routes read this cookie via -`AuthOrRedirect` middleware for server-rendered surfaces. - -### Authorization Tiers - -| Tier | Middleware | Who | -|------|-----------|-----| -| Public | none | Anyone (health, login, register, public settings) | -| Authenticated | `Auth()` | Any logged-in user | -| Team Admin | `RequireTeamAdmin()` | Admin of the specific team | -| Platform Admin | `RequireAdmin()` | Users with `role = "admin"` | - -### Error Envelope - -All errors return: - -```json -{ "error": "human-readable message" } -``` - -Standard HTTP status codes: 400 (bad request), 401 (not authenticated), -403 (not authorized), 404 (not found), 409 (conflict), 500 (server error), -502 (upstream provider failure). - -### Pagination Envelope - -Endpoints that paginate return: - -```json -{ - "data": [...], - "page": 1, - "per_page": 50, - "total": 142, - "total_pages": 3 -} -``` - -Query params: `?page=1&per_page=50`. Not all list endpoints paginate — -some return `{ "data": [...] }` or a bare array in a named key. - -### ID Format - -All resource IDs are UUIDv4 strings, generated application-side via -`store.NewID()` (Go `uuid.New()`). This ensures compatibility across -both Postgres and SQLite backends. - -### Timestamps - -ISO 8601 with timezone: `"2025-06-15T14:30:00Z"`. Stored as -`TIMESTAMPTZ` (Postgres) or `TEXT` (SQLite). - ---- - -## 2. Auth - -Four endpoints. No token required for any of them. - -### Register - -``` -POST /auth/register -``` - -```json -{ - "username": "jdoe", - "password": "...", - "email": "jdoe@example.com", // optional - "display_name": "Jane Doe" // optional -} -``` - -Gated by `allow_registration` policy. Returns same shape as Login. - -### Login - -``` -POST /auth/login -``` - -```json -{ "username": "jdoe", "password": "..." } -``` - -**Response:** - -```json -{ - "access_token": "eyJ...", - "refresh_token": "opaque-string", - "user": { - "id": "uuid", - "username": "jdoe", - "email": "jdoe@example.com", - "display_name": "Jane Doe", - "role": "user", - "avatar_url": "/api/v1/profile/avatar?v=1234", - "created_at": "...", - "last_login": "..." - } -} -``` - -### Refresh - -``` -POST /auth/refresh -``` - -```json -{ "refresh_token": "opaque-string" } -``` - -Returns new `access_token` and `refresh_token`. Old refresh token is -invalidated (rotation). - -### Logout - -``` -POST /auth/logout -``` - -```json -{ "refresh_token": "opaque-string" } -``` - -Revokes the refresh token. Returns `{ "message": "logged out" }`. - ---- - -## 3. Channels & Conversations - -The **channel** is the universal conversation container. Messages, -tool activity, attachments, and KB bindings all hang off a channel. -Channels support multiple participants — users, personas, and -sessions — making them the foundation for collaborative and -multi-model workflows. - -### 3.1 Channel CRUD - -**List channels** — paginated, sorted by last activity. - -``` -GET /channels?page=1&per_page=50 -``` - -Returns pagination envelope. Each channel: - -```json -{ - "id": "uuid", - "title": "My Chat", - "type": "direct|group|workflow", - "description": "", - "model": "claude-sonnet-4-20250514", - "provider_config_id": "uuid|null", - "system_prompt": "", - "project_id": "uuid|null", - "folder": "string|null", - "tags": ["tag1", "tag2"], - "participant_count": 1, - "created_at": "...", - "updated_at": "..." -} -``` - -**Channel types:** - -| Type | Description | -|------|-------------| -| `direct` | Single-user chat (default, backward compatible) | -| `group` | Multi-user/multi-model collaborative channel | -| `workflow` | Staged channel driven by workflow definitions | - -**Create channel:** - -``` -POST /channels -``` - -```json -{ - "title": "New Chat", - "type": "direct", - "description": "", - "model": "claude-sonnet-4-20250514", - "provider_config_id": "uuid", - "system_prompt": "", - "folder": null, - "tags": [] -} -``` - -On create, the authenticated user is automatically added as a -participant with `role: "owner"`. If `model` and `provider_config_id` -are provided, the channel model roster (`channel_models`) is -auto-populated with an initial entry. - -**Get channel:** - -``` -GET /channels/:id -``` - -Returns single channel object. - -**Update channel:** - -``` -PUT /channels/:id -``` - -Accepts partial updates — only fields present in the body are changed. -Same field set as create. - -**Delete channel:** - -``` -DELETE /channels/:id -``` - -Cascades: deletes messages, attachments, channel_models, channel_kbs. - -### 3.2 Message Tree - -Switchboard uses a **tree** model for messages, not a flat list. Each -message has a `parent_id` (NULL for root). Editing creates a sibling -branch; regeneration creates an alternative sibling. The **cursor** -tracks which child is "active" at each branch point. - -**Get active path** — the primary endpoint for loading chat history: - -``` -GET /channels/:id/path -``` - -Returns the messages along the active branch from root to leaf, -following the cursor at each fork. This is what the UI renders. - -```json -{ - "messages": [ - { - "id": "uuid", - "channel_id": "uuid", - "parent_id": "uuid|null", - "role": "user|assistant|system|tool", - "content": "...", - "model": "claude-sonnet-4-20250514|null", - "provider_config_id": "uuid|null", - "participant_id": "uuid|null", - "participant_type": "user|persona|session|null", - "thinking": "...|null", - "tool_calls": [...], - "tool_results": [...], - "attachments": [...], - "has_siblings": true, - "sibling_index": 0, - "sibling_count": 2, - "created_at": "..." - } - ] -} -``` - -`participant_id` and `participant_type` identify who authored the -message. For `user` messages, this is the authenticated user. For -`assistant` messages, this is the persona (if set) or null for raw -model responses. Existing messages without participants return null. - -**List all messages** — includes all branches, flat: - -``` -GET /channels/:id/messages -``` - -Legacy/debug endpoint. Returns every message in the channel regardless -of branch. Not used by the standard frontend. - -**Create message:** - -``` -POST /channels/:id/messages -``` - -```json -{ - "role": "user", - "content": "Hello", - "parent_id": "uuid|null", - "attachments": ["attachment-uuid-1"] -} -``` - -**Edit message** — creates a sibling at the same level: - -``` -POST /channels/:id/messages/:msgId/edit -``` - -```json -{ "content": "Revised question" } -``` - -Creates a new message with the same `parent_id` as the original. -Automatically updates the cursor to point to the new sibling. - -**Regenerate** — creates an alternative assistant response: - -``` -POST /channels/:id/messages/:msgId/regenerate -``` - -```json -{ - "model": "claude-sonnet-4-20250514", - "provider_config_id": "uuid" -} -``` - -Creates a new empty assistant message as a sibling of `msgId`, -then streams the completion into it. The cursor is updated. - -**Update cursor** — switch to a different branch: - -``` -PUT /channels/:id/cursor -``` - -```json -{ - "message_id": "uuid", - "child_id": "uuid" -} -``` - -Sets which child is active at the `message_id` branch point. -Subsequent `GET /channels/:id/path` will follow the new branch. - -**List siblings** — all children of a message's parent: - -``` -GET /channels/:id/messages/:msgId/siblings -``` - -Returns `{ "siblings": [...] }` with abbreviated message objects -(id, role, content preview, created_at). - -### 3.3 Completions & Streaming - -The single most complex endpoint. Sends a message to an LLM provider -and streams the response via Server-Sent Events. - -``` -POST /chat/completions -``` - -**Request:** - -```json -{ - "channel_id": "uuid", - "message": "User's message text", - "model": "claude-sonnet-4-20250514", - "provider_config_id": "uuid", - "persona_id": "uuid|null", - "parent_id": "uuid|null", - "tools_enabled": true, - "tool_ids": ["web_search", "calculator"], - "attachments": ["attachment-uuid"], - "extra_body": {} -} -``` - -`channel_id` is required. - -`extra_body` is a freeform JSON object merged into the provider request -(provider-specific parameters like `temperature`, `reasoning`, etc.). - -**Participant scoping:** The requesting user must be a participant in -the channel. Messages are attributed to the authenticated user (or -session). In `group` channels, all participants see all messages in -real time via WebSocket. - -**Persona resolution:** The primary completion path. If `persona_id` -is set (explicitly or via `@mention` resolution from message content), -the handler loads the persona's system prompt, model, provider config, -and KB bindings. Per-message `model`/`provider_config_id` override the -persona's defaults. In multi-persona channels, `@mention` in the -message content is parsed to resolve the target persona from the -channel's participant list. - -**Routing resolution:** After config resolution, the routing policy -evaluator (`evaluateRouting()`) may redirect to a different provider -based on active policies (see §10.4). The winning provider config's -credentials are loaded and used for the actual API call. - -**SSE Stream Format:** - -The response is `Content-Type: text/event-stream`. Events: - -``` -data: {"content":"Hello"} - -data: {"content":" world"} - -data: {"reasoning":"Let me think..."} - -event: tool_use -data: {"id":"call_1","name":"web_search","input":{"query":"..."}} - -event: tool_result -data: {"id":"call_1","content":"Search results..."} - -data: {"content":"Based on the search..."} - -event: finish -data: {"message_id":"uuid","model":"claude-sonnet-4-20250514","usage":{"input_tokens":150,"output_tokens":42}} - -data: [DONE] -``` - -| Event | `event:` field | Payload | -|-------|---------------|---------| -| Content delta | _(unnamed)_ | `{ "content": "text" }` | -| Reasoning delta | _(unnamed)_ | `{ "reasoning": "text" }` | -| Tool invocation | `tool_use` | `{ "id", "name", "input" }` | -| Tool result | `tool_result` | `{ "id", "content" }` | -| Stream complete | `finish` | `{ "message_id", "model", "usage" }` | -| End sentinel | _(unnamed)_ | `[DONE]` (literal string) | -| Error mid-stream | `error` | `{ "error": "message" }` | - -**Tool cycle:** Tool use and tool result events can repeat up to -`max_tool_iterations` (configured server-side). The handler executes -tools, feeds results back to the model, and continues streaming. - -**Response headers:** - -``` -X-Switchboard-Provider: providerID/configID -X-Switchboard-Route: policy-name (when routing policy is active) -``` - -**List available tools:** - -``` -GET /tools -``` - -Returns `{ "tools": [...] }` where each tool has `name`, `description`, -`parameters` (JSON Schema), and `category`. - -### 3.4 Multi-Model Roster (Raw Access) - -The primary way to add AI to a channel is by adding a **persona as a -participant** (§3.7). Adding a persona automatically populates the -channel model roster with the persona's underlying model. - -For the rare case where a user needs raw model access without a -persona wrapper, the `channel_models` table provides direct model -roster management. This is the 1% escape hatch — most users should -never need it. - -``` -GET /channels/:id/models → { "models": [...] } -POST /channels/:id/models ← { "model": "...", "provider_config_id": "...", "display_name": "..." } -PATCH /channels/:id/models/:modelId ← { "display_name": "..." } -DELETE /channels/:id/models/:modelId -``` - -Each roster entry: - -```json -{ - "id": "uuid", - "channel_id": "uuid", - "model": "claude-sonnet-4-20250514", - "provider_config_id": "uuid", - "display_name": "Claude", - "is_default": true, - "persona_id": "uuid|null", - "created_at": "..." -} -``` - -`persona_id`: set when this roster entry was auto-created by adding a -persona participant. Null for manually-added raw models. - -**`@mention` resolution:** When a user types `@` in a channel, the -autocomplete shows persona participants first (by display name), then -raw model roster entries. The target of a `@mention` is always -resolved to a specific `provider_config_id` + `model` pair — never -an ambiguous bare model name. - -### 3.5 Channel Utilities - -**Summarize channel** (compaction): - -``` -POST /channels/:id/summarize -``` - -Triggers conversation summarization via the utility model role. -Returns `{ "message": "summarized", "summary_id": "uuid" }`. - -**Generate title:** - -``` -POST /channels/:id/generate-title -``` - -Uses the utility model to auto-generate a title from the first -exchange. Returns `{ "title": "Generated Title" }`. - -### 3.6 Files - -All binary content associated with channels — user uploads, tool- -generated artifacts, system files — lives in the unified `files` -table backed by the ObjectStore (see §X). The old `attachments` -table and routes are removed. - -**Upload (user):** - -``` -POST /channels/:id/files -Content-Type: multipart/form-data -``` - -Field: `file`. Sets `origin: "user_upload"`. Returns the file object. - -**Create (tool output):** - -``` -POST /files -``` - -```json -{ - "channel_id": "uuid", - "message_id": "uuid", - "filename": "generated_image.png", - "content_type": "image/png", - "display_hint": "inline", - "metadata": { "tool_name": "image_generation" } -} -``` - -Body: multipart `file` field or base64 `data` field. -Sets `origin: "tool_output"`. Returns file object. - -**List by channel:** - -``` -GET /channels/:id/files?origin=user_upload -``` - -Returns `{ "files": [...] }`. Filterable by `origin`, `content_type`. - -**List by message:** - -``` -GET /messages/:id/files → { "files": [...] } -``` - -How the frontend discovers generated artifacts to render inline. - -**List by user (file manager):** - -``` -GET /files?page=1&per_page=50 → { "files": [...], "total": N } -``` - -All files owned by the authenticated user, paginated. - -**Get metadata:** - -``` -GET /files/:id -``` - -```json -{ - "id": "uuid", - "channel_id": "uuid", - "message_id": "uuid|null", - "user_id": "uuid", - "origin": "user_upload|tool_output|system", - "filename": "report.pdf", - "content_type": "application/pdf", - "size_bytes": 1048576, - "display_hint": "inline|download|thumbnail", - "extracted_text": "...|null", - "metadata": {}, - "created_at": "...", - "updated_at": "..." -} -``` - -**Download:** - -``` -GET /files/:id/download -``` - -Returns the file with appropriate `Content-Type` and -`Content-Disposition` headers. - -**Thumbnail:** - -``` -GET /files/:id/thumbnail -``` - -**Delete:** - -``` -DELETE /files/:id -``` - -Deletes metadata + blob. Channel deletion cascades via FK + -`DeletePrefix`. - -### 3.7 Channel Participants - -The participant system is how users and AI interact in a channel. -**Personas are the primary way to add AI** — adding a persona as a -participant brings its full identity (name, avatar, system prompt, -model, provider config, KB bindings) into the channel. This is the -99% path. Raw model access without a persona is available via the -model roster (§3.4) for power users. - -Every channel has at least one participant (the owner). `direct` -channels have exactly one user participant and optionally one or more -persona participants. `group` and `workflow` channels support multiple -participants of mixed types. - -**List participants:** - -``` -GET /channels/:id/participants -``` - -Returns `{ "participants": [...] }`: - -```json -{ - "id": "uuid", - "channel_id": "uuid", - "participant_type": "user|persona|session", - "participant_id": "uuid", - "role": "owner|member|observer", - "display_name": "Jane Doe", - "avatar_url": "...|null", - "joined_at": "..." -} -``` - -**Participant types:** - -| Type | Description | -|------|-------------| -| `user` | Authenticated user. `participant_id` = `users.id` | -| `persona` | AI persona added to channel. `participant_id` = `personas.id`. Brings model, system prompt, KB bindings | -| `session` | Anonymous/session participant (workflow intake). `participant_id` = session token | - -**Participant roles:** - -| Role | Capabilities | -|------|-------------| -| `owner` | Full control: add/remove participants, delete channel, all member capabilities | -| `member` | Send messages, trigger completions, view history | -| `observer` | Read-only: view messages, no send | - -**Add participant:** - -``` -POST /channels/:id/participants -``` - -```json -{ - "participant_type": "user|persona", - "participant_id": "uuid", - "role": "member" -} -``` - -Requires `owner` role in the channel. Adding a `persona` participant: -- Makes the persona available as an `@mention` target -- Adds the persona's model to the channel model roster automatically -- Scopes the persona's KB bindings into the channel's KB resolution chain -- The persona's avatar and display name appear in the participant list - -**Update participant role:** - -``` -PATCH /channels/:id/participants/:participantId -``` - -```json -{ "role": "observer" } -``` - -**Remove participant:** - -``` -DELETE /channels/:id/participants/:participantId -``` - -Cannot remove the last owner. Removing a `persona` participant also -removes its auto-created model roster entry. - -**Completion routing:** When `@PersonaName` appears in message -content, the completion handler resolves to that persona's model, -provider config, and system prompt. When no `@mention` is present in -a multi-persona channel, the channel's default persona (first added) -handles the response. The `persona_id` field in the completion -request (§3.3) can also be set explicitly to override `@mention` -resolution. - -**Backward compatibility:** Existing `direct` channels that predate -the participant system are auto-migrated on first access — a single -`user` participant with `role: "owner"` is created from the channel's -legacy `user_id` column. - -### 3.8 Presence - -Real-time presence for channel participants. Delivered via WebSocket -to all subscribers of the channel room. - -**Who's here:** - -``` -GET /channels/:id/presence -``` - -Returns `{ "participants": [{ "participant_id", "display_name", "status", "last_seen" }] }`. - -`status`: `online`, `idle`, `offline`. - -**Typing indicators** are sent via WebSocket (see §18.4). The typing -event payload includes `participant_id` and `participant_type` so the -UI can distinguish between human and AI typing. - ---- - -## 4. Personas - -A **Persona** is the unit of access control and AI identity: system -prompt + model + provider config + KB bindings + grant scoping. Users -interact with Personas, not raw provider configs. Admins curate which -Personas are available; team admins create team-scoped Personas. - -**Model inheritance:** A Persona inherits all properties of its -underlying model. This includes context window size, max output -tokens, supported capabilities (thinking, tools, vision, streaming), -input/output pricing, and provider status. When the frontend displays -a Persona, it resolves the underlying model's capabilities and shows -the same pills/badges (e.g. thinking, tools, 200k context) that the -raw model would show. The Persona adds identity (name, avatar, system -prompt, KB bindings) on top of the model's capabilities — it never -masks or reduces them. - -Three scopes, three route namespaces, one domain: - -| Scope | Routes | Who manages | -|-------|--------|-------------| -| Personal | `GET/POST/PUT/DELETE /personas` | The user | -| Team | `GET/POST/PUT/DELETE /teams/:teamId/personas` | Team admin | -| Global (admin) | `GET/POST/PUT/DELETE /admin/personas` | Platform admin | - -All three return the same Persona object shape: - -```json -{ - "id": "uuid", - "name": "Code Reviewer", - "description": "Reviews code for bugs and style", - "system_prompt": "You are a senior code reviewer...", - "model": "claude-sonnet-4-20250514", - "provider_config_id": "uuid|null", - "scope": "personal|team|global", - "owner_id": "uuid|null", - "is_default": false, - "is_active": true, - "avatar_url": "/api/v1/personas/uuid/avatar?v=123", - "inherited": { - "context_window": 200000, - "max_output_tokens": 8192, - "supports_vision": true, - "supports_tools": true, - "supports_thinking": true, - "supports_streaming": true, - "input_price_per_m": 3.00, - "output_price_per_m": 15.00, - "provider_status": "healthy|degraded|down|null" - }, - "created_at": "...", - "updated_at": "..." -} -``` - -The `inherited` block is populated by the backend from the resolved -model capabilities at response time — it is not stored on the persona. -If the underlying model's capabilities change (e.g. provider upgrades -context window), the persona inherits the new values automatically. -The frontend uses `inherited` to render capability pills and context -size badges identical to the raw model. - -### 4.1 Personal Personas - -Gated by `allow_user_personas` policy. - -``` -GET /personas → { "personas": [...] } -POST /personas ← { "name", "description", "system_prompt", "model", ... } -PUT /personas/:id ← partial update -DELETE /personas/:id -``` - -### 4.2 Team Personas - -``` -GET /teams/:teamId/personas → { "personas": [...] } -POST /teams/:teamId/personas ← same shape -PUT /teams/:teamId/personas/:id -DELETE /teams/:teamId/personas/:id -``` - -### 4.3 Admin (Global) Personas - -``` -GET /admin/personas → { "personas": [...] } -POST /admin/personas ← same shape -PUT /admin/personas/:id -DELETE /admin/personas/:id -``` - -### 4.4 Persona KB Bindings - -A Persona can have Knowledge Bases bound to it. When a channel uses -that Persona, the bound KBs are automatically scoped into `kb_search` -tool calls. Users never see or manage the underlying KBs directly — -they talk to the Persona. - -**Get bindings:** - -``` -GET /personas/:id/knowledge-bases → { "data": [KB objects] } -GET /teams/:teamId/personas/:id/knowledge-bases → { "data": [...] } -GET /admin/personas/:id/knowledge-bases → { "data": [...] } -``` - -**Set bindings** (replace all): - -``` -PUT /personas/:id/knowledge-bases -PUT /teams/:teamId/personas/:id/knowledge-bases -PUT /admin/personas/:id/knowledge-bases -``` - -```json -{ - "kb_ids": ["kb-uuid-1", "kb-uuid-2"], - "auto_search": { - "kb-uuid-1": true, - "kb-uuid-2": false - } -} -``` - -`auto_search`: when `true`, the KB is always searched (prepended to -context). When `false`, it's available via the `kb_search` tool but -not auto-injected. - -### 4.5 Persona Avatars - -``` -POST /personas/:id/avatar ← multipart/form-data (field: "avatar") -DELETE /personas/:id/avatar -POST /admin/personas/:id/avatar -DELETE /admin/personas/:id/avatar -``` - -### 4.6 Resource Grants - -See §15.3 for the grant system. Personas use grants to control who can -see and use them beyond their base scope. - ---- - -## 5. Knowledge Bases - -The **Knowledge Base** (KB) is the RAG system: upload documents → chunk -→ embed (pgvector / app-level cosine for SQLite) → search via -`kb_search` tool during completions. - -### 5.1 KB CRUD - -``` -GET /knowledge-bases → { "data": [KB objects] } -POST /knowledge-bases ← { "name", "description", "scope", "team_id" } -GET /knowledge-bases/:id → KB object -PUT /knowledge-bases/:id ← partial update -DELETE /knowledge-bases/:id -``` - -KB object: - -```json -{ - "id": "uuid", - "name": "Product Docs", - "description": "Internal product documentation", - "scope": "personal|team|global", - "owner_id": "uuid|null", - "team_id": "uuid|null", - "discoverable": true, - "embedding_model": "text-embedding-3-small", - "chunk_size": 512, - "chunk_overlap": 50, - "document_count": 12, - "created_at": "...", - "updated_at": "..." -} -``` - -### 5.2 Documents - -**Upload:** - -``` -POST /knowledge-bases/:id/documents -Content-Type: multipart/form-data -``` - -Field: `file`. Supported: PDF, DOCX, TXT, MD, HTML, CSV, XLSX, PPTX. -Returns the document object with `status: "pending"`. - -**List:** - -``` -GET /knowledge-bases/:id/documents -``` - -Returns `{ "data": [document objects] }`. - -**Status** (poll during processing): - -``` -GET /knowledge-bases/:id/documents/:docId/status -``` - -Status progression: `pending → chunking → embedding → ready | error`. - -**Delete:** - -``` -DELETE /knowledge-bases/:id/documents/:docId -``` - -### 5.3 Search - -``` -POST /knowledge-bases/:id/search -``` - -```json -{ - "query": "How do I configure SSO?", - "limit": 5 -} -``` - -Returns `{ "results": [{ "content", "score", "document_id", "metadata" }] }`. - -### 5.4 Rebuild - -Re-chunks and re-embeds all documents: - -``` -POST /knowledge-bases/:id/rebuild -``` - -### 5.5 Channel KB Bindings - -A channel can have KBs explicitly bound to it (in addition to any KBs -coming from the active Persona or parent Project). - -**Get:** - -``` -GET /channels/:id/knowledge-bases -``` - -Returns `{ "data": [ChannelKB objects] }`: - -```json -{ - "kb_id": "uuid", - "kb_name": "Product Docs", - "enabled": true, - "document_count": 12 -} -``` - -**Set** (replace all): - -``` -PUT /channels/:id/knowledge-bases -``` - -```json -{ "kb_ids": ["kb-uuid-1", "kb-uuid-2"] } -``` - -### 5.6 Discoverable KBs - -The `discoverable` flag controls whether a KB appears in the user's KB -picker. Non-discoverable KBs are hidden from direct access but still -searchable through Persona bindings (enterprise KB mode). - -**List discoverable** (for KB picker UI): - -``` -GET /knowledge-bases-discoverable -``` - -Returns `{ "data": [KB objects] }` — only KBs the user can see -(personal + team + discoverable global). - -**Toggle discoverability** (admin-enforced in handler): - -``` -PUT /knowledge-bases/:id/discoverable -``` - -```json -{ "discoverable": false } -``` - -### 5.7 KB Resolution Chain - -When a completion fires, KBs are resolved in priority order: - -``` -Persona KBs → Project KBs → Channel KBs → Personal KBs -``` - -The `BuildKBHint` function assembles all applicable KBs into the -system prompt, and the `kb_search` tool searches across all of them. - -### 5.8 Persona KB Bindings - -See §4.4. Persona-KB binding is the primary enterprise mechanism for -controlled KB access. - ---- - -## 6. Notes - -**Obsidian-style** knowledge graph with `[[wikilinks]]`, backlinks, -graph visualization, and folder organization. Notes are user-scoped -(personal notebook). Future: channel-scoped notes for workflow -artifacts. - -### 6.1 CRUD - -``` -GET /notes → paginated list of noteListItem -POST /notes ← { "title", "content", "folder" } -GET /notes/:id → full note object -PUT /notes/:id ← { "title", "content", "folder" } -DELETE /notes/:id -``` - -Note object: - -```json -{ - "id": "uuid", - "user_id": "uuid", - "title": "Meeting Notes", - "content": "# Meeting Notes\n\nDiscussed [[Project Alpha]]...", - "folder": "work", - "source_message_id": "uuid|null", - "created_at": "...", - "updated_at": "..." -} -``` - -`source_message_id` links a note back to the chat message that -created it (provenance for AI-generated notes). - -`noteListItem` is the same but with `content` truncated or omitted. - -### 6.2 Search - -**Full-text search:** - -``` -GET /notes/search?q=project+alpha -``` - -Uses `plainto_tsquery` (Postgres) or `LIKE` fallback (SQLite). -Returns `{ "data": [searchResult objects] }` with match highlights. - -**Title search** (lightweight, for wikilink autocomplete): - -``` -GET /notes/search-titles?q=proj -``` - -Returns `{ "data": [{ "id", "title" }] }`. - -### 6.3 Graph - -**Full graph topology:** - -``` -GET /notes/graph -``` - -```json -{ - "nodes": [{ "id": "uuid", "title": "Note Title" }], - "edges": [{ "source": "uuid", "target": "uuid" }], - "unresolved": [{ "source": "uuid", "target_title": "Missing Note" }] -} -``` - -Edges are directed: source contains `[[target_title]]`. Unresolved -edges have a `target_title` but no matching note (dangling wikilink). -When a note with that title is created, the edge auto-resolves. - -**Backlinks** (who links to this note): - -``` -GET /notes/:id/backlinks -``` - -Returns `{ "data": [note objects] }` — all notes containing -`[[this note's title]]`. - -### 6.4 Folders - -``` -GET /notes/folders -``` - -Returns `{ "folders": ["work", "personal", "archive"] }` — distinct -folder values across all user notes. - -### 6.5 Bulk Operations - -``` -POST /notes/bulk-delete -``` - -```json -{ "ids": ["uuid-1", "uuid-2"] } -``` - ---- - -## 7. Workspaces & Git - -The **Workspace** is a virtual filesystem for the editor surface. -Each workspace has a root directory, file operations, optional Git -integration, and full-text indexing for code search. - -### 7.1 Workspace CRUD - -``` -GET /workspaces → { "data": [...] } -POST /workspaces ← { "name", "owner_type", "owner_id" } -GET /workspaces/:id → workspace object -PATCH /workspaces/:id ← partial update -DELETE /workspaces/:id -``` - -Workspace object: - -```json -{ - "id": "uuid", - "name": "my-project", - "owner_type": "user|project", - "owner_id": "uuid", - "status": "active|archived", - "storage_bytes": 1048576, - "file_count": 42, - "created_at": "...", - "updated_at": "..." -} -``` - -### 7.2 File Operations - -All paths are relative to the workspace root. - -``` -GET /workspaces/:id/files?path=/ → { "files": [entries] } -GET /workspaces/:id/files/read?path=/a.md → { "content": "...", "size": 123 } -PUT /workspaces/:id/files/write ← { "path": "/a.md", "content": "..." } -DELETE /workspaces/:id/files/delete ← { "path": "/a.md" } -POST /workspaces/:id/files/mkdir ← { "path": "/src" } -``` - -File entry: - -```json -{ - "name": "main.go", - "path": "/src/main.go", - "is_dir": false, - "size": 4096, - "modified": "..." -} -``` - -### 7.3 Archive - -**Download** (tar.gz of entire workspace): - -``` -GET /workspaces/:id/archive/download -``` - -**Upload** (restore from tar.gz): - -``` -POST /workspaces/:id/archive/upload -Content-Type: multipart/form-data -``` - -### 7.4 Reconcile, Stats, Index - -**Reconcile** (sync filesystem state with DB metadata): - -``` -POST /workspaces/:id/reconcile -``` - -**Stats:** - -``` -GET /workspaces/:id/stats -``` - -Returns `{ "file_count", "storage_bytes", "indexed_files", ... }`. - -**Index status:** - -``` -GET /workspaces/:id/index-status -``` - -Returns indexing progress for full-text search. - -### 7.5 Git Integration - -All Git operations are workspace-scoped. - -``` -POST /workspaces/:id/git/clone ← { "url", "branch", "credential_id" } -POST /workspaces/:id/git/pull -POST /workspaces/:id/git/push -GET /workspaces/:id/git/status → { "files": [{ "path", "status" }] } -GET /workspaces/:id/git/diff → { "diff": "..." } -POST /workspaces/:id/git/commit ← { "message", "files": [...] } -GET /workspaces/:id/git/log → { "commits": [...] } -GET /workspaces/:id/git/branches → { "branches": [...], "current": "main" } -POST /workspaces/:id/git/checkout ← { "branch": "feature-x" } -``` - -### 7.6 Git Credentials - -User-owned, encrypted credential storage for Git operations. -Encrypted with the user's UEK (same as BYOK keys — admins cannot -recover). - -``` -POST /git-credentials ← { "name", "auth_type", "token"|"username"+"password"|"ssh_key" } -GET /git-credentials → { "data": [GitCredentialSummary] } -DELETE /git-credentials/:id -``` - -`auth_type`: `https_pat`, `https_basic`, or `ssh_key`. - -Summary response (never exposes encrypted data): - -```json -{ - "id": "uuid", - "name": "GitHub PAT", - "auth_type": "https_pat", - "created_at": "..." -} -``` - ---- - -## 8. Projects - -**Projects** are organizational containers that group channels, -knowledge bases, notes, and files. They follow the scope model -(personal, team, global). - -### 8.1 Project CRUD - -``` -GET /projects → { "data": [...] } -POST /projects ← { "name", "description", "scope", "team_id", "persona_id", "system_prompt" } -GET /projects/:id → project object -PUT /projects/:id ← partial update -DELETE /projects/:id -``` - -Project object: - -```json -{ - "id": "uuid", - "name": "Q3 Research", - "description": "...", - "scope": "personal|team|global", - "owner_id": "uuid", - "team_id": "uuid|null", - "persona_id": "uuid|null", - "system_prompt": "...|null", - "is_archived": false, - "channel_count": 5, - "created_at": "...", - "updated_at": "..." -} -``` - -### 8.2 Channel Association - -``` -GET /projects/:id/channels → { "data": [channel objects] } -POST /projects/:id/channels ← { "channel_id": "uuid" } -DELETE /projects/:id/channels/:channelId -PUT /projects/:id/channels/reorder ← { "channel_ids": ["uuid1", "uuid2"] } -``` - -A channel can only belong to one project. `POST` performs an atomic -move if the channel is already in a different project. - -### 8.3 KB Association - -``` -GET /projects/:id/knowledge-bases → { "data": [KB objects] } -POST /projects/:id/knowledge-bases ← { "kb_id": "uuid" } -DELETE /projects/:id/knowledge-bases/:kbId -``` - -KBs bound to a project are automatically available to every channel -in that project (see §5.7 resolution chain). - -### 8.4 Note Association - -``` -GET /projects/:id/notes → { "data": [note objects] } -POST /projects/:id/notes ← { "note_id": "uuid" } -DELETE /projects/:id/notes/:noteId -``` - -### 8.5 Project Files - -``` -GET /projects/:id/files → { "files": [...] } -POST /projects/:id/files ← multipart/form-data -``` - -Project-level file uploads (distinct from workspace files and channel -attachments). - -### 8.6 Admin Project Management - -``` -GET /admin/projects → { "data": [...] } -DELETE /admin/projects/:id -``` - -Cross-instance visibility for platform admins. BYOK personal projects -remain private (admin can delete but not read content). - ---- - -## 9. Memory - -**Long-term memory** extracted from conversations. The system -identifies facts, preferences, and context about the user and stores -them for injection into future conversations. - -### 9.1 User Memory - -``` -GET /memories → { "data": [memory objects] } -GET /memories/count → { "count": 42 } -PUT /memories/:id ← { "content": "updated text" } -DELETE /memories/:id -POST /memories/:id/approve → approve a pending memory -POST /memories/:id/reject → reject a pending memory -``` - -Memory object: - -```json -{ - "id": "uuid", - "user_id": "uuid", - "persona_id": "uuid|null", - "scope": "user|persona", - "content": "User prefers Go for backend work", - "source_channel_id": "uuid", - "status": "pending|approved|rejected", - "confidence": 0.85, - "created_at": "...", - "updated_at": "..." -} -``` - -`scope`: `user` memories apply across all conversations. `persona` -memories are specific to interactions with that Persona. - -### 9.2 Admin Memory Review - -``` -GET /admin/memories/pending → { "data": [memory objects with user info] } -POST /admin/memories/bulk-approve ← { "ids": ["uuid1", "uuid2"] } -``` - -Platform admin can review and bulk-approve pending memories across -all users. - ---- - -## 10. Providers & Routing - -The **multi-provider** system. One or more LLM providers are configured, -each with their own API keys, endpoints, and model catalogs. The routing -layer decides which provider handles each request. - -### 10.1 User BYOK Provider Configs - -Gated by `allow_user_byok` policy. Personal API keys are encrypted with -the user's UEK (per-user encryption key, Argon2id-derived). Platform -admins cannot recover personal keys. - -``` -GET /api-configs → { "configs": [safeConfig objects] } -POST /api-configs ← { "name", "provider", "endpoint", "api_key", ... } -GET /api-configs/:id → safeConfig -PUT /api-configs/:id ← partial update (api_key optional) -DELETE /api-configs/:id -``` - -`safeConfig` — API keys are **never** returned: - -```json -{ - "id": "uuid", - "name": "My OpenAI", - "provider": "openai", - "endpoint": "https://api.openai.com/v1", - "model_default": "gpt-4o", - "scope": "personal", - "owner_id": "uuid", - "is_active": true, - "has_key": true, - "config": {}, - "headers": {}, - "settings": {}, - "created_at": "..." -} -``` - -**List models for a user config:** - -``` -GET /api-configs/:id/models -``` - -Returns `{ "models": [catalog entries] }`. - -**Fetch/sync models from provider API:** - -``` -POST /api-configs/:id/models/fetch -``` - -Calls the provider's model list API, upserts into the local catalog. - -### 10.2 Admin Global Provider Configs - -``` -GET /admin/configs → { "configs": [configWithKey objects] } -POST /admin/configs ← { "name", "provider", "endpoint", "api_key", "config", "headers", "settings", "is_private" } -PUT /admin/configs/:id ← partial update -DELETE /admin/configs/:id -``` - -`configWithKey` is the same as `safeConfig` but comes from -`ListGlobal` — still redacts API keys, just adds the `has_key` flag. - -Admin configs use the `ENCRYPTION_KEY` env var (not per-user UEK). -`is_private`: when true, the config is available for admin-created -Personas but not directly selectable by users. - -### 10.3 Model Catalog (Admin) - -The catalog is populated by fetching from provider APIs and stores -model metadata (capabilities, context window, pricing). - -``` -GET /admin/models → { "models": [catalog entries] } -PUT /admin/models/:id ← { "visibility", "display_name", ... } -PUT /admin/models/bulk ← { "provider_config_id", "visibility" } -DELETE /admin/models/:id -POST /admin/models/fetch ← { "provider_config_id": "uuid|empty" } -``` - -**Fetch** with empty `provider_config_id` syncs ALL active global -providers. Returns: - -```json -{ - "message": "models synced", - "added": 5, - "updated": 12, - "total": 47 -} -``` - -Or for multi-provider fetch: `{ "added", "updated", "total", "errors": [...] }`. - -**Bulk visibility** sets all models for a provider (or all models -globally if no `provider_config_id`) to the specified visibility -(`enabled`, `disabled`, `team`). - -### 10.4 Provider Health - -Real-time health tracking per provider config. The health accumulator -records success/failure/latency on every completion, flushes to DB -every 60 seconds. - -**Get all:** - -``` -GET /admin/providers/health -``` - -Returns `{ "data": [ProviderHealth objects] }`: - -```json -{ - "provider_config_id": "uuid", - "provider_config_name": "OpenAI Production", - "status": "healthy|degraded|down", - "error_rate": 0.02, - "avg_latency_ms": 450, - "timeout_rate": 0.01, - "rate_limit_count": 3, - "last_check": "...", - "last_error": "...|null" -} -``` - -**Get single:** - -``` -GET /admin/providers/:id/health -``` - -**Auto-disable:** After `PROVIDER_AUTO_DISABLE_THRESHOLD` consecutive -"down" windows (default: 3), the provider is automatically deactivated. - -### 10.5 Capability Overrides - -Admin can override any model capability detected by the catalog or -heuristic layer. - -``` -GET /admin/capability-overrides → { "data": [...] } -GET /admin/models/:id/capabilities → capabilities for one model -PUT /admin/models/:id/capabilities ← { "field": "value" } -DELETE /admin/models/:id/capabilities/:overrideId -``` - -Override fields: `supports_vision`, `supports_tools`, `supports_thinking`, -`context_window`, `max_output_tokens`, etc. - -### 10.6 Routing Policies - -Policy-based request routing. Evaluated after model/config resolution, -before provider dispatch. - -**Admin CRUD:** - -``` -GET /admin/routing/policies → { "data": [...] } -GET /admin/routing/policies/:id → policy object -POST /admin/routing/policies ← { "name", "scope", "team_id", "priority", "policy_type", "config", "is_active" } -PUT /admin/routing/policies/:id -DELETE /admin/routing/policies/:id -``` - -Policy object: - -```json -{ - "id": "uuid", - "name": "Prefer Anthropic", - "scope": "global|team", - "team_id": "uuid|null", - "priority": 10, - "policy_type": "provider_prefer|team_route|cost_limit|model_alias", - "config": {}, - "is_active": true -} -``` - -| Policy type | Config | Behavior | -|------------|--------|----------| -| `provider_prefer` | `{ "providers": ["cfg-1", "cfg-2"] }` | Ordered fallback list | -| `team_route` | `{ "providers": ["cfg-1"] }` | Restrict team to specific providers | -| `cost_limit` | `{ "max_cost_per_request": 0.50 }` | Heuristic cost cap | -| `model_alias` | `{ "alias": "fast", "target_model": "...", "target_config": "..." }` | Alias → provider+model rewrite | - -**Dry-run test:** - -``` -POST /admin/routing/test -``` - -```json -{ - "model": "claude-sonnet-4-20250514", - "user_id": "uuid", - "team_id": "uuid|null" -} -``` - -Returns the ranked candidate list with health status for each. - -### 10.7 Provider Types - -Registry of supported provider types with metadata. - -``` -GET /admin/provider-types -``` - -Returns `{ "types": [...] }` with name, display name, default endpoint, -profile schema, and supported features per type. - ---- - -## 11. Models & Preferences - -What models are available to the current user and how they control -visibility. - -### 11.1 Enabled Models - -The primary endpoint for populating model selectors: - -``` -GET /models/enabled -``` - -Returns `{ "models": [UserModel objects] }`: - -```json -{ - "id": "composite-id", - "model_id": "claude-sonnet-4-20250514", - "provider_config_id": "uuid", - "provider_config_name": "Anthropic", - "provider": "anthropic", - "display_name": "Claude Sonnet 4", - "model_type": "chat|embedding|image", - "context_window": 200000, - "max_output_tokens": 8192, - "supports_vision": true, - "supports_tools": true, - "supports_thinking": true, - "supports_streaming": true, - "input_price_per_m": 3.00, - "output_price_per_m": 15.00, - "provider_status": "healthy|degraded|down|null", - "scope": "global|team|personal", - "source": "catalog|heuristic", - "is_persona": false, - "persona_id": "uuid|null", - "persona_scope": "global|team|personal|null", - "persona_avatar": "url|null", - "persona_team_name": "string|null" -} -``` - -The capabilities on each model are resolved through the three-tier -chain: catalog DB → heuristic inference → admin overrides (see §10.5). - -When `is_persona` is `true`, the capability fields (`context_window`, -`max_output_tokens`, `supports_*`, pricing, `provider_status`) are -inherited from the persona's underlying model. The frontend renders -the same capability pills and badges for a persona as for its raw -model — the persona adds identity, not capability restrictions. - -### 11.2 User Model Preferences - -Users can hide models they don't want to see and set per-model -defaults. Preferences are keyed on the **composite identity** -`provider_config_id:model_id` — the same model from different -providers can have independent visibility. - -``` -GET /models/preferences → { "preferences": [PreferenceEntry objects] } -PUT /models/preferences ← { "model_id": "...", "provider_config_id": "uuid", "hidden": true } -POST /models/preferences/bulk ← { "entries": [{ "model_id": "...", "provider_config_id": "uuid", "hidden": true }] } -``` - -PreferenceEntry: - -```json -{ - "model_id": "grok-4.1-fast", - "provider_config_id": "uuid", - "hidden": true, - "preferred_temperature": 0.7, - "preferred_max_tokens": 4096, - "sort_order": 0 -} -``` - -**Identity rule:** `provider_config_id` is required on write. The same -bare `model_id` from two different provider configs (e.g. global Venice -vs personal BYOK Venice) are independent preference entries. The -frontend composite ID format is `{provider_config_id}:{model_id}`. - -**Persona preferences:** Personas are not in this table. A persona's -visibility is controlled by the grant system (§15.3) and the -`is_active` flag, not by model preferences. - ---- - -## 12. Notifications - -Real-time notification infrastructure with WebSocket delivery and -optional email transport. - -### 12.1 CRUD - -``` -GET /notifications → paginated list -GET /notifications/unread-count → { "count": 5 } -PATCH /notifications/:id/read → mark one as read -POST /notifications/mark-all-read → mark all as read -DELETE /notifications/:id -``` - -Notification object: - -```json -{ - "id": "uuid", - "user_id": "uuid", - "type": "role.fallback|memory.extracted|system.announcement|...", - "title": "Role Fallback Triggered", - "body": "Primary model unavailable, using fallback", - "metadata": {}, - "read": false, - "created_at": "..." -} -``` - -### 12.2 Preferences - -Users control per-type delivery: - -``` -GET /notifications/preferences → { "preferences": [...] } -PUT /notifications/preferences/:type ← { "in_app": true, "email": false } -DELETE /notifications/preferences/:type → reset to default -``` - -Resolution: specific type pref → user wildcard `*` pref → system -default (`in_app=true`, `email=false`). - ---- - -## 13. Extensions - -Plugin system with three tiers: Browser JS (client-side), Starlark -sandbox (server-side, future), Sidecar containers (server-side, future). -Currently only Browser tier is implemented. - -### 13.1 User Extensions - -``` -GET /extensions → { "extensions": [...] } -POST /extensions/:id/settings ← { "enabled": true, "config": {...} } -GET /extensions/:id/manifest → full manifest.json -GET /extensions/tools → { "tools": [tool schema objects] } -``` - -### 13.2 Admin Extension Management - -``` -GET /admin/extensions → { "extensions": [...] } -POST /admin/extensions ← { manifest + script content } -PUT /admin/extensions/:id ← updated manifest/script -DELETE /admin/extensions/:id -``` - -### 13.3 Asset Serving - -``` -GET /extensions/:id/assets/*path -``` - -Public (no auth required). Serves static assets (icons, CSS, JS) -from extension bundles. - ---- - -## 14. User Profile & Settings - -### 14.1 Profile - -``` -GET /profile → profileResponse -PUT /profile ← { "display_name", "email" } -``` - -```json -{ - "id": "uuid", - "username": "jdoe", - "email": "jdoe@example.com", - "display_name": "Jane Doe", - "role": "user|admin", - "avatar_url": "...", - "created_at": "...", - "last_login": "..." -} -``` - -### 14.2 Avatar - -``` -POST /profile/avatar ← multipart/form-data (field: "avatar") -DELETE /profile/avatar -``` - -### 14.3 Password - -``` -POST /profile/password -``` - -```json -{ - "current_password": "...", - "new_password": "..." -} -``` - -On password change, the UEK is re-wrapped with the new password -(BYOK keys remain accessible without re-encryption). - -### 14.4 App Settings - -``` -GET /settings → { "settings": {...} } -PUT /settings ← { "theme", "editor_keybindings", ... } -``` - -User-level preferences (theme, keybindings, default model, etc.). - ---- - -## 15. Teams & Access Control - -### 15.1 My Teams - -``` -GET /teams/mine → { "teams": [...] } -``` - -Returns teams the current user is a member of. - -### 15.2 Team Administration - -Team-admin-scoped routes (require `RequireTeamAdmin` middleware): - -**Team CRUD (platform admin):** - -``` -GET /admin/teams → { "teams": [...] } -POST /admin/teams -GET /admin/teams/:id -PUT /admin/teams/:id -DELETE /admin/teams/:id -``` - -**Members:** - -``` -GET /admin/teams/:id/members → { "members": [...] } -POST /admin/teams/:id/members ← { "user_id", "role" } -PUT /admin/teams/:id/members/:memberId ← { "role" } -DELETE /admin/teams/:id/members/:memberId -``` - -**Team-scoped routes** (team admin, not platform admin): - -``` -GET /teams/:teamId/members -POST /teams/:teamId/members ← { "user_id", "role" } -PUT /teams/:teamId/members/:memberId -DELETE /teams/:teamId/members/:memberId -``` - -**Team Providers:** - -``` -GET /teams/:teamId/providers → { "configs": [...] } -POST /teams/:teamId/providers ← { "name", "provider", "endpoint", "api_key", ... } -PUT /teams/:teamId/providers/:id -DELETE /teams/:teamId/providers/:id -GET /teams/:teamId/providers/:id/models → { "models": [...] } -``` - -**Team Models:** - -``` -GET /teams/:teamId/models → { "models": [...] } -``` - -Available models for this team (global + team-scoped). - -**Team Personas:** See §4.2. - -**Team Roles:** - -``` -GET /teams/:teamId/roles → { "roles": [...] } -PUT /teams/:teamId/roles/:role ← { "permissions": {...} } -DELETE /teams/:teamId/roles/:role -``` - -**Team Audit:** - -``` -GET /teams/:teamId/audit → paginated audit log -GET /teams/:teamId/audit/actions → { "actions": [...] } (distinct action types) -``` - -**Team Usage:** - -``` -GET /teams/:teamId/usage → { "totals": {...}, "results": [...] } -``` - -### 15.3 Groups & Resource Grants - -Groups are ACL containers that decouple access from team membership. -A resource grant controls who can see a Persona or KB beyond its base -scope. - -**My Groups:** - -``` -GET /groups/mine → { "groups": [...] } -``` - -**Admin Group CRUD:** - -``` -GET /admin/groups → { "groups": [...] } -POST /admin/groups ← { "name", "scope", "team_id" } -GET /admin/groups/:id -PUT /admin/groups/:id -DELETE /admin/groups/:id -``` - -**Group Members:** - -``` -GET /admin/groups/:id/members → { "members": [...] } -POST /admin/groups/:id/members ← { "user_id" } -DELETE /admin/groups/:id/members/:userId -``` - -**Resource Grants:** - -``` -GET /admin/grants/:type/:id → { "grant": {...} } -PUT /admin/grants/:type/:id ← { "grant_type": "team_only|global|groups", "group_ids": [...] } -DELETE /admin/grants/:type/:id -``` - -`:type` is `persona` or `kb`. `:id` is the resource ID. - -Grant types: - -| `grant_type` | Visibility | -|--------------|-----------| -| `team_only` | Only the owning team | -| `global` | All authenticated users | -| `groups` | Members of specified groups | - ---- - -## 16. Platform Administration - -Cross-cutting admin operations that don't belong to a specific domain. - -### 16.1 User Management - -``` -GET /admin/users → { "users": [...] } -POST /admin/users ← { "username", "password", "email", "role" } -PUT /admin/users/:id/role ← { "role": "admin|user" } -PUT /admin/users/:id/active ← { "active": false } -DELETE /admin/users/:id -POST /admin/users/:id/reset-password ← { "new_password": "..." } -POST /admin/users/:id/vault/reset → destroys user's UEK (BYOK keys lost) -``` - -### 16.2 Global Settings & Policies - -Settings are key-value pairs in `global_settings`. Policies are -boolean flags that gate features. - -``` -GET /admin/settings → all settings -GET /admin/settings/:key → single setting -PUT /admin/settings/:key ← { "value": ... } -``` - -The handler auto-detects: if the value is a string and the key is a -known policy name, it writes to the `policies` table. Otherwise it -writes to `global_config` as JSON. - -**Public settings** (non-admin, for FE bootstrapping): - -``` -GET /settings/public -``` - -```json -{ - "banner": { "enabled": true, "text": "DEVELOPMENT", "bg": "#007a33", "fg": "#ffffff", "position": "both" }, - "branding": { "instance_name": "Switchboard", "logo_url": "...", "tagline": "..." }, - "has_admin_prompt": true, - "storage_configured": true, - "paste_to_file_chars": 2000, - "policies": { - "allow_registration": "true", - "allow_user_byok": "true", - "allow_user_personas": "true" - } -} -``` - -### 16.3 Banner Configuration - -The environment banner is stored as a single `global_config` entry -under the key `"banner"`. It's a simple object: - -```json -{ - "enabled": true, - "text": "DEVELOPMENT", - "position": "both|top|bottom", - "bg": "#007a33", - "fg": "#ffffff" -} -``` - -Set via `PUT /admin/settings/banner` with `{ "value": { ... } }`. -The admin UI provides a color picker, position selector, and preset -dropdown. The Go template base layout reads the banner from -`PageData` and renders top/bottom strips with CSS custom properties. - -### 16.4 Platform Stats - -``` -GET /admin/stats -``` - -```json -{ - "users": 42, - "channels": 156, - "messages": 12847 -} -``` - -### 16.5 Storage & Vault - -**Storage status:** - -``` -GET /admin/storage/status → { "backend", "healthy", "file_count", "total_bytes", ... } -GET /admin/storage/orphans → { "count": 3 } -POST /admin/storage/cleanup → removes orphaned blobs -GET /admin/storage/extraction → extraction queue status -``` - -**Vault:** - -``` -GET /admin/vault/status → { "encryption_key_set", "user_vaults_count", ... } -``` - -### 16.6 Audit Log - -``` -GET /admin/audit?page=1&per_page=50&action=user.create&actor_id=uuid -GET /admin/audit/actions → { "actions": ["user.create", "policy.update", ...] } -``` - -Audit entry: - -```json -{ - "id": "uuid", - "actor_id": "uuid", - "action": "user.create", - "resource_type": "user", - "resource_id": "uuid", - "metadata": {}, - "created_at": "..." -} -``` - -### 16.7 Usage & Pricing - -``` -GET /admin/usage → { "totals", "results" } -GET /admin/usage/teams/:id → team-specific usage -GET /admin/usage/users/:id → user-specific usage -GET /admin/pricing → { "data": [...] } -PUT /admin/pricing ← { "provider", "model", "input_per_m", "output_per_m" } -DELETE /admin/pricing/:provider/:model -``` - -**Personal usage** (non-admin): - -``` -GET /usage → { "totals", "results" } -``` - -Scoped to the user's BYOK provider usage only. - -### 16.8 Roles (Platform) - -``` -GET /admin/roles → { "roles": [...] } -GET /admin/roles/:role -PUT /admin/roles/:role ← { "permissions": {...} } -POST /admin/roles/:role/test ← test role permissions -``` - -### 16.9 Email Test - -``` -POST /admin/notifications/test-email -``` - -```json -{ "to": "admin@example.com" } -``` - -Sends a test email using the configured SMTP settings. - ---- - -## 17. Export & Utility - -### 17.1 Health Check - -``` -GET /health → { "status": "ok" } -``` - -Public, no auth. Also available at `/api/v1/health`. - -### 17.2 Export - -Convert markdown to PDF or DOCX via pandoc: - -``` -POST /export -``` - -```json -{ - "content": "# My Document\n\nBody text...", - "format": "pdf|docx", - "filename": "my-document" -} -``` - -Returns the binary file with appropriate Content-Type. Requires -`pandoc` in the container (available in the unified and backend -Docker images). - ---- - -## 17b. Files & Storage - -All binary content in Chat Switchboard flows through a single -**ObjectStore** abstraction backed by PVC (filesystem) or S3. All -file metadata lives in one `files` table. The old `attachments` -table is dropped. - -### 17b.1 Storage Backend - -| Operation | Description | -|-----------|-------------| -| `Put(key, reader, size, contentType)` | Store a blob | -| `Get(key)` → reader, size, contentType | Retrieve a blob | -| `Delete(key)` | Remove a blob | -| `DeletePrefix(prefix)` | Bulk remove (channel deletion) | -| `Exists(key)` | Check without reading | -| `Healthy()` | Backend health check | -| `Stats()` | Aggregate metrics | - -Backends: `pvc` (`STORAGE_PATH=/data/storage`) or `s3` -(`S3_ENDPOINT`, `S3_BUCKET`, `S3_ACCESS_KEY`, `S3_SECRET_KEY`). - -### 17b.2 Key Namespacing - -| Prefix | Pattern | What | -|--------|---------|------| -| `attachments/` | `attachments/{channel_id}/{file_id}_{filename}` | User uploads + AI outputs (legacy prefix retained for existing blobs) | -| `kb/` | `kb/{kb_id}/{doc_id}_{filename}` | Knowledge base documents | -| `exports/` | `exports/{user_id}/{export_id}_{filename}` | Chat/data exports | -| `workspace/` | `workspace/{workspace_id}/{path}` | Workspace files | - -### 17b.3 File Object - -```sql -CREATE TABLE files ( - id UUID PRIMARY KEY, - channel_id UUID REFERENCES channels(id) ON DELETE CASCADE, - message_id UUID REFERENCES messages(id) ON DELETE SET NULL, - user_id UUID REFERENCES users(id) ON DELETE SET NULL, - project_id UUID REFERENCES projects(id) ON DELETE SET NULL, - origin TEXT NOT NULL DEFAULT 'user_upload' - CHECK (origin IN ('user_upload','tool_output','system')), - filename TEXT NOT NULL, - content_type TEXT NOT NULL DEFAULT 'application/octet-stream', - size_bytes BIGINT NOT NULL DEFAULT 0, - storage_key TEXT NOT NULL, - display_hint TEXT NOT NULL DEFAULT 'download' - CHECK (display_hint IN ('inline','download','thumbnail')), - extracted_text TEXT, - metadata JSONB DEFAULT '{}', - created_at TIMESTAMPTZ DEFAULT NOW(), - updated_at TIMESTAMPTZ DEFAULT NOW() -); -``` - -**Origin:** - -| Value | Who creates | Examples | -|-------|-------------|---------| -| `user_upload` | User via upload UI | PDF, image, doc attached to message | -| `tool_output` | Tool execution during completion | DALL-E image, video gen, code artifact | -| `system` | Platform | Export, avatar stored as file | - -**Display hint:** - -| Value | Frontend behavior | -|-------|-------------------| -| `inline` | Render in message stream (images, video, audio, HTML) | -| `download` | Filename + size + download button | -| `thumbnail` | Thumbnail preview, click to expand | - -### 17b.4 Tool Output Flow - -1. Tool executes (image gen, video render, etc.) -2. Tool writes blob via `ObjectStore.Put` -3. Tool creates file record: `POST /files` with `origin: "tool_output"`, `message_id` = current assistant message -4. Tool result includes `file_id` reference -5. WebSocket emits `file.created` event: - -```json -{ - "event": "file.created", - "channel_id": "uuid", - "message_id": "uuid", - "file": { ...file object... } -} -``` - -6. Frontend renders inline based on `display_hint` + `content_type` - -The event fires during streaming so the user sees the artifact -before the completion finishes. - -### 17b.5 Content Type → Display Hint Defaults - -| Content Type | Default Hint | Rendering | -|-------------|-------------|-----------| -| `image/*` | `inline` | `` in message, lightbox on click | -| `video/*` | `inline` | `