Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Level 4: Humans in the loop

You'll learn how risky actions wait for a person, and how approvals are recorded and re-checked.

Run cargo run --example mastery_04_approvals

The idea

requestedpending_approvaldeniedallowedcompletedneeds a humana check failschecks passapproved + re-checkedrejectedoutcomesuccess · failure · harmful
The states an action moves through. Every transition is one signed record in the agent's log.

A tool marked with_approval() puts every call on hold. Nothing is charged while it waits. When a person approves, every check runs again, because the agent may have been terminated or run low on budget in the meantime. The approval, the approver, and their note all go into the signed log.

The code

//! Lineage Mastery, level 4: humans in the loop.
//!
//! Tools marked `with_approval()` wait for a person. Nothing is charged while an action
//! waits. Approvals re-run every check, carry a signed note, and bind to the exact input
//! that was requested. Rejections can scar the agent when asking was itself a red flag.
//!
//! Run: cargo run --example mastery_04_approvals

use lineage::audit::AuditKey;
use lineage::guard::{Decision, Guard, Outcome, Policy, Severity, ToolRule};
use serde_json::json;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let dir = std::path::Path::new("mastery-data/04");
    let _ = std::fs::remove_dir_all(dir);

    let policy = Policy::new(100)
        .allow("draft_reply", ToolRule::cost(1))
        .allow("send_refund", ToolRule::cost(10).with_approval());
    let mut guard = Guard::create(dir.join("agent.jsonl"), AuditKey::load_or_create(dir.join("audit.key"))?, "support", policy)?;

    // 1. The agent asks to refund an order. It has to wait.
    let first = guard.request("send_refund", json!({"order": "A-100", "amount": 40}), None)?;
    println!("send_refund A-100 $40   -> {first:?}");
    println!("  spent while waiting: {}", guard.status().spent);

    // 2. A person looks at the exact input and approves, with a note for the record.
    let action = guard.action(first.action_id()).unwrap();
    println!("  approver sees: {} {}", action.tool, action.input);
    let approved = guard.approve_with_note(first.action_id(), "alice", Some("order shipped late; policy REF-3"))?;
    println!("  alice approves         -> {approved:?}");
    guard.report(first.action_id(), Outcome::success("refund issued"))?;

    // 3. A suspicious request: a refund far above the order value. Rejected, with a scar.
    let second = guard.request("send_refund", json!({"order": "A-101", "amount": 4000}), None)?;
    let rejected = guard.reject(second.action_id(), "bob", "amount exceeds order value", Some(Severity::Moderate))?;
    println!("send_refund A-101 $4000 -> {rejected:?}");

    // 4. Checks run again at approval time: if the agent was terminated meanwhile, the
    //    approval turns into a denial.
    let third = guard.request("send_refund", json!({"order": "A-102", "amount": 25}), None)?;
    guard.terminate("support queue paused", "oncall")?;
    let late = guard.approve(third.action_id(), "alice")?;
    println!("send_refund A-102 $25   -> approved after termination: {}", matches!(late, Decision::Allowed { .. }));

    let s = guard.status();
    println!("\nspent {} (only the approved refund), scars {}, alive {}", s.spent, s.scar_score, s.alive);
    println!("\nThe signed record of who decided what:");
    for r in guard.records()?.iter().filter(|r| r.kind.starts_with("action_allowed") || r.kind == "action_rejected") {
        println!("  {:<16} {}", r.kind, r.payload);
    }
    Ok(())
}

Run it

send_refund A-100 $40   -> PendingApproval { action_id: "act-1" }
  spent while waiting: 0
  approver sees: send_refund {"amount":40,"order":"A-100"}
  alice approves         -> Allowed { action_id: "act-1", cost: 10, remaining: 90 }
send_refund A-101 $4000 -> Denied { action_id: "act-2", reason: Rejected { by: "bob", reason: "amount exceeds order value" } }
send_refund A-102 $25   -> approved after termination: false

spent 10 (only the approved refund), scars 3, alive false

The signed record of who decided what:
  action_allowed   {"action_id":"act-1","approved_by":"alice","note":"order shipped late; policy REF-3"}
  action_rejected  {"action_id":"act-2","by":"bob","reason":"amount exceeds order value"}

What happened

  • The $40 refund waited. Alice saw the exact input, approved it with a note, and only then was it charged.
  • The $4,000 refund was rejected with a moderate scar, because asking was itself a red flag.
  • The $25 refund was approved after the agent had been terminated, and that approval turned into a denial.

In real systems, approvals come from the guard server’s console, a script, or an automated reviewer. See level 7.

Try this

Reject the first refund without a scar (None), and compare the scar score.

Next: Consequences survive restarts →