AI agents · Policy enforcement · Runtime controls

AI agent policy enforcement: runtime controls for autonomous system actions

Policy enforcement for AI agents has two distinct layers that require different tools. The static layer controls what an agent is authorized to do: which endpoints it can call, which resources it can access. IAM, OAuth scopes, and Stripe Restricted Keys handle this well — you configure once, they enforce at every call. The dynamic layer controls what an agent can do within a specific run: how much money it can spend before being stopped, whether it can be revoked mid-execution if it behaves unexpectedly, whether this run's credential expires in 30 minutes or 24 hours. Standard policy tooling cannot enforce the dynamic layer, because it requires stateful enforcement — tracking cumulative spend across calls, issuing per-run credentials with run-specific policies, maintaining the counter that makes "deny if spend > $500" possible. This page covers why the dynamic layer matters, where standard policy tools hit their limits, and how to implement runtime policy enforcement via a proxy that evaluates policies at call time against a live spend counter.

TL;DR

Static policies (IAM, Stripe Restricted Keys) scope what the agent can call. Dynamic runtime policies scope how much it can spend and for how long within a specific run. The dynamic layer requires three things: a scoped credential issued per run (so policies can differ between runs), a stateful counter maintained across all calls using that credential (so you can enforce "stop at $500"), and an enforcement point that evaluates both before forwarding the call (so the cap fires before money moves, not after). A reverse proxy between the agent and vendor APIs is the natural enforcement point — it sees every call, holds the cumulative counter, and can enforce the cap before the call reaches Stripe.

Where static policy tools stop

Static policy tools enforce access control at call time against a fixed rule set:

-- IAM policy: agent can call Stripe payment_intents endpoint
{
  "Effect": "Allow",
  "Action": "execute-api:Invoke",
  "Resource": "arn:aws:execute-api:*:*:*/*/POST/stripe/v1/payment_intents"
}

-- Stripe Restricted Key: only payment_intents create permission
{
  "permissions": ["payment_intents:write"],
  "metadata": { "agent": "billing-agent" }
}

Both rules evaluate correctly at every call: does the caller have permission to make this request? What they cannot evaluate: how much has this caller spent so far in this run, and should this call be denied because the total has exceeded the run's budget? That evaluation requires a counter that increments with each call — a stateful artifact that neither IAM nor Stripe Restricted Keys maintain. The Stripe Restricted Key doesn't know it has been used to make 499 previous charges today. It will approve call 500 the same way it approved call 1.

The three enforcement layers for AI agent vendor calls

LayerWhat it controlsHow it's configuredWhat it cannot do
Static access control Which endpoints the agent can call; which resource IDs it can access; whether it can read vs write; which IAM role it uses IAM policies, OAuth scopes, Stripe Restricted Key permissions — configured once, applied to all calls using that credential Enforce per-run spend limits; differentiate policies between agent runs; revoke a single run without affecting other runs; track cumulative spend
Dynamic runtime policy How much this run can spend (daily_usd_cap); which exact endpoint paths are allowed (allowed_endpoints); when the credential expires (expires_in); labels for audit attribution (agent_run_label) Vault key issued per run with run-specific parameters — policy defined in code at run time, not in a YAML file applied at cluster level Control which resource IDs the agent accesses (IAM handles this); control network-layer access (ZTNA handles this)
Policy audit log What was called, what was the policy verdict (allowed/cap_exhausted/endpoint_blocked), what did it cost, what credential was used, when Automatically populated by the proxy for every call — no SDK instrumentation required in agent code Enforce policies (enforcement is at call time, audit is post-call); predict future violations (use anomaly detection queries on the audit log)

Why OPA and Cedar don't enforce AI agent spend caps

OPA (Open Policy Agent) and Cedar (AWS's policy language) evaluate policies against request attributes: caller identity, requested resource, method, time. A Cedar policy like permit(principal == Agent::"billing-agent", action == Action::"POST", resource == Resource::"stripe/payment_intents") evaluates deterministically at each call. What it cannot express: a policy that says "deny if the cumulative spend for this run exceeds $500" — because OPA and Cedar evaluate each request independently against a static policy, they have no concept of this run's accumulated spend across prior calls. Implementing spend-cap enforcement in OPA would require an external data source that OPA queries at decision time — a live counter of spend for the current vault key. At that point, you've built a custom spend counter system that OPA is querying, which is equivalent to what a proxy with built-in spend tracking provides, plus the OPA evaluation overhead.

Implementing runtime policy enforcement in code

// Policy-as-code: runtime policies defined at run instantiation
async function issueRunPolicy(runContext: AgentRunContext): Promise<string> {
  const res = await fetch('https://proxy.keybrake.com/vault/keys', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.KEYBRAKE_API_KEY}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      vendor: 'stripe',

      // Policy: total spend for this run cannot exceed the approved budget
      daily_usd_cap: runContext.approvedBudgetUsd,

      // Policy: only the exact endpoints this run is authorized to call
      // (narrower than the Stripe Restricted Key's general write permission)
      allowed_endpoints: runContext.allowedStripeEndpoints,

      // Policy: credential expires when the run is expected to complete
      // (even if someone captures the vault key, it's useless after expiry)
      expires_in: runContext.expectedDuration,

      // Policy: every call is attributed to this run in the audit log
      agent_run_label: `${runContext.agentName}/${runContext.runId}`
    })
  });

  const { vault_key } = await res.json();
  return vault_key;
}

