2226 lines
52 KiB
Markdown
2226 lines
52 KiB
Markdown
# ICD-API — Chat Switchboard Backend API Contract
|
|
|
|
**Version:** 0.22.7
|
|
**Updated:** 2026-03-03
|
|
**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)
|
|
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 `<meta>` 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).
|
|
|
|
### Naming Note
|
|
|
|
The domain concept is **Persona** (identity + system prompt + model +
|
|
KB bindings + grant scoping). API routes currently use `/personas` for
|
|
historical reasons. A pre-1.0 rename to `/personas` is planned. This
|
|
document uses "Persona" for the concept and shows the current route
|
|
paths as-is.
|
|
|
|
---
|
|
|
|
## 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.
|
|
Today channels are single-user direct chats; the architecture
|
|
anticipates multi-participant channels for v0.23.0.
|
|
|
|
### 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",
|
|
"user_id": "uuid",
|
|
"title": "My Chat",
|
|
"type": "direct",
|
|
"description": "",
|
|
"model": "claude-sonnet-4-20250514",
|
|
"provider_config_id": "uuid|null",
|
|
"system_prompt": "",
|
|
"project_id": "uuid|null",
|
|
"folder": "string|null",
|
|
"tags": ["tag1", "tag2"],
|
|
"created_at": "...",
|
|
"updated_at": "..."
|
|
}
|
|
```
|
|
|
|
**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, 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",
|
|
"thinking": "...|null",
|
|
"tool_calls": [...],
|
|
"tool_results": [...],
|
|
"attachments": [...],
|
|
"has_siblings": true,
|
|
"sibling_index": 0,
|
|
"sibling_count": 2,
|
|
"created_at": "..."
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
**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. `chat_id` is accepted as a deprecated alias
|
|
(maps to `channel_id`; frontend no longer sends it — removal planned).
|
|
|
|
`extra_body` is a freeform JSON object merged into the provider request
|
|
(provider-specific parameters like `temperature`, `reasoning`, etc.).
|
|
|
|
**Persona resolution:** If `persona_id` is set, the handler loads the
|
|
persona's system prompt, model, provider_config_id, and KB bindings.
|
|
Per-message `model`/`provider_config_id` override the persona's defaults.
|
|
|
|
**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
|
|
|
|
Channels can have multiple AI models. The `channel_models` table tracks
|
|
the roster. `@mention` in message content routes to specific models.
|
|
|
|
```
|
|
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,
|
|
"created_at": "..."
|
|
}
|
|
```
|
|
|
|
### 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 Attachments
|
|
|
|
File attachments are channel-scoped. Upload stores the file in blob
|
|
storage; a background extraction pipeline processes documents (PDF,
|
|
DOCX, XLSX, etc.) into searchable text.
|
|
|
|
**Upload:**
|
|
|
|
```
|
|
POST /channels/:id/attachments
|
|
Content-Type: multipart/form-data
|
|
```
|
|
|
|
Field: `file`. Returns the attachment object.
|
|
|
|
**List by channel:**
|
|
|
|
```
|
|
GET /channels/:id/attachments
|
|
```
|
|
|
|
Returns `{ "attachments": [...] }`.
|
|
|
|
**Get metadata:**
|
|
|
|
```
|
|
GET /attachments/:id
|
|
```
|
|
|
|
```json
|
|
{
|
|
"id": "uuid",
|
|
"channel_id": "uuid",
|
|
"user_id": "uuid",
|
|
"filename": "report.pdf",
|
|
"content_type": "application/pdf",
|
|
"size": 1048576,
|
|
"storage_key": "attachments/chan-id/att-id_report.pdf",
|
|
"extracted_text_key": "...|null",
|
|
"metadata": {},
|
|
"created_at": "..."
|
|
}
|
|
```
|
|
|
|
**Download:**
|
|
|
|
```
|
|
GET /attachments/:id/download
|
|
```
|
|
|
|
Returns the file with appropriate `Content-Type` and
|
|
`Content-Disposition` headers.
|
|
|
|
**Delete:**
|
|
|
|
```
|
|
DELETE /attachments/:id
|
|
```
|
|
|
|
---
|
|
|
|
## 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.
|
|
|
|
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,
|
|
"avatar_url": "/api/v1/personas/uuid/avatar?v=123",
|
|
"created_at": "...",
|
|
"updated_at": "..."
|
|
}
|
|
```
|
|
|
|
### 4.1 Personal Personas
|
|
|
|
Gated by `allow_user_personas` policy.
|
|
|
|
```
|
|
GET /personas → { "personas": [...], "personas": [...] }
|
|
POST /personas ← { "name", "description", "system_prompt", "model", ... }
|
|
PUT /personas/:id ← partial update
|
|
DELETE /personas/:id
|
|
```
|
|
|
|
**Note:** List response currently sends both `"personas"` and `"personas"`
|
|
keys (identical arrays) for backward compatibility. The `"personas"` key
|
|
is what the frontend reads. Dual keys will be collapsed pre-1.0.
|
|
|
|
### 4.2 Team Personas
|
|
|
|
```
|
|
GET /teams/:teamId/personas → { "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": [...], "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
|
|
```
|
|
|
|
> **Note:** `GET /models` exists as an alias. The alias is unused by
|
|
> the frontend and will be removed pre-1.0.
|
|
|
|
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"
|
|
}
|
|
```
|
|
|
|
The capabilities on each model are resolved through the three-tier
|
|
chain: catalog DB → heuristic inference → admin overrides (see §10.5).
|
|
|
|
### 11.2 User Model Preferences
|
|
|
|
Users can hide models they don't want to see.
|
|
|
|
```
|
|
GET /models/preferences → { "preferences": { "model-id": { "hidden": true } } }
|
|
PUT /models/preferences ← { "model_id": "...", "hidden": true }
|
|
POST /models/preferences/bulk ← { "preferences": { "id1": { "hidden": true }, "id2": { ... } } }
|
|
```
|
|
|
|
---
|
|
|
|
## 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).
|
|
|
|
---
|
|
|
|
## 18. WebSocket Protocol
|
|
|
|
Real-time bidirectional event bus. Single connection per client.
|
|
|
|
### 18.1 Connection
|
|
|
|
```
|
|
ws://{host}{BASE_PATH}/ws?token={access_token}
|
|
```
|
|
|
|
**Lifecycle:**
|
|
- Server sends `ping` every 54 seconds
|
|
- Client must respond with `pong` within 60 seconds or disconnection
|
|
- Max message size: 4 KB
|
|
- Write deadline: 10 seconds
|
|
|
|
### 18.2 Event Envelope
|
|
|
|
All messages are JSON:
|
|
|
|
```json
|
|
{
|
|
"type": "event.type",
|
|
"payload": { ... }
|
|
}
|
|
```
|
|
|
|
### 18.3 Room Subscription
|
|
|
|
Clients subscribe to rooms for scoped event delivery:
|
|
|
|
```json
|
|
{ "type": "subscribe", "payload": { "room": "channel:uuid" } }
|
|
{ "type": "unsubscribe", "payload": { "room": "channel:uuid" } }
|
|
```
|
|
|
|
### 18.4 Event Routing Table
|
|
|
|
| Prefix | Direction | Description |
|
|
|--------|-----------|-------------|
|
|
| `channel.created` | → client | New channel created |
|
|
| `channel.updated` | → client | Channel metadata changed |
|
|
| `channel.deleted` | → client | Channel removed |
|
|
| `message.created` | → client | New message in subscribed channel |
|
|
| `message.updated` | → client | Message content changed |
|
|
| `message.deleted` | → client | Message removed |
|
|
| `cursor.updated` | → client | Branch cursor moved |
|
|
| `typing.start` | ↔ both | User/AI started typing |
|
|
| `typing.stop` | ↔ both | Typing ended |
|
|
| `model.changed` | → client | Channel model roster changed |
|
|
| `persona.updated` | → client | Persona metadata changed |
|
|
| `kb.updated` | → client | Knowledge base changed |
|
|
| `kb.document.status` | → client | Document processing status update |
|
|
| `note.created` | → client | Note created |
|
|
| `note.updated` | → client | Note content changed |
|
|
| `note.deleted` | → client | Note removed |
|
|
| `notification.new` | → client | New notification |
|
|
| `notification.read` | → client | Notification marked read |
|
|
| `memory.extracted` | → client | New memory extracted |
|
|
| `memory.status` | → client | Memory approved/rejected |
|
|
| `role.fallback` | → client | Role fallback triggered |
|
|
| `workspace.updated` | → client | Workspace state changed |
|
|
| `project.updated` | → client | Project metadata changed |
|
|
| `extension.updated` | → client | Extension config changed |
|
|
| `settings.updated` | → client | Global settings changed |
|
|
| `user.updated` | → client | User profile changed |
|
|
|
|
Direction: `→ client` = server-to-client only, `← client` = client-to-server
|
|
only, `↔ both` = bidirectional.
|
|
|
|
---
|
|
|
|
## 19. Appendix: Enums
|
|
|
|
### Channel Types
|
|
|
|
`direct` (current default), `group` (v0.23.0), `workflow` (v0.25.0)
|
|
|
|
### Message Roles
|
|
|
|
`user`, `assistant`, `system`, `tool`
|
|
|
|
### Scopes
|
|
|
|
`personal`, `team`, `global`
|
|
|
|
### Visibility (model catalog)
|
|
|
|
`enabled`, `disabled`, `team`
|
|
|
|
### User Roles
|
|
|
|
`admin`, `user`
|
|
|
|
### Team Member Roles
|
|
|
|
`admin`, `member`
|
|
|
|
### Provider Types
|
|
|
|
`openai`, `anthropic`, `openrouter`, `venice` (extensible via registry)
|
|
|
|
### Model Types
|
|
|
|
`chat`, `embedding`, `image`
|
|
|
|
### Memory Scopes
|
|
|
|
`user`, `persona`
|
|
|
|
### Memory Statuses
|
|
|
|
`pending`, `approved`, `rejected`
|
|
|
|
### Workspace Owner Types
|
|
|
|
`user`, `project`
|
|
|
|
### Workspace Statuses
|
|
|
|
`active`, `archived`
|
|
|
|
### Index Statuses
|
|
|
|
`pending`, `indexing`, `ready`, `error`
|
|
|
|
### KB Document Statuses
|
|
|
|
`pending`, `chunking`, `embedding`, `ready`, `error`
|
|
|
|
### Provider Health Statuses
|
|
|
|
`healthy`, `degraded`, `down`
|
|
|
|
### Routing Policy Types
|
|
|
|
`provider_prefer`, `team_route`, `cost_limit`, `model_alias`
|
|
|
|
### Extension Tiers
|
|
|
|
`browser` (implemented), `starlark` (future), `sidecar` (future)
|
|
|
|
### Grant Types
|
|
|
|
`team_only`, `global`, `groups`
|
|
|
|
### Resource Grant Scopes
|
|
|
|
`persona`, `kb`
|
|
|
|
### Notification Types
|
|
|
|
`role.fallback`, `memory.extracted`, `system.announcement`, plus
|
|
extensible via notification service.
|
|
|
|
### Git Auth Types
|
|
|
|
`https_pat`, `https_basic`, `ssh_key`
|
|
|
|
### Policies
|
|
|
|
| Key | Default | Description |
|
|
|-----|---------|-------------|
|
|
| `allow_registration` | `"true"` | Allow new user self-registration |
|
|
| `allow_user_byok` | `"true"` | Allow users to add personal API keys |
|
|
| `allow_user_personas` | `"true"` | Allow users to create personal Personas |
|
|
| `allow_team_providers` | `"true"` | Allow team admins to configure team providers |
|
|
| `kb_direct_access` | `"true"` | Show KB picker in channel (false = strict enterprise mode) |
|
|
| `require_email` | `"false"` | Require email on registration |
|
|
| `default_system_prompt` | `""` | Injected into all conversations (admin override) |
|
|
|
|
---
|
|
|
|
## Page Routes (Non-API)
|
|
|
|
Server-rendered Go template surfaces. Not REST endpoints — these
|
|
return HTML pages.
|
|
|
|
| Route | Surface | Description |
|
|
|-------|---------|-------------|
|
|
| `/login` | Login | Standalone login/register page |
|
|
| `/` | Chat | Main chat interface |
|
|
| `/chat/:chatID` | Chat | Chat with specific channel loaded |
|
|
| `/editor` | Editor | Workspace file editor |
|
|
| `/editor/:wsId` | Editor | Editor with specific workspace |
|
|
| `/notes` | Notes | Notes interface |
|
|
| `/notes/:noteId` | Notes | Notes with specific note loaded |
|
|
| `/admin` | Admin | Platform administration |
|
|
| `/admin/:section` | Admin | Admin with specific section |
|
|
| `/settings` | Settings | User settings |
|
|
| `/settings/:section` | Settings | Settings with specific section |
|
|
|
|
All page routes (except `/login`) require authentication via
|
|
`AuthOrRedirect` middleware (reads `sb_token` cookie). Admin routes
|
|
additionally require `RequireAdminPage()`.
|
|
|
|
**Dynamic surface routing** for admin-installed extension surfaces is
|
|
not yet implemented. The current five surfaces are hardcoded in Go
|
|
route registration. Future: extension manifests declare surface routes,
|
|
the page engine dynamically registers them with appropriate data
|
|
loaders. See EXTENSIONS.md for the design direction.
|