← Blog

Approvals & policy · · 8 min read

Policy at action time: what the gate evaluates

The five inputs and three checks a runtime policy gate needs to decide whether an agent action should run, with policy-language and approval-UX considerations.

Jonathan Haas

The control plane sits between the agent's decision and the system the agent wants to act on. Where that gate lives is the first architectural decision in any agent runtime:

  • In-process: lowest latency, worst isolation.
  • Sidecar: a few milliseconds of latency in exchange for clean isolation and a separate failure domain.
  • Gateway: centralizes the policy surface for all agents and adds a network hop.

Most production deployments end up with a gateway, because the policy surface is the most useful thing to centralize and the latency cost stays small.

Wherever it lives, the gate decides before the action runs whether to allow, require approval, or deny. The decision depends on five inputs and three checks.

The five inputs#

  1. Identity. Which agent is asking, which sub-agent in which run, and which operator launched the run. Each has an id, and each propagates. A gate that only knows the service account is operating on the wrong principal.

  2. Resource. What the action targets: pg.write to which database, github.repos.delete to which repository, s3.put to which bucket prefix. Some resources are coarse (a database) and some are fine (a row scoped to a tenant id). The gate's resource model has to match the granularity of the underlying system's access control.

  3. Arguments. What the agent intends to do with the resource. A pg.write with a row id from the customer the agent is helping gets a different decision than one with a row id from another tenant. The gate has to read the specific row, bucket, or repo the action targets. Argument inspection is where most cross-tenant reach incidents become detectable. Without it, the gate only checks the broad permission, which the agent always has.

  4. Run context. Why the run is happening. A run triggered by an inbound support ticket carries a different intent than a run started by a scheduled posture sweep,, and that intent is the prior on what the agent should plausibly do. Policies that ignore run context end up too permissive (the support agent can do whatever the posture sweep can) or too restrictive (the sweep must re-prove what every operator already accepts).

  5. Policy version. Which version of the policy is in force right now. The security team will evaluate the audit row against that version six months later. Without it, the team can answer "is this allowed under the policy that is active now" but not "was this allowed under the policy that was active then."

The three checks#

Scope check. The resource and arguments fall inside the identity's scope for this run. If the agent has scope to customer A, this action is on customer A. This check fires on cross-tenant reach attempts, the failures hardest for operators to detect downstream.

Type check. The action class is allowed at all: read from a known list of resources, write only with approval, delete only with approval plus a second reviewer. The type check maps to RBAC and ABAC rules, and it is where most teams start. On its own it does not prevent cross-tenant reach.

Outcome check. The action's effect can be undone or contained inside the run. Destructive actions that escape the run require approval before they execute. Teams underbuild this check because the gate has to model what state each action changes and whether the change is reversible inside the run. An s3.put to a bucket the agent regularly writes to is undoable in seconds. An s3.delete-bucket is not, and a gate that does not distinguish them treats them as the same action.

Latency budget#

Microsoft's Agent OS, part of their open-source Agent Governance Toolkit, targets sub-millisecond evaluation per action (Microsoft blog). Above 10 milliseconds per action, the agent's response time starts to feel different to the user. Above 50, interactive runs become painful. Above 100, agent authors route around the gate.

Latency hides in three places:

  • Policy compilation: turning Rego or Cedar text into an evaluator. Amortized once per policy version.
  • Policy evaluation: running the evaluator against the inputs. The hot path.
  • Policy I/O: fetching the identity, the policy version, and any state the policy depends on. This grows without limit if the policy fetches external state on every action, so compile state into the policy at version-load time.

OPA Rego vs Cedar#

Two open-source policy languages dominate runtime authorization in 2026: Rego, the language of Open Policy Agent (a CNCF-graduated project), and Cedar, open-sourced by AWS and used internally for IAM and Verified Permissions.

Rego is general-purpose. It treats policy as a logic program with rules that compose freely. That expressiveness encodes any policy you can describe, and makes it easy to write policies with unexpected evaluation cost.

Cedar is purpose-built for authorization and more restrictive by design. Cedar policies can be analyzed before evaluation, and the engine can prove a policy terminates in a finite number of steps.

For an agent-runtime gate:

  • Cedar's analyzability is useful when the security team needs to prove the policy never allows a class of action.
  • Rego's expressiveness is useful when the policy needs complex state, such as the number of times this agent has performed this action in the last hour.

