engine-v2: add canonical capability status vocabulary (#2825)

* Add canonical engine capability status enum

* refactor(engine): add hash support for capability status
This commit is contained in:
Henry Park
2026-04-22 05:05:45 -07:00
committed by GitHub
parent 417ee611df
commit 9d651ea366
2 changed files with 71 additions and 2 deletions

View File

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

View File

@@ -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);
}
}
}