Proving the log was not edited
An audit log matters only if somebody who did not write it can believe it. This one chains its rows together with hashes, so a changed row can be detected. This lesson changes one and catches it.
Every row stores a hash of its own contents and the hash of the row before it. Break either and the chain stops agreeing with itself.
import os, tempfile
from agent_control_plane import AgentKernel, PolicyEngine, FlightRecorder
db = os.path.join(tempfile.mkdtemp(), "audit.db")
recorder = FlightRecorder(db_path=db)
policy = PolicyEngine(); policy.add_constraint("support", ["lookup_order"])
kernel = AgentKernel(policy_engine=policy, audit_logger=recorder)
for tool in ["lookup_order", "delete_order"]:
kernel.intercept_tool_execution("support", tool, {})
recorder.flush()
print(recorder.verify_integrity())Valid, and it says how many entries it checked. That number matters: a log that has been truncated rather than edited still verifies, so the count is the part that catches deletion.
Editing a row
Now the attack. Somebody with access to the database decides the blocked refund should read as allowed, and edits it in SQLite directly, going nowhere near the toolkit.
import sqlite3
recorder.close()
connection = sqlite3.connect(db)
connection.execute("UPDATE audit_log SET policy_verdict='allowed' WHERE policy_verdict='blocked'")
connection.commit()
connection.close()
print("edited")The row now says the refund was allowed. Read it back through query_logs and it looks entirely ordinary, because nothing about the row itself is obviously wrong.
Catching it
checked = FlightRecorder(db_path=db).verify_integrity()
print("valid:", checked["valid"])
print("error:", checked["error"])
print("first tampered id:", checked["first_tampered_id"])Caught, and the row is named. The stored content hash covers the verdict along with the rest of the row, so changing the verdict alone is enough to break it.
To make it worth more than that, the database has to live somewhere the agent's own tools cannot reach. A governed agent with a file-writing tool and a policy that forgot to protect the audit path can rewrite its own history, and lesson 6's protected-path check is what stops it.
- Delete the last row instead of editing it and see whether the log still verifies.
- Edit
tool_argsrather than the verdict and confirm the content hash still catches it. - Add a row of your own with plain SQL and find out which check fails.
You understood something today that you didn't yesterday.