Files
HUANG Chengandjubaoliang bd73bb60aa fix(plugins): offload oversized octop_ui payloads to ToolMessage.artifact (#1032) (#1116)
Plugin tools returning large octop_ui payloads (e.g. a bangumi season
with 1200+ episodes, ~228KB JSON) blew up the LLM context because the
same tool-result string served both the model and the UI.

Add OctopUiOffloadMiddleware at the innermost agent middleware layer:
when a ToolMessage content is a >=4000-char string carrying a non-empty
octop_ui renderer and data (and no file:// media refs), the payload is
moved in-place to ToolMessage.artifact and the model-visible content is
replaced by a compact result that keeps title/summary. Tool id,
tool_call_id, name and status are preserved.

Frontend restores rendering from artifact: ToolCallData.artifact,
closeToolCall extraction, history replay path, and resolvePluginUiData
prefers explicit data over data_ref. serialize.py surfaces artifact on
tool_result blocks; history recorder backfills artifact as fallback.

Spec: docs/octop-ui-payload-offload.md

Co-authored-by: jubaoliang <jubaoliang@gmail.com>
2026-09-25 23:06:19 +08:00

32 KiB

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).

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/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/resume-wizard public Issue a fresh wizard token mid-setup
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, ...}
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
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
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.

Method Path Auth Notes
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"} (attach to an in-flight turn without cancelling on disconnect). {"type":"cancel","thread_id"} stops the active turn (explicit stop; disconnect alone does not cancel).
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"}. Optional hitl_policy (ask / allow_all / allow_tools) is a thread-scoped bypass and does not change global tool-approval settings.

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.

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/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. 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.

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
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 /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)

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 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)

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

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/... user Ollama model discovery + downloads
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:

{ "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)