Security

Writing the Policy Rule That Catches What the File Missed

Antonio Evans9 min read

A split scene: on the left, a tangle of grey dashed lines representing fallback rules branching into a dark box labeled 'unlisted actions'; on the right, the same actions flow into clean.
Key points

Key points

  1. 1A fallback rule is a net, not a policy: every action it catches is a defect you have not yet named.
  2. 2Turn each catch into a named rule with an owner, a deterministic test, and an expiry date on the fallback path it replaces.
  3. 3Enforce the rule where the tool call is dispatched, not in the system prompt, so the model cannot route around it.
  4. 4Review rules on a fixed cadence and retire any rule the logs show has never fired.

The fallback is a net, not a policy

Most teams building agents end up with a catch-all: if the action is not on the allow list, ask a human. It is a reasonable first move. It also quietly becomes permanent, and then it becomes the policy. The problem is that a fallback tells you nothing. It does not say which action was proposed, why it was not on the list, or who should decide next time. It just stops the run and puts a stranger in front of a screen.

The distinction matters because a fallback is a stopgap, and stopgaps rot. In production, a fallback that fires constantly trains reviewers to click approve without reading, which the Arthur AI guide on human-in-the-loop governance describes as worse than no gate at all, because it creates the appearance of oversight without the substance (Arthur AI). A fallback that fires rarely leaves a hole nobody notices until the incident review.

So the goal of the loop below is not better fallback coverage. It is an empty fallback: a net that has nothing left to catch because every action worth doing has been named, owned, tested and scheduled for review.

One more framing before the steps. A rule in the system prompt is text the model reads, and a prompt injection can override it; a rule at the dispatch path sits outside the model's reach. Governance only exists when the system can deny a call before side effects occur, rather than hoping the model self-restrains (NHI Mgmt Group). Everything below assumes the gate sits at dispatch.

Step 1: Capture the unlisted action from the logs

The loop starts with evidence, not a workshop. Your fallback already emits one event per catch. That event is your raw material, and it should carry more than "action blocked."

Log, at minimum: the agent identity, the requesting user, the tool name, the full arguments, the target resource, the environment (staging or production), the outcome, and the fallback reason. The Sweet Security guide to agent policy enforcement makes a similar point, that decision records should capture the requested action, agent identity, the context, the policy evaluated, the verdict, enforcement action, the timestamp and the outcome, because those fields are what make audit and tuning possible later (Sweet Security).

Then do the boring part: deduplicate. Group catches by (tool, argument signature, environment). A month of fallback events usually collapses to a dozen distinct signatures. That shortlist is your work queue, and it is finite.

Two filters save time:

  • Reads first. Read-only actions caught by the fallback are almost always a misconfigured allow list, not a policy question. Classify them, permit them, and move on.
  • Sensitive reads get redaction, not approval. A read of secrets or customer records should return a masked result rather than queue a human, which the NOFire guide to runtime policy enforcement spells out in its action tier table (NOFire AI).

What you are looking for is the third bucket: writes that recur. Those are the ones the fallback was never designed to handle indefinitely.

Step 2: Name the rule and give it one owner

A named rule has five fields, and a rule missing any of them is a wish:

FieldWhat it holdsBad exampleGood example
NameA stable identifier referenced in logs and tests"refund policy"refund.issue.under_200_usd
ConditionDeterministic fields the gate evaluates"reasonable amounts"amount_usd <= 200 AND currency == "USD"
DecisionOne of allow, deny, mask, require approval"review"allow
OwnerOne named person, not a team alias#platform-securityNamed engineer plus a backup
Review dateWhen the rule is re-examinednoneA fixed date, no more than a quarter out

The owner field is where most rule sets fail. A rule owned by a team is owned by nobody; when it misfires, everyone assumes someone else is watching. Assign one person and one backup, the way GitLab requires named approvers with specific roles on protected branches rather than an open call for review (GitLab docs).

Write the condition against structured fields only. The AI SDK policy documentation draws the line clearly: numeric thresholds, scopes, allow lists and time windows are strong fits for a policy engine, while "do not use bad language" is a weak fit that is hard to verify and easy to bypass (AI SDK). If you cannot point at a field in the tool-call input and write an exact check on it, the rule is not ready; keep the fallback for that case and revisit.

Here is a rule that would replace a recurring fallback, in Rego:

package agent.call

# Anything unmatched is denied, not allowed.
default decision := {"decision": "deny", "reason": "no rule covers this action"}

decision := {"decision": "allow"} {
  input.tool.name == "billing"
  input.args.operation == "refund"
  input.args.amount_usd <= 200
  input.args.currency == "USD"
  input.args.environment == "production"
}

