open-standards/joule-code/joule-code-rs/crates/joule-code-jcp/src/lib.rs
Transaction Science 289d584b02
All checks were successful
standards workspaces build + test / which workspaces this change touches (pull_request) Successful in 15s
repository checks / lockfiles, tests in CI, workflow hygiene, spec and section references, public text (pull_request) Successful in 1m24s
joule-code conformance gate / Build + round-trip C1-C4 vectors (pull_request) Successful in 2m3s
jouleclaw public surface must stay documented / Public-surface audit (jouleclaw-rs) (pull_request) Successful in 58s
standards workspaces build + test / joule-code-rs (joule-code/joule-code-rs, joule-code-rs, linux-arm64, true) (pull_request) Successful in 10m11s
standards workspaces build + test / jcp-rs (jcp-conformance, jcp/jcp-rs, -p jcp-energy --features nvml, jcp-rs, linux) (pull_request) Successful in 1m52s
standards workspaces build + test / joule-ui-rs (joule-ui/joule-ui-rs, joule-ui-rs, linux) (pull_request) Successful in 1m11s
standards workspaces build + test / joulecontract-rs (joulecontract/joulecontract-rs, joulecontract-rs, linux-arm64, true) (pull_request) Successful in 4m31s
standards workspaces build + test / joulehook-rs (joulehook/joulehook-rs, joulehook-rs, linux) (pull_request) Successful in 1m14s
standards workspaces build + test / jouleclaw-rs (-nodebug, jouleclaw/jouleclaw-rs, -p jouleclaw-energy --features rapl,nvml, jouleclaw-rs, linux-arm64, -C target-feature=+fp16) (pull_request) Successful in 17m16s
standards workspaces build + test / mesh-rs (mesh/mesh-rs, -p mesh-engine-gguf -p mesh-openai --features mesh-engine-gguf/gguf,mesh-openai/server, mesh-rs, linux-arm64) (pull_request) Successful in 1m45s
standards workspaces build + test / proof-rs (proof-conformance, proof/proof-rs, proof-rs, linux, true) (pull_request) Successful in 1m37s
standards workspaces build + test / sandbox-rs (sandbox-conformance, sandbox/sandbox-rs, sandbox-rs, linux) (pull_request) Successful in 2m28s
standards workspaces build + test / macOS energy backends (pull_request) Successful in 7m41s
standards workspaces build + test / jouledesk-rs (jouledesk/jouledesk-rs, jouledesk-rs, macos) (pull_request) Successful in 1m54s
jcp: a ModelIn caveat says which model a grant authorizes, and the frame names the model it runs
A grant's capability names one resource by content address. When that
resource is an inference endpoint, the model it runs was chosen inside the
payload, and the decision point sees only the payload's hash. So a grant
could authorize "this endpoint" but never "this endpoint, running these
models". Only PayloadIs came close, and it makes the grant single-use. JCP
enforced a model's integrity (refuse wrong bytes) but not authorization
over which model runs.

- `Frame.model: Option<ContentId>` is the content address of the model the
  action runs. The dispatching runtime fills it, since it knows what it will
  load. It is omitted from the encoding when absent, so frames and every
  receipt and vector over them keep their bytes.
- `Caveat::ModelIn(Vec<ContentId>)` requires the frame to name one of these
  models. It fails closed: a frame naming no model is denied
  `caveat-unsatisfied`, so a dispatcher that does not say what it will run
  cannot exercise the grant. A delegate narrows the set by adding a smaller
  ModelIn beside the inherited one; both must hold, so the intersection
  governs. This is authorization by content address, not a registry
  (SPEC §12).
- SPEC §4.9 and §6 describe both, and C24's conformance check now covers
  ModelIn.
- Every Frame literal in the repository gains `model: None`. That is 28
  sites across jcp-rs, joule-code-rs, joule-ui-rs, jouleclaw-rs and
  sandbox-rs.

Checked on the build host:
- jcp-rs: 496 tests pass, and the conformance suite passes all checks,
  including C24 with ModelIn.
- Every vector gen-vectors writes is byte-identical to the published one.
- All eight workspaces that depend on jcp-schema pass `cargo check
  --all-targets`. The consumers jouleclaw-jcp, joule-code-jcp, sandbox-jcp
  and joule-ui pass their tests.
- Mutations: letting a frame with no model pass, and encoding an absent
  model, each fail tests.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-17 04:21:21 -04:00

380 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,
budget_funds: None,
caveats: Vec::new(),
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()),
model: None,
};
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,
budget_funds: None,
caveats: Vec::new(),
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);
}
}