shepherd-launcher/crates/shepherd-ipc
2026-02-07 17:47:16 -05:00
..
src Implement connection check 2026-02-07 17:47:16 -05:00
Cargo.toml (Hopefully) productionized shepherdd 2025-12-26 15:35:27 -05:00
README.md Add crate documentation 2025-12-29 16:54:57 -05:00

shepherd-ipc

IPC layer for Shepherd.

Overview

This crate provides the local inter-process communication infrastructure between the Shepherd service (shepherdd) and its clients (launcher UI, HUD overlay, admin tools). It includes:

  • Unix domain socket server - Listens for client connections
  • NDJSON protocol - Newline-delimited JSON message framing
  • Client management - Connection tracking and cleanup
  • Peer authentication - UID-based role assignment
  • Event broadcasting - Push events to subscribed clients

Architecture

┌─────────────────────────────────────────────────────────┐
│                      shepherdd                          │
│  ┌──────────────────────────────────────────────────┐   │
│  │                  IpcServer                       │   │
│  │  ┌──────────┐ ┌─────────┐ ┌─────────┐            │   │
│  │  │Client 1  │ │Client 2 │ │Client 3 │ ...        │   │
│  │  │(Launcher)│ │ (HUD)   │ │ (Admin) │            │   │
│  │  └────┬─────┘ └────┬────┘ └────┬────┘            │   │
│  │       │            │           │                 │   │
│  │       └────────────┴───────────┘                 │   │
│  │              Unix Domain Socket                  │   │
│  └──────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────┘
         │              │              │
    ┌────┴────┐    ┌────┴────┐    ┌────┴────┐
    │Launcher │    │   HUD   │    │  Admin  │
    │   UI    │    │ Overlay │    │  Tool   │
    └─────────┘    └─────────┘    └─────────┘

Server Usage

Starting the Server

use shepherd_ipc::IpcServer;

let mut server = IpcServer::new("/run/shepherdd/shepherdd.sock");
server.start().await?;

// Get message receiver for the main loop
let mut messages = server.take_message_receiver().await.unwrap();

// Accept connections in background
tokio::spawn(async move {
    server.run().await
});

// Process messages in main loop
while let Some(msg) = messages.recv().await {
    match msg {
        ServerMessage::Request { client_id, request } => {
            // Handle request, send response
            let response = handle_request(request);
            server.send_response(&client_id, response).await?;
        }
        ServerMessage::ClientConnected { client_id, info } => {
            println!("Client {} connected as {:?}", client_id, info.role);
        }
        ServerMessage::ClientDisconnected { client_id } => {
            println!("Client {} disconnected", client_id);
        }
    }
}

Broadcasting Events

use shepherd_api::Event;

// Send to all subscribed clients
server.broadcast_event(Event::new(EventPayload::StateChanged(snapshot))).await;

Client Roles

Clients are assigned roles based on their peer UID:

UID Role Permissions
root (0) Admin All commands
Service user Admin All commands
Other Shell Read + Launch/Stop
// Role-based command filtering
match (request.command, client_info.role) {
    (Command::ReloadConfig, ClientRole::Admin) => { /* allowed */ }
    (Command::ReloadConfig, ClientRole::Shell) => { /* denied */ }
    (Command::Launch { .. }, _) => { /* allowed for all */ }
    // ...
}

Client Usage

Connecting

use shepherd_ipc::IpcClient;

let mut client = IpcClient::connect("/run/shepherdd/shepherdd.sock").await?;

Sending Commands

use shepherd_api::{Command, Response};

// Request current state
client.send(Command::GetState).await?;
let response: Response = client.recv().await?;

// Launch an entry
client.send(Command::Launch { 
    entry_id: "minecraft".into() 
}).await?;
let response = client.recv().await?;

Subscribing to Events

// Subscribe to event stream
client.send(Command::SubscribeEvents).await?;

// Receive events
loop {
    match client.recv_event().await {
        Ok(event) => {
            match event.payload {
                EventPayload::WarningIssued { remaining, .. } => {
                    println!("Warning: {} seconds remaining", remaining.as_secs());
                }
                EventPayload::SessionEnded { .. } => {
                    println!("Session ended");
                }
                _ => {}
            }
        }
        Err(IpcError::ConnectionClosed) => break,
        Err(e) => eprintln!("Error: {}", e),
    }
}

Protocol

Message Format

Messages use NDJSON (newline-delimited JSON):

{"type":"request","id":1,"command":"get_state"}\n
{"type":"response","id":1,"payload":{"api_version":1,...}}\n
{"type":"event","payload":{"type":"state_changed",...}}\n

Request/Response

Each request has an ID, matched in the response:

// Request
{"type":"request","id":42,"command":{"type":"launch","entry_id":"minecraft"}}

// Response
{"type":"response","id":42,"success":true,"payload":{...}}

Events

Events are pushed without request IDs:

{"type":"event","payload":{"type":"warning_issued","threshold":60,"remaining":{"secs":60}}}

Socket Permissions

The socket is created with mode 0660:

  • Owner can read/write
  • Group can read/write
  • Others have no access

This allows the service to run as a dedicated user while permitting group members (e.g., shepherd group) to connect.

Rate Limiting

Per-client rate limiting prevents buggy or malicious clients from overwhelming the service:

// Default: 10 commands per second per client
if rate_limiter.check(&client_id) {
    // Process command
} else {
    // Respond with rate limit error
}

Error Handling

use shepherd_ipc::IpcError;

match result {
    Err(IpcError::ConnectionClosed) => {
        // Client disconnected
    }
    Err(IpcError::Json(e)) => {
        // Protocol error
    }
    Err(IpcError::Io(e)) => {
        // Socket error
    }
    _ => {}
}

Dependencies

  • tokio - Async runtime
  • serde / serde_json - JSON serialization
  • nix - Unix socket peer credentials
  • shepherd-api - Message types
  • shepherd-util - Client IDs