INLINE DECISION POINT

Policy engine

One deterministic verdict on every governed request: allow, block, redact, or park it for a human.

A Token Observe policy is a trigger, an action and a scope, evaluated inline on every governed request before the payload reaches a provider. Triggers match on the tool being called and its argument values, the model requested and its estimated input size, accumulated spend, request and token rate, the classes of sensitive data detected in the payload, the prompt-injection score and the source it came from, or the hour of day in UTC. Actions are block, require a human approval, redact, warn, and suspend the agent, resolved to a single verdict in which a block beats an approval and an approval beats a redaction. Every rule can run in shadow mode first, recording what it would have done without stopping anything, so you learn its false-positive rate before it blocks real work. Where the deployment turns the gate on, no rule may begin enforcing until a backtest of that exact rule has been replayed against recorded traffic and acknowledged by a named person.
Trigger kinds
Seven: tool call, model request, spend, rate, data class, injection, time window
Actions
Five: block, require approval, redact, warn, suspend the agent
Data classes detected
Eleven, three of them checksum-validated — Luhn, IBAN mod-97, NHS mod-11
Injection heuristics
Nine weighted patterns, scored 1.25× when the text is a tool result
Not a classifierInjection scoring is nine fixed patterns, not a model
On this page
the problem

A rule that lives in the agent’s own code is not a control

Whether an agent may put a customer’s card number in a prompt, or act on a support ticket containing the sentence “ignore your previous instructions”, is usually decided inside the agent: a regular expression in a try block, a system prompt asking the model to be careful, an amount threshold written by whoever built the refunds agent. Each of those is enforced by the thing being governed, changes whenever someone redeploys, and is invisible to the person who has to answer for it in an audit.

The second failure is the one that costs you. Injection arriving in a user’s own message is the demonstration; injection arriving in a tool result — a ticket body, a scraped page, a row from a database somebody else writes to — is the channel that actually hijacks agents, because the model reads data as instructions and that data was written by whoever filed the ticket. A guardrail that only inspects what a user typed does not see it at all, and neither does one that inspects the text after the model has already read it.

Then there is the control that breaks the business. Most policy engines let you write a rule and switch it on, and the first thing you learn about its false-positive rate is which team’s work stopped. That is why a rule here can be staged in shadow mode, and why the backtest that replays it against recorded traffic reports “unknown” rather than “zero” whenever the recorded evidence cannot answer the question — a zero reads as safe to promote, and the promotion is the outage.

the mechanism

How it actually works

Step 6 of the request path. The order is load-bearing rather than incidental: it is encoded in the evaluator, and the reason each step sits where it does is the reason the guarantee holds.

  1. 01

    Sanitise, before any detector reads the payload

    Every piece of model-visible text is normalised first: the Unicode tag block, which encodes a complete invisible ASCII alphabet, plus zero-width characters, bidi overrides and isolates, the soft hyphen, the invisible maths operators, the supplementary private-use planes and orphaned surrogate halves. The pass loops to a fixpoint, up to four times, because stripping one layer can reveal another. The zero-width joiner is deliberately left in place: emoji sequences need it.
  2. 02

    Scan each fragment under its own source

    Eleven PII and secret detectors and nine injection heuristics run over the sanitised text — the system prompt, every message block, tool results, tool-call arguments, tool definitions and their JSON schemas, and vendor passthrough fields, because a prompt smuggled into an unknown top-level field is read by the model exactly like one in the messages array. The highest injection score across fragments is taken rather than the average, so the multiplier that makes a tool result score higher than the same words from a user is not diluted by the rest of the payload.
  3. 03

    Select the rules that apply

    A policy is enabled or not, and its scope selects agents by id, by team or by tag; a policy with an empty scope is global. The applicable set is sorted by priority ascending — lower number first — and every rule in it is evaluated. Priority decides whose reason is reported and which redaction mode wins, not which rules are consulted.
  4. 04

    Decide, first hard failure wins

    An engaged kill switch beats everything, then agent lifecycle status, then deny-by-default permissions including any delegation chain, then the agent’s own budget and rate ceilings. Only then are policies evaluated, and the verdict is one of allow, block or require approval. A shadow-mode match is recorded and skipped; among enforcing matches, the first block or suspend wins, an approval requirement is taken from the first rule that asks for one, and redaction kinds accumulate across every rule that redacts.
  5. 05

    Enact, or refuse rather than rewrite

    On allow, the redaction plan is applied to the outbound payload and a redaction event records kinds and counts, never values. Two cases are refused instead of rewritten: sensitive data inside a JSON object key, because renaming an executable key changes which argument a tool receives, and sensitive data found by OCR inside an image, because text redaction cannot reach pixels. On block, a typed error is returned and the trace closes as blocked.
  6. 06

    Govern what comes back

    Data-class rules are evaluated again on the response leg — after the full response on a buffered call, and before the first byte on a streamed one, because bytes already written cannot be recalled. Tool results returning through the MCP gateway are sanitised, scanned as tool results and evaluated against data-class and injection rules only, so a tool-call or rate rule does not fire twice for one logical call. Credentials are masked on the way out whether or not any policy asks.
  7. 07

    Record the decision, including the one not taken

    Every match — enforcing or shadow — appends a policy decision event to the trace carrying the policy id, its name, its mode, its action and the detail line that explains why it matched. A shadow policy nobody can see is not a dry run; it is a rule that does nothing.

