Core Messages
Message Framing
Section titled “Message Framing”PCP uses a framed stream protocol over Unix domain sockets. Each frame starts with a type tag (u8, 0..255) followed by a length-prefixed payload. On the client socket, payloads serialize as JSON. On the per-app sockets, payloads use a compact binary encoding.
The daemon exposes one client-facing socket and one per-app socket for each Tier 1 application. These are separate transport paths with different message sets, so there is no ambiguity about which encoding applies.
Full Message Registry (27 types)
Section titled “Full Message Registry (27 types)”The protocol defines 27 StreamMessageType variants. Each has a numeric tag, a direction, a liveness status, and a payload type.
| # | Variant | Direction | Live? | Payload |
|---|---|---|---|---|
| 1 | DetectRequest |
client to daemon | yes | DetectRequestPayload { app_id } |
| 2 | DetectResponse |
daemon to client | yes | DetectResponsePayload { result: DetectionResult } |
| 3 | ExecuteRequest |
client to daemon | yes | ExecuteRequestPayload { capability, target?, params, admin } |
| 4 | ExecuteResponse |
daemon to client | yes | ExecuteResponsePayload { result: ExecutionResult } |
| 5 | EventNotification |
daemon to client | yes | PushEventPayload (alias of #16) |
| 6 | RegistryQuery |
client to daemon | yes | RegistryQueryPayload { kind } |
| 7 | RegistryResponse |
daemon to client | yes | RegistryResponsePayload { kind } |
| 8 | Ping |
either direction | yes | PingPayload (empty) |
| 9 | Pong |
daemon to client | yes | PongPayload { capability_count, app_count, subscription_count, journal_length, uptime_secs } |
| 10 | RegisterCapability |
reserved | no | RegisterCapabilityPayload |
| 11 | UnregisterCapability |
reserved | no | UnregisterCapabilityPayload |
| 12 | ExecuteCapability |
reserved | no | alias of #3 |
| 13 | QueryCapabilities |
reserved | no | QueryCapabilitiesPayload { filter } |
| 14 | SubscribeEvents |
client to daemon | yes | SubscribeEventsPayload { filter } |
| 15 | UnsubscribeEvents |
client to daemon | yes | UnsubscribeEventsPayload { subscription_id } |
| 16 | PushEvent |
daemon to client | yes | PushEventPayload { event, sequence } |
| 17 | ErrorResponse |
daemon to client | yes | ErrorResponsePayload { code: u16, message } |
| 18 | CapabilityResponse |
reserved | no | CapabilityResponsePayload { capability? } |
| 19 | InvokeCapability |
daemon to app | yes | binary payload (Tier 1 path) |
| 20 | CapabilityResultMsg |
app to daemon | yes | binary payload |
| 21 | GetManifest |
daemon to app | yes | binary payload |
| 22 | ManifestResponse |
app to daemon | yes | binary payload |
| 23 | ValidateParamsMsg |
daemon to app | yes | binary payload |
| 24 | ValidateResultMsg |
app to daemon | yes | binary payload |
| 25 | QueryState |
daemon to app | yes | binary payload |
| 26 | StateResponse |
app to daemon | yes | binary payload |
| 27 | AppShutdown |
daemon to app | yes | binary payload (no response expected) |
“Reserved” variants exist in the type enum for forward compatibility but are not handled by the current daemon. Sending them on the client socket yields an ErrorResponse with code 400 (“unsupported message type”).
Direction key
Section titled “Direction key”Messages tagged client to daemon travel on the client socket. Messages tagged daemon to client are replies or push events on that same socket. Messages tagged daemon to app and app to daemon travel on per-app sockets, which use binary encoding rather than JSON.
Accepted Client Requests
Section titled “Accepted Client Requests”The daemon’s IPC handler accepts exactly six request types on the client socket:
- Ping (#8) – liveness check, no side effects
- RegistryQuery (#6) – read registry state
- DetectRequest (#1) – trigger adapter-based app detection
- ExecuteRequest (#3) – invoke a capability
- SubscribeEvents (#14) – begin event subscription
- UnsubscribeEvents (#15) – cancel event subscription
Any other message type on the client socket produces ErrorResponse 400.
ExecuteRequest Dispatch
Section titled “ExecuteRequest Dispatch”When the daemon receives an ExecuteRequest (tag 3), it runs the four-step dispatch pipeline:
ExecuteRequest | 1. In-process ownership? | registry lookup to find owner app_id | if registered in InProcessDispatcher, invoke directly | (runtime service capabilities: learning, recovery, inspection, coordination) | 2. Domain == "system" or "compositor"? | delegate to PlatformAdapter | 3. App-domain capability: | a. If Tier 1 socket exists for owner app, invoke via per-app socket | b. Else use AT-SPI2 path: | - accessible_path missing? run element auto-resolution | - registered caps: try adapters in priority order, first success wins | - unregistered caps: run detect() to find owner, then execute once | 4. Timeouts and errors: 30s timeout -> ErrorResponse 504 adapter failure -> ErrorResponse 500 unsupported -> ErrorResponse 400 rate-limited -> ErrorResponse 429Every execution is audited on completion. The audit entry carries a chain hash for tamper detection.
RegistryQuery Kinds
Section titled “RegistryQuery Kinds”A RegistryQuery (tag 6) carries a RegistryQueryKind that tells the daemon what to look up. There are six variants:
| Kind | Fields | Description |
|---|---|---|
Capabilities |
filter: Option<RegistryFilter> |
List capabilities matching a filter. Omit the filter for all. |
CapabilityById |
id: String |
Look up a single capability by its ID. |
Apps |
(none) | List all registered applications with their descriptors. |
EventHistory |
limit: Option<usize> |
Retrieve recent events from the event journal. |
Health |
(none) | Health check. Returns a PongPayload equivalent. |
ElementTree |
app_id: String |
Discover the semantic element tree for an application. |
RegistryResponse Kinds
Section titled “RegistryResponse Kinds”The daemon replies with a RegistryResponseKind that matches the query:
| Kind | Fields | Matches Query |
|---|---|---|
Capabilities |
caps: Vec<CapabilityDescriptor> |
Capabilities, CapabilityById |
Apps |
apps: Vec<AppDescriptor> |
Apps |
Events |
events: Vec<CapabilityEvent> |
EventHistory |
Health |
health: PongPayload |
Health |
Elements |
elements: Vec<ElementInfo> |
ElementTree |
ElementInfo
Section titled “ElementInfo”The ElementTree query returns ElementInfo structs rather than full Element objects. This is a flat, wire-friendly view of the accessibility tree:
pub struct ElementInfo { pub accessible_path: String, pub bus_name: String, pub role: String, pub name: Option<String>, pub actions: Vec<String>, pub states: Vec<String>, pub text: Option<String>,}Each ElementInfo represents one node in the AT-SPI2 accessibility tree. The accessible_path is the D-Bus object path for the node. The daemon’s element auto-resolution splits this path when mapping an ElementId back to a concrete node.
Ping / Pong
Section titled “Ping / Pong”Ping (tag 8) can be sent by either side. The daemon replies with Pong (tag 9), which carries diagnostic counters:
{ "capability_count": 87, "app_count": 12, "subscription_count": 3, "journal_length": 1042, "uptime_secs": 3624}When a Tier 1 app receives a Ping on its per-app socket, it replies with a zeroed Pong.
SubscribeEvents / UnsubscribeEvents
Section titled “SubscribeEvents / UnsubscribeEvents”SubscribeEvents (tag 14) takes a filter and returns an acknowledgement carrying a subscription_id. After that, the daemon pushes PushEvent (tag 16) frames whenever matching events occur. Each push carries the event data and a monotonically increasing sequence number.
UnsubscribeEvents (tag 15) takes a subscription_id and cancels that subscription. No further push events are delivered for it.
ErrorResponse
Section titled “ErrorResponse”The daemon sends ErrorResponse (tag 17) for any failure. The payload carries a numeric code and a human-readable message:
| Code | Meaning |
|---|---|
| 400 | Unsupported message type |
| 404 | Capability or app not found |
| 429 | Rate-limited, slow down |
| 500 | Adapter failure |
| 504 | Execution timed out (30s default) |
The ErrorResponse is the universal failure envelope. There is no separate JSON-RPC error layer; the tag-based framing replaces the JSON-RPC method dispatch model entirely.
Per-App Socket Messages (Tier 1)
Section titled “Per-App Socket Messages (Tier 1)”Tier 1 applications serve the PcpServer trait on their per-app sockets. The daemon sends these messages, and the app replies:
| # | Message | Handler | Reply |
|---|---|---|---|
| 21 | GetManifest |
PcpServer::manifest() |
22: ManifestResponse |
| 19 | InvokeCapability |
PcpServer::invoke() |
20: CapabilityResultMsg |
| 23 | ValidateParamsMsg |
PcpServer::validate_params() |
24: ValidateResultMsg |
| 25 | QueryState |
PcpServer::query_dynamic_state() |
26: StateResponse |
| 27 | AppShutdown |
PcpServer::shutdown() |
(none, connection closes) |
These six messages form the entire Tier 1 contract. Errors within the per-app protocol are carried inside CapabilityResultMsg rather than as separate error frames.
Transport Summary
Section titled “Transport Summary”| Path | Encoding | Used for |
|---|---|---|
| Client socket | JSON | Ping, RegistryQuery, Detect, Execute, Subscribe, Unsubscribe, and all daemon replies |
| Per-app sockets | binary | GetManifest, Invoke, ValidateParams, QueryState, Shutdown |
Both paths carry the same StreamMessageType tags. The difference is payload encoding only.