You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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/oidc/status, /api/auth/oidc/start,
/api/auth/oidc/callback, /api/auth/oidc/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.
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
POST
/auth/login
public
body {username, password} (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
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)
Chat (WebSocket)
Path
Auth
Notes
WS /agents/{id}/chat/ws?token=<jwt>
owner
Primary dashboard turn endpoint. Send {"type":"user_turn", ...} frames; 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).
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: [...]} → SSE chunk stream; finishes with {"type":"done"}
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.
body {trigger, prompt, session_key?, fresh_thread?, model?, task_type?} → 201
GET
/agents/{aid}/cron/{cid}
owner
cron row
PATCH
/agents/{aid}/cron/{cid}
owner
body subset → updated row
DELETE
/agents/{aid}/cron/{cid}
owner
204
POST
/agents/{aid}/cron/{cid}/run-now
owner
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.
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 harness-agent
GET
/models
user
resolved models across enabled providers
GET
/models/active
user
{provider_name, model}
PUT
/models/active
admin
body {provider_name, model}
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/mbti_profiles.py —
see Personas.
Experts
Method
Path
Auth
Notes
GET
/experts
user
bundled expert catalog (locale-aware)
GET
/experts/{expert_id}
user
full expert template (SOUL.md, skills, files)
POST
/agents/from-expert/{expert_id}
user
body {name, locale?, ...} → 201
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
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/{kind}/start
user
start an OAuth flow
GET
/connectors/oauth/callback
public
OAuth redirect target
GET
/connectors/oauth/pending/{state_id}
user
poll the OAuth result
Internal MCP (harness agents)
Method
Path
Auth
Notes
POST/GET/…
/internal/mcp/*
public (mTLS / network-isolated)
MCP gateway used by 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
remote Playwright sessions, screenshots, live streams
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)