constle docs constle docs v0.5.0pre-1.0

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
Explainer · 0:48 · The audit log · English captions
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

shell
constle audit verify --agentfile=agent.yaml ~/.constle/logs/my-agent-2026-08-08.jsonl
Output
✓ audit log verified: ~/.constle/logs/my-agent-2026-08-08.jsonl

  entries:   2 (all signatures valid, hash chain intact)
  signed by: did:key:z6MkgroKowQYDZjDmqbn82mJv4YFPKowS2xDhxGYrp4u3P1o

After a single byte of that file is edited:

Output
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 signing

verify 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_approved that does not re-verify as approved is an approval the declared approver never gave;
  • a gate_signature_invalid that 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.