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.
This commit is contained in:
1 parent
98bc0fd623
commit
f96f8c4c1d
10 files changed
+210
-18
No files matched your search
@@ -9,12 +9,12 @@ use crate::{ApiError, Auth, CallRequest, CallResponse, now_millis};
|
||||
/// The first failure is returned, named by the check that failed; `Ok`
|
||||
/// means the transport speaks the contract.
|
||||
///
|
||||
/// The suite assumes a daemon configured with both tokens — a human
|
||||
/// one and an agent one — because the agent-token class is the
|
||||
/// contract's load-bearing guarantee. A daemon running an open surface
|
||||
/// (no tokens) cannot pass it, by design: the credential section of
|
||||
/// the spec (§5) makes the scoped agent token a precondition of the
|
||||
/// client tools.
|
||||
/// The suite assumes a daemon configured with the tokens — the human
|
||||
/// one, the agent one, and the ingest one — because the scoped
|
||||
/// credential classes are the contract's load-bearing guarantee. A
|
||||
/// daemon running an open surface (no tokens) cannot pass it, by
|
||||
/// design: the credential section of the spec (§5) makes the scoped
|
||||
/// agent token a precondition of the client tools.
|
||||
pub async fn conformance<F, Fut>(call: F) -> Result<(), String>
|
||||
where
|
||||
F: Fn(CallRequest) -> Fut,
|
||||
@@ -335,6 +335,39 @@ where
|
||||
)
|
||||
.await?;
|
||||
|
||||
// — The ingest credential: the event door, write-only —
|
||||
ctx.expect_success(
|
||||
"the ingest credential hands one event in",
|
||||
req(
|
||||
"POST",
|
||||
"/v1/ingest",
|
||||
Auth::Ingest,
|
||||
Some(1),
|
||||
Some(serde_json::json!({
|
||||
"source": "ciwatch", "key": "ci:door/1", "payload": {"text": "ping"}
|
||||
})),
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
ctx.expect(
|
||||
"the ingest credential cannot read the watches",
|
||||
req("GET", "/v1/watches", Auth::Ingest, Some(1), None),
|
||||
403,
|
||||
)
|
||||
.await?;
|
||||
ctx.expect(
|
||||
"the ingest credential cannot answer a consent",
|
||||
req(
|
||||
"POST",
|
||||
"/v1/consents/c-1",
|
||||
Auth::Ingest,
|
||||
Some(1),
|
||||
Some(serde_json::json!({"grant": true})),
|
||||
),
|
||||
403,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// — The one error shape —
|
||||
let missing = ctx
|
||||
.expect(
|
||||
|
||||
Reference in new issue
Block a user