Every step Kanonik takes with a policy change is on this page, in the order it happens.
It’s written for the security engineer who’s been asked to review Kanonik before your company buys it. Where the product has a name for something, we use it. That goes for the tool calls and the signing algorithms too.
We think you should be able to check what a vendor tells you about security. So each step says where it runs and what gets written down.
Your AI client
Mei’s AI signs in to Kanonik as Mei.
Harbor Ledger, our sample company, appears in every step. It’s a 40-person fintech. Dana Reyes runs security there, Sam Okafor is a reviewer, and Mei Lin writes most of the policies. This week Mei wants the incident response policy brought up to date. She works in Claude.
Mei connects Claude to Kanonik once, by adding Kanonik’s address as a connector. Kanonik is the MCP server. Claude reads Kanonik’s OAuth protected-resource metadata (RFC 9728), finds the authorization server from it and sends Mei to sign in with OAuth and PKCE. Nobody pastes a key or a setting into the client. From then on, each call her AI makes carries Mei’s identity, and it can do in Kanonik what Mei’s role allows.
The model behind Claude runs on Harbor Ledger’s own account with Anthropic. Kanonik never calls it and never sees its keys. It works the same way with any other MCP client that signs in with OAuth.
Say Mei’s AI calls a tool. Before it runs, the call passes through Kanonik’s MCP gateway. The gateway checks the token, the workspace and Mei’s role, applies rate limits, then writes the call down as an event in Harbor Ledger’s record, with the tool’s name, who called it, the skill in use and how it ended. A call that changes something carries an idempotency key, so a retry after a dropped connection can’t write the same thing twice.
Example
{
"resource": "<gateway URL>",
"authorization_servers": ["<authorization server URL>"]
}Kanonik’s gateway
Kanonik records which instructions Mei’s AI loaded.
When Claude connects, it loads Kanonik’s skills. A skill is a set of plain-text instructions that shows a general-purpose model how compliance work is done in Kanonik, such as how to draft a policy or what makes a usable piece of evidence. Kanonik signs each one. Loading a skill gives the model no permission it didn’t already have.
Each load becomes an event of its own. It holds the skill’s name and version, a hash of the exact instructions Kanonik served, and whether the signature checked out. Much later, the export checker tests the signature on each skill in use against Kanonik’s skill-signing key, which is pinned inside the checker.
An AI should be able to say which version of the instructions it followed, the same as a person would.
The Verifier’s prompts are kept apart from the skills, on Kanonik’s servers, and a skill can’t change them.
Your AI client
Mei’s AI does the reading on Harbor Ledger’s side.
Mei asks her AI to update the incident response policy. It reads the current version through Kanonik’s read tools. To see how incidents really get raised, it opens the on-call runbook in Harbor Ledger’s GitHub, through the connection Mei’s team already gave Claude.
Kanonik isn’t part of that connection. We hold no credentials for Harbor Ledger’s cloud account or its code. Nothing the AI reads there changes the record, and all Kanonik receives is what the AI drafts from it.
Each tool that keeps a copy of your cloud keys turns into one more thing your team reviews every year.
Kanonik’s gateway
The new draft goes in as a proposal and changes nothing yet.
Mei’s AI calls propose_policy with the new text. There’s a separate tool for each kind of entry, so a risk goes through propose_risk, a vendor through propose_vendor, and so on. The call records the draft as policy.proposed.v1 and returns a policy_id. Mei can see the draft in her workspace straight away. The approved incident response policy stays exactly as it was.
The same call runs the Verifier’s fixed rules and returns their result, so the AI hears about an obvious problem early. The model review hasn’t happened yet. That’s the next step.
To get the draft approved, the AI adds it to a batch with commit_batch. A batch holds up to 20 proposals and becomes one approval request. Each member must carry the id its propose call returned, so nobody can build a batch from text that never showed up in the workspace as a draft.
commit_batch can also run as a dry run, which checks the batch and writes nothing.
The Verifier
Every draft is graded before anyone is asked to read it.
Next the AI calls commit_batch with its mode set to grade. That runs the Verifier, Kanonik’s check on drafts, on Kanonik’s servers. The AI has no Verifier tool to call or to route around. Grading is a stop on the way to an approval link, and a batch that goes straight to commit is graded there instead.
The Verifier makes two passes. The first is a set of fixed rules, with no model involved. Mei’s first draft still said [DATE] where the next review date belonged. The rule kanonik.policy.placeholder-content-detection refused it, and the model never saw that version.
The AI filled in the date and graded the draft again. This time the rules passed and a model read the policy against the framework requirements it’s meant to meet. The model asks whether the policy says who is responsible, how often things happen and what’s in scope, and whether an assessor could design a test for it. The second draft passed.
Grading can take up to three rounds. During them Kanonik mints no token and sends no email, so nobody at Harbor Ledger is asked anything. The AI polls kanonik_get_batch_job for each result. If a draft still fails after round three, the AI stops and tells Mei which one failed and why.
A verdict comes out as APPROVED, REJECTED or HUMAN_REVIEW. The Verifier fails closed. If the model errors, or isn’t confident, the draft becomes HUMAN_REVIEW and reaches a person with a mark on it. A verdict never writes anything to the record by itself.
Some kinds of entry get the rules and no model review. Exceptions, risk acceptances and remediation decisions are among them. Those calls depend on your business, and people make them.
Each verdict goes on the record as verifier.verdict.v1. It names the model provider and model, the prompt version, a SHA-256 hash of the prompt and a hash of the exact inputs. A grade is reused only for the same model and the same prompt. Change either one and the draft is graded again.
The model review runs on Kanonik’s own Anthropic account, not on Harbor Ledger’s. Anthropic’s commercial terms don’t allow training on what we send for a check, and we don’t train on your data either.
Example
# round 1 passed false rule_id_if_rejected kanonik.policy.placeholder-content-detection
# round 2 passed true llm_tier_outcome <APPROVED | REJECTED | HUMAN_REVIEW> prompt_version <version> prompt_hash <sha-256> model_provider <provider> model_id <model> canonical_inputs_hash <sha-256>
A named person
Mei approves in the web app, from a link that works once.
With the grade done, the AI calls commit_batch again, this time with its mode set to commit. Kanonik mints an approval token for the request. It’s a JWT signed with ES256, which is ECDSA P-256, by the transit engine of our managed secrets engine, so the private key never leaves it.
The op claim is a SHA-256 hash of the proposal’s canonical JSON, so the token fits this exact proposal and no other. It carries two names: sub for who proposed it, and a second claim for who may use it, which here is Mei. Its jti makes it work once, and it expires an hour after it’s sent.
The link reaches Mei by email, or in Slack if Harbor Ledger has turned that on. Her AI gets a short link to hand her in the chat, and that’s all it gets. A test in our build drives every commit and finalize tool and fails if a result, an error message or a URL carries anything shaped like the token. A second test fails if any MCP tool can redeem one.
We don’t want the AI holding the thing that approves its own work. The link and the web app are the only ways to approve.
Mei opens the link signed in as herself. Kanonik checks the token’s signature and workspace, then checks that the person signed in is the one the token names. Anyone else is refused. Then Kanonik checks that the jti hasn’t been spent.
She reads the draft with its changes marked and the Verifier’s result beside it. The Accept button stays off until she ticks a box saying she has read the item and the check, and Kanonik keeps that tick with her decision. If something’s wrong, she sends it back with a note. Her AI passes the note, in her words, into the next grade round.
When she accepts, the service that writes the record works out the proposal’s hash again and checks the token once more. If one byte of the proposal had changed since the token was minted, the hashes wouldn’t match and nothing would be written. The token is marked as spent only after the write succeeds. If the hour runs out before Mei gets to it, the work is saved, and she reopens it for another hour.
Example
header { "alg": "ES256", "typ": "JWT" } op <sha-256 of the proposal's canonical JSON> sub <workspace id>:<who proposed> jti <single-use id> exp <an hour after it was sent>
A second person
Sam confirms the policy, because a policy needs a second person.
Mei’s Accept doesn’t seal the policy. Harbor Ledger has a second person who can approve, so the policy shows as accepted and waiting for him. Sam finds it in To decide, marked as his to confirm. He reads it, ticks that he has, and confirms. His confirmation goes through a token of its own, bound to Sam. Now the new policy is on the record.
Kanonik asks for a second person on policies, procedures, controls, Statement of Applicability decisions, exceptions, risk acceptances and confirmations that something meets a framework requirement. Evidence, vendors, assets, risks and questionnaire answers can be approved by the person who drafted them. Anyone with the admin or reviewer role counts as a second person. An auditor’s read-only seat doesn’t.
In a workspace where only one person can approve, that person seals a policy alone. They tick a second box that reads “I am the only person in this workspace, and I am sealing my own governance item.” Kanonik records a named exception with those exact words.
The export checker doesn’t take that exception on trust. It reads the role events on the chain, and it accepts the exception only if nobody else could approve at that moment. It also checks that each policy one person accepted was sealed by someone else, and that each approval records who ticked the box and what it said.
A one-person company shouldn’t have to invent a second approver, so the record says plainly that there was nobody else.
The record
The sealed policy joins Harbor Ledger’s hash chain.
Sealing appends an event to Harbor Ledger’s log. Its event_hash is a SHA-256 over a fixed, length-prefixed layout of the event, which covers the workspace, the event’s id and type, both its timestamps, the caller, the previous event’s hash and the payload as canonical JSON. Because each event carries the hash of the one before it, a workspace’s events form one chain. Edit an old event and every hash after it stops matching.
The database rejects every edit and delete of an event. A trigger refuses each UPDATE and DELETE on the event table, and on the table of chain signatures. A correction goes in as a new event that points back at the old one, so you can always read what the policy said before. The database also refuses a second event with the same idempotency key in the same workspace.
Kanonik signs the head of each workspace’s chain with ECDSA P-384 over a SHA-384 digest. That key lives in our managed secrets engine too, and our services ask it to sign. Signing happens on a schedule, and again whenever an audit session is finalized.
The policy’s event keeps the Verifier’s verdict with it, along with Mei’s acceptance and Sam’s confirmation. Drafts that were sent back stay in the log as well.
Nobody can change the record without the change showing. The signature is what lets someone outside Harbor Ledger check it without calling us.
Example
# the event sequence_number <n> prev_hash <sha-256> event_hash <sha-256> # the signed chain head highest_seq <n> highest_hash <sha-256> signed_at <time> signer_key_id <key id> signature <ECDSA P-384 signature>
The record
Every entry has two dates, and you can read the record as of either one.
One is valid_time, when the entry took effect. For most changes, that is the moment of approval. For evidence, it is the date it was collected, as the AI reported it. The other is transaction_time, when Kanonik recorded it. Auditors look at the entries where the dates differ, such as a document collected in February and uploaded in March.
So an auditor can ask two different questions about 1 March. What was in place that day? And what did the record say that day? Kanonik answers both. The read tools get_standing and get_soa take an as_of_time for the first question and a system_as_of_time for the second, so Mei’s AI can ask them too.
A correction made in April doesn’t rewrite March. Read as of a system time in March, and you see what the record said back then.
Example
# what was in place on 1 March get_standing { "as_of_time": "2026-03-01T00:00:00Z" }
# what the record said on 1 March get_standing { "system_as_of_time": "2026-03-01T00:00:00Z" }
The auditor
The export checker runs offline, with Kanonik’s public keys pinned inside it.
When the audit comes round, Dana builds the Sealed Audit Package from the Audit log. It covers the whole record to date. It holds every event, the signed chain head and the checker that verifies them. Dana sends it to Priya Shah of Shah Assurance, Harbor Ledger’s auditor.
The checker comes in three forms, all inside the package. The Python version, verify.py, is a single MIT-licensed file. Its one dependency, the cryptography package, is needed only for the signature check. verify.mjs runs under Node. verify.html runs the same checks in a browser; Priya drags the archive onto the page, and nothing is uploaded. None of them needs a network connection or a Kanonik account.
There are eleven checks. The checker recalculates every event hash and walks the chain. It confirms that the signed head covers every event, so nothing was dropped after signing. Then it checks the signature against Kanonik’s public signing keys, which are pinned inside the checker. It never uses the key file that came in the package, so a package someone altered and signed again with their own key fails.
The rest is about who did what. Every graded draft has to carry the Verifier’s verdict. Self-approvals need their recorded exception, and a policy one person accepted needs a different person’s seal. Every verdict has to name its model and prompt, and its prompt_hash has to match a prompt Kanonik has released. The package carries those hashes, and the prompt text stays with us. Last, every file has to match the SHA-256 the manifest gives it.
If Priya’s machine doesn’t have the cryptography package, the signature check reads DEGRADED. So the checker says “Passed with notes” when it couldn’t check something, and gives the reason.
She can read verify.py before she runs it.
Example
{
"overall_status": "PASS",
"summary": { "PASS": 11, "FAIL": 0, "DEGRADED": 0 },
"checks": [
{ "check": "hash_chain_integrity", "status": "PASS" },
{ "check": "chain_completeness", "status": "PASS" },
{ "check": "chain_root_signature", "status": "PASS" },
{ "check": "verifier_verdict_presence", "status": "PASS" },
{ "check": "sod_separation_of_duties", "status": "PASS" },
{ "check": "two_act_oversight", "status": "PASS" },
{ "check": "member_attestation", "status": "PASS" },
{ "check": "skill_provenance", "status": "PASS" },
{ "check": "reproducibility_tuple_completeness", "status": "PASS" },
{ "check": "prompt_integrity", "status": "PASS" },
{ "check": "file_checksums", "status": "PASS" }
]
}A buyer
A buyer can check a questionnaire answer without seeing the rest of your record.
Say Northshore Bank is reviewing Harbor Ledger as a supplier, and its reviewer wants the evidence behind one questionnaire answer, the one about how incidents are handled. Dana doesn’t send the package. In Kanonik she downloads the record proof for the answer set, a single file named proof-<kind>-<id>.json.
The file holds the sealed event with every field its hash covers, and a checkpoint that Kanonik’s chain signer signed over that one event. It also carries the Verifier’s verdict, the approval, and whether a newer version existed when the file was made. Nothing about the rest of the record is in it.
Northshore’s reviewer runs verify.py --record on it, or drops it onto verify.html. Six checks run. If Harbor Ledger had replaced that answer since, the currency check reads DEGRADED, and the bank knows to ask for the current one.
Every plan includes a proof file per answer.
Northshore Bank’s reviewer asked for the proof behind one answer, the one about how incidents are handled. Dana downloaded it from Kanonik. It’s one file, and this is what’s in it.
The reviewer drops it on the checker. Six checks run, one after the other.
All six pass. The bank can read the answer and see who approved it. The signature covers that exact record, and nothing else from Harbor Ledger’s record is in the file.
Someone changes one answer in the file. The checker works the hash out again from the bytes it was given, and it isn’t the hash that was sealed.
They work out a new hash and put it in the file. Anyone can do that, and that check passes now. But the signature covers the old hash, and the key that signs isn’t theirs.
Harbor Ledger has since replaced this answer set. The file was made after that, so it says a newer version exists. The record is still genuine, so the checker passes it with a note. The bank sees that and knows to ask for the current file.
Back as it was. Every check passes again.
- kind
- questionnaire_response
- canonical_id
- 7d2c41b8...c7f1
event
- event_type
- questionnaire_response.
committed. v1 - sequence_number
- 4012
- valid_time
- 2026-09-17T09:00:00Z
- prev_hash
- 3aVJHAmC...EHg=
- payload
- base64, decodes to
- entries[0]
- question_key
- 7.1
- value
- Yes
- state
- from_record
- fitted_text
- The incident response procedure names who leads and how customers are told.The incident response procedure names who leads and how customers are told. It is tested every quarter.
- event_hash
- YX3bGKLt...HuE=zXJZr3zS...9u0=
# the hash covers more of the event than this; the full list is under the checker
checkpoint
- version
- record-v1
- event_hash
- YX3bGKLt...HuE=
- signed_at
- 2026-09-28T14:05:11.482913Z
- signing_key_id
- sample-
key- for- this- page - signature
- MGYCMQCZ...cH8=
verdict
# none in this file: an answer set is checked by the rule tier, not a model
approval
- approved_by_subject
- mei.lin
@harborledger.example - approved_at
- 2026-09-17T09:00:00Z
- decision_basis
- author_
self_ seal_ record_ kind
currency
- is_latest_for_entity
- truefalse
- exported_at
- 2026-09-28T14:05:12Z
- latest_canonical_id
- 0192fe40...5e72
Not runNothing checked yet.
PassedEvery check below passed.
Passed with notesThe record is intact. The notes below say what to know.
FailedThe failing check is marked below.
waitingrecord_schema_and_version
A format this checker reads.
file needs verifier 2.7.1; this is 2.7.13
waitingrecord_event_hash
The record still hashes to the value it was sealed under.
event hash recomputed from the record’s own bytes and matches
the event’s content does not hash to the event_hash it states: a byte changed after the seal
record_event_hash_mismatchwaitingrecord_checkpoint_signature
The checkpoint’s signature covers that exact hash.
ECDSA P-384 + SHA-384 signature verified against the key made for this page, not Kanonik’s key
the checkpoint signs a different event hash than the event in the file
record_checkpoint_hash_mismatchwaitingrecord_verdict
The Verifier’s verdict, if this kind carries one.
a questionnaire_response is checked by the rule tier at the seal and carries no model verdict by design
waitingrecord_approval
A named person approved it, on a basis this checker knows.
sealed by [email protected] at 2026-09-17T09:00:00Z; basis author_self_seal_record_kind
waitingrecord_currency
A newer version, if one existed when the file was made.
latest sealed version as of 2026-09-28T14:05:12Z
a newer sealed version (0192fe40-7b2d-7c18-a6e3-41d90b8c5e72) existed when this file was made on 2026-09-28T14:05:12Z; this record verifies as the version it is, not as the current one. Ask the sender for the current file.
The hash is worked out in your browser the way the product works it out: the fields in the order listed below, the payload as canonical JSON, then SHA-256. The checks and their words are the offline checker’s own. This sample file holds one answer, where a real one holds all 64. Its signing key was made for this page. It isn’t Kanonik’s, so the real checker would stop at the signature check on this file. Here the page treats its own key as the pinned one.
What the hash covers
tenant_id, event_id, event_type, schema_version, entity_type, entity_id, idempotency_key, transaction_time, valid_time, caller_subject, caller_session, caller_tool, prev_hash, payload. Each is written with its length, the previous hash as 32 bytes and the payload last, then the whole thing goes through SHA-256 (packages/auditorexport/chain/event_hash.py).
Foresight
Foresight reads the approved record and writes nothing back.
An engineer at Harbor Ledger asks the team’s AI whether moving the database backups to Brookvale Backup on 3 November breaks anything. Customers will be told ten days before that. The AI reads Brookvale’s data processing addendum on Harbor Ledger’s side and proposes the facts the promises need, each with its clause, through propose_operational_fact. Dana approves the five facts, from the same kind of link Mei used.
Then the AI calls rehearse_change with the change, the vendors involved, the go-live date and the notice date. Kanonik pins one point in Harbor Ledger’s chain and reads the approved promises, facts and answer sets as they stood there. A fixed evaluator compares them, and no model takes part. Apart from the usual record of the tool call, nothing is written.
Each promise comes back as Conflict, Needs information, No conflict or Not applicable, with the reason beside it. For the Brookvale move, three promises conflict, one needs information and one has no conflict. The result also lists the approved questionnaire answers that make a promise now in doubt, so Dana knows which ones to reread.
Every result carries a replay envelope. Send it back months later and Kanonik runs the check again at the same point in the chain, with the same result digest. So Dana can show an auditor what the check said at the time.
Example
| Harbor Ledger’s promise | Result | Why |
|---|---|---|
| Customer data is stored only in the EU, backups included | Conflict | Brookvale stores backups in the United States. |
| Customer data is never used to train AI models | Needs information | The addendum doesn’t say. |
Kanonik’s database
Harbor Ledger’s workspace is kept apart from every other workspace.
Every row in the record carries a workspace id. Postgres row-level security is forced on the record’s tables, the event log and the chain signatures among them, so it binds the table owner as well as the application’s own database role. Our services pass the workspace between them in a signed context, and each request is checked against it.
Connections from the internet use TLS 1.2 or 1.3 and end at Cloudflare. Our application services talk over mutual TLS.
Each workspace has its own key in our managed secrets engine. Today it encrypts a few fields, namely the workspace owner’s email address, the addresses approval requests go to and your proof-of-purchase reference. Like the keys that sign approval tokens and chain heads, it never leaves the secrets engine. Our services ask the engine to encrypt, decrypt and sign.
Each framework is a versioned data package of requirements and controls. Kanonik works with 17 frameworks: SOC 2, ISO/IEC 27001, HIPAA, GDPR, NIST CSF 2.0, NIST SP 800-53 Rev. 5, NIST SP 800-171 Rev. 2, NIST SP 800-171 Rev. 3, FedRAMP Rev. 5 baselines, GovRAMP Rev. 5 baselines, CCPA/CPRA, DORA, NIS2, NYDFS Part 500, EU AI Act, NIST AI RMF 1.0 and NIST SSDF. Adding a framework, or a new version of one, means adding a new package.
Here are the words this page uses, with what each one means.
- MCP
- The protocol AI clients use to call outside tools. Kanonik is an MCP server. Signing in
- Skill
- A signed set of plain-text instructions your AI client loads. Loading one adds no permission, and each load is recorded. Skills
- Proposal
- A draft sent through a propose tool. It shows in the workspace and changes nothing until a person approves it. The proposal
- Grade round
- One pass of the Verifier over a batch, run with commit_batch and its mode set to grade. A draft gets up to three. The Verifier
- Verifier
- Kanonik’s check on drafts. Fixed rules come first, then a model review on Kanonik’s own account. It runs on our servers, and the export checker is a different thing. The Verifier
- Approval token
- The ES256-signed token behind an approval link. It’s bound to a hash of the proposal and to one person, it works once and it expires an hour after it’s sent. The AI never holds it. The approval link
- Second person
- The reviewer or admin who confirms a governance entry, such as a policy, after its author accepts it. The second person
- Self-approval exception
- The named entry Kanonik records when the only person who can approve seals a governance entry alone. The second person
- Hash chain
- Each workspace’s events, each one carrying the SHA-256 of the one before. Its head is signed with ECDSA P-384. The chain
- Valid time and system time
- When something was true, and when Kanonik recorded it. You can read the record as of either. Two dates
- Sealed Audit Package
- The export an auditor receives, with the events, the signed chain head and the checker. The Sealed Audit Package
- Export checker
- verify.py, verify.mjs and verify.html, which check a package or a record proof offline. The Sealed Audit Package
- Record proof
- One sealed entry with its own signed checkpoint, for a buyer who asked about an answer. One answer's proof
- Foresight
- The check that compares a planned vendor change with your approved promises and facts, and writes nothing. Foresight
- Workspace
- One company’s record, kept apart from the others by row-level security and its own key. Separation and keys
A trial workspace records the same events, so you can check them yourself.
Your AI can call propose_policy and commit_batch there just as they appear above, and the approval link that reaches you works like Mei’s.