decision := {"decision": "requires-approval", "reason": "refund above auto limit"} {
  input.tool.name == "billing"
  input.args.operation == "refund"
  input.args.amount_usd > 200
}

Note the default. The AI SDK package normalizes "no matching rule" to allow unless you set a default deny, which is the single most consequential line in any policy file you will write (AI SDK). Start from deny, then carve out the permits you can justify.

Step 3: Write the test that fails until the rule exists

This is the step teams skip, and it is the only one that keeps the loop honest. Before the rule ships, write a test case for the exact catch you pulled from the logs. The test asserts the decision the new rule should produce. It fails today because the fallback fires instead.

That inversion does two things. It proves the rule addresses the real action rather than a plausible one, and it gives you a regression test for the next time someone edits the policy. The AI SDK documentation explicitly calls out that policies can be tested in CI with opa test, independent of the SDK (AI SDK). Treat policy as a reviewable artifact with its own test suite, not a function buried in agent setup.

A workable test table for one rule:

CaseInputExpected decisionWhy it exists
Small refund, productionamount 50 USDallowThe common case the rule unlocks
Large refund, productionamount 900 USDrequires-approvalThreshold holds
Small refund, wrong currencyamount 50 EURdenyCurrency guard holds
Unknown tooltool crm, deletedenyDefault deny holds
Refund in stagingamount 50 USD, stagingdenyEnvironment guard holds

Write at least one negative case per rule, the "this should still be stopped" case. Rules that pass every test are usually rules that permit too much.

Step 4: Shrink the fallback and set a removal date

A new rule does not close the gap by itself; you have to remove the fallback's claim on that action. Otherwise the catch keeps firing and you have added a rule on top of a net that still catches it.

After the rule ships, watch the fallback events for that signature. If the rule is working, the signature drops to zero. If it does not, the rule's condition is wrong, not the fallback. Give every new rule a checkpoint: if its signature still appears in fallback logs after a set window, the owner reopens the rule.

The same discipline applies to the fallback itself. Put an expiry date on the catch-all path. Anthropic's published framework for trustworthy agents, cited in the Scalekit write-up on approval gates, recommends agents prefer reversible actions and request narrower scope when reversibility is unclear (Scalekit). A fallback that survives its expiry date without review is the clearest signal that governance has stalled.

Where the fallback is doing real work and you cannot yet name a rule, make it safety-preserving rather than silent. If no approver responds within the window, deny by default. An approval that times out into execution is a gate that does not exist (Arthur AI).

A worked example: refunds over a threshold

Take a support agent that issues refunds. The original policy is a fallback: any refund not on the allow list needs approval. Logs show 340 catches in a month. Dedupe collapses them to four signatures.

Signature A: refunds under $200 in USD. 310 catches. This is not a policy question; it is a missing threshold. Name the rule refund.issue.under_200_usd, allow, owner the payments engineer, review date one quarter out. Test cases from the table above.

Signature B: refunds between $200 and $2,000. 24 catches. Name the rule refund.issue.under_2000_usd, require approval from the on-call support lead, with the approval expiring in one business day. This is where the human actually adds value, so keep the gate narrow and put the evidence in front of the approver: amount, order, customer tier, prior refunds.

Signature C: refunds in a non-USD currency. 5 catches. Deny for now, with a named owner and a note that multi-currency is a separate project. Deny is a legitimate rule: it is explicit, testable and visible.

Signature D: one-off refunds to a large enterprise account. 1 catch. Leave the fallback in place and schedule a review.

The arithmetic is the insight. After one loop, the fallback stops catching 311 of the 340 actions it used to see, and the 29 remaining catches are all cases where a human decision is genuinely correct. Reviewers go from rubber-stamping 340 prompts to judging 29. The gate regains its meaning, which is the whole point.

This mirrors how mature multi-level approval systems already work. Oracle's approval workflow example routes a $5,000 budget to one approver, $5,001 to $20,000 to two, and anything above $20,000 to three (Oracle docs). The structure is not novel; applying it to agent tool calls, with named owners and test cases, is.

The review cadence that keeps rules current

A rule set decays in three ways: the business changes underneath it, the agent's tool surface expands, and nobody removes anything. Cadence fixes all three if you separate the reviews.

Run a review of new fallback signatures on a short cycle, weekly is workable for active agents. Its job is intake: which catches are rising, which are duplicates, which need a rule this sprint. Run a review of existing rules quarterly. Its job is pruning: for each rule, check how many times it fired and how many times its decision was overridden. A rule that never fires is dead weight that makes the policy harder to read; a rule overridden more than a small fraction of the time has the wrong threshold.

Run a review of the whole policy after any change to the agent's tools or permissions. That is the moment a new capability appears without a matching rule, and it is exactly when the fallback catches something new.

