AWS Marketplace·Enterprise deployment — listing in progress.Contact sales·View pricing

INTERTRACE — RUNTIME AI SECURITY • GATEWAY PROTECTION • RUNTIME VERIFICATION • BEHAVIORAL INTELLIGENCE • PROMPT INJECTION DEFENSE • PII REDACTION • SUB-50MS CLASSIFICATION • COMPLIANCE REPORTING • MANAGED AGENTS • OWASP LLM TOP 10 • INTERTRACE — RUNTIME AI SECURITY • GATEWAY PROTECTION • RUNTIME VERIFICATION • BEHAVIORAL INTELLIGENCE • PROMPT INJECTION DEFENSE • PII REDACTION • SUB-50MS CLASSIFICATION • COMPLIANCE REPORTING • MANAGED AGENTS • OWASP LLM TOP 10 • INTERTRACE — RUNTIME AI SECURITY • GATEWAY PROTECTION • RUNTIME VERIFICATION • BEHAVIORAL INTELLIGENCE • PROMPT INJECTION DEFENSE • PII REDACTION • SUB-50MS CLASSIFICATION • COMPLIANCE REPORTING • MANAGED AGENTS • OWASP LLM TOP 10 •
← Back

Pre-action authorization and execution permits for tool-using agents

Technical paper · June 9, 2026 · 13 min readBy Samuel OyanEngineer
paperauthorizepermitsagents

Technical paper: why POST /v1/actions/authorize plus a scoped permit is the canonical control, and why a chat-time tool_calls decision is not a substitute for enforcement at execution.

Abstract

Agent frameworks emit tool calls as a side effect of token generation. If the only policy check is a classifier on the prompting turn, execution can diverge: retries, parallel tools, delayed MCP calls, or a second hop that never re-enters the chat completion path. We specify a two-phase control. Phase one authorizes an intended action against tenant policy via POST /v1/actions/authorize. Phase two binds a time-limited, scoped execution permit that the runtime must present and consume at the tool, MCP, Service, or A2A boundary. Missing, expired, replayed, or mismatched permits fail closed. Legacy POST /v1/runtime/tool_calls/decision remains compatible; it is not the source of truth for new work. We describe the request and decision contracts, permit claims, consumption points on a production agentgateway, and the failure modes that chat-time decisions cannot see.

Phase 1 authorize · Phase 2 execute
Agenttool intentPOST /v1/actionsauthorizePermitscoped · TTLExecutetool / MCP

The permit is the artifact. The model’s suggestion is evidence of intent, not authorization.

1. Introduction

Large language model APIs popularized a completion-centric security model: inspect the prompt, perhaps inspect the completion, then hope that any tool the model named was either not executed or was executed exactly as classified. That model was already incomplete for plugins. It is structurally false for agents. An agent is a loop. It plans, calls tools, reads results, plans again, and may run tools that never appeared in the first completion. Output scanning on the final message does not constrain the HTTP call that already posted to a payment API.

Time-of-check to time-of-use (TOCTOU) is native to this loop. A completion may list three tools; only two run; the third runs after a human leaves. A retry may change arguments. A parallel branch may call a networked MCP server that was not in the original tool list. A delayed worker may execute a queued call with a stale decision. If authorization is a property of the chat turn, none of those executions are in scope.

The correct object of authorization is the action: a named tool or operation, a concrete argument digest, a tenant, an agent, a session, and a policy version. The correct artifact of authorization is a permit: a signed, expiring, scoped token that an executor must consume before side effects. The model’s suggestion is evidence of intent. It is not a capability.

1.1 Contributions

  • A two-phase control: canonical POST /v1/actions/authorize, then a scoped TTL execution permit consumed at the execution boundary.
  • A claim set that binds tenant, agent, session, tool, operation, resource, argument SHA-256, decision id, policy version, expiry, and one-time jti.
  • A compatibility rule: POST /v1/runtime/tool_calls/decision remains supported and is not an invitation to a third dialect.
  • An implementation sketch on Fly agentgateway (v1.3.1) with Intertrace Go enforcer ExtAuthz / ExtMCP, fail-closed on missing permits, and async evidence emission.
  • A threat model covering replay, argument mutation, untrusted instruction sources, and control-plane unavailability.

2. Problem and threat model

2.1 What chat-time decisions cannot see

A typical SDK path is: send messages to POST /v1/chat/completions, parse tool_calls from the assistant message, execute tools locally or via MCP, append results, repeat. A gateway that only classifies the chat request sees the names the model proposed at that hop. It does not see whether the client actually invoked those tools, invoked extra tools, mutated arguments, or delayed execution until the classification context was gone. Frameworks that stream partial tool calls make this worse: the security check may run on an incomplete argument object.

