Policy as code for AI
A policy exported to another engine covers matching, not the enforcement around it
every questionOn this page
Why the policy language should be boring
There is a strong pull towards an expressive rule language. Real policies have edge cases, somebody always wants a lookup or a bit of arithmetic, and a general-purpose expression evaluator is a week of work. Resist it, for two reasons that are worth more than the expressiveness.
The first is security. A policy engine sitting in the request path evaluates rules against input that includes model-proposed tool arguments and text an attacker wrote. An expression language in that position is a second execution surface inside the component whose job is to bound the first one. Token Observe applies the rule to its own most tempting case: effect contracts, which constrain irreversible tool calls, are deliberately not an expression language — they select JSON values by path, compare them with six bounded operators and copy them into pinned tool arguments, and they cannot run code or interpolate a template. A contract that could execute arbitrary logic would be exactly the thing it exists to prevent.
The second is testability, which is the entire subject of this guide. A rule that is a value can be hashed, so the thing a person reviewed is provably the thing that shipped. It can be diffed, so a change is legible. It can be replayed against recorded decision inputs without re-executing anything, so you can ask what last month would have looked like. And it can be compiled into another engine’s language for comparison, which an arbitrary program cannot be. Every one of those properties disappears the moment the rule contains code.
The cost is real and should be stated: there will be a rule you cannot express, and the answer will be to add a trigger to the grammar rather than to add a language. That is slower, and it is the trade this design makes on purpose.
The grammar, and the parts of it that are easy to get backwards
A rule in Token Observe is five fields. A trigger says what it fires on. An action says what happens. A scope says which subjects it selects. A mode says whether it enforces or observes. A priority orders evaluation, lower first.
There are seven triggers and they are worth knowing individually because they are the vocabulary. A tool call, optionally filtered by a tool name pattern and by matchers over the argument tree — a dot path, one of eight comparison operators, and a value — which is what lets a rule say refunds over a threshold rather than refunds. A model request, optionally filtered by a model pattern or by an estimated input-token count. Spend over a threshold in a window. A rate over requests, tool calls or tokens in a window. A data class, naming the kinds of personal data or secret it fires on and the direction. An injection score above a minimum confidence, optionally restricted to the sources the finding came from. And a time window in which the agent may operate.
There are five actions: block, park on a named human, redact, warn, and suspend the agent. Having more than one verb matters more than it sounds: a single-verb engine forces every rule to be an outage or a log line, and teams respond to that by writing no rules at all.
Two details are the ones that silently disable a rule. The first is direction, on a data-class trigger, which is stated from the enterprise’s point of view: outbound means data leaving your control, which is the direction that matters for preventing disclosure, and inbound means data arriving back, which is the direction that matters for catching an injected instruction. Getting it backwards produces a rule that never fires and reports nothing, and the mistake is invisible in the console. The second is scope: an empty scope means global, and a scope that names an agent id, a team or a tag selects on any of them — with team matched case-insensitively and tag matched case-sensitively. That asymmetry is deliberate and worth knowing before you name things, because to a kill switch two spellings of a team are the same team, while to a policy two spellings of a tag are two different tags.
{
"name": "Refunds over 200 need a human",
"mode": "shadow",
"priority": 20,
"scope": { "tags": ["payments"] },
"trigger": {
"kind": "tool_call",
"toolPattern": "payments/issue_refund",
"argMatchers": [{ "path": "amount", "op": "gt", "value": 200 }]
},
"action": { "type": "require_approval", "approvalTtlMinutes": 60 }
}
// hashable, diffable, replayable, promotable by digest
// no expression to evaluate over attacker-influenced inputThree tests, at three different costs and answering three different questions
The cheapest test answers does this rule fire on this one case. A dry-run endpoint takes a hand-typed sample request and reports whether the rule matches and what it would do. It is the right tool while authoring, it catches the direction mistake and the pattern typo, and it tells you nothing whatever about production.
The middle test answers what is this rule doing to real traffic right now. Observation mode evaluates the rule exactly as an enforcing rule would, records the match on the trace with the policy, its action and why it matched, and then skips it so the request proceeds untouched. It is forward-looking, it costs a day or a week of waiting, and it is the only way to see traffic that has not happened yet. On the streaming path the same discipline applies through an observe-only scan over the same safe boundaries, so a dry run stays distinguishable from an outage.
The expensive test answers what would this rule have done to last month. A replay takes a candidate rule and runs it against recorded traffic, reporting what would have changed against today’s rulebook. Token Observe’s reads only the decision inputs the pipeline already writes onto each call — the requested model, the estimated input tokens, the estimated cost, the personal-data kinds, the injection score and the heuristic names — and never the prompt text, which is a deliberate limit on how far into the payload corpus the governance layer reaches.
Use all three, in that order, and use the third specifically before a rule goes anywhere near month-end. Observation mode staged on a Tuesday tells you about Tuesdays. A replay is the only thing that tells you what the rule does to a quarterly reporting run.
Reading a replay without fooling yourself
Three properties decide whether a replay report means anything, and each one is a place where a naive rendering turns an unmeasured result into a reassuring one.
Coverage is full, partial or none, and where it is none the counters come back as null rather than zero. The distinction is the whole point: null means the recorded traffic never carried the input this rule triggers on, so nothing was measured, while zero means it did and the rule would have changed nothing. Rendering the first as the second reports an unmeasured rule as a safe one, which is precisely the mistake a replay exists to prevent.
The denominator is the count of requests evaluated, not the count scanned. Requests refused before policy evaluation — a bad credential, a permission denial, an engaged kill switch, a delegation refusal — are counted separately as unreachable and excluded, because a rule cannot be credited with stopping a request that never reached it. A report that quotes the scanned figure as its base will understate the rule’s hit rate on the traffic it actually governs.
The earliest trace actually seen is reported, which is the only place a retention-trimmed window is visible. Asking for ninety days when retention keeps thirty produces a report about thirty days, and without that field it reads as a report about ninety.
One more property is worth checking in any implementation: whether the replay reads decision inputs or re-runs detection over stored payloads. The second is more accurate and much more invasive, and it changes the answer to who can run a replay from an operator to somebody who should have a reason to read the prompt corpus.
- coverage: none → null counters
- Nothing was measured. A zero in that position would report an unmeasured rule as one that changes nothing, which is the failure this field exists to prevent.
- evaluated, not scanned
- Requests refused before policy — credential, permissions, kill switch, delegation — are excluded, because a rule cannot be credited with stopping something it never saw.
- earliest trace seen
- A window trimmed by retention is visible here and nowhere else. Without it, a thirty-day answer reads as a ninety-day one.
- decision inputs, not prompt text
- A replay that re-runs detection over stored payloads is more accurate and turns running one into an act of reading the prompt corpus.
Promotion as a separate, attributable act
Authoring a rule and deciding to enforce it are two decisions and should be two acts by two people on two occasions. The mechanism that makes that real rather than procedural is a gate: promotion into enforcement can be refused until a replay of that exact rule has been acknowledged by a named person.
The design details are where this succeeds or fails. The acknowledgement is single-use, because overwriting it would erase the name of the person who accepted the figures, which is the entire point of the record; and the counters they accepted are copied into the audit entry so a later reader need not trust that the stored report is unchanged. The gate is reported as a state a console can read — required, satisfied, the latest report, whether it is stale — so a button can be disabled rather than a 409 discovered.
The digest the gate checks covers the trigger, the action, the scope and the priority, and deliberately not the mode or the enabled flag. Including mode would make the gate unsatisfiable, since promoting is itself a change to mode. And editing a rule that is already enforcing is never blocked, for a reason worth generalising: a gate that stops an operator fixing a live rule is a gate they turn off.
The gate itself is off by default, because it is a process control and imposing one mid-upgrade would block a change already in flight. That is the right default and it means somebody has to decide to turn it on — which is a small decision with a large effect on how the rulebook is treated.
Getting the rulebook between environments without a CI job holding the pen
The last mile of policy as code is promotion between environments, and the interesting requirement is that a review pipeline should be able to see the change without being given authority to make it.
Token Observe’s shape is an export that emits a locked bundle carrying one hash per resource plus a digest over the whole bundle; a plan that imports a bundle and compares declared, locked and deployed state, optionally against an observed inventory in a portable software-bill-of-materials format; and a promote that emits a target-environment artefact only if the reviewed source digest still matches. Promotion never applies changes implicitly. That separation is what lets a review process read the artefact without a build job holding write authority over the live registry.
Two properties of that comparison are worth copying. Import rejects duplicate resources, changed locks, unknown kinds and credential-shaped fields, and keeps URL paths, query strings and credential-shaped metadata write-only — so a bundle is a state and drift artefact rather than a backup of your secrets. And a component in the observed inventory whose identity matches but which carries no configuration hash is reported as unverified rather than in sync, because identity alone is not configuration evidence. That single rule is the difference between drift detection and a green tick.
Compiling to another engine deserves a caveat in the same register. Token Observe can emit a policy-matching artefact with witnesses for an external evaluator, and it lists every source policy it rejected or could not represent — which is the honest half. The caveat is that the emitted artefact covers policy scope and trigger matching only. It is not a replacement for the permission model, the ceilings, the approval workflow, the kill switch or the effect authority, all of which live in the enforcement point rather than in the rule. A compiled rulebook that is treated as the whole control is a rulebook that has quietly shed four-fifths of the enforcement it was part of.
How to put policy as code for AI into practice
- 01
Write rules as data in a closed grammar
A fixed set of triggers and actions, scope as identifiers, no arbitrary evaluation anywhere in the path. Add a trigger when you need one rather than adding a language. - 02
Check the direction and the scope before anything else
Outbound is data leaving your control and inbound is data arriving back; getting it backwards silently disables the rule. Remember that team matching is case-insensitive and tag matching is not. - 03
Dry-run one sample while authoring
It catches the pattern typo and the direction mistake in seconds, and it tells you nothing at all about production traffic. Treat it as a syntax check rather than as evidence. - 04
Stage in observation mode over live traffic
Evaluated exactly as enforcement, recorded on the trace with the policy, the action and the reason, then skipped. Leave it long enough to see the traffic shapes you actually care about. - 05
Replay against recorded traffic before month-end matters
Observation mode tells you about the week you ran it. A replay is the only thing that tells you what the rule does to a quarterly reporting run you have already had. - 06
Read the replay for coverage, denominator and window
Null counters mean nothing was measured; the base is what was evaluated rather than scanned; and the earliest trace seen is where a retention-trimmed window becomes visible. - 07
Promote as a separate act by a named person
A single-use acknowledgement whose counters are copied into the audit entry, a digest over trigger, action, scope and priority, and no gate on editing a rule that is already live. - 08
Move rulebooks between environments by digest, not by apply
Export a locked bundle, plan the comparison, and promote an artefact only while the reviewed digest still matches. A matching identity without a configuration hash is unverified, not in sync.
Where this argument meets an implementation
Policy engine
One deterministic verdict on every governed request: allow, block, redact, or park it for a human.
Effect contracts
The action leaves once, and success is what a second pinned tool observed.
Flight recorder
Every governed request in a timeline a compliance officer can read, and a search box that never writes SQL.
Agent permissions
Deny by default, explicit deny wins, and delegation intersects — so an agent cannot borrow authority it was never granted.
Those pages are one product's implementation of what this guide argues for; describe what your agents actually do and you will get a straight answer about whether you need any of it, including when the answer is no.
Talk it throughWhy not use a general-purpose policy language?
Because a rule that is a program cannot be replayed, diffed or locked by digest, and because an expression evaluator in the request path is a second execution surface inside the component whose job is to bound the first one — evaluating over model-proposed arguments and text an attacker wrote. Token Observe applies the same discipline to its most tempting case: effect contracts select JSON values, compare them with six bounded operators and copy them into pinned tool arguments, and cannot run code or interpolate a template. The cost is a rule you occasionally cannot express, and the answer to that is a new trigger rather than a language.
What is the difference between testing a rule and backtesting it?
A test answers whether the rule fires on one sample you typed, which is a syntax check. Observation mode answers what the rule is doing to traffic arriving now, which is forward-looking and costs a week of waiting. A replay answers what the rule would have done to traffic that has already happened, which is the only one that can tell you about month-end when you staged the rule on a Tuesday. They are complements: use the first while authoring, the second before promoting, and the third before promoting anything whose blast radius includes a reporting period.
Why does a replay return null instead of zero?
Because they mean opposite things. Null means the recorded traffic never carried the input this rule triggers on, so nothing was measured; zero means it did and the rule would have changed nothing. Rendering the first as the second reports an unmeasured rule as a safe one, which is the exact failure a replay exists to prevent. The same discipline runs through the report: the denominator is what was evaluated rather than what was scanned, and the earliest trace actually seen is reported so a window trimmed by retention cannot masquerade as the window you asked for.
Should promotion to enforcement require a sign-off?
It should be available, and it should be off by default. Available, because the single most common way a policy programme fails is a rule promoted blind and an outage with a policy id attached. Off by default, because it is a process control and imposing one mid-upgrade would block a change already in flight. Two design details decide whether it works: the acknowledgement is single-use with its counters copied into the audit entry, so the record cannot be quietly replaced, and editing a rule that is already enforcing is never blocked — a gate that stops an operator fixing a live rule is a gate they turn off.
Can I keep my policies in version control and deploy them like code?
You can review them like code, and you should stop short of letting a build job hold the pen. The shape that works is an export producing a locked bundle with one hash per resource and a digest over the whole thing, a plan that compares declared, locked and deployed state against an optional observed inventory, and a promote that emits a target artefact only while the reviewed digest still matches — never applying implicitly. That lets a pipeline read and diff without holding write authority over the live registry, which is the property you actually want, and it keeps credential-shaped fields write-only so a bundle is a drift artefact rather than a secret backup.
Prefer to ask a person? Write to us →
Bring us the question this guide did not answer.
Write to hello@tenhaw.com with what your agents do, which providers they call and what would have to be true for you to put something in front of them. James Rooney replies. You will get a straight answer about whether Token Observe fits, including when it does not.
no form · no qualification step · no sales desk · the other three ways in