Arcezia

Arcezia / AI agent security / Agent governance

AI agent governance: a rule, a decision per action, a record you can check

AI agent governance is the set of rules an organisation sets for what its agents may do, and the evidence that the rules held. The term covers two jobs. One is inventory and posture: knowing which agents run, with which tools and settings. The other is governance of each action: every tool call an agent attempts is decided against your rules before it runs, and the decision is kept as evidence. Arcezia does the second, in four steps: you write a rule; each tool call gets ALLOW, REVIEW or BLOCK before it runs; each answer is kept as a signed record; an auditor checks the records offline, without trusting Arcezia. Below, the four steps run end to end on the live service.

The four steps

  1. Rule. A contract states what a tool does and your own rules for it, such as a rule that refunds above a set amount never run. A session’s scope, signed by the person in charge, says what one run may do at most.
  2. Decision. Each tool call is answered before it runs: ALLOW, REVIEW (held, with what would release it) or BLOCK (refused, with why). Your code runs the tool on ALLOW and on nothing else.
  3. Signed record. Each answer is kept as a record signed with Ed25519 and linked to the record before it, so a changed or missing record shows. When a person’s approval was attached, the record shows whether it counted and whether it was used up; for a plan of several steps, an approval for a step that never ran reads “not counted: step not run”.
  4. Offline check. An open-source checker verifies the records with the account’s key fingerprint, which the auditor gets from the account holder, not from the file.

Run it end to end

pip install -U arcezia cryptography, set ARCEZIA_API_KEY to an owner key from the Keys page at app.arcezia.com, and save the two files below. rule_call.py writes the rule and sends the calls:

import os
import arcezia
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey

PATH = "principal_key.pem"   # your signing key: keep it where the agent cannot read it
if not os.path.exists(PATH):
    with open(PATH, "wb") as f:
        f.write(Ed25519PrivateKey.generate().private_bytes(
            serialization.Encoding.PEM, serialization.PrivateFormat.PKCS8, serialization.NoEncryption()))
key = serialization.load_pem_private_key(open(PATH, "rb").read(), password=None)

# 1. The rule: refunds above 500 never run.
admin = arcezia.Arcezia(task="refund customer orders", signing_key=key)
admin.register_token_key(key.public_key())      # once per account; safe to repeat with the same key
for c in admin.contracts():
    if c["name"] == "refund-policy":
        admin.delete_contract("refund-policy")
contract = admin.register_contract({
    "facts": {"large_refund": {"means": "the refund is above 500", "from": "call_amount > 500"}},
    "tools": {"issue_refund": {
        "pack": "agent_action",
        "not_present": ["outbound", "trust_boundary_crossing", "sensitive_data", "mass_scope", "irreversible"],
        "arguments": {"amount": ["amount"]},
        "rules": [{"when": ["large_refund"], "never": True}],
    }},
}, name="refund-policy")
REFUND = contract["domains"]["issue_refund"]

# 2. The calls: the same tool, two amounts, in one session you sign.
az = arcezia.Arcezia(task="refund customer orders", signing_key=key)
az.start_session(capability_envelope=contract["session_grants"])
for amount in (40, 2400):
    cert = az.verify(action_type="issue_refund", action_description=f"refund order 42: {amount} EUR",
                     domain=REFUND, action_parameters={"order_id": "42", "amount": amount})
    print("amount", amount, "|", cert.verdict, "| release", cert.release, "| reason", cert.reason,
          "| record", cert.log_id)

govern.sh runs it, exports the two records and checks them offline:

API=https://api.arcezia.com
AUTH="Authorization: Bearer $ARCEZIA_API_KEY"

# 1-2. Declare the rule, send the calls (prints each answer and its record id).
python3 rule_call.py | tee answers.txt
FIRST=$(grep -o 'record [0-9]*' answers.txt | head -1 | cut -d' ' -f2)

# 3. Export those records, and get the account's record-signing key fingerprint.
curl -s "$API/v1/account/export?after_id=$((FIRST - 1))&limit=2" -H "$AUTH" > records.ndjson
FP=$(curl -s "$API/v1/account/record_keys" -H "$AUTH" | python3 -c 'import json,sys; print(json.load(sys.stdin)["fingerprint_sha256"])')

# 4. Check the records offline.
arcezia-verify-records records.ndjson --use-export-keys --partial --fingerprint "$FP"

Output of sh govern.sh:

