open-standards/joule-code/joule-code-rs/crates/joule-code-jcp/src/lib.rs
Transaction Science 73eb2999aa joule-code-jcp: public authorize + self-issued local() gate for non-Stage consumers
The gate's decision logic was reachable only through wrap()/GatedStage —
fine for a Stage-based cascade, but a consumer whose tiers are async
(a model call, a remote ACP turn) can't express them as the sync Stage
trait. Two small additions open the same gate to them:

- JcpGate::authorize is now public: `if gate.authorize(tier, &req) { run }`
  — the exact per-consultation decision (decide_chained against the chain,
  debit on allow, seal the JCR-1 receipt either way) that GatedStage runs,
  callable directly.
- JcpGate::local(resource, actions, budget_uj) issues a self-signed,
  single-link grant naming the tier verbs the consumer is willing to use,
  under a joule ceiling. The air-gapped, own-issuer case: omit "l4" and the
  remote frontier agent is structurally unreachable — no policy server
  required. Deterministic local keys; receipts still seal and aggregate.

Two tests: granted tiers allow / ungranted l4 denies with all three
consultations sealed; budget caps total spend at the third L3 attempt.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-10 15:56:27 -04:00

375 lines
15 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! Joule Code as a JCP consumer: the L0–L4 cascade gated by a metered Grant.
//!
//! The cascade's stop-at-first-close rule picks the *cheapest sufficient* tier;
//! this crate adds the missing half of the sentence — cheapest sufficient
//! **and authorized**. Every stage consultation is decided by the JCP runtime
//! against a delegation chain *before* the stage runs: the tier is the
//! `action`, the source tree is the content-addressed `resource`, and the
//! tier's energy class is debited from the grant on allow. A tier the grant
//! doesn't name (no `l4` action → no remote frontier agent, ever) or can't
//! afford is skipped, and the cascade falls through to nothing rather than
//! overspend. Every consultation — allowed and denied — seals as a JCR-1
//! receipt.
//!
//! This is the L4 story made structural: the escape hatch to a remote agent
//! exists only when a signed grant says so, with a joule ceiling the runtime
//! enforces outside any model.
use std::sync::{Arc, Mutex};
use ed25519_dalek::SigningKey;
use jcp_grant::issue;
use jcp_runtime::Runtime;
use jcp_schema::{
Capability, ContentId, Did, Energy, Frame, FrameKind, Grant, SignedGrant, Trust,
};
use joule_code_cascade::{Request, Resolution, Stage};
use joule_code_schema::Tier;
/// The JCP action string for a tier — the verb a grant must name for the
/// cascade to consult that tier.
pub fn tier_action(tier: Tier) -> &'static str {
match tier {
Tier::L0 => "l0",
Tier::L1 => "l1",
Tier::L2 => "l2",
Tier::L3 => "l3",
Tier::L4 => "l4",
}
}
/// Order-of-magnitude attempt cost per tier, in µJ, with honest `Estimator`
/// provenance — the class markers of SPEC §1.1, not measurements. A deployment
/// with a real meter substitutes measured figures via [`JcpGate::with_costs`].
pub fn tier_cost_estimate(tier: Tier) -> u64 {
match tier {
Tier::L0 => 1, // picojoule class, floored at 1 µJ integer
Tier::L1 => 10, // nanojoule class
Tier::L2 => 1_000, // sub-millijoule class
Tier::L3 => 1_000_000, // joule class
Tier::L4 => 10_000_000, // tens-of-joules class — the escape hatch
}
}
struct Inner {
runtime: Runtime,
chain: Vec<SignedGrant>,
caller: Did,
steward_sk: SigningKey,
steward: String,
costs: fn(Tier) -> u64,
now: u64,
receipts: Vec<Vec<u8>>,
}
/// A shared JCP gate for one cascade: the runtime that decides every stage
/// consultation, the delegation chain it decides against, and the receipt
/// stream it seals. Wrap each stage with [`JcpGate::wrap`].
pub struct JcpGate {
inner: Mutex<Inner>,
}
impl JcpGate {
/// Open a gate over a delegation chain (root first; a single grant is a
/// one-link chain). `caller` is the acting agent — the leaf's subject.
pub fn new(
chain: Vec<SignedGrant>,
caller: Did,
steward_sk: SigningKey,
steward: impl Into<String>,
) -> Arc<Self> {
Arc::new(Self {
inner: Mutex::new(Inner {
runtime: Runtime::new(),
chain,
caller,
steward_sk,
steward: steward.into(),
costs: tier_cost_estimate,
now: 0,
receipts: Vec::new(),
}),
})
}
/// Open a gate over a fresh, self-signed grant naming `actions` (the tier
/// verbs `"l0"`..`"l4"`) with a `budget_uj` joule ceiling over `resource` (a
/// repo/tree identifier). The convenience for a single-endpoint consumer
/// that is its own issuer — the air-gapped, no-external-authority case: it
/// grants itself exactly the tiers it is willing to use (omit `"l4"` and the
/// remote frontier agent is unreachable, structurally) under a ceiling the
/// runtime enforces. Deterministic local keys; the receipts still seal.
pub fn local(resource: &str, actions: &[&str], budget_uj: u64) -> Arc<Self> {
let issuer = SigningKey::from_bytes(&[0x6a; 32]);
let did = Did::from_verifying_key(&issuer.verifying_key());
let grant = Grant {
issuer: did,
subject: did,
capability: Capability::new(
ContentId::of_bytes(resource.as_bytes()),
actions.iter().copied(),
),
budget_uj,
expiry: 0,
parent: None,
delegate_depth: None,
nonce: [0; 16],
};
let signed = issue(&issuer, grant).expect("self-issued local grant is valid");
let steward = SigningKey::from_bytes(&[0x5e; 32]);
Self::new(vec![signed], did, steward, "did:joule-code:local")
}
/// Substitute a per-tier cost table (e.g. host-calibrated figures).
pub fn with_costs(self: &Arc<Self>, costs: fn(Tier) -> u64) -> &Arc<Self> {
self.inner.lock().expect("gate lock").costs = costs;
self
}
/// Set decision time (seconds). Time is an input, not ambient state.
pub fn set_now(&self, now: u64) {
self.inner.lock().expect("gate lock").now = now;
}
/// Wrap a stage so every consultation is decided by this gate first.
pub fn wrap<S: Stage + 'static>(self: &Arc<Self>, stage: S) -> GatedStage<S> {
GatedStage { stage, gate: Arc::clone(self) }
}
/// Joules debited so far against the leaf grant.
pub fn spent(&self) -> u64 {
let inner = self.inner.lock().expect("gate lock");
let leaf = inner.chain.last().expect("non-empty chain").grant.id();
inner.runtime.spent(&leaf)
}
/// The sealed JCR-1 receipts for every consultation, allowed and denied.
pub fn receipts(&self) -> Vec<Vec<u8>> {
self.inner.lock().expect("gate lock").receipts.clone()
}
/// Decide one stage consultation. On allow the tier's cost is debited from
/// every link of the chain; either way the decision seals. Public so a
/// consumer whose tiers are not [`Stage`]s (e.g. async tiers) can gate them
/// directly: `if gate.authorize(tier, &req) { /* run the tier */ }`.
pub fn authorize(&self, tier: Tier, req: &Request) -> bool {
let mut inner = self.inner.lock().expect("gate lock");
let leaf = inner.chain.last().expect("non-empty chain").grant.clone();
let frame = Frame {
kind: FrameKind::AgentTask,
trust: Trust::Control,
grant_id: leaf.id(),
action: tier_action(tier).to_string(),
resource: leaf.capability.resource,
payload_hash: ContentId::of_bytes(format!("{}\n{}", req.prior, req.intent).as_bytes()),
};
let cost = Energy::estimator((inner.costs)(tier));
let (caller, now) = (inner.caller, inner.now);
let chain = inner.chain.clone();
let decision = inner.runtime.decide_chained(&frame, &chain, caller, cost, now);
let chain_ids: Vec<_> = chain.iter().map(|sg| sg.grant.id()).collect();
if let Ok(receipt) =
jcp_receipt::seal_chained(&frame, &chain_ids, &leaf, &decision, &inner.steward_sk, &inner.steward)
{
inner.receipts.push(receipt);
}
decision.is_allow()
}
}
/// A cascade stage that consults the JCP gate before running. A denied tier
/// returns `None` — the cascade falls through exactly as if the tier could not
/// close the request, because for this caller it can't.
pub struct GatedStage<S: Stage> {
stage: S,
gate: Arc<JcpGate>,
}
impl<S: Stage> Stage for GatedStage<S> {
fn tier(&self) -> Tier {
self.stage.tier()
}
fn try_resolve(&self, req: &Request) -> Option<Resolution> {
if !self.gate.authorize(self.stage.tier(), req) {
return None;
}
self.stage.try_resolve(req)
}
}
#[cfg(test)]
mod tests {
use super::*;
use jcp_grant::issue;
use jcp_schema::{Capability, Grant};
use joule_code_cascade::Cascade;
fn sk(s: u8) -> SigningKey {
SigningKey::from_bytes(&[s; 32])
}
fn did(k: &SigningKey) -> Did {
Did::from_verifying_key(&k.verifying_key())
}
/// A stage that always closes at its tier.
struct Always(Tier);
impl Stage for Always {
fn tier(&self) -> Tier {
self.0
}
fn try_resolve(&self, _req: &Request) -> Option<Resolution> {
Some(Resolution {
op: None,
answer: Some(format!("closed at {:?}", self.0)),
receipt: joule_code_receipt::Receipt {
op_id: joule_code_schema::ContentId::of_bytes(b"op"),
tier: self.0,
energy: joule_code_receipt::Energy {
measured_uj: None,
estimated_uj: tier_cost_estimate(self.0),
provenance: joule_code_receipt::Provenance::Estimator,
},
sequence: 0,
nonce: [0; 8],
},
})
}
}
/// A stage that never closes (e.g. a cache miss).
struct Never(Tier);
impl Stage for Never {
fn tier(&self) -> Tier {
self.0
}
fn try_resolve(&self, _req: &Request) -> Option<Resolution> {
None
}
}
fn grant_for(actions: &[&str], budget: u64) -> (Vec<SignedGrant>, Did) {
let issuer = sk(1);
let agent = sk(2);
let tree = ContentId::of_bytes(b"repo:joule-code");
let g = Grant {
issuer: did(&issuer),
subject: did(&agent),
capability: Capability::new(tree, actions.iter().copied()),
budget_uj: budget,
expiry: 0,
parent: None,
delegate_depth: None,
nonce: [0; 16],
};
(vec![issue(&issuer, g).unwrap()], did(&agent))
}
fn req() -> Request {
Request { prior: "tree-hash".into(), intent: "rename the function".into() }
}
#[test]
fn ungranted_l4_never_fires_even_when_it_would_close() {
// The grant names l0–l3 but NOT l4: no remote frontier agent, ever.
let (chain, caller) = grant_for(&["l0", "l1", "l2", "l3"], u64::MAX);
let gate = JcpGate::new(chain, caller, sk(7), "did:example:joule-code");
let cascade = Cascade::new([
Box::new(gate.wrap(Never(Tier::L1))) as Box<dyn Stage>,
Box::new(gate.wrap(Always(Tier::L4))),
]);
assert!(cascade.resolve(&req()).is_none()); // L4 denied → nothing closes
// both consultations sealed: one allow (L1, didn't close), one deny (L4)
assert_eq!(gate.receipts().len(), 2);
}
#[test]
fn granted_l4_closes_and_debits_its_class() {
let (chain, caller) = grant_for(&["l1", "l4"], 20_000_000);
let gate = JcpGate::new(chain, caller, sk(7), "did:example:joule-code");
let cascade = Cascade::new([
Box::new(gate.wrap(Never(Tier::L1))) as Box<dyn Stage>,
Box::new(gate.wrap(Always(Tier::L4))),
]);
let res = cascade.resolve(&req()).expect("granted L4 closes");
assert_eq!(res.receipt.tier, Tier::L4);
// L1 attempt (10) + L4 attempt (10_000_000) debited
assert_eq!(gate.spent(), 10_000_010);
}
#[test]
fn an_exhausted_budget_stops_the_escape_hatch() {
// budget affords exactly one L4 consultation (plus the L1 probes)
let (chain, caller) = grant_for(&["l1", "l4"], 10_000_020);
let gate = JcpGate::new(chain, caller, sk(7), "did:example:joule-code");
let cascade = Cascade::new([
Box::new(gate.wrap(Never(Tier::L1))) as Box<dyn Stage>,
Box::new(gate.wrap(Always(Tier::L4))),
]);
assert!(cascade.resolve(&req()).is_some()); // first run closes at L4
assert!(cascade.resolve(&req()).is_none()); // second: L4 no longer affordable
// the deny is sealed evidence, not a silent skip
let steward_vk = sk(7).verifying_key().to_bytes();
let receipts = gate.receipts();
let last = jcp_receipt::extract(receipts.last().unwrap(), "did:example:joule-code", &steward_vk).unwrap();
assert_eq!(last.verdict, "deny");
assert_eq!(last.deny_reason.as_deref(), Some("budget-exceeded"));
}
#[test]
fn a_local_self_issued_gate_still_walls_off_l4() {
// the air-gapped case: the runtime grants itself l0–l3 only
let gate = JcpGate::local("repo:local", &["l0", "l1", "l2", "l3"], 1_000_000);
let cascade = Cascade::new([
Box::new(gate.wrap(Always(Tier::L4))) as Box<dyn Stage>,
]);
assert!(cascade.resolve(&req()).is_none()); // no l4 in the self-grant
assert_eq!(gate.spent(), 0);
let cascade = Cascade::new([
Box::new(gate.wrap(Always(Tier::L1))) as Box<dyn Stage>,
]);
assert!(cascade.resolve(&req()).is_some()); // granted tiers still close
}
#[test]
fn cheaper_tiers_stay_cheap_and_receipts_attribute_per_tier() {
let (chain, caller) = grant_for(&["l0", "l1"], 1_000);
let gate = JcpGate::new(chain, caller, sk(7), "did:example:joule-code");
let cascade = Cascade::new([
Box::new(gate.wrap(Always(Tier::L1))) as Box<dyn Stage>,
]);
for _ in 0..3 {
assert!(cascade.resolve(&req()).is_some());
}
assert_eq!(gate.spent(), 30);
let steward_vk = sk(7).verifying_key().to_bytes();
let receipts = gate.receipts();
let (totals, bad) =
jcp_receipt::aggregate(receipts.iter().map(|r| r.as_slice()), "did:example:joule-code", &steward_vk);
assert_eq!(bad, 0);
assert_eq!(totals.values().map(|t| t.spent_uj).sum::<u64>(), 30);
}
#[test]
fn local_gate_authorizes_granted_tiers_and_structurally_denies_the_rest() {
// A self-issued local grant naming l0..l3 but NOT l4.
let gate = JcpGate::local("repo:test", &["l0", "l1", "l2", "l3"], u64::MAX);
let request = req();
assert!(gate.authorize(Tier::L0, &request));
assert!(gate.authorize(Tier::L3, &request));
// l4 is not in the grant → the remote frontier agent is unreachable.
assert!(!gate.authorize(Tier::L4, &request));
// Every consultation — allow and deny — sealed a receipt.
assert_eq!(gate.receipts().len(), 3);
}
#[test]
fn local_gate_budget_caps_total_spend() {
// Budget affords two L3 consultations (1_000_000 µJ each), not a third.
let gate = JcpGate::local("repo:test", &["l3"], 2_000_000);
let request = req();
assert!(gate.authorize(Tier::L3, &request));
assert!(gate.authorize(Tier::L3, &request));
assert!(!gate.authorize(Tier::L3, &request)); // over budget → denied
assert_eq!(gate.spent(), 2_000_000);
}
}