The ownership point is what makes this work. Every rule has a name, an owner and a review date, and every decision the gate makes should record which version of the rule applied. GitLab's approval policies show the pattern in a code review setting: rules are enforced on protected branches, a maximum of five rules attach to each policy, and when rules are invalid or cannot be evaluated, approval is required by default (GitLab docs). That last behavior is the one to copy. A policy that cannot be evaluated should fail closed.

Two signals tell you the cadence is failing. The first is a fallback signature that appears in three consecutive review meetings with no owner assigned. The second is an approval queue where the median decision time drops to seconds, the rubber stamp showing up in the telemetry (ThreatLocker). Both mean the same thing: the loop stopped.

When the fallback is the right answer, temporarily

Not every caught action deserves a rule. Three categories should stay in the net, deliberately and with a label.

Novel actions. If an action has been seen once, writing a rule from a single sample risks encoding a mistake. Leave it in the fallback, tag it as novel, and revisit if it recurs.

Judgment calls. Some decisions genuinely need a person: a refund to a high-value account, a message to a regulator, anything legal or clinical. The evidence that these belong behind an approval gate is strong, and the Arthur AI guidance recommends pre-execution approval specifically for irreversible, high blast-radius actions (Arthur AI). The error is not gating them; the error is gating everything else alongside them.

Regression cover. Keep one deliberate catch-all path, default deny, for anything the policy file does not recognize. This is not the original fallback of "ask a human and proceed." It is the opposite: an unrecognized action is refused, and the refusal is logged as a defect for the next review. The NHI Mgmt Group analysis makes the case that the absence of coverage should be a security decision rather than an open door (NHI Mgmt Group).

That reframes the fallback from a queue into a sensor. It stays in the architecture forever, but its purpose changes from catching work to reporting gaps. When the report reads zero for a review cycle, you have closed the loop, and the next tool you add to the agent starts the cycle again.

If you are setting this up across several agents and need one place to route every model call while you build the policy layer, Quanta Inference exposes text, image, video, audio and embedding models through one OpenAI-compatible API key, which keeps the gateway stable while the rules underneath it change (Quanta Inference).

FAQ

FAQ

Is default-deny too strict to ship?

It is strict only on the first day. Starting from deny forces you to write permits for the actions you actually want, and those permits are the policy. The AI SDK documentation shows that a policy with no matching rule is treated as allow unless you explicitly set a default deny, so strictness is a configuration choice you make once, in one line (AI SDK).

How many rules should one agent have?

Fewer than you expect, and each one should be readable by the business owner. If the policy file needs a specialist to interpret, you have moved the gap rather than closed it. Prune rules that never fire during the quarterly review, and keep the whole set under a size a reviewer can read in one sitting, the way GitLab caps a policy at five rules so that each one stays legible (GitLab docs).

What should the approval request show the reviewer?

Enough to decide without investigating: the proposed action, the arguments, the target resource, the environment, why it was escalated, and what happens if it is wrong. The ThreatLocker guidance recommends the request identify the agent, the requesting user, the tool, the target, the data involved, the permission requested and the expected result, and that approval be narrow to that action rather than a standing grant (ThreatLocker).

Do we need the test cases if the policy engine already has defaults?

Yes. Defaults protect the unmatched case, not the matched one. A test suite proves the permit you wrote actually fires on the amounts, currencies and environments you intended, and it catches the day someone widens a threshold during an incident. Treating policy as a testable artifact separate from agent code is the core argument in the AI SDK policy documentation (AI SDK).

How do we prove the loop works to an auditor?

Show the record. For each consequential action, the log should carry the policy version, the rule name, the decision, the approver's identity and the time of the decision. That chain is what distinguishes governance from a promise of supervision, and it is how the Sweet Security guide frames the value of immutable policy decision records for audit and incident response (Sweet Security).

Sources

Sources

  1. Human-in-the-Loop Governance for AI Agents - Arthur AI
  2. Default-deny controls for AI agents: why runtime ...
  3. AI Agent Policy Enforcement: A Complete 2026 Guide
  4. How to implement runtime policy enforcement for AI agents
  5. Merge request approval policies
  6. Policy-Based Tool Approvals
  7. Human-in-the-Loop Tool Calling: Approval Gates for AI ...
  8. Using the Supervisory List Builder for Multilevel Approval ...
  9. What should require human approval when using AI agents?
About the author

About the author

Antonio Evans
Antonio Evans

Antonio Evans is the founder of Quanta Intelligence and a fractional CTO who builds HIPAA-ready AI systems for health and life-science companies. Before that he was technical co-founder of FriendsEAT for twenty years, CEO of Socialdraft, and a data engineer at IBM. Antonio lives with his dog Santino in Lisbon, Portugal.

All articles