amount 40 | REVIEW | release ['approval:user'] | reason [] | record 13567
amount 2400 | BLOCK | release [] | reason ['contract:large_refund'] | record 13568
account        : 26
keys           : 1 key(s) matched a fingerprint supplied out of band (keys read from the export)
  key 4c5e576d5765a733  sha256 4c5e576d5765a7334c9d1a34bef62084627b66206d2018dff957987717c3c15c  (compare with the account holder's)
records        : 2
times shown    : the signed time inside each record; the export's plain 'timestamp' column is not covered by the signature

  record    13567  UNVERIFIED  signed 2026-10-07T15:01:52.778285+00:00  verdict REVIEW
                      - the first record here follows records not in this export (partial export)
  record    13568  PASS        signed 2026-10-07T15:01:53.248097+00:00  verdict BLOCK

export headers:
  records.ndjson  PASS        header signature matches and agrees with the file

checkpoints (carried inside the export):
  checkpoint    25  PASS        up to record 13542, 3301 record(s), as of 2026-10-07T11:04:47.712682+00:00: signature matches
  checkpoint    24  PASS        up to record 13500, 3259 record(s), as of 2026-10-07T10:51:43.072731+00:00: signature matches
  checkpoint    23  PASS        up to record 13474, 3236 record(s), as of 2026-10-07T10:47:13.058137+00:00: signature matches
  checkpoint    22  PASS        up to record 13102, 2864 record(s), as of 2026-10-07T10:19:50.461882+00:00: signature matches
  checkpoint    21  PASS        up to record 13001, 2774 record(s), as of 2026-10-07T09:53:20.289230+00:00: signature matches
  checkpoint    20  PASS        up to record 13000, 2773 record(s), as of 2026-10-07T09:53:18.369772+00:00: signature matches
  checkpoint    19  PASS        up to record 12941, 2714 record(s), as of 2026-10-07T03:13:25.916291+00:00: signature matches
  checkpoint    18  PASS        up to record 12500, 2467 record(s), as of 2026-09-20T19:05:05.696928+00:00: signature matches
  checkpoint    17  PASS        up to record 12000, 2052 record(s), as of 2026-09-16T20:45:49.349811+00:00: signature matches
  checkpoint    16  PASS        up to record 11500, 1636 record(s), as of 2026-09-16T19:16:26.565572+00:00: signature matches
  checkpoint    14  PASS        up to record 10863, 1273 record(s), as of 2026-09-12T11:28:35.306509+00:00: signature matches
  checkpoint    13  PASS        up to record 10863, 1273 record(s), as of 2026-09-12T11:28:27.678777+00:00: signature matches
  checkpoint    12  PASS        up to record 10769, 1179 record(s), as of 2026-09-12T11:17:11.490864+00:00: signature matches
  checkpoint    11  PASS        up to record 10769, 1179 record(s), as of 2026-09-12T11:17:05.970650+00:00: signature matches
  checkpoint    10  PASS        up to record 10717, 1127 record(s), as of 2026-09-12T11:15:47.888685+00:00: signature matches
  checkpoint     9  PASS        up to record 10717, 1127 record(s), as of 2026-09-12T11:15:42.562457+00:00: signature matches
  checkpoint     8  PASS        up to record 10614, 1059 record(s), as of 2026-09-11T10:10:54.502046+00:00: signature matches
  checkpoint     7  PASS        up to record 10614, 1059 record(s), as of 2026-09-11T10:10:47.853331+00:00: signature matches
  checkpoint     6  PASS        up to record 10500, 972 record(s), as of 2026-09-11T09:59:48.253208+00:00: signature matches
  checkpoint     4  PASS        up to record 9500, 694 record(s), as of 2026-09-07T17:04:14.502500+00:00: signature matches
  checkpoint     3  PASS        up to record 9065, 341 record(s), as of 2026-09-07T09:45:12.593718+00:00: signature matches
  checkpoint     2  PASS        up to record 9065, 341 record(s), as of 2026-09-07T09:45:05.375206+00:00: signature matches
  checkpoint     1  PASS        up to record 9000, 323 record(s), as of 2026-09-07T09:39:46.687967+00:00: signature matches

completeness   : PASS  the export reaches record 13568, at or beyond the strongest signed checkpoint (record 13542, carried inside the export)
                 (these checkpoints travelled inside the export; one kept outside Arcezia (--checkpoint) is stronger evidence)

PASS 1   PASS-ATTESTED 0   FAIL 0   UNVERIFIED 1
RESULT         : UNVERIFIED

Measured 7 October 2026, 15:01:48 UTC to 15:01:57 UTC, with the arcezia Python SDK 1.0.7 from PyPI, against https://api.arcezia.com, on Arcezia’s self-test account. The contract was deleted afterwards. On your account the record ids and times will differ.

What the run shows

CallAnswerWhyRecord
issue_refund, amount 40REVIEWrelease ['approval:user']: a refund is a lasting change, so a person approves it13567
issue_refund, amount 2400BLOCKreason ['contract:large_refund']: your rule refuses it13568

An audit uses the complete export, every page from the account’s first record, without --partial. A real sample, and what PASS, UNVERIFIED and FAIL mean: audit evidence. To remove the rule afterwards: az.delete_contract("refund-policy").

What this does not cover

Questions

What is the difference between AI governance and AI agent governance?

AI governance covers how an organisation builds and uses AI: risk assessment, documentation, oversight. Agent governance adds the actions. An agent changes real systems, so its rules have to apply to each action when it happens, and leave evidence that they did.

Can a person approve an action that a rule holds?

A held call (REVIEW) names what would release it, such as approval:user. A person’s approval is a token your backend signs, bound to that one call and used once; an approval the agent only claims is refused. A rule written as never refuses the call outright. Approvals in the developer docs.