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