car-ffi-common 0.32.0

Shared logic for FFI bindings (NAPI, PyO3) — JSON wrappers for verify, multi-agent, scheduler
//! JSON wrapper for skill trust & lifecycle governance (arXiv 2602.12430).
//!
//! Stateless helper, binding-only — see
//! `docs/proposals/skill-trust-governance.md`. Maps a skill's provenance to a
//! trust tier and gates the deployment capability that tier permits, reusing
//! `car-policy`'s `PermissionTier` vocabulary.

use car_policy::permission::PermissionTier;
use car_policy::skill_trust::{gate_skill_deployment, SkillProvenance};

/// Gate a skill's requested deployment capability against its provenance.
/// `provenance_json` is a `SkillProvenance` (`{ signed?, signer_trusted?,
/// scanned?, vulnerabilities?, source?, success_count?, fail_count? }`);
/// `requested_tier` is `"read_only" | "sandbox_edit" | "full_access"`. Returns
/// the `SkillDeploymentDecision` JSON `{ trust, ceiling, granted, outcome, reason
/// }` — `outcome` is `allow` | `downgrade` | `deny`. A vulnerable, unsigned, or
/// degraded skill is denied regardless of the requested tier.
pub fn gate(provenance_json: &str, requested_tier: &str) -> Result<String, String> {
    let prov: SkillProvenance = crate::from_json("provenance", provenance_json)?;
    let requested = PermissionTier::from_str_opt(requested_tier).ok_or_else(|| {
        format!(
            "invalid requested_tier '{requested_tier}' \
             (expected read_only|sandbox_edit|full_access)"
        )
    })?;
    let decision = gate_skill_deployment(&prov, requested);
    crate::to_json(&decision)
}

#[cfg(test)]
mod tests {
    use super::*;
    use serde_json::Value;

    #[test]
    fn official_full_access_allowed() {
        let p = r#"{"signed":true,"signer_trusted":true,"scanned":true,"source":"official"}"#;
        let v: Value = serde_json::from_str(&gate(p, "full_access").unwrap()).unwrap();
        assert_eq!(v["trust"], "official");
        assert_eq!(v["outcome"], "allow");
        assert_eq!(v["granted"], "full_access");
    }

    #[test]
    fn vulnerable_is_denied() {
        let p = r#"{"signed":true,"signer_trusted":true,"scanned":true,"vulnerabilities":2,"source":"official"}"#;
        let v: Value = serde_json::from_str(&gate(p, "read_only").unwrap()).unwrap();
        assert_eq!(v["trust"], "untrusted");
        assert_eq!(v["outcome"], "deny");
    }

    #[test]
    fn verified_downgrades_full_to_sandbox() {
        let p = r#"{"signed":true,"scanned":true,"source":"community"}"#;
        let v: Value = serde_json::from_str(&gate(p, "full_access").unwrap()).unwrap();
        assert_eq!(v["trust"], "verified");
        assert_eq!(v["outcome"], "downgrade");
        assert_eq!(v["granted"], "sandbox_edit");
    }

    #[test]
    fn bad_tier_errors() {
        assert!(gate("{}", "root").is_err());
    }

    #[test]
    fn bad_json_errors() {
        assert!(gate("nope", "read_only").is_err());
    }
}