APL Grammar
This document defines the accepted grammar for APL (Authorization Policy Layer).
APL (Authorization Policy Layer) defines PPE enforcement pipelines. Each capability an agent may invoke (e.g., a tool, resource, prompt, or A2A method) defines a route that sequences its boundary controls.
APL configuration comprises routes, phases, predicates, rules, and field pipelines:
data.* namespace.Policy is organized by route: an operation PPE mediates, identified by the tool or other interface it governs. Each route runs through four phases, in order:
The first deny in any phase halts that phase and every later phase. Nothing
reaches the backend after a deny in args or authorization.pre_invocation.
authorization names when the phase runs, not a pure allow/deny gate:
alongside the decision, pre_invocation (and post_invocation) can carry
obligations and effects — taint(...), delegate(...), and run(...)
(which may transform the payload) — that run as part of the phase.
routes:
- tool: get_employee
args:
employee_id: "str"
authorization:
pre_invocation:
- "require(authenticated)"
- "delegation.depth > 2: deny"
result:
ssn: "str | redact(!perm.view_ssn)"
salary: "int | redact(!role.hr)"
employee_id: "str | mask(4)"
pre_invocation: and post_invocation: nest under authorization:. Each phase
is an ordered list of rules and effects. An authorization: block contains at
least one phase.
A predicate reads attributes resolved from the caller’s identity and request context (see Identity for where attributes come from). The forms:
authenticated, role.hr, perm.view_ssn.delegation.depth > 2, client.trust_level == 'trusted'.
Operators: ==, !=, >, >=, <, <=.subject.id in authorized_users, subject.id not in banned_list.exists(delegation.origin_subject_id) is true when the
attribute is present.security.labels contains "secret".& (and), | (or), ! (not). Precedence is () >
! > & > |.- "(role.hr | role.security) & !delegated"
A pre_invocation: (or post_invocation:) entry is a rule. Two forms:
require(...) denies unless the predicate holds:
- "require(authenticated)"
- "require(role.hr)"
- "require(!delegated)"
require(a, b) denies if either is false (an implicit and). require(a | b)
denies only if both are false.
predicate: effect runs the effect when the predicate holds:
- "delegation.depth > 2: deny"
- "security.labels contains \"secret\": deny('session touched secret data', 'session_tainted')"
deny takes an optional reason and code: deny, deny('reason'), or
deny('reason', 'code'). The code is surfaced to the caller and the audit log.
For richer conditionals, use the when / do form, where do is a single
effect or a list:
- when: "role.hr & !perm.view_ssn"
do:
- "taint(restricted, session)"
- "run(audit-log)"
By default a deny surfaces a reason and code, and the host renders its own
denial. A route can instead attach a custom HTTP response — status, body,
headers — through a response: block, a sibling of the route’s authorization:
block:
routes:
- tool: locked
authorization:
pre_invocation:
- "require(authenticated)"
response:
status: 403
body: "{\"error\":\"forbidden\"}"
headers:
WWW-Authenticate: "Bearer"
All three fields are optional; an absent block leaves the host’s default denial
unchanged. When the route denies, the status/body/headers are carried on the
violation for the host to render on the wire. response: is honored at route
scope and at global scope (below); it is inert — and warns at load time —
under defaults or a policy bundle. It is scope-local: a global response:
is not inherited by entity routes.
Routes key on tool, prompt, resource, or LLM. A generic
HTTP request that carries no such entity is authorized by the global
policy instead, or by an http: route that selects on the request line
(see HTTP Routing). When global declares an
authorization: (or args:) block, PPE evaluates it for these requests,
reading the request line (http.method, http.path, http.host,
http.scheme) and headers. Pair it with a global response: to return a
custom denial.
global:
authorization:
pre_invocation:
- "http.method != 'GET': deny"
response:
status: 405
headers:
Allow: "GET"
The host must populate http.host from a validated request authority, never a
raw client Host header, so host-based predicates cannot be spoofed by the
caller.
args: and result: map a field to a pipeline of stages separated by |.
Stages run left to right; a failed validator denies the phase.
result:
ssn: "str | redact(!perm.view_ssn)"
email: "email"
employee_id: "str | mask(4)"
The accepted stages:
| Category | Stages |
|---|---|
| Type validators | str, int, bool, float, email, url, uuid |
| Constraint validators | enum(a, b, c), regex("..."), len(1..100), range like 0..100 |
| Transforms | mask(N) (keep last N), redact, redact(!predicate) (redact unless), omit, hash |
| Scans | pii.redact, pii.detect, injection.scan |
| Dispatch | run(name), taint(label[, scope]) |
plugin(name) is not a spelling here or in step position. run(name) is
the one form that invokes a plugin, in a step list and a pipe chain
alike, and writing plugin(name) is an error naming the replacement.
Named-validator dispatch (validate(name)) is refused rather than
unimplemented. The stub would have let every value through, which is a
silent hole in a validator. Use regex("...") for a pattern check, or
run(name) to hand the field to a plugin.
A pre_invocation: rule can also call a decision point, mint a delegated
token, or invoke a plugin. Those effects and how they sequence are
covered in Effects.
Two blocks sit alongside authorization: on the same sections and are
not policy terms themselves:
assertions: renders engine-derived identity onto the upstream
request as headers and filters what an upstream may tell a client
back. It runs after the applicable policy phase. See
Header Assertions.authentication: names the identity-resolution plugins that run
before policy. See Identity.Every fragment on this page is drawn from the praxis-policy-apl-core parser
tests and the
reference deployments, so the forms shown here parse as written.
This document defines the accepted grammar for APL (Authorization Policy Layer).
Some controls are not about whether an operation runs, but where it runs.
When PPE forwards an operation to a backend, the backend needs a credential.
An APL rule does something. That something is an effect. Effects are the building blocks of policy: a pre_invocation: block is an ordered list of them, and they run in sequence until one denies.
Some operations should not proceed on the caller’s say-so alone.
Policy reads attributes: role.hr, perm.view_ssn, subject.id.
APL predicates handle attribute checks well: roles, permissions, scopes, comparisons.
Carry sensitivity labels across requests in a session, then deny later operations that would expose tainted data.
Policy reads attributes. Most come from the request: the verified subject and its roles (Identity), request headers, session labels.