The `cite` example records a retrieval of 4_200 µJ that no fetch spent (no document is read) and decided and sealed it as Energy::measured, so its jcp receipt claimed a hardware reading of work no meter saw. The cost now comes from `retrieval_cost()`, the citation's figure graded Estimator with no measured figure; the docs and the first output line say the retrieval is illustrative. The figure, and so the receipt's cost_uj and the citation commitment, are unchanged. tests/cite_example.rs compiles the example in as a module: its cost must be Estimator with no measured figure, and the receipt sealed over it must carry energy_provenance "Estimator", decoded from the signed core. Co-Authored-By: Claude Opus 5.5 <[email protected]>
111 lines
4.7 KiB
Rust
111 lines
4.7 KiB
Rust
//! An agent answers a question, records what it READ, and seals a receipt that
|
|
//! commits the provenance. Then a reviewer checks the three things a rendered
|
|
//! citation link cannot tell them: is the answer unedited, are the sources
|
|
//! intact, and which sources may this reviewer not open.
|
|
//!
|
|
//! ```text
|
|
//! cargo run -p jcp-cite --example cite
|
|
//! ```
|
|
//!
|
|
//! The retrieval is illustrative: no document is fetched, so the energy the
|
|
//! citation records is a chosen figure, and the decision it is debited under
|
|
//! seals it as an estimate ([`retrieval_cost`]) — never as a hardware reading
|
|
//! of work no meter saw.
|
|
|
|
use ed25519_dalek::SigningKey;
|
|
use jcp_cite::{verify, Citation, Source};
|
|
use jcp_grant::issue;
|
|
use jcp_receipt::{extract, seal};
|
|
use jcp_runtime::Runtime;
|
|
use jcp_schema::{Capability, ContentId, Did, Energy, Grant};
|
|
|
|
fn main() {
|
|
let issuer = SigningKey::from_bytes(&[1u8; 32]);
|
|
let agent = SigningKey::from_bytes(&[2u8; 32]);
|
|
let agent_did = Did::from_verifying_key(&agent.verifying_key());
|
|
let corpus = ContentId::of_bytes(b"acme-workspace");
|
|
|
|
// The agent holds two grants — one per sensitivity domain.
|
|
let g_finance = ContentId::of_bytes(b"grant:finance");
|
|
let g_people = ContentId::of_bytes(b"grant:people");
|
|
|
|
// It reads two documents to answer, and records EACH with the content it saw
|
|
// and the grant it read under.
|
|
let doc_a = b"Q3 revenue was 4.2M, up 18% YoY.";
|
|
let doc_b = b"Headcount plan: 12 hires in Q4.";
|
|
let answer = b"Revenue grew 18%, and 12 hires are planned for Q4.";
|
|
|
|
let citation = Citation::new(
|
|
agent_did,
|
|
ContentId::of_bytes(answer),
|
|
vec![
|
|
Source::new(ContentId::of_bytes(doc_a), g_finance).at("/finance/q3-report"),
|
|
Source::new(ContentId::of_bytes(doc_b), g_people).at("/people/q4-plan"),
|
|
],
|
|
4_200, // µJ the retrieval is put at: illustrative, nothing read it
|
|
[7; 16],
|
|
);
|
|
println!(
|
|
"agent read {} sources → answer, {} µJ (illustrative: sealed as an estimate)",
|
|
citation.sources.len(),
|
|
citation.energy_uj
|
|
);
|
|
|
|
// The retrieval is decided and metered under a real grant, and the receipt
|
|
// commits the citation by content id.
|
|
let grant = Grant {
|
|
issuer: Did::from_verifying_key(&issuer.verifying_key()),
|
|
subject: agent_did,
|
|
capability: Capability::new(corpus, ["retrieve"]),
|
|
budget_uj: 1_000_000,
|
|
budget_funds: None,
|
|
caveats: Vec::new(),
|
|
expiry: 0,
|
|
parent: None,
|
|
delegate_depth: None,
|
|
nonce: [0; 16],
|
|
};
|
|
let sg = issue(&issuer, grant.clone()).unwrap();
|
|
let frame = citation.frame(grant.id(), corpus).unwrap();
|
|
let decision = Runtime::new().decide(&frame, &sg, agent_did, retrieval_cost(&citation), 1);
|
|
let steward = SigningKey::from_bytes(&[9u8; 32]);
|
|
let receipt = seal(&frame, &grant, &decision, &steward, "did:example:steward").unwrap();
|
|
let core = extract(&receipt, "did:example:steward", &steward.verifying_key().to_bytes()).unwrap();
|
|
|
|
let sealed = citation.clone().seal(&agent).unwrap();
|
|
println!(
|
|
"receipt commits the citation: {} ({} µJ on the receipt)\n",
|
|
core.payload_hash.as_deref() == Some(citation.id().unwrap().to_hex().as_str()),
|
|
core.cost_uj
|
|
);
|
|
|
|
// ---- a reviewer, holding only the receipt + the signed citation ----
|
|
println!("── reviewer checks ──");
|
|
println!(" citation signature valid: {}", verify(&sealed).is_ok());
|
|
println!(" answer unedited: {}", sealed.citation.verify_answer(answer).is_ok());
|
|
println!(" source 0 intact: {}", sealed.citation.verify_source(0, doc_a).is_ok());
|
|
|
|
// Now the finance doc is edited after the fact. A URL would still resolve —
|
|
// to the NEW text. The content-bound citation catches it.
|
|
let edited = b"Q3 revenue was 9.9M.";
|
|
println!(
|
|
" source 0 after an edit: {} ← dangling detected, not silently wrong",
|
|
sealed.citation.verify_source(0, edited).is_ok()
|
|
);
|
|
|
|
// A reviewer cleared only for finance is TOLD which sources they may not open.
|
|
let unreadable = sealed.citation.unreadable_by(&[g_finance]);
|
|
println!(" sources this reviewer may not open: {:?} (of {})", unreadable, sealed.citation.sources.len());
|
|
for i in &unreadable {
|
|
if let Some(loc) = sealed.citation.sources[*i].locator.as_deref() {
|
|
println!(" → {loc} requires authority you do not hold");
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The cost the retrieval is debited and sealed at: the citation's recorded
|
|
/// figure, graded `Estimator` with no measured figure, because nothing in
|
|
/// this example read it.
|
|
pub fn retrieval_cost(citation: &Citation) -> Energy {
|
|
Energy::estimator(citation.energy_uj)
|
|
}
|