Add crate documentation

This commit is contained in:
Albert Armea 2025-12-29 16:54:57 -05:00
parent f5d7d69578
commit c444426507
13 changed files with 2156 additions and 1 deletions

View file

@ -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, Contributions are welcome for improvements and not yet implemented backends,
such as: such as:
* Content-aware media player [TODO: link to issue]
* Pre-booted Steam to improve launch time [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] * 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] * Legacy Win9x via DOSBox, QEMU, or PCem, including scripts to create a boot-to-app image [TODO: link to issue]

View file

@ -236,10 +236,11 @@ end = "20:00"
# Omitting limits entirely uses default_max_run_seconds # Omitting limits entirely uses default_max_run_seconds
## === Media === ## === 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). # Files can be local on your system or URLs (YouTube, etc).
# "lofi hip hop radio 📚 beats to relax/study to" streamed live from YouTube # "lofi hip hop radio 📚 beats to relax/study to" streamed live from YouTube
# This should eventually be replaced with the media type.
[[entries]] [[entries]]
id = "lofi-beats" id = "lofi-beats"
label = "Lofi Beats" label = "Lofi Beats"

View file

@ -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

View file

@ -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

View file

@ -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<Local>,
/// None means unlimited (no time limit)
pub deadline: Option<MonotonicInstant>,
pub warnings: Vec<ScheduledWarning>,
}
```
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<Local>`) - 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

View file

@ -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<HostSessionHandle>;
/// 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<HostEvent>;
// 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<u8>;
/// Set volume (0-100)
async fn set_volume(&self, level: u8) -> HostResult<()>;
/// Check if muted
async fn is_muted(&self) -> HostResult<bool>;
/// Set mute state
async fn set_muted(&self, muted: bool) -> HostResult<()>;
/// Subscribe to volume changes
fn subscribe(&self) -> mpsc::UnboundedReceiver<VolumeEvent>;
}
```
## 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

View file

@ -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

View file

@ -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<SessionInfo>,
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).

View file

@ -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

View file

@ -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<EntryView>, // From service
current_session: Option<SessionInfo>,
connected: bool,
launching: Option<EntryId>,
}
```
### 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.

View file

@ -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<Vec<AuditEvent>>;
// Usage accounting
fn get_usage(&self, entry_id: &EntryId, day: NaiveDate) -> StoreResult<Duration>;
fn add_usage(&self, entry_id: &EntryId, day: NaiveDate, duration: Duration) -> StoreResult<()>;
// Cooldown tracking
fn get_cooldown_until(&self, entry_id: &EntryId) -> StoreResult<Option<DateTime<Local>>>;
fn set_cooldown_until(&self, entry_id: &EntryId, until: DateTime<Local>) -> StoreResult<()>;
fn clear_cooldown(&self, entry_id: &EntryId) -> StoreResult<()>;
// State snapshot
fn load_snapshot(&self) -> StoreResult<Option<StateSnapshot>>;
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

View file

@ -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

270
crates/shepherdd/README.md Normal file
View file

@ -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.