use crate::permission::{ApprovalDecision, ApprovalLedger, PermissionTier};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SkillSource {
Official,
FirstParty,
Community,
Unknown,
}
impl Default for SkillSource {
fn default() -> Self {
SkillSource::Unknown
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct SkillProvenance {
#[serde(default)]
pub signed: bool,
#[serde(default)]
pub signer_trusted: bool,
#[serde(default)]
pub scanned: bool,
#[serde(default)]
pub vulnerabilities: u64,
#[serde(default)]
pub source: SkillSource,
#[serde(default)]
pub success_count: u64,
#[serde(default)]
pub fail_count: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TrustTier {
Untrusted,
Community,
Verified,
Official,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum DeploymentOutcome {
Allow,
Downgrade,
Deny,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SkillDeploymentDecision {
pub trust: TrustTier,
pub ceiling: Option<PermissionTier>,
pub granted: Option<PermissionTier>,
pub outcome: DeploymentOutcome,
pub reason: String,
}
fn degraded(p: &SkillProvenance) -> bool {
p.fail_count > p.success_count + 2
}
pub fn classify_trust(p: &SkillProvenance) -> TrustTier {
if degraded(p) {
return TrustTier::Untrusted; }
if p.vulnerabilities > 0 {
return TrustTier::Untrusted; }
if p.signed && p.signer_trusted && p.scanned && p.source == SkillSource::Official {
TrustTier::Official
} else if p.signed && p.scanned {
TrustTier::Verified
} else if p.scanned {
TrustTier::Community
} else {
TrustTier::Untrusted
}
}
pub fn ceiling(tier: TrustTier) -> Option<PermissionTier> {
match tier {
TrustTier::Official => Some(PermissionTier::FullAccess),
TrustTier::Verified => Some(PermissionTier::SandboxEdit),
TrustTier::Community => Some(PermissionTier::ReadOnly),
TrustTier::Untrusted => None,
}
}
pub fn gate_skill_deployment(
p: &SkillProvenance,
requested: PermissionTier,
) -> SkillDeploymentDecision {
let trust = classify_trust(p);
let ceiling = ceiling(trust);
match ceiling {
None => SkillDeploymentDecision {
trust,
ceiling,
granted: None,
outcome: DeploymentOutcome::Deny,
reason: format!(
"skill is {trust:?}: deployment denied (unsigned/unscanned, vulnerable, or \
degraded provenance)"
),
},
Some(cap) => {
let granted = requested.min(cap);
let outcome = if granted == requested {
DeploymentOutcome::Allow
} else {
DeploymentOutcome::Downgrade
};
let reason = match outcome {
DeploymentOutcome::Allow => format!(
"skill is {trust:?}: granted {} (≤ {} ceiling)",
granted.as_str(),
cap.as_str()
),
DeploymentOutcome::Downgrade => format!(
"skill is {trust:?}: requested {} exceeds the {} ceiling — downgraded to {}",
requested.as_str(),
cap.as_str(),
granted.as_str()
),
DeploymentOutcome::Deny => unreachable!(),
};
SkillDeploymentDecision {
trust,
ceiling,
granted: Some(granted),
outcome,
reason,
}
}
}
}
pub fn skill_deployment_fingerprint(skill_name: &str, requested: PermissionTier) -> String {
format!("skill_deploy:{skill_name}:{}", requested.as_str())
}
#[derive(Debug, Clone, Serialize)]
pub struct PendingSkillApproval {
pub fingerprint: String,
pub skill_name: String,
pub requested: PermissionTier,
pub decision: SkillDeploymentDecision,
}
#[derive(Debug, Clone, Serialize)]
pub struct SkillDeploymentEnforcement {
pub deploy: bool,
pub effective_tier: Option<PermissionTier>,
pub overridden: bool,
pub blocked: bool,
pub pending: Option<PendingSkillApproval>,
pub reason: String,
}
pub fn enforce_deployment(
decision: &SkillDeploymentDecision,
skill_name: &str,
requested: PermissionTier,
ledger: &ApprovalLedger,
) -> SkillDeploymentEnforcement {
match decision.outcome {
DeploymentOutcome::Allow | DeploymentOutcome::Downgrade => SkillDeploymentEnforcement {
deploy: true,
effective_tier: decision.granted,
overridden: false,
blocked: false,
pending: None,
reason: decision.reason.clone(),
},
DeploymentOutcome::Deny => {
let fp = skill_deployment_fingerprint(skill_name, requested);
match ledger.lookup(&fp).map(|r| r.decision) {
Some(ApprovalDecision::Approved) => SkillDeploymentEnforcement {
deploy: true,
effective_tier: Some(requested),
overridden: true,
blocked: false,
pending: None,
reason: format!(
"skill '{skill_name}' was denied by provenance, but an operator approved \
deploying it at {} anyway",
requested.as_str()
),
},
Some(ApprovalDecision::Rejected) => SkillDeploymentEnforcement {
deploy: false,
effective_tier: None,
overridden: false,
blocked: true,
pending: None,
reason: format!(
"skill '{skill_name}' deployment was rejected by an operator"
),
},
None => SkillDeploymentEnforcement {
deploy: false,
effective_tier: None,
overridden: false,
blocked: false,
pending: Some(PendingSkillApproval {
fingerprint: fp,
skill_name: skill_name.to_string(),
requested,
decision: decision.clone(),
}),
reason: format!(
"skill '{skill_name}' denied by provenance — awaiting an operator decision \
to override"
),
},
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn prov() -> SkillProvenance {
SkillProvenance::default()
}
#[test]
fn official_signed_scanned_is_official_full_access() {
let p = SkillProvenance {
signed: true,
signer_trusted: true,
scanned: true,
source: SkillSource::Official,
..prov()
};
assert_eq!(classify_trust(&p), TrustTier::Official);
let d = gate_skill_deployment(&p, PermissionTier::FullAccess);
assert_eq!(d.outcome, DeploymentOutcome::Allow);
assert_eq!(d.granted, Some(PermissionTier::FullAccess));
}
#[test]
fn signed_scanned_non_official_is_verified() {
let p = SkillProvenance {
signed: true,
scanned: true,
source: SkillSource::Community,
..prov()
};
assert_eq!(classify_trust(&p), TrustTier::Verified);
let d = gate_skill_deployment(&p, PermissionTier::FullAccess);
assert_eq!(d.outcome, DeploymentOutcome::Downgrade);
assert_eq!(d.granted, Some(PermissionTier::SandboxEdit));
}
#[test]
fn scanned_unsigned_is_community_read_only() {
let p = SkillProvenance {
scanned: true,
..prov()
};
assert_eq!(classify_trust(&p), TrustTier::Community);
let d = gate_skill_deployment(&p, PermissionTier::ReadOnly);
assert_eq!(d.outcome, DeploymentOutcome::Allow);
let d2 = gate_skill_deployment(&p, PermissionTier::SandboxEdit);
assert_eq!(d2.outcome, DeploymentOutcome::Downgrade);
assert_eq!(d2.granted, Some(PermissionTier::ReadOnly));
}
#[test]
fn unsigned_unscanned_is_untrusted_denied() {
let d = gate_skill_deployment(&prov(), PermissionTier::ReadOnly);
assert_eq!(d.trust, TrustTier::Untrusted);
assert_eq!(d.outcome, DeploymentOutcome::Deny);
assert_eq!(d.granted, None);
}
#[test]
fn vulnerabilities_force_untrusted_even_if_signed() {
let p = SkillProvenance {
signed: true,
signer_trusted: true,
scanned: true,
vulnerabilities: 1,
source: SkillSource::Official,
..prov()
};
assert_eq!(classify_trust(&p), TrustTier::Untrusted);
assert_eq!(
gate_skill_deployment(&p, PermissionTier::ReadOnly).outcome,
DeploymentOutcome::Deny
);
}
#[test]
fn degraded_track_record_demotes_to_untrusted() {
let p = SkillProvenance {
signed: true,
signer_trusted: true,
scanned: true,
source: SkillSource::Official,
success_count: 1,
fail_count: 5,
..prov()
};
assert_eq!(classify_trust(&p), TrustTier::Untrusted);
}
#[test]
fn healthy_track_record_does_not_demote() {
let p = SkillProvenance {
signed: true,
signer_trusted: true,
scanned: true,
source: SkillSource::Official,
success_count: 10,
fail_count: 3,
..prov()
};
assert_eq!(classify_trust(&p), TrustTier::Official);
}
use crate::permission::{ApprovalDecision, ApprovalLedger, ApprovalRecord};
fn approval(fp: &str, decision: ApprovalDecision) -> ApprovalRecord {
ApprovalRecord {
fingerprint: fp.to_string(),
required_tier: PermissionTier::FullAccess,
decision,
reviewer: "human".into(),
reason: "test".into(),
evidence: None,
decided_at: "2026-06-30T00:00:00Z".into(),
}
}
#[test]
fn enforce_allow_deploys_at_granted() {
let p = SkillProvenance {
signed: true,
signer_trusted: true,
scanned: true,
source: SkillSource::Official,
..prov()
};
let d = gate_skill_deployment(&p, PermissionTier::FullAccess);
let e = enforce_deployment(&d, "deployer", PermissionTier::FullAccess, &ApprovalLedger::new());
assert!(e.deploy);
assert_eq!(e.effective_tier, Some(PermissionTier::FullAccess));
assert!(!e.overridden && !e.blocked && e.pending.is_none());
}
#[test]
fn enforce_downgrade_deploys_autonomously_at_cap() {
let p = SkillProvenance {
scanned: true,
..prov()
};
let d = gate_skill_deployment(&p, PermissionTier::SandboxEdit);
assert_eq!(d.outcome, DeploymentOutcome::Downgrade);
let e = enforce_deployment(&d, "capped", PermissionTier::SandboxEdit, &ApprovalLedger::new());
assert!(e.deploy, "a downgrade is a safe mitigation, not a hazard");
assert_eq!(e.effective_tier, Some(PermissionTier::ReadOnly));
assert!(e.pending.is_none() && !e.blocked);
}
#[test]
fn enforce_deny_unseen_is_pending() {
let d = gate_skill_deployment(&prov(), PermissionTier::ReadOnly);
assert_eq!(d.outcome, DeploymentOutcome::Deny);
let e = enforce_deployment(&d, "shady", PermissionTier::ReadOnly, &ApprovalLedger::new());
assert!(!e.deploy && !e.blocked);
let pending = e.pending.expect("a novel deny awaits a human");
assert_eq!(pending.skill_name, "shady");
assert_eq!(
pending.fingerprint,
skill_deployment_fingerprint("shady", PermissionTier::ReadOnly)
);
}
#[test]
fn enforce_deny_previously_approved_is_overridden() {
let d = gate_skill_deployment(&prov(), PermissionTier::SandboxEdit);
let fp = skill_deployment_fingerprint("trusted-anyway", PermissionTier::SandboxEdit);
let mut ledger = ApprovalLedger::new();
ledger.record(approval(&fp, ApprovalDecision::Approved)).unwrap();
let e = enforce_deployment(&d, "trusted-anyway", PermissionTier::SandboxEdit, &ledger);
assert!(e.deploy && e.overridden);
assert_eq!(e.effective_tier, Some(PermissionTier::SandboxEdit));
}
#[test]
fn enforce_deny_previously_rejected_is_blocked() {
let d = gate_skill_deployment(&prov(), PermissionTier::ReadOnly);
let fp = skill_deployment_fingerprint("nope", PermissionTier::ReadOnly);
let mut ledger = ApprovalLedger::new();
ledger.record(approval(&fp, ApprovalDecision::Rejected)).unwrap();
let e = enforce_deployment(&d, "nope", PermissionTier::ReadOnly, &ledger);
assert!(!e.deploy && e.blocked && e.pending.is_none());
}
#[test]
fn fingerprint_is_stable_per_skill_and_tier() {
assert_eq!(
skill_deployment_fingerprint("s", PermissionTier::ReadOnly),
skill_deployment_fingerprint("s", PermissionTier::ReadOnly)
);
assert_ne!(
skill_deployment_fingerprint("s", PermissionTier::ReadOnly),
skill_deployment_fingerprint("s", PermissionTier::FullAccess)
);
}
}