Legacy POST /v1/runtime/tool_calls/decision was built for that chat-shaped world. It can be a useful compatibility shim: given a proposed tool call, return allow, deny, or transform. It is not a substitute for enforcement at execution. Nothing forces the client to call it. Nothing binds the later HTTP POST to the decision. Nothing prevents a second call with different arguments under the same conversational id. Compatibility is not a control.

2.2 Attacker goals

  • Execute a high-impact tool that was never authorized, or was authorized with different arguments.
  • Replay a valid permit against a new session, tenant, or payload.
  • Use an untrusted instruction source (document, tool result, decoded channel, model output) to mint a high-impact permit as if an operator had asked.
  • Race a policy change: authorize under pack vN, execute after pack vN+1 would have denied.
  • Treat ExtAuthz timeout as allow, or treat ALLOW_MONITOR as an execution grant.

We assume the client SDK may be honest, buggy, or hostile. We assume MCP servers, A2A peers, and Service backends are untrusted at the content layer even when they are allowlisted at the network layer. We assume the control plane can be slow or down. High-impact paths must not fail open. Low-risk optional detectors may degrade; they must not mint permits they did not earn.

3. Canonical control: POST /v1/actions/authorize

The canonical pre-action API is POST /v1/actions/authorize. New SDKs, MCP ExtMCP mapping, Service and A2A execution, and the Fly product path use this endpoint. The request names a tenant context that must already have been derived from a runtime key (itr_rt_*) via ExtAuthz → POST /api/runtime/verify-key. A client-supplied tenant_id that disagrees with the verified key is a deny, not a merge. Tenancy is key-derived. Authorize does not invent it.

3.1 Request

The body carries schema_version, tenant_id, trace_id, optional session and MCP session identifiers, an agent object (id, optional version and declared purpose), and an action spec. The action spec includes tool_id, operation, arguments or an arguments_sha256, optional target resources and destination, data classes, expected side effect, reversibility, and protocol fields (tool_protocol, mcp_server_name, mcp_tool_name). contains_sensitive_data is a first-class boolean propagated from inspect, not a hint the model is invited to set.

instruction_source is operator | document | tool_result | decoded_channel | model_output. Untrusted sources cannot mint high-impact execution permits. That rule exists because agents launder instructions: a PDF tells the model to transfer funds; the model then asks authorize as if the operator had typed the transfer. Parent decision ids allow a chain to be audited without treating a child call as a new unconstrained grant.

3.2 Decision

The response carries decision_id, a decision enum (allow, transform-allow, deny, require-approval, and monitor-class outcomes), reason codes, policy version, and—when execution is actually granted—an execution permit. ALLOW_MONITOR does not mint a permit under the v1 contract. An executor that treats monitor as allow will fail closed with PERMIT_REQUIRED. That is intentional. Observing a dangerous call is not permission to make it.

Deterministic deny always wins over classifier output (ADR 0002). Protected assets always ForceAI (ADR 0004); regex freeze in classifier/regex.go and riskgate/gate.go is not a license to skip authorize. OPA and policy packs are inputs to the decision. They are not a second allow path that bypasses the handler. Preloop CEL/YAML authoring, when used, compiles into Intertrace policy surfaces. Preloop is not the authorize source of truth and not the customer UI.

4. Execution permits

4.1 Why a second artifact

Authorize can run in-process with ExtAuthz. Execution may be a later hop: agentgateway selecting an MCP backend, a Service upstream, or an A2A peer. Caching a route-level allow without a single-use token would reuse the grant. Passing the model’s tool name as a header would be forgeable. The permit is the unforgeable link between decision and side effect. v1 permits are compact HS256 JWTs (intertrace.execution-permit.v1). Asymmetric JWS is a follow-up behind the same claim schema.

4.2 Claims

  • Scoped: tenant/org, agent, session or context id, tool name, operation, concrete resource (mcp backend, service:{key}, or a2a:{agent_key}), canonical argument SHA-256.
  • Versioned: decision_id, policy bundle version, containment snapshot version when available.
  • Expiring: nbf/exp. TTL is short enough that a stolen permit is not a standing grant. Two minutes is an operational order of magnitude, not a slogan; clock skew is explicit.
  • Single-use: jti consumed at the execution boundary. Replay returns PERMIT_REPLAY and does not execute.
  • Auditable: non-secret permit id and decision id may be attached as upstream audit headers. The consumed token is not forwarded to the MCP server or Service backend.

The executor verifies signature, expiry, tenant and agent identity, session, tool and operation, resource, exact body digest, and versions embedded at authorization. Missing, expired, replayed, or mismatched permits fail closed. A client-supplied X-Intertrace-Execution-Permit header is removed so callers cannot inject a permit the enforcer did not issue for this hop.

4.3 Consumption points

