Skip to content

Master API Reference

This document collects the HTTP API exposed by the CapOwn Master.

The Master is the coordination point in a CapOwn network. It authenticates clients and workers, tracks worker liveness and ownership, routes tasks over worker SSE connections, receives task results, and manages user/token state.

This file describes the REST API implemented by master/api/*.py and the MCP adapter mounted by master/mcp/. The CLI commands in capown-master may provide higher-level workflows that are not exposed as HTTP endpoints yet.

Base URL

Examples use the direct Master URL:

http://127.0.0.1:9210

Deployments behind a reverse proxy commonly expose the same routes under a public URL such as:

https://master.example.com/v1

All JSON request bodies use Content-Type: application/json. Bearer-token authentication uses:

Authorization: Bearer <token>

Auth Modes

Mode Used by Credential
Public GET /health None
Bundle enrollment GET /api/enroll/{token} Token embedded in URL plus X-CapOwn-Enrollment-Role
Worker enrollment POST /api/workers/register enrollment_token in JSON body
Worker session Worker heartbeat, SSE, result reporting Session token from Ed25519 challenge-response
Client token Worker listing and task dispatch client auth token
MCP client All /mcp operations, including initialization and tool discovery client auth token
Admin token /api/admin/* admin auth token

Current admin API authentication is based on token_type == "admin". Planned dashboard identity changes should update this section when they land.

Error Shape

CapOwn-layer errors are JSON:

{
  "error": "human-readable message",
  "error_code": "schema_invalid"
}

Common error_code values are defined in shared/protocol.py, including:

auth_denied
schema_invalid
worker_offline
timeout
user_not_found
token_revoked
enrollment_invalid
enrollment_expired
enrollment_exhausted
enrollment_role_not_allowed
enrollment_revoked
missing_role_header
signature_invalid

Endpoint Summary

Method Path Auth Purpose
GET /health Public Master health and connected worker IDs
GET, POST /mcp Client token MCP Streamable HTTP transport
GET /api/enroll/{token} Bundle enrollment URL Return role-specific enrollment TOML
POST /api/workers/register Enrollment token in body Register a worker and bind ownership
POST /api/workers/auth/challenge Public worker identity Issue an Ed25519 nonce
POST /api/workers/auth/verify Signed nonce Issue a worker session token
POST /api/workers/{worker_id}/reconnect Worker session token Report updated metadata after restart
POST /api/workers/heartbeat Worker session token Refresh worker liveness
GET /api/workers Client token List workers owned by the client user
POST /api/workers/{worker_id}/rename Client token or worker session Rename an owned worker
GET /api/events?worker_id=... Worker session token Open worker SSE stream
POST /api/tasks/dispatch Client token Dispatch a task asynchronously
POST /api/tasks/dispatch_sync Client token Dispatch and wait up to soft timeout
POST /api/tasks/result Worker session token Report task result
GET /api/tasks/{task_id} Client token Deprecated status endpoint
GET /api/admin/health Admin token Check admin API access
POST /api/admin/users Admin token Create a user
GET /api/admin/users/{username} Admin token Get user details
POST /api/admin/users/{username}/tokens Admin token Create a client or admin auth token
GET /api/admin/users/{username}/tokens Admin token List auth tokens for a user
POST /api/admin/workers/{worker_id}/rename Admin token Admin rename a worker
POST /api/admin/tokens/{token_id}/revoke Admin token Revoke an auth token

Health

GET /health

Returns basic Master health and the worker IDs currently connected to the in-memory broker.

Response:

{
  "status": "ok",
  "connected_workers": ["worker-id"],
  "mcp_enabled": true
}

mcp_enabled reports whether the MCP adapter initialized successfully. It does not report client authentication or Worker connectivity.

MCP API

GET, POST /mcp

Exposes CapOwn Worker, shell, file, system, and task operations through the MCP Streamable HTTP transport. MCP uses the same client bearer tokens and Worker ownership rules as the REST client API. Admin tokens and Worker session tokens are rejected.

Authentication is checked before MCP initialization, tool discovery, and tool calls. Requests that include an Origin header must match the configured Master public_url by scheme, host, and port. A supplied Origin is rejected if public_url is unset, malformed, or does not match. Non-browser MCP clients may omit the Origin header.

See MCP Support for the tool reference, client configuration, request headers, error behavior, and reverse proxy configuration.

Enrollment API

GET /api/enroll/{token}

Redeems a temporary bundle enrollment URL and returns role-specific TOML. The same URL can be used separately by a client and a worker when the bundle token allows both roles.

Required header:

X-CapOwn-Enrollment-Role: client

or:

X-CapOwn-Enrollment-Role: worker

Success response:

Content-Type: text/plain

Client TOML response:

# SPDX-License-Identifier: Apache-2.0

role = "client"
master_url = "https://master.example.com"
client_token = "..."

[client]
soft_timeout = 30

Worker TOML response:

# SPDX-License-Identifier: Apache-2.0

role = "worker"
master_url = "https://master.example.com"
enrollment_token = "..."

[worker]
execution_mode = "host"
workspace = "/"
max_runtime = 86400
idle_timeout = 600
reconnect_interval = 5

Notes:

  • The token in the URL is the credential.
  • The role is supplied by header, not by URL path.
  • Bundle role counters are claimed atomically.
  • Worker TOML receives a newly issued one-shot worker enrollment token.
  • Client TOML receives a newly issued client auth token.

Worker API

POST /api/workers/register

Registers a worker using a one-shot worker enrollment token. The token is consumed only after registration succeeds.

Request:

{
  "enrollment_token": "cown_enroll_xxx",
  "worker_name": "worker-1",
  "public_key": "hex-ed25519-public-key",
  "hostname": "host-a",
  "os": "linux",
  "mode": "container",
  "capabilities": ["shell.run", "file.read"],
  "workspace": "/workspace"
}

Response 201:

{
  "worker_id": "abc123",
  "worker_name": "worker-1"
}

Important behavior:

  • The Master stores the worker public key for later challenge-response auth.
  • The worker is bound to the enrollment token owner's user ID.
  • A duplicate worker_name returns 409 and does not consume the enrollment token.

POST /api/workers/auth/challenge

Starts worker session authentication by issuing a nonce for a registered worker.

Request:

{
  "worker_id": "abc123"
}

Response:

{
  "nonce": "hex-nonce",
  "expires_at": "2026-07-08T00:00:00"
}

POST /api/workers/auth/verify

Verifies the worker signature and returns a session token. The session token is used for heartbeat, SSE, and result reporting.

Request:

{
  "worker_id": "abc123",
  "nonce": "hex-nonce",
  "signature": "hex-signature"
}

Response:

{
  "status": "ok",
  "session_token": "cown_sess_..."
}

Notes:

  • The nonce is single-use and expires after 5 minutes.
  • The signature is verified against the registered Ed25519 public key.
  • Worker session tokens are in-memory and expire after 24 hours.

POST /api/workers/heartbeat

Updates liveness for a worker.

Auth: worker session token.

Request:

{
  "worker_id": "abc123"
}

Response:

{
  "status": "ok"
}

The session token is bound to one worker ID. A mismatch returns 403.

POST /api/workers/{worker_id}/reconnect

Updates runtime metadata (hostname, OS, execution mode, capabilities, workspace) for a worker after a restart or reconnection. Identity fields (worker_id, worker_name, public_key, ownership, registration time) are preserved.

Auth: worker session token.

Request:

{
  "hostname": "new-host",
  "os": "windows",
  "mode": "host",
  "capabilities": ["shell.run", "file.read"],
  "workspace": "/new/workspace"
}

Response:

{
  "worker_id": "wrk_a1b2c3d4e5f6",
  "worker_name": "worker-1",
  "hostname": "new-host",
  "os": "windows",
  "mode": "host",
  "capabilities": ["shell.run", "file.read"],
  "workspace": "/new/workspace",
  "status": "online",
  "last_heartbeat": "2026-07-10T12:00:00",
  "registered_at": "2026-07-08T00:00:00",
  "previous_worker_name": null,
  "renamed_at": null
}

Notes:

  • The session token must belong to the worker in the URL path.
  • Only the request fields are updated; worker_id, worker_name, public_key, ownership, and registered_at are never changed.
  • A revoked or unknown worker returns 404 with error_code: "worker_offline".
  • The Worker calls this endpoint automatically after each authentication cycle.

GET /api/events?worker_id=abc123

Opens the Server-Sent Events stream used by Master to deliver tasks to a worker.

Auth: worker session token.

SSE event examples:

event: task
data: {"task_id":"...","target_worker":"abc123","payload":{...}}
event: ping
data:

Notes:

  • The request must include worker_id.
  • The Master marks the worker online when the stream is connected.
  • The Master emits ping events on idle intervals.
  • Disconnect marks the worker offline in the registry.

Client API

GET /api/workers

Lists workers owned by the authenticated client user.

Auth: client token.

Response:

[
  {
    "worker_id": "abc123",
    "worker_name": "worker-1",
    "hostname": "host-a",
    "os": "linux",
    "mode": "container",
    "capabilities": ["shell.run", "file.read"],
    "workspace": "/workspace",
    "status": "online",
    "last_heartbeat": "2026-07-08T00:00:00",
    "registered_at": "2026-07-08T00:00:00",
    "previous_worker_name": null,
    "renamed_at": null
  }
]

Only workers owned by the token's user are returned.

Workers are sorted by registered_at ascending (oldest first), with workers missing a registration timestamp placed last. Equal timestamps are resolved by worker_id ascending.

POST /api/workers/{worker_id}/rename

Renames an owned worker. The worker_id is preserved and routing continues to the same physical node. The URL path must use the canonical worker_id (not worker_name).

Auth: client token (owner check) or worker session token.

Request:

{
  "worker_name": "new-worker-name"
}

Response:

{
  "worker_id": "wrk_a1b2c3d4e5f6",
  "worker_name": "new-worker-name",
  "hostname": "host-a",
  "os": "linux",
  "mode": "container",
  "capabilities": ["shell.run", "file.read"],
  "workspace": "/workspace",
  "status": "online",
  "last_heartbeat": "2026-07-08T00:00:00",
  "registered_at": "2026-07-08T00:00:00",
  "previous_worker_name": "old-worker-name",
  "renamed_at": "2026-07-09T12:00:00"
}

Validation:

  • worker_name must be 3-48 ASCII characters (lowercase letters, digits, dots, hyphens, underscores), starting and ending with alphanumeric.
  • The name must not be reserved (wrk_* prefix, common reserved words).
  • Duplicate names per owner return 409.

Task API

Task dispatch requests use the shared DispatchRequest model. The target_worker field is always a canonical worker_id - the client CLI resolves worker_name to worker_id before sending, but the API itself does not accept worker names for routing.

{
  "target_worker": "abc123",
  "payload": {
    "task_type": "shell",
    "params": {
      "command": "uname -a"
    }
  },
  "timeout": 30
}

Supported task_type values are defined in shared/protocol.py:

shell
file_read
file_write
list_dir
system_info
task_status
task_cancel
task_list
file_edit
file_find
file_search
task_command

The Master checks that the authenticated client user owns target_worker.

POST /api/tasks/dispatch

Dispatches a task asynchronously over the worker SSE connection.

Auth: client token.

Response:

{
  "task_id": "abc123def456",
  "status": "dispatched"
}

The Master does not persist task history. Store both target_worker and task_id; worker task IDs are worker-local for follow-up operations.

POST /api/tasks/dispatch_sync

Dispatches a task and waits up to the request timeout for a result.

Auth: client token.

Completed response:

{
  "task_id": "abc123def456",
  "worker_id": "abc123",
  "status": "completed",
  "output": "hello\n",
  "error": "",
  "error_code": null,
  "truncated": false,
  "completed_at": "2026-07-08T00:00:00",
  "started_at": "2026-07-08T00:00:00",
  "updated_at": "2026-07-08T00:00:00",
  "output_size": 6,
  "error_size": 0,
  "preview": "",
  "hint": "",
  "next_actions": []
}

If a shell task is still running after the soft timeout, the Master queries the worker for status and may return:

{
  "task_id": "abc123def456",
  "worker_id": "abc123",
  "status": "running",
  "output": "partial output",
  "preview": "partial output",
  "output_size": 1024,
  "error_size": 0,
  "hint": "Task is still running. To check progress later: capown task abc123 abc123def456. To wait longer for the final result: capown wait abc123 abc123def456",
  "next_actions": ["check_later", "wait_longer", "cancel"]
}

For non-shell tasks that do not finish in time, the response is a 504 timeout error.

POST /api/tasks/result

Receives the result of a task from a worker.

Auth: worker session token.

Request:

{
  "task_id": "abc123def456",
  "worker_id": "abc123",
  "status": "completed",
  "output": "hello\n",
  "error": "",
  "error_code": null,
  "truncated": false,
  "output_size": 6,
  "error_size": 0
}

Response:

{
  "status": "ok"
}

The session token must be bound to the reporting worker ID, and the worker must be owned by the session user.

GET /api/tasks/{task_id}

Deprecated. This endpoint always returns a schema error because task status requires an explicit target worker.

Response 400:

{
  "error": "task status requires explicit target_worker; use dispatch_sync task_status",
  "error_code": "schema_invalid"
}

Use a task_status task through POST /api/tasks/dispatch_sync instead.

Admin API

Admin endpoints currently require an admin auth token. Client tokens and worker session tokens are rejected.

GET /api/admin/health

Checks admin API authentication.

Response:

{
  "ok": true,
  "role": "master",
  "admin_api": true
}

POST /api/admin/users

Creates a user.

Request:

{
  "username": "alice",
  "role": "user"
}

Response 201:

{
  "user_id": "abc123def456",
  "username": "alice",
  "role": "user",
  "status": "active"
}

Current API accepts role values user and admin.

GET /api/admin/users/{username}

Gets a user by username.

Response:

{
  "user_id": "abc123def456",
  "username": "alice",
  "role": "user",
  "status": "active"
}

POST /api/admin/users/{username}/tokens

Creates an auth token for a user.

Request:

{
  "token_type": "client",
  "name": "laptop"
}

token_type may be client or admin.

Response 201:

{
  "token": "plaintext-token-shown-once",
  "token_id": "abc123def456",
  "token_type": "client",
  "token_prefix": "abcdef",
  "created_at": "2026-07-08T00:00:00"
}

Notes:

  • Plaintext token values are returned only at creation time.
  • Admin token creation rotates prior active admin tokens for that user.
  • Bundle enrollment URLs are currently created by capown-master tokens create rather than this HTTP endpoint.

GET /api/admin/users/{username}/tokens

Lists auth tokens for a user.

Response:

[
  {
    "token_id": "abc123def456",
    "token_type": "client",
    "token_prefix": "abcdef",
    "name": "laptop",
    "created_at": "2026-07-08T00:00:00",
    "last_used_at": null,
    "revoked_at": null
  }
]

POST /api/admin/tokens/{token_id}/revoke

Revokes an auth token.

Response:

{
  "status": "revoked"
}

Only auth_tokens are handled here. Enrollment token revocation is currently a local Master CLI operation.

POST /api/admin/workers/{worker_id}/rename

Admin endpoint to rename a worker. Does not require worker ownership - the admin token grants full access.

Auth: admin token.

Request:

{
  "worker_name": "new-worker-name"
}

Same response shape and validation rules as POST /api/workers/{worker_id}/rename.

State Ownership

The Master uses persistent SQLite-backed state for:

  • Users.
  • Auth token hashes.
  • Enrollment token hashes and usage counters.
  • Worker registry records.
  • Worker ownership.

The Master uses in-memory state for:

  • Connected worker SSE queues.
  • Pending task result waiters.
  • Worker challenge nonces.
  • Worker session tokens.

The Master does not persist task history or command history. Worker task state and shell command history live on each Worker.

Security Notes

  • Do not expose direct HTTP Master access without TLS in production.
  • Treat enrollment URLs, client tokens, admin tokens, and worker session tokens as secrets.
  • The URL token for /api/enroll/{token} can create credentials, so it must not be logged or pasted into public channels.
  • Client task routes enforce worker ownership before dispatch.
  • MCP requests require a client token and use the same ownership-aware routing as REST requests.
  • Browser-originated MCP requests require an exact scheme, host, and port match against the configured public_url.
  • Worker routes enforce session binding to the authenticated worker ID.
  • Admin routes currently trust admin auth tokens. Future dashboard work should update this model if user-role-based temporary access tokens replace admin tokens.