This document sketches the native Arbiter HTTP protocol that replaces the old public transport surface.
Arbiter is still pre-release, so this design intentionally does not preserve the old endpoint, command names, or URL contract unless a later release decision explicitly adds a compatibility bridge.
- Expose a simple native HTTP protocol over mandatory TLS.
- Keep transport internals out of user-facing URLs, commands, docs, errors, and generated configuration.
- Make
arbiter.urla stable server base URL, not a protocol endpoint. - Support progressive discovery so clients and agents do not fetch every full operation schema up front.
- Support small inline artifacts, large streamed artifacts, and future large attachment uploads.
Tentative defaults for the native HTTP protocol:
- Installed server:
8075 - Staging server:
18075
Examples:
https://127.0.0.1:8075
https://127.0.0.1:18075
Do not append transport-specific path suffixes to user-facing URLs.
GET /_health_
GET /api/v1/info
GET /api/v1/plugins
GET /api/v1/plugins/{plugin_id}
GET /api/v1/plugins/{plugin_id}/accounts
GET /api/v1/plugins/{plugin_id}/accounts/{account}
GET /api/v1/plugins/{plugin_id}/policies/{policy}
GET /api/v1/plugins/{plugin_id}/operations
GET /api/v1/operations/{operation_id}
POST /api/v1/operations/{operation_id}
GET /api/v1/artifacts/{artifact_id}
GET /api/v1/artifacts/{artifact_id}/content
POST /api/v1/artifacts/uploads
PUT /api/v1/artifacts/{artifact_id}/content
Operation IDs are URL path segments. They may contain :, so this is valid:
/api/v1/operations/smtp:send_email
Operation IDs must not contain /, ?, #, spaces, or other characters that
would make path handling ambiguous.
The health endpoint is intentionally tiny and safe to expose to Docker, systemd, load balancers, and human operators:
GET /_health_{
"status": "ok"
}It should not include plugin names, account names, configuration details, or other discovery data.
GET /api/v1/info answers "what Arbiter server am I talking to?"
Example:
{
"name": "arbiter",
"version": "0.9.2.dev1",
"deployment_scope": "staged"
}This endpoint may include small server-level metadata, but detailed capability discovery belongs in the plugin and operation endpoints.
Discovery has three levels.
GET /api/v1/plugins returns a compact capability menu.
Example:
{
"plugins": [
{
"id": "smtp",
"summary": "Send email through configured SMTP accounts."
},
{
"id": "imap",
"summary": "Read and manage mailbox messages."
}
]
}This level should stay small enough to fetch on normal startup.
GET /api/v1/plugins/{plugin_id}/operations returns the operation menu for one
plugin. It includes enough information for a human or agent to choose the right
operation, but it does not include full schemas.
Example:
{
"plugin": "smtp",
"operations": [
{
"id": "smtp:send_email",
"summary": "Send an email message.",
"when_to_use": "Use when you need Arbiter to send a plain text or MIME email through a configured SMTP account."
}
]
}GET /api/v1/operations/{operation_id} returns full details for one operation.
Example:
{
"id": "smtp:send_email",
"plugin": "smtp",
"summary": "Send an email message.",
"description": "Send a message through a configured SMTP account, subject to account policy.",
"input_schema": {},
"output_schema": {},
"artifact_policy": {
"inline_max_bytes": 5120,
"supports_uploads": true
}
}Clients may cache discovery responses. The protocol can later add ETag and
If-None-Match support without changing the response shapes.
Use the same operation URL for description and invocation:
GET /api/v1/operations/smtp:send_email
POST /api/v1/operations/smtp:send_emailGET describes the operation. POST invokes it.
Example request:
{
"args": {
"account": "bot",
"to": ["ops@example.com"],
"subject": "Hello",
"text_body": "Hi"
}
}Example response:
{
"result": {},
"artifacts": [],
"warnings": []
}If asynchronous execution is needed later, POST can return 202 Accepted
with a run handle and status URL.
Successful responses should be shaped like the resource or result they return.
Do not wrap every successful response in a universal {"ok": true, "data": ...}
envelope. HTTP status codes already distinguish success from failure, and
resource-shaped responses are easier for users, clients, and docs to read.
Errors should use a consistent envelope:
{
"error": {
"code": "validation_error",
"message": "Missing required argument: account",
"details": {
"field": "account"
}
}
}Use standard HTTP status codes where possible:
400: invalid request shape401: unauthenticated403: forbidden by authentication, authorization, or policy404: unknown plugin, operation, artifact, or route409: conflict413: payload too large415: unsupported media type422: valid request rejected by operation validation or policy429: rate limited500: server bug503: server not ready or dependency unavailable
Arbiter-specific semantics should normally live in error.code, not in custom
HTTP status codes. Private HTTP codes remain an option if a later design needs
them, but they should be introduced deliberately because standard tooling
handles standard codes better.
Operation responses may include artifacts.
Every artifact should include integrity metadata:
{
"id": "art_456",
"name": "archive.zip",
"mime_type": "application/zip",
"size": 981234,
"sha256": "4e07408562bedb8b60ce05c1decfe3ad16b7223092...",
"content_url": "/api/v1/artifacts/art_456/content"
}Small artifacts may be returned inline up to the server's configured inline limit. The initial target limit is 5 KiB.
Text example:
{
"id": "art_123",
"name": "messages.json",
"mime_type": "application/json",
"size": 3210,
"sha256": "4e07408562bedb8b60ce05c1decfe3ad16b7223092...",
"inline": {
"encoding": "utf-8",
"data": "[...]"
}
}Binary example:
{
"id": "art_124",
"name": "image.png",
"mime_type": "image/png",
"size": 4096,
"sha256": "4e07408562bedb8b60ce05c1decfe3ad16b7223092...",
"inline": {
"encoding": "base64",
"data": "..."
}
}Large artifacts are fetched through HTTP streaming:
GET /api/v1/artifacts/{artifact_id}/contentThe response should use normal HTTP content headers when available:
Content-Type: application/json
Content-Length: 123456
Digest: sha-256=...The JSON artifact metadata remains the durable integrity contract. The digest header is useful transport metadata when available.
Large uploads should use temporary artifacts that operations can reference. Implementation and tests can come later, but the high-level flow is:
-
Create an upload slot.
POST /api/v1/artifacts/uploads
{ "artifact": { "id": "tmp_art_123", "kind": "upload", "expires_at": "2026-06-17T12:00:00Z" }, "upload_url": "/api/v1/artifacts/tmp_art_123/content" } -
Stream bytes to the upload slot.
PUT /api/v1/artifacts/tmp_art_123/content
-
Reference the uploaded artifact from an operation.
{ "args": { "account": "bot", "to": ["ops@example.com"], "subject": "Report", "attachments": [ { "artifact_id": "tmp_art_123", "filename": "report.pdf" } ] } }
Temporary upload artifacts should expire if they are not consumed.