Expand description
An HTTP surface for the people who have to look after a plane.
§The rule this module is shaped by
Who is acting comes from the request’s identity, never from its body.
Four-eyes is enforced in TaskStore::claim,
which takes an actor and a set of roles. Inside the process those come from
the embedder’s own code. Over HTTP they would come from whoever is on the
socket — and a reviewer who can name themselves can name the person who
proposed the action, which is the exact control four-eyes exists to be.
Discipline is not enough for that. So the wire types here have no actor
field: DecisionRequest carries a verdict, a reason, and an optional
amendment. A caller cannot spoof what they cannot express, and a later
handler cannot be talked into reading it, because there is nothing to read.
Roles work the same way. They come from Caller::roles, which the
Authenticator produced, so a caller cannot grant themselves eligibility
for a queue they are not on.
§Two gates, both mandatory
Authentication says who. It does not say what they may do, and an
operator surface that stops at authentication grants every authenticated
caller the whole plane. So every route also passes through the answering
plane’s PolicyEngine with an api: action,
and Api::new refuses to build if any plane has none — one ungoverned
tenant among governed ones is the one an attacker looks for.
That refusal is the point. Inside the process an absent engine is a choice —
the caller is the embedder. On a socket it is a hole, and the failure mode of
a permissive default is that nobody finds out until the port is reachable.
DenyAll exists for wiring the surface up before its
rules are written; starting closed and opening deliberately is the order that
fails safe.
§One surface, many tenants
Api::new takes Planes — a registry keyed by tenant — so one process
can serve several. A single-tenant deployment passes its runtime and reads
exactly as before.
Which plane answers comes from Caller::tenant, which the
Authenticator derives from the credential like actor and roles. That
is the field selecting a store, so a body-supplied one would be a
cross-tenant read with an authentication step in front of it.
The gate hands each route the resolved plane with the caller, and this
struct holds no runtime of its own. A handler therefore cannot read a store
without having established whose it is, and every lookup on the registry
names a caller rather than a tenant — so the tenant a route can reach is
the one its credential resolved to, and reaching another is
Planes::cross, which records the crossing first. A caller whose tenant
has no plane is refused, never served by a default — a fallback would turn
an unregistered tenant into somebody else’s data, and it would look like
working software.
§No authenticator is shipped
Same reasoning as the policy engine and the tracing exporter: the deployment
owns its identity system, and a bearer-token parser baked in here would be
wrong for the mutual-TLS deployment and load-bearing for the other. What this
crate owns is the shape — every route runs behind
Authenticator::authenticate, and there is no route that does not.
§What an operator actually needs
The endpoints come from the questions a person asks at three in the morning, not from the crate’s type graph:
- What is this run doing, and why is it not finishing? — a suspended run reports what it is waiting for, because “suspended” alone sends someone into the journal.
- What is waiting for me? — the worklist, filtered to the caller’s roles, with each item saying whether this caller may decide it and who has already reserved it.
- This one is mine; don’t let a colleague duplicate the work. — claim, and release again if it turns out not to be theirs after all.
- Let me approve this. — with four-eyes intact.
- This message arrived; wake whoever wanted it. — event delivery.
- Stop it. — the other half of oversight, and the one most surfaces omit.
- What has happened on this matter? — the case, its deadlines, its tasks.
There is no /health and no /metrics. The embedder owns the port and the
process; a liveness probe answered by this router would be one more route
that must not authenticate, and the one route that skips the gate is the one
that eventually grows a feature.
§Wiring it up
// `Api::new` fails here if the runtime has no policy engine.
let router = Api::new(runtime, Arc::new(MyAuth))?.router();
let listener = tokio::net::TcpListener::bind("127.0.0.1:8080").await?;
axum::serve(listener, router).await?;Modules§
- a2a
- Serving A2A: this plane, as an agent other agents can call.
- action
- Actions this surface asks the policy engine about.
- tokens
- Bearer tokens mapped to callers, from a file the operator writes.
Structs§
- Api
- Everything the routes need.
- Caller
- Who is on the other end of the request.
- Cancel
Request - What a stop request looks like on the wire.
- Decision
Request - What a decision looks like on the wire.
- Planes
- The tenants this process serves, one plane each.
- RunView
- A run, to somebody working out why it has stopped.
- Task
View - A task as an operator sees it.
- Worklist
- One page of the worklist.
Enums§
- ApiSetup
Error - Why the surface could not be built.
- Auth
Error - Why a request has no usable identity.
Traits§
- Authenticator
- Establishes who is calling.