What a policy can match on

Seven trigger kinds, and no eighth. There is no free-text trigger: you cannot write a rule that blocks any prompt containing a particular phrase, because the payload conditions available are the ones the detectors and the scanner produce.

Scope is the other half of matching. Agent ids, team names and tags each select, and an empty scope is global. The comparison is deliberately asymmetric — team names are compared case-insensitively because a human types them under incident pressure, tags are compared case-sensitively — and the backtest coverage report names that asymmetry explicitly, because a scope of tags PCI selects nothing when the agents are tagged pci and the resulting report would otherwise read as a clean zero.

Tool and model patterns are wildcards, not regular expressions: an asterisk matches any run of characters including a slash, matching is case-insensitive, and every other metacharacter is escaped, so a pattern cannot become a regular expression by accident. The shipped refund rule matches the pattern rather than a single server for a reason the source states plainly: the action is what matters, not who hosts it, so it catches an issue_refund on the order database and on the payments service alike.

tool_call
A tool name pattern plus any number of argument conditions, all of which must hold. Each condition is a dot path into the arguments and one of eight operators — greater than, at least, less than, at most, equals, does not equal, contains, or matches a regular expression. Contains and regex comparisons are case-insensitive.
model_request
A model name pattern, optionally with a maximum estimated input token count above which the rule fires. The shipped example warns above 250,000 input tokens, on the reasoning that this is usually a context leak or an unbounded retrieval loop rather than a real need.
data_class
One or more of the eleven detected kinds, with a direction stated from your organisation’s point of view: outbound is a prompt on its way to a provider or arguments on their way to an external tool, inbound is a completion or a tool result arriving back, and a rule can take both. Getting the direction backwards silently disables the rule, which is why the frame is fixed in one place.
injection
A minimum confidence between 0 and 1, optionally filtered to findings from particular sources: a user message, a tool result, or the system prompt. A rule scoped to tool results only is how you police indirect injection without blocking the person typing into your support console.
spend and rate
Accumulated spend over roughly an hour, a day or a month against a USD threshold; or requests, tool calls and tokens per minute against a ceiling. These read the same windows the pre-flight budget check reads, so a warn rule can sit below a hard ceiling and flag an anomaly before work stops.
time_window
An allowed start and end hour in UTC, wrapping across midnight when the end is earlier than the start. The rule fires outside the window, not inside it.
The refund rule Token Observe ships with, as stored.
name       Refunds over £200 require human approval
priority   10
scope      {}                          // empty scope: global
trigger    tool_call
             toolPattern  '*issue_refund'
             argMatchers  [{ path: 'amount', op: 'gt', value: 200 }]
action     require_approval, approvalTtlMinutes 60
mode       enforce