On MCP tools/call, agentgateway authenticates and applies ExtAuthz, then ExtMCP CheckRequest maps params into POST /v1/actions/authorize. ALLOW or TRANSFORM_ALLOW must contain a permit. ExtMCP verifies and consumes it in-process, bound to tenant, agent, session, tool, backend resource, and argument digest. Only then does CheckRequest return Pass and the call proceed to the selected MCP server. That Pass is the execution enforcement point, not the chat completion that mentioned the tool.

Service and A2A follow a two-check path: route ExtAuthz issues a permit as request metadata; the uncached backend ExtAuthz verifies and consumes jti before dispatch. Neither check is cached for those executions. Caching a route response would reuse a single-use permit; caching the backend response would bypass consumption. Service non-read operations (POST, PUT, PATCH, DELETE, or custom writes) require a permit. A2A message/send, message/stream, and task cancellation require a permit. Health, discovery, agent cards, and registered read-only Service operations stay on normal authorization without permit consumption.

Instruction source is an anti-laundering control, not a UX field. If the SDK always sends operator, the control is theater. ExtMCP and document ingest paths should set tool_result or document. Model-proposed follow-up tools should set model_output. High-impact money movement, identity change, and bulk export must not mint permits from untrusted sources even when the tool name is in expected_tools. expected_tools is a baseline against list drift, not a standing capability for any argument digest.

5. Compatibility

Existing clients that call POST /v1/runtime/tool_calls/decision continue to function. That endpoint is an interoperability tax. It is not the SSOT for new SDKs, new planes, or new papers. Dual support must not grow a third decision dialect. protectTool / protect_tool default to authorizeAction plus verifyExecutionPermit; the legacy decision path is opt-in. Framework adapters (LangChain, Vercel AI, duck-typed) wrap the canonical pair.

Teams sometimes ask whether chat-time classification plus the legacy endpoint is “good enough” if they promise to call it. Promises are not enforcement. The Fly path terminates /v1 and /mcp. The enforcer can require a permit at those boundaries regardless of SDK virtue. That is the reason the control lives in the gateway rather than in a library the application might forget to wrap.

6. Fail-closed properties

The Go enforcer is fail-closed on ExtAuthz, promptGuard, ExtMCP, and authorize plus permits. Transient authorizer failures deny (HTTP 403), matching agentgateway failureMode default Deny. PERMIT_REQUIRED, PERMIT_VERIFIER_UNAVAILABLE, PERMIT_REPLAY, and context mismatches deny. High-impact missing-auth context fails closed (BLOCK or REQUIRE_APPROVAL). Optional detector failures on low-risk paths may fail_open_monitor; they still do not mint a high-impact permit.

Control-plane unavailability is a separate case. Cached runtime key auth and a last signed policy bundle may allow authorize to continue for already-primed keys (see the control-plane-down runbook). Cold high-impact paths without cache fail closed and must not issue a permit. Event ingest to the dashboard is asynchronous. A slow SecurityEventEnvelope write must not stall authorize. Evidence reporter failures must not change ExtAuthz allow/deny. Those are easy sentences to write and easy to violate in a helper that awaits ingest before returning Pass.

7. Implementation notes (Intertrace)

Production clients use https://intertrace.fly.dev with pin v1.3.1 and Bearer itr_rt_*. Railway gateway-v2 is not the product path; do not design permit work against it. YAML for agentgateway --file is compiled in-repo. All planes are live: LLM /v1/*, MCP /mcp, Inference, Service /service/* with a per-tenant registry, and A2A /a2a with deep message/send inspect. Service is not an uninspected tunnel. Registry entries are tenant-scoped and subject to the same authorize story as tools.

Identity for humans is Clerk; entitlements are Convex; telemetry is Supabase with RLS on org_members. Runtime keys do not substitute for Clerk. Authorize decisions persist as runtime outcomes and as envelope fields. Finding creation remains a product choice: do not auto-create an incident for every deny. Manual create lives in the Work hub. Status changes on findings append history via trigger with status_change_source.

Replay protection is process-local unless a Redis nonce store is attached. Process-local jti state does not survive restart and is not shared across Fly machines. That is a documented residual limitation, not a secret. Multi-machine production should attach shared nonce state before treating replay protection as a fleet property. Permit verification does not refetch policy between authorize and consume when both occur in the same request hook; versions in the claims are what the executor checks.

8. Measurement

A permit control that cannot be tested will rot into a header. Minimum live checks: authorize ALLOW mints a permit; first verify consumes it; second verify returns PERMIT_REPLAY; argument mutation after authorize fails digest match; tenant mismatch fails closed; ALLOW_MONITOR does not execute. E2E scripts against the Fly edge (itr_rt_* mint, /v1/actions/authorize, MCP tools/call) are the product proof. Unit tests on HS256 claims are necessary and not sufficient.

