agent-scroll 0.1.0

Canonical byte-deterministic transcript format for AI-agent conversations (Rust port of @p-vbordei/agent-scroll)
Documentation
//! agent-scroll quickstart — build a 3-turn signed chain, verify, then tamper.

use agent_scroll::{seal_chain, verify, SignOpts};
use ed25519_dalek::SigningKey;
use rand::rngs::OsRng;
use serde_json::{json, Value};

fn main() {
    // 1. Generate an Ed25519 keypair.
    let sk = SigningKey::generate(&mut OsRng);
    let sign = SignOpts {
        privkey: sk.to_bytes(),
        pubkey: sk.verifying_key().to_bytes(),
    };

    // 2. Build three plain turns. timestamp_ns differs so each turn hashes uniquely.
    let mk = |turn: u64, role: &str, content: &str, ts_offset: u64| {
        json!({
            "version": "scroll/0.1",
            "turn": turn,
            "role": role,
            "model": { "vendor": "anthropic", "id": "claude-opus-4-7" },
            "params": { "temperature": 0, "top_p": 1 },
            "messages": [{ "role": role, "content": content }],
            "timestamp_ns": 1_700_000_000_000_000_000u64 + ts_offset,
        })
    };
    let turns: Vec<Value> = vec![
        mk(0, "user", "What is the capital of France?", 0),
        mk(1, "assistant", "Paris.", 1),
        mk(2, "user", "Thanks!", 2),
    ];

    // 3. Seal + sign as a chain. seal_chain links each turn via prev_hash.
    let chain = seal_chain(&turns, Some(&sign)).expect("seal_chain");
    println!("sealed {} turns, all hashes set", chain.len());

    // 4. Verify the clean chain.
    let clean = verify(&chain, Some(&sign.pubkey));
    println!("verify clean: ok={}", clean.ok);

    // 5. Tamper: replace one hex char in turn[1]'s hash.
    let mut tampered = chain.clone();
    let h = tampered[1]["hash"].as_str().unwrap().to_string();
    let last = h.chars().last().unwrap();
    let new_last = if last == '0' { 'f' } else { '0' };
    let mut new_h: String = h[..h.len() - 1].to_string();
    new_h.push(new_last);
    tampered[1]["hash"] = json!(new_h);

    let bad = verify(&tampered, Some(&sign.pubkey));
    let reason = bad.failures.first().map(|f| f.reason).unwrap_or("none");
    println!("verify tampered: ok={} reason={}", bad.ok, reason);
}