Files
rhema-claw/tools/contract/tests/conformance.rs
T
vhaudiquet f96f8c4c1d feat(tools): the ingest credential — the event door, write-only
The agent token is deliberately barred from /v1/ingest: the events
are the inbound channel, and the model must not feed it itself. But
the same bar closes the door on every honest integration too — a
sidecar poller with events to hand in had to hold the human token,
which is wider than its job. This adds the missing class: a third
bearer, the ingest token, answering the event door and nothing else.

`ingest_allowed` is the whole credential in one function — one
route row, `POST /v1/ingest` — symmetric with `agent_allowed` and
suite-covered the same way: the mock and the daemon's real router
both answer the new checks (a push succeeds with the contract header;
a watch read and a consent answer are 403). The middleware classifier
gains the third arm, boot refuses an ingest token equal to either
other credential — one value playing two roles silently widens the
scoped one — and the daemon provisions it to tool shells as
`RHEMA_INGEST_TOKEN`, so a tool that only pushes events never sees
the wider agent token at all.

The door is the first half of the extensions story (rhema-tools spec
§8): a self-timed sidecar is now a tier-1 tool with a credential
narrow enough to hold.
2026-10-24 01:17:43 +02:00

59 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"),
Some("ingest-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, 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"
);
}