What a policy can do about it

Five actions, resolved to one verdict. Every applicable rule is evaluated, and then the outcomes are combined rather than the first one winning outright: a block anywhere in the set blocks, an approval requirement is honoured only if nothing blocked, and redaction kinds from every redacting rule are unioned into a single plan. The redaction mode is taken from the first enforcing rule that redacts, in priority order, and defaults to masking when that rule does not name one.

Redaction has two modes and one exception. Masking replaces the value with an irreversible marker naming the kind. Tokenising swaps it for a stable placeholder — every occurrence of the same value gets the same placeholder everywhere in one governed call, the prompt on the way out and the response on the way back alike, so the model can still treat a customer, an account and a card as distinct entities it can refer back to, without ever being given any of them. The map that does that is built per call rather than stored, so the number after the kind is assigned by order of first appearance within the payload; because a chat request carries the whole prior transcript with it, the placeholders stay coherent across the conversation as the client replays it, but nothing is remembered between calls. The exception is credentials: API keys, AWS access keys, JWTs and private keys are always masked irreversibly, even in tokenise mode, because a reversible placeholder for a live credential is a credential leak with extra steps.

The same masking runs on the way back. Credential kinds are masked in model output whether or not any policy asks, and the egress plan is a union with the policy’s kinds rather than a replacement — a defect found and fixed in this build was exactly that: enabling card-number redaction had switched off the unconditional credential masking, because the plan replaced the secret kinds instead of adding to them.

Where a rewrite would change meaning, Token Observe refuses instead. The IBAN detector writes its separator before each group character rather than after, because ending a match on a space swallowed the space that followed the IBAN and the redacted prompt reached the model as “<IBAN_1>for the payout” — a silent edit to what the prompt said. Sensitive data in a JSON object key is refused rather than renamed, because the key is an executable contract identifier. Sensitive data found by OCR inside an image is refused rather than declared masked, because text redaction cannot reach pixels.

block
A typed error, a trace closed as blocked, and nothing sent upstream. The reason names the policy and the detail line that explains the match.
require_approval
The request stops and an approval record is created with a time-to-live, sixty minutes by default. The approval is bound to a SHA-256 of the canonicalised payload and is single-use, so it cannot authorise a different action or the same action twice.
redact
Mask or tokenise. The plan carries the kinds, and the trace records kinds, counts and how many placeholders were issued — never the matched value, which would move the leak from the provider into the flight recorder.
warn
The request proceeds and a warning is recorded and published as an event. This is the action to reach for below a hard ceiling: the shipped rule warns on an agent spending more than $25 in an hour rather than stopping it.
suspend_agent
Blocks the request and takes the agent out of service, writing the status change into the audit log. If the status write fails the block still stands: losing the suspension is serious, but not a reason to let the request through.
Tokenise and mask, in one governed call carrying two turns.
in    turn 1  card 4111 1111 1111 1111, key sk-ant-api03-XXXX…
      turn 2  the card 4111 1111 1111 1111 again

out   turn 1  card <CREDIT_CARD_1>, key [REDACTED:API_KEY]
      turn 2  the card <CREDIT_CARD_1> again

// one call, one token map: the same value takes the same
// placeholder everywhere in the payload and in the response.
// The map is not carried between calls, and credentials are
// never given a reversible placeholder at all.

Prompt injection, on prompts and on tool results

Injection scoring is nine weighted patterns run over sanitised text, with the weights summed, multiplied by 1.25 when the source is a tool result, and capped at 1. The multiplier is the whole point: a directive inside a tool result is more suspicious than the same words typed by a user, because the user of a tool result is data rather than a principal. A tool-result directive scores 0.4 on its own and 0.5 arriving from a tool, so a rule set at a minimum confidence of 0.5 fires on the ticket body and not on the person typing into your console.

Sanitisation runs first, and that ordering is load-bearing rather than tidy. A scanner reading un-normalised text is looking at a different document from the one the model will read, which is the entire ASCII-smuggling attack: the Unicode tag block encodes a complete invisible alphabet, so the word “hello” can be written in characters no reviewer and no naive pattern will see. Normalising to a fixpoint first means every downstream detector, and every human reading the trace, sees what the model actually receives.

