shepherd-launcher/crates/shepherd-host-api
2025-12-29 16:54:57 -05:00
..
src :%s/daemon/service/g 2025-12-29 12:52:03 -05:00
Cargo.toml (Hopefully) productionized shepherdd 2025-12-26 15:35:27 -05:00
README.md Add crate documentation 2025-12-29 16:54:57 -05:00

shepherd-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:

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:

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:

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:

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:

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:

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:

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