Changeset 0.28.0.1 (#173)
This commit is contained in:
155
docs/ICD/README.md
Normal file
155
docs/ICD/README.md
Normal file
@@ -0,0 +1,155 @@
|
||||
# ICD-API — Chat Switchboard Backend API Contract
|
||||
|
||||
**Version:** 0.28.0
|
||||
**Updated:** 2026-03-11
|
||||
**Audience:** Frontend developers, integrators, API consumers
|
||||
|
||||
> **Organization principle:** Each file is one domain — the complete story.
|
||||
> Every endpoint that touches Knowledge Bases is in `knowledge.md`, every
|
||||
> endpoint that touches Personas is in `personas.md`, etc.
|
||||
|
||||
## Files
|
||||
|
||||
| File | Domain | Endpoints |
|
||||
|------|--------|-----------|
|
||||
| [auth.md](auth.md) | Auth — login, register, refresh, OIDC, mTLS | 6 |
|
||||
| [channels.md](channels.md) | Channels, messages, completions, participants, folders, presence | ~35 |
|
||||
| [personas.md](personas.md) | Personas, persona groups, avatars, KB bindings, tool grants | ~20 |
|
||||
| [knowledge.md](knowledge.md) | Knowledge bases, documents, search, channel/persona bindings | ~12 |
|
||||
| [notes.md](notes.md) | Notes, wikilinks, graph, search, folders | ~10 |
|
||||
| [workspaces.md](workspaces.md) | Workspaces, file ops, git, credentials | ~20 |
|
||||
| [projects.md](projects.md) | Projects, channel/KB/note associations | ~14 |
|
||||
| [memory.md](memory.md) | Memory extraction, review, admin | 7 |
|
||||
| [providers.md](providers.md) | Provider configs, health, capabilities, routing policies | ~25 |
|
||||
| [models.md](models.md) | Enabled models, user preferences | 4 |
|
||||
| [notifications.md](notifications.md) | Notifications, preferences | 7 |
|
||||
| [extensions.md](extensions.md) | Extensions, user settings, admin | 8 |
|
||||
| [profile.md](profile.md) | User profile, avatar, password, app settings | 7 |
|
||||
| [teams.md](teams.md) | Teams, members, groups, permissions, grants | ~25 |
|
||||
| [admin.md](admin.md) | Platform admin — users, settings, stats, audit, usage, vault | ~25 |
|
||||
| [utility.md](utility.md) | Health, export, files, storage | ~12 |
|
||||
| [workflows.md](workflows.md) | Workflow definitions, stages, instances, assignments, entry | ~20 |
|
||||
| [tasks.md](tasks.md) | Task definitions, runs, scheduler, team tasks | ~14 |
|
||||
| [surfaces.md](surfaces.md) | Surface registry, extension surfaces | 6 |
|
||||
| [websocket.md](websocket.md) | WebSocket protocol, events, rooms | — |
|
||||
| [enums.md](enums.md) | All enum values, policies | — |
|
||||
|
||||
## 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.
|
||||
|
||||
### Auth Modes
|
||||
|
||||
| Mode | `AUTH_MODE` | How it works |
|
||||
|------|------------|--------------|
|
||||
| Builtin | `builtin` (default) | Username/password, bcrypt, JWT |
|
||||
| mTLS | `mtls` | Reverse proxy cert headers, auto-provision |
|
||||
| OIDC | `oidc` | Authorization code flow (Keycloak etc.) |
|
||||
|
||||
All three modes issue the same JWT after authentication. Downstream
|
||||
middleware is auth-mode-agnostic.
|
||||
|
||||
### Authorization Tiers
|
||||
|
||||
| Tier | Middleware | Who |
|
||||
|------|-----------|-----|
|
||||
| Public | none | Anyone (health, login, register, public settings) |
|
||||
| Session | `AuthOrSession()` | JWT user OR anonymous session cookie (workflow visitors) |
|
||||
| Authenticated | `Auth()` | Any logged-in user |
|
||||
| Permission | `RequirePermission(perm)` | User whose groups grant `perm` |
|
||||
| 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).
|
||||
|
||||
## Page Routes (Non-API)
|
||||
|
||||
Server-rendered Go template surfaces. Not REST endpoints — return HTML.
|
||||
|
||||
| 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 |
|
||||
| `/w/:id` | Workflow Landing | Branded workflow entry page |
|
||||
| `/w/:id/:slug` | Workflow Landing | Slug-suffixed workflow entry |
|
||||
| `/s/:slug` | Extension Surface | Dynamic surface from registry |
|
||||
|
||||
All page routes (except `/login`, `/w/`) require authentication via
|
||||
`AuthOrRedirect` middleware. Admin routes additionally require
|
||||
`RequireAdminPage()`. Workflow landing pages use `AuthOrSession`.
|
||||
198
docs/ICD/admin.md
Normal file
198
docs/ICD/admin.md
Normal file
@@ -0,0 +1,198 @@
|
||||
# Platform Administration
|
||||
|
||||
Cross-cutting admin operations that don't belong to a specific domain.
|
||||
|
||||
### 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)
|
||||
```
|
||||
|
||||
### 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"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 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.
|
||||
|
||||
### Platform Stats
|
||||
|
||||
```
|
||||
GET /admin/stats
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"users": 42,
|
||||
"channels": 156,
|
||||
"messages": 12847
|
||||
}
|
||||
```
|
||||
|
||||
### 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", ... }
|
||||
```
|
||||
|
||||
### 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": "..."
|
||||
}
|
||||
```
|
||||
|
||||
### 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.
|
||||
|
||||
### Roles (Platform)
|
||||
|
||||
```
|
||||
GET /admin/roles → { "roles": [...] }
|
||||
GET /admin/roles/:role
|
||||
PUT /admin/roles/:role ← { "permissions": {...} }
|
||||
POST /admin/roles/:role/test ← test role permissions
|
||||
```
|
||||
|
||||
### Email Test
|
||||
|
||||
```
|
||||
POST /admin/notifications/test-email
|
||||
```
|
||||
|
||||
```json
|
||||
{ "to": "admin@example.com" }
|
||||
```
|
||||
|
||||
Sends a test email using the configured SMTP settings.
|
||||
|
||||
---
|
||||
|
||||
### Archived Channels
|
||||
|
||||
```
|
||||
GET /admin/channels/archived → { "data": [...] }
|
||||
DELETE /admin/channels/:id/purge → permanent delete (ignores retention)
|
||||
```
|
||||
|
||||
**Auth:** Platform admin.
|
||||
|
||||
### Surfaces Admin
|
||||
|
||||
See [surfaces.md](surfaces.md) for the full surface management API.
|
||||
|
||||
```
|
||||
GET /admin/surfaces → all surfaces (including disabled)
|
||||
GET /admin/surfaces/:id → surface with manifest
|
||||
POST /admin/surfaces/install ← multipart .surface archive
|
||||
PUT /admin/surfaces/:id/enable
|
||||
PUT /admin/surfaces/:id/disable
|
||||
DELETE /admin/surfaces/:id
|
||||
```
|
||||
|
||||
### Tasks Admin
|
||||
|
||||
See [tasks.md](tasks.md) for the full task management API.
|
||||
|
||||
```
|
||||
GET /admin/tasks → all tasks across all users/teams
|
||||
POST /admin/tasks/:id/run → force run
|
||||
POST /admin/tasks/:id/kill → cancel active run
|
||||
DELETE /admin/tasks/:id → delete task
|
||||
```
|
||||
175
docs/ICD/auth.md
Normal file
175
docs/ICD/auth.md
Normal file
@@ -0,0 +1,175 @@
|
||||
# Auth
|
||||
|
||||
Three auth modes, configured via `AUTH_MODE` env var. All three issue the
|
||||
same JWT after authentication — downstream middleware is auth-mode-agnostic.
|
||||
|
||||
## Builtin Auth (default)
|
||||
|
||||
Username/password authentication with bcrypt hashing.
|
||||
|
||||
### Register
|
||||
|
||||
```
|
||||
POST /auth/register
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"username": "jdoe",
|
||||
"password": "...",
|
||||
"email": "jdoe@example.com",
|
||||
"display_name": "Jane Doe"
|
||||
}
|
||||
```
|
||||
|
||||
Gated by `allow_registration` policy. Returns same shape as Login.
|
||||
A unique `handle` is auto-generated from `username` (collision-safe
|
||||
with `-2`, `-3` suffixes).
|
||||
|
||||
### 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",
|
||||
"handle": "jdoe",
|
||||
"role": "user",
|
||||
"auth_source": "builtin",
|
||||
"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" }`.
|
||||
|
||||
## mTLS Auth
|
||||
|
||||
Mutual TLS via reverse proxy headers. The proxy terminates TLS and
|
||||
forwards cert DN fields in headers. The backend auto-provisions
|
||||
users on first connection.
|
||||
|
||||
**Configured by:** `AUTH_MODE=mtls` + `MTLS_HEADER_*` env vars.
|
||||
|
||||
No explicit login/register endpoints. User identity is extracted from
|
||||
the TLS certificate on every request. `password_hash` is NULL for
|
||||
mTLS users.
|
||||
|
||||
**Headers parsed:**
|
||||
- `MTLS_HEADER_CN` (default: `X-SSL-Client-CN`) — common name → username
|
||||
- `MTLS_HEADER_DN` (default: `X-SSL-Client-DN`) — distinguished name → metadata
|
||||
- `MTLS_HEADER_VERIFY` (default: `X-SSL-Client-Verify`) — must be `SUCCESS`
|
||||
- `MTLS_HEADER_FINGERPRINT` (default: `X-SSL-Client-Fingerprint`) — stable identity
|
||||
|
||||
On first request with a valid cert, the system creates a user with
|
||||
`auth_source=mtls` and `external_id=fingerprint`.
|
||||
|
||||
## OIDC Auth
|
||||
|
||||
OpenID Connect authorization code flow. Tested with Keycloak but
|
||||
compatible with any OIDC-compliant IdP.
|
||||
|
||||
**Configured by:** `AUTH_MODE=oidc` + `OIDC_*` env vars.
|
||||
|
||||
### OIDC Login (redirect)
|
||||
|
||||
```
|
||||
GET /auth/oidc/login
|
||||
```
|
||||
|
||||
Redirects to the IdP authorization endpoint. Stores `state` + `nonce`
|
||||
in `oidc_auth_state` table (cleaned up after callback).
|
||||
|
||||
### OIDC Callback
|
||||
|
||||
```
|
||||
GET /auth/oidc/callback?code=...&state=...
|
||||
```
|
||||
|
||||
Exchanges authorization code for tokens, validates ID token signature
|
||||
via JWKS, extracts claims. Auto-provisions user on first login with
|
||||
`auth_source=oidc` and `external_id=sub`. Returns HTML fragment that
|
||||
passes tokens to the frontend via URL fragment.
|
||||
|
||||
**Claim mapping:**
|
||||
- `sub` → `external_id`
|
||||
- `preferred_username` → `username` + `handle`
|
||||
- `email` → `email`
|
||||
- `name` → `display_name`
|
||||
- `groups` → synced to internal groups with `source=oidc`
|
||||
|
||||
**Split-horizon issuer:** `OIDC_EXTERNAL_ISSUER_URL` can differ from
|
||||
`OIDC_ISSUER_URL` when the IdP's internal address (Docker/K8s service)
|
||||
differs from its external address.
|
||||
|
||||
**OIDC env vars:**
|
||||
|
||||
| Var | Required | Description |
|
||||
|-----|----------|-------------|
|
||||
| `OIDC_ISSUER_URL` | Yes | IdP issuer URL for discovery |
|
||||
| `OIDC_EXTERNAL_ISSUER_URL` | No | External issuer (if different from internal) |
|
||||
| `OIDC_CLIENT_ID` | Yes | Client ID |
|
||||
| `OIDC_CLIENT_SECRET` | Yes | Client secret |
|
||||
| `OIDC_REDIRECT_URL` | No | Callback URL (auto-derived if not set) |
|
||||
|
||||
## Login Page Adaptation
|
||||
|
||||
The `/login` page adapts by `AUTH_MODE`:
|
||||
|
||||
| Mode | UI |
|
||||
|------|-----|
|
||||
| `builtin` | Username/password form + register link |
|
||||
| `mtls` | Certificate status display |
|
||||
| `oidc` | "Sign in with SSO" button |
|
||||
|
||||
## User Identity Fields
|
||||
|
||||
All auth modes produce users with:
|
||||
|
||||
| Field | Description |
|
||||
|-------|-------------|
|
||||
| `auth_source` | `builtin`, `mtls`, or `oidc` |
|
||||
| `external_id` | IdP subject (OIDC) or cert fingerprint (mTLS). NULL for builtin. |
|
||||
| `handle` | Unique @mention handle. Auto-generated, collision-safe. |
|
||||
|
||||
`handle` is the canonical identifier for @mentions (replacing username
|
||||
in the resolution chain since v0.24.0).
|
||||
711
docs/ICD/channels.md
Normal file
711
docs/ICD/channels.md
Normal file
@@ -0,0 +1,711 @@
|
||||
# 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.
|
||||
|
||||
### 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|dm|group|channel|workflow|service",
|
||||
"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) |
|
||||
| `dm` | Human-to-human direct message, AI silent unless @mentioned |
|
||||
| `group` | Multi-user/multi-model collaborative channel |
|
||||
| `channel` | Named persistent space, configurable ai_mode |
|
||||
| `workflow` | Staged channel driven by workflow definitions |
|
||||
| `service` | Autonomous task execution, no human participant |
|
||||
|
||||
**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.
|
||||
|
||||
### 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).
|
||||
|
||||
### 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`.
|
||||
|
||||
### 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.
|
||||
|
||||
### 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" }`.
|
||||
|
||||
### 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`.
|
||||
|
||||
### 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.
|
||||
|
||||
### Presence
|
||||
|
||||
Heartbeat-based online status. Not channel-scoped — tracks global user presence.
|
||||
|
||||
**Heartbeat:**
|
||||
|
||||
```
|
||||
POST /presence/heartbeat
|
||||
```
|
||||
|
||||
Client sends every 30 seconds. Server upserts `user_presence` row.
|
||||
No request body needed. Returns `{ "ok": true }`.
|
||||
|
||||
**Auth:** Authenticated.
|
||||
|
||||
**Bulk query:**
|
||||
|
||||
```
|
||||
GET /presence?users=alice,bob,charlie
|
||||
```
|
||||
|
||||
Returns online status for listed usernames. Threshold: online if
|
||||
last heartbeat < 90 seconds ago.
|
||||
|
||||
```json
|
||||
{
|
||||
"users": {
|
||||
"alice": { "status": "online", "last_seen": "..." },
|
||||
"bob": { "status": "offline", "last_seen": "..." }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Subsequent updates delivered via WebSocket `presence.changed` events.
|
||||
|
||||
**Typing indicators** are sent via WebSocket (see websocket.md). The
|
||||
typing event payload includes `participant_id` and `participant_type`.
|
||||
|
||||
### Chat Folders
|
||||
|
||||
User-scoped grouping for chats. Folders have no semantic weight — they
|
||||
are named drawers.
|
||||
|
||||
```
|
||||
GET /folders → list user's folders
|
||||
POST /folders ← { "name": "Research" }
|
||||
PUT /folders/:id ← { "name": "Renamed" }
|
||||
DELETE /folders/:id → chats become unfiled
|
||||
```
|
||||
|
||||
**Folder object:**
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "uuid",
|
||||
"user_id": "uuid",
|
||||
"name": "Research",
|
||||
"parent_id": "uuid|null",
|
||||
"sort_order": 0,
|
||||
"created_at": "...",
|
||||
"updated_at": "..."
|
||||
}
|
||||
```
|
||||
|
||||
Moving a chat into/out of a folder is done via `PUT /channels/:id`
|
||||
with `{ "folder_id": "uuid" }` or `{ "folder_id": null }`.
|
||||
|
||||
**Auth:** Authenticated (user-scoped).
|
||||
|
||||
### Channel Configuration
|
||||
|
||||
Additional channel fields (set via `PUT /channels/:id`):
|
||||
|
||||
| Field | Type | Default | Description |
|
||||
|-------|------|---------|-------------|
|
||||
| `ai_mode` | string | `auto` | `auto`, `mention_only`, or `off` |
|
||||
| `topic` | string | null | Short description shown in header |
|
||||
| `allow_anonymous` | boolean | false | Session participants can join (workflow channels) |
|
||||
| `kb_auto_inject` | boolean | false | Top-K KB chunks auto-prepend to context |
|
||||
|
||||
`ai_mode` behavioral matrix:
|
||||
|
||||
| Channel type | Default ai_mode | Notes |
|
||||
|-------------|----------------|-------|
|
||||
| `direct` | `auto` | Existing behavior |
|
||||
| `dm` | `mention_only` | AI silent unless @mentioned |
|
||||
| `group` | `auto` | Configurable per channel |
|
||||
| `channel` | `auto` | Configurable per channel |
|
||||
|
||||
### Mark Read
|
||||
|
||||
```
|
||||
POST /channels/:id/mark-read
|
||||
```
|
||||
|
||||
Updates the user's `last_read_at` cursor. Used for unread count
|
||||
calculation.
|
||||
|
||||
**Auth:** Authenticated.
|
||||
|
||||
### User Search
|
||||
|
||||
```
|
||||
GET /users/search?q=alice
|
||||
```
|
||||
|
||||
Returns `{ "data": [{ "id", "username", "handle", "display_name", "avatar_url" }] }`.
|
||||
Used for DM creation and participant picker. Matches on handle.
|
||||
|
||||
**Auth:** Authenticated.
|
||||
|
||||
|
||||
---
|
||||
250
docs/ICD/enums.md
Normal file
250
docs/ICD/enums.md
Normal file
@@ -0,0 +1,250 @@
|
||||
# Appendix: Enums & Constants
|
||||
|
||||
All enum values used across the API. Definitive source of truth.
|
||||
|
||||
## Channel Types
|
||||
|
||||
| Value | Description |
|
||||
|-------|-------------|
|
||||
| `direct` | Single-user chat (default, backward compatible) |
|
||||
| `dm` | Human-to-human direct message, AI silent unless @mentioned |
|
||||
| `group` | Multi-user/multi-model collaborative channel |
|
||||
| `channel` | Named persistent space, configurable ai_mode |
|
||||
| `workflow` | Staged channel driven by workflow definitions |
|
||||
| `service` | Autonomous task execution, no human participant |
|
||||
|
||||
## Channel AI Modes
|
||||
|
||||
| Value | Description |
|
||||
|-------|-------------|
|
||||
| `auto` | AI responds to every message (default) |
|
||||
| `mention_only` | AI responds only when @mentioned (default for `dm`) |
|
||||
| `off` | AI disabled entirely |
|
||||
|
||||
## Participant Types
|
||||
|
||||
| Value | Description |
|
||||
|-------|-------------|
|
||||
| `user` | Authenticated user. `participant_id` = `users.id` |
|
||||
| `persona` | AI persona. `participant_id` = `personas.id` |
|
||||
| `session` | Anonymous visitor. `participant_id` = session token |
|
||||
|
||||
## Participant Roles
|
||||
|
||||
| Value | Description |
|
||||
|-------|-------------|
|
||||
| `owner` | Full control: add/remove participants, delete channel |
|
||||
| `member` | Send messages, trigger completions, view history |
|
||||
| `observer` | Read-only: view messages, no send |
|
||||
| `visitor` | Anonymous session participant (workflow intake) |
|
||||
|
||||
## Presence Statuses
|
||||
|
||||
`online`, `away`, `offline`
|
||||
|
||||
## 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`, `persona_user`
|
||||
|
||||
## Memory Statuses
|
||||
|
||||
`active`, `pending_review`, `archived`
|
||||
|
||||
## Workspace Owner Types
|
||||
|
||||
`user`, `project`, `channel`, `team`
|
||||
|
||||
## Workspace Statuses
|
||||
|
||||
`active`, `archived`, `deleting`
|
||||
|
||||
## File Index Statuses
|
||||
|
||||
`pending`, `indexing`, `ready`, `error`, `skipped`
|
||||
|
||||
## KB Document Statuses
|
||||
|
||||
`pending`, `chunking`, `embedding`, `ready`, `error`
|
||||
|
||||
## Provider Health Statuses
|
||||
|
||||
`healthy`, `degraded`, `down`
|
||||
|
||||
## Routing Policy Types
|
||||
|
||||
| Value | Description |
|
||||
|-------|-------------|
|
||||
| `provider_prefer` | Ordered provider fallback list |
|
||||
| `team_route` | Restrict team to specific providers |
|
||||
| `cost_limit` | Heuristic cost cap per request |
|
||||
| `model_alias` | Alias → provider+model rewrite |
|
||||
| `capability_match` | Match cheapest model with required capabilities |
|
||||
|
||||
## Extension Tiers
|
||||
|
||||
`browser` (implemented), `starlark` (future), `sidecar` (future)
|
||||
|
||||
## Grant Types
|
||||
|
||||
| Value | Visibility |
|
||||
|-------|-----------|
|
||||
| `team_only` | Only the owning team |
|
||||
| `global` | All authenticated users |
|
||||
| `groups` | Members of specified groups |
|
||||
|
||||
## Resource Grant Resource Types
|
||||
|
||||
`persona`, `knowledge_base`, `project`
|
||||
|
||||
## Group Sources
|
||||
|
||||
| Value | Description |
|
||||
|-------|-------------|
|
||||
| `manual` | Admin-created |
|
||||
| `oidc` | Synced from IdP groups claim |
|
||||
| `system` | Platform-seeded (e.g., Everyone group) |
|
||||
|
||||
## Auth Sources
|
||||
|
||||
| Value | Description |
|
||||
|-------|-------------|
|
||||
| `builtin` | Username/password, bcrypt |
|
||||
| `mtls` | Mutual TLS via reverse proxy |
|
||||
| `oidc` | OpenID Connect (Keycloak etc.) |
|
||||
|
||||
## Workflow Statuses
|
||||
|
||||
| Value | Description |
|
||||
|-------|-------------|
|
||||
| `active` | Instance is running |
|
||||
| `completed` | All stages finished |
|
||||
| `stale` | No activity within threshold |
|
||||
| `cancelled` | Manually cancelled |
|
||||
|
||||
## Workflow Entry Modes
|
||||
|
||||
`public_link`, `team_only`
|
||||
|
||||
## Workflow History Modes
|
||||
|
||||
| Value | Description |
|
||||
|-------|-------------|
|
||||
| `full` | Next stage sees complete chat history |
|
||||
| `summary` | Utility-role summary of previous stages |
|
||||
| `fresh` | Clean slate — no history carried forward |
|
||||
|
||||
## Task Types
|
||||
|
||||
`prompt`, `workflow`
|
||||
|
||||
## Task Run Statuses
|
||||
|
||||
| Value | Description |
|
||||
|-------|-------------|
|
||||
| `running` | Currently executing |
|
||||
| `completed` | Finished successfully |
|
||||
| `failed` | Error during execution |
|
||||
| `budget_exceeded` | Hit token/tool/wall-clock limit |
|
||||
| `cancelled` | Manually killed |
|
||||
|
||||
## Task Scopes
|
||||
|
||||
`personal`, `team`, `global`
|
||||
|
||||
## Task Output Modes
|
||||
|
||||
`channel`, `note`, `webhook`
|
||||
|
||||
## Assignment Statuses
|
||||
|
||||
`unassigned`, `claimed`, `completed`
|
||||
|
||||
## Surface Sources
|
||||
|
||||
`core`, `extension`
|
||||
|
||||
## Notification Types
|
||||
|
||||
`role.fallback`, `memory.extracted`, `system.announcement`,
|
||||
`workflow.assigned`, `workflow.claimed`, `task.completed`,
|
||||
`task.failed`, `user.mentioned`
|
||||
|
||||
## Git Auth Types
|
||||
|
||||
`https_pat`, `https_basic`, `ssh_key`
|
||||
|
||||
## File Origins
|
||||
|
||||
`user_upload`, `tool_output`, `system`
|
||||
|
||||
## File Display Hints
|
||||
|
||||
`inline`, `download`, `thumbnail`
|
||||
|
||||
## Proxy Modes
|
||||
|
||||
`system`, `direct`, `custom`
|
||||
|
||||
## Permissions
|
||||
|
||||
12 permission constants, `domain.action` convention:
|
||||
|
||||
| Permission | Description |
|
||||
|-----------|-------------|
|
||||
| `model.use` | Use AI models for completions |
|
||||
| `kb.read` | Search knowledge bases |
|
||||
| `kb.create` | Create knowledge bases |
|
||||
| `kb.write` | Upload documents to KBs |
|
||||
| `channel.create` | Create new channels |
|
||||
| `channel.invite` | Add participants to channels |
|
||||
| `persona.create` | Create personal personas |
|
||||
| `persona.manage` | Edit/delete own personas |
|
||||
| `workflow.create` | Create/edit workflow definitions |
|
||||
| `tasks.create` | Create/manage tasks |
|
||||
| `tasks.admin` | Manage all tasks (admin) |
|
||||
| `admin.access` | Access admin panel |
|
||||
|
||||
Permissions are granted via groups. The `Everyone` group
|
||||
(ID `00000000-0000-0000-0000-000000000001`) provides default
|
||||
permissions to all authenticated users. It is system-seeded
|
||||
and editable by admins.
|
||||
|
||||
## Policies
|
||||
|
||||
| Key | Default | Description |
|
||||
|-----|---------|-------------|
|
||||
| `allow_registration` | `"true"` | Allow new user self-registration |
|
||||
| `allow_user_byok` | `"false"` | Allow users to add personal API keys |
|
||||
| `allow_user_personas` | `"false"` | Allow users to create personal Personas |
|
||||
| `allow_team_providers` | `"true"` | Allow team admins to configure team providers |
|
||||
| `allow_raw_model_access` | `"true"` | Allow raw model selection without persona |
|
||||
| `kb_direct_access` | `"true"` | Show KB picker in channel (false = strict enterprise) |
|
||||
| `default_user_active` | `"false"` | New users active immediately (vs admin approval) |
|
||||
34
docs/ICD/extensions.md
Normal file
34
docs/ICD/extensions.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# 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.
|
||||
|
||||
### 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] }
|
||||
```
|
||||
|
||||
### Admin Extension Management
|
||||
|
||||
```
|
||||
GET /admin/extensions → { "extensions": [...] }
|
||||
POST /admin/extensions ← { manifest + script content }
|
||||
PUT /admin/extensions/:id ← updated manifest/script
|
||||
DELETE /admin/extensions/:id
|
||||
```
|
||||
|
||||
### Asset Serving
|
||||
|
||||
```
|
||||
GET /extensions/:id/assets/*path
|
||||
```
|
||||
|
||||
Public (no auth required). Serves static assets (icons, CSS, JS)
|
||||
from extension bundles.
|
||||
|
||||
---
|
||||
167
docs/ICD/knowledge.md
Normal file
167
docs/ICD/knowledge.md
Normal file
@@ -0,0 +1,167 @@
|
||||
# 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.
|
||||
|
||||
### 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": "..."
|
||||
}
|
||||
```
|
||||
|
||||
### 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
|
||||
```
|
||||
|
||||
### Search
|
||||
|
||||
```
|
||||
POST /knowledge-bases/:id/search
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"query": "How do I configure SSO?",
|
||||
"limit": 5
|
||||
}
|
||||
```
|
||||
|
||||
Returns `{ "results": [{ "content", "score", "document_id", "metadata" }] }`.
|
||||
|
||||
### Rebuild
|
||||
|
||||
Re-chunks and re-embeds all documents:
|
||||
|
||||
```
|
||||
POST /knowledge-bases/:id/rebuild
|
||||
```
|
||||
|
||||
### 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"] }
|
||||
```
|
||||
|
||||
### 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 }
|
||||
```
|
||||
|
||||
### 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.
|
||||
|
||||
### Persona KB Bindings
|
||||
|
||||
See §4.4. Persona-KB binding is the primary enterprise mechanism for
|
||||
controlled KB access.
|
||||
|
||||
---
|
||||
48
docs/ICD/memory.md
Normal file
48
docs/ICD/memory.md
Normal file
@@ -0,0 +1,48 @@
|
||||
# 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.
|
||||
|
||||
### 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.
|
||||
|
||||
### 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.
|
||||
|
||||
---
|
||||
88
docs/ICD/models.md
Normal file
88
docs/ICD/models.md
Normal file
@@ -0,0 +1,88 @@
|
||||
# Models & Preferences
|
||||
|
||||
What models are available to the current user and how they control
|
||||
visibility.
|
||||
|
||||
### 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.
|
||||
|
||||
### 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.
|
||||
|
||||
---
|
||||
105
docs/ICD/notes.md
Normal file
105
docs/ICD/notes.md
Normal file
@@ -0,0 +1,105 @@
|
||||
# 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.
|
||||
|
||||
### 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.
|
||||
|
||||
### 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" }] }`.
|
||||
|
||||
### 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]]`.
|
||||
|
||||
### Folders
|
||||
|
||||
```
|
||||
GET /notes/folders
|
||||
```
|
||||
|
||||
Returns `{ "folders": ["work", "personal", "archive"] }` — distinct
|
||||
folder values across all user notes.
|
||||
|
||||
### Bulk Operations
|
||||
|
||||
```
|
||||
POST /notes/bulk-delete
|
||||
```
|
||||
|
||||
```json
|
||||
{ "ids": ["uuid-1", "uuid-2"] }
|
||||
```
|
||||
|
||||
---
|
||||
44
docs/ICD/notifications.md
Normal file
44
docs/ICD/notifications.md
Normal file
@@ -0,0 +1,44 @@
|
||||
# Notifications
|
||||
|
||||
Real-time notification infrastructure with WebSocket delivery and
|
||||
optional email transport.
|
||||
|
||||
### 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": "..."
|
||||
}
|
||||
```
|
||||
|
||||
### 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`).
|
||||
|
||||
---
|
||||
204
docs/ICD/personas.md
Normal file
204
docs/ICD/personas.md
Normal file
@@ -0,0 +1,204 @@
|
||||
# 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.
|
||||
|
||||
### 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
|
||||
```
|
||||
|
||||
### Team Personas
|
||||
|
||||
```
|
||||
GET /teams/:teamId/personas → { "personas": [...] }
|
||||
POST /teams/:teamId/personas ← same shape
|
||||
PUT /teams/:teamId/personas/:id
|
||||
DELETE /teams/:teamId/personas/:id
|
||||
```
|
||||
|
||||
### Admin (Global) Personas
|
||||
|
||||
```
|
||||
GET /admin/personas → { "personas": [...] }
|
||||
POST /admin/personas ← same shape
|
||||
PUT /admin/personas/:id
|
||||
DELETE /admin/personas/:id
|
||||
```
|
||||
|
||||
### 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.
|
||||
|
||||
### 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
|
||||
```
|
||||
|
||||
### Persona Tool Grants
|
||||
|
||||
Control which tools a persona can use during completions. The completion
|
||||
handler applies tool grants as a second-pass allowlist.
|
||||
|
||||
```
|
||||
GET /personas/:id/tool-grants → { "grants": ["web_search", "kb_search", ...] }
|
||||
PUT /personas/:id/tool-grants ← { "tool_ids": ["web_search", "calculator"] }
|
||||
```
|
||||
|
||||
Admin equivalents:
|
||||
|
||||
```
|
||||
GET /admin/personas/:id/tool-grants
|
||||
PUT /admin/personas/:id/tool-grants
|
||||
```
|
||||
|
||||
When a workflow version is published, persona tool grants at that moment
|
||||
are frozen into the version snapshot.
|
||||
|
||||
### Persona Groups
|
||||
|
||||
Saved roster templates. A persona group is a named set of personas that
|
||||
can be stamped onto new conversations.
|
||||
|
||||
```
|
||||
GET /persona-groups → { "data": [...] }
|
||||
POST /persona-groups ← { "name", "description", "scope", "team_id" }
|
||||
GET /persona-groups/:id → group with members
|
||||
PUT /persona-groups/:id ← partial update
|
||||
DELETE /persona-groups/:id
|
||||
```
|
||||
|
||||
**Members:**
|
||||
|
||||
```
|
||||
POST /persona-groups/:id/members ← { "persona_id", "is_leader": false, "sort_order": 0 }
|
||||
DELETE /persona-groups/:id/members/:memberId
|
||||
```
|
||||
|
||||
`is_leader`: the leader persona responds when no @mention is present in
|
||||
a group channel. Only one leader per group.
|
||||
|
||||
**Persona Group Object:**
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "uuid",
|
||||
"name": "Support Team",
|
||||
"description": "...",
|
||||
"owner_id": "uuid",
|
||||
"scope": "personal|team|global",
|
||||
"team_id": "uuid|null",
|
||||
"members": [
|
||||
{ "id": "uuid", "persona_id": "uuid", "is_leader": true, "sort_order": 0 }
|
||||
],
|
||||
"created_at": "...",
|
||||
"updated_at": "..."
|
||||
}
|
||||
```
|
||||
|
||||
### Resource Grants
|
||||
|
||||
See [teams.md](teams.md) §15.3 for the grant system. Personas use grants
|
||||
to control who can see and use them beyond their base scope.
|
||||
|
||||
55
docs/ICD/profile.md
Normal file
55
docs/ICD/profile.md
Normal file
@@ -0,0 +1,55 @@
|
||||
# User Profile & Settings
|
||||
|
||||
### 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": "..."
|
||||
}
|
||||
```
|
||||
|
||||
### Avatar
|
||||
|
||||
```
|
||||
POST /profile/avatar ← multipart/form-data (field: "avatar")
|
||||
DELETE /profile/avatar
|
||||
```
|
||||
|
||||
### 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).
|
||||
|
||||
### App Settings
|
||||
|
||||
```
|
||||
GET /settings → { "settings": {...} }
|
||||
PUT /settings ← { "theme", "editor_keybindings", ... }
|
||||
```
|
||||
|
||||
User-level preferences (theme, keybindings, default model, etc.).
|
||||
|
||||
---
|
||||
87
docs/ICD/projects.md
Normal file
87
docs/ICD/projects.md
Normal file
@@ -0,0 +1,87 @@
|
||||
# Projects
|
||||
|
||||
**Projects** are organizational containers that group channels,
|
||||
knowledge bases, notes, and files. They follow the scope model
|
||||
(personal, team, global).
|
||||
|
||||
### 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": "..."
|
||||
}
|
||||
```
|
||||
|
||||
### 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.
|
||||
|
||||
### 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).
|
||||
|
||||
### Note Association
|
||||
|
||||
```
|
||||
GET /projects/:id/notes → { "data": [note objects] }
|
||||
POST /projects/:id/notes ← { "note_id": "uuid" }
|
||||
DELETE /projects/:id/notes/:noteId
|
||||
```
|
||||
|
||||
### 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).
|
||||
|
||||
### 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).
|
||||
|
||||
---
|
||||
221
docs/ICD/providers.md
Normal file
221
docs/ICD/providers.md
Normal file
@@ -0,0 +1,221 @@
|
||||
# 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.
|
||||
|
||||
### 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.
|
||||
|
||||
### 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.
|
||||
|
||||
### 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`).
|
||||
|
||||
### 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.
|
||||
|
||||
### 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.
|
||||
|
||||
### 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|capability_match",
|
||||
"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 |
|
||||
| `capability_match` | `{ "require": ["tool_calling"], "prefer": "cheapest" }` | Match cheapest model with required capabilities |
|
||||
|
||||
**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.
|
||||
|
||||
### 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.
|
||||
|
||||
---
|
||||
118
docs/ICD/surfaces.md
Normal file
118
docs/ICD/surfaces.md
Normal file
@@ -0,0 +1,118 @@
|
||||
# Surfaces
|
||||
|
||||
Dynamic surface lifecycle. Core surfaces are registered at startup.
|
||||
Extension surfaces are uploaded by admins and served from the
|
||||
`surface_registry` table.
|
||||
|
||||
## User Endpoints
|
||||
|
||||
### List Enabled Surfaces
|
||||
|
||||
```
|
||||
GET /surfaces
|
||||
```
|
||||
|
||||
Returns surfaces the user can access (enabled in registry).
|
||||
|
||||
```json
|
||||
{
|
||||
"surfaces": [
|
||||
{ "id": "chat", "title": "Chat", "source": "core", "enabled": true },
|
||||
{ "id": "editor", "title": "Editor", "source": "core", "enabled": true },
|
||||
{ "id": "hello-dashboard", "title": "Hello Dashboard", "source": "extension", "enabled": true }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Auth:** Authenticated.
|
||||
|
||||
## Admin Endpoints
|
||||
|
||||
### List All Surfaces
|
||||
|
||||
```
|
||||
GET /admin/surfaces
|
||||
```
|
||||
|
||||
Returns all surfaces including disabled ones.
|
||||
|
||||
**Auth:** Platform admin.
|
||||
|
||||
### Get Surface
|
||||
|
||||
```
|
||||
GET /admin/surfaces/:id
|
||||
```
|
||||
|
||||
Returns full surface details including manifest.
|
||||
|
||||
### Install Surface
|
||||
|
||||
```
|
||||
POST /admin/surfaces/install
|
||||
Content-Type: multipart/form-data
|
||||
```
|
||||
|
||||
Field: `archive` — a `.surface` archive (tar.gz containing `manifest.json`
|
||||
+ static assets). The manifest declares `id`, `title`, `route`, required
|
||||
data loaders, scripts, and CSS.
|
||||
|
||||
**Auth:** Platform admin.
|
||||
|
||||
### Enable / Disable
|
||||
|
||||
```
|
||||
PUT /admin/surfaces/:id/enable
|
||||
PUT /admin/surfaces/:id/disable
|
||||
```
|
||||
|
||||
Disabled surfaces redirect to `/` and hide from navigation.
|
||||
|
||||
**Auth:** Platform admin.
|
||||
|
||||
### Delete Surface
|
||||
|
||||
```
|
||||
DELETE /admin/surfaces/:id
|
||||
```
|
||||
|
||||
Removes the surface and its static assets. Core surfaces cannot be deleted.
|
||||
|
||||
**Auth:** Platform admin.
|
||||
|
||||
## Surface Registry Object
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "hello-dashboard",
|
||||
"title": "Hello Dashboard",
|
||||
"manifest": { "route": "/s/hello-dashboard", "scripts": [...], "css": [...] },
|
||||
"enabled": true,
|
||||
"source": "core|extension",
|
||||
"installed_at": "...",
|
||||
"updated_at": "..."
|
||||
}
|
||||
```
|
||||
|
||||
## Page Routes
|
||||
|
||||
Extension surfaces are served at `/s/:slug` via `RenderExtensionSurface()`.
|
||||
The page engine does a runtime DB lookup — no server restart needed after install.
|
||||
|
||||
Static assets are served from `/surfaces/:id/` (nginx location block in
|
||||
both unified and split deployment).
|
||||
|
||||
## Surface Archive Format
|
||||
|
||||
A `.surface` file is a tar.gz containing:
|
||||
|
||||
```
|
||||
manifest.json — required
|
||||
script.js — optional
|
||||
style.css — optional
|
||||
assets/ — optional (icons, images, etc.)
|
||||
```
|
||||
|
||||
See [EXTENSION-SURFACES.md](../EXTENSION-SURFACES.md) for the full
|
||||
authoring guide including manifest schema, platform API, and CSS
|
||||
custom properties.
|
||||
290
docs/ICD/tasks.md
Normal file
290
docs/ICD/tasks.md
Normal file
@@ -0,0 +1,290 @@
|
||||
# Tasks
|
||||
|
||||
Autonomous agent scheduling. A task is a cron-driven or one-shot prompt
|
||||
execution in a headless service channel — no human participant in the loop.
|
||||
|
||||
## Configuration
|
||||
|
||||
Global config keys (set via `PUT /admin/settings/:key`):
|
||||
|
||||
| Key | Default | Description |
|
||||
|-----|---------|-------------|
|
||||
| `tasks.enabled` | `true` | Master kill switch for task scheduler |
|
||||
| `tasks.allow_personal` | `true` | Allow non-admin users to create tasks |
|
||||
| `tasks.max_concurrent` | `5` | Max tasks running simultaneously |
|
||||
| `tasks.personal_require_byok` | `false` | Personal tasks must use BYOK provider |
|
||||
|
||||
Default budget ceilings (overridable per task):
|
||||
|
||||
| Key | Default |
|
||||
|-----|---------|
|
||||
| `tasks.default_max_tokens` | `4096` |
|
||||
| `tasks.default_max_tool_calls` | `10` |
|
||||
| `tasks.default_max_wall_clock` | `300` (seconds) |
|
||||
|
||||
## Personal Tasks
|
||||
|
||||
### List My Tasks
|
||||
|
||||
```
|
||||
GET /tasks
|
||||
```
|
||||
|
||||
Returns tasks owned by the current user.
|
||||
|
||||
**Auth:** Authenticated.
|
||||
|
||||
### Create Task
|
||||
|
||||
```
|
||||
POST /tasks
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Morning News Digest",
|
||||
"description": "Summarize top tech news",
|
||||
"task_type": "prompt",
|
||||
"persona_id": "uuid|null",
|
||||
"model_id": "claude-sonnet-4-20250514",
|
||||
"system_prompt": "You are a news summarizer.",
|
||||
"user_prompt": "Summarize the top 5 tech news stories from today.",
|
||||
"schedule": "@daily",
|
||||
"timezone": "America/New_York",
|
||||
"max_tokens": 4096,
|
||||
"max_tool_calls": 10,
|
||||
"max_wall_clock": 300,
|
||||
"output_mode": "channel",
|
||||
"webhook_url": "https://...",
|
||||
"provider_config_id": "uuid|null",
|
||||
"notify_on_complete": false,
|
||||
"notify_on_failure": true,
|
||||
"tool_grants": ["web_search", "url_fetch"]
|
||||
}
|
||||
```
|
||||
|
||||
`task_type`: `prompt` (direct LLM execution) or `workflow` (instantiate
|
||||
a workflow in the service channel).
|
||||
|
||||
`schedule`: cron expression or `once`. Supported patterns: `@hourly`,
|
||||
`@daily`, `@weekly`, `*/N * * * *` (every N minutes), full 5-field cron
|
||||
via `robfig/cron/v3`.
|
||||
|
||||
`output_mode`: `channel` (default — output stays in service channel),
|
||||
`note` (creates a note from the output), `webhook` (POST result to URL).
|
||||
|
||||
**Auth:** `tasks.create` permission required.
|
||||
|
||||
### Get Task
|
||||
|
||||
```
|
||||
GET /tasks/:id
|
||||
```
|
||||
|
||||
**Auth:** Owner or admin.
|
||||
|
||||
### Update Task
|
||||
|
||||
```
|
||||
PUT /tasks/:id
|
||||
```
|
||||
|
||||
Partial update. All fields from create are accepted.
|
||||
|
||||
**Auth:** `tasks.create` permission, must be owner.
|
||||
|
||||
### Delete Task
|
||||
|
||||
```
|
||||
DELETE /tasks/:id
|
||||
```
|
||||
|
||||
**Auth:** `tasks.create` permission, must be owner.
|
||||
|
||||
### Run Now (Manual Trigger)
|
||||
|
||||
```
|
||||
POST /tasks/:id/run
|
||||
```
|
||||
|
||||
Immediately executes the task regardless of schedule. Creates a new
|
||||
task run. Returns 409 if a run is already active.
|
||||
|
||||
**Auth:** `tasks.create` permission, must be owner.
|
||||
|
||||
### Kill Active Run
|
||||
|
||||
```
|
||||
POST /tasks/:id/kill
|
||||
```
|
||||
|
||||
Cancels the active run. Sets status to `cancelled`.
|
||||
|
||||
**Auth:** `tasks.create` permission, must be owner.
|
||||
|
||||
### List Runs
|
||||
|
||||
```
|
||||
GET /tasks/:id/runs
|
||||
```
|
||||
|
||||
Returns run history for the task, most recent first.
|
||||
|
||||
**Auth:** Owner or admin.
|
||||
|
||||
## Team Tasks
|
||||
|
||||
Team-scoped tasks visible to all team members, manageable by team admins.
|
||||
|
||||
### List Team Tasks (Member)
|
||||
|
||||
```
|
||||
GET /teams/:teamId/tasks
|
||||
```
|
||||
|
||||
Returns tasks scoped to the team. Read-only for members.
|
||||
|
||||
**Auth:** Team member.
|
||||
|
||||
### List Team Task Runs (Member)
|
||||
|
||||
```
|
||||
GET /teams/:teamId/tasks/:id/runs
|
||||
```
|
||||
|
||||
**Auth:** Team member.
|
||||
|
||||
### Create Team Task (Admin)
|
||||
|
||||
```
|
||||
POST /teams/:teamId/tasks
|
||||
```
|
||||
|
||||
Same shape as personal task creation. Team scope is injected automatically.
|
||||
|
||||
**Auth:** `tasks.create` permission, team admin.
|
||||
|
||||
### Update/Delete/Run/Kill Team Task
|
||||
|
||||
```
|
||||
PUT /teams/:teamId/tasks/:id
|
||||
DELETE /teams/:teamId/tasks/:id
|
||||
POST /teams/:teamId/tasks/:id/run
|
||||
POST /teams/:teamId/tasks/:id/kill
|
||||
```
|
||||
|
||||
**Auth:** `tasks.create` permission, team admin.
|
||||
|
||||
## Admin Tasks
|
||||
|
||||
Platform-wide task management.
|
||||
|
||||
### List All Tasks
|
||||
|
||||
```
|
||||
GET /admin/tasks
|
||||
```
|
||||
|
||||
Returns all tasks across all users and teams.
|
||||
|
||||
**Auth:** Platform admin.
|
||||
|
||||
### Admin Run/Kill/Delete
|
||||
|
||||
```
|
||||
POST /admin/tasks/:id/run
|
||||
POST /admin/tasks/:id/kill
|
||||
DELETE /admin/tasks/:id
|
||||
```
|
||||
|
||||
**Auth:** Platform admin.
|
||||
|
||||
## Task Object
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "uuid",
|
||||
"owner_id": "uuid",
|
||||
"team_id": "uuid|null",
|
||||
"name": "Morning News Digest",
|
||||
"description": "...",
|
||||
"scope": "personal|team|global",
|
||||
"task_type": "prompt|workflow",
|
||||
"persona_id": "uuid|null",
|
||||
"model_id": "claude-sonnet-4-20250514",
|
||||
"system_prompt": "...",
|
||||
"user_prompt": "...",
|
||||
"workflow_id": "uuid|null",
|
||||
"tool_grants": ["web_search", "url_fetch"],
|
||||
"schedule": "@daily",
|
||||
"timezone": "America/New_York",
|
||||
"is_active": true,
|
||||
"max_tokens": 4096,
|
||||
"max_tool_calls": 10,
|
||||
"max_wall_clock": 300,
|
||||
"output_mode": "channel|note|webhook",
|
||||
"output_channel_id": "uuid|null",
|
||||
"webhook_url": "https://...",
|
||||
"webhook_secret": "...",
|
||||
"provider_config_id": "uuid|null",
|
||||
"notify_on_complete": false,
|
||||
"notify_on_failure": true,
|
||||
"last_run_at": "...|null",
|
||||
"next_run_at": "...|null",
|
||||
"run_count": 42,
|
||||
"created_at": "...",
|
||||
"updated_at": "..."
|
||||
}
|
||||
```
|
||||
|
||||
## Task Run Object
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "uuid",
|
||||
"task_id": "uuid",
|
||||
"channel_id": "uuid|null",
|
||||
"status": "running|completed|failed|budget_exceeded|cancelled",
|
||||
"started_at": "...",
|
||||
"completed_at": "...|null",
|
||||
"tokens_used": 1234,
|
||||
"tool_calls": 3,
|
||||
"wall_clock": 45,
|
||||
"error": "...|null"
|
||||
}
|
||||
```
|
||||
|
||||
## Execution
|
||||
|
||||
The `TaskScheduler` is a background goroutine polling every 30 seconds.
|
||||
|
||||
1. Finds tasks where `next_run_at <= now` and `is_active = true`
|
||||
2. Skips if an active run exists (`GetActiveRun` returns non-nil)
|
||||
3. Creates or reuses a `service` channel (`output_channel_id`)
|
||||
4. Persists the `user_prompt` as a message in the service channel
|
||||
5. Runs `CoreToolLoop` (headless completion — same as streaming but no SSE)
|
||||
6. Enforces budgets: `max_tokens`, `max_tool_calls`, `max_wall_clock`
|
||||
7. On budget breach: status = `budget_exceeded`, owner notified
|
||||
8. On completion: updates `last_run_at`, calculates `next_run_at`
|
||||
|
||||
**Provider resolution:** BYOK → team provider → global provider → routing
|
||||
policy. If `tasks.personal_require_byok` is true, personal tasks that
|
||||
don't have a BYOK provider fail at step 5.
|
||||
|
||||
## Webhooks
|
||||
|
||||
Tasks with `webhook_url` fire a POST on completion:
|
||||
|
||||
```json
|
||||
{
|
||||
"task_id": "uuid",
|
||||
"run_id": "uuid",
|
||||
"status": "completed|failed|budget_exceeded",
|
||||
"output": "...",
|
||||
"tokens_used": 1234,
|
||||
"timestamp": "..."
|
||||
}
|
||||
```
|
||||
|
||||
Signed with HMAC-SHA256 using `webhook_secret` in the
|
||||
`X-Switchboard-Signature` header. Retry: 3 attempts, exponential backoff.
|
||||
177
docs/ICD/teams.md
Normal file
177
docs/ICD/teams.md
Normal file
@@ -0,0 +1,177 @@
|
||||
# Teams & Access Control
|
||||
|
||||
### My Teams
|
||||
|
||||
```
|
||||
GET /teams/mine → { "teams": [...] }
|
||||
```
|
||||
|
||||
Returns teams the current user is a member of.
|
||||
|
||||
### 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": [...] }
|
||||
```
|
||||
|
||||
### 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 |
|
||||
|
||||
### Permissions
|
||||
|
||||
Groups carry fine-grained permissions. Permission resolution:
|
||||
union of all group permissions + Everyone group permissions.
|
||||
|
||||
**List all permissions:**
|
||||
|
||||
```
|
||||
GET /admin/permissions → { "permissions": ["model.use", "kb.read", ...] }
|
||||
```
|
||||
|
||||
**Get user's effective permissions:**
|
||||
|
||||
```
|
||||
GET /admin/users/:id/permissions → { "permissions": [...], "groups": [...] }
|
||||
```
|
||||
|
||||
Returns the resolved permission set and contributing groups.
|
||||
|
||||
**Group permission fields** (set via `PUT /admin/groups/:id`):
|
||||
|
||||
```json
|
||||
{
|
||||
"permissions": ["model.use", "kb.read", "channel.create"],
|
||||
"token_budget_daily": 100000,
|
||||
"token_budget_monthly": 3000000,
|
||||
"allowed_models": ["claude-sonnet-4-20250514", "gpt-4o"]
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `permissions` | string[] | Permission constants granted to members |
|
||||
| `token_budget_daily` | int/null | Daily token ceiling (NULL = unlimited) |
|
||||
| `token_budget_monthly` | int/null | Monthly token ceiling (NULL = unlimited) |
|
||||
| `allowed_models` | string[]/null | Model allowlist (NULL = unrestricted) |
|
||||
|
||||
**Everyone group:** System-seeded group (ID `00000000-0000-0000-0000-000000000001`)
|
||||
whose permissions apply to all authenticated users implicitly (no membership
|
||||
row needed). Editable by admins, cannot be deleted (`source=system`).
|
||||
|
||||
**RequirePermission middleware:** Routes gated by permission check user's
|
||||
resolved permission set. Returns 403 if the required permission is not present.
|
||||
|
||||
See [enums.md](enums.md) for the full permission constant list.
|
||||
|
||||
146
docs/ICD/utility.md
Normal file
146
docs/ICD/utility.md
Normal file
@@ -0,0 +1,146 @@
|
||||
# Export & Utility
|
||||
|
||||
### Health Check
|
||||
|
||||
```
|
||||
GET /health → { "status": "ok" }
|
||||
```
|
||||
|
||||
Public, no auth. Also available at `/api/v1/health`.
|
||||
|
||||
### 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.
|
||||
|
||||
### 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`).
|
||||
|
||||
### 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 |
|
||||
|
||||
### 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 |
|
||||
|
||||
### 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.
|
||||
|
||||
### Content Type → Display Hint Defaults
|
||||
|
||||
| Content Type | Default Hint | Rendering |
|
||||
|-------------|-------------|-----------|
|
||||
| `image/*` | `inline` | `<img>` in message, lightbox on click |
|
||||
| `video/*` | `inline` | `<video>` player |
|
||||
| `audio/*` | `inline` | `<audio>` player |
|
||||
| `text/html` | `inline` | Sandboxed iframe |
|
||||
| `application/pdf` | `thumbnail` | Thumb + PDF viewer on click |
|
||||
| `text/*`, `application/json` | `inline` | Syntax-highlighted code block |
|
||||
| Everything else | `download` | Filename + download button |
|
||||
|
||||
Tools can override the default hint explicitly.
|
||||
|
||||
### File Manager (Future Surface)
|
||||
|
||||
A user-scoped file browser backed by `GET /files?page=1&per_page=50`.
|
||||
Displays all files owned by the user across all channels, grouped by
|
||||
channel or date. Supports download and delete. Deleting a file that
|
||||
is referenced by a message replaces the inline render with a
|
||||
"file deleted" placeholder.
|
||||
219
docs/ICD/websocket.md
Normal file
219
docs/ICD/websocket.md
Normal file
@@ -0,0 +1,219 @@
|
||||
# WebSocket Protocol
|
||||
|
||||
Real-time bidirectional event bus. Single connection per client.
|
||||
|
||||
### 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
|
||||
|
||||
### Event Envelope
|
||||
|
||||
All messages are JSON:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "event.type",
|
||||
"payload": { ... }
|
||||
}
|
||||
```
|
||||
|
||||
### Room Subscription
|
||||
|
||||
Clients subscribe to rooms for scoped event delivery:
|
||||
|
||||
```json
|
||||
{ "type": "subscribe", "payload": { "room": "channel:uuid" } }
|
||||
{ "type": "unsubscribe", "payload": { "room": "channel:uuid" } }
|
||||
```
|
||||
|
||||
### 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 | Participant started typing (payload includes `participant_id`, `participant_type`) |
|
||||
| `typing.stop` | ↔ both | Typing ended |
|
||||
| `participant.joined` | → client | Participant added to channel |
|
||||
| `participant.left` | → client | Participant removed from channel |
|
||||
| `participant.updated` | → client | Participant role changed |
|
||||
| `presence.update` | → client | Participant online/idle/offline status change |
|
||||
| `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 |
|
||||
| `file.created` | → client | File uploaded or generated (§17b.4) |
|
||||
| `file.deleted` | → client | File removed |
|
||||
|
||||
Direction: `→ client` = server-to-client only, `← client` = client-to-server
|
||||
only, `↔ both` = bidirectional.
|
||||
|
||||
---
|
||||
|
||||
## 19. Appendix: Enums
|
||||
|
||||
### Channel Types
|
||||
|
||||
`direct` (single-user), `group` (multi-user/multi-model), `workflow` (staged)
|
||||
|
||||
### Participant Types
|
||||
|
||||
`user`, `persona`, `session`
|
||||
|
||||
### Participant Roles
|
||||
|
||||
`owner`, `member`, `observer`
|
||||
|
||||
### Presence Statuses
|
||||
|
||||
`online`, `idle`, `offline`
|
||||
|
||||
### 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.
|
||||
472
docs/ICD/workflows.md
Normal file
472
docs/ICD/workflows.md
Normal file
@@ -0,0 +1,472 @@
|
||||
# Workflows
|
||||
|
||||
Team-owned, stage-based process execution. The channel is the runtime —
|
||||
personas drive each stage, and the existing tool/notes infrastructure
|
||||
handles structured data collection.
|
||||
|
||||
## Definitions
|
||||
|
||||
### List Workflows
|
||||
|
||||
```
|
||||
GET /workflows?team_id=...
|
||||
```
|
||||
|
||||
Returns `{ "data": [...] }`. If `team_id` is provided, scoped to that team.
|
||||
Otherwise returns global (team_id IS NULL) workflows.
|
||||
|
||||
**Auth:** Authenticated.
|
||||
|
||||
### Create Workflow
|
||||
|
||||
```
|
||||
POST /workflows
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Customer Intake",
|
||||
"slug": "customer-intake",
|
||||
"description": "Collect customer info and route to support",
|
||||
"entry_mode": "public_link",
|
||||
"team_id": "uuid|null",
|
||||
"branding": { "accent_color": "#2563eb", "tagline": "Welcome" },
|
||||
"retention": { "mode": "archive", "delete_after_days": 90 }
|
||||
}
|
||||
```
|
||||
|
||||
`slug` auto-generated from `name` if omitted. Must be 2-64 chars,
|
||||
lowercase alphanumeric and hyphens. Unique within scope (team or global).
|
||||
|
||||
`entry_mode`: `public_link` (visitors can start via URL) or `team_only`
|
||||
(only team members can start instances).
|
||||
|
||||
**Auth:** `workflow.create` permission required.
|
||||
|
||||
**Response:** 201 with workflow object.
|
||||
|
||||
### Get Workflow
|
||||
|
||||
```
|
||||
GET /workflows/:id
|
||||
```
|
||||
|
||||
Returns workflow with its stages included in the `stages` array.
|
||||
|
||||
**Auth:** Authenticated.
|
||||
|
||||
### Update Workflow
|
||||
|
||||
```
|
||||
PATCH /workflows/:id
|
||||
```
|
||||
|
||||
Partial update. Accepted fields: `name`, `description`, `branding`,
|
||||
`entry_mode`, `is_active`, `on_complete`, `retention`, `webhook_url`.
|
||||
|
||||
Any edit increments the `version` number.
|
||||
|
||||
**Auth:** `workflow.create` permission required.
|
||||
|
||||
### Delete Workflow
|
||||
|
||||
```
|
||||
DELETE /workflows/:id
|
||||
```
|
||||
|
||||
Cascades: deletes all stages, versions, and active instances.
|
||||
|
||||
**Auth:** `workflow.create` permission required.
|
||||
|
||||
### Workflow Object
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "uuid",
|
||||
"team_id": "uuid|null",
|
||||
"name": "Customer Intake",
|
||||
"slug": "customer-intake",
|
||||
"description": "...",
|
||||
"branding": { "accent_color": "#2563eb", "logo_url": "...", "tagline": "..." },
|
||||
"entry_mode": "public_link|team_only",
|
||||
"is_active": true,
|
||||
"version": 2,
|
||||
"on_complete": { "action": "start_workflow", "target_slug": "follow-up", "data_map": {"name": "customer_name"} },
|
||||
"retention": { "mode": "archive|delete", "delete_after_days": 90 },
|
||||
"webhook_url": "https://...",
|
||||
"webhook_secret": "...",
|
||||
"created_by": "uuid",
|
||||
"created_at": "...",
|
||||
"updated_at": "...",
|
||||
"stages": [...]
|
||||
}
|
||||
```
|
||||
|
||||
## Stages
|
||||
|
||||
Ordered steps within a workflow. Each stage has a driving persona and
|
||||
optional human assignment team.
|
||||
|
||||
### List Stages
|
||||
|
||||
```
|
||||
GET /workflows/:id/stages
|
||||
```
|
||||
|
||||
Returns `{ "data": [...] }` ordered by `ordinal`.
|
||||
|
||||
**Auth:** Authenticated.
|
||||
|
||||
### Create Stage
|
||||
|
||||
```
|
||||
POST /workflows/:id/stages
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Collect Info",
|
||||
"ordinal": 0,
|
||||
"persona_id": "uuid|null",
|
||||
"assignment_team_id": "uuid|null",
|
||||
"form_template": { "fields": ["name", "email", "issue"] },
|
||||
"history_mode": "full|summary|fresh",
|
||||
"auto_transition": false,
|
||||
"transition_rules": { "auto_assign": "round_robin" }
|
||||
}
|
||||
```
|
||||
|
||||
`history_mode`: what chat history the next stage sees. `full` = complete,
|
||||
`summary` = utility-role summary, `fresh` = clean slate.
|
||||
|
||||
`form_template`: injected into the completion system prompt as guidance
|
||||
for what the persona should collect. Not rendered as UI — the LLM is
|
||||
told what to ask for.
|
||||
|
||||
`transition_rules.auto_assign`: `round_robin` assigns to team members
|
||||
in rotation. Null = unassigned (manual claim).
|
||||
|
||||
**Auth:** `workflow.create` permission required.
|
||||
|
||||
### Update Stage
|
||||
|
||||
```
|
||||
PUT /workflows/:id/stages/:sid
|
||||
```
|
||||
|
||||
Full replacement of stage fields.
|
||||
|
||||
**Auth:** `workflow.create` permission required.
|
||||
|
||||
### Delete Stage
|
||||
|
||||
```
|
||||
DELETE /workflows/:id/stages/:sid
|
||||
```
|
||||
|
||||
**Auth:** `workflow.create` permission required.
|
||||
|
||||
### Reorder Stages
|
||||
|
||||
```
|
||||
PATCH /workflows/:id/stages/reorder
|
||||
```
|
||||
|
||||
```json
|
||||
{ "ordered_ids": ["stage-uuid-2", "stage-uuid-1", "stage-uuid-3"] }
|
||||
```
|
||||
|
||||
Sets ordinals based on array position.
|
||||
|
||||
**Auth:** `workflow.create` permission required.
|
||||
|
||||
### Stage Object
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "uuid",
|
||||
"workflow_id": "uuid",
|
||||
"ordinal": 0,
|
||||
"name": "Collect Info",
|
||||
"persona_id": "uuid|null",
|
||||
"assignment_team_id": "uuid|null",
|
||||
"form_template": {},
|
||||
"history_mode": "full",
|
||||
"auto_transition": false,
|
||||
"transition_rules": {},
|
||||
"created_at": "..."
|
||||
}
|
||||
```
|
||||
|
||||
## Versions (Immutable Snapshots)
|
||||
|
||||
### Publish
|
||||
|
||||
```
|
||||
POST /workflows/:id/publish
|
||||
```
|
||||
|
||||
Creates an immutable snapshot of the current workflow definition, stages,
|
||||
and persona tool grants. Running instances are pinned to their version.
|
||||
|
||||
Returns 201 with the version object. Returns 409 if the current version
|
||||
number has already been published (edit the workflow to increment first).
|
||||
|
||||
**Auth:** `workflow.create` permission required.
|
||||
|
||||
### Get Version
|
||||
|
||||
```
|
||||
GET /workflows/:id/versions/:version
|
||||
```
|
||||
|
||||
Returns the version snapshot.
|
||||
|
||||
**Auth:** Authenticated.
|
||||
|
||||
### Version Object
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "uuid",
|
||||
"workflow_id": "uuid",
|
||||
"version_number": 2,
|
||||
"snapshot": { "workflow": {...}, "stages": [...] },
|
||||
"created_at": "..."
|
||||
}
|
||||
```
|
||||
|
||||
## Instances
|
||||
|
||||
A workflow **instance** is a channel with `type=workflow`. The channel's
|
||||
`workflow_id`, `workflow_version`, `current_stage`, `stage_data`, and
|
||||
`workflow_status` columns track instance state.
|
||||
|
||||
### Start Instance
|
||||
|
||||
```
|
||||
POST /workflows/:id/start
|
||||
```
|
||||
|
||||
Creates a new workflow channel, sets `current_stage=0`, pins the latest
|
||||
published version. Returns the channel object.
|
||||
|
||||
**Auth:** Authenticated (for team members). Visitors use the entry API.
|
||||
|
||||
### Get Instance Status
|
||||
|
||||
```
|
||||
GET /channels/:id/workflow/status
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"workflow_id": "uuid",
|
||||
"workflow_version": 2,
|
||||
"current_stage": 1,
|
||||
"stage_data": { "name": "Jane", "email": "jane@co.com" },
|
||||
"status": "active|completed|stale",
|
||||
"last_activity_at": "..."
|
||||
}
|
||||
```
|
||||
|
||||
**Auth:** Authenticated.
|
||||
|
||||
### Advance Stage
|
||||
|
||||
```
|
||||
POST /channels/:id/workflow/advance
|
||||
```
|
||||
|
||||
```json
|
||||
{ "data": { "name": "Jane", "email": "jane@co.com" } }
|
||||
```
|
||||
|
||||
Merges `data` into the channel's accumulated stage data and moves to
|
||||
the next stage. If the current stage is the last, marks the workflow
|
||||
as `completed` and sets `ai_mode=off`.
|
||||
|
||||
On completion, triggers `on_complete` chaining and webhook delivery
|
||||
if configured.
|
||||
|
||||
**Auth:** Authenticated.
|
||||
|
||||
### Reject (Go Back)
|
||||
|
||||
```
|
||||
POST /channels/:id/workflow/reject
|
||||
```
|
||||
|
||||
```json
|
||||
{ "reason": "Missing required field: email" }
|
||||
```
|
||||
|
||||
Moves back one stage. Cannot go below stage 0. `reason` is required —
|
||||
persisted as a system message in the channel history.
|
||||
|
||||
**Auth:** Authenticated.
|
||||
|
||||
### Workflow Advance Tool
|
||||
|
||||
The `workflow_advance` tool is available to personas during workflow
|
||||
completions. When the LLM determines enough data has been collected,
|
||||
it calls this tool to trigger the stage transition programmatically.
|
||||
The tool has a `RequireWorkflow` predicate — it only appears in
|
||||
workflow channels.
|
||||
|
||||
## Assignments
|
||||
|
||||
Human review queue for workflow stages that have an `assignment_team_id`.
|
||||
|
||||
### List My Assignments
|
||||
|
||||
```
|
||||
GET /workflow-assignments/mine
|
||||
```
|
||||
|
||||
Returns assignments where `assigned_to` is the current user or
|
||||
`status=unassigned` for the user's teams.
|
||||
|
||||
**Auth:** Authenticated.
|
||||
|
||||
### Claim Assignment
|
||||
|
||||
```
|
||||
POST /workflow-assignments/:id/claim
|
||||
```
|
||||
|
||||
Sets `assigned_to` to the current user, status to `claimed`.
|
||||
Returns 409 if the assignment is already claimed or not found.
|
||||
|
||||
**Auth:** Authenticated.
|
||||
|
||||
### Complete Assignment
|
||||
|
||||
```
|
||||
POST /workflow-assignments/:id/complete
|
||||
```
|
||||
|
||||
Marks assignment as `completed`. Does NOT auto-advance the stage —
|
||||
the human reviews and manually advances or the persona tool does it.
|
||||
Returns 409 if the assignment is not in `claimed` state.
|
||||
|
||||
**Auth:** Authenticated.
|
||||
|
||||
### List Team Assignments
|
||||
|
||||
```
|
||||
GET /teams/:teamId/assignments?status=unassigned
|
||||
```
|
||||
|
||||
Assignments for the team, filtered by `status` (default: `unassigned`).
|
||||
Valid values: `unassigned`, `claimed`, `completed`.
|
||||
|
||||
**Auth:** Authenticated (team member).
|
||||
|
||||
### Assignment Object
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "uuid",
|
||||
"channel_id": "uuid",
|
||||
"stage": 1,
|
||||
"team_id": "uuid",
|
||||
"assigned_to": "uuid|null",
|
||||
"status": "unassigned|claimed|completed",
|
||||
"created_at": "...",
|
||||
"claimed_at": "...|null",
|
||||
"completed_at": "...|null"
|
||||
}
|
||||
```
|
||||
|
||||
## Visitor Entry
|
||||
|
||||
Public-facing workflow entry for unauthenticated visitors.
|
||||
|
||||
### Landing Page
|
||||
|
||||
```
|
||||
GET /w/:id
|
||||
GET /w/:id/:slug
|
||||
```
|
||||
|
||||
Server-rendered branded page. Shows workflow name, tagline, branding
|
||||
colors, and a "Start" button. If the visitor has an existing session
|
||||
cookie for this workflow, resumes the conversation.
|
||||
|
||||
Not an API endpoint — returns HTML.
|
||||
|
||||
### Start Visitor Session
|
||||
|
||||
```
|
||||
POST /api/v1/workflow-entry/:scope/:slug
|
||||
```
|
||||
|
||||
No request body. Display name is auto-generated (`Visitor`,
|
||||
`Visitor 2`, etc.).
|
||||
|
||||
`:scope` is a team ID or `global`. Creates a session participant,
|
||||
creates a workflow channel, sets `allow_anonymous=true`. Returns:
|
||||
|
||||
```json
|
||||
{
|
||||
"channel_id": "uuid",
|
||||
"session_id": "uuid",
|
||||
"redirect_to": "/w/channel-uuid"
|
||||
}
|
||||
```
|
||||
|
||||
The session token is set as a cookie (`sb_session`) for subsequent
|
||||
requests. `redirect_to` is the workflow chat surface URL.
|
||||
|
||||
### Visitor Message/Completion
|
||||
|
||||
Visitors interact through the workflow channel using session-scoped routes:
|
||||
|
||||
```
|
||||
POST /api/v1/w/:id/messages
|
||||
GET /api/v1/w/:id/messages
|
||||
POST /api/v1/w/:id/completions
|
||||
```
|
||||
|
||||
These use `AuthOrSession` middleware — the session cookie identifies
|
||||
the visitor without requiring a JWT.
|
||||
|
||||
## Staleness Sweep
|
||||
|
||||
Background goroutine. Checks active workflow instances for staleness
|
||||
(no activity within `WORKFLOW_STALE_HOURS`, default 72). Stale instances
|
||||
are marked `workflow_status=stale`. Retention policy then applies:
|
||||
`archive` keeps the channel, `delete` removes it after `delete_after_days`.
|
||||
|
||||
## On-Complete Chaining
|
||||
|
||||
When a workflow completes and has `on_complete` configured:
|
||||
|
||||
```json
|
||||
{
|
||||
"on_complete": {
|
||||
"action": "start_workflow",
|
||||
"target_slug": "follow-up-survey",
|
||||
"data_map": { "name": "customer_name", "case_id": "case_id" }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`action` must be `start_workflow`. `target_slug` identifies the workflow
|
||||
to chain into (looked up in the same team scope). `data_map` is a flat
|
||||
key remapping from source stage data keys to target initial data keys.
|
||||
If `data_map` is empty/omitted, all stage data is passed through as-is.
|
||||
|
||||
The system starts a new instance of the target workflow, passing mapped
|
||||
stage data as initial context. Webhook delivery (if configured) fires
|
||||
before chaining.
|
||||
|
||||
## WebSocket Events
|
||||
|
||||
| Event | When |
|
||||
|-------|------|
|
||||
| `workflow.advanced` | Stage transition (includes new stage info) |
|
||||
| `workflow.completed` | Workflow finished |
|
||||
| `workflow.assigned` | New assignment created |
|
||||
| `workflow.claimed` | Assignment claimed by user |
|
||||
137
docs/ICD/workspaces.md
Normal file
137
docs/ICD/workspaces.md
Normal file
@@ -0,0 +1,137 @@
|
||||
# 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.
|
||||
|
||||
### 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": "..."
|
||||
}
|
||||
```
|
||||
|
||||
### 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": "..."
|
||||
}
|
||||
```
|
||||
|
||||
### 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
|
||||
```
|
||||
|
||||
### 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.
|
||||
|
||||
### 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" }
|
||||
```
|
||||
|
||||
### 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": "..."
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
Reference in New Issue
Block a user