diff --git a/crates/ironclaw_engine/src/lib.rs b/crates/ironclaw_engine/src/lib.rs index 22d74eb9de..fd2fc3b308 100644 --- a/crates/ironclaw_engine/src/lib.rs +++ b/crates/ironclaw_engine/src/lib.rs @@ -36,8 +36,8 @@ pub mod workspace; // ── Re-exports: types ─────────────────────────────────────── pub use types::capability::{ - ActionDef, Capability, CapabilityLease, EffectType, GrantedActions, LeaseId, PolicyCondition, - PolicyEffect, PolicyRule, + ActionDef, Capability, CapabilityLease, CapabilityStatus, EffectType, GrantedActions, LeaseId, + PolicyCondition, PolicyEffect, PolicyRule, }; pub use types::error::{CapabilityError, EngineError, StepError, ThreadError}; pub use types::event::{EventId, EventKind, ThreadEvent}; diff --git a/crates/ironclaw_engine/src/types/capability.rs b/crates/ironclaw_engine/src/types/capability.rs index 6e25137f3a..28f39b1b75 100644 --- a/crates/ironclaw_engine/src/types/capability.rs +++ b/crates/ironclaw_engine/src/types/capability.rs @@ -135,6 +135,31 @@ pub struct ActionDef { pub requires_approval: bool, } +/// Canonical model-visible status for capability background surfacing. +/// +/// This is a normalized projection over host runtime truth. It is not itself +/// a source of truth for auth, activation, or installation state. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum CapabilityStatus { + /// Capability is directly usable now. + Ready, + /// Capability is usable now, but only through a scoped or indirect route. + ReadyScoped, + /// Capability exists but needs authentication before use. + NeedsAuth, + /// Capability exists but needs setup before auth or execution can proceed. + NeedsSetup, + /// Capability is installed or known, but not currently active. + Inactive, + /// Capability is known to the runtime but not yet activated into a direct action. + Latent, + /// Capability lookup or readiness failed with a concrete runtime error. + Error, + /// Capability is known in the registry but not installed. + AvailableNotInstalled, +} + // ── Capability ────────────────────────────────────────────── /// A capability — bundles actions, knowledge, and policies. @@ -257,6 +282,7 @@ impl CapabilityLease { #[cfg(test)] mod tests { use super::*; + use serde_json::json; fn make_lease() -> CapabilityLease { CapabilityLease { @@ -359,4 +385,47 @@ mod tests { lease.granted_actions = GrantedActions::Specific(vec!["list-prs".into()]); assert!(lease.covers_action("list_prs")); } + + #[test] + fn capability_status_serializes_as_snake_case() { + let cases = [ + (CapabilityStatus::Ready, json!("ready")), + (CapabilityStatus::ReadyScoped, json!("ready_scoped")), + (CapabilityStatus::NeedsAuth, json!("needs_auth")), + (CapabilityStatus::NeedsSetup, json!("needs_setup")), + (CapabilityStatus::Inactive, json!("inactive")), + (CapabilityStatus::Latent, json!("latent")), + (CapabilityStatus::Error, json!("error")), + ( + CapabilityStatus::AvailableNotInstalled, + json!("available_not_installed"), + ), + ]; + + for (status, expected) in cases { + assert_eq!(serde_json::to_value(status).unwrap(), expected); + } + } + + #[test] + fn capability_status_round_trips_from_wire_values() { + let cases = [ + ("ready", CapabilityStatus::Ready), + ("ready_scoped", CapabilityStatus::ReadyScoped), + ("needs_auth", CapabilityStatus::NeedsAuth), + ("needs_setup", CapabilityStatus::NeedsSetup), + ("inactive", CapabilityStatus::Inactive), + ("latent", CapabilityStatus::Latent), + ("error", CapabilityStatus::Error), + ( + "available_not_installed", + CapabilityStatus::AvailableNotInstalled, + ), + ]; + + for (wire, expected) in cases { + let parsed: CapabilityStatus = serde_json::from_value(json!(wire)).unwrap(); + assert_eq!(parsed, expected); + } + } }