diff --git a/README.md b/README.md index c5ba425..e791ee6 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ TODO: show the following running with some subset of the above features highligh Contributions are welcome for improvements and not yet implemented backends, such as: +* Content-aware media player [TODO: link to issue] * Pre-booted Steam to improve launch time [TODO: link to issue] * Android apps via Waydroid, including pre-booting Android if necessary [TODO: link to issue] * Legacy Win9x via DOSBox, QEMU, or PCem, including scripts to create a boot-to-app image [TODO: link to issue] diff --git a/config.example.toml b/config.example.toml index f93395e..fcb289f 100644 --- a/config.example.toml +++ b/config.example.toml @@ -236,10 +236,11 @@ end = "20:00" # Omitting limits entirely uses default_max_run_seconds ## === Media === -# Just use `mpv` to play media. +# Just use `mpv` to play media (for now). # Files can be local on your system or URLs (YouTube, etc). # "lofi hip hop radio πŸ“š beats to relax/study to" streamed live from YouTube +# This should eventually be replaced with the media type. [[entries]] id = "lofi-beats" label = "Lofi Beats" diff --git a/crates/shepherd-api/README.md b/crates/shepherd-api/README.md new file mode 100644 index 0000000..0342016 --- /dev/null +++ b/crates/shepherd-api/README.md @@ -0,0 +1,153 @@ +# 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 + +```rust +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: + +```rust +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: + +```rust +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: + +```rust +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: + +```rust +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 diff --git a/crates/shepherd-config/README.md b/crates/shepherd-config/README.md new file mode 100644 index 0000000..e28168c --- /dev/null +++ b/crates/shepherd-config/README.md @@ -0,0 +1,204 @@ +# shepherd-config + +Configuration parsing and validation for Shepherd. + +## Overview + +This crate handles loading, parsing, and validating the TOML configuration that defines what entries are available, when they're available, and for how long. It provides: + +- **Schema definitions** - Raw configuration structure as parsed from TOML +- **Policy objects** - Validated, ready-to-use policy structures +- **Validation** - Detailed error messages for misconfiguration +- **Hot reload support** - Configuration can be reloaded at runtime + +## Configuration Format + +Shepherd uses TOML for configuration. Here's a complete example: + +```toml +config_version = 1 + +[service] +socket_path = "/run/shepherdd/shepherdd.sock" +data_dir = "/var/lib/shepherdd" +default_max_run_seconds = 1800 # 30 minutes default + +# Global volume restrictions +[service.volume] +max_volume = 80 +allow_unmute = true + +# Default warning thresholds (seconds before expiry) +[[service.default_warnings]] +seconds_before = 300 # 5 minutes +severity = "info" + +[[service.default_warnings]] +seconds_before = 60 # 1 minute +severity = "warn" + +[[service.default_warnings]] +seconds_before = 10 +severity = "critical" +message_template = "Closing in {remaining} seconds!" + +# Entry definitions +[[entries]] +id = "minecraft" +label = "Minecraft" +icon = "minecraft" +kind = { type = "snap", snap_name = "mc-installer" } + +[entries.availability] +[[entries.availability.windows]] +days = "weekdays" +start = "15:00" +end = "18:00" + +[[entries.availability.windows]] +days = "weekends" +start = "10:00" +end = "20:00" + +[entries.limits] +max_run_seconds = 1800 # 30 minutes per session +daily_quota_seconds = 7200 # 2 hours per day +cooldown_seconds = 600 # 10 minutes between sessions + +[[entries]] +id = "educational-game" +label = "GCompris" +icon = "gcompris-qt" +kind = { type = "process", command = "gcompris-qt" } + +[entries.availability] +always = true # Always available + +[entries.limits] +max_run_seconds = 3600 # 1 hour +``` + +## Usage + +### Loading Configuration + +```rust +use shepherd_config::{load_config, parse_config, Policy}; +use std::path::Path; + +// Load from file +let policy = load_config("/etc/shepherdd/config.toml")?; + +// Parse from string +let toml_content = std::fs::read_to_string("config.toml")?; +let policy = parse_config(&toml_content)?; + +// Access entries +for entry in &policy.entries { + println!("{}: {:?}", entry.label, entry.kind); +} +``` + +### Entry Kinds + +Entries can be of several types: + +```toml +# Regular process +kind = { type = "process", command = "/usr/bin/game", args = ["--fullscreen"] } + +# Snap application +kind = { type = "snap", snap_name = "mc-installer" } + +# Virtual machine (future) +kind = { type = "vm", driver = "qemu", args = { disk = "game.qcow2" } } + +# Media playback (future) +kind = { type = "media", library_id = "movies" } + +# Custom type +kind = { type = "custom", type_name = "my-launcher", payload = { ... } } +``` + +### Time Windows + +Time windows control when entries are available: + +```toml +[entries.availability] +[[entries.availability.windows]] +days = "weekdays" # or "weekends", "all" +start = "15:00" +end = "18:00" + +[[entries.availability.windows]] +days = ["sat", "sun"] # Specific days +start = "09:00" +end = "21:00" +``` + +### Limits + +Control session duration and frequency: + +```toml +[entries.limits] +max_run_seconds = 1800 # Max duration per session +daily_quota_seconds = 7200 # Total daily limit +cooldown_seconds = 600 # Wait time between sessions +``` + +## Validation + +The configuration is validated at load time. Validation catches: + +- **Duplicate entry IDs** - Each entry must have a unique ID +- **Empty commands** - Process entries must specify a command +- **Invalid time windows** - Start time must be before end time +- **Invalid thresholds** - Warning thresholds must be less than max run time +- **Negative durations** - All durations must be positive +- **Unknown kinds** - Entry types must be recognized (unless Custom) + +```rust +use shepherd_config::{parse_config, ConfigError}; + +let result = parse_config(toml_str); +match result { + Ok(policy) => { /* Use policy */ } + Err(ConfigError::ValidationFailed { errors }) => { + for error in errors { + eprintln!("Config error: {}", error); + } + } + Err(e) => eprintln!("Failed to load config: {}", e), +} +``` + +## Hot Reload + +Configuration can be reloaded at runtime via the service's `ReloadConfig` command or by sending `SIGHUP` to the service process. Reload is atomic: either the new configuration is fully applied or the old one remains. + +Active sessions continue with their original time limits when configuration is reloaded. + +## Key Types + +- `Policy` - Validated policy ready for the core engine +- `Entry` - A launchable entry definition +- `AvailabilityPolicy` - Time window rules +- `LimitsPolicy` - Duration and quota limits +- `WarningPolicy` - Warning threshold configuration +- `VolumePolicy` - Volume restrictions + +## Design Philosophy + +- **Human-readable** - TOML is easy to read and write +- **Strict validation** - Catch errors at load time, not runtime +- **Versioned schema** - `config_version` enables future migrations +- **Sensible defaults** - Minimal config is valid + +## Dependencies + +- `toml` - TOML parsing +- `serde` - Deserialization +- `chrono` - Time types +- `thiserror` - Error types diff --git a/crates/shepherd-core/README.md b/crates/shepherd-core/README.md new file mode 100644 index 0000000..0f779ac --- /dev/null +++ b/crates/shepherd-core/README.md @@ -0,0 +1,204 @@ +# shepherd-core + +Core policy engine and session state machine for Shepherd. + +## Overview + +This crate is the heart of Shepherd, containing all policy evaluation and session management logic. It is completely platform-agnostic and makes no assumptions about the underlying operating system or display environment. + +### Responsibilities + +- **Policy evaluation** - Determine what entries are available, when, and for how long +- **Session lifecycle** - Manage the state machine from launch to termination +- **Warning scheduling** - Compute and emit warnings at configured thresholds +- **Time enforcement** - Track deadlines using monotonic time +- **Quota management** - Track daily usage and cooldowns + +## Session State Machine + +Sessions progress through the following states: + +``` + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ Idle β”‚ (no session) + β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜ + β”‚ Launch requested + β–Ό + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ Launching β”‚ + β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜ + β”‚ Process spawned + β–Ό + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”Œβ”€β”€β”€β”€β”€β”€β”€β–Άβ”‚ Running │◀──────┐ + β”‚ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜ β”‚ + β”‚ β”‚ Warning threshold + β”‚ β–Ό + β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ β”‚ Warned β”‚ (multiple levels) + β”‚ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜ + β”‚ β”‚ Deadline reached + β”‚ β–Ό + β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ β”‚ Expiring β”‚ (termination in progress) + β”‚ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜ + β”‚ β”‚ Process ended + β”‚ β–Ό + β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + └────────│ Ended │───────▢ (return to Idle) + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +## Key Types + +### CoreEngine + +The main policy engine: + +```rust +use shepherd_core::CoreEngine; +use shepherd_config::Policy; +use shepherd_host_api::HostCapabilities; +use shepherd_store::Store; +use std::sync::Arc; + +// Create the engine +let engine = CoreEngine::new( + policy, // Loaded configuration + store, // Persistence layer + host.capabilities().clone(), // What the host can do +); + +// List entries with current availability +let entries = engine.list_entries(Local::now()); + +// Request to launch an entry +match engine.request_launch(&entry_id, Local::now()) { + LaunchDecision::Approved(plan) => { + // Spawn via host adapter, then start session + engine.start_session(plan, host_handle, MonotonicInstant::now()); + } + LaunchDecision::Denied { reasons } => { + // Cannot launch, explain why + } +} +``` + +### Session Plan + +When a launch is approved, the engine computes a complete session plan: + +```rust +pub struct SessionPlan { + pub session_id: SessionId, + pub entry_id: EntryId, + pub entry: Entry, + pub started_at: DateTime, + /// None means unlimited (no time limit) + pub deadline: Option, + pub warnings: Vec, +} +``` + +The plan is computed once at launch time. Deadlines and warnings are deterministic. + +### Events + +The engine emits events for the IPC layer and host adapter: + +```rust +pub enum CoreEvent { + // Session lifecycle + SessionStarted { session_id, entry_id, deadline }, + Warning { session_id, threshold_secs, remaining, severity, message }, + ExpireDue { session_id }, + SessionEnded { session_id, reason }, + + // Policy + PolicyReloaded { entry_count }, +} +``` + +### Tick Processing + +The engine must be ticked periodically to check for warnings and expiry: + +```rust +// In the service main loop +let events = engine.tick(MonotonicInstant::now()); +for event in events { + match event { + CoreEvent::Warning { .. } => { /* Notify clients */ } + CoreEvent::ExpireDue { .. } => { /* Terminate session */ } + // ... + } +} +``` + +## Time Handling + +The engine uses two time sources: + +1. **Wall-clock time** (`DateTime`) - For availability windows and display +2. **Monotonic time** (`MonotonicInstant`) - For countdown enforcement + +This separation ensures: +- Availability follows the user's local clock (correct behavior for "3pm-6pm" windows) +- Session enforcement cannot be bypassed by changing the system clock + +```rust +// Availability uses wall-clock +let is_available = entry.availability.is_available(&Local::now()); + +// Countdown uses monotonic +let remaining = session.time_remaining(MonotonicInstant::now()); +``` + +## Policy Evaluation + +For each entry, the engine evaluates: + +1. **Explicit disable** - Entry may be disabled in config +2. **Host capabilities** - Can the host run this entry kind? +3. **Time window** - Is "now" within an allowed window? +4. **Active session** - Is another session already running? +5. **Cooldown** - Has enough time passed since the last session? +6. **Daily quota** - Is there remaining quota for today? + +Each check that fails adds a `ReasonCode` to the entry view, allowing UIs to explain unavailability. + +## Design Philosophy + +- **Determinism** - Given the same inputs, the engine produces the same outputs +- **Platform agnosticism** - No OS-specific code +- **Authority** - The engine is the single source of truth for policy +- **Auditability** - All decisions can be explained via reason codes + +## Testing + +The engine is designed for testability: + +```rust +#[test] +fn test_time_window_evaluation() { + // Create engine with mock store + // Set specific time + // Verify entry availability +} + +#[test] +fn test_warning_schedule() { + // Launch with known deadline + // Tick at specific times + // Verify warnings emitted at correct thresholds +} +``` + +## Dependencies + +- `chrono` - Date/time handling +- `shepherd-api` - Shared types +- `shepherd-config` - Policy definitions +- `shepherd-host-api` - Capability types +- `shepherd-store` - Persistence trait +- `shepherd-util` - ID and time utilities diff --git a/crates/shepherd-host-api/README.md b/crates/shepherd-host-api/README.md new file mode 100644 index 0000000..600c3ad --- /dev/null +++ b/crates/shepherd-host-api/README.md @@ -0,0 +1,202 @@ +# shepherd-host-api + +Host adapter trait interfaces for Shepherd. + +## Overview + +This crate defines the capability-based interface between the Shepherd core and platform-specific implementations. It contains **no platform code itself**β€”only traits, types, and a mock implementation for testing. + +### Purpose + +Desktop operating systems have fundamentally different process control models: +- **Linux** can kill process groups and use cgroups +- **macOS** requires MDM for true kiosk mode +- **Windows** uses job objects and shell policies +- **Android** has managed launcher and device-owner workflows + +`shepherd-host-api` acknowledges these differences honestly through a capability-based design rather than pretending all platforms are equivalent. + +## Key Concepts + +### Capabilities + +The `HostCapabilities` struct declares what a host adapter can actually do: + +```rust +use shepherd_host_api::HostCapabilities; + +let caps = host.capabilities(); + +// Check supported entry kinds +if caps.supports_kind(EntryKindTag::Process) { /* ... */ } +if caps.supports_kind(EntryKindTag::Snap) { /* ... */ } + +// Check enforcement capabilities +if caps.can_kill_forcefully { /* Can use SIGKILL/TerminateProcess */ } +if caps.can_graceful_stop { /* Can request graceful shutdown */ } +if caps.can_group_process_tree { /* Can kill entire process tree */ } + +// Check optional features +if caps.can_observe_window_ready { /* Get notified when GUI appears */ } +if caps.can_force_foreground { /* Can bring window to front */ } +if caps.can_force_fullscreen { /* Can set fullscreen mode */ } +``` + +The core engine uses these capabilities to: +- Filter available entries (don't show what can't be run) +- Choose termination strategies +- Decide whether to attempt optional behaviors + +### Host Adapter Trait + +Platform adapters implement this trait: + +```rust +use shepherd_host_api::{HostAdapter, SpawnOptions, StopMode, HostEvent}; + +#[async_trait] +pub trait HostAdapter: Send + Sync { + /// Get the capabilities of this host adapter + fn capabilities(&self) -> &HostCapabilities; + + /// Spawn a new session + async fn spawn( + &self, + session_id: SessionId, + entry_kind: &EntryKind, + options: SpawnOptions, + ) -> HostResult; + + /// Stop a running session + async fn stop(&self, handle: &HostSessionHandle, mode: StopMode) -> HostResult<()>; + + /// Subscribe to host events (exits, window ready, etc.) + fn subscribe(&self) -> mpsc::UnboundedReceiver; + + // Optional methods with default implementations + async fn set_foreground(&self, handle: &HostSessionHandle) -> HostResult<()>; + async fn set_fullscreen(&self, handle: &HostSessionHandle) -> HostResult<()>; + async fn ensure_shell_visible(&self) -> HostResult<()>; +} +``` + +### Session Handles + +`HostSessionHandle` is an opaque container for platform-specific identifiers: + +```rust +use shepherd_host_api::HostSessionHandle; + +// Created by spawn(), contains platform-specific data +let handle: HostSessionHandle = host.spawn(session_id, &entry_kind, options).await?; + +// On Linux, internally contains pid, pgid +// On Windows, would contain job object handle +// On macOS, might contain bundle identifier +``` + +### Stop Modes + +Session termination can be graceful or forced: + +```rust +use shepherd_host_api::StopMode; +use std::time::Duration; + +// Try graceful shutdown with timeout, then force +host.stop(&handle, StopMode::Graceful { + timeout: Duration::from_secs(5) +}).await?; + +// Immediate termination +host.stop(&handle, StopMode::Force).await?; +``` + +### Host Events + +Adapters emit events via an async channel: + +```rust +use shepherd_host_api::HostEvent; + +let mut events = host.subscribe(); +while let Some(event) = events.recv().await { + match event { + HostEvent::Exited { handle, status } => { + // Process ended (normally or killed) + } + HostEvent::WindowReady { handle } => { + // GUI window appeared (if observable) + } + HostEvent::SpawnFailed { session_id, error } => { + // Launch failed after handle was created + } + } +} +``` + +### Volume Control + +The crate also defines a volume controller interface: + +```rust +use shepherd_host_api::VolumeController; + +#[async_trait] +pub trait VolumeController: Send + Sync { + /// Get current volume (0-100) + async fn get_volume(&self) -> HostResult; + + /// Set volume (0-100) + async fn set_volume(&self, level: u8) -> HostResult<()>; + + /// Check if muted + async fn is_muted(&self) -> HostResult; + + /// Set mute state + async fn set_muted(&self, muted: bool) -> HostResult<()>; + + /// Subscribe to volume changes + fn subscribe(&self) -> mpsc::UnboundedReceiver; +} +``` + +## Mock Implementation + +For testing, the crate provides `MockHost`: + +```rust +use shepherd_host_api::MockHost; + +let mock = MockHost::new(); + +// Spawns will "succeed" with fake handles +let handle = mock.spawn(session_id, &entry_kind, options).await?; + +// Inject events for testing +mock.inject_exit(handle.clone(), ExitStatus::Code(0)); +``` + +## Design Philosophy + +- **Honest capabilities** - Don't pretend all platforms are equal +- **Platform code stays out** - This crate is pure interface +- **Extensible** - New capabilities can be added without breaking existing adapters +- **Testable** - Mock implementation enables unit testing + +## Available Implementations + +| Adapter | Crate | Status | +|---------|-------|--------| +| Linux | `shepherd-host-linux` | Implemented | +| macOS | `shepherd-host-macos` | Planned | +| Windows | `shepherd-host-windows` | Planned | +| Android | `shepherd-host-android` | Planned | + +## Dependencies + +- `async-trait` - Async trait support +- `tokio` - Async runtime types +- `serde` - Serialization for handles +- `shepherd-api` - Entry kind types +- `shepherd-util` - ID types diff --git a/crates/shepherd-host-linux/README.md b/crates/shepherd-host-linux/README.md new file mode 100644 index 0000000..29af682 --- /dev/null +++ b/crates/shepherd-host-linux/README.md @@ -0,0 +1,191 @@ +# shepherd-host-linux + +Linux host adapter for Shepherd. + +## Overview + +This crate implements the `HostAdapter` trait for Linux systems, providing: + +- **Process spawning** with process group isolation +- **Process termination** via graceful (SIGTERM) and forceful (SIGKILL) signals +- **Exit observation** through async process monitoring +- **Snap application support** via systemd scope-based management +- **stdout/stderr capture** to log files +- **Volume control** with auto-detection of sound systems (PipeWire, PulseAudio, ALSA) + +## Capabilities + +The Linux adapter reports these capabilities: + +```rust +HostCapabilities { + // Supported entry kinds + spawn_kind_supported: [Process, Snap], + + // Enforcement capabilities + can_kill_forcefully: true, // SIGKILL + can_graceful_stop: true, // SIGTERM + can_group_process_tree: true, // Process groups (pgid) + can_observe_exit: true, // async wait + + // Optional features (not yet implemented) + can_observe_window_ready: false, + can_force_foreground: false, + can_force_fullscreen: false, + can_lock_to_single_app: false, +} +``` + +## Usage + +### Creating the Adapter + +```rust +use shepherd_host_linux::LinuxHost; + +let host = LinuxHost::new(); + +// Check capabilities +let caps = host.capabilities(); +assert!(caps.can_kill_forcefully); +``` + +### Spawning Processes + +```rust +use shepherd_host_api::{SpawnOptions, EntryKind}; + +let entry_kind = EntryKind::Process { + command: "/usr/bin/game".to_string(), + args: vec!["--fullscreen".to_string()], + env: Default::default(), + cwd: None, +}; + +let options = SpawnOptions { + capture_stdout: true, + capture_stderr: true, + log_path: Some("/var/log/shepherdd/sessions".into()), + fullscreen: false, + foreground: false, +}; + +let handle = host.spawn(session_id, &entry_kind, options).await?; +``` + +### Spawning Snap Applications + +Snap applications are managed using systemd scopes for proper process tracking: + +```rust +let entry_kind = EntryKind::Snap { + snap_name: "mc-installer".to_string(), + command: None, // Defaults to snap_name + args: vec![], + env: Default::default(), +}; + +// Spawns via: snap run mc-installer +// Process group is isolated within a systemd scope +let handle = host.spawn(session_id, &entry_kind, options).await?; +``` + +### Stopping Sessions + +```rust +use shepherd_host_api::StopMode; +use std::time::Duration; + +// Graceful: SIGTERM, wait 5s, then SIGKILL +host.stop(&handle, StopMode::Graceful { + timeout: Duration::from_secs(5), +}).await?; + +// Force: immediate SIGKILL +host.stop(&handle, StopMode::Force).await?; +``` + +### Monitoring Exits + +```rust +let mut events = host.subscribe(); + +tokio::spawn(async move { + while let Some(event) = events.recv().await { + match event { + HostEvent::Exited { handle, status } => { + println!("Session {} exited: {:?}", handle.session_id(), status); + } + _ => {} + } + } +}); +``` + +## Volume Control + +The crate includes `LinuxVolumeController` which auto-detects the available sound system: + +```rust +use shepherd_host_linux::LinuxVolumeController; + +let controller = LinuxVolumeController::new().await?; + +// Get current volume (0-100) +let volume = controller.get_volume().await?; + +// Set volume with enforcement of configured maximum +controller.set_volume(75).await?; + +// Mute/unmute +controller.set_muted(true).await?; +``` + +### Sound System Detection Order + +1. **PipeWire** (`wpctl` or `pw-cli`) - Modern default on Ubuntu 22.04+, Fedora +2. **PulseAudio** (`pactl`) - Legacy but widely available +3. **ALSA** (`amixer`) - Fallback for systems without a sound server + +## Process Group Handling + +All spawned processes are placed in their own process group: + +```rust +// Internally uses setsid() or setpgid() +// This allows killing the entire process tree +``` + +When stopping a session: +1. SIGTERM is sent to the process group (`-pgid`) +2. After timeout, SIGKILL is sent to the process group +3. Orphaned children are cleaned up + +## Log Capture + +stdout and stderr can be captured to session log files: + +``` +/var/log/shepherdd/sessions/ +β”œβ”€β”€ 2025-01-15-abc123-minecraft.log +β”œβ”€β”€ 2025-01-15-def456-gcompris.log +└── ... +``` + +## Future Enhancements + +Planned features (hooks are designed in): + +- **cgroups v2** - CPU/memory/IO limits per session +- **Namespace isolation** - Optional sandboxing +- **Sway/Wayland integration** - Focus and fullscreen control +- **D-Bus monitoring** - Window readiness detection + +## Dependencies + +- `nix` - Unix system calls +- `tokio` - Async runtime +- `tracing` - Logging +- `serde` - Serialization +- `shepherd-host-api` - Trait definitions +- `shepherd-api` - Entry types diff --git a/crates/shepherd-hud/README.md b/crates/shepherd-hud/README.md new file mode 100644 index 0000000..9e10045 --- /dev/null +++ b/crates/shepherd-hud/README.md @@ -0,0 +1,184 @@ +# shepherd-hud + +Always-visible HUD overlay for Shepherd. + +## Overview + +`shepherd-hud` is a GTK4 layer-shell overlay that remains visible during active sessions. It provides essential information and controls that must always be accessible, regardless of what fullscreen application is running underneath. + +## Features + +- **Time remaining** - Authoritative countdown from the service +- **Battery level** - Current charge percentage and status +- **Volume control** - Adjust system volume with enforced limits +- **Session controls** - End session button +- **Power controls** - Suspend, shutdown, restart +- **Warning display** - Visual and audio alerts for time warnings + +## Architecture + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Sway / Wayland Compositor β”‚ +β”‚ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ HUD (layer-shell overlay) β”‚ β”‚ +β”‚ β”‚ [Battery] [Volume] [Time Remaining] [Controls] β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ Running Application (fullscreen) β”‚ β”‚ +β”‚ β”‚ β”‚ β”‚ +β”‚ β”‚ β”‚ β”‚ +β”‚ β”‚ β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +The HUD uses Wayland's **wlr-layer-shell** protocol to remain above all other surfaces. + +## Usage + +### Running + +```bash +# With default socket path +shepherd-hud + +# With custom socket path +shepherd-hud --socket /run/shepherdd/shepherdd.sock + +# Custom position and size +shepherd-hud --anchor top --height 48 +``` + +### Command-Line Options + +| Option | Default | Description | +|--------|---------|-------------| +| `-s, --socket` | `/run/shepherdd/shepherdd.sock` | Service socket path | +| `-l, --log-level` | `info` | Log verbosity | +| `-a, --anchor` | `top` | Screen edge (`top` or `bottom`) | +| `--height` | `48` | HUD bar height in pixels | + +## Display Elements + +### Time Remaining + +Shows the countdown timer for the current session: + +- `MM:SS` format for times under 1 hour +- `H:MM:SS` format for longer sessions +- Visual emphasis when below warning thresholds +- Shows "∞" for unlimited sessions + +### Battery + +Displays current battery status: + +- Percentage (0-100%) +- Charging/discharging indicator +- Data sourced from UPower (not the service) + +### Volume + +Shows and controls system volume: + +- Current level (0-100%) +- Mute indicator +- Click to adjust (sends commands to service) +- Volume maximum may be restricted by policy + +### Controls + +- **End Session** - Stops the current session (if allowed) +- **Power** - Opens menu with Suspend/Shutdown/Restart + +## Event Handling + +### Warnings + +When the service emits a `WarningIssued` event: + +1. Visual banner appears on the HUD +2. Time display changes color based on severity +3. Optional audio cue plays +4. Banner auto-dismisses or requires acknowledgment + +Severity levels: +- `Info` (e.g., 5 minutes remaining) - Subtle notification +- `Warn` (e.g., 1 minute remaining) - Prominent warning +- `Critical` (e.g., 10 seconds remaining) - Urgent, full-width banner + +### Session Expired + +When time runs out: + +1. "Time's Up" overlay appears +2. Audio notification plays +3. HUD remains visible until launcher reappears + +### Disconnection + +If the service connection is lost: + +1. "Disconnected" indicator shown +2. All controls disabled +3. Automatic reconnection attempted +4. **Time display frozen** (not fabricated) + +## Styling + +The HUD is designed to be: + +- **Unobtrusive** - Small footprint, doesn't cover content +- **High contrast** - Readable over any background +- **Touch-friendly** - Large touch targets +- **Minimal** - Icons over text where possible + +## Layer-Shell Details + +```rust +// Layer-shell configuration +layer: Overlay // Always above normal windows +anchor: Top // Attached to top edge +exclusive_zone: 48 // Reserves space (optional) +keyboard_interactivity: OnDemand // Only when focused +``` + +## State Management + +The HUD maintains local state synchronized with the service: + +```rust +struct HudState { + // From service + session: Option, + volume: VolumeInfo, + + // Local + battery: BatteryInfo, // From UPower + connected: bool, +} +``` + +**Key principle**: The HUD never independently computes time remaining. All timing comes from the service. + +## Dependencies + +- `gtk4` - GTK4 bindings +- `gtk4-layer-shell` - Wayland layer-shell support +- `tokio` - Async runtime +- `shepherd-api` - Protocol types +- `shepherd-ipc` - Client implementation +- `upower` - Battery monitoring +- `clap` - Argument parsing +- `tracing` - Logging + +## Building + +```bash +cargo build --release -p shepherd-hud +``` + +Requires GTK4 development libraries and a Wayland compositor with layer-shell support (e.g., Sway, Hyprland). diff --git a/crates/shepherd-ipc/README.md b/crates/shepherd-ipc/README.md new file mode 100644 index 0000000..948c99c --- /dev/null +++ b/crates/shepherd-ipc/README.md @@ -0,0 +1,234 @@ +# 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 + +```rust +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 + +```rust +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 | + +```rust +// 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 + +```rust +use shepherd_ipc::IpcClient; + +let mut client = IpcClient::connect("/run/shepherdd/shepherdd.sock").await?; +``` + +### Sending Commands + +```rust +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 + +```rust +// 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: + +```json +// 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: + +```json +{"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: + +```rust +// Default: 10 commands per second per client +if rate_limiter.check(&client_id) { + // Process command +} else { + // Respond with rate limit error +} +``` + +## Error Handling + +```rust +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 diff --git a/crates/shepherd-launcher-ui/README.md b/crates/shepherd-launcher-ui/README.md new file mode 100644 index 0000000..f0e389b --- /dev/null +++ b/crates/shepherd-launcher-ui/README.md @@ -0,0 +1,221 @@ +# shepherd-launcher-ui + +Main launcher grid interface for Shepherd. + +## Overview + +`shepherd-launcher-ui` is the primary user-facing shell for the Shepherd kiosk environment. It presents a grid of available entries (applications, games, media) and allows users to launch them. + +This is what users see when no session is activeβ€”the "home screen" of the environment. + +## Features + +- **Entry grid** - Large, touch-friendly tiles for each available entry +- **Availability display** - Visual indication of enabled/disabled entries +- **Launch requests** - Send launch commands to the service +- **State synchronization** - Always reflects service's authoritative state + +## Architecture + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Sway / Wayland Compositor β”‚ +β”‚ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ Launcher UI (fullscreen) β”‚ β”‚ +β”‚ β”‚ β”‚ β”‚ +β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ +β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ +β”‚ β”‚ β”‚Minecrafβ”‚ β”‚GComprisβ”‚ β”‚ Movies β”‚ β”‚ Books β”‚ β”‚ β”‚ +β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ +β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ +β”‚ β”‚ β”‚ β”‚ +β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ +β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ +β”‚ β”‚ β”‚ ScummVMβ”‚ β”‚Bedtime β”‚ β”‚ β”‚ +β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ +β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ +β”‚ β”‚ β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +## Usage + +### Running + +```bash +# With default socket path +shepherd-launcher + +# With custom socket path +shepherd-launcher --socket /run/shepherdd/shepherdd.sock +``` + +### Command-Line Options + +| Option | Default | Description | +|--------|---------|-------------| +| `-s, --socket` | `/run/shepherdd/shepherdd.sock` | Service socket path | +| `-l, --log-level` | `info` | Log verbosity | + +## Grid Behavior + +### Entry Tiles + +Each tile displays: + +- **Icon** - Large, recognizable icon +- **Label** - Entry name +- **Status** - Enabled (bright) or disabled (dimmed) +- **Time indicator** - Max duration if started now (e.g., "30 min") + +### Enabled Entries + +When an entry is enabled: +1. Tile is fully visible and interactive +2. Tapping sends `Launch` command to service +3. Grid shows "Launching..." state +4. On success: launcher hides, application starts + +### Disabled Entries + +When an entry is disabled it is not displayed. + +### Launch Flow + +``` +User taps tile + β”‚ + β–Ό +Launcher sends Launch command + β”‚ + β–Ό +Grid input disabled +"Starting..." overlay shown + β”‚ + β–Ό +β”Œβ”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β” +β”‚ β”‚ +β–Ό β–Ό +Success Failure +β”‚ β”‚ +β–Ό β–Ό +Launcher Error message +hides Grid restored +``` + +## State Management + +The launcher maintains a reactive state model: + +```rust +struct LauncherState { + entries: Vec, // From service + current_session: Option, + connected: bool, + launching: Option, +} +``` + +### Event Handling + +| Event | Launcher Response | +|-------|-------------------| +| `StateChanged` | Update entry grid | +| `SessionStarted` | Hide launcher | +| `SessionEnded` | Show launcher | +| `PolicyReloaded` | Refresh entry list | + +### Visibility Rules + +The launcher is visible when: +- No session is running, OR +- User explicitly returns to home (via HUD) + +The launcher hides when: +- A session is actively running +- (Fullscreen app is in front) + +## Error Handling + +### Service Unavailable + +If the service is not running at startup: + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ β”‚ +β”‚ System Not Ready β”‚ +β”‚ β”‚ +β”‚ Waiting for shepherd service... β”‚ +β”‚ β”‚ +β”‚ [Retry] β”‚ +β”‚ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +### Launch Failure + +If launching fails: + +1. Error notification appears +2. Grid is restored to interactive state +3. User can try again or choose another entry + +### Connection Loss + +If connection to service is lost: + +1. Entries become disabled +2. Reconnection attempted automatically +3. State refreshed on reconnection + +## Accessibility + +- **Touch-first** - Large touch targets (minimum 44px) +- **High contrast** - Clear visual hierarchy +- **Minimal text** - Icon-first design +- **Keyboard navigation** - Arrow keys and Enter +- **No hover-only interactions** - All actions accessible via tap + +## Styling + +The launcher uses a child-friendly design: + +- Large, colorful icons +- Rounded corners +- Clear enabled/disabled distinction +- Smooth transitions +- Dark background (for contrast) + +## Dependencies + +- `gtk4` - GTK4 bindings +- `tokio` - Async runtime +- `shepherd-api` - Protocol types +- `shepherd-ipc` - Client implementation +- `clap` - Argument parsing +- `tracing` - Logging + +## Building + +```bash +cargo build --release -p shepherd-launcher-ui +``` + +The resulting binary is named `shepherd-launcher`. + +## Relationship to Service + +**Critical**: The launcher is purely a presentation layer. It: +- Displays what the service allows +- Sends launch requests +- Shows service state + +It does *not*: +- Tracks time independently +- Decide availability +- Enforce policy + +If the launcher crashes, the service continues enforcement. If the launcher is replaced, the system still works. diff --git a/crates/shepherd-store/README.md b/crates/shepherd-store/README.md new file mode 100644 index 0000000..92245b3 --- /dev/null +++ b/crates/shepherd-store/README.md @@ -0,0 +1,220 @@ +# shepherd-store + +Persistence layer for Shepherd. + +## Overview + +This crate provides durable storage for the Shepherd service, including: + +- **Audit log** - Append-only record of all significant events +- **Usage accounting** - Track time used per entry per day +- **Cooldown tracking** - Remember when entries become available again +- **State snapshots** - Enable crash recovery + +## Purpose + +The store ensures: +- **Time accounting correctness** - Usage is recorded durably +- **Crash tolerance** - Service can resume after unexpected shutdown +- **Auditability** - All actions are logged for later inspection + +## Backend + +The primary implementation uses **SQLite** for reliability: + +```rust +use shepherd_store::SqliteStore; + +let store = SqliteStore::open("/var/lib/shepherdd/shepherdd.db")?; +``` + +SQLite provides: +- ACID transactions for usage accounting +- Automatic crash recovery via WAL mode +- Single-file database, easy to backup + +## Store Trait + +All storage operations go through the `Store` trait: + +```rust +pub trait Store: Send + Sync { + // Audit log + fn append_audit(&self, event: AuditEvent) -> StoreResult<()>; + fn get_recent_audits(&self, limit: usize) -> StoreResult>; + + // Usage accounting + fn get_usage(&self, entry_id: &EntryId, day: NaiveDate) -> StoreResult; + fn add_usage(&self, entry_id: &EntryId, day: NaiveDate, duration: Duration) -> StoreResult<()>; + + // Cooldown tracking + fn get_cooldown_until(&self, entry_id: &EntryId) -> StoreResult>>; + fn set_cooldown_until(&self, entry_id: &EntryId, until: DateTime) -> StoreResult<()>; + fn clear_cooldown(&self, entry_id: &EntryId) -> StoreResult<()>; + + // State snapshot + fn load_snapshot(&self) -> StoreResult>; + fn save_snapshot(&self, snapshot: &StateSnapshot) -> StoreResult<()>; + + // Health + fn is_healthy(&self) -> bool; +} +``` + +## Usage + +### Recording Session Usage + +```rust +use chrono::Local; + +// When a session ends +let duration = session.actual_duration(); +let today = Local::now().date_naive(); + +store.add_usage(&entry_id, today, duration)?; +``` + +### Checking Quota Remaining + +```rust +let today = Local::now().date_naive(); +let used = store.get_usage(&entry_id, today)?; + +if let Some(quota) = entry.limits.daily_quota { + let remaining = quota.saturating_sub(used); + if remaining.is_zero() { + // Quota exhausted + } +} +``` + +### Setting Cooldowns + +```rust +use chrono::{Duration, Local}; + +// After session ends, set cooldown +let cooldown_until = Local::now() + Duration::minutes(10); +store.set_cooldown_until(&entry_id, cooldown_until)?; +``` + +### Checking Cooldown + +```rust +if let Some(until) = store.get_cooldown_until(&entry_id)? { + if until > Local::now() { + // Still in cooldown + } +} +``` + +## Audit Log + +The audit log records significant events: + +```rust +use shepherd_store::{AuditEvent, AuditEventType}; + +// Event types logged +store.append_audit(AuditEvent::new(AuditEventType::PolicyLoaded { entry_count: 5 }))?; +store.append_audit(AuditEvent::new(AuditEventType::SessionStarted { + session_id, + entry_id +}))?; +store.append_audit(AuditEvent::new(AuditEventType::SessionEnded { + session_id, + reason: SessionEndReason::Expired +}))?; +store.append_audit(AuditEvent::new(AuditEventType::WarningIssued { + session_id, + threshold_secs: 60 +}))?; +``` + +### Audit Event Types + +- `PolicyLoaded` - Configuration loaded/reloaded +- `SessionStarted` - New session began +- `SessionEnded` - Session terminated (with reason) +- `WarningIssued` - Time warning shown to user +- `LaunchDenied` - Launch request rejected (with reasons) +- `ConfigReloaded` - Configuration hot-reloaded +- `ServiceStarted` - Service process started +- `ServiceStopped` - Service process stopped + +## State Snapshots + +For crash recovery, the service can save state snapshots: + +```rust +use shepherd_store::{StateSnapshot, SessionSnapshot}; + +// Save current state +let snapshot = StateSnapshot { + timestamp: Local::now(), + active_session: Some(SessionSnapshot { + session_id, + entry_id, + started_at, + deadline, + warnings_issued: vec![300, 60], + }), +}; +store.save_snapshot(&snapshot)?; + +// On startup, check for unfinished session +if let Some(snapshot) = store.load_snapshot()? { + if let Some(session) = snapshot.active_session { + // Potentially recover or clean up + } +} +``` + +## Database Schema + +The SQLite store uses this schema: + +```sql +-- Audit log (append-only) +CREATE TABLE audit_log ( + id INTEGER PRIMARY KEY, + timestamp TEXT NOT NULL, + event_type TEXT NOT NULL, + event_data TEXT NOT NULL -- JSON +); + +-- Usage tracking (one row per entry per day) +CREATE TABLE usage ( + entry_id TEXT NOT NULL, + day TEXT NOT NULL, -- YYYY-MM-DD + duration_secs INTEGER NOT NULL, + PRIMARY KEY (entry_id, day) +); + +-- Cooldown tracking +CREATE TABLE cooldowns ( + entry_id TEXT PRIMARY KEY, + until TEXT NOT NULL -- ISO 8601 timestamp +); + +-- State snapshot (single row) +CREATE TABLE snapshot ( + id INTEGER PRIMARY KEY CHECK (id = 1), + data TEXT NOT NULL -- JSON +); +``` + +## Design Philosophy + +- **Durability over performance** - Writes are synchronous by default +- **Simple queries** - No complex joins or aggregations needed at runtime +- **Append-only audit** - Never modify history +- **Portable format** - JSON for event data enables future migration + +## Dependencies + +- `rusqlite` - SQLite bindings +- `serde` / `serde_json` - Event serialization +- `chrono` - Timestamp handling +- `thiserror` - Error types diff --git a/crates/shepherd-util/README.md b/crates/shepherd-util/README.md new file mode 100644 index 0000000..16878c4 --- /dev/null +++ b/crates/shepherd-util/README.md @@ -0,0 +1,70 @@ +# shepherd-util + +Shared utilities for the Shepherd ecosystem. + +## Overview + +This crate provides common utilities and types used across all Shepherd crates, including: + +- **ID types** - Type-safe identifiers (`EntryId`, `SessionId`, `ClientId`) +- **Time utilities** - Monotonic time handling and duration helpers +- **Error types** - Common error definitions +- **Rate limiting** - Helpers for command rate limiting + +## Purpose + +`shepherd-util` serves as the foundational layer that other Shepherd crates depend on. It ensures consistency across the codebase by providing: + +1. **Unified ID management** - All identifiers are strongly typed to prevent mix-ups +2. **Reliable time handling** - Monotonic time for enforcement (immune to wall-clock changes) +3. **Common error patterns** - Consistent error handling across crates + +## Key Types + +### IDs + +```rust +use shepherd_util::{EntryId, SessionId, ClientId}; + +// Create IDs +let entry_id = EntryId::new("minecraft"); +let session_id = SessionId::new(); // UUID-based +let client_id = ClientId::new(); // UUID-based +``` + +### Time + +```rust +use shepherd_util::MonotonicInstant; + +// Monotonic time for countdown logic +let start = MonotonicInstant::now(); +// ... later ... +let elapsed = start.elapsed(); +``` + +### Rate Limiting + +```rust +use shepherd_util::RateLimiter; +use std::time::Duration; + +// Per-client command rate limiting +let limiter = RateLimiter::new(10, Duration::from_secs(1)); +if limiter.check(&client_id) { + // Process command +} +``` + +## Design Philosophy + +- **No platform-specific code** - Pure Rust, works everywhere +- **Minimal dependencies** - Only essential crates +- **Type safety** - Prefer typed wrappers over raw strings/numbers + +## Dependencies + +- `uuid` - For generating unique session/client IDs +- `chrono` - For time handling +- `serde` - For serialization +- `thiserror` - For error types diff --git a/crates/shepherdd/README.md b/crates/shepherdd/README.md new file mode 100644 index 0000000..7ac0d3c --- /dev/null +++ b/crates/shepherdd/README.md @@ -0,0 +1,270 @@ +# shepherdd + +The Shepherd background service. + +## Overview + +`shepherdd` is the authoritative policy and enforcement service for the Shepherd ecosystem. It is the central coordinator that: + +- Loads and validates configuration +- Evaluates policy to determine availability +- Manages session lifecycles +- Enforces time limits +- Emits warnings and events +- Serves multiple clients via IPC + +**Key principle**: `shepherdd` is the single source of truth. User interfaces only request actions and display stateβ€”they never enforce policy independently. + +## Architecture + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ shepherdd β”‚ +β”‚ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ Config β”‚ β”‚ Store β”‚ β”‚ Core Engine β”‚ β”‚ +β”‚ β”‚ Loader │──▢│ (SQLite) │──▢│ (Policy + Session) β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ +β”‚ β”‚ Host β”‚ β”‚ IPC β”‚β—€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ Adapter │◀─│ Server β”‚ β”‚ +β”‚ β”‚ (Linux) β”‚ β”‚ β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ β”‚ β”‚ +β”‚ β”‚ Unix Domain Socket β”‚ +β”‚ β”‚ β”‚ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ β”‚ + β–Ό β–Ό + Supervised β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” + Applications β”‚Launcher β”‚ β”‚ HUD β”‚ β”‚ Admin β”‚ + β”‚ UI β”‚ β”‚ Overlay β”‚ β”‚ Tools β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +## Usage + +### Running + +```bash +# With default config location +shepherdd + +# With custom config +shepherdd --config /path/to/config.toml + +# Override socket and data paths +shepherdd --socket /tmp/shepherdd.sock --data-dir /tmp/shepherdd-data + +# Debug logging +shepherdd --log-level debug +``` + +### Command-Line Options + +| Option | Default | Description | +|--------|---------|-------------| +| `-c, --config` | `/etc/shepherdd/config.toml` | Configuration file path | +| `-s, --socket` | From config | IPC socket path | +| `-d, --data-dir` | From config | Data directory | +| `-l, --log-level` | `info` | Log verbosity | + +### Environment Variables + +| Variable | Description | +|----------|-------------| +| `SHEPHERD_SOCKET` | Override socket path | +| `SHEPHERD_DATA_DIR` | Override data directory | +| `RUST_LOG` | Tracing filter (e.g., `shepherdd=debug`) | + +## Main Loop + +The service runs an async event loop that processes: + +1. **IPC messages** - Commands from clients +2. **Host events** - Process exits, window events +3. **Timer ticks** - Check for warnings and expiry +4. **Signals** - SIGHUP for config reload, SIGTERM for shutdown + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Main Loop β”‚ +β”‚ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ IPC β”‚ β”‚ Host β”‚ β”‚ Timer β”‚ β”‚ Signal β”‚ β”‚ +β”‚ β”‚ Channel β”‚ β”‚ Events β”‚ β”‚ Tick β”‚ β”‚ Handler β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ β”‚ +β”‚ β–Ό β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ Process Event β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ β”‚ +β”‚ β–Ό β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ Broadcast Events β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +## Command Handling + +### Client Commands + +| Command | Description | Role Required | +|---------|-------------|---------------| +| `GetState` | Get full state snapshot | Any | +| `ListEntries` | Get available entries | Any | +| `Launch` | Start a session | Shell/Admin | +| `StopCurrent` | End current session | Shell/Admin | +| `ReloadConfig` | Hot-reload configuration | Admin | +| `SubscribeEvents` | Subscribe to event stream | Any | +| `GetHealth` | Health check | Any | +| `SetVolume` | Set system volume | Shell/Admin | +| `GetVolume` | Get volume info | Any | + +### Response Flow + +``` +Client Request + β”‚ + β–Ό +Role Check ──────▢ Denied Response + β”‚ + β–Ό +Command Handler + β”‚ + β–Ό +Core Engine + β”‚ + β–Ό +Response + Events ──────▢ Broadcast to Subscribers +``` + +## Session Lifecycle + +### Launch + +1. Client sends `Launch { entry_id }` +2. Core engine evaluates policy +3. If denied: respond with reasons +4. If approved: create session plan +5. Host adapter spawns process +6. Session transitions to Running +7. `SessionStarted` event broadcast + +### Enforcement + +1. Timer ticks every 100ms +2. Core engine checks warnings and expiry +3. At warning thresholds: `WarningIssued` event +4. At deadline: initiate graceful stop +5. After grace period: force kill +6. `SessionEnded` event broadcast + +### Termination + +1. Stop triggered (expiry, user, admin, process exit) +2. Host adapter signals process (SIGTERM) +3. Wait for grace period +4. Force kill if needed (SIGKILL) +5. Record usage in store +6. Set cooldown if configured +7. Clear session state + +## Configuration Reload + +On SIGHUP or `ReloadConfig` command: + +1. Parse new configuration file +2. Validate completely +3. If invalid: keep old config, log error +4. If valid: atomic swap to new policy +5. Emit `PolicyReloaded` event +6. Current session continues with original plan + +## Health Monitoring + +The service exposes health status via `GetHealth`: + +```json +{ + "status": "healthy", + "policy_loaded": true, + "store_healthy": true, + "host_healthy": true, + "uptime_seconds": 3600, + "current_session": null +} +``` + +## Logging + +Uses structured logging via `tracing`: + +``` +2025-01-15T14:30:00.000Z INFO shepherdd: Starting shepherd service +2025-01-15T14:30:00.050Z INFO shepherd_config: Configuration loaded entries=5 +2025-01-15T14:30:00.100Z INFO shepherd_ipc: IPC server listening path=/run/shepherdd/shepherdd.sock +2025-01-15T14:30:15.000Z INFO shepherd_core: Session started session_id=abc123 entry_id=minecraft +2025-01-15T14:59:45.000Z WARN shepherd_core: Warning issued session_id=abc123 threshold=60 +2025-01-15T15:00:45.000Z INFO shepherd_core: Session expired session_id=abc123 +``` + +## Persistence + +State is persisted to SQLite: + +``` +/var/lib/shepherdd/ +β”œβ”€β”€ shepherdd.db # SQLite database +└── logs/ + └── sessions/ # Session stdout/stderr +``` + +## Signals + +| Signal | Action | +|--------|--------| +| `SIGHUP` | Reload configuration | +| `SIGTERM` | Graceful shutdown | +| `SIGINT` | Graceful shutdown | + +## Dependencies + +This binary wires together all the library crates: + +- `shepherd-config` - Configuration loading +- `shepherd-core` - Policy engine +- `shepherd-host-api` - Host adapter trait +- `shepherd-host-linux` - Linux implementation +- `shepherd-ipc` - IPC server +- `shepherd-store` - Persistence +- `shepherd-api` - Protocol types +- `shepherd-util` - Utilities +- `tokio` - Async runtime +- `clap` - CLI parsing +- `tracing` - Logging +- `anyhow` - Error handling + +## Building + +```bash +cargo build --release -p shepherdd +``` + +## Installation + +The service is typically started by the compositor: + +`sway.conf` +```conf +# Start shepherdd FIRST - it needs to create the socket before HUD/launcher connect +# Running inside sway ensures all spawned processes use the nested compositor +exec ./target/debug/shepherdd -c ./config.example.toml +``` + +See [CONTRIBUTING.md](../../CONTRIBUTING.md) for development setup.