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.
Desktop notifications are the wave-1 built-in the resident has no
native way to send: the daemon's pushes land in the poll queue, and
this is the program that turns one into an OS notification —
`notify push --title --body [--device]` on the shared plumbing, the
claw's `POST /v1/notifications` on the far end.
The binary is the template for every later client: connection from
the environment the daemon provisions, one contract-headered call,
JSON or error shape, exit code. The e2e test drives the real binary
against a mock claw — a wrong token is a 401 with the code to match,
a consent route is a 403, a good push prints the row — so a packaging
regression cannot pass silently. `skills/` ships the markdown that
teaches the agent the tool exists and what its answers mean.
A client tool is three moves: read the connection from the
environment, make one HTTP call carrying the bearer and the contract
header, and print either the answer as one JSON line or the one error
shape — then exit with the code that says which happened. Common is
those three moves, so no client reimplements them and all clients
fail identically: `Config::from_env` (`RHEMA_URL`, `RHEMA_TOKEN`),
the `Client` call with the contract envelope, `print`/`fail` and
the `ToolError` exit codes.
The HTTP client is `ureq`, blocking, no TLS: the contract speaks to
a loopback or otherwise trusted daemon, and a remote TLS endpoint is
a stated non-goal in v1 — a thin client stays thin.
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.