mirror of
https://github.com/TencentCloud/Octop.git
synced 2026-10-20 06:19:17 +08:00
* feat(skills): shared packages, file tree editor, and BOM-safe frontmatter Add skill-package sharing (private by default), package skill file-tree APIs aligned with personalization, and strip UTF-8 BOM when parsing SKILL.md. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(tests): bump backup schema assertions to v22 Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: jubaoliang <jubaoliang@tencent.com> Co-authored-by: Cursor <cursoragent@cursor.com>
704 lines
45 KiB
Markdown
704 lines
45 KiB
Markdown
# API Reference
|
||
|
||
Every route is mounted under `/api`. JSON unless otherwise noted; SSE
|
||
endpoints emit `text/event-stream`; chat turns use a WebSocket
|
||
described in [Chat (WebSocket)](#chat-websocket).
|
||
|
||
> **Interactive docs** (Scalar) live at `/api/docs` when
|
||
> `enable_api_docs=true` (or `OCTOP_ENABLE_API_DOCS=1`). The schema
|
||
> is at `/api/openapi.json`.
|
||
|
||
## Authentication
|
||
|
||
| Header | Value |
|
||
|--------|-------|
|
||
| `Authorization` | `Bearer <access_token>` from `POST /api/auth/login` |
|
||
|
||
Tokens expire after `OCTOP_ACCESS_TOKEN_TTL` seconds (default 24 h).
|
||
Rotating the JWT secret (`octop admin rotate-jwt-secret`) invalidates
|
||
every outstanding token immediately. Login attempts are rate-limited
|
||
(`OCTOP_LOGIN_MAX_ATTEMPTS` / `OCTOP_LOGIN_LOCKOUT_SECONDS`); the
|
||
admin can clear the lockout with `POST /api/users/{id}/unlock-login`.
|
||
|
||
### Auth column legend
|
||
|
||
- **public** — no token required.
|
||
- **user** — any logged-in account.
|
||
- **owner** — same user that owns the resource (or admin).
|
||
- **admin** — admin role required.
|
||
|
||
### Public endpoints (no token)
|
||
|
||
`/api/docs`, `/api/openapi.json`, `/api/health`, `/api/setup/*`,
|
||
`/api/auth/login`, `/api/auth/captcha`, `/api/auth/oidc/status`, `/api/auth/oidc/start`,
|
||
`/api/auth/oidc/callback`, `/api/auth/oidc/exchange`, `/api/auth/oauth/status`,
|
||
`/api/auth/oauth/start`, `/api/auth/oauth/callback`, `/api/auth/oauth/exchange`,
|
||
`/api/auth/ldap/status`,
|
||
`/api/connectors/oauth/callback`, and `/api/internal/mcp/*`. All other routes are JWT-gated by
|
||
`api/middleware/jwt_auth.py`; the setup lockdown middleware
|
||
(`api/middleware/setup_lockdown.py`) additionally blocks non-setup
|
||
routes until the wizard finishes.
|
||
|
||
Password login may require a vendor captcha token. `GET /api/auth/captcha`
|
||
returns `{provider: "slider"}` by default (dashboard slider only; no server
|
||
check). When a strong provider is active (`turnstile`, `hcaptcha`,
|
||
`recaptcha`, `recaptcha-v3`, `tencent`), `POST /api/auth/login` must include
|
||
`captcha_token` (max 4096 characters; for `tencent` the dashboard sends the
|
||
callback pair as `ticket:randstr`, verified via GET with the client IP).
|
||
Captcha failures do not increment
|
||
login lockout. OIDC login is unchanged. A request for an unknown username
|
||
with a garbage token still triggers one outbound siteverify request (bounded
|
||
by a 10s timeout and the token length limit); a known locked user is
|
||
rejected before siteverify.
|
||
|
||
Offline recovery: delete the `captcha.settings` row via local CLI /
|
||
`settings_repo` if a settings-sourced strong provider is unreachable. If
|
||
the process refused to start because `OCTOP_CAPTCHA_PROVIDER` is strong
|
||
without both keys and no readable settings blob, edit `~/.octop/env` on
|
||
disk — the dashboard and `PUT /api/envs` are unreachable until the
|
||
process starts. `GET /api/envs` redacts `OCTOP_CAPTCHA_SECRET` as
|
||
`********`; sending that sentinel on PUT keeps the on-disk value.
|
||
|
||
If you serve the dashboard behind a CSP, allow
|
||
`challenges.cloudflare.com`; `js.hcaptcha.com`, `newassets.hcaptcha.com`,
|
||
`api.hcaptcha.com`; `www.google.com`, `www.gstatic.com`;
|
||
`turing.captcha.qcloud.com`, `captcha.qq.com`, `ssl.captcha.qq.com`. Octop
|
||
does not set these headers itself.
|
||
|
||
## Setup & auth
|
||
|
||
| Method | Path | Auth | Notes |
|
||
|--------|------|------|-------|
|
||
| `GET` | `/setup/status` | public | `{required, completed, has_admin}` |
|
||
| `GET` | `/setup/presets` | public | Provider templates for the wizard |
|
||
| `POST` | `/setup/begin` | public | Begin a wizard session (no password) |
|
||
| `POST` | `/setup/verify-password` | public | Exchange the wizard password for a session token |
|
||
| `GET` | `/setup/validate-token` | public | Check a wizard session token |
|
||
| `POST` | `/setup/initial-admin` | public | body `{username, password, display_name?, email?}` → `201` |
|
||
| `POST` | `/setup/test-provider` | public | Ping a provider draft (kind/base_url/api_key/model) |
|
||
| `POST` | `/setup/finish` | public | Finalise setup and unlock the rest of the API |
|
||
| `GET` | `/auth/captcha` | public | `{provider, site_key?}` for the login widget; 503 while setup is required |
|
||
| `POST` | `/auth/login` | public | body `{username, password, captcha_token?}` (`username` may be email) → `{access_token, role, user, ...}`. When no local password matches, the credentials are retried against the configured LDAP directory; a first directory login provisions an Octop account (unless `auto_provision` is off) and derives the role from `admin_groups` |
|
||
| `GET` | `/auth/oidc/status` | public | OIDC login availability and provider display name |
|
||
| `POST` | `/auth/oidc/start` | public | body `{redirect_after?}` → identity-provider authorization URL |
|
||
| `GET` | `/auth/oidc/callback` | public | Identity-provider callback; redirects to dashboard login completion |
|
||
| `POST` | `/auth/oidc/exchange` | public | body `{code}` → same JWT response as `/auth/login` |
|
||
| `GET` | `/auth/oidc/config` | admin | OIDC provider configuration and callback URL; client secret is omitted |
|
||
| `PUT` | `/auth/oidc/config` | admin | Write OIDC provider configuration; `client_secret` is write-only |
|
||
| `POST` | `/auth/oidc/config/test` | admin | Verify configured discovery metadata and JWKS endpoint |
|
||
| `GET` | `/auth/oauth/status` | public | `{providers:[{kind, display_name, enabled}]}` |
|
||
| `POST` | `/auth/oauth/start` | public | body `{kind, redirect_after?}` → authorization URL |
|
||
| `GET` | `/auth/oauth/callback` | public | App OAuth callback (`code` or DingTalk `authCode`); same completion page as OIDC |
|
||
| `POST` | `/auth/oauth/exchange` | public | Same one-time code exchange as `/auth/oidc/exchange` |
|
||
| `POST` | `/auth/oauth/bind/start` | user | body `{kind, redirect_after?}` → bind the identity to the current user |
|
||
| `POST` | `/auth/oauth/unbind` | user | Unlink one SSO identity by `kind` (requires a local password when it is the last login method) |
|
||
| `GET` | `/auth/oauth/providers/{kind}` | admin | Provider config; `kind` is `oidc`, `feishu`, `dingtalk`, or `wecom` |
|
||
| `PUT` | `/auth/oauth/providers/{kind}` | admin | Upsert provider config; `client_secret` is write-only; WeCom uses `extra.agent_id` |
|
||
| `POST` | `/auth/oauth/providers/{kind}/test` | admin | Test provider credentials |
|
||
| `GET` | `/auth/ldap/status` | public | `{enabled, display_name}` for the login page hint |
|
||
| `GET` | `/auth/ldap/config` | admin | Directory settings; `bind_password` is never returned (`has_bind_password` instead) |
|
||
| `PUT` | `/auth/ldap/config` | admin | Upsert directory settings; `bind_password` is write-only (omit to keep the stored one) |
|
||
| `POST` | `/auth/ldap/config/test` | admin | Service bind + probe of `user_base_dn` → `{ok, detail}` |
|
||
| `POST` | `/auth/logout` | user | `204` |
|
||
| `GET` | `/auth/me` | user | `{id, username, role, display_name, locale, ...}` |
|
||
| `PATCH` | `/auth/me` | user | body `{display_name?, locale?, ...}` |
|
||
| `POST` | `/auth/change-password` | user | body `{old_password, new_password}` → `204`. Accounts without a local password (directory/SSO-provisioned) get `PASSWORD_NOT_SET` |
|
||
| `GET` | `/health` | public | `{status: "ok", version}` |
|
||
|
||
## Users (admin)
|
||
|
||
| Method | Path | Auth | Notes |
|
||
|--------|------|------|-------|
|
||
| `GET` | `/users` | admin | `[{id, username, role, display_name, email, enabled, ...}]` |
|
||
| `POST` | `/users` | admin | body `{username, password, role, display_name?, email?, permissions?, workspace_root_dir?, token_quota?}` → `201` |
|
||
| `GET` | `/users/{id}` | admin | full user row |
|
||
| `PATCH` | `/users/{id}` | admin | body subset of `{role, display_name, email, enabled, locale}` |
|
||
| `POST` | `/users/{id}/reset-password` | admin | body `{new_password}` → `204` |
|
||
| `POST` | `/users/{id}/unlock-login` | admin | `204` (clears the lockout) |
|
||
| `DELETE` | `/users/{id}` | admin | `204` |
|
||
|
||
## Agents
|
||
|
||
| Method | Path | Auth | Notes |
|
||
|--------|------|------|-------|
|
||
| `GET` | `/agents` | user | `[{id, agent_id, name, persona_mbti, state, unread_count, ...}]` |
|
||
| `POST` | `/agents` | user | body `{name, persona_mbti?, default_model?, system_prompt?, description?, icon?, template_name?, config?}` → `201` |
|
||
| `GET` | `/agents/{id}` | owner | full agent row |
|
||
| `PATCH` | `/agents/{id}` | owner | body subset of create body |
|
||
| `DELETE` | `/agents/{id}` | owner | `204` |
|
||
| `POST` | `/agents/{id}/start` | owner | `204` |
|
||
| `POST` | `/agents/{id}/stop` | owner | `204` |
|
||
| `POST` | `/agents/{id}/reload` | owner | `204` (rebuild harness runtime) |
|
||
| `POST` | `/agents/{id}/read` | owner | `204` (mark unread badge cleared) |
|
||
| `GET` | `/agents/{id}/status` | owner | `{state, last_error?, memory_maintenance?, ...}` |
|
||
| `POST` | `/agents/from-expert/{expert_id}` | user | body `{name, ...}` → `201` (creates from bundled expert template) |
|
||
| `GET` | `/agents/{id}/tool-settings` | owner | built-in + installed plugin tools with enable / disableable / available flags |
|
||
| `PUT` | `/agents/{id}/tool-settings` | owner | body `{disabled_builtin: string[], plugins?}` — persists denylist + plugin flags (hot-sync, no reload) |
|
||
| `PATCH` | `/agents/{id}/tool-settings/{tool_name}` | owner | body `{enabled, source, plugin_id?}` — toggle one tool (hot-sync) |
|
||
|
||
## Skills and skill packages
|
||
|
||
Skill copy operations create snapshots; they do not keep the source and
|
||
destination synchronized after the request completes.
|
||
|
||
Packages default to private (`shared=false`): only the creator (and admins) can
|
||
list or open them. When `shared=true`, other users with the `skill_packages`
|
||
permission can view and mount them; only the creator/admin can edit. `copy_policy`
|
||
and `allow_download` still apply to users who can already view the package.
|
||
|
||
| Method | Path | Auth | Notes |
|
||
|--------|------|------|-------|
|
||
| `GET` | `/skill-packages` | user (`skill_packages`) | Own packages + shared packages; admins see all |
|
||
| `GET` | `/skill-packages?writable_only=true` | user (`skill_packages`) | List only packages the current user may modify; admins may modify all packages |
|
||
| `POST` | `/agents/{id}/skill-packages/{package_id}/copy` | owner (`skill_packages`) | body `{skill_slugs: string[], overwrite?: boolean}`; copy selected package skills into the workspace |
|
||
| `POST` | `/agents/{id}/skills/{slug}/push-to-package` | owner (`skill_packages`) | body `{package_id, overwrite?: boolean}`; copy a workspace skill into a package created by the user (or any package for admins) |
|
||
|
||
## Chat (WebSocket)
|
||
|
||
| Path | Auth | Notes |
|
||
|------|------|-------|
|
||
| `WS /agents/{id}/chat/ws?token=<jwt>` | owner | Primary dashboard turn endpoint. Send `{"type":"user_turn", ...}` frames (optional `hitl_policy` is a thread-scoped tool-approval bypass); server replies with harness stream chunks ending in `{"type":"done"}` or `{"type":"error","message":"..."}`. `{"type":"ping"}` → `{"type":"pong"}`. `{"type":"subscribe","thread_id"}` → `{"type":"turn_status","thread_id","active"}` plus a `steer.queue_snapshot` baseline (attach to an in-flight turn without cancelling on disconnect). `{"type":"cancel","thread_id"}` stops the active turn and discards its queued steers (explicit stop; disconnect alone does **not** cancel). See [Mid-turn steering](#mid-turn-steering) for steer frames. |
|
||
| `GET /agents/{id}/chat/welcome` | agent access | `{welcome_message, quick_prompts, task_examples}`; `task_examples` is `null` when the workspace field is absent |
|
||
| `POST /agents/{id}/chat/polish` | owner | body `{text, default_model?}` → `{text}` (one-shot prompt refinement) |
|
||
| `POST /agents/{id}/chat/hitl/resume` | owner | body `{thread_id, decisions: [...], hitl_policy?}` → SSE chunk stream; finishes with `{"type":"done"}`. When the resume stops on another approval, the done frame is `{"type":"done","hitl_paused":true}` and parked steers stay queued. Optional `hitl_policy` (`ask` / `allow_all` / `allow_tools`) is a thread-scoped bypass and does not change global tool-approval settings. |
|
||
|
||
Model calls retain LangChain's default retry eligibility and the configured
|
||
retry budget/backoff. Exhausted calls raise and remain failed
|
||
checkpoint tasks, without a synthetic assistant reply. WebUI, IM and team tasks
|
||
display the failure separately. Team rooms publish an `error` frame followed by
|
||
`done`; stored error bubbles carry `additional_kwargs.octop_stream_error` for
|
||
history display. These diagnostics and automatic team failure notifications do
|
||
not become long-term memory.
|
||
|
||
### Legacy SSE
|
||
|
||
The previous `POST /agents/{id}/chat/stream` is gone. The dashboard
|
||
streams turns over the WebSocket above; HITL resume stays on SSE
|
||
because each request is a one-shot continuation.
|
||
|
||
### Mid-turn steering
|
||
|
||
While a turn is active, a `user_turn` frame may carry `delivery`
|
||
(`auto` \| `steer` \| `follow_up`), `client_pending_id` (client UUID for
|
||
correlation) and `expected_turn_id` (the `turn.started` id, for optimistic
|
||
concurrency). The server is authoritative: busy `auto`/`steer` steers the
|
||
active turn (queued server-side, bounded by depth and byte caps); idle `auto`
|
||
starts a normal new turn; `follow_up` while busy is rejected; new steers are
|
||
rejected while a HITL card is pending. Replies arrive on the thread:
|
||
|
||
| Frame | Meaning |
|
||
|-------|---------|
|
||
| `turn.started` / `turn.resumed` / `turn.ended` | logical turn lifecycle; `turn.ended` carries `hitl_paused: true` when steers stay parked across an approval pause |
|
||
| `steer.ack` | direct reply on the sending socket (`queued` / `rejected` / `removed`, with `reason`) |
|
||
| `steer.queued` | steer accepted into the active turn queue (`pending_id`, `queue_length`, `preview`) |
|
||
| `steer.drained` | queued steers injected as a user message; the same turn continues (`turn.resumed` follows) |
|
||
| `steer.discarded` | queue dropped (`turn_cancelled` / `turn_failed` / `continuation_exhausted` / `park_expired`) |
|
||
| `steer.removed` / `steer.rejected` | `steer_remove` confirmation / structured rejection |
|
||
| `steer.queue_snapshot` | server-authoritative queue baseline (on subscribe) |
|
||
|
||
Drain happens only at safe boundaries: when tools have run, the first model
|
||
output after the completed tool batch preempts the stream (that chunk and
|
||
anything after it, except `done` / `error`, are not forwarded) and the queued
|
||
steers continue the same logical turn; after a pure-text answer steers drain
|
||
at the natural turn end. Image and file blocks on the `user_turn` are queued
|
||
with the text and replayed on drain. Steers parked across an HITL pause drain
|
||
after the approval resumes, and only when that resume does not stop on a new
|
||
approval card. A park older than 30 minutes is discarded (`park_expired`).
|
||
`{"type":"steer_remove",...}` withdraws one queued item before it drains. The client puts the draft back only after `steer.ack` with `status: "removed"` and without `already_drained`; an already-drained item stays in the continuation.
|
||
|
||
## Threads & history
|
||
|
||
| Method | Path | Auth | Notes |
|
||
|--------|------|------|-------|
|
||
| `GET` | `/agents/{id}/chat/sessions` | owner | `[{id, thread_id, title, archived, last_active, unread, ...}]` |
|
||
| `POST` | `/agents/{id}/chat/sessions` | owner | body `{session_key?}` → `{thread_id, session_key}` |
|
||
| `PATCH` | `/agents/{id}/chat/sessions/{thread_id}` | owner | body `{title?, pinned?}` → updated row |
|
||
| `DELETE` | `/agents/{id}/chat/sessions/{thread_id}` | owner | `204` (archives the active row) |
|
||
| `GET` | `/agents/{id}/chat/sessions/{thread_id}/history` | owner | paginated message history; `turn_active` tells a reconnecting client whether to re-`subscribe` over the chat WebSocket |
|
||
|
||
**Tool result blocks.** `tool_result` blocks in history (and `tool_result`
|
||
frames on the chat WebSocket) carry the tool's return value in `output`. When a
|
||
plugin returns a large `octop_ui` payload, the backend offloads the envelope's
|
||
`data` field: `output` then contains the slim envelope with
|
||
`data_ref: "artifact"`, and the full payload is on the block's `artifact` key
|
||
(absent otherwise). An explicit `data` key always takes precedence over
|
||
`data_ref` when both appear. Clients that render plugin UIs must resolve
|
||
`data_ref` from `artifact`; clients that only read `output` keep working.
|
||
|
||
### Trajectory ledger
|
||
|
||
| Method | Path | Auth | Notes |
|
||
|--------|------|------|-------|
|
||
| `GET` | `/agents/{id}/threads/{thread_id}/trajectory` | owner | paginated event summaries; use `before_seq` for older rows |
|
||
| `GET` | `/agents/{id}/threads/{thread_id}/trajectory/events/{event_id}` | owner | full event payload |
|
||
| `GET` | `/agents/{id}/threads/{thread_id}/trajectory/metrics` | owner | aggregated turn, timing, and token metrics |
|
||
| `GET` | `/agents/{id}/threads/{thread_id}/trajectory/stream` | owner | live SSE events; resume with `after_seq` or `Last-Event-ID` |
|
||
| `GET` | `/agents/{id}/threads/{thread_id}/trajectory/export` | owner | full ledger download as JSONL (default) or JSON |
|
||
|
||
## Channels
|
||
|
||
| Method | Path | Auth | Notes |
|
||
|--------|------|------|-------|
|
||
| `GET` | `/agents/{aid}/channels` | owner | list of channel rows |
|
||
| `POST` | `/agents/{aid}/channels` | owner | body `{kind, name, config}` → `201` |
|
||
| `GET` | `/agents/{aid}/channels/{cid}` | owner | channel row |
|
||
| `PATCH` | `/agents/{aid}/channels/{cid}` | owner | body subset → updated row |
|
||
| `DELETE` | `/agents/{aid}/channels/{cid}` | owner | `204` |
|
||
| `POST` | `/agents/{aid}/channels/{cid}/test` | owner | `{ok, error?}` (instantiate → start → stop) |
|
||
| `POST` | `/agents/{aid}/channels/probe` | owner | `{ok, reason?, detail?}` — preflight a candidate config |
|
||
| `POST` | `/agents/{aid}/channels/{platform}/qrcode/generate` | owner | platform-specific bot creator (wecom, weixin, feishu, yuanbao) |
|
||
| `POST` | `/agents/{aid}/channels/{platform}/qrcode/poll` | owner | poll bot creator state |
|
||
| `POST` | `/agents/{aid}/channels/{platform}/bot-creator/start` | owner | start a bot-creator flow |
|
||
| `POST` | `/agents/{aid}/channels/{platform}/bot-creator/poll` | owner | poll progress |
|
||
| `POST` | `/agents/{aid}/channels/{platform}/bot-creator/stop` | owner | stop an in-flight bot creator |
|
||
|
||
## Cron
|
||
|
||
| Method | Path | Auth | Notes |
|
||
|--------|------|------|-------|
|
||
| `GET` | `/settings/timezone` | user | process-level `{timezone}` from `default_timezone` |
|
||
| `GET` | `/settings/upload` | user | `{max_upload_mb, max_upload_bytes}` from `max_upload_mb` |
|
||
| `GET` | `/settings/hitl` | user | `{enabled, tool_guard_require_approval, show_approval_ui}` — `show_approval_ui` is true when tool HITL is on or command guard is `require_approval` |
|
||
| `GET` | `/settings/captcha` | `captcha` | `{active, available, providers, source, v3_min_score}`; secrets omitted |
|
||
| `PUT` | `/settings/captcha` | `captcha` | merge `{active?, providers?}`; empty secret keeps ciphertext; `null` removes a pair |
|
||
| `GET` | `/cron/settings` | user | compat alias of `/settings/timezone` |
|
||
| `GET` | `/agents/{aid}/cron/examples` | agent access | `{task_examples}` from workspace `.octop/manifest.json` (`{zh,en}` string arrays, display-normalized to 3 or 6); `null` if the field is absent (dashboard keeps default cards). Prefer `GET /agents/{aid}/chat/welcome` which includes the same field. |
|
||
| `GET` | `/agents/{aid}/cron` | owner only | list cron rows; non-owners (including admin) get `[]` |
|
||
| `POST` | `/agents/{aid}/cron` | owner only | body `{name?, trigger, prompt, session_key?, fresh_thread?, enabled?, model?, task_type?}` → `201` |
|
||
| `GET` | `/agents/{aid}/cron/{cid}` | owner only | cron row |
|
||
| `PATCH` | `/agents/{aid}/cron/{cid}` | owner only | body subset → updated row |
|
||
| `DELETE` | `/agents/{aid}/cron/{cid}` | owner only | `204` |
|
||
| `POST` | `/agents/{aid}/cron/{cid}/run-now` | owner only | `204` (fire immediately, off-schedule) |
|
||
|
||
`task_type` is `"text"` (push prompt directly to the session) or
|
||
`"agent"` (run the prompt through the LLM and push the reply).
|
||
Default: `"agent"`. `trigger` accepts cron expressions
|
||
(`"0 9 * * *"`) plus the `interval:N` / `date:ISO8601` aliases
|
||
documented in `infra/cron/trigger.py`. A `date:` job runs once and
|
||
then disables itself, whether the run succeeds or fails; the row stays
|
||
in the list. If the process is down for more than 24 hours past that
|
||
time and the job never ran, `last_status` is `missed` and the job is
|
||
disabled. `prompt` must be non-empty and ≤ 2000 characters. `name` is
|
||
an optional display label; when omitted, the server derives one from
|
||
`prompt`.
|
||
|
||
## Providers
|
||
|
||
| Method | Path | Auth | Notes |
|
||
|--------|------|------|-------|
|
||
| `GET` | `/providers` | user | providers visible to the user (own + shared) |
|
||
| `POST` | `/providers` | user | body `{name, kind, base_url?, api_key?, model?, ...}` → `201` |
|
||
| `PATCH` | `/providers/{id}` | owner | body subset → updated row |
|
||
| `DELETE` | `/providers/{id}` | owner | `204` (refuses if any agent references it) |
|
||
| `POST` | `/providers/{id}/test` | user | `{ok, latency_ms?, error?}` (one-token ping with 10 s timeout) |
|
||
| `POST` | `/admin/providers` | admin | same body as user POST; row has `user_id = NULL` |
|
||
| `PATCH` | `/admin/providers/{id}` | admin | as user PATCH but works on shared rows |
|
||
| `DELETE` | `/admin/providers/{id}` | admin | `204` |
|
||
|
||
## Models
|
||
|
||
| Method | Path | Auth | Notes |
|
||
|--------|------|------|-------|
|
||
| `GET` | `/models/presets` | user | provider templates from `octop-harness` |
|
||
| `GET` | `/models` | user | resolved models across enabled providers |
|
||
| `GET` | `/models/active` | user | `{provider_name, model}` |
|
||
| `PUT` | `/models/active` | admin | body `{provider_name, model}` |
|
||
|
||
### Media generation models
|
||
|
||
These instance-wide endpoints require the `providers` permission (administrators bypass
|
||
permission checks). The Ark API key is write-only and encrypted at rest. Saving settings
|
||
reloads running agents so the image and video tools receive the new configuration.
|
||
|
||
| Method | Path | Auth | Notes |
|
||
|--------|------|------|-------|
|
||
| `GET` | `/admin/media-generation` | providers | Return Volcengine Ark image/video settings; never returns the API key |
|
||
| `PUT` | `/admin/media-generation` | providers | Save enabled tools and Seedream/Seedance model IDs; an included API key is verified before saving |
|
||
| `POST` | `/admin/media-generation/test` | providers | Test credentials or a selected image/video model; model tests submit real, potentially billable requests |
|
||
|
||
## Voice
|
||
|
||
| Method | Path | Auth | Notes |
|
||
|--------|------|------|-------|
|
||
| `GET` | `/voice/presets` | user | voice provider presets |
|
||
| `GET` | `/voice/providers` | user | user's voice providers |
|
||
| `GET` | `/voice/active` | user | active TTS / STT configuration |
|
||
| `PUT` | `/voice/active` | user | update active voice configuration |
|
||
| `POST` | `/voice/stt` | user | body `{audio, format?, language?}` → `{text, segments?}` |
|
||
| `POST` | `/voice/tts` | user | body `{text, voice?, format?}` → audio bytes |
|
||
| `GET`/`POST`/`PATCH`/`DELETE` | `/admin/voice/providers` | admin | admin voice provider CRUD |
|
||
|
||
## MBTI & personas
|
||
|
||
| Method | Path | Auth | Notes |
|
||
|--------|------|------|-------|
|
||
| `GET` | `/mbti/codes` | user | `[{code: "INTJ", ...}, ...]` (16 codes + `_default`) |
|
||
| `GET` | `/mbti/codes/{code}` | user | full profile (dimensions, behaviour, UI metadata) |
|
||
| `GET` | `/mbti/preview/{code}` | user | rendered persona template (legacy `/api/personas/{code}`) |
|
||
| `PUT` | `/agents/{aid}/mbti` | owner | body `{code}` → apply persona and reload |
|
||
| `GET` | `/personas` | user | `[{code}, ...]` (compat shim) |
|
||
| `GET` | `/personas/{code}` | user | rendered template (compat shim) |
|
||
|
||
Persona content lives in `src/octop/infra/agents/persona/mbti_profiles.py` —
|
||
see [Personas](./personas.md).
|
||
|
||
## Experts
|
||
|
||
| Method | Path | Auth | Notes |
|
||
|--------|------|------|-------|
|
||
| `GET` | `/experts` | user | bundled expert catalog (includes `task_examples` `{zh,en}` when present) |
|
||
| `GET` | `/experts/{expert_id}` | user | full expert template (SOUL.md, skills, files, `task_examples`) |
|
||
| `POST` | `/agents/from-expert/{expert_id}` | user | body `{name, locale?, quick_prompts?, ...}` → `201`; optional `quick_prompts` overwrites workspace cards after seed |
|
||
|
||
Bundled experts live in `src/octop/infra/agents/experts/library/`
|
||
(en/zh divisions); the catalog is locale-aware via
|
||
`Accept-Language` / user preference.
|
||
|
||
## Workspace, skills, subagents, memory, files
|
||
|
||
| Method | Path | Auth | Notes |
|
||
|--------|------|------|-------|
|
||
| `GET` | `/agents/{aid}/workspace/tree` | owner | list dir (default `.`) |
|
||
| `GET` | `/agents/{aid}/workspace/file` | owner | read a content file |
|
||
| `PUT` | `/agents/{aid}/workspace/file` | owner | write a content file (via `BackendWorkspace`) |
|
||
| `DELETE` | `/agents/{aid}/workspace/file` | owner | delete a content file |
|
||
| `POST` | `/agents/{aid}/workspace/rename` | owner | rename / move |
|
||
| `POST` | `/agents/{aid}/workspace/upload` | owner | multipart upload → backend (not `max_upload_mb`) |
|
||
| `GET` | `/agents/{aid}/workspace/download` | owner | download a file |
|
||
| `GET` | `/agents/{aid}/workspace/glob` | owner | glob backend paths |
|
||
| `GET` | `/agents/{aid}/workspace/grep` | owner | grep backend files |
|
||
| `GET` | `/agents/{aid}/workspace/...` | owner | see `api/routers/workspace.py` |
|
||
| `GET` | `/agents/{aid}/skills` | owner | list installed skills |
|
||
| `GET` | `/agents/{aid}/skills/{slug}/export` | owner | download one workspace skill as a ZIP (`{slug}/SKILL.md` plus siblings) |
|
||
| `POST` | `/agents/{aid}/skills/export` | owner | body `{slugs: string[]}`; one ZIP, each slug a top-level folder |
|
||
| `GET` | `/skill-packages/{id}/export` | user (`skill_packages`) | download every skill in the package as one ZIP. `allow_download=false` returns 403 for users other than the creator and admins |
|
||
| `PUT` | `/agents/{aid}/skills/{slug}` | owner | enable / disable a skill |
|
||
| `GET` | `/agents/{aid}/skills/hub/search` | user | Skill Hub search |
|
||
| `GET` | `/agents/{aid}/skills/hub/rankings` | user | Skill Hub rankings |
|
||
| `POST` | `/agents/{aid}/skills/hub/install` | owner | body `{slug, version?}` → `201` |
|
||
| `GET` | `/subagent-catalog/divisions` | user | bundled subagent divisions |
|
||
| `GET` | `/subagent-catalog` | user | bundled subagent catalog |
|
||
| `GET` | `/subagent-catalog/{slug}` | user | full subagent definition |
|
||
| `GET` | `/agents/{aid}/subagents` | owner | installed subagents for an agent |
|
||
| `POST` | `/agents/{aid}/subagents` | owner | install a bundled subagent |
|
||
| `GET` | `/agents/{aid}/heartbeat-config` | owner | read heartbeat YAML |
|
||
| `PUT` | `/agents/{aid}/heartbeat-config` | owner | write heartbeat YAML |
|
||
| `GET` | `/agents/{aid}/memory/daily` | owner | list daily memory files |
|
||
| `GET`/`PUT` | `/agents/{aid}/memory/strong` | owner | read/edit user-maintained MEMORY.md, including for stopped agents; new sessions snapshot full content once, existing sessions keep their snapshot |
|
||
| `GET`/`PUT` | `/agents/{aid}/memory/user` | owner | read/edit user-maintained USER.md with the same session snapshot policy; distinct from the derived profile |
|
||
| `GET` | `/agents/{aid}/memory/manual-review` | owner | read persisted reconciliation status against current documents; no model call or memory mutation |
|
||
| `POST` | `/agents/{aid}/memory/manual-review` | owner | reconcile saved USER.md + MEMORY.md against up to 100 unchecked atoms; duplicates remain stored, proven conflicts retire the old atom and create a replacement review candidate with evidence |
|
||
| `GET` | `/agents/{aid}/memory/daily/{filename}` | owner | read one daily memory |
|
||
| `DELETE` | `/agents/{aid}/memory/daily/{filename}` | owner | delete one daily memory |
|
||
| `GET`/`POST` | `/memory/...` | user | memory API (dashboard memory tab) |
|
||
|
||
### Manual document reconciliation
|
||
|
||
The manual editors save files first, then request reconciliation. The response reports
|
||
`complete`, `partial` (continue checking), `uncertain`, `unavailable`, `failed`, `stale`,
|
||
`disabled`, `too_large`, or `not_checked`, plus conflict/duplicate counts and candidate IDs.
|
||
Semantic checks use the configured memory auxiliary model and require literal document
|
||
evidence; unavailable or uncertain checks never silently retire facts. Combined document
|
||
input above 100,000 characters is explicitly refused. Current documents are checked again
|
||
when accepting a document-derived candidate; stale candidates require review of the new
|
||
version. The file save remains successful if the subsequent semantic check fails.
|
||
|
||
### Current-expert memory storage maintenance
|
||
|
||
SQLite restore requires a harness runtime with `MemoryMiddleware.pause_background`
|
||
([octop-harness #69](https://github.com/TencentCloud/octop-harness/pull/69)). Older runtimes
|
||
reject restore before stopping the expert or changing its database. Independent retry
|
||
controls also require this runtime; the extraction config API reports supported options.
|
||
|
||
| Method | Path | Auth | Notes |
|
||
|--------|------|------|-------|
|
||
| `GET` | `/agents/{aid}/memory/storage/check` | owner/admin | Read-only inspection; backend, scope, per-expert statistics and cleanup preview |
|
||
| `GET` | `/agents/{aid}/memory/storage/maintenance` | owner/admin | Current progress and this expert's last result, retained until host restart |
|
||
| `POST` | `/agents/{aid}/memory/storage/maintenance` | owner/admin | Body `{confirmed: true}` → `202`; run one host-owned maintenance job |
|
||
| `GET` | `/agents/{aid}/memory/storage/backups` | owner/admin | List this expert’s maintenance and pre-restore SQLite backups from disk |
|
||
| `DELETE` | `/agents/{aid}/memory/storage/backups` | owner/admin | Body `{confirmed: true, backup_id: "server-generated filename"}` → `204`; permanently delete only the selected backup; rejected while maintenance/restore is active |
|
||
| `POST` | `/agents/{aid}/memory/storage/restore` | owner/admin | Body `{confirmed: true, backup_id: "server-generated filename"}` → `202`; back up current state, restore and reload only this expert |
|
||
|
||
The job waits for the selected expert to become idle and reserves only that
|
||
expert's turn admission. Ownership and storage isolation are checked again after
|
||
waiting. Other experts' conversations can continue. Shared SQLite files and
|
||
colliding PostgreSQL namespaces reject maintenance.
|
||
|
||
SQLite creates a full file backup before checkpoint deduplication and compaction.
|
||
SQLite restore accepts only regular sibling backup files matching the host’s naming scheme; arbitrary paths and symlinks are rejected. It validates SQLite integrity, namespace ownership and checkpoint references, drains this expert’s background workers, closes its runtime, and creates and fsyncs a full current-state backup before overwriting. SQLite’s backup API manages the database and WAL; files are not blindly replaced. A failed post-restore validation rolls back from the safety snapshot. The expert is recreated before waiting conversations are released. Backups remain discoverable after restart. The control-plane conversation lists/message archives, attachments and configuration are not restored.
|
||
Its inspection uses a separate read-only connection and a consistent snapshot.
|
||
`memory_counts` reports only the selected namespace's logical categories, excluding
|
||
full-text index duplicates. `checkpoint_counts` reports checkpoints, writes, shared
|
||
content blocks and distinct session IDs in a dedicated expert file. Shared files
|
||
omit checkpoint counts because those tables have no expert namespace, and disable
|
||
file maintenance, including when residual foreign namespaces are detected.
|
||
Each count has `rows`, `status` (`ok`, `missing`, `timed_out`) and `elapsed_ms`;
|
||
unavailable counts are null, never zero. Count queries share a five-second budget.
|
||
`elapsed_ms` and `file_check_ms` measure server-side inspection only, excluding
|
||
network transfer; they are not a performance benchmark. Completed SQLite jobs
|
||
include `inspection_before` and `inspection_after` snapshots in `report`, alongside
|
||
file/WAL totals, deduplication statistics and backup size. Counts may remain stable
|
||
because maintenance preserves history; concurrent background writes may affect them.
|
||
|
||
PostgreSQL limits every read/delete to the live expert's verified namespace and
|
||
backs up exact target rows to a private JSONL file under
|
||
`OCTOP_HOME/backups/memory-maintenance/` before committing a single transaction:
|
||
|
||
- Rejected candidates decided more than 30 days ago, with no atom referencing them.
|
||
- Operational journal entries older than 14 days (`extract_run`, `gc_*`,
|
||
`page_regen`, `page_regen_failed`; only the fixed action allowlist is eligible).
|
||
- At most 500 rows per category per pass; locked rows are skipped and another pass
|
||
can process remaining eligible records.
|
||
|
||
Active memories, raw evidence, decision journals (including consolidation), and
|
||
all checkpoints remain intact. PostgreSQL results report deleted rows and backup
|
||
size, **not physical disk reclamation**. No shared-table `VACUUM` or host-wide
|
||
orphan sweep runs from this page. Row-byte statistics are estimates that exclude
|
||
indexes, shared overhead and checkpoints. The JSONL header identifies the format,
|
||
namespace and row count; subsequent records contain table names and original rows
|
||
excluding generated search columns. PostgreSQL JSONL restore is not supported by this endpoint.
|
||
|
||
To verify the PostgreSQL isolation/backup/rollback gate against a dedicated test
|
||
database, set `OCTOP_MEMORY_MAINTENANCE_TEST_DSN` and run
|
||
`uv run pytest tests/integration/test_postgres_memory_storage.py -q`. Never point
|
||
this gate at production: it creates test namespaces and a temporary failure trigger.
|
||
|
||
## ACP (Agent Client Protocol)
|
||
|
||
| Method | Path | Auth | Notes |
|
||
|--------|------|------|-------|
|
||
| `GET` | `/acp` | user | current user's global runner list |
|
||
| `PUT` | `/acp` | user | replace global runners |
|
||
| `GET` | `/acp/{runner_name}` | user | one runner |
|
||
| `PUT` | `/acp/{runner_name}` | user | upsert one runner |
|
||
| `DELETE` | `/acp/{runner_name}` | user | delete a custom runner (built-ins are protected) |
|
||
| `GET` | `/agents/{aid}/acp` | owner | global runners + this agent's `tool_enabled` flag |
|
||
| `PUT` | `/agents/{aid}/acp` | owner | update `tool_enabled` and optionally the global list |
|
||
| `PUT` | `/agents/{aid}/acp/tool` | owner | toggle the `acp_runner` tool only |
|
||
|
||
See [ACP integration](./acp.md) for the runner object schema and the
|
||
Zed setup example.
|
||
|
||
## Storage backends
|
||
|
||
| Method | Path | Auth | Notes |
|
||
|--------|------|------|-------|
|
||
| `GET`/`POST`/`PATCH`/`DELETE` | `/storage-backends` | user | per-user remote backend connections |
|
||
| `GET`/`POST`/`PATCH`/`DELETE` | `/admin/storage-backends` | admin | admin-managed backends |
|
||
|
||
## Host filesystem (dashboard)
|
||
|
||
Browse the host OS directory tree when configuring a local backend
|
||
`root_dir` (`local_shell` / `filesystem`). Authenticated users only;
|
||
sensitive mounts (`/proc`, `/sys`, `/dev`, `/etc`, `/root` on POSIX)
|
||
are rejected, except the process home and its subdirectories (so a
|
||
server running as root may use `/root` as the default `root_dir`).
|
||
Listing is single-level and capped; write probe runs only
|
||
for non-`/` paths.
|
||
|
||
| Method | Path | Auth | Notes |
|
||
|--------|------|------|-------|
|
||
| `GET` | `/filesystem/dirs?path=<abs>` | user | `{path, entries: [{path, name}]}` — one directory level |
|
||
| `POST` | `/filesystem/probe` | user | body `{path}` → `{ok, path?}` or `{ok: false, code, detail?}` (`not_directory`, `permission_denied`, `write_failed`, `not_allowed`) |
|
||
| `POST` | `/filesystem/mkdir` | user | body `{path, base_name?}` → `{path, name}` — create child dir (`base_name` defaults to `New Folder`; collisions become `Name (2)`, …) |
|
||
| `POST` | `/filesystem/rename` | user | body `{path, new_name}` → `{path, name}` — rename basename only |
|
||
|
||
## Connectors & OAuth
|
||
|
||
| Method | Path | Auth | Notes |
|
||
|--------|------|------|-------|
|
||
| `GET` | `/connectors/catalog` | user | connector catalog (Notion, Figma, …) |
|
||
| `GET` | `/connectors/test-credentials` | user | preflight credentials |
|
||
| `GET` | `/connector-instances` | user | list instances |
|
||
| `POST` | `/connector-instances` | user | create instance |
|
||
| `GET`/`PATCH`/`DELETE` | `/connector-instances/{id}` | user | CRUD on an instance |
|
||
| `POST` | `/connector-instances/{id}/test` | user | test a configured instance |
|
||
| `POST` | `/connector-instances/{id}/refresh` | user | refresh OAuth tokens |
|
||
| `GET` | `/connectors/auth/{kind}/info` | user | auth flow info |
|
||
| `GET` | `/connectors/auth/{kind}/authorize-url` | user | build the authorize URL |
|
||
| `POST` | `/connectors/auth/{kind}/exchange-code` | user | exchange auth code |
|
||
| `POST` | `/connectors/oauth/start` | user | start OAuth (catalog or custom MCP via `target`) |
|
||
| `POST` | `/connectors/oauth/{kind}/start` | user | legacy catalog OAuth start |
|
||
| `PUT` | `/connectors/custom-mcp` | user | save custom MCP server map |
|
||
| `PATCH` | `/connectors/custom-mcp/servers/{name}` | user | patch `enabled` / `default_open` on one server |
|
||
| `POST` | `/connectors/custom-mcp/test` | user | probe a custom MCP server (inline spec or saved name) |
|
||
| `GET` | `/connectors/oauth/callback` | public | OAuth redirect target |
|
||
| `GET` | `/connectors/oauth/pending/{state_id}` | user | poll the OAuth result |
|
||
|
||
Custom MCP OAuth (streamable HTTP, public HTTPS URL only): Octop discovers the authorization
|
||
server from the MCP URL (401 / RFC 9728 protected-resource metadata), requires dynamic client
|
||
registration (DCR), stores encrypted tokens in the custom MCP spec, and injects `Authorization:
|
||
Bearer` when loading tools. Loopback and LAN MCP URLs may use HTTP and do not use remote OAuth
|
||
discovery.
|
||
|
||
## Internal MCP (octop-harness agents)
|
||
|
||
| Method | Path | Auth | Notes |
|
||
|--------|------|------|-------|
|
||
| `POST`/`GET`/… | `/internal/mcp/*` | public (mTLS / network-isolated) | MCP gateway used by octop-harness agents (not the dashboard) |
|
||
|
||
## Octop Agent MCP Gateway
|
||
|
||
External agents call installed Octop experts through the authenticated
|
||
Streamable HTTP endpoint:
|
||
|
||
`POST/GET/DELETE /mcp/agents`
|
||
|
||
Use the same JWT bearer token as the REST API. Use HTTPS when the endpoint is
|
||
available outside the local host. The default token lifetime is 24 hours.
|
||
|
||
The server exposes three tools:
|
||
|
||
| Tool | Purpose |
|
||
|------|---------|
|
||
| `list_octop_experts` | List experts visible to the authenticated user |
|
||
| `call_octop_expert` | Start a new isolated expert conversation, or continue one with `thread_id` |
|
||
| `resume_octop_expert` | Resume a call that stopped for tool approval or user input |
|
||
|
||
`call_octop_expert` returns `thread_id`. Store this value in the external
|
||
agent's working state. Pass it only when the next task must use the same Octop
|
||
conversation. Omit it for a new task. MCP threads cannot attach to dashboard,
|
||
CLI, or IM threads.
|
||
|
||
When a tool needs approval, the call returns `status: "approval_required"` and
|
||
an `approval` object. Read `approval.request.action_requests` and
|
||
`approval.request.review_configs`. Send one allowed decision for each action to
|
||
`resume_octop_expert`. A resumed call can return another approval request.
|
||
|
||
Example client configuration:
|
||
|
||
```json
|
||
{
|
||
"mcpServers": {
|
||
"octop-agents": {
|
||
"type": "streamable_http",
|
||
"url": "http://127.0.0.1:8090/api/mcp/agents",
|
||
"headers": {
|
||
"Authorization": "Bearer <octop-access-token>"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
The exact transport key differs by MCP client. Some clients use `http`
|
||
instead of `streamable_http`.
|
||
|
||
## Observability & security
|
||
|
||
| Method | Path | Auth | Notes |
|
||
|--------|------|------|-------|
|
||
| `GET`/`PUT` | `/admin/observability` | admin | Langfuse configuration (host, project, env) |
|
||
| `GET` | `/admin/security` | admin | global security policy |
|
||
| `PUT` | `/admin/security` | admin | update global policy |
|
||
| `GET` | `/admin/security/tool-guard/rules` | admin | active command guard rules |
|
||
| `GET` | `/admin/security/tool-guard/rules/raw` | admin | editable YAML |
|
||
| `PUT` | `/admin/security/tool-guard/rules/raw` | admin | save YAML |
|
||
| `POST` | `/admin/security/tool-guard/rules/reset` | admin | reset to shipped defaults |
|
||
| `GET` | `/admin/security/defaults` | admin | defaults + rule catalogs |
|
||
|
||
## TLS (Let's Encrypt)
|
||
|
||
| Method | Path | Auth | Notes |
|
||
|--------|------|------|-------|
|
||
| `GET` | `/admin/tls/status` | admin | current cert + issuance task status |
|
||
| `POST` | `/admin/tls/preflight` | admin | preflight (port 80, DNS) |
|
||
| `POST` | `/admin/tls/issue` | admin | start HTTP-01 issuance |
|
||
|
||
`GET /.well-known/acme-challenge/{token}` is the HTTP-01 challenge
|
||
endpoint (public, mounted directly in `api/app.py`).
|
||
|
||
## Browser, terminal, uploads
|
||
|
||
| Method | Path | Auth | Notes |
|
||
|--------|------|------|-------|
|
||
| `WS`/`POST`/`GET`/… | `/agents/{aid}/terminal` | owner | AI-assisted remote PTY |
|
||
| `GET` | `/agents/{aid}/terminal/context` | owner | recent terminal context for the AI helper |
|
||
| `WS`/`POST`/`GET`/… | `/browser/...` | user | octop-browser sessions, live stream, record/replay |
|
||
| `POST` | `/browser/shutdown` | user | stop the current user's Octop-managed Chrome |
|
||
| `POST` | `/agents/{aid}/upload` | user | multipart upload → `{workspace}/inbound/` |
|
||
| `POST` | `/agents/{aid}/files/access-urls` | user | refresh inbound media URLs (signed) |
|
||
| `GET` | `/agents/{aid}/files/{path}` | owner | read an inbound file |
|
||
| `POST` | `/downloads/link` | user | multipart `file` → `{url, filename, expires_in}`: short-lived link for in-app browsers (WeChat, …) that cannot save `blob:` downloads; capped at `max_upload_mb` |
|
||
| `GET` | `/downloads/link/{token}` | public (token) | serve that file as an attachment until it expires (10 min); the token is the only credential |
|
||
|
||
## Updates, ollama, i18n, plugins, slash, preferences
|
||
|
||
| Method | Path | Auth | Notes |
|
||
|--------|------|------|-------|
|
||
| `GET`/`POST` | `/update/status`, `/check`, `/upgrade`, `/progress`, `/restart` | admin | in-place server update flow |
|
||
| `GET`/`POST`/`DELETE` | `/ollama-models/...` | `ollama_models` | Ollama model discovery + downloads |
|
||
| `GET`/`PUT` | `/ollama-models/service` | `ollama_models` | Local daemon toggle; omit `enabled` to set `models_dir` only |
|
||
| `GET` | `/i18n/tools` | user | server-owned tool display names (locale-aware) |
|
||
| `GET` | `/i18n/locales` | public | available locales + fallback chain |
|
||
| `GET` | `/i18n/locales/{locale}/{namespace}` | public | one namespace bundle (errors, tools, channel, slash) |
|
||
| `GET`/`POST` | `/preferences` | user | UI preferences (per-user key/value) |
|
||
| `GET` | `/slash/commands` | user | slash command catalog for the composer menu |
|
||
| `GET`/`POST` | `/plugins` | user | installed plugin list / install flow |
|
||
| `POST` | `/plugins/reload` | admin (`plugins`) | reload plugins from disk into process |
|
||
| `PATCH` | `/plugins/{id}` | admin (`plugins`) | enable/disable plugin (`{ "enabled": bool }`) |
|
||
| `GET` | `/plugins/{id}/ui/{path}` | user | serve prebuilt plugin UI assets (`ui/dist/…`) |
|
||
| `DELETE` | `/plugins/{id}` | admin (`plugins`) | uninstall plugin |
|
||
|
||
## Usage & admin
|
||
|
||
| Method | Path | Auth | Notes |
|
||
|--------|------|------|-------|
|
||
| `GET` | `/usage/summary` | user | token usage summary for the current user |
|
||
| `GET` | `/admin/usage` | admin | global token usage summary |
|
||
| `GET` | `/admin/overview` | admin | `{user_count, agent_count, ...}` |
|
||
| `GET` | `/admin/audit-log` | admin | recent audit rows |
|
||
| `GET` | `/admin/metrics` | admin | `{messages_total, stream_errors_total, cron_runs_total, cron_errors_total, agent_active, ...}` |
|
||
|
||
## Envs
|
||
|
||
| Method | Path | Auth | Notes |
|
||
|--------|------|------|-------|
|
||
| `GET`/`PUT` | `/agents/{aid}/envs` | owner | env-var preset for an agent's tool calls |
|
||
| `GET`/`POST`/`DELETE` | `/envs/presets` | user | reusable presets |
|
||
|
||
## Error envelope
|
||
|
||
Every non-2xx JSON response uses:
|
||
|
||
```json
|
||
{ "error": { "code": "AGENT_NOT_FOUND", "message": "...", "details": { } } }
|
||
```
|
||
|
||
`code` matches the `ErrorCode` enum in `octop.infra.errors`; the
|
||
server-localized `message` is rendered by `OctopError.to_envelope`
|
||
with the locale from `Accept-Language` (falling back to `en`). The
|
||
dashboard mirrors every code under `apiErrors.*` in
|
||
`dashboard/src/locales/{en,zh}.json`.
|
||
|
||
| Code | HTTP | Meaning |
|
||
|------|------|---------|
|
||
| `AUTH_FAILED` | 401 | Bad credentials |
|
||
| `TOKEN_EXPIRED` | 401 | JWT past its TTL |
|
||
| `LOGIN_LOCKED` | 423 | Too many failed attempts — wait `login_lockout_seconds` or call `/users/{id}/unlock-login` |
|
||
| `SETUP_REQUIRED` | 409 | Initial admin not yet created (or wizard not finished) |
|
||
| `FORBIDDEN` | 403 | Authenticated but not allowed |
|
||
| `NOT_FOUND` | 404 | No such row / route |
|
||
| `USER_DISABLED` | 403 | Account flag flipped off |
|
||
| `USERNAME_TAKEN` | 409 | Conflict on `users.username` |
|
||
| `AGENT_NOT_FOUND` | 404 | Agent row missing or owned by another user |
|
||
| `AGENT_FAILED` | 500 | Runtime errored during a call |
|
||
| `AGENT_BUSY` | 409 | Operation refused while another is in flight |
|
||
| `PROVIDER_NAME_TAKEN` | 409 | Conflict on `providers.name` |
|
||
| `PROVIDER_NOT_VISIBLE` | 400 | Agent config references a provider the user can't see |
|
||
| `PROVIDER_REFERENCED` | 409 | Delete blocked because agents still reference the row |
|
||
| `PROVIDER_TEST_FAILED` | 400 | `/providers/{id}/test` failed |
|
||
| `CHANNEL_KIND_UNSUPPORTED` | 400 | `kind` not in registered builders |
|
||
| `CHANNEL_INVALID_CREDENTIALS` | 400 | Channel config rejected by the platform |
|
||
| `CHANNEL_PROBE_INCOMPLETE` | 400 | Probe couldn't reach the platform |
|
||
| `CRON_TRIGGER_INVALID` | 400 | Trigger string did not parse |
|
||
| `CRON_PROMPT_INVALID` | 400 | Empty or too-long prompt |
|
||
| `SLASH_UNKNOWN` | 400 | `/<cmd>` is not a registered handler |
|
||
| `SLASH_BAD_ARGS` | 400 | Slash handler rejected its arguments |
|
||
| `ATTACHMENT_UNSUPPORTED_TYPE` | 400 | Chat / inbound attachment rejected (media type not allowed) |
|
||
| `ATTACHMENT_TOO_LARGE` | 413 | Chat / inbound attachment exceeds the configured size limit (`max_mb` in details) |
|
||
| `WORKSPACE_PATH_INVALID` | 400 | Path outside the agent's workspace |
|
||
| `STORAGE_BACKEND_UNREACHABLE` | 502 | Remote backend connect / list failed |
|
||
| `CONNECTOR_OAUTH_FAILED` | 400 | OAuth flow could not complete |
|
||
| `INTERNAL_ERROR` | 500 | Unhandled exception (logged with traceback) |
|