// Different run types get different policies
const nightlyBillingKey = await issueRunPolicy({
  agentName: 'billing-agent',
  runId: crypto.randomUUID(),
  approvedBudgetUsd: 50_000,          // high cap: charges many customers
  allowedStripeEndpoints: [
    'POST /v1/payment_intents',
    'GET /v1/customers'
  ],
  expectedDuration: '4h'
});

const refundProcessorKey = await issueRunPolicy({
  agentName: 'refund-agent',
  runId: crypto.randomUUID(),
  approvedBudgetUsd: 5_000,           // lower cap: refunds on specific tickets only
  allowedStripeEndpoints: [
    'POST /v1/refunds'                 // cannot create new charges
  ],
  expectedDuration: '30m'
});

Each run type has its own policy: the nightly billing agent gets a $50,000 cap and 4 hours; the refund processor gets $5,000 and 30 minutes and is explicitly restricted to the refunds endpoint — it cannot create new payment intents even though the billing agent can. These policies are checked at call time against a live counter, not evaluated statically once. When the refund processor's $5,000 cap is exhausted, the next refund call is rejected before it reaches Stripe. The billing agent's policy doesn't change — it's a separate vault key with its own counter.

Revoking a policy mid-run without redeployment

// Emergency revocation: takes effect on the next proxied request
// No redeployment, no secret rotation, no infrastructure change
async function revokeAgentRun(keyId: string, reason: string): Promise<void> {
  const res = await fetch(`https://proxy.keybrake.com/vault/keys/${keyId}`, {
    method: 'DELETE',
    headers: {
      'Authorization': `Bearer ${process.env.KEYBRAKE_API_KEY}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({ reason })
  });

  if (!res.ok) throw new Error(`Revoke failed: ${res.status}`);

  // From this point, any Stripe call using this vault key returns 401.
  // Other runs using their own vault keys are unaffected.
  // The audit log records the revocation event with the reason.
  console.log(`Vault key ${keyId} revoked: ${reason}`);
}

// Wire into your anomaly detection alerting
async function onSpendAnomalyDetected(runLabel: string, spend: number): Promise<void> {
  const keys = await listActiveKeysForRun(runLabel);  // query audit log
  await Promise.all(keys.map(key =>
    revokeAgentRun(key.id, `spend_anomaly: $${spend} in last hour`)
  ));
}

How Keybrake fits

Keybrake is the proxy layer that enforces dynamic runtime policies between your agent and Stripe, Twilio, or Resend. It issues vault keys per run with run-specific policies (spend cap, endpoint allowlist, expiry), maintains the cumulative spend counter for each vault key, enforces the cap before forwarding calls to the vendor (pre-call enforcement, not post-hoc alerting), and stores every call in a queryable audit log with policy verdict. It does not replace IAM or Stripe Restricted Keys — those handle the static access control layer. Keybrake handles the dynamic layer: run-level spend bounds, per-run expiry, and mid-run revocation that takes effect in milliseconds without touching your infrastructure configuration.

Get early access

Related questions

How does Keybrake's policy enforcement compare to OPA or Kyverno for AI agents?

OPA and Kyverno evaluate policies against request attributes at a single point in time. They're excellent for access control decisions: can this service account call this endpoint? They don't maintain spend counters across calls — implementing a spend-cap policy in OPA requires an external data source with a live counter that OPA queries during policy evaluation, which effectively means you've built the spend-counter infrastructure yourself and are using OPA as a query layer over it. Kyverno is Kubernetes-admission-focused; it evaluates resources at admission time, not at vendor API call time. Keybrake maintains the spend counter natively in the proxy layer, where it can be checked before every call without an external data source lookup. Use OPA for identity-based access control decisions; use Keybrake for spend-based enforcement at vendor API call time.

Can I define spend policies declaratively (YAML/JSON) rather than in code?

The vault key issuance API accepts JSON, so your "policy definition" is a JSON payload sent to the Keybrake API at run start. If you prefer a declarative format, you can store the policy template in a YAML configuration file and render it to JSON at run time — the resulting JSON is passed verbatim to the vault key endpoint. For teams using GitOps, policy templates can live in a repository as YAML files (e.g. policies/billing-agent.yaml with daily_usd_cap: 50000, allowed_endpoints: [...]), loaded at run instantiation and serialized to the vault key API call. The policy is still evaluated dynamically at call time — the declarative format is just a configuration layer over the same runtime enforcement.

How do I handle policy violations — is there a webhook or alert when an agent hits a cap?

The proxy returns HTTP 429 with X-Keybrake-Cap-Hit: true and a JSON body containing code: "cap_exhausted" and the current cap values. Your agent code receives this response and can handle it — throw a non-retryable exception in your orchestration framework, log the cap hit, and route to a notification flow. For proactive alerting, run the anomaly detection query on the audit log on a schedule (every 5 minutes) and trigger your alerting pipeline when the query returns rows. The audit log records every policy_verdict: "cap_exhausted" event, so you can page on cap-hit rate (e.g. >10 cap hits per hour across the fleet) in addition to the dollar-based anomaly detection.

Further reading