Level 1: Your first guarded action
You'll learn what a policy is, how an agent asks before acting, and what gets written down.
Run cargo run --example mastery_01_first_action
The idea
An agent may only do what its policy lists. Before every action it asks the guard, and the guard answers allowed or denied. Both the question and the answer are written to the agent’s log, and each record is signed.
The code
//! Lineage Mastery, level 1: your first guarded action.
//!
//! An agent may only do what its policy lists. Ask the guard before acting; the guard
//! answers allowed or denied, and writes both the question and the answer to a signed log.
//!
//! Run: cargo run --example mastery_01_first_action
use lineage::audit::AuditKey;
use lineage::guard::{Decision, Guard, Outcome, Policy, ToolRule};
use serde_json::json;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let dir = std::path::Path::new("mastery-data/01");
let _ = std::fs::remove_dir_all(dir); // every run of this lesson starts fresh
// The policy: one tool, "search", costing 1 credit. Nothing else is allowed.
let policy = Policy::new(10).allow("search", ToolRule::cost(1));
// A signing key, and a new agent whose log is agent.jsonl.
let key = AuditKey::load_or_create(dir.join("audit.key"))?;
let mut guard = Guard::create(dir.join("agent.jsonl"), key, "helper", policy)?;
// Ask before acting.
for tool in ["search", "send_email"] {
match guard.request(tool, json!({ "query": "status page" }), None)? {
Decision::Allowed { action_id, remaining, .. } => {
println!("{tool:<11} allowed ({action_id}, {remaining} credits left)");
// ... run the tool here, then say how it went:
guard.report(&action_id, Outcome::success("found 3 results"))?;
}
Decision::Denied { action_id, reason } => println!("{tool:<11} DENIED ({action_id}: {reason})"),
Decision::PendingApproval { .. } => unreachable!("nothing in this policy needs approval"),
}
}
println!("\nThe log now holds {} signed records:", guard.records()?.len());
for record in guard.records()? {
println!(" {:>2} {:<17} {}", record.seq, record.kind, record.payload);
}
println!("\nOpen {} to see them yourself.", dir.join("agent.jsonl").display());
Ok(())
}
Run it
search allowed (act-1, 9 credits left)
send_email DENIED (act-2: tool 'send_email' is not allowed)
The log now holds 8 signed records:
0 genesis {"log_id":"helper","public_key":"1c897cff…"}
1 policy {"policy":{"budget":10,"default_rule":null,"rate_limit":null,"scar_limit":10,"tools":{"search":{…}}}}
2 action_requested {"action_id":"act-1","cost":1,"input":{"query":"status page"},"tool":"search"}
3 action_allowed {"action_id":"act-1","approved_by":null}
4 outcome {"action_id":"act-1","detail":"found 3 results","status":"success"}
5 action_requested {"action_id":"act-2","cost":0,"input":{"query":"status page"},"tool":"send_email"}
6 action_denied {"action_id":"act-2","reason":{"code":"tool_not_allowed","tool":"send_email"}}
7 scar {"action_id":"act-2","reason":"tool 'send_email' is not allowed","severity":"moderate"}
What happened
- The first two records are the agent’s birth:
genesisnames its signing key, andpolicyfixes what it may do, forever. searchis in the policy, so it was allowed and cost 1 credit. The agent reported the outcome.send_emailisn’t in the policy. It was denied without running, and the attempt left a moderate scar. Asking for a tool you weren’t given is a warning sign.- Denied requests are recorded too. An audit shows what the agent tried, not just what it did.
Try this
- Add
send_emailto the policy with.allow("send_email", ToolRule::cost(2))and run again. - Open
mastery-data/01/agent.jsonl. Every line is one record, with itshash, itsprev_hash, and asignature.