The scan is inline, synchronous and running on attacker-controlled text, so its cost is a security property. Node cannot interrupt a running regular expression: one pattern that backtracks super-linearly stalls every other request sharing the process. Two such patterns were found in this build. The reported one took 121 ms on a 200 KB input; auditing the rest found a worse one, the markdown exfiltration pattern, at 51 seconds on the same input — a single-request denial of service. Both were rewritten to be linear, a 64 KB scan cap was added, and every adversarial 200–400 KB input was re-measured at under 4 ms. Every pattern added since must be linear in the length of its input, and the constraint is written into the file that holds them.

The 64 KB cap is a deliberate trade. An injection payload has to be read by the model to work, so anything worth detecting is already in the first 64 KB; beyond that a caller is only buying scan time that is charged to every other request on the process. Text past the cap is not scanned.

unicode_tag_smuggling · 0.8
Characters from the Unicode tag block, U+E0000 to U+E007F — an invisible instruction channel. The highest-weighted pattern, because there is no legitimate reason for it to be in a prompt.
exfiltration_markdown · 0.7
A markdown image pointing at a URL with data-bearing query parameters: the classic channel for getting a secret out of a context window by making the renderer fetch it.
instruction_override · 0.6
Ignore, disregard or forget, within a bounded distance of previous, prior, above, all or earlier, within a bounded distance of instructions, prompts, rules or context. The spans are bounded because an unbounded one is the denial-of-service vector.
tool_result_directive · 0.4
An imperative addressed to the model inside data — “IMPORTANT … you must call …”. Weighted lower on its own, and the pattern the 1.25× tool-result multiplier was written for.
large_base64_blob · 0.3
An unbroken 256-character base64 run: obfuscated content, weighted low because plenty of legitimate payloads contain one. It contributes rather than decides.
and four more
System-prompt probing, role reassignment to an unrestricted persona, fake system and admin control markers, and runs of three or more zero-width characters. Three is the floor so that a stray byte-order mark is not a finding.

Shadow mode, and what it does not tell you

A policy in shadow mode is evaluated exactly as an enforcing one is, and then its match is recorded and skipped. The request proceeds untouched, and the trace carries a policy decision event naming the rule, its mode, its action and why it matched — the same event it would carry had the rule been enforcing. That is how you learn a rule’s false-positive rate before it starts blocking real work, and it is why a new policy created in the console defaults to shadow and why the shipped out-of-hours rule for finance agents ships in shadow rather than enforcing.

Shadow works on both legs and on both transports. A shadow data-class rule bearing on a model response is observed through the same boundary-safe hold-back buffer as an enforcing one, so the observation is made over the same text; its classes are simply never added to the mask set, and at the end of the stream each shadow rule that actually matched writes a payload-free decision event. Buffered and streamed responses use the same event shape, so policy reporting does not depend on which transport an SDK happened to choose.

What shadow mode does not give you is the counterfactual payload. A shadow redact rule is skipped before any redaction plan is built, so you learn that it matched and which kinds it would have covered — you do not get to see the rewritten prompt it would have produced. If what you need to review is the rewrite rather than the match, enforce the rule against a scoped test agent instead.

The other honest gap is coverage. Shadow mode only tells you about traffic that arrives while it is running, so a rule staged on a Tuesday says nothing about month-end. That is the question backtesting answers, and the two are complements rather than alternatives: one is forward-looking against live traffic, the other is retrospective against recorded traffic.

Backtesting before a rule is allowed to enforce

A backtest replays a candidate rule against recorded traffic and reports what it would have changed. It runs the real evaluator twice per request — once with the baseline rulebook, once with that same rulebook plus the candidate forced to enabled and enforcing — and reports the delta. The candidate is removed from the baseline by id first, so backtesting a rule that is already stored compares it against the estate without it rather than against itself. Because the candidate is then the only difference between the two runs, every difference in outcome is attributable to it, without the replay re-implementing a line of the decision path.