Latency belongs in the same scorecard. Authorize plus ExtMCP consume sits on the user-visible path for tools. Event persistence does not. If p95 authorize exceeds the interactive budget, the failure mode is still deny on timeout for high-impact classes—not skip. Teams that “fix” latency by omitting the permit have not optimized. They have removed the control. Report authorize duration (the Go histogram already exists) next to block rate, replay count, and PERMIT_REQUIRED volume. A spike in PERMIT_REQUIRED after an SDK change is usually a client that stopped presenting the grant, not an enforcer regression.

Policy pack IDs recorded on the decision are inputs to forensics, not proof of industry-solution readiness. A permit that names pack financial_services does not make the org SOC 2 certified. Evidence-computed readiness is a separate catalog function. Authorize should still stamp pack and OPA bundle versions so an investigator can replay which rules fired. Confusing those layers is how a security kernel starts minting certificates.

Capability-security and object-capability literature treats unforgeable tokens as the right to invoke a method. We apply that idea to LLM tool execution with short TTL and argument binding, because natural-language intent is not a capability. OAuth access tokens are a cousin and a bad fit: they are usually too coarse, too long-lived, and not bound to a SHA-256 of this POST body. SPIFFE and workload identity (ADR 0007) authenticate the gateway instance to the control plane; they do not replace per-action permits for customer agents.

Model Context Protocol standardized tool list/call, resources, and prompts. Standards concentrate attack surface. ExtMCP inspect plus authorize plus permit is how we refuse to treat /mcp as a second, weaker firewall. A2A message/send is the same philosophy with a different envelope. Chat safety literature that stops at the prompt is necessary and insufficient once tools exist.

10. Limitations

  1. HS256 permits are symmetric. Compromise of the enforcer signing secret is fleet-wide for the validity window. Asymmetric JWS reduces that blast radius; it is not v1.
  2. Argument SHA-256 is exact. Canonicalization bugs (key order, unicode, float formatting) become either false denials or missed mutations. The spec must name the canonical JSON form.
  3. Response-side semantic inspection of MCP results and A2A artifacts is incomplete. Indirect injection in tool returns remains a detection problem even when the call was authorized.
  4. Read-only Service and discovery paths without permits are a deliberate hole in the permit story, not in ExtAuthz. They still authenticate the runtime key.
  5. SDK users who execute tools entirely outside the gateway (local shell, unmanaged MCP) are outside this control. Estate views will not see them. That is shadow AI, not a permit bug.

Human-in-the-loop is a decision outcome, not a second protocol. REQUIRE_APPROVAL must not mint an execution permit until the approval record exists and authorize is re-entered. Pausing /v1/chat/completions for HITL is the wrong layer: operators will disable it, and ordinary chat is not the side effect. The product spine collects approvals; the enforcer still fail-closes if the permit is absent at tools/call.

11. Conclusion

Runtime AI security that only classifies prompts will miss the action. The canonical control is POST /v1/actions/authorize plus a scoped, expiring, single-use execution permit consumed at the MCP, Service, or A2A boundary. The model’s tool_calls array is a proposal. The permit is the grant. Legacy POST /v1/runtime/tool_calls/decision remains for compatibility. New work should not add a third dialect, should not treat monitor as allow, and should not fail open when the enforcer or verify-key is down.

Gateways already terminate /v1 and /mcp. That is the right place to require a permit. Agents will keep looping. Authorization has to loop with them—on the action, with an artifact an executor can refuse.

References (selected)

  • Intertrace ADR 0002 — gateway runtime modes and execution permits (FAST_GUARD default; deterministic deny wins; HS256 v1 permits).
  • Intertrace ADR 0004 — AI-first detection; regex freeze; ForceAI on protected assets; benign skip opt-in and default off.
  • Intertrace ADR 0005 — unified platform composition; authorize + OPA + permits as policy decision SSOT; Preloop not customer UI.
  • Intertrace ADR 0007 — gateway workload identity; verify-key audience and capability; fail-closed on identity mismatch.
  • docs/architecture/MCP-EXECUTION-PERMITS.md — ExtMCP CheckRequest consume-before-Pass on tools/call.
  • docs/architecture/SERVICE-A2A-EXECUTION-PERMITS.md — uncached backend ExtAuthz and jti consumption.
  • Model Context Protocol specification — tools/list, tools/call, resources, prompts as concentrated attack surface.
  • Capability-based security (Dennis & Van Horn; Miller) — unforgeable references versus ambient authority.
  • OWASP Top 10 for LLM Applications — LLM01 prompt injection, LLM08 excessive agency, as vocabulary, not as a certificate.
  • Envoy External Authorization — failure mode Deny as the only acceptable default for a security kernel.

Continue reading

← Back to blog