Guide · Security

Policy-as-code: how to protect systems from autonomous agents.

Last updated 6 July 2026 · By Udal Systems

An agent that can call tools, read data, and spend money is a production system. Prompts do not protect production systems. Policy-as-code does. This is the pattern regulated institutions use to make agent controls testable, auditable, and impossible to bypass.

Why prompts are not a control

A system prompt is a suggestion the model reads before it acts. It lives inside the same context that tool outputs, retrieved documents, and user input all pour into. Anything in that context can override anything else in that context. That is a design property, not a bug, and no amount of prompt hardening changes it.

Real controls have to sit outside the model — between the agent and the tools, data, and money it can touch — where a deny decision is a deny decision no matter what the model believed it was doing.

What policy-as-code means for agents

Policy-as-code expresses organisational controls as versioned, testable code. For agents, it answers four questions on every request:

  • Who — which identity is this agent acting as, and what is that identity allowed to do?
  • What — which tool is being called, with which arguments, against which data class?
  • How much — is the daily budget intact, and does this action fall under the approval threshold?
  • On whose behalf — does the invoking user actually have the permission the agent is trying to exercise?

The two gates

A working control surface has policy running in two places:

Promotion gate

Before it ships

The agent manifest is evaluated in CI against organisation policy. Disallowed tools, unbounded budgets, missing approvals, or unscoped identities fail the build. The agent never reaches a cluster.

Request gate

Every action, at runtime

Every tool call, data read, and outbound request is evaluated by the same policy engine — typically OPA — with the invoking user's claims and the agent's declared scope as inputs. Deny is enforced by the runtime, not the model.

What a policy looks like

A concrete example: a treasury agent may read ledger data, but any outbound payment over $10,000 requires a second human approval and never runs outside business hours.

package udal.agents.treasury

default allow := false

# Read-only ledger access is allowed inside the analyst role.
allow if {
  input.tool == "oracle-erp"
  input.action == "read"
  "analyst" in input.identity.roles
}

# Outbound payments over 10k require dual control and business hours.
allow if {
  input.tool == "payments"
  input.action == "transfer"
  input.args.amount <= 10000
  business_hours
}

allow if {
  input.tool == "payments"
  input.action == "transfer"
  input.args.amount > 10000
  input.approvals.count >= 2
  business_hours
}

business_hours if {
  h := time.clock(time.now_ns())[0]
  h >= 8
  h < 18
}

Five properties any agent policy layer needs

  1. 1. Deny by default

    The base state is that an agent can do nothing. Every capability is an explicit allow, scoped to identity, tool, and data class.

  2. 2. Same policy in CI and at runtime

    If the promotion gate and the request gate disagree, the runtime always wins — but they should never disagree. Versioned bundles keep them in sync.

  3. 3. Human identity flows through

    An agent inherits the permissions of the user who invoked it. It cannot exceed them, and revoking the user revokes the agent's ability to act on their behalf.

  4. 4. Hard budgets and kill-switches

    Cost is a policy input, not an afterthought. Risk should be able to stop an agent from spending another token without filing a ticket.

  5. 5. Every decision is logged

    Allow, deny, and the inputs that produced each decision are written to a tamper-evident log. This is what turns policy-as-code into audit evidence.

How Udal implements this

Udal ships policy-as-code as the default path. Every agent is described by a manifest (udal.io/v1alpha1) that declares its identity, tools, budgets, and approval rules. The Check stage runs OPA against the manifest in CI. The Govern stage runs the same engine on every request, using the invoking user's claims. The Observe stage writes every decision to a hash-chained audit log with OpenTelemetry traces attached, so the answer to "what did this agent do, and why was it allowed?" is always one query away.

Where to go next