1use 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 pub permissions: PermissionSetV1,
41}
42
43#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
44#[serde(deny_unknown_fields)]
45pub struct TrustGrantV1 {
46 pub schema_version: u32,
47 pub target: String,
48 pub capability: TrustCapabilityV1,
49 pub code_digest: SnapshotDigestV1,
50 pub permissions: PermissionSetV1,
51}
52
53impl TrustGrantV1 {
54 pub fn for_reviewed_requirement(requirement: &TrustRequirementV1) -> Self {
55 Self {
56 schema_version: TRUST_GRANT_SCHEMA_VERSION,
57 target: requirement.target.clone(),
58 capability: requirement.capability,
59 code_digest: requirement.code_digest,
60 permissions: requirement.permissions.clone(),
61 }
62 }
63
64 pub fn matches(&self, requirement: &TrustRequirementV1) -> bool {
65 self.schema_version == TRUST_GRANT_SCHEMA_VERSION
66 && self.target == requirement.target
67 && self.capability == requirement.capability
68 && self.code_digest == requirement.code_digest
69 && self.permissions == requirement.permissions
70 }
71}
72
73#[derive(Clone, Copy, Debug, Eq, PartialEq)]
74pub enum TrustDecisionV1 {
75 Trusted,
76 Missing,
77 CodeChanged,
78 PermissionsChanged,
79 UnsupportedGrantSchema,
80}
81
82impl TrustDecisionV1 {
83 pub const fn code(self) -> &'static str {
84 match self {
85 Self::Trusted => "trusted",
86 Self::Missing => "external_code_trust_missing",
87 Self::CodeChanged => "external_code_trust_code_changed",
88 Self::PermissionsChanged => "external_code_trust_permissions_changed",
89 Self::UnsupportedGrantSchema => "external_code_trust_schema_unsupported",
90 }
91 }
92}
93
94pub fn evaluate_trust(
95 grants: &[TrustGrantV1],
96 requirement: &TrustRequirementV1,
97) -> TrustDecisionV1 {
98 let candidates = grants.iter().filter(|grant| {
99 grant.target == requirement.target && grant.capability == requirement.capability
100 });
101 let mut saw_supported_candidate = false;
102 let mut saw_unsupported_candidate = false;
103 let mut saw_code = false;
104 for grant in candidates {
105 if grant.schema_version != TRUST_GRANT_SCHEMA_VERSION {
106 saw_unsupported_candidate = true;
107 continue;
108 }
109 saw_supported_candidate = true;
110 if grant.code_digest == requirement.code_digest {
111 saw_code = true;
112 if grant.permissions == requirement.permissions {
113 return TrustDecisionV1::Trusted;
114 }
115 }
116 }
117 if saw_code {
118 TrustDecisionV1::PermissionsChanged
119 } else if saw_supported_candidate {
120 TrustDecisionV1::CodeChanged
121 } else if saw_unsupported_candidate {
122 TrustDecisionV1::UnsupportedGrantSchema
123 } else {
124 TrustDecisionV1::Missing
125 }
126}
127
128#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
129#[serde(deny_unknown_fields)]
130pub struct TrustStoreV1 {
131 pub schema_version: u32,
132 #[serde(default)]
133 pub grants: Vec<TrustGrantV1>,
134}
135
136impl Default for TrustStoreV1 {
137 fn default() -> Self {
138 Self {
139 schema_version: TRUST_STORE_SCHEMA_VERSION,
140 grants: Vec::new(),
141 }
142 }
143}
144
145#[cfg(test)]
146mod tests {
147 use super::*;
148 use crate::plan::{PermissionSetV1, PermissionV1, SnapshotDigestV1};
149
150 fn requirement() -> TrustRequirementV1 {
151 TrustRequirementV1 {
152 target: "app/demo".to_string(),
153 capability: TrustCapabilityV1::AppGenerator,
154 code_digest: SnapshotDigestV1::builder("code").finish(),
155 permissions: PermissionSetV1::new([PermissionV1::Command {
156 program: "bun".to_string(),
157 }]),
158 }
159 }
160
161 #[test]
162 fn exact_requirement_matches_and_scope_changes_fail_closed() {
163 let requirement = requirement();
164 let grant = TrustGrantV1::for_reviewed_requirement(&requirement);
165 assert_eq!(
166 evaluate_trust(std::slice::from_ref(&grant), &requirement),
167 TrustDecisionV1::Trusted
168 );
169
170 let mut other = requirement.clone();
171 other.target = "app/other".to_string();
172 assert_eq!(
173 evaluate_trust(std::slice::from_ref(&grant), &other),
174 TrustDecisionV1::Missing
175 );
176
177 other = requirement.clone();
178 other.code_digest = SnapshotDigestV1::builder("changed").finish();
179 assert_eq!(
180 evaluate_trust(std::slice::from_ref(&grant), &other),
181 TrustDecisionV1::CodeChanged
182 );
183
184 other = requirement;
185 other.permissions = PermissionSetV1::default();
186 assert_eq!(
187 evaluate_trust(std::slice::from_ref(&grant), &other),
188 TrustDecisionV1::PermissionsChanged
189 );
190 }
191
192 #[test]
193 fn serialized_grant_contains_only_reviewable_identities() {
194 let encoded =
195 serde_json::to_string(&TrustGrantV1::for_reviewed_requirement(&requirement())).unwrap();
196 assert!(encoded.contains("app/demo"));
197 assert!(encoded.contains("app-generator"));
198 assert!(!encoded.contains("secret-value"));
199 assert!(!encoded.contains("/Users/"));
200 }
201
202 #[test]
203 fn trust_store_toml_round_trips_nonempty_grants() {
204 let store = TrustStoreV1 {
205 schema_version: TRUST_STORE_SCHEMA_VERSION,
206 grants: vec![TrustGrantV1::for_reviewed_requirement(&requirement())],
207 };
208 let encoded = toml::to_string_pretty(&store).unwrap();
209 let decoded: TrustStoreV1 = toml::from_str(&encoded).unwrap();
210 assert_eq!(decoded, store);
211 }
212
213 #[test]
214 fn unsupported_grant_does_not_mask_a_valid_grant() {
215 let requirement = requirement();
216 let mut unsupported = TrustGrantV1::for_reviewed_requirement(&requirement);
217 unsupported.schema_version += 1;
218 assert_eq!(
219 evaluate_trust(&[unsupported.clone()], &requirement),
220 TrustDecisionV1::UnsupportedGrantSchema
221 );
222 assert_eq!(
223 evaluate_trust(
224 &[
225 unsupported,
226 TrustGrantV1::for_reviewed_requirement(&requirement),
227 ],
228 &requirement,
229 ),
230 TrustDecisionV1::Trusted
231 );
232 }
233}