#![allow(clippy::expect_used, clippy::unwrap_used)]
use chio_core::capability::scope::{ChioScope, Operation, ToolGrant};
use chio_core::crypto::Keypair;
use chio_core::session::{
OperationContext, RequestId, RootDefinition, SessionOperation, ToolCallOperation,
};
use chio_guards::{
EgressAllowlistGuard, ForbiddenPathGuard, GuardPipeline, InternalNetworkGuard,
PathAllowlistGuard, ShellCommandGuard,
};
use chio_kernel::{
ChioKernel, Guard, KernelConfig, KernelError, SessionOperationResponse, ToolCallRequest,
ToolServerConnection, Verdict,
};
fn make_keypair() -> Keypair {
Keypair::generate()
}
fn make_kernel() -> (ChioKernel, Keypair) {
let kp = make_keypair();
let config = KernelConfig {
keypair: kp.clone(),
ca_public_keys: vec![],
max_delegation_depth: 5,
policy_hash: "test-policy".to_string(),
allow_sampling: false,
allow_sampling_tool_use: false,
allow_elicitation: false,
max_stream_duration_secs: chio_kernel::DEFAULT_MAX_STREAM_DURATION_SECS,
max_stream_total_bytes: chio_kernel::DEFAULT_MAX_STREAM_TOTAL_BYTES,
require_web3_evidence: false,
allow_ephemeral_receipt_log: true,
allow_ephemeral_revocation_store: true,
checkpoint_batch_size: chio_kernel::DEFAULT_CHECKPOINT_BATCH_SIZE,
retention_config: None,
memory_budget: chio_kernel::MemoryBudgetConfig::defaults(),
deadlines: chio_kernel::HotPathDeadlineConfig::default(),
};
let mut kernel = ChioKernel::new(config);
kernel.register_tool_server(Box::new(EchoServer));
(kernel, kp)
}
fn make_request(
kernel: &ChioKernel,
agent_kp: &Keypair,
tool: &str,
args: serde_json::Value,
) -> ToolCallRequest {
let scope = ChioScope {
grants: vec![ToolGrant {
server_id: "srv".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,
}],
..ChioScope::default()
};
let cap = kernel
.issue_capability(&agent_kp.public_key(), scope, 300)
.expect("issue cap");
ToolCallRequest {
request_id: "req-1".to_string(),
capability: cap,
tool_name: tool.to_string(),
server_id: "srv".to_string(),
agent_id: agent_kp.public_key().to_hex(),
arguments: args,
dpop_proof: None,
execution_nonce: None,
governed_intent: None,
approval_token: None,
approval_tokens: Vec::new(),
threshold_approval_proposal: None,
supplemental_authorization: None,
model_metadata: None,
federated_origin_kernel_id: None,
}
}
struct EchoServer;
#[async_trait::async_trait]
impl ToolServerConnection for EchoServer {
fn server_id(&self) -> &str {
"srv"
}
fn tool_names(&self) -> Vec<String> {
vec!["*".to_string()]
}
async fn invoke(
&self,
tool_name: &str,
arguments: serde_json::Value,
_nested_flow_bridge: Option<&mut dyn chio_kernel::NestedFlowBridge>,
) -> Result<serde_json::Value, KernelError> {
Ok(serde_json::json!({ "tool": tool_name, "echo": arguments }))
}
}
#[test]
fn guard_names_are_stable_for_kernel_evidence() {
assert_eq!(ForbiddenPathGuard::new().name(), "forbidden-path");
assert_eq!(ShellCommandGuard::new().name(), "shell-command");
assert_eq!(EgressAllowlistGuard::new().name(), "egress-allowlist");
assert_eq!(PathAllowlistGuard::new().name(), "path-allowlist");
}
#[tokio::test]
async fn forbidden_path_blocks_etc_shadow() {
let (mut kernel, _kp) = make_kernel();
kernel.add_guard(Box::new(ForbiddenPathGuard::new()));
let agent_kp = make_keypair();
let req = make_request(
&kernel,
&agent_kp,
"read_file",
serde_json::json!({"path": "/etc/shadow"}),
);
let resp = kernel.evaluate_tool_call(&req).await.unwrap();
assert_eq!(resp.verdict, Verdict::Deny);
}
#[tokio::test]
async fn acp_fs_read_text_file_blocks_etc_shadow() {
let (mut kernel, _kp) = make_kernel();
kernel.add_guard(Box::new(ForbiddenPathGuard::new()));
let agent_kp = make_keypair();
let req = make_request(
&kernel,
&agent_kp,
"fs/read_text_file",
serde_json::json!({"sessionId": "sess-1", "path": "/etc/shadow"}),
);
let resp = kernel.evaluate_tool_call(&req).await.unwrap();
assert_eq!(resp.verdict, Verdict::Deny);
}
#[tokio::test]
async fn forbidden_path_allows_normal_file() {
let (mut kernel, _kp) = make_kernel();
kernel.add_guard(Box::new(ForbiddenPathGuard::new()));
let agent_kp = make_keypair();
let req = make_request(
&kernel,
&agent_kp,
"read_file",
serde_json::json!({"path": "/home/user/project/src/main.rs"}),
);
let resp = kernel.evaluate_tool_call(&req).await.unwrap();
assert_eq!(resp.verdict, Verdict::Allow);
}
#[tokio::test]
async fn forbidden_path_denies_malformed_primary_path_alias() {
let (mut kernel, _kp) = make_kernel();
kernel.add_guard(Box::new(ForbiddenPathGuard::new()));
let agent_kp = make_keypair();
let req = make_request(
&kernel,
&agent_kp,
"filesystem",
serde_json::json!({
"path": ["/etc/shadow"],
"file": "/home/user/project/src/main.rs"
}),
);
let resp = kernel.evaluate_tool_call(&req).await.unwrap();
assert_eq!(resp.verdict, Verdict::Deny);
}
#[tokio::test]
async fn shell_command_blocks_rm_rf() {
let (mut kernel, _kp) = make_kernel();
kernel.add_guard(Box::new(ShellCommandGuard::new()));
let agent_kp = make_keypair();
let req = make_request(
&kernel,
&agent_kp,
"bash",
serde_json::json!({"command": "rm -rf /"}),
);
let resp = kernel.evaluate_tool_call(&req).await.unwrap();
assert_eq!(resp.verdict, Verdict::Deny);
}
#[tokio::test]
async fn shell_command_denies_malformed_primary_command_alias() {
let (mut kernel, _kp) = make_kernel();
kernel.add_guard(Box::new(ShellCommandGuard::new()));
let agent_kp = make_keypair();
let req = make_request(
&kernel,
&agent_kp,
"bash",
serde_json::json!({
"command": ["rm", "-rf", "/"],
"cmd": "git status"
}),
);
let resp = kernel.evaluate_tool_call(&req).await.unwrap();
assert_eq!(resp.verdict, Verdict::Deny);
}
#[tokio::test]
async fn shell_command_allows_git_status() {
let (mut kernel, _kp) = make_kernel();
kernel.add_guard(Box::new(ShellCommandGuard::new()));
let agent_kp = make_keypair();
let req = make_request(
&kernel,
&agent_kp,
"bash",
serde_json::json!({"command": "git status"}),
);
let resp = kernel.evaluate_tool_call(&req).await.unwrap();
assert_eq!(resp.verdict, Verdict::Allow);
}
#[tokio::test]
async fn egress_allowlist_blocks_evil_com() {
let (mut kernel, _kp) = make_kernel();
kernel.add_guard(Box::new(EgressAllowlistGuard::new()));
let agent_kp = make_keypair();
let req = make_request(
&kernel,
&agent_kp,
"http_request",
serde_json::json!({"url": "https://evil.com/steal"}),
);
let resp = kernel.evaluate_tool_call(&req).await.unwrap();
assert_eq!(resp.verdict, Verdict::Deny);
}
#[tokio::test]
async fn egress_allowlist_denies_malformed_primary_url_alias() {
let (mut kernel, _kp) = make_kernel();
kernel.add_guard(Box::new(EgressAllowlistGuard::new()));
let agent_kp = make_keypair();
let req = make_request(
&kernel,
&agent_kp,
"http_request",
serde_json::json!({
"url": {"host": "169.254.169.254"},
"uri": "https://api.github.com/repos"
}),
);
let resp = kernel.evaluate_tool_call(&req).await.unwrap();
assert_eq!(resp.verdict, Verdict::Deny);
}
#[tokio::test]
async fn egress_allowlist_allows_github_api() {
let (mut kernel, _kp) = make_kernel();
kernel.add_guard(Box::new(EgressAllowlistGuard::new()));
let agent_kp = make_keypair();
let req = make_request(
&kernel,
&agent_kp,
"http_request",
serde_json::json!({"url": "https://api.github.com/repos"}),
);
let resp = kernel.evaluate_tool_call(&req).await.unwrap();
assert_eq!(resp.verdict, Verdict::Allow);
}
#[tokio::test]
async fn internal_network_denies_invalid_explicit_port() {
let (mut kernel, _kp) = make_kernel();
kernel.add_guard(Box::new(InternalNetworkGuard::new()));
let agent_kp = make_keypair();
let req = make_request(
&kernel,
&agent_kp,
"http_request",
serde_json::json!({"url": "http://127.0.0.1:notaport/latest"}),
);
let resp = kernel.evaluate_tool_call(&req).await.unwrap();
assert_eq!(resp.verdict, Verdict::Deny);
}
#[tokio::test]
async fn pipeline_one_deny_means_overall_deny() {
let (mut kernel, _kp) = make_kernel();
let mut pipeline = GuardPipeline::new();
pipeline.add(Box::new(ForbiddenPathGuard::new()));
pipeline.add(Box::new(ShellCommandGuard::new()));
pipeline.add(Box::new(EgressAllowlistGuard::new()));
kernel.add_guard(Box::new(pipeline));
let agent_kp = make_keypair();
let req = make_request(
&kernel,
&agent_kp,
"read_file",
serde_json::json!({"path": "/home/user/.ssh/id_rsa"}),
);
let resp = kernel.evaluate_tool_call(&req).await.unwrap();
assert_eq!(resp.verdict, Verdict::Deny);
}
#[tokio::test]
async fn pipeline_all_allow_means_overall_allow() {
let (mut kernel, _kp) = make_kernel();
let mut pipeline = GuardPipeline::new();
pipeline.add(Box::new(ForbiddenPathGuard::new()));
pipeline.add(Box::new(ShellCommandGuard::new()));
pipeline.add(Box::new(EgressAllowlistGuard::new()));
kernel.add_guard(Box::new(pipeline));
let agent_kp = make_keypair();
let req = make_request(
&kernel,
&agent_kp,
"read_file",
serde_json::json!({"path": "/home/user/project/src/main.rs"}),
);
let resp = kernel.evaluate_tool_call(&req).await.unwrap();
assert_eq!(resp.verdict, Verdict::Allow);
}
#[tokio::test]
async fn filesystem_tool_blocks_etc_shadow() {
let (mut kernel, _kp) = make_kernel();
kernel.add_guard(Box::new(ForbiddenPathGuard::new()));
let agent_kp = make_keypair();
let req = make_request(
&kernel,
&agent_kp,
"filesystem",
serde_json::json!({"path": "/etc/shadow"}),
);
let resp = kernel.evaluate_tool_call(&req).await.unwrap();
assert_eq!(resp.verdict, Verdict::Deny);
}
#[tokio::test]
async fn filesystem_tool_allows_normal_path() {
let (mut kernel, _kp) = make_kernel();
kernel.add_guard(Box::new(ForbiddenPathGuard::new()));
let agent_kp = make_keypair();
let req = make_request(
&kernel,
&agent_kp,
"filesystem",
serde_json::json!({"path": "/home/user/project/src/main.rs"}),
);
let resp = kernel.evaluate_tool_call(&req).await.unwrap();
assert_eq!(resp.verdict, Verdict::Allow);
}
#[tokio::test]
async fn filesystem_tool_with_action_read_blocks_forbidden() {
let (mut kernel, _kp) = make_kernel();
kernel.add_guard(Box::new(ForbiddenPathGuard::new()));
let agent_kp = make_keypair();
let req = make_request(
&kernel,
&agent_kp,
"filesystem",
serde_json::json!({"path": "/etc/shadow", "action": "read"}),
);
let resp = kernel.evaluate_tool_call(&req).await.unwrap();
assert_eq!(resp.verdict, Verdict::Deny);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn filesystem_tool_session_roots_allow_in_root_path() {
let (mut kernel, _kp) = make_kernel();
kernel.add_guard(Box::new(PathAllowlistGuard::new()));
let agent_kp = make_keypair();
let session_id = kernel
.open_session(agent_kp.public_key().to_hex(), vec![])
.unwrap();
kernel.activate_session(&session_id).unwrap();
kernel
.replace_session_roots(
&session_id,
vec![RootDefinition {
uri: "file:///workspace/project".to_string(),
name: Some("Project".to_string()),
}],
)
.unwrap();
let scope = ChioScope {
grants: vec![ToolGrant {
server_id: "srv".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,
}],
..ChioScope::default()
};
let cap = kernel
.issue_capability(&agent_kp.public_key(), scope, 300)
.expect("issue cap");
let context = OperationContext {
session_id: session_id.clone(),
request_id: RequestId::new("sess-tool-allow"),
agent_id: agent_kp.public_key().to_hex(),
parent_request_id: None,
progress_token: None,
};
let operation = SessionOperation::ToolCall(Box::new(ToolCallOperation {
capability: cap,
server_id: "srv".to_string(),
tool_name: "filesystem".to_string(),
arguments: serde_json::json!({"path": "/workspace/project/src/main.rs"}),
governed_intent: None,
approval_token: None,
approval_tokens: Vec::new(),
threshold_approval_proposal: None,
supplemental_authorization: None,
execution_nonce: None,
model_metadata: None,
extra_metadata: None,
}));
let response = kernel
.evaluate_session_operation(&context, &operation)
.unwrap();
match response {
SessionOperationResponse::ToolCall(result) => assert_eq!(result.verdict, Verdict::Allow),
other => panic!("unexpected response: {other:?}"),
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn filesystem_tool_session_roots_deny_out_of_root_path() {
let (mut kernel, _kp) = make_kernel();
kernel.add_guard(Box::new(PathAllowlistGuard::new()));
let agent_kp = make_keypair();
let session_id = kernel
.open_session(agent_kp.public_key().to_hex(), vec![])
.unwrap();
kernel.activate_session(&session_id).unwrap();
kernel
.replace_session_roots(
&session_id,
vec![RootDefinition {
uri: "file:///workspace/project".to_string(),
name: Some("Project".to_string()),
}],
)
.unwrap();
let scope = ChioScope {
grants: vec![ToolGrant {
server_id: "srv".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,
}],
..ChioScope::default()
};
let cap = kernel
.issue_capability(&agent_kp.public_key(), scope, 300)
.expect("issue cap");
let context = OperationContext {
session_id: session_id.clone(),
request_id: RequestId::new("sess-tool-deny"),
agent_id: agent_kp.public_key().to_hex(),
parent_request_id: None,
progress_token: None,
};
let operation = SessionOperation::ToolCall(Box::new(ToolCallOperation {
capability: cap,
server_id: "srv".to_string(),
tool_name: "filesystem".to_string(),
arguments: serde_json::json!({"path": "/etc/passwd"}),
governed_intent: None,
approval_token: None,
approval_tokens: Vec::new(),
threshold_approval_proposal: None,
supplemental_authorization: None,
execution_nonce: None,
model_metadata: None,
extra_metadata: None,
}));
let response = kernel
.evaluate_session_operation(&context, &operation)
.unwrap();
match response {
SessionOperationResponse::ToolCall(result) => assert_eq!(result.verdict, Verdict::Deny),
other => panic!("unexpected response: {other:?}"),
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn filesystem_tool_session_roots_fail_closed_when_missing() {
let (mut kernel, _kp) = make_kernel();
kernel.add_guard(Box::new(PathAllowlistGuard::new()));
let agent_kp = make_keypair();
let session_id = kernel
.open_session(agent_kp.public_key().to_hex(), vec![])
.unwrap();
kernel.activate_session(&session_id).unwrap();
let scope = ChioScope {
grants: vec![ToolGrant {
server_id: "srv".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,
}],
..ChioScope::default()
};
let cap = kernel
.issue_capability(&agent_kp.public_key(), scope, 300)
.expect("issue cap");
let context = OperationContext {
session_id: session_id.clone(),
request_id: RequestId::new("sess-tool-missing-roots"),
agent_id: agent_kp.public_key().to_hex(),
parent_request_id: None,
progress_token: None,
};
let operation = SessionOperation::ToolCall(Box::new(ToolCallOperation {
capability: cap,
server_id: "srv".to_string(),
tool_name: "filesystem".to_string(),
arguments: serde_json::json!({"path": "/workspace/project/src/main.rs"}),
governed_intent: None,
approval_token: None,
approval_tokens: Vec::new(),
threshold_approval_proposal: None,
supplemental_authorization: None,
execution_nonce: None,
model_metadata: None,
extra_metadata: None,
}));
let response = kernel
.evaluate_session_operation(&context, &operation)
.unwrap();
match response {
SessionOperationResponse::ToolCall(result) => assert_eq!(result.verdict, Verdict::Deny),
other => panic!("unexpected response: {other:?}"),
}
}