Level 6: Proving what happened
You'll learn how anyone can verify an agent's history, and how tampering is caught.
Run cargo run --example mastery_06_audit
The idea
Each record includes the previous record’s hash, and is signed with the agent’s Ed25519 key. With only the public key, anyone can check that no record was changed, removed or reordered. With a checkpoint (seq:hash) published earlier, somewhere the writer can’t change, they can also prove nothing was cut off the end.
The code
//! Lineage Mastery, level 6: proving what happened.
//!
//! Every record is hash-chained to the one before and signed with Ed25519. With the public
//! key alone, anyone can verify a log. With a checkpoint published earlier, they can also
//! prove that nothing was cut off the end. This lesson tampers with a log three ways and
//! shows each one being caught.
//!
//! Run: cargo run --example mastery_06_audit
use std::fs;
use lineage::audit::{self, AuditKey, VerifyOptions};
use lineage::guard::{Guard, Outcome, Policy, ToolRule};
use serde_json::json;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let dir = std::path::Path::new("mastery-data/06");
let _ = fs::remove_dir_all(dir);
let log = dir.join("agent.jsonl");
let mut guard = Guard::create(&log, AuditKey::load_or_create(dir.join("audit.key"))?, "payer",
Policy::new(1000).allow("pay", ToolRule::cost(1)))?;
for amount in [120, 75, 310] {
let id = guard.request("pay", json!({"to": "ACME", "amount": amount}), Some(amount))?.action_id().to_string();
guard.report(&id, Outcome::success("sent"))?;
}
let public_key = guard.status().public_key;
let checkpoint = guard.head(); // publish this somewhere the writer can't reach
drop(guard);
println!("public key {public_key}\ncheckpoint {}:{}\n", checkpoint.seq, checkpoint.hash);
let trusted = VerifyOptions { public_key: Some(public_key.clone()), checkpoint: Some(checkpoint.clone()) };
let original = fs::read_to_string(&log)?;
check("original log", &log, &trusted);
// 1. Change a payment amount.
fs::write(&log, original.replacen("\"amount\":310", "\"amount\":31", 1))?;
check("amount 310 edited to 31", &log, &trusted);
// 2. Delete a record from the middle.
let lines: Vec<&str> = original.lines().collect();
let without: Vec<&str> = lines.iter().enumerate().filter(|(i, _)| *i != 5).map(|(_, l)| *l).collect();
fs::write(&log, without.join("\n") + "\n")?;
check("record 5 deleted", &log, &trusted);
// 3. Cut the last records off. The chain is still internally valid...
fs::write(&log, lines[..lines.len() - 3].join("\n") + "\n")?;
check("last 3 records cut (key only)", &log, &VerifyOptions { public_key: Some(public_key), checkpoint: None });
// ...which is exactly why checkpoints exist.
check("last 3 records cut (with checkpoint)", &log, &trusted);
fs::write(&log, original)?;
println!("\nTry it on the command line:\n lineage audit verify {} --public-key <key> --checkpoint {}:{}", log.display(), checkpoint.seq, checkpoint.hash);
Ok(())
}
fn check(label: &str, log: &std::path::Path, options: &VerifyOptions) {
let report = audit::verify_file(log, options);
match report.failure {
None => println!("{label:<38} OK ({} records)", report.records),
Some(failure) => println!("{label:<38} FAILED {failure}"),
}
}
Run it
public key 95a2fd0cbac87cfa44b3e7660c61feb61b1dfb9ee63f5c90cf6f9781fdea3925
checkpoint 10:948cc8797ab8c691734c3ee59434cfaf3f450dc7b7f7b4e3a40955bedc096e36
original log OK (11 records)
amount 310 edited to 31 FAILED line 9 (seq 8): hash does not match record content
record 5 deleted FAILED line 6 (seq 6): expected seq 5, found 6
last 3 records cut (key only) OK (8 records)
last 3 records cut (with checkpoint) FAILED log ends at seq 7 before checkpoint (truncated)
What happened
| Tampering | Caught by |
|---|---|
| A payment amount changed | The record’s hash no longer matches its content |
| A record deleted | The sequence numbers and prev_hash links no longer line up |
| The tail cut off | Only the checkpoint. A shorter chain is still internally valid |
That last row is why you publish checkpoints, for example to a ticket, another system, or a daily email. In production the guard server gives you one from GET /v1/agents/:id/verify.
Try this
Verify the log from the command line, as an auditor would:
lineage audit verify mastery-data/06/agent.jsonl \
--public-key "$(lineage audit pubkey mastery-data/06/audit.key)"
Then write your own verifier from the Log format spec. It’s about forty lines of Python.