Policy decision for an infrastructure gateway (PEP)
Decision Mode endpoint (ADR-056 / epic #2426). The customer's infrastructure gateway (Policy Enforcement Point) calls this endpoint per request to get a verdict (allow or deny) and enforces the result. AxonFlow is consulted, never on the traffic path.
The shared-policy engine behind this endpoint is the same engine that backs Gateway Mode's POST /api/policy/pre-check. The difference is the caller: Gateway Mode is called by application code; Decision Mode is called by an infrastructure gateway.
M1 scope: static policies only (PII detection, SQL injection, dangerous patterns, RBI India PII, compliance categories) to keep the inline RPC budget in single-digit milliseconds. Dynamic/custom policy support is M2 scope per the epic.
OTel trace correlation: the response carries a W3C-compatible 32-hex trace_id. When the caller passes a traceparent header, its trace-id is reused so multi-gateway-layer decisions stitch into one end-to-end trace. Each decision also emits an OpenTelemetry span on the axonflow.agent.decision tracer.
Available at all tiers (Community through Enterprise) — the policy engine is the same one Gateway Mode uses.
Request
- Base URLhttps://agent.getaxonflow.com
- Authnone declared
- TypeScriptA function with typed inputs. Its response is checked against the schema.
Headers
The approval a retry spends (#4370). A call held for a person's approval is answered a pending_approval naming an id; once a person approves it in the portal, the caller retries the SAME call naming that id here (or in the body's approval_id field, for a client that cannot set headers - a header and a field naming different ids are refused approval_not_found).
The retry is decided exactly as the first call was. Only when it is again held for approval, and the approval is approved by a person who is not the caller, before its expiry, unspent, and granted for this very call (the same input, tool, route, requester and requirement), does it pass - and the approval is spent: it admits exactly one call. A call that is allowed anyway spends nothing; no approval lifts a deny. Every other outcome is refused with one of the ApprovalHoldReason codes. Enterprise; the Community build has no approval queue and ignores the header.
WHAT "THE SAME CALL" MEANS. On the MCP routes: the connector, the tool, the operation, the statement and its parameters (and the row limit on /mcp/resources/query). On /api/v1/decide: the WHOLE request the engine decided - stage, target, query AND context. A retry that changes any of it is refused bound_input_changed: resend the same context, and carry per-request values (request ids, timestamps) in headers, never in context - the correlation id is a header.
Optional client-version telemetry header (<client>/<version>, e.g. mcp-proxy/0.3.1 or claude-code/1.9.1). Enterprise deployments with the client_version_telemetry capability count validated values in the axonflow_client_version_requests_total metric on the decide and MCP check-output planes. Telemetry only — never used for authentication or authorization; invalid values are ignored.
base64url (padding optional) of the PEPHandshake document.
The ADR-065 PEP capability handshake: base64url of a compact JSON document in which an external enforcement point declares what it is and which obligations it can discharge. See PEPHandshake for the document.
Absent is the default and changes nothing. A caller that omits the header takes byte-for-byte the path it took before this header existed.
What an absent header means for a redaction depends on the plane, by design (PRD v11 section 1 item 16, #4257). The MCP passes discharge a redaction of the content they hand back: the request pass (check-input, check_policy) masks the statement, and a redaction that masks nothing in the statement is refused unsupported_obligation to every caller, and one that masks a request parameter to every caller that has not declared field_redact at version 2 (on Community, to every caller) (#4264); the response passes (check-output, the MCP server's check_output) mask the rows or the message. /api/v1/decide and the gateway pre-check return a decision rather than content, so a required redaction is a field_redact obligation for the enforcement point, and a caller that has not declared field_redact is refused unsupported_obligation. A caller that declares NO redaction (capabilities: []) is refused on each of these planes; on Community the MCP passes still return a checksum validator's masked content to it (reachable only through a directly inserted detection_action_overrides row) until #4122.
A header that is PRESENT and cannot be read is refused, never treated as absent: degrading a malformed declaration to "legacy caller" would go on handing an enforcement point obligations it had just said it cannot discharge. The refusal is 400 and its message names this header, which matters on /api/v1/access/evaluation where the refusal is rendered through that surface's existing incomplete_evaluation code and the message is the only thing distinguishing a malformed HEADER from a malformed body ENVELOPE.
Present more than once is refused: RFC 7230 permits an intermediary to join repeated field lines with a comma, and a comma is outside the base64 alphabet, so a joined pair can only decode to malformed. That is why the document is base64 rather than raw JSON, which would join into something a lenient parser might accept.
When a decision carries a mandatory obligation the declared set does not cover, the request is answered 200 with verdict: deny and the reason unsupported_obligation (ADR-065 invariant 8) - a decision about the request, not a transport error. That holds on every edition. An Enterprise deployment adds a second reason beginning pep_capability_unsupported naming the gap, and refuses a checksum validator's mask on the MCP passes; a Community deployment hands that masked content over (#4257 split 2, #4122).
W3C trace-context header. When present and valid, the trace-id is reused in the response so multi-layer decisions correlate into one end-to-end trace.
Request body
Example request
{
"target": {
"model": "gpt-4o",
"provider": "openai",
"server": "postgres",
"tool": "query"
}
}Response
Decision verdict (allow / deny)
Example response
{
"obligations": [
{
"type": "redact_pii",
"fulfillment": {
"endpoint": "/api/v1/mcp/check-input",
"method": "POST",
"content_types": [
"text/plain"
]
}
}
]
}Changes
No recorded changes to this operation across all 1 revision of this API.