Concepts and glossary
Every term the dashboard and the evidence page use, and — more usefully — what each one would look like if something were wrong.
Session
One agent run. The vendor opens it when the run starts, and closes it when the run ends; closing freezes the chain head. A session carries the agent, its version, the principal it ran for, and a hash of the original request — never the request itself.
Principal
Who the agent ran for: human or service, plus an opaque reference like user_881. An email or a phone number is rejected at the door with a 400 — storing a person, rather than a reference to one, is the thing Heron promises not to do.
Action
One tool call an agent is about to make, announced through the before-action hook. It carries the tool name, a hash of the full arguments, a redacted rendering of them, and its sequence number within the session. The arguments themselves never reach Heron.
Decision
Heron’s synchronous answer to the hook, judged against the active policy and signed with our Ed25519 key — ALLOW, DENY or STEP_UP, and enforced: the vendor honours it before it executes. It fails closed (no answer, or no policy Heron can load, means DENY), and a decision carrying the reason break_glass is one made while enforcement was deliberately suspended.
Recompute basis (classification)
The verdict is enforced on the hot path; alongside it Heron signs the classification — the action placed along a fixed cascade (operation, data class, destination, magnitude, reversibility) that the rules were applied to. That signed classification is what lets a reviewer re-run the published rules and reproduce the enforced verdict: they cannot recompute the classification themselves, because it reads signals we never publish. It is deliberately notan intent checker — it reads the tool name and the vendor’s signals, never the request behind them.
Policy
The rules the enforced verdict is judged against. Heron ships a managed set enabled by default — reads are allowed, moving money and irreversible deletes are denied, a send we cannot confirm is internal asks for a human — and a vendor turns rules off from Settings → Policy. The enabled rules form an immutable bundle, and its hash is stamped on every decision receipt.
Two properties make the policy half of AARM R3 verified rather than vendor-declared:
- Every rule is published in full — its text, its version, the combining algorithm (
deny-overridesover an implicit deny) and the thresholds it reads. So a reviewer can recompute our verdict from the bundle: the evidence page has a Recompute button that re-runs the exact same engine in your browser, and the offline verifier does the same from the command line. If our signed verdict does not come back, R3 goes red. - Turning a rule off is an event, not an edit. It is a signed entry in the operator logwith a reason, published beside the rule as the vendor’s own claim. Old receipts keep the hash they were judged under; disabling a rule never rewrites history, and disabling every rule drops R3 to a gap rather than flattering it.
What it still cannot do: the classificationis Heron’s statement about a call — derived from the tool name and the signals the vendor computes, some of which we cannot check. You can verify we applied our own published rules; you cannot verify the classification was true. And where a class is uncertain, the verdict is STEP_UP (a human decides), never a guess in our favour.
Tool catalogue
The vendor’s signed statement of what each of its tools is— the operation it performs, the class of data it touches, where it sends, whether it can be undone. One statement per tool, sent once per deploy, signed with the vendor’s own key and published here in full.
It exists because of what happens when it is missing. Where the vendor states nothing, Heron works the dimension out from the tool’s name and publishes it as derived — which reads as an independent inference and is not one: the name is a string the audited party chose, and nothing binds it to what the tool does. A catalogue is the same knowledge said under a key, with a date on it. It does not make the claim true; it makes being wrong something you can find rather than something only we could have noticed, and a rename becomes an event rather than a silent change of verdict.
Only facts that hold for every call belong in it. A recipient count or an amount is a fact about one call and travels with that call, where it always wins over the catalogue.
Two numbers on the evidence page are worth reading together. Tools described says how much of what actually ran the catalogue in force covers, and separates a tool it lists and states nothing about — the vendor saying it has no constant fact to offer — from one it never mentions, which is an omission. The signal-source table beside it says the same thing per dimension: declared is testimony about that one call, catalogued is this statement about the tool, derived is us reading a name.
There is a fourth, and it is the one to read hardest. inferredis an answer a language model on the vendor’s side gave — testimony rather than a measurement, and on the wire indistinguishable from one, which is why only the vendor can mark it and why the mark is a separate column rather than a footnote. A model is asked only where nothing else determined the dimension, so it can resolve an unknown and can never overturn what the catalogue stated or what the tool name implied. Each such classification names the model, the hash of the question put to it, and how much of the conversation it saw.
Execution evidence
The vendor’s own signed statement of what it did with a decision: EXECUTED, SKIPPED, FAILED, BLOCKED, ESCALATED, ABANDONED (started, and it stopped waiting) or NOT_ATTEMPTED(never put into the world, for a reason of its own), plus a hash of the result. Signed with the vendor’s key, whose private half Heron never holds — so we could not forge it, and they cannot later disown it.
Evidence whose signature does not verify is stored, not rejected: refusing the request would delete the evidence that something is wrong. It becomes an INVALID_VENDOR_SIGNATURE finding.
Receipt
A signed payload — a decision, a classification, or an operator action — where the signed bytes are the RFC 8785 canonical form of the payload shown on the page. Nothing is derived from the database at verification time, which is what lets a stranger recompute the bytes and check them against our public JWKS offline.
Receipts are immutable, and a public key is never removed from the JWKS: dropping one would silently invalidate every receipt it ever signed.
Hash chain
Each action is linked to the one before it inside its session:
head₀ = SHA256( JCS(session_genesis_record) ) record_hash = SHA256( JCS(action_record) ‖ prev_hash_bytes ) head = record_hash of the last action
The chain is recomputed from the stored rows, not trusted from a stored hash. That is the whole point: edit a row in the database after the fact, and the recomputation stops matching the hash we signed at the time — BROKEN_CHAIN. The vendor may also echo the prev_hash it believes it is extending; a mismatch means it dropped an event or is replaying against a stale head.
Reconciliation
A background job that pairs our decision with the vendor’s execution evidence once the window (default 60 seconds) expires, and again whenever late evidence lands. Every disagreement it finds becomes an anomaly. This is the product: not the signing, not the dashboard — the comparison.
Anomalies
Ten kinds, each with a fixed severity. They are raised idempotently per action, so a reconciliation that runs twice does not double-report.
Issues — the grouped reading
An anomaly is one event: this call, at this second, did not reconcile. The dashboard’s Issues section groups them by the problem they are instances of — one issue per kind — and shows how often it fires, how many agent runs and how many tools it spans, and when it was last seen. A guard that logs verdicts instead of obeying them raises one finding per call; without grouping, a single BROKEN_CHAIN sits four hundred rows below it.
The tool is deliberately not part of that grouping. One bug in one guard reaches every tool the vendor exposes, so keying by it would report a single problem as forty rows, each too small to act on. Which tools — and which agents, environments, verdicts and rules — is answered underneath, as distributions over the issue’s own events, and each value narrows the findings below it to that value.
Nothing about an issue is stored or configured: it is recomputed from the events on every read, and its status is derived from theirs. Reviewing a whole issue writes one signed audit entry per finding, all carrying the one reason — and, like reviewing a single finding, it does not remove anything from the evidence page.
MISSING_EXECUTION · medium
Heron decided, and no statement has arrived since. Something ran without anybody stating what happened — or the middleware crashed, or Heron was unreachable when the statement was sent. The finding does not distinguish those; the vendor has to. What it does not mean is “slow”: absence is judged against a bound far above any tool’s runtime, so a call still running is pending, not withheld.
EXECUTION_WITHOUT_DECISION · high
Evidence arrived for an action Heron never decided on — or one belonging to another tenant, which from inside this organization is the same thing: an action that does not exist.
EXECUTED_DESPITE_DENY · high
The vendor executed an action Heron returned DENY for. Under enforcement this is a real incident: the verdict was handed to the vendor before it acted, and a cooperating vendor honours a DENY by not running the tool and reporting BLOCKED. An EXECUTED here means it did not.
EXECUTED_DESPITE_STEP_UP · high
The vendor ran a STEP_UP action on the spot instead of pausing for a human. The honoured path is to post ESCALATED, wait for a person, and re-submit a newaction carrying the human’s decision — which Heron re-judges from scratch. Running the stepped-up action directly bypasses the human the step-up exists to insert.
EXECUTED_DESPITE_MODIFY · high
Heron answered MODIFY — the call does not run in this shape, and the decision named the narrowing to apply — and the vendor ran the original anyway. It is the same disobedience as running a DENY, and it is the one a vendor reaches first: the managed rule on irreversible deletes answers MODIFY, so an integration with no transform path produces one of these per delete while its DENY count stays clean.
EXECUTED_DESPITE_DEFER · high
Heron answered DEFER — the action is withheld until the session carries the context the rule names — and the vendor ran it without establishing that context first.
INVALID_VENDOR_SIGNATURE · critical
The vendor’s signature does not verify against the public key we hold, or the key is unknown or revoked. Benign causes exist (a rotated key that was never imported); so does the other kind. The evidence is stored either way — that is what makes the finding possible at all.
BROKEN_CHAIN · critical
Recomputing the chainfrom the stored rows disagrees with what Heron signed — a record was edited after the fact — or the vendor’s claimed prev_hash does not match ours, which means it dropped an event.
LATE_EVIDENCE · low
Evidence arrived later than this tool normally reports — measured against the p99 of that tool’s own paired calls, not a fixed number, because a CRM read and a call that runs a whole sub-agent cannot share one. Usually a slow queue on the vendor’s side. The pair is formed anyway; the delay is recorded.
INTENT_CONTRADICTION · medium
The vendor marked a dimension as a model’s claim (inferred), and the model’s answer disagrees with what was actually measured — the fork calls a send internal where the signed tool catalogue says third_party. The verdict is unaffected: a model may resolve a dimension nothing else answered, and it may never overturn one that was stated, so the measurement governed and the contradiction is the diff.
It names a disagreement, not a culprit. The fork may be confused or reading injected content, the catalogue may describe the wrong tool, or Heron’s own derivation from the tool name may simply be wrong — and the finding is published whichever side turns out to be at fault. Agreement is worth nothing here: a model told what to say agrees with whatever is safe to agree with. The disagreement is the part that carries information.
Findings about the agent, which are never published
Every kind above answers one question: did the platform tell us what it did. That is what a reviewer is owed, and it is published. The three below answer a different one — what did the agent do— and they stay on the operator’s own screens and in the findings report. A permanent, signed, published record that a vendor’s agent did something dangerous is the artefact that gets held up against them in a dispute, and the report exists to be read and acted on rather than to be produced in evidence later. They are recorded, chained, resolvable and delivered to the vendor’s webhooks like any other finding; they simply do not reach a third party.
UNNAMED_RECIPIENT· medium — the call reached somebody the user’s request did not name. Often legitimate: an agent that looks an address up in a CRM and writes to it is doing its job. It is raised anyway because the case worth catching — an agent steered into a recipient nobody asked for — leaves exactly this trace and no other. A request that named nobody raises nothing: silence is not compliance, and it is not a breach either.
INSTRUCTIONS_CHANGED · medium — the digest of the text governing the agent differs from the last commitment in the same session. Ordinary on its own; compaction rewrites transcripts and a working plan lives in that slot. It is here because an injected instruction has to move through this observable and has no other. Heron sees that it changed, never what changed.
SENSITIVE_THEN_EXTERNAL · high — the session read personal, financial or credential data earlier, and this call sends outward. Both ends are established; whether the first travelled in the second is not visible from here and is not claimed. It is never raised where nobody established the destination — the most common gap in the data must not become the loudest finding on the page.
Coverage
The share of actions that have execution evidence. It is a number the vendor declares, not one Heron proves: an action taken around the hook leaves no trace, and the absence of a record is indistinguishable from the absence of an action. The evidence page says this out loud instead of implying the gap does not exist.
Evidence page
The read-only artefact at /e/{slug}?t=…that a vendor hands to a security reviewer. No login, no personal data, verifiable in the reviewer’s own browser. How to read one.
Observation window
How far back the evidence page reaches (default 30 days). Shortening it hides older findings without deleting anything — which is exactly why the change itself is written to the operator log, with the previous value, and published to the reviewer.
API key
What the vendor’s middleware authenticates with (hrn_live_…). Stored as a SHA-256 and a prefix, readable exactly once — at the moment it is minted. Revoking it makes the middleware still using it fail with 401, which means its actions go unrecorded: mint and deploy the replacement first.
Vendor key
The public half of the key the vendor signs execution evidence with. Imported once — never fetched at verification time, because that would make our evidence depend on their uptime, and would let them swap the key under evidence we had already verified.
A key that has verified nothing can be deleted. A key that has verified evidence can only be revoked: deleting it would destroy the ability to re-check what it signed. Revocation is forward-only — new evidence signed with it is flagged, and the verdicts already recorded stand.
Redaction and retention
Arguments are redacted before storage: emails, cards, tokens, JWTs, phones and IPs are scrubbed, and any field outside the project’s allowlist collapses to <redacted:field>. The shape of the call is kept, never its contents. The vendor is expected to redact first; Heron does not trust that and redacts again.
Redacted arguments expire with the retention setting (default 90 days), and so does the redacted original request: an hourly sweep in the worker clears both once the window has passed. Hashes and receipts are kept indefinitely — dropping them would destroy the proof, so retention removes what a human could read and never what the chain was computed over.
Operator log
Every action a person takes inside Heron that could weaken the evidence — closing a finding, revoking a vendor key, changing the window, rotating the link, narrowing what is stored — is chained and signed in the same transaction as the change it describes. Nothing is ever updated in place: resolve → reopen → resolve is three entries.
The signed record names the actor by a pseudonym derived from the chainand the user, never by email. The reviewer can recompute the chain from what we published and still cannot learn who anyone is — and because the salt is the chain rather than the tenant, one admin working across two projects appears as two unrelated pseudonyms, so two of a vendor’s customers cannot compare notes and identify the same person.
There are two chains, not one filtered chain. Everything a reviewer of a project is owed goes in that project’s; actions about people and projects go in the organization’s, which is published to nobody. They cannot be one chain separated by a filter: a chain is verified by recomputing it from its first entry, so a sequence with rows removed has holes and fails to verify — and from the outside that is indistinguishable from tampering.
Receipt versions
Every receipt payload carries a v. Three ship together today: a decision and an evaluation receipt are v: 4 and name the project they were judged under, the policy that judged them and the tool catalogue the classification read; an audit receipt is v: 2 and names the chain its entry belongs to.
The number moves whenever a field changes meaning, and that is the whole point of it: a receipt whose subject silently changed from an organization to a project would still verify, and would be saying something else. A verifier checks the signature over whatever payload it is handed — an older version keeps verifying — and reads v only to know which shape it may then recompute against.
Canonical JSON (RFC 8785)
Everything signed or hashed is canonicalized first, so that key order and number formatting cannot decide whether a third party can verify us. A signature over JSON.stringify output would verify only against the exact serializer that produced it — which is the same as verifying nothing.