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_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}