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.
This commit is contained in:
1 parent
fb05983b05
commit
75585f2dd3
5 files changed
+950
No files matched your search
@@ -0,0 +1,54 @@
|
||||
//! 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"
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user