# 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