skilllite_core/skill/
trust.rs1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
4#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
5pub enum TrustTier {
6 Trusted,
7 Reviewed,
8 Community,
9 #[default]
10 Unknown,
11}
12
13#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
14#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
15pub enum TrustDecision {
16 Allow,
17 RequireConfirm,
18 Deny,
19}
20
21#[derive(Debug, Clone, PartialEq, Eq)]
22pub enum IntegritySignal {
23 Ok,
24 HashChanged,
25 SignatureInvalid,
26 Unsigned,
27}
28
29#[derive(Debug, Clone, PartialEq, Eq)]
30pub enum SignatureSignal {
31 Unsigned,
32 Valid,
33 Invalid,
34}
35
36#[derive(Debug, Clone)]
37pub struct TrustAssessment {
38 pub tier: TrustTier,
39 pub score: u8,
40 pub reasons: Vec<String>,
41 pub decision: TrustDecision,
42}
43
44pub fn assess_skill_trust(
45 source: Option<&str>,
46 signature: SignatureSignal,
47 integrity: IntegritySignal,
48 has_critical_scan: bool,
49 has_high_scan: bool,
50) -> TrustAssessment {
51 let mut reasons = Vec::new();
52
53 if matches!(
54 integrity,
55 IntegritySignal::HashChanged | IntegritySignal::SignatureInvalid
56 ) || matches!(signature, SignatureSignal::Invalid)
57 || has_critical_scan
58 {
59 if matches!(integrity, IntegritySignal::HashChanged) {
60 reasons.push("content hash drift detected".to_string());
61 }
62 if matches!(
63 integrity,
64 IntegritySignal::SignatureInvalid | IntegritySignal::Unsigned
65 ) && matches!(signature, SignatureSignal::Invalid)
66 {
67 reasons.push("signature validation failed".to_string());
68 }
69 if has_critical_scan {
70 reasons.push("critical security scan findings".to_string());
71 }
72 return TrustAssessment {
73 tier: TrustTier::Unknown,
74 score: 0,
75 reasons,
76 decision: TrustDecision::Deny,
77 };
78 }
79
80 let mut score: i32 = 0;
81 let src = source.unwrap_or("").to_lowercase();
82 if src.contains("clawhub:") || src.contains("github.com/exboys/skilllite") {
83 score += 25;
84 reasons.push("official source".to_string());
85 } else if src.contains("github.com/") || src.contains('/') {
86 score += 15;
87 reasons.push("known repository source".to_string());
88 } else if !src.is_empty() {
89 score += 8;
90 reasons.push("local/custom source".to_string());
91 }
92
93 match signature {
94 SignatureSignal::Valid => {
95 score += 25;
96 reasons.push("signature verified".to_string());
97 }
98 SignatureSignal::Unsigned => {
99 score += 8;
100 reasons.push("unsigned package".to_string());
101 }
102 SignatureSignal::Invalid => {}
103 }
104
105 match integrity {
106 IntegritySignal::Ok => score += 20,
107 IntegritySignal::Unsigned => score += 20, IntegritySignal::HashChanged | IntegritySignal::SignatureInvalid => {}
109 }
110
111 if has_high_scan {
112 score += 8;
113 reasons.push("high-risk scan findings present".to_string());
114 } else {
115 score += 20;
116 }
117
118 if score > 100 {
119 score = 100;
120 }
121 let score_u8 = score as u8;
122
123 let (tier, decision) = if score_u8 >= 85 {
124 (TrustTier::Trusted, TrustDecision::Allow)
125 } else if score_u8 >= 65 {
126 (TrustTier::Reviewed, TrustDecision::Allow)
127 } else if score_u8 >= 40 {
128 (TrustTier::Community, TrustDecision::RequireConfirm)
129 } else {
130 (TrustTier::Unknown, TrustDecision::RequireConfirm)
131 };
132
133 TrustAssessment {
134 tier,
135 score: score_u8,
136 reasons,
137 decision,
138 }
139}