Daemon Protocol & Rust Client
Headless daemons and service bridges (such as matrix-xmsg, genie-guard, and xmsg-mcp-gateway)
connect to xmsg as attested services (<name>@<node>) over register.sock and agent.sock.
Service wire protocol (register.sock)
Section titled “Service wire protocol (register.sock)”To prevent arbitrary processes from squatting service names, xmsg serve requires
--svc-exe <NAME>=<PATH> (or XMSG_SVC_EXE) on host nodes, or --leaf --leaf-principal svc:<NAME>
in container leaf mode.
- Registration frame:
The service connects to
$XDG_RUNTIME_DIR/xmsg/register.sockand sends:{ "harness": "svc", "name": "genie-expert", "cwd": "/optional/working/dir" }xmsgverifies the peer UID (peer_uid == server_uid) and the canonical executable path (/proc/<pid>/exeon Linux,proc_pidpathon macOS), then answers with the service address, node name, and effective server limits:{"status": "ok","address": "genie-expert@alice.workstation","node": "alice.workstation","reply_ttl_secs": 604800,"idempotency_ttl_secs": 86400,"svc_queue_cap": 100,"max_body_bytes": 65536} - Long-poll (
poll):Returns either{ "action": "poll", "waitSecs": 30 }{"action": "timeout"}or a delivery frame with structuredoriginattestation:{"action": "deliver","messageId": "01M4FG...","fromName": "sender · 6f1c2a7e-...@alice.workstation","origin": {"id": "6f1c2a7e-...","node": "alice.workstation","label": "sender"},"text": "Task payload...","envelope": "[xmsg] from=... message_id=...\n\nTask payload...","inReplyTo": null} - Explicit acknowledgment (
ack) andreply:{ "action": "ack", "messageId": "01M4FG..." }A message’s queue row is marked{ "action": "reply", "messageId": "01M4FG...", "text": "Done.", "idempotencyKey": "opt-key" }deliveredonly whenackis received.xmsgtracksin_flightmessage IDs per connection so pipelined polls receive the next queued message in FIFO order, and any unacknowledged messages redeliver if the connection drops.
Rust service client (xmsg::client)
Section titled “Rust service client (xmsg::client)”External Rust services can depend on xmsg with default-features = false, features = ["client"]
(which pulls in 23 packages instead of the full 176-package daemon graph) to use the validated
xmsg::client transport:
pub const SCHEMA_VERSION: u32 = 2;
#[derive(Debug, Error)]pub enum ClientError { #[error("invalid client configuration")] Configuration, #[error("untrusted local socket")] Peer, #[error("xmsg transport I/O failed")] Io(#[from] std::io::Error), #[error("xmsg transport deadline exceeded; submission may be unknown")] Timeout, #[error("invalid xmsg protocol response")] Protocol, #[error("xmsg refused request: {code}")] Refused { code: String }, #[error("service is disconnected")] Disconnected,}
#[derive(Clone, Debug)]pub struct ClientOptions { pub register_socket: PathBuf, pub agent_socket: PathBuf, pub max_frame_bytes: usize, pub operation_timeout: Duration, pub service: String, pub expected_node: Option<String>, pub daemon_uid: u32,}
#[derive(Clone, Debug, Default)]pub struct ListQuery { pub name: Option<String>, pub node: Option<String>, pub kind: Option<String>, pub status: Option<String>, pub within: Option<u64>,}
#[derive(Clone, Debug)]pub struct Delivery(DeliveryFrame);
impl Delivery { pub fn message_id(&self) -> &str { &self.0.message_id } pub fn from_name(&self) -> &str { &self.0.from_name } pub fn text(&self) -> &str { &self.0.text } pub fn envelope(&self) -> &str { &self.0.envelope } pub fn origin(&self) -> &Origin { &self.0.origin } pub fn in_reply_to(&self) -> Option<&str> { self.0.in_reply_to.as_deref() }}Registration and AgentClient
Section titled “Registration and AgentClient”- Registering (
RegisteredService::register(ClientOptions)): Checks socket-directory ownership and mode (0700), socket ownership and mode (0600), peer UID (SO_PEERCRED/getpeereid), and verifies that the returned address equals<service>@<reported-node>(and matchesexpected_nodewhenSome(node)is configured). - Concurrent agent operations (
agent_client()): KeepRegisteredServicealive forpollandack. Callingregistered.agent_client()returns a clonableAgentClienthandle forlist,send, andreplyoveragent.sock(using schema version 2 frames) without serializing behind an active long-poll. - Authorization boundary:
Delivery::origin()exposes the validatedOrigin(id,service,node,label). Display fields (from_name,envelope,label) are never promoted to authenticated identity; callers must authorize onorigin().service,origin().id, andorigin().node, and callackonly after recording the delivery durably.
Nonblocking registration health (health())
Section titled “Nonblocking registration health (health())”Both RegisteredService::health() and AgentClient::health() return a point-in-time,
nonblocking, non-consuming snapshot of the registration socket under a short descriptor-lifetime
mutex:
- On Linux, performs 2 nonblocking syscalls: zero-timeout
pollplus a 1-byterecv(MSG_PEEK | MSG_DONTWAIT). - On macOS, performs 4 nonblocking syscalls:
kqueue, zero-timeoutkeventcheckingEV_EOF,close, plusrecv(MSG_PEEK | MSG_DONTWAIT). - Peer close or write-half shutdown returns
ClientError::Disconnected, even when unread bytes precede EOF. AgentClientoperations checkhealth()before submitting a frame onagent.sockand recheck it after any I/O error or timeout. When a post-failure recheck returnsClientError::Disconnected, request submission remains unknown, so callers retrying a mutation after reconnecting must reuse their storedidempotency_key.
Palettes adapted from Catppuccin (Macchiato) (MIT), Tokyo Night (Apache-2.0), gruvbox (MIT), Catppuccin (Latte) (MIT), Rosé Pine (Dawn) (MIT).