HTTP Routing
On this page
Most routes select an operation by name: a tool:, a resource:, a
prompt:, an llm:. An http: route selects on the request line
instead, so plain HTTP traffic that names no entity can still be
authorized.
One PPE instance can therefore cover an agent’s tool calls and the surrounding HTTP traffic under one APL document.
Selector forms
A bare path, or a list of them, matches exactly:
routes:
- http: /admin
authorization:
pre_invocation:
- "require(role.admin)"
- http: [/metrics, /healthz]
authorization:
pre_invocation:
- "allow"
The map form takes exactly one of path: or path_prefix:, optionally
narrowed by method::
routes:
- http:
path_prefix: /api/v1
method: [POST, PUT, PATCH]
authorization:
pre_invocation:
- "require(perm.write)"
- http:
path: /api/v1/status
method: GET
authorization:
pre_invocation:
- "allow"
Anything other than path, path_prefix, and method in that map is
a load error.
Precedence
When more than one route could match, the winner is decided in this order:
- An exact path outranks a prefix.
- A longer prefix outranks a shorter one.
- A narrower method set breaks a remaining tie.
/api/v1/status above wins over the /api/v1 prefix beside it,
regardless of declaration order.
Exact paths compare byte for byte. /admin and /admin/ are two
different paths, and a route declaring one does not match the other. A
prefix is normalized instead: one trailing slash is dropped at parse
time, so path_prefix: /api/ and path_prefix: /api are the same
selector. A prefix matches on segment boundaries, so /api does not
match /apidocs.
The catch-all
A request that matches no http: route falls through to global policy.
That is a real decision rather than an accident, so a configuration
declaring http: routes without a catch-all is reported at load.
To write one, use the root prefix:
routes:
- http:
path_prefix: /
authorization:
pre_invocation:
- "require(authenticated)"
Host obligations
Missing either obligation changes which policy runs without failing the request.
The request line at the identity hook. A route’s own
authentication: list is only reachable if the engine knows the method
and path when identity is resolved. A host that puts them on the HTTP
extension at the identity invocation unlocks per-route authentication;
a host that does not gets global authentication instead, and the engine
warns rather than pretending the route’s list ran.
The response hook, invoked explicitly. A global result: or
post_invocation: block installs a handler on http.response, but the
host fires it. Before turning that on, review your global post steps:
HTTP steps that were previously inert become active, and result.* is
absent for a request carrying no entity.
Response bodies are not modeled. The response half covers headers and extensions.
Reading the response status
http.status carries the response status to post-phase policy. The
host sets the optional value on response invocation; it enters the
attribute bag as an integer and uses the existing read_headers
capability.
The key is absent on requests, so a rule reading it belongs under
post_invocation:.
routes:
- http:
path_prefix: /api
authorization:
post_invocation:
- "require(http.status < 500)"
Dispatch mode
http: routes require the default dispatch: policy. Under
dispatch: hooks a routes: block is a load error outright, so a
configuration carrying an http: route is in policy mode by
construction.
A policy body replaces the route’s structural plugin chain, so a listed
plugin runs only where a step names it with run(name).
Next
- Identity and Delegation: resolve inbound principals and mint outbound credentials.
- Header Assertions: control the identity and response headers crossing the boundary.
- Deployment: place the HTTP enforcement point.