feat(apl): generic-HTTP authorization#110
Conversation
Add optional method/path/host/scheme to HttpExtension and surface them in the APL bag as http.method/path/host/scheme. These let CEL/APL policies reason over the HTTP request line — needed by the Praxis AuthPolicy transpiler, where Kuadrant predicates over request.method/path/host map to http.* (Praxis spike Phase B / U1). The request line rides the existing read_headers capability: the `http` extension slot is gated as a whole in cpex-core's filter_extensions, so a base-tier split would require granular http sub-field filtering (deferred). The host field is documented to be populated from a validated authority (e.g. HTTP/2 :authority), never a raw client Host header, so host-based policy cannot be bypassed. Signed-off-by: Frederico Araujo <frederico.araujo@ibm.com>
Add a per-route `response:` block (the transpiled form of a Kuadrant AuthPolicy `denyWith`) that lets a route declare a custom HTTP status, body, and headers for its denials (Praxis spike Phase B / U2). - New optional DenyResponse on CompiledRoute (additive; most-specific layer wins in apply_layer). Read out-of-band from the route YAML by the apl-cpex visitor, like the `policy:` block — cpex-core tolerates the key. - On Decision::Deny, route_handler stashes status/body/headers into the existing PluginViolation.details map under http.status / http.body / http.headers. No new fields on PluginViolation and no new APL grammar — the violation type stays stable and reason-only denies are unchanged. A host (e.g. the Praxis policy filter) reads details to render a custom denial response; absent → host default behavior. Signed-off-by: Frederico Araujo <frederico.araujo@ibm.com>
Make the catch-all `global` policy enforce on generic (non-MCP/A2A) HTTP
requests, which carry no entity (Praxis spike Phase B / U3).
- New reserved coordinates: ENTITY_HTTP ("http") / ENTITY_NAME_GLOBAL ("*")
and the HOOK_CMF_HTTP_REQUEST ("cmf.http_request") hook.
- The visitor installs a Pre-phase AplRouteHandler bound to the compiled
global policy under those coordinates, granted read_headers so the
policy can read the request line/headers. Entity routes still stack
`global` via apply_layer; this adds the entity-less evaluation path.
- A global-scope `response:` block (transpiled denyWith) is carried onto
the global handler and surfaced on deny via PluginViolation.details (U2).
A host fires invoke_named::<CmfHook>("cmf.http_request", ...) with
meta.entity_type/name set to the reserved coordinates. End-to-end tests
cover allow, deny, and custom-denyWith — exercising U1 + U2 + U3 together.
Signed-off-by: Frederico Araujo <frederico.araujo@ibm.com>
|
Nice — the feature is well-structured and the additive claims hold up. I traced the capability-namespace / Would block on1. The deny-response decoration isn't gated to actual denials — it can corrupt the HIL elicitation protocol. if let (Some(v), Some(resp)) = (violation.as_mut(), self.route.response.as_ref()) { ... }runs after the pending-elicitation assignment ( 2. A Gaps worth a look3. The R18 4. 5. A malformed route Nits6. 7. The global-handler install block duplicates 8. The detail keys Design question (non-blocking)The |
Address review feedback on the generic-HTTP authorization PR: - Stop apply_layer from propagating `response`, so a `global` catch-all denyWith no longer leaks onto inherited entity (tool/llm/prompt/resource) denials with no opt-out. - Decorate only genuine denials via a shared decorate_denial_response helper, and apply it at the session load/persist fail-closed sites too (previously they rendered the default shape). - Warn when `response:` sits at default/policy-bundle scope, where it is inert, instead of dropping it silently. - Parse the route `response:` once above the per-entity loop. - Extract snapshot_dispatch_state to share the registry/router/store read between the global and per-route handler installs. - Promote the http.status/body/headers detail keys to DETAIL_HTTP_* constants shared by producer and consumer. Signed-off-by: Frederico Araujo <frederico.araujo@ibm.com>
|
Thanks @terylt — all addressed in 4f03fdb.
Note: Keeping the |
Signed-off-by: Frederico Araujo <frederico.araujo@ibm.com>
Gate the catch-all handler install on args OR policy (not policy alone), so an args-only global.apl still authorizes entity-less HTTP traffic. Warn when a global response: is configured but no installable policy exists, including the bare response-only block that hit visit_global's early return. Accept response: nested under apl: as well as top-level, with top-level taking precedence (documented as deliberate). Cover the fail-closed session-store denials and the new paths with tests. Signed-off-by: Frederico Araujo <frederico.araujo@ibm.com>
…ity-less HTTP authz Add http.method/path/host/scheme to the extensions and read_headers tables. Document the route/global response: (denyWith) block and the global-policy path that authorizes generic HTTP requests carrying no MCP/A2A entity. Signed-off-by: Frederico Araujo <frederico.araujo@ibm.com>
Summary
Engine-side additions that let CPEX authorize generic (non-MCP/A2A) HTTP requests. These are consumed by the Praxis
policyfilter's new experimentalenforcement: httpmode to run Kuadrant-AuthPolicy-style authorization (see the companion Praxis PR). All changes are additive and non-breaking — existing MCP/entity behavior is unchanged.Draft: pairs with the Praxis AuthPolicy spike praxis-proxy/praxis#746. Cut a release from this branch before Praxis pins it (Praxis currently consumes it via a temporary path dep).
What's here
HttpExtension. Optionalmethod/path/host/scheme, surfaced in the APL bag ashttp.method/http.path/http.host/http.scheme. Ride the existingread_headerscapability (thehttpslot is gated as a whole infilter_extensions; a base-tier split would need granular sub-field filtering — deferred).hostis documented to come from a validated authority, never a raw clientHost.PluginViolation.details. A per-routeresponse:block (transpileddenyWith) is carried onCompiledRoute(additive; most-specific layer wins) and, onDecision::Deny, stashed into the existingdetailsmap underhttp.status/http.body/http.headers. No new fields onPluginViolation, no new APL grammar — reason-only denies are unchanged.globalpolicy for entity-less requests. New reserved coordinatesENTITY_HTTP/ENTITY_NAME_GLOBALand thecmf.http_requesthook. The visitor installs a Pre-phase handler bound to the compiledglobalpolicy under those coordinates (grantedread_headers), so a host can authorize a request that carries no MCP entity. Entity routes still stackglobalviaapply_layer.Tests
apl-cmf: request-line surfacing in the bag.apl-core:DenyResponseapply_layermerge.apl-cpex:response:sub-block parsing; and a new end-to-endglobal_http_authztest exercising everything together — allow, deny, and customdenyWithfor an entity-less HTTP request.Full
apl-core/apl-cpex/cpex-coresuites green; clippy + fmt clean.