Skip to main content

shine_core/
trust.rs

1//! Versioned, target-local trust for opaque external Preset code.
2//!
3//! Permission declarations describe author intent and Plan approvals authorize
4//! one exact mutation. A trust grant is deliberately separate: it records that
5//! a user reviewed one exact external-code identity and permission set.
6
7use crate::plan::{PermissionSetV1, SnapshotDigestV1};
8use serde::{Deserialize, Serialize};
9
10pub const TRUST_GRANT_SCHEMA_VERSION: u32 = 1;
11pub const TRUST_STORE_SCHEMA_VERSION: u32 = 1;
12
13#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
14#[serde(rename_all = "kebab-case")]
15pub enum TrustCapabilityV1 {
16    AppHook,
17    AppGenerator,
18    AppArtifact,
19    SysBootstrapScript,
20    SysProfileCode,
21}
22
23impl TrustCapabilityV1 {
24    pub const fn as_str(self) -> &'static str {
25        match self {
26            Self::AppHook => "app-hook",
27            Self::AppGenerator => "app-generator",
28            Self::AppArtifact => "app-artifact",
29            Self::SysBootstrapScript => "sys-bootstrap-script",
30            Self::SysProfileCode => "sys-profile-code",
31        }
32    }
33}
34
35#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
36pub struct TrustRequirementV1 {
37    pub target: String,
38    pub capability: TrustCapabilityV1,
39    pub code_digest: SnapshotDigestV1,
40    /// Whether the Preset explicitly supplied a validated permission declaration.
41    /// An explicit declaration may intentionally normalize to an empty set when all
42    /// required effects are derived from typed metadata.
43    pub permissions_declared: bool,
44    pub permissions: PermissionSetV1,
45}
46
47#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
48#[serde(deny_unknown_fields)]
49pub struct TrustGrantV1 {
50    pub schema_version: u32,
51    pub target: String,
52    pub capability: TrustCapabilityV1,
53    pub code_digest: SnapshotDigestV1,
54    pub permissions: PermissionSetV1,
55}
56
57impl TrustGrantV1 {
58    pub fn for_reviewed_requirement(requirement: &TrustRequirementV1) -> Self {
59        Self {
60            schema_version: TRUST_GRANT_SCHEMA_VERSION,
61            target: requirement.target.clone(),
62            capability: requirement.capability,
63            code_digest: requirement.code_digest,
64            permissions: requirement.permissions.clone(),
65        }
66    }
67
68    pub fn matches(&self, requirement: &TrustRequirementV1) -> bool {
69        self.schema_version == TRUST_GRANT_SCHEMA_VERSION
70            && self.target == requirement.target
71            && self.capability == requirement.capability
72            && self.code_digest == requirement.code_digest
73            && self.permissions == requirement.permissions
74    }
75}
76
77#[derive(Clone, Copy, Debug, Eq, PartialEq)]
78pub enum TrustDecisionV1 {
79    Trusted,
80    Missing,
81    CodeChanged,
82    PermissionsChanged,
83    UnsupportedGrantSchema,
84}
85
86impl TrustDecisionV1 {
87    pub const fn code(self) -> &'static str {
88        match self {
89            Self::Trusted => "trusted",
90            Self::Missing => "external_code_trust_missing",
91            Self::CodeChanged => "external_code_trust_code_changed",
92            Self::PermissionsChanged => "external_code_trust_permissions_changed",
93            Self::UnsupportedGrantSchema => "external_code_trust_schema_unsupported",
94        }
95    }
96}
97
98pub fn evaluate_trust(
99    grants: &[TrustGrantV1],
100    requirement: &TrustRequirementV1,
101) -> TrustDecisionV1 {
102    let candidates = grants.iter().filter(|grant| {
103        grant.target == requirement.target && grant.capability == requirement.capability
104    });
105    let mut saw_supported_candidate = false;
106    let mut saw_unsupported_candidate = false;
107    let mut saw_code = false;
108    for grant in candidates {
109        if grant.schema_version != TRUST_GRANT_SCHEMA_VERSION {
110            saw_unsupported_candidate = true;
111            continue;
112        }
113        saw_supported_candidate = true;
114        if grant.code_digest == requirement.code_digest {
115            saw_code = true;
116            if grant.permissions == requirement.permissions {
117                return TrustDecisionV1::Trusted;
118            }
119        }
120    }
121    if saw_code {
122        TrustDecisionV1::PermissionsChanged
123    } else if saw_supported_candidate {
124        TrustDecisionV1::CodeChanged
125    } else if saw_unsupported_candidate {
126        TrustDecisionV1::UnsupportedGrantSchema
127    } else {
128        TrustDecisionV1::Missing
129    }
130}
131
132#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
133#[serde(deny_unknown_fields)]
134pub struct TrustStoreV1 {
135    pub schema_version: u32,
136    #[serde(default)]
137    pub grants: Vec<TrustGrantV1>,
138}
139
140impl Default for TrustStoreV1 {
141    fn default() -> Self {
142        Self {
143            schema_version: TRUST_STORE_SCHEMA_VERSION,
144            grants: Vec::new(),
145        }
146    }
147}
148
149#[cfg(test)]
150mod tests {
151    use super::*;
152    use crate::plan::{PermissionSetV1, PermissionV1, SnapshotDigestV1};
153
154    fn requirement() -> TrustRequirementV1 {
155        TrustRequirementV1 {
156            target: "app/demo".to_string(),
157            capability: TrustCapabilityV1::AppGenerator,
158            code_digest: SnapshotDigestV1::builder("code").finish(),
159            permissions_declared: true,
160            permissions: PermissionSetV1::new([PermissionV1::Command {
161                program: "bun".to_string(),
162            }]),
163        }
164    }
165
166    #[test]
167    fn exact_requirement_matches_and_scope_changes_fail_closed() {
168        let requirement = requirement();
169        let grant = TrustGrantV1::for_reviewed_requirement(&requirement);
170        assert_eq!(
171            evaluate_trust(std::slice::from_ref(&grant), &requirement),
172            TrustDecisionV1::Trusted
173        );
174
175        let mut other = requirement.clone();
176        other.target = "app/other".to_string();
177        assert_eq!(
178            evaluate_trust(std::slice::from_ref(&grant), &other),
179            TrustDecisionV1::Missing
180        );
181
182        other = requirement.clone();
183        other.code_digest = SnapshotDigestV1::builder("changed").finish();
184        assert_eq!(
185            evaluate_trust(std::slice::from_ref(&grant), &other),
186            TrustDecisionV1::CodeChanged
187        );
188
189        other = requirement;
190        other.permissions = PermissionSetV1::default();
191        assert_eq!(
192            evaluate_trust(std::slice::from_ref(&grant), &other),
193            TrustDecisionV1::PermissionsChanged
194        );
195    }
196
197    #[test]
198    fn serialized_grant_contains_only_reviewable_identities() {
199        let encoded =
200            serde_json::to_string(&TrustGrantV1::for_reviewed_requirement(&requirement())).unwrap();
201        assert!(encoded.contains("app/demo"));
202        assert!(encoded.contains("app-generator"));
203        assert!(!encoded.contains("secret-value"));
204        assert!(!encoded.contains("/Users/"));
205    }
206
207    #[test]
208    fn trust_store_toml_round_trips_nonempty_grants() {
209        let store = TrustStoreV1 {
210            schema_version: TRUST_STORE_SCHEMA_VERSION,
211            grants: vec![TrustGrantV1::for_reviewed_requirement(&requirement())],
212        };
213        let encoded = toml::to_string_pretty(&store).unwrap();
214        let decoded: TrustStoreV1 = toml::from_str(&encoded).unwrap();
215        assert_eq!(decoded, store);
216    }
217
218    #[test]
219    fn unsupported_grant_does_not_mask_a_valid_grant() {
220        let requirement = requirement();
221        let mut unsupported = TrustGrantV1::for_reviewed_requirement(&requirement);
222        unsupported.schema_version += 1;
223        assert_eq!(
224            evaluate_trust(&[unsupported.clone()], &requirement),
225            TrustDecisionV1::UnsupportedGrantSchema
226        );
227        assert_eq!(
228            evaluate_trust(
229                &[
230                    unsupported,
231                    TrustGrantV1::for_reviewed_requirement(&requirement),
232                ],
233                &requirement,
234            ),
235            TrustDecisionV1::Trusted
236        );
237    }
238}