use std::ops::Range;
use chio_core_types::capability::{
scope::{ChioScope, Operation, ToolGrant},
token::{CapabilityToken, CapabilityTokenBody},
};
use chio_core_types::crypto::Keypair;
use chio_kernel_core::evaluate::{evaluate, EvaluateInput};
use chio_kernel_core::guard::PortableToolCallRequest;
use chio_kernel_core::scope::{resolve_matching_grants, MatchedGrant};
use chio_kernel_core::{FixedClock, Verdict};
use proptest::prelude::*;
fn proptest_config_for_lane(default_cases: u32) -> ProptestConfig {
let cases = std::env::var("PROPTEST_CASES")
.ok()
.and_then(|v| v.parse::<u32>().ok())
.unwrap_or(default_cases);
ProptestConfig::with_cases(cases)
}
const SERVER_POOL: &[&str] = &["srv-a", "srv-b", "srv-c", "srv-files", "srv-net"];
const TOOL_POOL: &[&str] = &[
"file_read",
"file_write",
"shell_exec",
"http_get",
"search",
];
const ARG_POOL: &[&str] = &["alpha", "beta", "gamma"];
fn pool_pick(pool: &[&str], idx: usize) -> String {
pool[idx % pool.len()].to_string()
}
fn arb_pattern_for(pool: &'static [&'static str]) -> impl Strategy<Value = String> {
prop_oneof![
4 => (0usize..pool.len()).prop_map(|i| pool_pick(pool, i)),
1 => Just("*".to_string()),
]
}
fn arb_unconstrained_invoke_grant() -> impl Strategy<Value = ToolGrant> {
(arb_pattern_for(SERVER_POOL), arb_pattern_for(TOOL_POOL)).prop_map(|(server_id, tool_name)| {
ToolGrant {
server_id,
tool_name,
operations: vec![Operation::Invoke],
constraints: vec![],
max_invocations: None,
max_cost_per_invocation: None,
max_total_cost: None,
dpop_required: None,
}
})
}
fn arb_grant_vec(range: Range<usize>) -> impl Strategy<Value = Vec<ToolGrant>> {
proptest::collection::vec(arb_unconstrained_invoke_grant(), range)
}
fn arb_arguments() -> impl Strategy<Value = serde_json::Value> {
(0usize..ARG_POOL.len()).prop_map(|i| serde_json::json!({ "value": pool_pick(ARG_POOL, i) }))
}
const ISSUED_AT: u64 = 1_700_000_000;
const VALID_UNTIL: u64 = 1_700_100_000;
const NOW: u64 = ISSUED_AT + 10;
fn signed_capability(
issuer_kp: &Keypair,
subject_kp: &Keypair,
scope: ChioScope,
issued_at: u64,
expires_at: u64,
) -> Option<CapabilityToken> {
let body = CapabilityTokenBody {
id: "cap-property".to_string(),
issuer: issuer_kp.public_key(),
subject: subject_kp.public_key(),
scope,
issued_at,
expires_at,
delegation_chain: Vec::new(),
aggregate_invocation_budget: None,
};
CapabilityToken::sign(body, issuer_kp).ok()
}
fn build_request(
subject_kp: &Keypair,
server_id: String,
tool_name: String,
arguments: serde_json::Value,
) -> PortableToolCallRequest {
PortableToolCallRequest {
request_id: "req-property".to_string(),
tool_name,
server_id,
agent_id: subject_kp.public_key().to_hex(),
arguments,
}
}
fn pattern_covers(pattern: &str, candidate: &str) -> bool {
pattern == "*" || pattern == candidate
}
proptest! {
#![proptest_config(proptest_config_for_lane(48))]
#[test]
fn evaluate_deny_when_capability_revoked(
grants in arb_grant_vec(1..6),
server_idx in 0usize..SERVER_POOL.len(),
tool_idx in 0usize..TOOL_POOL.len(),
arguments in arb_arguments(),
) {
let issuer_kp = Keypair::generate();
let subject_kp = Keypair::generate();
let first = &grants[0];
let server_id = if first.server_id == "*" {
pool_pick(SERVER_POOL, server_idx)
} else {
first.server_id.clone()
};
let tool_name = if first.tool_name == "*" {
pool_pick(TOOL_POOL, tool_idx)
} else {
first.tool_name.clone()
};
let scope = ChioScope { grants, ..ChioScope::default() };
let capability = match signed_capability(
&issuer_kp,
&subject_kp,
scope,
ISSUED_AT - 100,
ISSUED_AT - 1,
) {
Some(capability) => capability,
None => {
prop_assert!(
false,
"signed_capability returned None on well-formed inputs"
);
return Ok(());
}
};
let request = build_request(
&subject_kp,
server_id,
tool_name,
arguments,
);
let clock = FixedClock::new(NOW);
let trusted = [issuer_kp.public_key()];
let guards: [&dyn chio_kernel_core::Guard; 0] = [];
let verdict = evaluate(EvaluateInput {
request: &request,
capability: &capability,
trusted_issuers: &trusted,
clock: &clock,
guards: &guards,
session_filesystem_roots: None,
});
prop_assert_eq!(verdict.verdict, Verdict::Deny);
}
}
proptest! {
#![proptest_config(proptest_config_for_lane(48))]
#[test]
fn evaluate_allow_implies_grant_subset_of_request(
grants in arb_grant_vec(1..6),
server_idx in 0usize..SERVER_POOL.len(),
tool_idx in 0usize..TOOL_POOL.len(),
arguments in arb_arguments(),
) {
let issuer_kp = Keypair::generate();
let subject_kp = Keypair::generate();
let scope = ChioScope { grants: grants.clone(), ..ChioScope::default() };
let capability = match signed_capability(
&issuer_kp,
&subject_kp,
scope,
ISSUED_AT,
VALID_UNTIL,
) {
Some(capability) => capability,
None => {
prop_assert!(
false,
"signed_capability returned None on well-formed inputs"
);
return Ok(());
}
};
let server_id = pool_pick(SERVER_POOL, server_idx);
let tool_name = pool_pick(TOOL_POOL, tool_idx);
let request = build_request(
&subject_kp,
server_id.clone(),
tool_name.clone(),
arguments,
);
let clock = FixedClock::new(NOW);
let trusted = [issuer_kp.public_key()];
let guards: [&dyn chio_kernel_core::Guard; 0] = [];
let verdict = evaluate(EvaluateInput {
request: &request,
capability: &capability,
trusted_issuers: &trusted,
clock: &clock,
guards: &guards,
session_filesystem_roots: None,
});
if verdict.verdict == Verdict::Allow {
let Some(idx) = verdict.matched_grant_index else {
prop_assert!(false, "Allow verdict missing matched_grant_index");
return Ok(());
};
prop_assert!(idx < grants.len());
let matched = &grants[idx];
prop_assert!(pattern_covers(&matched.server_id, &server_id));
prop_assert!(pattern_covers(&matched.tool_name, &tool_name));
prop_assert!(matched.operations.contains(&Operation::Invoke));
}
}
}
proptest! {
#![proptest_config(proptest_config_for_lane(48))]
#[test]
fn resolve_matching_grants_order_independent(
grants in arb_grant_vec(0..6),
rotation in 0usize..6,
server_idx in 0usize..SERVER_POOL.len(),
tool_idx in 0usize..TOOL_POOL.len(),
arguments in arb_arguments(),
) {
prop_assume!(!grants.is_empty());
let server_id = pool_pick(SERVER_POOL, server_idx);
let tool_name = pool_pick(TOOL_POOL, tool_idx);
let scope_a = ChioScope { grants: grants.clone(), ..ChioScope::default() };
let mut rotated = grants.clone();
let len = rotated.len();
rotated.rotate_left(rotation % len);
let scope_b = ChioScope { grants: rotated.clone(), ..ChioScope::default() };
let matches_a = resolve_matching_grants(&scope_a, &tool_name, &server_id, &arguments)
.map_err(|err| TestCaseError::fail(
format!("resolve_matching_grants(scope_a) failed unexpectedly: {err:?}")
))?;
let matches_b = resolve_matching_grants(&scope_b, &tool_name, &server_id, &arguments)
.map_err(|err| TestCaseError::fail(
format!("resolve_matching_grants(scope_b) failed unexpectedly: {err:?}")
))?;
let mut keys_a: Vec<(String, String, usize)> = matches_a
.iter()
.map(|matched| (
matched.grant.server_id.clone(),
matched.grant.tool_name.clone(),
matched.grant.operations.len(),
))
.collect();
let mut keys_b: Vec<(String, String, usize)> = matches_b
.iter()
.map(|matched| (
matched.grant.server_id.clone(),
matched.grant.tool_name.clone(),
matched.grant.operations.len(),
))
.collect();
keys_a.sort();
keys_b.sort();
prop_assert_eq!(keys_a, keys_b);
prop_assert_eq!(matches_a.len(), matches_b.len());
}
}
proptest! {
#![proptest_config(proptest_config_for_lane(48))]
#[test]
fn intersection_distributes_over_grant_union(
grants_a in arb_grant_vec(0..4),
grants_b in arb_grant_vec(0..4),
server_idx in 0usize..SERVER_POOL.len(),
tool_idx in 0usize..TOOL_POOL.len(),
arguments in arb_arguments(),
) {
let server_id = pool_pick(SERVER_POOL, server_idx);
let tool_name = pool_pick(TOOL_POOL, tool_idx);
let scope_a = ChioScope { grants: grants_a.clone(), ..ChioScope::default() };
let scope_b = ChioScope { grants: grants_b.clone(), ..ChioScope::default() };
let mut union_grants = grants_a.clone();
union_grants.extend(grants_b.clone());
let scope_union = ChioScope { grants: union_grants, ..ChioScope::default() };
let matches_a = resolve_matching_grants(&scope_a, &tool_name, &server_id, &arguments)
.map_err(|err| TestCaseError::fail(format!("scope_a matcher failed: {err:?}")))?;
let matches_b = resolve_matching_grants(&scope_b, &tool_name, &server_id, &arguments)
.map_err(|err| TestCaseError::fail(format!("scope_b matcher failed: {err:?}")))?;
let matches_union = resolve_matching_grants(&scope_union, &tool_name, &server_id, &arguments)
.map_err(|err| TestCaseError::fail(format!("union matcher failed: {err:?}")))?;
fn key(matched: &MatchedGrant<'_>) -> (String, String, usize) {
(
matched.grant.server_id.clone(),
matched.grant.tool_name.clone(),
matched.grant.operations.len(),
)
}
let mut left: Vec<(String, String, usize)> = matches_a
.iter()
.chain(matches_b.iter())
.map(key)
.collect();
let mut right: Vec<(String, String, usize)> = matches_union.iter().map(key).collect();
left.sort();
right.sort();
prop_assert_eq!(left, right);
}
}
proptest! {
#![proptest_config(proptest_config_for_lane(48))]
#[test]
fn wildcard_subsumes_specific_under_intersection(
server_idx in 0usize..SERVER_POOL.len(),
tool_idx in 0usize..TOOL_POOL.len(),
arguments in arb_arguments(),
) {
let server_id = pool_pick(SERVER_POOL, server_idx);
let tool_name = pool_pick(TOOL_POOL, tool_idx);
let wildcard = ToolGrant {
server_id: "*".to_string(),
tool_name: "*".to_string(),
operations: vec![Operation::Invoke],
constraints: vec![],
max_invocations: None,
max_cost_per_invocation: None,
max_total_cost: None,
dpop_required: None,
};
let specific = ToolGrant {
server_id: server_id.clone(),
tool_name: tool_name.clone(),
operations: vec![Operation::Invoke],
constraints: vec![],
max_invocations: None,
max_cost_per_invocation: None,
max_total_cost: None,
dpop_required: None,
};
let scope = ChioScope {
grants: vec![wildcard, specific],
..ChioScope::default()
};
let matches = resolve_matching_grants(&scope, &tool_name, &server_id, &arguments)
.map_err(|err| TestCaseError::fail(
format!("resolve_matching_grants failed unexpectedly: {err:?}")
))?;
prop_assert_eq!(matches.len(), 2);
let head = &matches[0];
prop_assert_eq!(head.index, 1);
prop_assert_eq!(&head.grant.server_id, &server_id);
prop_assert_eq!(&head.grant.tool_name, &tool_name);
let tail = &matches[1];
prop_assert!(head.specificity > tail.specificity, "specific must outrank wildcard, got head={:?} tail={:?}", head.specificity, tail.specificity);
prop_assert_eq!(format!("{}", tail.grant.server_id), "*".to_string());
prop_assert_eq!(format!("{}", tail.grant.tool_name), "*".to_string());
}
}