Code

Daemon API

aivoryd is the only process that reads your agent sessions. The desktop app, the phone app and the aivoryd call command are all clients of one JSON-RPC 2.0 API. This page shows you how to connect, how to authenticate, and which methods and events exist.

  • JSON-RPC 2.0, one JSON message for each line (newline-delimited JSON)
  • Three transports: a local named pipe or Unix socket, a WebSocket on the LAN, and iroh (QUIC) outside the LAN
  • One feed: warroom/subscribe returns a snapshot, then warroom/delta notifications
  • A JSON Schema describes every method and type: aivory-protocol.schema.json

Quick Start

Start a daemon with demo data and a private endpoint name. Then call it from a second terminal:

aivoryd serve --demo --endpoint docs-try
aivoryd call daemon/info --endpoint docs-try

aivoryd call <METHOD> --params '<JSON>' sends one request and prints every message that comes back. Stop the demo daemon with aivoryd call daemon/shutdown --endpoint docs-try.

Without --demo, aivoryd serve shows the real sessions of your machine (Claude Code, Codex CLI and Gemini CLI). See Daemon for what the daemon stores.


Transports

Transport Start For Frames
Local pipe or socket aivoryd serve The desktop app, the CLI, local tools One JSON message per line
WebSocket aivoryd serve --remote <ADDR> A paired phone on the same network One JSON message per text frame
iroh aivoryd serve --iroh A paired phone anywhere, with hole punching and a relay 4-byte length, then one JSON message

You can run --remote and --iroh together, and with --demo.

Local Pipe or Socket

The endpoint name is 1 to 64 characters of [A-Za-z0-9._-]. The default name is default. Choose another name with --endpoint <NAME>.

OS Address
Windows \\.\pipe\aivory-code-<name>
Linux and macOS $XDG_RUNTIME_DIR/aivory/<name>.sock
Linux and macOS without XDG_RUNTIME_DIR <temp folder>/aivory-<user>/<name>.sock

Open the pipe or socket, write one request as one line, and read lines. The local connection is trusted: it needs no token. It is the only transport that may call the local-only methods.

This Python script talks to a demo daemon on Windows. On Linux and macOS, open an AF_UNIX socket to the path above and use the same lines:

import json

pipe = open(r"\\.\pipe\aivory-code-docs-try", "r+b", buffering=0)


def call(id, method, params=None):
    msg = {"jsonrpc": "2.0", "id": id, "method": method}
    if params is not None:
        msg["params"] = params
    pipe.write((json.dumps(msg) + "\n").encode())
    # A notification has no id. Skip it until the response with this id arrives.
    while True:
        line = b""
        while not line.endswith(b"\n"):
            line += pipe.read(1)
        reply = json.loads(line)
        if reply.get("id") == id:
            return reply


print(call(1, "daemon/info")["result"]["version"])
print(call(2, "autopilot/get"))

WebSocket

aivoryd serve --remote 0.0.0.0:7420

The daemon listens on that address. The pairing QR code names the LAN address of the machine and the same port. Connect with a WebSocket client to ws://<host>:<port>/ and add a query:

Query Use
code=<code>&device=<name>&os=<os> First connection of a new device.
token=<token> Every later connection.
key=<64 hex characters> Optional. The Ed25519 public key of the device, so that it can use iroh without a new pairing.

The server answers 401 Unauthorized for an unknown token or a code that is used or expired, and 400 Bad Request when the request is no WebSocket upgrade. Each text frame is one JSON-RPC message, in both directions. A frame can hold up to 8 MB. The LAN link has no TLS. Use it on networks that you trust, or use iroh.

iroh

aivoryd serve --iroh --relay default

--relay takes a URL, default for the iroh relays, or none for direct paths only. Open a QUIC connection with the ALPN aivory/rpc/1 and one bidirectional stream. Each message is a frame: a 4-byte big-endian length, then one JSON-RPC message (up to 8 MB).

The first frame must be the request link/hello:

Field Type Meaning
key string The Ed25519 public key of the device, 64 hex characters. It must be the key that the QUIC handshake proved.
device string A name for the device.
os string The operating system.
code string The pairing code. Needed on the first connection only.
needToken boolean true asks for a LAN token too.

The reply has deviceId, name, newlyPaired, node (the node id of the daemon) and, for a new device or needToken, a token for the WebSocket link. A device that is not allowed gets an error with code -32001 and the daemon closes the stream.


Message Format

Every message is a JSON object.

