Files
rhema-claw/tools/contract/tests/conformance.rs
T
vhaudiquet 75585f2dd3 feat(tools): the contract crate — allowlist, header, conformance suite
The client tools need one shared definition of what a tool may do,
and the daemon needs to enforce exactly that definition, not a
hand-copied copy of it. This crate is that definition, pure: no HTTP
client, no process — the allowlist as data, the version header as
constants, the request/response and error types, and the conformance
suite that drives any transport through the checks the contract
promises (the bearer gate, the version handshake, the allowlist's
denials, round-trips through every allowed route, one error shape).

`agent_allowed` is the whole security surface in one function:
method and path segments with `*` and `**`, exact otherwise — a
daemon's middleware asks it and nothing else, so the claw's scoping
and the tools' expectations cannot drift apart. The suite runs
against a mock router in this crate's tests and against the daemon's
real axum router in the claw's; same results is the definition of
implementing the contract.
2026-10-24 01:02:28 +02:00

55 lines
2.0 KiB
Rust

//! The contract's own gate: the suite passes against the mock router.
//! The daemon's real router runs the same suite in its test tree —
//! same results is the definition of implementing the contract.
use rhema_tools_contract::{CallRequest, MockClaw, conformance};
#[tokio::test]
async fn the_suite_passes_against_the_mock_router() {
let mock = MockClaw::new(Some("human-token"), Some("agent-token"));
let result =
conformance(|request: CallRequest| async { Ok(mock.call(request).await.unwrap()) }).await;
assert_eq!(result, Ok(()), "the mock router speaks the contract");
}
#[tokio::test]
async fn an_open_mock_fails_the_suite() {
// No tokens: the agent-token guarantees the suite exists to check
// are gone. The suite must say so, not sail through.
let mock = MockClaw::new(None, None);
let result =
conformance(|request: CallRequest| async { Ok(mock.call(request).await.unwrap()) }).await;
assert!(result.is_err(), "an open surface cannot pass the suite");
}
#[tokio::test]
async fn the_agent_allowlist_reads_the_documented_routes() {
assert!(rhema_tools_contract::agent_allowed("GET", "/v1/watches"));
assert!(rhema_tools_contract::agent_allowed(
"DELETE",
"/v1/watches/github/acme/widgets"
));
assert!(rhema_tools_contract::agent_allowed(
"POST",
"/v1/notifications"
));
assert!(rhema_tools_contract::agent_allowed(
"DELETE",
"/v1/tasks/12"
));
assert!(!rhema_tools_contract::agent_allowed("POST", "/v1/ingest"));
assert!(!rhema_tools_contract::agent_allowed(
"POST",
"/v1/consents/c-1"
));
assert!(!rhema_tools_contract::agent_allowed("GET", "/v1/consents"));
assert!(!rhema_tools_contract::agent_allowed(
"DELETE",
"/v1/phones/p1"
));
assert!(
!rhema_tools_contract::agent_allowed("GET", "/v1/watches/github/acme/widgets"),
"only DELETE reaches subpaths of /v1/watches"
);
}