Three properties make it a control rather than a report. First, a reachability gate: a request stopped by a kill switch, a dormant agent, a permission or delegation denial, a rate limit or a budget never reached policy evaluation at all, so it is excluded from the denominator rather than counted as traffic your rule left alone. Second, per-trigger coverage as a first-class field, with null counters rather than zeros whenever the recorded evidence cannot answer — null is a question, zero is a false answer. Third, a digest that survives promotion, so the evidence you gathered on a shadow rule is still valid when you flip it to enforce.

The digest covers exactly what the rule does: its trigger, its action, its scope and its priority. Mode and enabled are excluded deliberately, because a digest that changed the moment you flipped shadow to enforce would invalidate the very backtest being used to authorise the flip — an unsatisfiable gate. Priority is covered, because re-ordering a rule changes which other rules shadow it, which is exactly what a backtest measures.

Where a deployment sets the flag, the promotion gate has two halves and is only a gate with both. One refuses a shadow-to-enforce or disabled-to-enabled promotion without an acknowledged backtest of the policy as it will be after the update, so patching the trigger and flipping to enforce in one request cannot borrow the acknowledgement of the rule that was tested. The other refuses to create a rule that is already enforcing — without it, an operator refused a promotion could delete the rule, post it back as enforcing, and be enforcing untested in two requests. Authoring stays unrestricted: a shadow rule and a disabled rule enforce nothing, so both are created freely. The gate is off by default, because imposing a process control on an existing install mid-upgrade would block a change already in flight.

Acknowledgement is a human act, not a checkbox on a report. It is single-use, it records who accepted the result, and the audit entry copies the coverage level and the impact figures that were accepted, so a later reader does not have to trust that the stored report is unchanged. A report whose coverage came back as none is still acknowledgeable — that is a person explicitly accepting “impact unknown”, which is a decision someone’s name should be on, and a different act from having measured a safe result.

What is replayed
Only the persisted governance feature vector: the requested model, estimated input tokens, estimated cost, the PII kinds found, the injection score and the heuristic names. Never prompt text — the recorder is forbidden from keeping it, which is precisely what makes a faithful replay possible without a payload store.
Coverage none, not zero
A rule testing tool-call arguments, a rule filtering injection findings by source, a rule governing the inbound response leg, or a rule whose scope selects none of the scanned agents all return null counters and a plain-English reason. Each is a case where replaying would make the rule look quieter than it will be in production.
Coverage partial
Spend and rate rules never score better than partial, because the windows are rebuilt from the traces inside the scan: spend incurred just before the window opens is invisible and the earliest requests look cheaper than they were. Impact is reported as a lower bound and says so.
Dead code and break rate
The report says whether an existing higher-priority rule already decides everything the candidate matched — the rule is inert — and gives the break rate as newly blocked plus newly gated over evaluated. That is the will-this-break-me number, and it is the question a single hand-typed dry run structurally cannot answer.
Bounds
Twenty thousand traces, a thirty-day default window, a ten-second wall-clock budget, and at most twenty worked examples returned as trace ids and verdicts rather than payloads. The report names the earliest trace it actually scanned, so a retention-trimmed window cannot masquerade as a full one.
A report that answers nothing, and says so rather than saying zero.
coverage              none
impact.newlyBlocked   null      // not 0
impact.newlyGated     null
breakRate             null
deadCode              null

coverageReasons       this rule tests tool-call arguments, and
                      arguments are not replayable: the model-
                      gateway path never persists them, and the
                      MCP path persists them only after redaction
                      and truncation. Impact is unknown, not zero —
                      stage this rule in shadow mode and read the
                      shadow matches instead.
the limits

What this does not do