Recent benchmarks put both within an order of magnitude of each other in raw evaluation speed, with the gap depending heavily on policy shape (Teleport benchmark).

Microsoft's Agent OS supports both, plus a YAML rule format for simpler cases. Letting teams pick a language per use case is becoming standard.

The same policy in both languages#

The policy: an agent acting on behalf of an operator can delete a repo only if the operator is in the repo-admins group and the repo is owned by that operator's organization.

In Rego:

package agent.policy

default decision = "deny"

decision = "allow" if {
  input.action == "github.repos.delete"
  input.operator.groups[_] == "repo-admins"
  input.resource.owner == input.operator.org
}

In Cedar:

permit (
  principal in Group::"repo-admins",
  action == Action::"github.repos.delete",
  resource
) when {
  resource.owner == principal.org
};

The Cedar version is shorter. The Rego version is more flexible: adding "and only between 9am and 6pm in the operator's timezone" is straightforward in Rego and awkward in Cedar.

Allow vs require-approval vs deny#

A gate that only allows or denies is a coarse instrument. Production gates emit a three-valued decision.

Allow. The action proceeds without human intervention. The default for actions the policy explicitly covers: reads of resources the agent's identity is scoped to, writes with low blast radius, and tool calls that do not touch persistent state.

Require-approval. The action is held pending operator confirmation and the agent waits. The operator gets a notification. On confirmation the action proceeds. On rejection the agent receives a denied outcome and decides what to do next. Right for actions that are reasonable in context but consequential: writes to customer-impacting state, deletes of recoverable resources, and sends to external systems. It is also what makes negative discovery lag possible, because the operator sees the action before it runs.

Deny. The action does not proceed. The agent receives the decision and continues with whatever its plan looks like without the denied action. Right for actions that are out of scope regardless of intent. A customer-support agent trying to read another customer's data gets denied without an approval prompt.

Teams that start with binary allow/deny default-deny aggressively and end up with an agent that constantly fails. Teams that start with require-approval-on-doubt flood operators with prompts and train them to click through. Getting the split right is policy work: decide which classes of action are reasonable in some context and which are out of scope in every context.

Approval UX: sync vs async#

When the gate returns require-approval, the agent waits. The design question is where the operator approves.

Synchronous. The operator is already watching the run, in a chat interface or a CLI session, and approves in-stream. This fails for runs that outlive the operator's attention.

Asynchronous. The operator gets a notification by Slack, email, or a web inbox and approves out-of-band while the agent waits. One operator can oversee many runs from anywhere.

Production agent runtimes ship both, chosen per policy. A policy that says "writes to the production database require approval" can run sync during a developer's interactive session and async during a scheduled posture sweep.

An action that has waited an hour for approval is probably no longer the action that should run, because the agent's plan has moved on. Policies that emit require-approval should also emit a deadline after which the approval auto-denies.

The MCP gateway pattern#

In 2026, the most common place for the gate is in front of the MCP servers the agent calls. Model Context Protocol is the dominant substrate for agent-tool integration, and an MCP gateway centralizes identity verification, policy evaluation, and audit emission at a single chokepoint between agents and tools.

Red Hat's OpenShift MCP gateway, Kong's MCP tool governance, Tyk's enterprise MCP gateway, and Arcade's MCP gateway are public instances of the pattern. Each puts tools behind a gateway that attaches an identity to the agent's requests, evaluates the policy, and emits the audit row. Microsoft's Agent OS sits in the same architectural slot but intercepts before the MCP request is constructed instead of at the MCP boundary.

The gateway position has two advantages over per-tool enforcement:

  • Tool servers do not each have to reimplement scope-checking, operator-claim verification, and audit emission.
  • The policy version that evaluated the action and the audit row that recorded it come from the same component, so they cannot disagree.

The action-time tradeoff#

Evaluating at action time costs latency on every action. A check that only runs at run start cannot see actions the agent picks later in the run. Cross-tenant reach incidents emerge from those mid-run actions, because the agent's later decisions depend on what it observed earlier.

The visibility benefit grows with the complexity of the run. For runtimes where any action can reach data the operator should not reach, action-time evaluation is the correct default.

Deixic applies policy and holds sensitive actions for approval in the app queue or in Slack, with a tamper-evident history of decisions and results. See What you can do for the approval flow and Trust for containment.