{"jsonrpc":"2.0","id":1,"method":"autopilot/get"}
Rule Detail
Request jsonrpc must be "2.0". method is a string. params is an object, or you leave it out for methods without parameters. null and {} both work.
Response {"jsonrpc":"2.0","id":<id>,"result":...} or {"jsonrpc":"2.0","id":<id>,"error":{"code":...,"message":...}}.
Notification from you A request without id gets no response.
Notification from the daemon {"jsonrpc":"2.0","method":"<name>","params":...} without id.
Batches Not supported.
Parallel requests A connection handles one request at a time. Open a second connection for parallel work.
Line length A line longer than 4 MiB closes a local connection.
Slow readers A connection buffers 1024 messages. A feed drops a client that falls behind. Subscribe again.

Names in JSON are camelCase. Time values are milliseconds since the Unix epoch.


Pair a Remote Client

A remote client has no token at first. A local client creates a pairing code, and the remote client trades it for a token.

  1. A local client calls devices/pair. The result has code (AIV- and 6 characters), expires (a time in ms, 10 minutes ahead) and url.
  2. Show url as a QR code, or give the code to the remote client. The link has the form aivory://pair?node=<node id>&relay=<relay url>&host=<ip:port>&direct=<ip:port,...>&code=<code>. Each field is optional.
  3. The remote client connects once with the code. On the WebSocket link it adds ?code=<code>&device=<name>. On iroh it sends the code in link/hello.
  4. On a new pairing, the first message from the daemon is a notification:
{"jsonrpc":"2.0","method":"device/paired","params":{"token":"<64 hex characters>","device":{"id":"d1a2b3c4","name":"Pixel","os":"Android","paired":0,"lastSeen":0,"online":true}}}
  1. Store the token. The daemon keeps only its SHA-256, so the token comes once. Use ?token=<token> on later WebSocket connections. On iroh, the key of the device proves who it is.

A code works once and ends after 10 minutes. devices/remove with a device id deletes the device and closes its iroh connection. settings/get lists the devices.

A paired device can register a push target with devices/push. The daemon sends the sealed phone push to the gateway that you set with --push-url <URL> (or the setting remote.pushUrl). Without a URL, the daemon sends no push.

Local-only Methods

A paired device gets the error -32004 (NOT_AVAILABLE) for these methods. Only the local pipe or socket may call them:

repos/add, repos/remove, project/new, map/addPart, providers/addAcp, providers/removeAcp, settings/importClaude, daemon/restart, theme/inspect, theme/add, theme/remove, theme/checkUpdates, theme/update, theme/diff, theme/job, registry/add, registry/remove, registry/refresh.

All other methods are open to a paired device. theme/list, theme/get and theme/asset are how a phone loads a theme.


Follow the War Room

warroom/snapshot returns the full view once. For a live view, use the feed:

  1. Call warroom/subscribe with {}. The result has epoch, cursor and a snapshot (the full WarRoom).
  2. The daemon then sends warroom/delta notifications on the same connection. Each has params.seq (increasing) and params.delta.
  3. Apply each delta to your copy of the view and set your cursor to seq.
  4. After a reconnect, call warroom/subscribe with {"cursor": <last seq>, "epoch": <epoch>}. If the epoch is the same, the result has deltas (everything you missed) and no snapshot. If the daemon restarted, the epoch changed and you get a fresh snapshot.
{"jsonrpc":"2.0","method":"warroom/delta","params":{"seq":42,"delta":{"type":"tick","now":1792593300000}}}

A delta has a type:

type Fields Change
session session (a Pane) Add or replace a session with the same id.
sessionRemoved id Remove a session.
lines id, subagent?, lines, total New transcript lines. Lines of a subagent only count.
activity sample A new sample of the pulse chart, or a new value for the same at.
autopilot autopilot The autopilot rules.
counts queued, inboxOpen The counts of the badges.
usage usage The provider usage windows.
links hub, relay The state of the hub and relay links.
codebase codebase Add or replace a codebase.
codebaseRemoved id Remove a codebase.
tick now The daemon time.
board board The gates (needsYou, working, blocked, idle) and the order of the asks. A client that does not know a delta type keeps its old value.

The Rust type WarRoom has apply(seq, delta) that implements these rules. The session states are needsYou, error, working, idle, parked and done.

Other Notifications

Notification Sent when Params
warroom/delta After warroom/subscribe. seq, delta
device/paired The first WebSocket connection of a new device. token, device
session/event During sessions/resume, before its response. request_id, provider, session_id, seq, event

The ADR for themes names a notification theme/changed. The daemon does not send it yet. Call theme/list again to see changes.


Methods

