Audit log and verification
One JSONL file per agent per UTC day. With identity.did set, every entry is Ed25519-signed and hash-chained, and constle audit verify reports the exact line and kind of any tampering.
On this page
Transcript
0:00 When something goes wrong, you need a record the agent couldn't quietly rewrite. Every event — connections, gates, charges — becomes a JSON line, signed with the agent's did:key. Each line also carries the hash of the line before it. Edit, delete or reorder one, and the chain breaks. constle audit verify checks every signature and every link.
0:25 Change a single byte…
0:29 …and verify reports tampering, at the exact line.
0:34 An intact chain proves the log wasn't altered after the fact — not that the runtime told the truth. Constle says so.
0:42 Signed, chained, and verifiable offline.
What gets loggedLink to this section
Every run writes to ~/.constle/logs/<agent>-<YYYY-MM-DD>.jsonl (the day is UTC). The log records enforcement and activity events, among them:
| Events | Written when |
|---|---|
run_started, run_finished |
a run begins and ends (run_started records the requested and achieved isolation) |
network_allowed, network_blocked |
the egress proxy allows or refuses a connection |
mcp_tool_blocked, mcp_request_blocked |
the MCP gate refuses a tool or a request |
tool_call_start, tool_call_end |
around every MCP tool call forwarded upstream |
gate_triggered, gate_approved, gate_denied, gate_timeout |
a human gate opens and resolves |
gate_signature_invalid, gate_request_id_mismatch, gate_digest_mismatch |
a webhook decision fails verification |
spending_limit_reached |
a cap trips, a run is refused, or the daily warning threshold is crossed |
terminated_by_limit |
max_duration_seconds elapses |
a2a_call_sent, a2a_call_received, a2a_call_rejected |
agent-to-agent round trips |
Signing and the hash chainLink to this section
When the agent has an identity, every entry, written by the same single logger that writes every other event, carries three extra fields:
| Field | Content |
|---|---|
did |
the signing agent's did:key identifier |
prev_hash |
hex SHA-256 of the previous raw log line; an all-zero genesis value for the first line of a file |
sig |
base64 Ed25519 signature over the serialized entry (with sig itself absent) |
Because sig is always the last JSON field, the signed bytes are exactly the raw line minus the ,"sig":"…"} suffix. Verification operates on the very bytes on disk, with no re-canonicalization. The chain resumes across runs: a new run's first entry chains to the last line already in the file.
The chain makes the log tamper-evident as a sequence rather than a set of independently signed lines:
| Tampering | Reported as |
|---|---|
| Editing a line | invalid_signature, at the exact line |
| Deleting a line | chain_break_missing_entry, at the exact position |
| Reordering lines | chain_break_reordered, at the exact position |
| Rewriting the whole log with a different key | did_mismatch, when the DID is pinned |
Verifying a logLink to this section
constle audit verify --agentfile=agent.yaml ~/.constle/logs/my-agent-2026-08-08.jsonl✓ audit log verified: ~/.constle/logs/my-agent-2026-08-08.jsonl
entries: 2 (all signatures valid, hash chain intact)
signed by: did:key:z6MkgroKowQYDZjDmqbn82mJv4YFPKowS2xDhxGYrp4u3P1oAfter a single byte of that file is edited:
error: TAMPERING DETECTED in ~/.constle/logs/my-agent-2026-08-08.jsonl
line 1: invalid_signature — signature does not verify against did:key:z6Mkg… — the entry was edited after signingverify recovers each key from the DID string itself: no registry, no network call. It checks every signature and the whole chain, and re-verifies the human-gate decisions the log records.
Pin the trust anchors
--agentfile=<path> takes both pins from the Agentfile: the agent's identity.did and human_gates.approver_pubkey. --did= and --approver-pubkey= pin them one at a time. Unpinned, a check shows only that the log is internally consistent: a log rewritten wholesale with another key would verify against itself.
Recorded gate decisionsLink to this section
A gate decided over the signed webhook records the decision's signed fields, the identifying fields of the request (never tool_call.arguments, which routinely carry secrets) and the approver key actually used. Verification compares that cryptography against the event the log claims, in both directions:
- a
gate_approvedthat does not re-verify as approved is an approval the declared approver never gave; - a
gate_signature_invalidthat now verifies cleanly is a denial the log misattributes to the approver; - an approval with no recorded decision fails, unless it says
decided_by: terminal, because a terminal approval signs nothing and leaves nothing to re-verify.
What an intact chain provesLink to this section
| Proves | Does not prove |
|---|---|
| The runtime's own account of the run is unaltered since it was signed. | That the account is true: the host wrote it, and the host is trusted by construction. |
A recorded webhook approval was signed by the pinned approver key over that subject_digest. |
Which tool call a signed digest was for: the recorded request omits the arguments, so a genuine decision could be relabelled (the request_id settles it against the approver's own records). |
With identity.did declared and no matching key on the machine, constle run refuses to start, so a run can't silently fall back to an unsigned log. See Identity.