Stated here rather than discovered during an evaluation. Every line below closes off a reasonable assumption a reader would otherwise carry into a proof of concept.

  • Injection detection is nine fixed weighted patterns, not a classifier and not a model. A novel phrasing that matches none of them scores zero, and a rule with a minimum confidence never fires on it.
  • The eleven data classes are the detectors written into the code. A policy chooses which of them to match on; it cannot add a twelfth, and there is no free-text trigger that blocks a prompt for containing a particular phrase.
  • Redaction rewrites text. It does not rewrite a JSON object key or the pixels of an image — Token Observe refuses those requests rather than changing an executable contract or claiming to have masked something it cannot reach.
  • A backtest cannot replay tool-call arguments. They are never persisted on the model-gateway path and are persisted on the tool path only after redaction and truncation, so a rule with argument conditions reports coverage none rather than zero impact.
  • None of this governs an agent that does not route through Token Observe. A model call made straight to a provider is not sanitised, not scanned and not policed; finding it is the Shadow-AI radar’s job, not the policy engine’s.

If one of those limits is the thing that decides it for you, say so and you will get a straight answer about whether it is on the roadmap or out of scope.

Talk it through

What can a Token Observe policy match on?

Seven trigger kinds: a tool call, matched by name pattern and by conditions on its argument values; a model request, matched by name pattern and estimated input size; accumulated spend against a USD threshold; requests, tool calls or tokens per minute; the classes of sensitive data detected in the payload, with a direction; the prompt-injection score, optionally filtered to findings from a user message, a tool result or the system prompt; and the hour of day in UTC. Each rule is scoped to agent ids, teams or tags, and a rule with an empty scope applies fleet-wide.

Does Token Observe use a model to detect prompt injection?

No. Injection scoring is nine weighted regular expressions run over sanitised text, summed, multiplied by 1.25 when the source is a tool result, and capped at 1. There is no machine-learning dependency and no semantic understanding, which is both the reason it is fast enough to run inline on every request and the reason it will miss a phrasing nobody wrote a pattern for. Treat the score as one signal among several rather than as a verdict, stage injection rules in shadow mode first, and read the heuristic names recorded on the trace when one fires.

Why are tool results scored higher than user messages?

Because that is the channel that actually hijacks agents. A directive in a user message comes from a principal you can identify; a directive in a tool result comes from data — a ticket body, a scraped page, a database row written by someone outside your organisation — and the model reads it as instruction. Token Observe scans each fragment under its own source and multiplies a tool result’s score by 1.25, so a directive scoring 0.4 from a user scores 0.5 from a tool, and a rule set at 0.5 catches the indirect case without blocking the person typing into your support console.

What is the difference between masking and tokenising?

Masking replaces the value with an irreversible marker naming the kind it was. Tokenising replaces it with a stable placeholder that stays the same for that value everywhere in one governed call — the outbound payload and the response leg share a token map — so the model can still reason about the entity without ever receiving it. The map is per call rather than persisted, so coherence across a conversation comes from the transcript being resent with each request rather than from anything Token Observe remembers. Credentials are the exception and are always masked irreversibly even when a rule asks for tokenising, because a placeholder that can be swapped back for a live secret defeats the point of catching it.

What does shadow mode record, and what does it miss?

A shadow rule is evaluated exactly as an enforcing one, then its match is recorded and skipped: the trace gets a decision event naming the policy, its action and why it matched, and the request proceeds untouched. It works on the request leg, on buffered responses and on streams, using the same event shape throughout. What it cannot give you is the counterfactual payload — a shadow redact rule is skipped before any redaction plan is built, so you learn which kinds it would have covered but never see the rewritten prompt. For that, enforce the rule against a scoped test agent.

What does a policy backtest actually prove?

That the candidate rule, run against your own recorded traffic, would have newly blocked or gated a stated number of requests from stated agents — or that the recording cannot answer the question. Requests that never reached policy evaluation are excluded from the denominator rather than counted as safe, and where a trigger depends on something the recorder does not persist, the counters come back null rather than zero. Where the deployment sets the flag, a rule cannot start enforcing until a backtest of that exact rule — trigger, action, scope and priority — has been acknowledged by a named person.

Ask about this capability
Ask how this one actually works, where it sits in the request path, or what it will not do. Answers stay inside what this page claims.

Prefer to ask a person? Write to us →

get in touch

Bring us the agent you are least comfortable with.

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