1use alloc::format;
21use alloc::string::{String, ToString};
22#[cfg(kani)]
23use alloc::vec;
24use alloc::vec::Vec;
25
26use chio_core_types::capability::{
27 scope::{ChioScope, Constraint, Operation, ToolGrant},
28 token::CapabilityToken,
29};
30
31#[derive(Debug, Clone, Copy)]
37pub struct MatchedGrant<'a> {
38 pub index: usize,
40 pub grant: &'a ToolGrant,
42 pub specificity: (u8, u8, usize),
44}
45
46#[derive(Debug, Clone, PartialEq, Eq)]
53pub enum ScopeMatchError {
54 OutOfScope,
56 ConstraintError(String),
59}
60
61pub fn resolve_matching_grants<'a>(
68 scope: &'a ChioScope,
69 tool_name: &str,
70 server_id: &str,
71 arguments: &serde_json::Value,
72) -> Result<Vec<MatchedGrant<'a>>, ScopeMatchError> {
73 #[cfg(kani)]
74 if scope.grants.len() == 1 {
75 let grant = &scope.grants[0];
76 if !grant.constraints.is_empty() {
77 return Err(ScopeMatchError::ConstraintError(
78 "portable kernel cannot safely evaluate constrained Kani fixture".to_string(),
79 ));
80 }
81
82 if matches_pattern(&grant.server_id, server_id)
83 && matches_pattern(&grant.tool_name, tool_name)
84 && grant.operations.contains(&Operation::Invoke)
85 {
86 return Ok(vec![MatchedGrant {
87 index: 0,
88 grant,
89 specificity: (
90 u8::from(pattern_exact(&grant.server_id, server_id)),
91 u8::from(pattern_exact(&grant.tool_name, tool_name)),
92 grant.constraints.len(),
93 ),
94 }]);
95 }
96
97 return Ok(Vec::new());
98 }
99
100 let mut matches: Vec<MatchedGrant<'a>> = Vec::new();
101
102 for (index, grant) in scope.grants.iter().enumerate() {
103 let covered = match grant_covers(grant, tool_name, server_id, arguments) {
104 Ok(covered) => covered,
105 Err(error @ ScopeMatchError::ConstraintError(_)) => return Err(error),
106 Err(error) => return Err(error),
107 };
108 if !covered {
109 continue;
110 }
111
112 matches.push(MatchedGrant {
113 index,
114 grant,
115 specificity: (
116 u8::from(grant.server_id == server_id),
117 u8::from(grant.tool_name == tool_name),
118 grant.constraints.len(),
119 ),
120 });
121 }
122
123 #[cfg(kani)]
124 if matches.len() <= 1 {
125 return Ok(matches);
128 }
129
130 matches.sort_by(|left, right| {
131 right
132 .specificity
133 .cmp(&left.specificity)
134 .then_with(|| left.index.cmp(&right.index))
135 });
136
137 Ok(matches)
138}
139
140pub fn resolve_capability_grants<'a>(
143 capability: &'a CapabilityToken,
144 tool_name: &str,
145 server_id: &str,
146 arguments: &serde_json::Value,
147) -> Result<Vec<MatchedGrant<'a>>, ScopeMatchError> {
148 let matches = resolve_matching_grants(&capability.scope, tool_name, server_id, arguments)?;
149 if matches.is_empty() {
150 return Err(ScopeMatchError::OutOfScope);
151 }
152 Ok(matches)
153}
154
155fn grant_covers(
156 grant: &ToolGrant,
157 tool_name: &str,
158 server_id: &str,
159 arguments: &serde_json::Value,
160) -> Result<bool, ScopeMatchError> {
161 if !matches_pattern(&grant.server_id, server_id)
162 || !matches_pattern(&grant.tool_name, tool_name)
163 || !grant.operations.contains(&Operation::Invoke)
164 {
165 return Ok(false);
166 }
167
168 #[cfg(kani)]
169 let constraints_ok = if grant.constraints.is_empty() {
170 true
171 } else {
172 constraints_match(&grant.constraints, arguments)?
173 };
174
175 #[cfg(not(kani))]
176 let constraints_ok = constraints_match(&grant.constraints, arguments)?;
177
178 Ok(constraints_ok)
179}
180
181fn constraints_match(
182 constraints: &[Constraint],
183 arguments: &serde_json::Value,
184) -> Result<bool, ScopeMatchError> {
185 for constraint in constraints {
186 if !constraint_matches(constraint, arguments)? {
187 return Ok(false);
188 }
189 }
190 Ok(true)
191}
192
193fn constraint_matches(
194 constraint: &Constraint,
195 arguments: &serde_json::Value,
196) -> Result<bool, ScopeMatchError> {
197 let string_leaves = collect_string_leaves(arguments);
198
199 match constraint {
200 Constraint::PathPrefix(prefix) => {
201 let candidates: Vec<&str> = string_leaves
202 .iter()
203 .filter(|leaf| {
204 leaf.key.as_deref().is_some_and(is_path_key) || looks_like_path(&leaf.value)
205 })
206 .map(|leaf| leaf.value.as_str())
207 .collect();
208 Ok(!candidates.is_empty()
209 && candidates
210 .into_iter()
211 .all(|path| path_has_prefix(path, prefix)))
212 }
213 Constraint::DomainExact(expected) => {
214 let expected = normalize_domain(expected);
215 let domains = collect_domain_candidates(&string_leaves);
216 Ok(!domains.is_empty() && domains.into_iter().all(|domain| domain == expected))
217 }
218 Constraint::DomainGlob(pattern) => {
219 let pattern = pattern.to_ascii_lowercase();
220 let domains = collect_domain_candidates(&string_leaves);
221 Ok(!domains.is_empty()
222 && domains
223 .into_iter()
224 .all(|domain| wildcard_matches(&pattern, &domain)))
225 }
226 Constraint::MaxLength(max) => Ok(string_leaves.iter().all(|leaf| leaf.value.len() <= *max)),
227 Constraint::MaxArgsSize(max) => Ok(arguments.to_string().len() <= *max),
228 Constraint::Custom(key, expected) => Ok(argument_contains_custom(arguments, key, expected)),
229 Constraint::AudienceAllowlist(allowed) => {
230 Ok(audience_allowlist_matches(arguments, allowed))
231 }
232 Constraint::MemoryStoreAllowlist(allowed) => {
233 Ok(memory_store_allowlist_matches(arguments, allowed))
234 }
235 Constraint::RegexMatch(_)
236 | Constraint::GovernedIntentRequired
237 | Constraint::RequireApprovalAbove { .. }
238 | Constraint::RequireCumulativeApprovalAbove { .. }
239 | Constraint::SellerExact(_)
240 | Constraint::MinimumRuntimeAssurance(_)
241 | Constraint::MinimumAutonomyTier(_)
242 | Constraint::TableAllowlist(_)
243 | Constraint::ColumnDenylist(_)
244 | Constraint::MaxRowsReturned(_)
245 | Constraint::OperationClass(_)
246 | Constraint::ContentReviewTier(_)
247 | Constraint::MaxTransactionAmountUsd(_)
248 | Constraint::RequireDualApproval(_)
249 | Constraint::ModelConstraint { .. }
250 | Constraint::MemoryWriteDenyPatterns(_) => Err(ScopeMatchError::ConstraintError(format!(
251 "portable kernel cannot safely evaluate {}",
252 constraint_name(constraint)
253 ))),
254 }
255}
256
257fn matches_pattern(pattern: &str, candidate: &str) -> bool {
258 #[cfg(kani)]
259 {
260 let pattern = pattern.as_bytes();
261 let candidate = candidate.as_bytes();
262 if pattern.len() == 1 && pattern[0] == b'*' {
263 return true;
264 }
265 if pattern.len() != candidate.len() {
266 return false;
267 }
268 if pattern.len() == 1 {
269 return pattern[0] == candidate[0];
270 }
271 }
272
273 pattern == "*" || pattern == candidate
274}
275
276#[cfg(kani)]
277fn pattern_exact(pattern: &str, candidate: &str) -> bool {
278 #[cfg(kani)]
279 {
280 let pattern = pattern.as_bytes();
281 let candidate = candidate.as_bytes();
282 if pattern.len() != candidate.len() {
283 return false;
284 }
285 if pattern.len() == 1 {
286 return pattern[0] == candidate[0];
287 }
288 }
289
290 pattern == candidate
291}
292
293fn path_has_prefix(candidate: &str, prefix: &str) -> bool {
294 let Some(candidate) = normalize_path(candidate) else {
295 return false;
296 };
297 let Some(prefix) = normalize_path(prefix) else {
298 return false;
299 };
300 if candidate.is_absolute != prefix.is_absolute {
301 return false;
302 }
303 if prefix.segments.len() > candidate.segments.len() {
304 return false;
305 }
306 prefix
307 .segments
308 .iter()
309 .zip(candidate.segments.iter())
310 .all(|(expected, actual)| expected == actual)
311}
312
313#[derive(Debug, PartialEq, Eq)]
314struct NormalizedPath {
315 is_absolute: bool,
316 segments: Vec<String>,
317}
318
319fn normalize_path(path: &str) -> Option<NormalizedPath> {
320 let is_absolute = path.starts_with('/') || path.starts_with('\\');
321 let mut segments = Vec::new();
322 for segment in path.split(['/', '\\']) {
323 if segment.is_empty() || segment == "." {
324 continue;
325 }
326 if segment == ".." {
327 segments.pop()?;
328 continue;
329 }
330 segments.push(segment.to_string());
331 }
332 Some(NormalizedPath {
333 is_absolute,
334 segments,
335 })
336}
337
338fn constraint_name(constraint: &Constraint) -> &'static str {
339 match constraint {
340 Constraint::PathPrefix(_) => "path_prefix",
341 Constraint::DomainExact(_) => "domain_exact",
342 Constraint::DomainGlob(_) => "domain_glob",
343 Constraint::RegexMatch(_) => "regex_match",
344 Constraint::MaxLength(_) => "max_length",
345 Constraint::MaxArgsSize(_) => "max_args_size",
346 Constraint::GovernedIntentRequired => "governed_intent_required",
347 Constraint::RequireApprovalAbove { .. } => "require_approval_above",
348 Constraint::RequireCumulativeApprovalAbove { .. } => "require_cumulative_approval_above",
349 Constraint::SellerExact(_) => "seller_exact",
350 Constraint::MinimumRuntimeAssurance(_) => "minimum_runtime_assurance",
351 Constraint::MinimumAutonomyTier(_) => "minimum_autonomy_tier",
352 Constraint::Custom(_, _) => "custom",
353 Constraint::TableAllowlist(_) => "table_allowlist",
354 Constraint::ColumnDenylist(_) => "column_denylist",
355 Constraint::MaxRowsReturned(_) => "max_rows_returned",
356 Constraint::OperationClass(_) => "operation_class",
357 Constraint::AudienceAllowlist(_) => "audience_allowlist",
358 Constraint::ContentReviewTier(_) => "content_review_tier",
359 Constraint::MaxTransactionAmountUsd(_) => "max_transaction_amount_usd",
360 Constraint::RequireDualApproval(_) => "require_dual_approval",
361 Constraint::ModelConstraint { .. } => "model_constraint",
362 Constraint::MemoryStoreAllowlist(_) => "memory_store_allowlist",
363 Constraint::MemoryWriteDenyPatterns(_) => "memory_write_deny_patterns",
364 }
365}
366
367#[derive(Clone)]
368struct StringLeaf {
369 key: Option<String>,
370 value: String,
371}
372
373fn collect_string_leaves(arguments: &serde_json::Value) -> Vec<StringLeaf> {
374 let mut leaves = Vec::new();
375 collect_string_leaves_inner(arguments, None, &mut leaves);
376 leaves
377}
378
379fn collect_string_leaves_inner(
380 arguments: &serde_json::Value,
381 current_key: Option<&str>,
382 leaves: &mut Vec<StringLeaf>,
383) {
384 match arguments {
385 serde_json::Value::String(value) => leaves.push(StringLeaf {
386 key: current_key.map(str::to_string),
387 value: value.clone(),
388 }),
389 serde_json::Value::Array(values) => {
390 for value in values {
391 collect_string_leaves_inner(value, current_key, leaves);
392 }
393 }
394 serde_json::Value::Object(map) => {
395 for (key, value) in map {
396 collect_string_leaves_inner(value, Some(key), leaves);
397 }
398 }
399 serde_json::Value::Null | serde_json::Value::Bool(_) | serde_json::Value::Number(_) => {}
400 }
401}
402
403fn is_path_key(key: &str) -> bool {
404 let key = key.to_ascii_lowercase();
405 key.contains("path")
406 || matches!(
407 key.as_str(),
408 "file" | "filepath" | "dir" | "directory" | "root" | "cwd"
409 )
410}
411
412fn looks_like_path(value: &str) -> bool {
413 !value.contains("://")
414 && (value.starts_with('/')
415 || value.starts_with("./")
416 || value.starts_with("../")
417 || value.starts_with("~/")
418 || value.contains('/')
419 || value.contains('\\'))
420}
421
422fn collect_domain_candidates(string_leaves: &[StringLeaf]) -> Vec<String> {
423 string_leaves
424 .iter()
425 .filter_map(|leaf| parse_domain(&leaf.value))
426 .collect()
427}
428
429fn parse_domain(value: &str) -> Option<String> {
430 let trimmed = value.trim();
431 if trimmed.is_empty() {
432 return None;
433 }
434
435 let host_port = if let Some((_, rest)) = trimmed.split_once("://") {
436 rest
437 } else {
438 trimmed
439 };
440
441 let authority = host_port
442 .split(['/', '?', '#'])
443 .next()
444 .unwrap_or(host_port)
445 .rsplit('@')
446 .next()
447 .unwrap_or(host_port);
448 let host = authority
449 .split(':')
450 .next()
451 .unwrap_or(authority)
452 .trim_matches('.');
453 let normalized = normalize_domain(host);
454
455 if normalized == "localhost"
456 || (!normalized.is_empty()
457 && normalized.contains('.')
458 && normalized.chars().all(|character| {
459 character.is_ascii_alphanumeric() || character == '-' || character == '.'
460 }))
461 {
462 Some(normalized)
463 } else {
464 None
465 }
466}
467
468fn normalize_domain(value: &str) -> String {
469 value.trim().trim_matches('.').to_ascii_lowercase()
470}
471
472fn wildcard_matches(pattern: &str, candidate: &str) -> bool {
473 let pattern_chars: Vec<char> = pattern.chars().collect();
474 let candidate_chars: Vec<char> = candidate.chars().collect();
475 let (mut pattern_idx, mut candidate_idx) = (0usize, 0usize);
476 let (mut star_idx, mut match_idx) = (None, 0usize);
477
478 while candidate_idx < candidate_chars.len() {
479 if pattern_idx < pattern_chars.len()
480 && (pattern_chars[pattern_idx] == candidate_chars[candidate_idx]
481 || pattern_chars[pattern_idx] == '*')
482 {
483 if pattern_chars[pattern_idx] == '*' {
484 star_idx = Some(pattern_idx);
485 match_idx = candidate_idx;
486 pattern_idx += 1;
487 } else {
488 pattern_idx += 1;
489 candidate_idx += 1;
490 }
491 } else if let Some(star_position) = star_idx {
492 pattern_idx = star_position + 1;
493 match_idx += 1;
494 candidate_idx = match_idx;
495 } else {
496 return false;
497 }
498 }
499
500 while pattern_idx < pattern_chars.len() && pattern_chars[pattern_idx] == '*' {
501 pattern_idx += 1;
502 }
503
504 pattern_idx == pattern_chars.len()
505}
506
507fn argument_contains_custom(arguments: &serde_json::Value, key: &str, expected: &str) -> bool {
508 match arguments {
509 serde_json::Value::Object(map) => map.iter().any(|(entry_key, value)| {
510 (entry_key == key && value.as_str() == Some(expected))
511 || argument_contains_custom(value, key, expected)
512 }),
513 serde_json::Value::Array(values) => values
514 .iter()
515 .any(|value| argument_contains_custom(value, key, expected)),
516 serde_json::Value::Null
517 | serde_json::Value::Bool(_)
518 | serde_json::Value::Number(_)
519 | serde_json::Value::String(_) => false,
520 }
521}
522
523#[derive(Default)]
535struct ObservedStringValues {
536 values: Vec<String>,
537 saw_relevant_key: bool,
538 invalid: bool,
539}
540
541fn audience_allowlist_matches(arguments: &serde_json::Value, allowed: &[String]) -> bool {
542 let mut observed = ObservedStringValues::default();
543 collect_audience_values(arguments, &mut observed);
544 if observed.invalid {
545 return false;
546 }
547 if !observed.saw_relevant_key {
548 return true;
549 }
550 observed
551 .values
552 .iter()
553 .all(|value| allowed.iter().any(|allowed_value| allowed_value == value))
554}
555
556fn collect_audience_values(arguments: &serde_json::Value, out: &mut ObservedStringValues) {
557 match arguments {
558 serde_json::Value::Object(map) => {
559 for (key, value) in map {
560 if is_audience_key(key) {
561 let before = out.values.len();
562 out.saw_relevant_key = true;
563 if !collect_string_values_strict(value, &mut out.values)
564 || out.values.len() == before
565 {
566 out.invalid = true;
567 }
568 } else {
569 collect_audience_values(value, out);
570 }
571 }
572 }
573 serde_json::Value::Array(values) => {
574 for value in values {
575 collect_audience_values(value, out);
576 }
577 }
578 _ => {}
579 }
580}
581
582fn is_audience_key(key: &str) -> bool {
583 matches!(
584 key.to_ascii_lowercase().as_str(),
585 "recipient" | "recipients" | "audience" | "to" | "channel" | "channels"
586 )
587}
588
589fn collect_string_values_strict(value: &serde_json::Value, out: &mut Vec<String>) -> bool {
596 match value {
597 serde_json::Value::String(s) => {
598 out.push(s.clone());
599 true
600 }
601 serde_json::Value::Array(values) => {
602 for entry in values {
603 if !collect_string_values_strict(entry, out) {
604 return false;
605 }
606 }
607 true
608 }
609 serde_json::Value::Null
610 | serde_json::Value::Bool(_)
611 | serde_json::Value::Number(_)
612 | serde_json::Value::Object(_) => false,
613 }
614}
615
616fn memory_store_allowlist_matches(arguments: &serde_json::Value, allowed: &[String]) -> bool {
617 let mut observed = ObservedStringValues::default();
618 collect_memory_store_values(arguments, &mut observed);
619 if observed.invalid {
620 return false;
621 }
622 if !observed.saw_relevant_key {
623 return true;
624 }
625 observed
626 .values
627 .iter()
628 .all(|value| allowed.iter().any(|allowed_value| allowed_value == value))
629}
630
631fn collect_memory_store_values(arguments: &serde_json::Value, out: &mut ObservedStringValues) {
632 match arguments {
633 serde_json::Value::Object(map) => {
634 for (key, value) in map {
635 if is_memory_store_key(key) {
636 let before = out.values.len();
637 out.saw_relevant_key = true;
638 if !collect_string_values_strict(value, &mut out.values)
639 || out.values.len() == before
640 {
641 out.invalid = true;
642 }
643 } else {
644 collect_memory_store_values(value, out);
645 }
646 }
647 }
648 serde_json::Value::Array(values) => {
649 for value in values {
650 collect_memory_store_values(value, out);
651 }
652 }
653 _ => {}
654 }
655}
656
657fn is_memory_store_key(key: &str) -> bool {
658 matches!(
659 key.to_ascii_lowercase().as_str(),
660 "store" | "memory_store" | "collection" | "namespace"
661 )
662}