Agent socket API
The implemented JSON-RPC methods and events on Canvas's local Unix socket.
The running Canvas app serves a small JSON-RPC 2.0 API on a local Unix domain socket. Use it to read live panel state, present a note, save and restore desks, and receive change events.
This page documents only the methods that Canvas implements today. The broader design in Agent API proposal is proposed and is not built. Do not call a method from that proposal unless it is listed here.
For most work, prefer the canvas CLI. The CLI works when the
app is closed. The socket works only while the app is running.
Connection
| Property | Value |
|---|---|
| Transport | Unix domain socket, SOCK_STREAM |
| Path | ~/Library/Application Support/Blink/blink.sock, or the first nonempty $CANVAS_HOME/blink.sock / $BLINK_HOME/blink.sock override |
| Access control | File mode 0600. Only the user who runs Canvas can connect. There is no token. |
| Framing | One JSON object per line, terminated by \n, in both directions |
| Protocol | JSON-RPC 2.0. Every request must include "jsonrpc": "2.0" and a string method. |
| Lifetime | Canvas creates the socket at launch and removes it at quit. |
The client and the app must use the same home directory. If the socket file does
not exist, Canvas is not running with that home. The CLI reports this condition
as Blink is not running with the same BLINK_HOME (missing blink.sock).
Private access
Canvas 2.3.0 keeps this socket path and protocol, with no token. It enforces the following checks.
- Canvas creates its home directory with mode
0700and refuses symlinks in the path. - The app accepts a connection only from a peer with the same user ID.
- The CLI refuses a socket, or a socket directory, that another user owns or that is group- or world-accessible.
Canvas's agent socket does not move into ~/.lattices/run or open a TCP port.
Canvas's paired iOS note sync remains a separate Multipeer Connectivity service.
A paired tailnet client can call a reviewed subset of agent methods through the
opt-in Lattices gateway using the product identifier blink, one exact grant
per method: system.hello,
placements.list, notes.list and notes.get to read, and notes.present,
desk.save and desk.restore to act. Canvas event streams stay local. See
Private IPC and the gateway.
Example with nc:
printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"system.hello"}' \
| nc -U "$HOME/Library/Application Support/Blink/blink.sock"Methods
| Method | Params | Result |
|---|---|---|
system.hello |
none | { "protocol": "2.0", "version": "<app version>", "app": "Blink" } |
placements.list |
none | Array of open panels (see below). |
notes.list |
none | Array of { "id", "title", "updated" }. updated is ISO 8601. |
notes.get |
id (string, required), content (bool, optional) |
{ "id", "title", "frontmatter" }, plus content when content is true. frontmatter holds the foreign frontmatter lines that Canvas does not own. |
notes.present |
id (string, required), content (string), slot (integer), style (string) |
{ "id" } |
desk.save |
name (string, required) |
{ "name", "updated", "panels" }. panels is the panel count. |
desk.restore |
name (string, required) |
{ "name", "updated", "panels" } |
events.subscribe |
topics (optional) |
{ "subscribed": <topics> } |
placements.list
Each entry describes one open panel:
{
"id": "standup",
"frame": { "x": 980, "y": 412, "width": 460, "height": 260 },
"screen": 1,
"mode": "read",
"shaded": false,
"focused": true,
"z": 0,
"slot": 6
}frameis in AppKit screen coordinates (origin at the bottom left of the main display). This differs fromblink desk open, which takes display-local coordinates from the top left.screenis a one-based display index.zis the front-to-back order.0is the frontmost panel.slotisnullwhen the panel has no grid slot.
notes.present
notes.present creates the note if it does not exist, then opens or focuses its
panel.
- Canvas converts
idto a slug. contentreplaces the whole body. If you omit it, the body does not change.slotsetsblink.slot.stylesetsblink.style. Other presentation keys do not change.- Canvas records the writer as
agent-socketinblink.lastWriterand in the edit ledger.
The CLI command blink present covers more presentation fields and does not
need the running app.
desk.save and desk.restore
desk.save captures the open panels into desks/<name>.json. desk.restore
reopens the panels in that file at their saved frames. A desk name must be 1 to
80 characters of letters, digits, -, or _.
blink desk save <name> and blink desk restore <name> call these methods.
blink desk ls, blink desk show, and blink desk rm read and delete the desk
files directly and do not use the socket.
events.subscribe
After events.subscribe, the app sends notifications on the same connection.
Notifications have no id:
{ "jsonrpc": "2.0", "method": "panel.moved", "params": { "id": "standup" } }| Event | Sent when |
|---|---|
note.created |
A note is created. |
note.updated |
A note's content or metadata changes. |
note.deleted |
A note is deleted. |
panel.moved |
A panel moves, including a slot move. |
panel.resized |
A panel is resized. |
Canvas does not filter by topics today. A subscribed connection receives every
event in this table. Each event carries only the note id. Call notes.get or
placements.list to read the new state.
Errors
Errors use the JSON-RPC error object:
{ "jsonrpc": "2.0", "id": 1, "error": { "code": -32004, "message": "Note not found" } }| Code | Meaning |
|---|---|
-32600 |
The line is not a valid JSON-RPC 2.0 request. The reply id is null. |
-32601 |
The method does not exist. |
-32602 |
A required parameter is missing. |
-32004 |
notes.get did not find the note. |
-32000 |
Another failure, such as an invalid desk name or a missing desk. The message gives the cause. |
Not implemented
These items appear in the proposal and do not exist:
- Authentication tokens, capability negotiation, and version ranges beyond
system.hello. - Topic filtering and richer event payloads.
autoplacement, collision routing, scenes, andtidy. See Placement.- An MCP server or SDK. See Agent integration proposal.