shepherd-launcher/crates/shepherd-api
Albert Armea 9da95a27b3 Add "steam"-specific type
This implementation allows each platform to choose how to launch Steam (on Linux, we use the snap as the examples suggested before), and keeps Steam alive after an activity exits so that save sync, game updates, etc. can continue to run.

Change written by Codex 5.2 on medium:

Consider this GitHub issue https://github.com/aarmea/shepherd-launcher/issues/4. On Linux, an activity that uses the "steam" type should launch Steam via the snap as shown in the example configuration in this repository.

Go ahead and implement the feature. I'm expecting one of the tricky bits to be killing the activity while keeping Steam alive, as we can no longer just kill the Steam snap cgroup.
2026-02-07 16:22:55 -05:00
..
src Add "steam"-specific type 2026-02-07 16:22:55 -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-api

Protocol types for Shepherd IPC communication.

Overview

This crate defines the stable API between the Shepherd service (shepherdd) and its clients (launcher UI, HUD overlay, admin tools). It contains:

  • Commands - Requests from clients to the service
  • Responses - Service replies to commands
  • Events - Asynchronous notifications from service to clients
  • Shared types - Entry views, session info, reason codes, etc.

Purpose

shepherd-api establishes the contract between components, ensuring:

  1. Stability - Versioned protocol with backward compatibility
  2. Type safety - Strongly typed messages prevent protocol errors
  3. Decoupling - Clients and service can evolve independently

API Version

use shepherd_api::API_VERSION;

// Current API version
assert_eq!(API_VERSION, 1);

Key Types

Commands

Commands are requests sent by clients to the service:

use shepherd_api::Command;

// Request available entries
let cmd = Command::ListEntries;

// Request to launch an entry
let cmd = Command::Launch { 
    entry_id: "minecraft".into() 
};

// Request to stop current session
let cmd = Command::StopCurrent { 
    mode: StopMode::Graceful 
};

// Subscribe to real-time events
let cmd = Command::SubscribeEvents;

Available commands:

  • GetState - Get full service state snapshot
  • ListEntries - List all entries with availability
  • Launch { entry_id } - Launch an entry
  • StopCurrent { mode } - Stop the current session
  • ReloadConfig - Reload configuration (admin only)
  • SubscribeEvents - Subscribe to event stream
  • GetHealth - Get service health status
  • SetVolume { level } - Set system volume
  • GetVolume - Get current volume

Events

Events are pushed from the service to subscribed clients:

use shepherd_api::{Event, EventPayload};

// Events received by clients
match event.payload {
    EventPayload::StateChanged(snapshot) => { /* Update UI */ }
    EventPayload::SessionStarted(info) => { /* Show HUD */ }
    EventPayload::WarningIssued { threshold, remaining, severity, message } => { /* Alert user */ }
    EventPayload::SessionExpired { session_id } => { /* Time's up */ }
    EventPayload::SessionEnded { session_id, reason } => { /* Return to launcher */ }
    EventPayload::PolicyReloaded { entry_count } => { /* Refresh entry list */ }
    EventPayload::VolumeChanged(info) => { /* Update volume display */ }
}

Entry Views

Entries as presented to UIs:

use shepherd_api::EntryView;

let view: EntryView = /* from service */;

if view.enabled {
    // Entry can be launched
    println!("Max run time: {:?}", view.max_run_if_started_now);
} else {
    // Entry unavailable, show reasons
    for reason in &view.reasons {
        match reason {
            ReasonCode::OutsideTimeWindow { next_window_start } => { /* ... */ }
            ReasonCode::QuotaExhausted { used, quota } => { /* ... */ }
            ReasonCode::CooldownActive { available_at } => { /* ... */ }
            ReasonCode::SessionActive { entry_id, remaining } => { /* ... */ }
            // ...
        }
    }
}

Session Info

Information about active sessions:

use shepherd_api::{SessionInfo, SessionState};

let session: SessionInfo = /* from snapshot */;

match session.state {
    SessionState::Launching => { /* Show spinner */ }
    SessionState::Running => { /* Show countdown */ }
    SessionState::Warned => { /* Highlight urgency */ }
    SessionState::Expiring => { /* Terminating... */ }
    SessionState::Ended => { /* Session over */ }
}

Reason Codes

Structured explanations for unavailability:

  • OutsideTimeWindow - Not within allowed time window
  • QuotaExhausted - Daily time limit reached
  • CooldownActive - Must wait after previous session
  • SessionActive - Another session is running
  • UnsupportedKind - Host doesn't support this entry type
  • Disabled - Entry explicitly disabled in config

Design Philosophy

  • Service is authoritative - Clients display state, service enforces policy
  • Structured reasons - UIs can explain "why is this unavailable?"
  • Event-driven - Clients subscribe and react to changes
  • Serializable - All types derive Serialize/Deserialize for JSON transport

Dependencies

  • serde - Serialization/deserialization
  • chrono - Timestamp types
  • shepherd-util - ID types