Skip to main content

Socket Protocol

Pad-Lattice clients communicate with the daemon through Wire Protocol 1: newline-delimited JSON over a local Unix stream socket. It carries agent state, session lifecycle, status, leases, capabilities, and targeted actions.

This contract is separate from Visual Protocol 1, device profile schema 1, and the browser-facing Web Surface Protocol. The wire protocol moves agent semantics; it does not describe LEDs, browser pixels, or MIDI addresses.

Prefer the Python client

Python integrations should use PadLatticeClient. It supplies protocol versions, framing, typed identities, states, actions, and bounded reads. Use raw JSON only when integrating another language or transport adapter.

Python Client

from pad_lattice import AgentIdentity, AgentState, PadLatticeClient

agent = AgentIdentity("my-agent", "session-42")
client = PadLatticeClient()
client.report_state(
AgentState.RUNNING,
agent=agent,
metadata={"label": "indexer"},
)

Subscribe to actions with a context manager so disconnecting removes the advertised capabilities:

from pad_lattice import ControlAction

with client.subscribe_actions(
agent,
(ControlAction.STOP,),
request_id="task-7",
one_shot=True,
) as subscription:
event = subscription.receive()

The public client also provides status(), ping(), and end_session().

Transport

The default socket path is resolved in this order:

  1. PAD_LATTICE_SOCKET
  2. $XDG_RUNTIME_DIR/pad-lattice.sock
  3. /tmp/pad-lattice-<uid>.sock

The daemon creates the socket with mode 0600. On Linux it also compares the kernel-reported peer UID with its own before accepting a client. It is a local same-user control channel, not a network service or a sandbox boundary. Do not proxy it or share an OS account with untrusted agent processes.

Every frame is one UTF-8 JSON object followed by \n, with a maximum encoded size of 64 KiB. Every command and response includes the integer wire version:

{"protocol":1,"type":"ping"}

A missing or unsupported version receives an unsupported_protocol error.

Domain Values

An agent is identified only by the pair (backend, session_id):

{"backend":"codex","session_id":"019f28ff-..."}

Additional non-empty string fields on an agent in a state message are display metadata, not identity.

StatesActions
runningstop
waiting_for_replyNone
user_typingNone
waiting_for_approvalapprove, reject
successNone
errorretry
cancelledretry

An action is available only when the state allows it and a connected client advertises that capability for the selected agent.

Message Summary

Client messageResponseConnection lifetime
stateOptional state_ackUsually short
session_endNoneShort
statusstatusShort
pingpongShort
subscribe_actionsZero or more action eventsLong-lived
session_leasesession_lease_ack, then optional binding updatesProcess lifetime
previewpreview_ackTemporary diagnostic connection
preview_endpreview_end_ackSame preview connection

State and Lifecycle

Send a state update:

{
"protocol": 1,
"type": "state",
"state": "running",
"agent": {
"backend": "codex",
"session_id": "019f28ff-...",
"label": "implementation"
},
"lease_id": "09f3...",
"reply": true
}

agent defaults to local/default for manual clients. lease_id binds the state to a launcher lease. With reply: true, the daemon returns a state_ack containing the identity, state, zero-based slot, one-based Scene, accent, selection, lease status, and label. A slot and Scene are null in overflow.

Remove a manually managed session explicitly:

{
"protocol": 1,
"type": "session_end",
"agent": {"backend":"codex","session_id":"019f28ff-..."}
}

Ending the selected session clears selection. The daemon never silently retargets another agent.

Targeted Actions

Keep this connection open while a client can consume the advertised actions:

{
"protocol": 1,
"type": "subscribe_actions",
"agent": {"backend":"codex","session_id":"019f28ff-..."},
"actions": ["approve", "reject"],
"request_id": "c42a...",
"one_shot": true
}

There is no subscription acknowledgement. Disconnecting immediately removes its capabilities and darkens controls that no other subscriber can consume.

A matching press produces one event:

{
"protocol": 1,
"type": "action",
"action": "approve",
"agent": {"backend":"codex","session_id":"019f28ff-..."},
"request_id": "c42a..."
}

Delivery requires all four gates:

  1. The target identity is selected.
  2. Its current state permits the action.
  3. A live subscriber advertises the action for that exact identity.
  4. The press is outside that identity/action debounce window.

Request-scoped subscribers take priority over unscoped listeners. Within each class, the oldest match receives the press. Actions are never broadcast, and a one-shot subscription is disabled before its first delivery.

Session Leases

A launcher can make socket lifetime represent process ownership:

{
"protocol": 1,
"type": "session_lease",
"lease_id": "09f3...",
"metadata": {"label":"implementation","cwd":"/work/pad-lattice"}
}

The daemon acknowledges the lease. A later state message carrying the same lease_id binds it to the real agent identity and produces a session_lease_bound update. Reconnecting launchers may include that known agent in the lease command.

Closing the active lease connection removes its session when no other live lease owns that identity. Replacing a connection with the same lease ID transfers ownership safely.

Inspection and Previews

{"protocol":1,"type":"status"} returns selection, sessions, visible slots, accents, leases, overflow, TTL, preview status, optional-audio status, and a surfaces array. Each surface entry identifies its kind (web or midi), profile, Visual Protocol version, and sanitized input/output description. The experience field is demo, show, or null. {"protocol":1,"type":"ping"} returns {"protocol":1,"type":"pong"}.

preview and preview_end are bounded diagnostic messages used by pad-lattice symbols. A preview has an owner connection, unique ID, semantic state, and TTL of at most 30 seconds. It never mutates authoritative session state, and disconnect or timeout restores the real view.

Errors and Schema

Rejected input receives a machine-readable error:

{
"protocol": 1,
"type": "error",
"code": "unknown_message_type",
"error": "unknown message type: example"
}

The authoritative runtime implementation is the typed builders and parsers in protocol.py. The packaged Protocol 1 JSON Schema supports external clients, editors, and conformance tools. The daemon performs small direct checks on live messages; it does not run JSON Schema validation in the real-time path.

Browser Protocol Boundary

The browser does not connect to Wire Protocol 1 and cannot send arbitrary agent state. web_surface.py exposes a narrower Web Surface Protocol over a same-origin WebSocket. Authenticated clients receive compiled visual frames and sanitized labels; they may request only Scene selection, currently available actions, and local-admin operations. Only an authenticated loopback administrator may create/revoke pairing or start/stop Demo and Show. Paired clients may still send Scene and action input while Demo is active.

Server messages include semantic surface snapshots, experience lifecycle, and exact full-surface RGB frames during Show. Audio fields identify built-in cue names or packaged assets; they never carry arbitrary URLs or audio bytes.

That protocol has its own packaged Web Surface Protocol 1 JSON Schema. Its runtime uses the same small typed-parser approach and message-size bounds as the local protocol. The two protocols share semantic domain values but are not interchangeable.

Wire Protocol 1, Web Surface Protocol 1, Visual Protocol 1, Device Profile Schema 1, Demo Manifest 1, and Performance Manifest 1 evolve independently. A future incompatible socket format must use a new integer protocol value rather than silently changing Protocol 1.