OceanCode
HTTP API reference
oceancode serve runs the engine as a long-lived HTTP server, by default on http://127.0.0.1:4096 (--port and --host change it; the default pager shape takes --listen <host:port>). Every route below is relative to that base URL. HTTP basic auth is off until OC_SERVER_PASSWORD is set; then it covers every route, SSE included, with the username from OC_SERVER_USERNAME (default oceancode). A non-loopback bind refuses to start without the password. Events stream over SSE: /event serves the v1 frames, /api/event and /global/event the v2 payload-wrapped envelope. Flags and variables: Headless and server.
curl -u "oceancode:$OC_SERVER_PASSWORD" http://127.0.0.1:4096/api/healthv2 — /api
| Method | Path | Operation | What it does |
|---|---|---|---|
| GET | /api/agent | v2.agent.list | built-in + config agents. |
| GET | /api/command | v2.command.list | — |
| PATCH | /api/credential/{credentialID} | v2.credential.update | (relabel). The contract declares 204. |
| DELETE | /api/credential/{credentialID} | v2.credential.remove | The contract declares 204. |
| GET | /api/event | — | The v2 GlobalEvent stream shared by GET /api/event (v2.event.subscribe) and GET /global/event (global.event). |
| GET | /api/fs/find | v2.fs.find | ranked recursive search. |
| GET | /api/fs/list | v2.fs.list | the direct children of a directory. |
| GET | /api/fs/read/{*path} | v2.fs.read | The wildcard path is the file to read; the body is the raw bytes under the detected mime. |
| GET | /api/health | v2.health.get | — |
| GET | /api/integration | v2.integration.list | — |
| GET | /api/integration/attempt/{attemptID} | v2.integration.attempt.status | An unknown attempt is a 404 NotFoundError. |
| DELETE | /api/integration/attempt/{attemptID} | v2.integration.attempt.cancel | The contract declares 204. |
| POST | /api/integration/attempt/{attemptID}/complete | v2.integration.attempt.complete | The contract declares 204. |
| GET | /api/integration/{integrationID} | v2.integration.get | — |
| POST | /api/integration/{integrationID}/connect/key | v2.integration.connect.key | The contract declares 204. |
| POST | /api/integration/{integrationID}/connect/oauth | v2.integration.connect.oauth | — |
| GET | /api/location | v2.location.get | The LocationInfo schema requires a resolved {directory, project} (project carries id + directory), so it is built via the shared helper rather than a bare {directory}. |
| GET | /api/model | v2.model.list | — |
| GET | /api/permission/request | — | the pending requests wrapped in {location, data}. |
| GET | /api/permission/saved | — | the always-allow rules as PermissionSavedInfos wrapped in {data}. |
| DELETE | /api/permission/saved/{id} | — | drops a saved rule addressed by the composite "<permission>:<pattern>" id. |
| GET | /api/provider | v2.provider.list | — |
| GET | /api/provider/{providerID} | v2.provider.get | — |
| GET | /api/pty | v2.pty.list | — |
| POST | /api/pty | v2.pty.create | — |
| GET | /api/pty/{ptyID} | v2.pty.get | — |
| PUT | /api/pty/{ptyID} | v2.pty.update | — |
| DELETE | /api/pty/{ptyID} | v2.pty.remove | The v2 contract is 204. |
| GET | /api/pty/{ptyID}/connect | v2.pty.connect | (WebSocket upgrade). |
| POST | /api/pty/{ptyID}/connect-token | v2.pty.connectToken | — |
| GET | /api/question/request | — | the {location, data} envelope over the request list. |
| GET | /api/reference | v2.reference.list | — |
| GET | /api/session | v2.session.list | — |
| POST | /api/session | v2.session.create | — |
| GET | /api/session/active | v2.session.active | The sessions with an in-flight coordinator drain, keyed by id. |
| GET | /api/session/{sessionID} | v2.session.get | — |
| POST | /api/session/{sessionID}/agent | v2.session.switchAgent | Publishes the durable agent-switched event the projector already handles. |
| POST | /api/session/{sessionID}/compact | v2.session.compact | Triggers a compaction turn via Runner::compact (the same summarize-then-replace path the runner's own overflow recovery uses). 204 on success; errors map through the shared session-error writer. |
| GET | /api/session/{sessionID}/context | v2.session.context | The active post-compaction context window, wrapped in {data:[…]}. |
| POST | /api/session/{sessionID}/interrupt | v2.session.interrupt | the v2 alias of the v1 session.abort semantics. |
| GET | /api/session/{sessionID}/message | v2.session.messages | — |
| GET | /api/session/{sessionID}/message/{messageID} | v2.session.message | — |
| POST | /api/session/{sessionID}/model | v2.session.switchModel | — |
| GET | /api/session/{sessionID}/permission | — | the pending requests scoped to one session. |
| POST | /api/session/{sessionID}/permission | — | evaluates the requested action/resources against the session's ruleset synchronously (no blocking wait) and returns {id, effect}, mirroring the v2 contract's non-interactive evaluation shape. |
| GET | /api/session/{sessionID}/permission/{requestID} | — | a single pending request. |
| POST | /api/session/{sessionID}/permission/{requestID}/reply | — | — |
| GET | /api/session/{sessionID}/permission/mode | — | the session's mode + lockout. |
| POST | /api/session/{sessionID}/permission/mode | — | sets the mode from { "mode": "acceptEdits" }; 400 on an unknown mode, 409 when bypass is locked. |
| POST | /api/session/{sessionID}/prompt | v2.session.prompt | Admits durable user input (the session.next.prompted event) then drives the coordinator to run the turn. The drain runs Runner::run, which streams the provider turn and projects the assistant message. |
| GET | /api/session/{sessionID}/question | — | — |
| POST | /api/session/{sessionID}/question/{requestID}/reject | — | TS validates the path params BEFORE the handler resolves the session, so a malformed requestID is a 400 even for a session that does not exist. |
| POST | /api/session/{sessionID}/question/{requestID}/reply | — | — |
| POST | /api/session/{sessionID}/revert/clear | — | the v2 alias of Store::unrevert. |
| POST | /api/session/{sessionID}/revert/commit | — | The ported store has no durable "committed" state distinct from "staged" (unlike upstream's stage/commit split, which also touches the working tree), so commit is a no-op success once a bookmark is confirmed to exist — staging already persisted the revert. |
| POST | /api/session/{sessionID}/revert/stage | — | Faithful to the v1 revert semantics (Store::revert stages a bookmark at messageID); files (a diff/file-list request) is accepted but the ported RevertState carries no diff/files projection, so those fields are omitted rather than fabricated. |
| POST | /api/session/{sessionID}/wait | v2.session.wait | TS's V2Session.wait is a stub: after confirming the session exists it unconditionally fails with Session.OperationUnavailableError, mapped to a 503 ServiceUnavailableError. Match the status and message exactly so a client cannot tell the servers apart — the existence check still produces 404 for an unknown id first. |
| POST | /api/session/{sessionID}/subagent/{childID}/message | — | Steers (delivery:"steer") or queues ("queue", the default) text into the child's live turn, or resumes it when it has settled. Returns the delivery status. A missing child is a 404. |
| POST | /api/session/{sessionID}/subagent/{childID}/resume | — | Re-enters a completed child with text on a fresh step budget and returns its new result. A missing child is a 404. |
| GET | /api/skill | v2.skill.list | — |
v1 — auth / catalog / config
| Method | Path | Operation | What it does |
|---|---|---|---|
| GET | /agent | app.agents | the v1 shape. |
| PUT | /auth/{providerID} | auth.set | persist a credential to auth.json. |
| DELETE | /auth/{providerID} | auth.remove | drop the stored credential. |
| GET | /command | command.list | the v1 shape. |
| GET | /config | config.get | the merged config. |
| PATCH | /config | config.update | Deep-merges the body over the project config document and persists it. The merged value is written back into Services so the cached read path (config.get, formatter.status, agent/command discovery) reflects the change. TS ConfigHttpApi.update returns ctx.payload — the SUBMITTED patch, not the merged config — so the request body is echoed. |
| GET | /config/providers | config.providers | TS config.providers returns providerSvc.list() — the RESOLVED, connected providers (those with a usable credential via auth.json, a stored DB credential, or a declared env key) — plus a per-provider default-model map. It is NOT the bare oceancode.json provider override block: the TUI's connect gate iterates this list, and an override-only answer left the gate permanently false. |
| GET | /event | event.subscribe | the v1 SSE stream ({id,type,properties} frames). Mirrors EventSubscribe. |
experimental
| Method | Path | Operation | What it does |
|---|---|---|---|
| GET | /experimental/capabilities | experimental.capabilities.get | Mirrors experimental.ts capabilities(): {backgroundSubagents: …}: on unless OC_EXPERIMENTAL_BACKGROUND_SUBAGENTS is 0/false — the same rule the task tool's background mode reads. |
| GET | /experimental/console | experimental.console.get | Mirrors experimental.ts getConsole(): {consoleManagedProviders, activeOrgName?, switchableOrgCount}. With no cloud accounts registered account.orgsByAccount() returns [] (account.ts:329 over an empty repo.list()), so switchableOrgCount is 0; consoleManagedProviders is [] (config.ts:589); activeOrgName is omitted. No 500 — the empty result is a success. |
| GET | /experimental/console/orgs | experimental.console.listOrgs | Mirrors experimental.ts listConsoleOrgs(): {orgs: [...]}. With no accounts, orgsByAccount() is [] so the flattened list is empty (experimental.ts:56). |
| POST | /experimental/console/switch | experimental.console.switchOrg | Mirrors experimental.ts switchConsole(): account.use(accountID, orgID) wrapped in a catch that maps any failure to a 400 BadRequest (experimental.ts:84). With no account registered use() fails, so this is the faithful 400. |
| POST | /experimental/control-plane/move-session | experimental.controlPlane.moveSession | Mirrors control-plane.ts moveSession(): 204 on success; failures map to MoveSessionError. MoveSession first loads the session (move-session.ts:77) — a missing one yields "Session not found: {id}" (control-plane.ts:33). The move itself is a local operation and is a no-op success when the destination is the session's current directory (move-session.ts:79). |
| POST | /experimental/project/{projectID}/copy | v2.projectCopy.create | — |
| DELETE | /experimental/project/{projectID}/copy | v2.projectCopy.remove | The contract declares 204. |
| POST | /experimental/project/{projectID}/copy/generate-name | experimental.projectCopy.generateName | The LLM naming path is not ported; a deterministic slug is returned, matching the {name} shape. |
| POST | /experimental/project/{projectID}/copy/refresh | v2.projectCopy.refresh | The contract declares 204. |
| GET | /experimental/resource | experimental.resource.list | Mirrors experimental.ts resource(): mcp.resources() -> Record<id, Resource>. The MCP manager bridges tools but does not enumerate resources, so a fresh instance returns the empty map (experimental.ts:169). |
| GET | /experimental/session | experimental.session.list | Mirrors experimental.ts session(): sessions.listGlobal(...) -> GlobalSession[]. The global cross-project aggregate (slug/summary/cost/tokens/project) is not ported; a fresh self-hosted instance's listGlobal yields [] (experimental.ts:134). |
| POST | /experimental/session/{sessionID}/background | experimental.session.background | Mirrors experimental.ts sessionBackground(): returns false immediately when the experimentalBackgroundSubagents runtime flag is off (experimental.ts:156). That flag is off by default. |
| GET | /experimental/tool | tool.list | every registered tool's id/description/parameters. |
| GET | /experimental/tool/ids | tool.ids | — |
| GET | /experimental/workspace | experimental.workspace.list | — |
| POST | /experimental/workspace | experimental.workspace.create | — |
| GET | /experimental/workspace/adapter | experimental.workspace.adapter.list | Mirrors workspace.ts adapters() -> listAdapters(projectID) (workspace.ts:16). The only built-in is the worktree adapter (control-plane/adapters/index.ts BUILTIN), reported with its name/description (adapters/worktree.ts:29-30). |
| GET | /experimental/workspace/status | experimental.workspace.status | Mirrors workspace.ts status(): [...connections.values()] filtered to the project's workspaces (workspace.ts:55, 833). With no active sync connections the map is empty. |
| POST | /experimental/workspace/sync-list | experimental.workspace.syncList | Mirrors workspace.ts syncList(): discover workspaces via the registered adapters and register any new ones, no body, 204 (workspace.ts:51, 743). The built-in worktree adapter discovers nothing without configured remotes, so this is a no-op 204. |
| POST | /experimental/workspace/warp | experimental.workspace.warp | Mirrors workspace.ts warp(): workspace.sessionWarp(...) where a missing workspace maps to a 404 NotFoundError (workspace.ts:81). With no synced workspaces the target is always absent. |
| DELETE | /experimental/workspace/{id} | experimental.workspace.remove | — |
| GET | /experimental/worktree | worktree.list | the copy worktree directories. |
| POST | /experimental/worktree | worktree.create | a linked worktree on a new branch under the repo of the request directory. |
| DELETE | /experimental/worktree | worktree.remove | INTENTIONAL, SAFER DIVERGENCE FROM TS, carried verbatim from Go: git worktree remove refuses to remove the repository's MAIN worktree and that refusal surfaces as a 400. The TS handler is permissive and reports 200 for the same request because its remove path does not guard the main worktree. The guard is kept — matching TS would mean performing (or claiming) a destructive removal of the primary checkout. The non-main path is identical (200) on all three servers. |
| POST | /experimental/worktree/reset | worktree.reset | hard-reset a linked worktree (the body's directory) back to the default branch of the repo identified by the request directory. |
files / search
| Method | Path | Operation | What it does |
|---|---|---|---|
| GET | /file | file.list | direct children as v1 FileNodes. |
| GET | /file/content | file.read | The TS v1 /file/content returns only {type, content} (no mimeType) and trims the decoded text; both are mirrored so the envelope matches byte-for-byte. |
| GET | /file/status | file.status | the working-tree git status mapped onto the v1 File shape. |
| GET | /find | find.text | ripgrep-style file-content search rooted at the request directory. pattern is the regexp; the results are the ripgrep match records the TS handler forwards verbatim. |
| GET | /find/file | find.files | ranked file-name search returning the matching paths. |
| GET | /find/symbol | find.symbols | The LSP client exposes hover/definition/diagnostics but not workspace/symbol, and no live client is retained, so symbol search has no backend; the endpoint returns an empty-but-valid array. |
| GET | /formatter | formatter.status | the per-formatter enabled/extensions status computed from config. |
global / instance
| Method | Path | Operation | What it does |
|---|---|---|---|
| GET | /global/config | global.config.get | — |
| PATCH | /global/config | global.config.update | TS GlobalHttpApi.configUpdate persists and returns only the submitted patch projection (an empty patch yields {} after provider/default stripping), not the full merged config. Echo the request body. |
| POST | /global/dispose | global.dispose | Mirrors global.ts dispose(): returns true after disposing. The server is a single long-lived process, so there is nothing to tear down per request (the OS process owns the lifecycle). |
| GET | /global/event | — | The v2 GlobalEvent stream shared by GET /api/event (v2.event.subscribe) and GET /global/event (global.event). |
| GET | /global/health | global.health | Mirrors global.ts health(): {healthy: true, version}, plus the four fields this port adds so every running instance says what it is: model_format is always "markdown" — the only payload format the product renders since D9; the field stays because clients read it, but it is no longer configurable — ledger says whether the graph's Session Ledger hook is installed (ocean_app::Services::ledger) — leaf T48b / T52b — build is the binary's own build stamp (crate::server_build, leaf T65), and dropped_events is the bus's drop-oldest counter (ocean_events::Bus::dropped_events, long-runs plan Task 13 / L-12): live events evicted from a saturated subscriber queue since boot — a saturation signal, not an error count, and never reset. |
| POST | /global/upgrade | global.upgrade | Mirrors global.ts upgrade(): when the installation method is "unknown" it reports {success:false, error:"Unknown installation method"}. This build is not produced by a known installer. The contract models this as the failure variant of the 200 anyOf (the 400 is reserved for the BadRequest envelope), so it is returned with status 200. |
| POST | /instance/dispose | instance.dispose | Mirrors instance.ts dispose(): returns true. Single-process, so no per-request teardown. |
| POST | /log | app.log | write the structured log line then return true. |
lsp / mcp
| Method | Path | Operation | What it does |
|---|---|---|---|
| GET | /lsp | lsp.status | The registry resolves candidate servers but does not hold live clients (spawn is on demand), so there is no connected server to report; the endpoint returns an empty-but-valid array. |
| GET | /mcp | mcp.status | a map of server name -> status. |
| POST | /mcp | mcp.add | register a server at runtime and connect it, returning the full status snapshot. |
| POST | /mcp/{name}/auth | mcp.auth.start | begin the remote OAuth flow, returning the authorization URL the caller opens. |
| DELETE | /mcp/{name}/auth | mcp.auth.remove | The 200 body is {"success": true} (additionalProperties:false), not a bare boolean. |
| POST | /mcp/{name}/auth/authenticate | mcp.auth.authenticate | — |
| POST | /mcp/{name}/auth/callback | mcp.auth.callback | completes the remote OAuth flow with the code returned to the loopback callback and answers with the server's resulting status. |
| POST | /mcp/{name}/connect | mcp.connect | — |
| POST | /mcp/{name}/disconnect | mcp.disconnect | — |
| GET | /mcp/{name}/prompts | — | the server's cached prompt list (P1-T38). |
| GET | /mcp/{name}/resources | — | the server's advertised resources (P1-T38), via McpManager::list_resources → Client::list_resources. |
| GET | /mcp/{name}/tools | — | the server's cached tool definitions (WP6d). |
| POST | /mcp/{name}/trust | mcp.trust | (GAP-06): persist the per-server trust decision to <home>/.oceancode/trust.json (keyed by the project dir) and apply it to the running manager, returning the refreshed GET /mcp map. |
| POST | /mcp/{name}/enable | mcp.enable | (GAP-06 parity): persist the enable/disable decision to <home>/.oceancode/trust.json's disabled_mcp (so the next boot honours it) and apply it live to the running manager, returning the refreshed GET /mcp map. An unknown server id is a 404. |
hooks (GAP-06)
| Method | Path | Operation | What it does |
|---|---|---|---|
| GET | /hooks | — | the boot-time hook source report plus this project's repo trust bit (GAP-06). Read-only: the rows are whatever the boot loader (or the last reload) recorded. |
| POST | /hooks | — | add a command hook to one of Claude Code's three writable settings files (GAP-06 parity), then reload the dispatcher and return the refreshed GET /hooks shape. An unknown event / empty command is a 400 (InvalidInput); a settings file that is not a JSON object is a 400 (InvalidData, never clobbered). The body is an ocean_app::hooks_boot::HookEdit. |
| POST | /hooks/reload | — | re-run the boot hook loader (re-attaching the dispatcher) and return the refreshed report (GAP-06). |
| POST | /hooks/remove | — | remove a hook from one of the three writable settings files (GAP-06 parity), then reload and return the refreshed GET /hooks shape. A path that is not one of the three writable scopes is a 400 (InvalidInput); a handler that is not present is a 404 (NotFound). The body is an ocean_app::hooks_boot::HookRemove. |
| POST | /trust/repo | — | flip this project's repo-tier trust (GAP-06): persist the decision, release/hold the repo's .mcp.json servers and reload the hook dispatcher, then return the refreshed GET /hooks shape. A trust-store write error is a 400. |
the Claude Code settings mirror (2026-09-10)
| Method | Path | Operation | What it does |
|---|---|---|---|
| GET | /claude/settings | — | the Claude Code settings mirror (model, effortLevel/modelSettings, ultracode, alwaysThinkingEnabled of the three tiered settings.json files) with the effective effort and the model it pins (ocean_app::claude_settings, 2026-09-10). Read-only: the rows are whatever boot or the last watcher reload recorded. |
path / permission
| Method | Path | Operation | What it does |
|---|---|---|---|
| GET | /path | path.get | The home/state/config values mirror the TS server's Global.Path.*, derived via the xdg-basedir port in ocean-config so they are byte-identical to the TS server on the same host/env (e.g. ~/.config/oceancode, not the macOS os.UserConfigDir() location). |
| GET | /permission | — | the v1 alias, a bare array. |
| POST | /permission/{requestID}/reply | — | the v1 alias, dispatched on requestID alone. |
plugin
| Method | Path | Operation | What it does |
|---|---|---|---|
| GET | /plugin | — | the installed plugins with their enable/trust bits. |
| POST | /plugin/install | — | install from source, returning {name, version, enabled, trusted}. |
| GET | /plugin/marketplace | — | the added marketplaces with their plugin entries. |
| POST | /plugin/marketplace/add | — | add a marketplace by spec. |
| DELETE | /plugin/marketplace/{name} | — | remove a marketplace, returning the refreshed marketplace list (GAP-06 parity). A marketplace that is not present is a 404. |
| POST | /plugin/{name}/enable | — | turn a plugin's loading on or off. |
| POST | /plugin/{name}/trust | — | grant or revoke a plugin's execution trust. A missing/invalid body is a 400 (falsify_trust_route_requires_body), so trust is never granted by an empty request. |
| GET | /plugin/{name} | — | the full detail of one installed plugin (GAP-06 parity). A name that is not installed is a 404. |
| DELETE | /plugin/{name} | — | uninstall a plugin, returning the refreshed installed list (GAP-06 parity). A plugin that is not installed is a 404. |
project
| Method | Path | Operation | What it does |
|---|---|---|---|
| GET | /project | project.list | — |
| GET | /project/current | project.current | resolve the request directory. |
| POST | /project/git/init | project.initGit | initialize a repository in the request directory (idempotent). |
| PATCH | /project/{projectID} | project.update | — |
| GET | /project/{projectID}/directories | project.directories | — |
provider
| Method | Path | Operation | What it does |
|---|---|---|---|
| GET | /provider | provider.list | {all, default, connected}. |
| GET | /provider/auth | provider.auth | Mirrors provider.ts auth(): svc.methods() -> Record<providerID, Method[]>. No v1 auth-method plugins are ported, so this is the empty map a fresh instance returns. |
| POST | /provider/{providerID}/oauth/authorize | provider.oauth.authorize | Mirrors provider.ts authorize(): with no oauth method registered for the provider the TS service hits an undefined method lookup, which the handler maps to a generic BadRequest ProviderAuthError. The Claude Pro/Max subscription login is served elsewhere (the v2 integration endpoints and the oceancode login CLI), so a v1 client is never offered a method here. |
| POST | /provider/{providerID}/oauth/callback | provider.oauth.callback | Mirrors provider.ts callback(): with no pending oauth result the TS service returns OauthMissing. |
pty
| Method | Path | Operation | What it does |
|---|---|---|---|
| GET | /pty | pty.list | — |
| POST | /pty | pty.create | — |
| GET | /pty/shells | pty.shells | the available login shells. |
| GET | /pty/{ptyID} | pty.get | — |
| PUT | /pty/{ptyID} | pty.update | — |
| DELETE | /pty/{ptyID} | pty.remove | The v1 contract answers 200 true (unlike the v2 delete, which is 204). |
| GET | /pty/{ptyID}/connect | pty.connect | (WebSocket upgrade). |
| POST | /pty/{ptyID}/connect-token | pty.connectToken | — |
question
| Method | Path | Operation | What it does |
|---|---|---|---|
| GET | /question | — | every pending question across sessions. |
| POST | /question/{requestID}/reject | — | dismisses a pending question, failing the waiting Ask with a rejection. |
| POST | /question/{requestID}/reply | — | resolves a pending question with the user's answers, unblocking the waiting Ask. |
session (v1)
| Method | Path | Operation | What it does |
|---|---|---|---|
| GET | /session | session.list | The v1 response is the bare array. |
| POST | /session | session.create | Maps the supported v1 body fields onto SessionStore::create; parentID, metadata and permission have no ported column and are dropped. |
| GET | /session/status | — | the run status of every session, keyed by id. |
| GET | /session/{sessionID} | session.get | — |
| DELETE | /session/{sessionID} | session.delete | Removes the session, its children, message projection and event log. |
| PATCH | /session/{sessionID} | session.update | the v1 title patch. |
| POST | /session/{sessionID}/abort | session.abort | The TS handler is idempotent: it interrupts any running turn and returns true even for an unknown session id (no 404 pre-check). Mirrored. The interrupt goes through the runner, not the bare coordinator, so it also reaches the session's sub-agents — foreground through the token cascade, background through the runner's job index (long-runs plan Task 12, L-9). |
| GET | /session/{sessionID}/children | — | the sub-sessions whose parentID is this session. The store has no parent filter, so this lists and filters in-process (the TS handler likewise scans). |
| POST | /session/{sessionID}/command | — | resolves a registered command, substitutes its arguments, and runs it as a prompt turn. An unknown command maps to CommandNotFoundError (404). |
| GET | /session/{sessionID}/diff | session.diff | the structured diff for the session's working directory (git mode). |
| POST | /session/{sessionID}/fork | — | copies the session into a new one (optionally truncated at messageID). |
| POST | /session/{sessionID}/init | — | runs the project-analysis (/init) flow as a prompt turn. |
| GET | /session/{sessionID}/checkpoints | — | the recorded checkpoints (id, reason, message bookmark, seq), oldest first. An unknown session lists nothing. |
| POST | /session/{sessionID}/undo | — | restore the last pre-write state. With no earlier checkpoint it is a no-op with a nothing to undo body, never an error. |
| POST | /session/{sessionID}/redo | — | re-apply the next checkpoint's files. Available only after an undo; otherwise a no-op with a nothing to redo body. |
| GET | /session/{sessionID}/checkpoints/{a}/diff/{b} | — | a unified diff between two checkpoints (text/plain); the header names both short ids. An unknown session or checkpoint is a 404. |
| POST | /session/{sessionID}/checkpoints/revert-hunk | — | restore one diff hunk of file to its pre-edit (checkpoint) bytes, leaving every other hunk. An unknown hunk id is a 404 (never a whole-file overwrite). |
| POST | /session/{sessionID}/checkpoints/apply-hunk | — | re-apply one previously reverted hunk of file. |
| GET | /session/{sessionID}/message | session.messages | the v1 alias. |
| POST | /session/{sessionID}/message | session.prompt | the v1 prompt alias. |
| GET | /session/{sessionID}/message/{messageID} | session.message | — |
| DELETE | /session/{sessionID}/message/{messageID} | session.deleteMessage | — |
| DELETE | /session/{sessionID}/message/{messageID}/part/{partID} | part.delete | — |
| PATCH | /session/{sessionID}/message/{messageID}/part/{partID} | part.update | The body is a v1 Part (the full tagged-union shape). The flat ported model stores assistant content (text/reasoning/tool) inline rather than as addressable Part rows, so for those types the update is applied to the inline content; for every type the updated Part is returned with the path-derived ids, matching the v1 contract. |
| POST | /session/{sessionID}/permissions/{permissionID} | — | the v1 session-scoped response. The {response} enum maps directly onto permission::Reply. |
| POST | /session/{sessionID}/prompt_async | — | admits durably and returns immediately; the turn runs in the background. |
| POST | /session/{sessionID}/revert | — | records a revert bookmark at the given message/part. |
| POST | /session/{sessionID}/share | — | mints (or returns) the share row. |
| DELETE | /session/{sessionID}/share | — | drops it. |
| POST | /session/{sessionID}/shell | — | runs a shell command in the session's working directory, recording it as a durable shell message. |
| GET | /session/{sessionID}/subagent | — | the session's effective sub-agent model (2026-09-16), so a remote TUI shows the identity row's sub-agents segment. The engine owns the whole precedence (ocean_app::Services::effective_subagent_model); this hands its answer back as {"model": ModelRef | null}. An unknown session is 404, like the sibling per-session GETs. |
| POST | /session/{sessionID}/summarize | — | runs a summarization turn and reports whether a summary was produced. |
| GET | /session/{sessionID}/todo | session.todo | The ETag header is the table's entity tag; send it back as If-Match on PUT for a conditional edit. |
| PUT | /session/{sessionID}/todo | — | the user's edit. Rows are matched by id or content; changed and new rows become user-authored; rows left out are removed. With If-Match set to a stale tag nothing is written and the answer is 412 carrying the current rows and tag. |
| POST | /session/{sessionID}/unrevert | — | clears the revert bookmark. |
| GET | /skill | app.skills | the v1 bare array. |
sync
| Method | Path | Operation | What it does |
|---|---|---|---|
| POST | /sync/history | sync.history.list | Mirrors sync.ts history(): select every durable event from the event table, excluding the (aggregateID, seq) cursors in the payload, ordered by seq (sync.ts:72). The payload maps aggregateID -> lastSeq the caller already has. |
| POST | /sync/replay | sync.replay | Mirrors sync.ts replay(): replay the serialized events into the event store, return {sessionID: events[0].aggregateID} (sync.ts:34). TS's ReplayPayload declares directory: Schema.String and events: Schema.NonEmptyArray(ReplayEvent), so the HttpApi rejects a missing directory or an empty array with a 400 BEFORE the handler runs. |
| POST | /sync/start | sync.start | Mirrors sync.ts start(): fork startWorkspaceSyncing with Effect.ignore, return true (sync.ts:27). The fork is best-effort and its result is ignored, so the response is unconditionally true. |
| POST | /sync/steal | sync.steal | Mirrors sync.ts steal(): requires the instance workspaceID; without one it returns 400 BadRequest (sync.ts:62). A self-hosted instance has no workspace binding. The contract's 400 is the effect HttpApiError BadRequest ({_tag:"BadRequest"}), not the {name,data} envelope. |
tui
| Method | Path | Operation | What it does |
|---|---|---|---|
| POST | /tui/append-prompt | tui.appendPrompt | — |
| POST | /tui/clear-prompt | tui.clearPrompt | — |
| GET | /tui/control/next | tui.control.next | long-poll for the next queued TUI request (FIFO). Parks until a request is enqueued or the client disconnects. |
| POST | /tui/control/response | tui.control.response | enqueue the TUI's reply for whoever is waiting (fire-and-forget FIFO). |
| POST | /tui/execute-command | tui.executeCommand | — |
| POST | /tui/open-help | tui.openHelp | — |
| POST | /tui/open-models | tui.openModels | — |
| POST | /tui/open-sessions | tui.openSessions | — |
| POST | /tui/open-themes | tui.openThemes | — |
| POST | /tui/publish | tui.publish | dispatch a TUI event onto the request queue for a connected client to pull via control/next. |
| POST | /tui/select-session | tui.selectSession | — |
| POST | /tui/show-toast | tui.showToast | — |
| POST | /tui/submit-prompt | tui.submitPrompt | — |
vcs
| Method | Path | Operation | What it does |
|---|---|---|---|
| GET | /vcs | vcs.get | current/default branch info. |
| POST | /vcs/apply | vcs.apply | applies a unified patch to the working tree. The contract's 200 body is {applied: boolean}. |
| GET | /vcs/diff | vcs.diff | the structured per-file diff. |
| GET | /vcs/diff/raw | vcs.diff.raw | The contract's 200 body is a text/x-diff string (not JSON), so the diff is written verbatim with that content type. |
| GET | /vcs/status | vcs.status | the working-tree file status list. |
Generated from crates/ocean-server/src/router.rs — 218 operations.