Skip to main content

Module api

Module api 

Source
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.
CancelRequest
What a stop request looks like on the wire.
DecisionRequest
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.
TaskView
A task as an operator sees it.
Worklist
One page of the worklist.

Enums§

ApiSetupError
Why the surface could not be built.
AuthError
Why a request has no usable identity.

Traits§

Authenticator
Establishes who is calling.