The schema has 77 methods, and the daemon has three more (sessions/*). Parameters with ? are optional. Result types are named in the schema $defs. The daemon returns -32004 for a method that needs the war-room engine on a daemon that does not run it.

Two methods work only with aivoryd serve --demo: session/resume and session/handoff. A live daemon answers -32004.

Daemon

Method Params Result
daemon/info none name, version, protocol (jsonrpc-2.0/ndjson), contract, pid, demo, host, cwd
daemon/shutdown none ok
daemon/restart daemon ok. Local only.
daemon/scale daemon, maxRunning Autopilot
resources/memory none totalMb, usedMb, agentsMb, samples, top
organizer/start codebase?, composer?, mode? id of the new session

War Room and Autopilot

Method Params Result
warroom/snapshot none WarRoom: epoch, cursor, now, codebases, sessions, activity, autopilot, queued, inboxOpen, usage, hub, relay, board
warroom/subscribe cursor?, epoch? epoch, cursor, snapshot?, deltas?
autopilot/get none Autopilot: on, parkAfterMin, maxRunning, maxPerCodebase, startQueued, order
autopilot/set any of the Autopilot fields Autopilot

Sessions

Here id is a session id from warroom/snapshot.

Method Params Result
session/send id, text, subagent?, attachments?, composer? ok
session/answer id, ask (the id of the open ask), option?, text? ok. A late answer for an ask that is closed gives -32006.
session/pause id ok. Interrupts the turn. The session stays live.
session/stop id ok. Kills the agent process.
session/park id ok. Stops the session and keeps it resumable.
session/resume id ok. Demo only.
session/new codebase, composer (mode, provider, model, effort), prompt, part?, title?, task?, attachments? id
session/dispatch prompt, codebase?, part?, agent?, model?, mode?, effort? session? or task?. With a free autopilot slot the daemon starts a session. Else it queues a task.
session/mode id, mode (plan, edit, auto) ok
session/handoff id, provider, model? id. Demo only.
session/fork id, at?, prompt?, provider? id
session/history id, before?, limit?, subagent? HistoryPage: lines, start, total
session/spans id SessionTrace: nodes, edges, spans, stats
session/diff id SessionDiff: files
sessions/list provider?, cwd?, limit? sessions, stats. Native sessions of the CLIs.
sessions/get id, provider? A session summary.
sessions/resume id, prompt, provider?, model?, include_raw? status, exit_code, events. Sends session/event notifications first.

The providers of sessions/* are claude, codex and gemini. The war-room methods also name opencode and goose.

Inbox and Tasks

Method Params Result
inbox/list filter? (all, answers, errors, reviews, conflicts) items, done
inbox/answer id, option?, text? The InboxItem with its result
inbox/snooze minutes, id? ok
tasks/list codebase? tasks
tasks/create codebase, part, title, prompt, playbook (feature, fix, port), when (now, queue, planOnly), priority?, provider?, mode?, composer? Task
tasks/update id, column? (planned, working, waiting, review, done), title?, prompt?, part? Task. A move to working does not start a session.
tasks/start id, composer? Task. A task that is not planned gives -32007.

Repos and Projects

Method Params Result
repos/list none codebases
repos/get id RepoDetail: codebase, parts, files, activity
repos/map id MapFile: the .aivory/map.toml of the codebase
repos/add path Codebase. Local only. A folder that is no git repository gives -32602.
repos/remove id ok. Local only.
project/new name, parent, template? (empty, rust, node, python) Codebase. Local only.
map/addPart codebase, name, paths MapFile. Local only.

Settings, Providers and Devices

Method Params Result
settings/get none SettingsView: settings, account, budget, daemons, devices, providers
settings/set key (dotted camelCase, for example display.look), value Settings. The key must exist and the value must fit.
settings/importClaude none ImportReport. Local only.
providers/addAcp name, command, args?, env? provider, found, version?. Local only.
providers/removeAcp id ok. Local only.
devices/pair none code, expires, url
devices/remove id ok
devices/push token, platform (apns, fcm), pushKey, id? ok. Needs --remote or --iroh.

Code Intelligence and the Ledger

Pass session to read the worktree of a session. The schema has the types of the positions, ranges and results.

Method Params Result
code/file codebase, path, lines?, rev?, session? CodeFile: text, language, highlights, outline, folding, diagnostics
code/definition codebase, path, line, col, page?, perPage?, session? PlacePage
code/references the same PlacePage
code/hover the same Hover
code/symbols codebase, query, kind?, page?, perPage?, session? SymbolPage
code/diagnostics codebase, path?, turn?, session? rows, added, removed, notes
code/rename codebase, path, line, col, newName, session? RenamePreview. The daemon never applies it.
ledger/turns session TurnList
ledger/blame codebase, path, lines?, session? FileBlame: the commit and the turn of each line
ledger/timeline codebase, path, session? Timeline
ledger/revert session, turn RevertResult. A session in the main checkout gives -32007.

Coordination

Method Params Result
claims/list codebase?, session? claims
radar/list codebase? hits, sessions
radar/ping to, kind (fyi, question, request, block), text, from?, keys? ok
mail/send the same as radar/ping ok
mail/list session items
block/override session, keys? resolved

Themes and Registries

A job method returns {"job": "<id>"}. Poll theme/job with that id until state is done or failed. A done job has result.

Method Params Result
theme/list none themes: id, name, version, author, license, source, edited, error?
theme/get id, display (desktop, phone, fold, watch) ThemeBundle: tokens, layouts (the root node of each screen), parts, strings, fonts, sha256, preview?. Your client resolves use nodes with parts.
theme/asset id, path path, bytes, base64
theme/inspect source A job. The result holds the token for theme/add.
theme/add token InstalledTheme
theme/remove id ok, clearedCurrent
theme/checkUpdates none A job
theme/update id A job
theme/diff id A job
theme/job job state (pending, done, failed), result?, error?
registry/list none registries
registry/add name, url RegistryEntry
registry/remove name ok
registry/refresh name? A job

To draw a theme in your client, read the theme reference and call theme/get with your display class. The settings key display.customTheme holds the id of the theme that the user selected.


Examples

These calls run against aivoryd serve --demo. The commands print one response per line.

Read the Daemon

aivoryd call daemon/info --endpoint docs-try

Request:

{"jsonrpc":"2.0","id":1,"method":"daemon/info"}

Response:

{"id":1,"jsonrpc":"2.0","result":{"contract":"0.1.0","cwd":"C:\\work","demo":true,"host":"CAPTIVA","name":"aivoryd","pid":7972,"protocol":"jsonrpc-2.0/ndjson","version":"0.1.0"}}

contract is the version of the protocol. cwd is shortened here. cwd, host and pid change on each run.

Change the Autopilot

aivoryd call autopilot/set --params '{"maxRunning":10}' --endpoint docs-try

Request and response:

{"jsonrpc":"2.0","id":1,"method":"autopilot/set","params":{"maxRunning":10}}
{"id":1,"jsonrpc":"2.0","result":{"maxPerCodebase":4,"maxRunning":10,"on":true,"order":"priority","parkAfterMin":15,"startQueued":true}}

The result is the whole Autopilot object. Fields that you leave out do not change.

Handle an Error

aivoryd call session/pause --params '{"id":"zzz"}' --endpoint docs-try

Response:

{"error":{"code":-32001,"message":"no session 'zzz'"},"id":1,"jsonrpc":"2.0"}

An unknown method gives -32601:

{"error":{"code":-32601,"message":"unknown method 'nope/x'"},"id":1,"jsonrpc":"2.0"}

Error Codes

Code Name Meaning
-32700 PARSE_ERROR The line is no valid JSON. id is null.
-32600 INVALID_REQUEST jsonrpc is not "2.0", or method is missing.
-32601 METHOD_NOT_FOUND The method does not exist.
-32602 INVALID_PARAMS The params do not fit the method.
-32000 SERVER_ERROR An error inside the daemon.
-32001 SESSION_NOT_FOUND No session with this id. On the iroh link, -32001 also means UNAUTHORIZED for link/hello.
-32002 SESSION_AMBIGUOUS More than one session matches. data.candidates lists them.
-32003 SESSION_HAS_NO_CWD The session has no working folder.
-32004 NOT_AVAILABLE This daemon cannot do it: a local-only method from a paired device, or a method that needs the war-room engine or a service that does not run.
-32005 NOT_FOUND No inbox item, task, codebase or device with this id.
-32006 STALE_ASK The answer is for an ask that is no longer open.
-32007 INVALID_STATE The session or task cannot do this in its state.

An error has code, message and sometimes data.


Versioning

Item Where Value now
Transport daemon/info field protocol jsonrpc-2.0/ndjson
Protocol version daemon/info field contract, and version in the schema 0.1.0
Feed epoch warroom/snapshot field epoch Changes when the daemon restarts
Theme catalog catalog in theme.toml 1

The protocol is at version 0.x and may change between releases. Read contract after you connect and compare it with the schema that you built against. Ignore unknown fields in results and unknown type values of a delta. The JSON Schema file is generated from the Rust types and is the source of truth: proto/schema/aivory-protocol.schema.json. The folder proto/fixtures/ of the repository has one real result for each demo method. Protobuf frames are planned. The wire format today is JSON-RPC 2.0.


Next Steps