Skip to main content

ratify_protocol/
scope.rs

1//! Canonical scope vocabulary for Ratify Protocol v1.
2//!
3//! MUST stay in lock-step with Go's scope.go, TS's scope.ts, and Python's scope.py.
4
5// --- Meeting scopes ---
6pub const SCOPE_MEETING_ATTEND: &str = "meeting:attend";
7pub const SCOPE_MEETING_SPEAK: &str = "meeting:speak";
8pub const SCOPE_MEETING_VIDEO: &str = "meeting:video";
9pub const SCOPE_MEETING_CHAT: &str = "meeting:chat";
10pub const SCOPE_MEETING_SHARE_SCREEN: &str = "meeting:share_screen";
11pub const SCOPE_MEETING_RECORD: &str = "meeting:record"; // sensitive
12
13// --- Communication scopes ---
14pub const SCOPE_COMMS_MESSAGE_READ: &str = "comms:message:read";
15pub const SCOPE_COMMS_MESSAGE_SEND: &str = "comms:message:send";
16pub const SCOPE_COMMS_MESSAGE_DELETE: &str = "comms:message:delete"; // sensitive
17pub const SCOPE_COMMS_EMAIL_READ: &str = "comms:email:read";
18pub const SCOPE_COMMS_EMAIL_SEND: &str = "comms:email:send";
19pub const SCOPE_COMMS_EMAIL_DELETE: &str = "comms:email:delete"; // sensitive
20pub const SCOPE_COMMS_CALENDAR_READ: &str = "comms:calendar:read";
21pub const SCOPE_COMMS_CALENDAR_WRITE: &str = "comms:calendar:write";
22
23// --- File scopes ---
24pub const SCOPE_FILES_READ: &str = "files:read";
25pub const SCOPE_FILES_WRITE: &str = "files:write"; // sensitive
26
27// --- Identity scopes ---
28pub const SCOPE_IDENTITY_PROVE: &str = "identity:prove";
29pub const SCOPE_IDENTITY_DELEGATE: &str = "identity:delegate"; // sensitive
30
31// --- Transaction scopes (v1, core to the "transaction horizon" thesis) ---
32pub const SCOPE_TRANSACT_PURCHASE: &str = "transact:purchase";
33pub const SCOPE_TRANSACT_SELL: &str = "transact:sell";
34pub const SCOPE_PAYMENTS_SEND: &str = "payments:send";
35pub const SCOPE_PAYMENTS_RECEIVE: &str = "payments:receive";
36pub const SCOPE_PAYMENTS_AUTHORIZE: &str = "payments:authorize"; // sensitive
37
38// --- Contract scopes ---
39pub const SCOPE_CONTRACT_READ: &str = "contract:read";
40pub const SCOPE_CONTRACT_SIGN: &str = "contract:sign"; // sensitive
41
42// --- Data scopes (structured application data, distinct from files) ---
43pub const SCOPE_DATA_READ: &str = "data:read";
44pub const SCOPE_DATA_WRITE: &str = "data:write"; // sensitive
45pub const SCOPE_DATA_DELETE: &str = "data:delete"; // sensitive
46pub const SCOPE_DATA_EXPORT: &str = "data:export"; // sensitive — exfiltration
47pub const SCOPE_DATA_SHARE: &str = "data:share";
48
49// --- Execute scopes ---
50pub const SCOPE_EXECUTE_TOOL: &str = "execute:tool";
51pub const SCOPE_EXECUTE_CODE: &str = "execute:code"; // sensitive
52
53// --- Generate scopes (AI content generation on someone's behalf) ---
54pub const SCOPE_GENERATE_CONTENT: &str = "generate:content";
55// Sensitive by policy: any "imitate a real person" generation creates
56// an auditable explicit authorization trail.
57pub const SCOPE_GENERATE_DEEPFAKE: &str = "generate:deepfake"; // sensitive
58
59// --- Physical-world scopes (v1, first-class coverage for embodied agents) ---
60// Ratify is channel-agnostic: same cert/bundle/verify semantics for software
61// agents and for robots, drones, vehicles, infrastructure controllers.
62// Location / time / speed / amount / rate bounds live in first-class
63// Constraint objects on DelegationCert (see types.rs).
64
65pub const SCOPE_PHYSICAL_ENTER: &str = "physical:enter";
66pub const SCOPE_PHYSICAL_EXIT: &str = "physical:exit";
67pub const SCOPE_PHYSICAL_ACTUATE: &str = "physical:actuate"; // sensitive
68pub const SCOPE_PHYSICAL_MANIPULATE: &str = "physical:manipulate"; // sensitive
69
70pub const SCOPE_ROBOT_OPERATE: &str = "robot:operate";
71pub const SCOPE_ROBOT_MOVE: &str = "robot:move";
72pub const SCOPE_ROBOT_INTERACT: &str = "robot:interact";
73
74pub const SCOPE_DRONE_FLY: &str = "drone:fly"; // sensitive
75pub const SCOPE_DRONE_DELIVER: &str = "drone:deliver";
76pub const SCOPE_DRONE_CAPTURE: &str = "drone:capture";
77
78pub const SCOPE_VEHICLE_OPERATE: &str = "vehicle:operate"; // sensitive
79pub const SCOPE_VEHICLE_TRANSPORT: &str = "vehicle:transport";
80pub const SCOPE_VEHICLE_CHARGE: &str = "vehicle:charge";
81
82pub const SCOPE_INFRASTRUCTURE_MONITOR: &str = "infrastructure:monitor";
83pub const SCOPE_INFRASTRUCTURE_CONTROL: &str = "infrastructure:control"; // sensitive
84pub const SCOPE_INFRASTRUCTURE_ACCESS: &str = "infrastructure:access"; // sensitive
85
86pub const SCOPE_ACTUATE_VALVE: &str = "actuate:valve"; // sensitive
87pub const SCOPE_ACTUATE_MOTOR: &str = "actuate:motor"; // sensitive
88pub const SCOPE_ACTUATE_SWITCH: &str = "actuate:switch"; // sensitive
89
90// --- Extension pattern ---
91/// Any scope string starting with CUSTOM_SCOPE_PREFIX is accepted by
92/// validate_scopes, passes through expand_scopes unchanged, and is treated as
93/// non-sensitive unless the application opts in via out-of-band policy.
94///
95/// Example: `"custom:acme:inventory:read"`
96pub const CUSTOM_SCOPE_PREFIX: &str = "custom:";
97
98fn sensitive_scopes() -> &'static [&'static str] {
99    &[
100        SCOPE_MEETING_RECORD,
101        SCOPE_COMMS_MESSAGE_DELETE,
102        SCOPE_COMMS_EMAIL_DELETE,
103        SCOPE_FILES_WRITE,
104        SCOPE_IDENTITY_DELEGATE,
105        SCOPE_PAYMENTS_AUTHORIZE,
106        SCOPE_CONTRACT_SIGN,
107        SCOPE_DATA_WRITE,
108        SCOPE_DATA_DELETE,
109        SCOPE_DATA_EXPORT,
110        SCOPE_EXECUTE_CODE,
111        SCOPE_GENERATE_DEEPFAKE,
112        SCOPE_PHYSICAL_ACTUATE,
113        SCOPE_PHYSICAL_MANIPULATE,
114        SCOPE_DRONE_FLY,
115        SCOPE_VEHICLE_OPERATE,
116        SCOPE_INFRASTRUCTURE_CONTROL,
117        SCOPE_INFRASTRUCTURE_ACCESS,
118        SCOPE_ACTUATE_VALVE,
119        SCOPE_ACTUATE_MOTOR,
120        SCOPE_ACTUATE_SWITCH,
121    ]
122}
123
124fn valid_scopes() -> &'static [&'static str] {
125    &[
126        SCOPE_MEETING_ATTEND,
127        SCOPE_MEETING_SPEAK,
128        SCOPE_MEETING_VIDEO,
129        SCOPE_MEETING_CHAT,
130        SCOPE_MEETING_SHARE_SCREEN,
131        SCOPE_MEETING_RECORD,
132        SCOPE_COMMS_MESSAGE_READ,
133        SCOPE_COMMS_MESSAGE_SEND,
134        SCOPE_COMMS_MESSAGE_DELETE,
135        SCOPE_COMMS_EMAIL_READ,
136        SCOPE_COMMS_EMAIL_SEND,
137        SCOPE_COMMS_EMAIL_DELETE,
138        SCOPE_COMMS_CALENDAR_READ,
139        SCOPE_COMMS_CALENDAR_WRITE,
140        SCOPE_FILES_READ,
141        SCOPE_FILES_WRITE,
142        SCOPE_IDENTITY_PROVE,
143        SCOPE_IDENTITY_DELEGATE,
144        SCOPE_TRANSACT_PURCHASE,
145        SCOPE_TRANSACT_SELL,
146        SCOPE_PAYMENTS_SEND,
147        SCOPE_PAYMENTS_RECEIVE,
148        SCOPE_PAYMENTS_AUTHORIZE,
149        SCOPE_CONTRACT_READ,
150        SCOPE_CONTRACT_SIGN,
151        SCOPE_DATA_READ,
152        SCOPE_DATA_WRITE,
153        SCOPE_DATA_DELETE,
154        SCOPE_DATA_EXPORT,
155        SCOPE_DATA_SHARE,
156        SCOPE_EXECUTE_TOOL,
157        SCOPE_EXECUTE_CODE,
158        SCOPE_GENERATE_CONTENT,
159        SCOPE_GENERATE_DEEPFAKE,
160        SCOPE_PHYSICAL_ENTER,
161        SCOPE_PHYSICAL_EXIT,
162        SCOPE_PHYSICAL_ACTUATE,
163        SCOPE_PHYSICAL_MANIPULATE,
164        SCOPE_ROBOT_OPERATE,
165        SCOPE_ROBOT_MOVE,
166        SCOPE_ROBOT_INTERACT,
167        SCOPE_DRONE_FLY,
168        SCOPE_DRONE_DELIVER,
169        SCOPE_DRONE_CAPTURE,
170        SCOPE_VEHICLE_OPERATE,
171        SCOPE_VEHICLE_TRANSPORT,
172        SCOPE_VEHICLE_CHARGE,
173        SCOPE_INFRASTRUCTURE_MONITOR,
174        SCOPE_INFRASTRUCTURE_CONTROL,
175        SCOPE_INFRASTRUCTURE_ACCESS,
176        SCOPE_ACTUATE_VALVE,
177        SCOPE_ACTUATE_MOTOR,
178        SCOPE_ACTUATE_SWITCH,
179    ]
180}
181
182fn wildcard_expansion(w: &str) -> Option<&'static [&'static str]> {
183    match w {
184        "meeting:*" => Some(&[
185            SCOPE_MEETING_ATTEND,
186            SCOPE_MEETING_SPEAK,
187            SCOPE_MEETING_VIDEO,
188            SCOPE_MEETING_CHAT,
189            SCOPE_MEETING_SHARE_SCREEN,
190        ]),
191        "comms:message:*" => Some(&[SCOPE_COMMS_MESSAGE_READ, SCOPE_COMMS_MESSAGE_SEND]),
192        "comms:email:*" => Some(&[SCOPE_COMMS_EMAIL_READ, SCOPE_COMMS_EMAIL_SEND]),
193        "comms:*" => Some(&[
194            SCOPE_COMMS_MESSAGE_READ,
195            SCOPE_COMMS_MESSAGE_SEND,
196            SCOPE_COMMS_EMAIL_READ,
197            SCOPE_COMMS_EMAIL_SEND,
198            SCOPE_COMMS_CALENDAR_READ,
199            SCOPE_COMMS_CALENDAR_WRITE,
200        ]),
201        "transact:*" => Some(&[SCOPE_TRANSACT_PURCHASE, SCOPE_TRANSACT_SELL]),
202        "payments:*" => Some(&[SCOPE_PAYMENTS_SEND, SCOPE_PAYMENTS_RECEIVE]),
203        "data:*" => Some(&[SCOPE_DATA_READ, SCOPE_DATA_SHARE]),
204        "execute:*" => Some(&[SCOPE_EXECUTE_TOOL]),
205        "generate:*" => Some(&[SCOPE_GENERATE_CONTENT]),
206        "physical:*" => Some(&[SCOPE_PHYSICAL_ENTER, SCOPE_PHYSICAL_EXIT]),
207        "robot:*" => Some(&[SCOPE_ROBOT_OPERATE, SCOPE_ROBOT_MOVE, SCOPE_ROBOT_INTERACT]),
208        "drone:*" => Some(&[SCOPE_DRONE_DELIVER, SCOPE_DRONE_CAPTURE]),
209        "vehicle:*" => Some(&[SCOPE_VEHICLE_TRANSPORT, SCOPE_VEHICLE_CHARGE]),
210        "infrastructure:*" => Some(&[SCOPE_INFRASTRUCTURE_MONITOR]),
211        // actuate:* — every member sensitive; NO wildcard expansion.
212        _ => None,
213    }
214}
215
216fn is_custom_scope(s: &str) -> bool {
217    s.starts_with(CUSTOM_SCOPE_PREFIX) && s.len() > CUSTOM_SCOPE_PREFIX.len()
218}
219
220/// Return an error message if any scope is invalid; None if all valid.
221/// Custom scopes (prefix "custom:") are accepted as valid extensions.
222pub fn validate_scopes(scopes: &[String]) -> Option<String> {
223    for s in scopes {
224        if valid_scopes().contains(&s.as_str()) {
225            continue;
226        }
227        if wildcard_expansion(s).is_some() {
228            continue;
229        }
230        if is_custom_scope(s) {
231            continue;
232        }
233        return Some(format!(
234            "unknown scope \"{}\": not in canonical vocabulary and not a custom: extension",
235            s
236        ));
237    }
238    None
239}
240
241/// True if the scope is flagged as sensitive. Custom scopes are non-sensitive
242/// by default; applications may enforce policy out-of-band.
243pub fn is_sensitive(scope: &str) -> bool {
244    sensitive_scopes().contains(&scope)
245}
246
247/// Replace wildcard scopes with their constituent non-sensitive scopes.
248/// Deduplicates and returns lex-sorted. Custom scopes pass through unchanged.
249pub fn expand_scopes(scopes: &[String]) -> Vec<String> {
250    let mut seen = std::collections::BTreeSet::new();
251    for s in scopes {
252        if let Some(children) = wildcard_expansion(s) {
253            for c in children {
254                seen.insert((*c).to_string());
255            }
256        } else {
257            seen.insert(s.clone());
258        }
259    }
260    seen.into_iter().collect()
261}
262
263pub fn has_scope(granted: &[String], required: &str) -> bool {
264    expand_scopes(granted).iter().any(|s| s == required)
265}
266
267/// Set of scopes in every input list after wildcard expansion. Lex-sorted.
268pub fn intersect_scopes(lists: &[&[String]]) -> Vec<String> {
269    if lists.is_empty() {
270        return Vec::new();
271    }
272    let mut effective: std::collections::BTreeSet<String> =
273        expand_scopes(lists[0]).into_iter().collect();
274    for list in &lists[1..] {
275        let expanded: std::collections::BTreeSet<String> =
276            expand_scopes(list).into_iter().collect();
277        effective = effective.intersection(&expanded).cloned().collect();
278    }
279    effective.into_iter().collect()
280}