From 6d7bc256a5d8f294055a1b7213feef013cf36d26 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Tue, 13 Oct 2026 02:41:41 -0400 Subject: [PATCH 1/2] jcp-intercept: a reading meter meters a seam as one span, divided among its steps by CPU time Every interceptor step was metered as a span of its own. Where the meter reads this process's energy count, each read first takes the reading thread off its core (#229): tens of microseconds and its own wake-up. An interceptor step is often a few microseconds, so a span of its own read as mostly the meter, and a chain of k interceptors paid 2k reads per seam. A meter that reads a counter now meters the whole seam pass as one span (two reads per seam), and the steps are sealed after it closes, each with its share of the reading by its own thread CPU time, which is exact and read at no cost. The shares are integer micro-joules that sum to the reading exactly (largest remainder); a share of a reading is ModelBased, a pass of one step keeps the reading's own grade, and coverage is the pass's. A meter that reads nothing (a model, a constant) still meters each step on its own, so the receipts sealed with it are byte-identical. jcp-energy exposes the thread CPU clock it already had (`thread_cpu_time`). On the M3 Ultra, three steps burning 2, 4 and 8 ms were sealed 10.8, 21.5 and 42.8 mJ, summing to the chain's figure and to 96-97% of an independent read around the pass, in 10 of 10 rounds. Co-Authored-By: Claude Opus 5.5 --- jcp/jcp-rs/Cargo.lock | 1 + jcp/jcp-rs/crates/jcp-energy/src/lib.rs | 10 + jcp/jcp-rs/crates/jcp-intercept/Cargo.toml | 5 + jcp/jcp-rs/crates/jcp-intercept/src/lib.rs | 229 +++++++++++++++--- .../jcp-intercept/tests/one_span_per_pass.rs | 115 +++++++++ 5 files changed, 324 insertions(+), 36 deletions(-) create mode 100644 jcp/jcp-rs/crates/jcp-intercept/tests/one_span_per_pass.rs diff --git a/jcp/jcp-rs/Cargo.lock b/jcp/jcp-rs/Cargo.lock index b5ec2fed0..4f29664be 100644 --- a/jcp/jcp-rs/Cargo.lock +++ b/jcp/jcp-rs/Cargo.lock @@ -657,6 +657,7 @@ dependencies = [ "jcp-energy", "jcp-receipt", "jcp-schema", + "libc", "serde_json", "thiserror 2.0.20", ] diff --git a/jcp/jcp-rs/crates/jcp-energy/src/lib.rs b/jcp/jcp-rs/crates/jcp-energy/src/lib.rs index 06a6c1872..8c596f58c 100644 --- a/jcp/jcp-rs/crates/jcp-energy/src/lib.rs +++ b/jcp/jcp-rs/crates/jcp-energy/src/lib.rs @@ -5527,6 +5527,16 @@ impl Ladder<'static> { } } +/// CPU time the calling thread has used, user and system +/// (`clock_gettime(CLOCK_THREAD_CPUTIME_ID)`), or `None` where the clock cannot +/// be read. Exact to the nanosecond and live: unlike a process's energy count, +/// which a thread's work joins only when the thread leaves its core, the +/// thread's CPU time is computed at the read. So it divides a span's reading +/// among work that ran in turn on one thread, at no cost to the span. +pub fn thread_cpu_time() -> Option { + liveness::thread_cpu_time() +} + impl Meter { /// Pick the best backend this environment exposes. Never fails: when no /// hardware interface exists the result is the time×power figure at the diff --git a/jcp/jcp-rs/crates/jcp-intercept/Cargo.toml b/jcp/jcp-rs/crates/jcp-intercept/Cargo.toml index d14132315..5e8734d0b 100644 --- a/jcp/jcp-rs/crates/jcp-intercept/Cargo.toml +++ b/jcp/jcp-rs/crates/jcp-intercept/Cargo.toml @@ -18,3 +18,8 @@ thiserror = { workspace = true } [lints] workspace = true + +# The real-counter test reads this process's energy count with its own FFI: +# an oracle that shares no code with the meter. +[target.'cfg(target_os = "macos")'.dev-dependencies] +libc = "0.2" diff --git a/jcp/jcp-rs/crates/jcp-intercept/src/lib.rs b/jcp/jcp-rs/crates/jcp-intercept/src/lib.rs index a93e3fe3c..6cd6cafdd 100644 --- a/jcp/jcp-rs/crates/jcp-intercept/src/lib.rs +++ b/jcp/jcp-rs/crates/jcp-intercept/src/lib.rs @@ -22,6 +22,18 @@ //! and a figure that covers only part of the step says so (`"coverage": //! "Partial"`), rather than being sealed as the whole step's energy. //! +//! Where the meter reads a counter, a seam is metered as one span around its +//! whole pass, not one per step ([`Chain::before_model`]). A counter read for +//! one process first takes the reading thread off its core, so each read +//! costs tens of microseconds and its own wake-up; an interceptor step is +//! often a few microseconds, which a span of its own would read as mostly the +//! meter. The pass's reading is divided among its steps by each step's own +//! thread CPU time ([`jcp_energy::thread_cpu_time`], exact and free to +//! read), so the steps' figures sum to the reading exactly; a step's share of +//! a reading is `ModelBased`, and a pass with one step keeps the reading's +//! own grade. A meter that reads nothing (a model or a constant) costs +//! nothing to run and still meters each step on its own. +//! //! A [`Chain`] runs its interceptors forward through the `before_*` seams and in //! reverse through `after_model` (the onion unwinds), stopping at the first //! [`Step::Halt`] (a guardrail refused) or [`Step::Hold`] (a human must approve). @@ -32,7 +44,8 @@ use ed25519_dalek::SigningKey; use jcp_energy::Meter; use jcp_receipt::jcr1; -use jcp_schema::{ContentId, Coverage, Energy}; +use jcp_schema::{ContentId, Coverage, Energy, Provenance}; +use std::time::Duration; use serde_json::{json, Value}; // ───────────────── the request/response/tool an interceptor transforms ───────────────── @@ -269,59 +282,98 @@ impl Chain { /// Run the request through every interceptor's `before_model` seam, forward /// order, sealing each step. Stops at the first refusal/hold. + /// + /// Where the meter reads a counter, the whole pass is one span and each + /// step is sealed with its share of it by thread CPU time (see the crate + /// docs); otherwise each step is metered on its own. pub fn before_model(&mut self, req: &mut ModelRequest) -> ChainOutcome { - for idx in 0..self.interceptors.len() { + let order: Vec = (0..self.interceptors.len()).collect(); + self.run_seam(Seam::BeforeModel, &order, |interceptor| { let before = req.digest(); - // Split-borrow: the meter (shared) and one interceptor (mut) are - // disjoint fields, so the metered call touches no other chain state. - let (step, energy) = { - let meter = &self.meter; - let interceptor = &mut self.interceptors[idx]; - meter.measure(|| interceptor.before_model(req)) - }; - let after = req.digest(); - let name = self.interceptors[idx].name().to_string(); - self.seal(&name, Seam::BeforeModel, &step, before, after, energy); - if let Some(outcome) = terminal(&name, &step) { - return outcome; - } - } - ChainOutcome::Proceed + let step = interceptor.before_model(req); + (step, before, req.digest()) + }) } /// Run the response through every interceptor's `after_model` seam, **reverse** /// order (the onion unwinding), sealing each step. pub fn after_model(&mut self, req: &ModelRequest, resp: &mut ModelResponse) -> ChainOutcome { - for idx in (0..self.interceptors.len()).rev() { + let order: Vec = (0..self.interceptors.len()).rev().collect(); + self.run_seam(Seam::AfterModel, &order, |interceptor| { let before = resp.digest(); - let (step, energy) = { - let meter = &self.meter; - let interceptor = &mut self.interceptors[idx]; - meter.measure(|| interceptor.after_model(req, resp)) - }; - let after = resp.digest(); - let name = self.interceptors[idx].name().to_string(); - self.seal(&name, Seam::AfterModel, &step, before, after, energy); - if let Some(outcome) = terminal(&name, &step) { - return outcome; - } - } - ChainOutcome::Proceed + let step = interceptor.after_model(req, resp); + (step, before, resp.digest()) + }) } /// Run a tool call through every interceptor's `before_tool` seam, forward /// order, sealing each step. Stops at the first refusal/hold. pub fn before_tool(&mut self, call: &mut ToolCall) -> ChainOutcome { - for idx in 0..self.interceptors.len() { + let order: Vec = (0..self.interceptors.len()).collect(); + self.run_seam(Seam::BeforeTool, &order, |interceptor| { let before = call.digest(); - let (step, energy) = { + let step = interceptor.before_tool(call); + (step, before, call.digest()) + }) + } + + /// Run one seam over the interceptors in `order`, stopping after the + /// first step that halts or holds, and seal every step that ran. + /// + /// A meter that reads a counter meters the pass as one span; the steps are + /// sealed after it closes, each with its [`apportion`]ed share by its own + /// thread CPU time, in the order they ran. Any other meter meters each step + /// as it runs, as one span of its own. + fn run_seam( + &mut self, + seam: Seam, + order: &[usize], + mut step_at: impl FnMut(&mut dyn Interceptor) -> (Step, ContentId, ContentId), + ) -> ChainOutcome { + if order.is_empty() { + return ChainOutcome::Proceed; + } + if self.meter.provenance() == Provenance::HwShunt { + let (ran, pass) = { + let meter = &self.meter; + let interceptors = &mut self.interceptors; + meter.measure(|| { + let mut ran = Vec::with_capacity(order.len()); + for &idx in order { + let cpu_before = jcp_energy::thread_cpu_time(); + let (step, before, after) = step_at(interceptors[idx].as_mut()); + let cpu = match (cpu_before, jcp_energy::thread_cpu_time()) { + (Some(a), Some(b)) => Some(b.saturating_sub(a)), + _ => None, + }; + let stops = !matches!(step, Step::Continue); + ran.push((idx, step, before, after, cpu)); + if stops { + break; + } + } + ran + }) + }; + let cpu: Vec> = ran.iter().map(|r| r.4).collect(); + let mut outcome = ChainOutcome::Proceed; + for ((idx, step, before, after, _), energy) in ran.into_iter().zip(apportion(&pass, &cpu)) { + let name = self.interceptors[idx].name().to_string(); + self.seal(&name, seam, &step, before, after, energy); + if let Some(stopped) = terminal(&name, &step) { + outcome = stopped; + } + } + return outcome; + } + for &idx in order { + let ((step, before, after), energy) = { let meter = &self.meter; let interceptor = &mut self.interceptors[idx]; - meter.measure(|| interceptor.before_tool(call)) + meter.measure(|| step_at(interceptor.as_mut())) }; - let after = call.digest(); let name = self.interceptors[idx].name().to_string(); - self.seal(&name, Seam::BeforeTool, &step, before, after, energy); + self.seal(&name, seam, &step, before, after, energy); if let Some(outcome) = terminal(&name, &step) { return outcome; } @@ -371,6 +423,59 @@ impl Chain { } } +/// Divide one pass's figure among the steps that ran in it, by each step's +/// thread CPU time (`cpu`, in run order): integer micro-joules that sum to the +/// pass's figure exactly (largest remainder; a tie goes to the earlier step). +/// +/// The shares are weighted by CPU time only where every step's was read and +/// they are not all zero; otherwise equally. A share of a reading is a model of +/// how the reading divides, so a pass graded `HwShunt` gives its steps +/// `ModelBased` shares; any other grade is kept. A pass of one step is not +/// divided and keeps its figure as it is. Coverage is the pass's: a `Partial` +/// reading gives `Partial` shares. The pass's span also holds the chain's own +/// work between steps (digests) and the meter's closing wake-up; all of it is +/// divided among the steps, so no step is under its own energy. +fn apportion(pass: &Energy, cpu: &[Option]) -> Vec { + let n = cpu.len(); + if n == 0 { + return Vec::new(); + } + if n == 1 { + return vec![*pass]; + } + let total = pass.uj(); + let weights: Vec = match cpu.iter().map(|c| c.map(|d| d.as_nanos())).collect::>>() { + Some(w) if w.iter().any(|&x| x > 0) => w, + _ => vec![1; n], + }; + let sum: u128 = weights.iter().sum(); + let mut shares: Vec = weights.iter().map(|&w| (u128::from(total) * w / sum) as u64).collect(); + let mut left = total - shares.iter().sum::(); + // Largest remainder first; equal remainders go to the earlier step. + let mut order: Vec = (0..n).collect(); + order.sort_by_key(|&i| std::cmp::Reverse(u128::from(total) * weights[i] % sum)); + for &i in &order { + if left == 0 { + break; + } + shares[i] += 1; + left -= 1; + } + let provenance = match pass.provenance { + Provenance::HwShunt => Provenance::ModelBased, + other => other, + }; + shares + .into_iter() + .map(|uj| Energy { + measured_uj: None, + estimated_uj: uj, + provenance, + coverage: pass.coverage, + }) + .collect() +} + fn terminal(name: &str, step: &Step) -> Option { match step { Step::Continue => None, @@ -791,6 +896,58 @@ mod tests { assert!(verify(&env, c.steward(), &c.steward_pubkey()).is_err()); } + fn ns(n: u64) -> Option { + Some(Duration::from_nanos(n)) + } + + /// A pass's reading divides among its steps by thread CPU time, in whole + /// micro-joules that sum to the reading exactly, as `ModelBased` shares. + #[test] + fn a_reading_divides_among_its_steps_by_cpu_time_and_sums_exactly() { + let pass = Energy::measured(1_000); + let shares = apportion(&pass, &[ns(1_000), ns(2_000), ns(7_000)]); + assert_eq!(shares.iter().map(Energy::uj).collect::>(), vec![100, 200, 700]); + for e in &shares { + assert_eq!((e.provenance, e.coverage, e.measured_uj), (Provenance::ModelBased, Coverage::Whole, None)); + e.check_provenance().unwrap(); + } + // Remainders: 1000 µJ over thirds is 333/333/334, never 999 or 1002. + for (cpu, want) in [ + (vec![ns(1), ns(1), ns(1)], vec![334, 333, 333]), + (vec![ns(3), ns(3), ns(1)], vec![429, 428, 143]), + (vec![ns(5), ns(0), ns(5)], vec![500, 0, 500]), + ] { + let got: Vec = apportion(&pass, &cpu).iter().map(Energy::uj).collect(); + assert_eq!(got, want, "{cpu:?}"); + assert_eq!(got.iter().sum::(), 1_000); + } + } + + /// One step is not divided: it keeps the reading, grade and all. + #[test] + fn a_pass_of_one_step_keeps_its_reading() { + let pass = Energy::measured(1_234); + assert_eq!(apportion(&pass, &[ns(9)]), vec![pass]); + assert!(apportion(&pass, &[]).is_empty()); + } + + /// Without every step's CPU time, or with none of it non-zero, the + /// reading divides equally; a partial reading gives partial shares; a + /// grade below a reading is kept. + #[test] + fn shares_fall_back_to_equal_and_keep_the_passs_coverage_and_lower_grade() { + let got: Vec = apportion(&Energy::measured(10), &[ns(5), None]).iter().map(Energy::uj).collect(); + assert_eq!(got, vec![5, 5]); + let got: Vec = apportion(&Energy::measured(10), &[ns(0), ns(0)]).iter().map(Energy::uj).collect(); + assert_eq!(got, vec![5, 5]); + for e in apportion(&Energy::measured_partial(9), &[ns(1), ns(2)]) { + assert_eq!((e.provenance, e.coverage), (Provenance::ModelBased, Coverage::Partial)); + } + for e in apportion(&Energy::estimator(9), &[ns(1), ns(2)]) { + assert_eq!(e.provenance, Provenance::Estimator); + } + } + #[test] fn energy_is_metered_and_accumulates_honestly() { let mut c = chain() diff --git a/jcp/jcp-rs/crates/jcp-intercept/tests/one_span_per_pass.rs b/jcp/jcp-rs/crates/jcp-intercept/tests/one_span_per_pass.rs new file mode 100644 index 000000000..1dcf6fb2b --- /dev/null +++ b/jcp/jcp-rs/crates/jcp-intercept/tests/one_span_per_pass.rs @@ -0,0 +1,115 @@ +//! A seam metered on this process's own energy count, as one span around the +//! whole pass, against an oracle that shares no code with the meter. +//! +//! Three interceptors each burn a different amount of their thread's CPU time +//! in one `before_model` pass. Each step is sealed with its share of the +//! pass's reading by thread CPU time (`ModelBased`), the shares sum to the +//! chain's figure, and that figure is the process's own energy over the pass: +//! no more than an oracle's window around it, and most of it. The oracle takes +//! its thread off its core its own way before each read (sleeps until the +//! process's context-switch count moves), then reads `ri_energy_nj` into a raw +//! buffer at the SDK's offset. + +#![cfg(target_os = "macos")] + +use ed25519_dalek::SigningKey; +use jcp_energy::Meter; +use jcp_intercept::{verify, Chain, Interceptor, ModelRequest, Step}; +use jcp_schema::Provenance; +use std::time::Duration; + +/// Burn `cpu` of this thread's own CPU time. +fn burn(cpu: Duration) { + let start = jcp_energy::thread_cpu_time().expect("the thread clock"); + let mut x = 1u64; + while jcp_energy::thread_cpu_time().expect("the thread clock") - start < cpu { + for _ in 0..10_000 { + x = std::hint::black_box(x.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1)); + } + } +} + +struct Burner { + name: &'static str, + cpu: Duration, +} + +impl Interceptor for Burner { + fn name(&self) -> &str { + self.name + } + fn before_model(&mut self, _req: &mut ModelRequest) -> Step { + burn(self.cpu); + Step::Continue + } +} + +/// This process's `ri_energy_nj` (`proc_pid_rusage`, `RUSAGE_INFO_V6`, the +/// u64 at byte 336), after this thread has left its core: sleep 1, 2, 4, 8 ms +/// until the process's context-switch count (`pti_csw` would do; the rusage +/// `ri_user_time` is not enough) has moved. `ru_nvcsw + ru_nivcsw` of this +/// process is read with `getrusage`. +fn oracle_nj() -> u64 { + fn switches() -> i64 { + // SAFETY: `ru` is a valid, writable `rusage`. + let mut ru: libc::rusage = unsafe { std::mem::zeroed() }; + assert_eq!(unsafe { libc::getrusage(libc::RUSAGE_SELF, &mut ru) }, 0); + ru.ru_nvcsw + ru.ru_nivcsw + } + let before = switches(); + for ms in [1, 2, 4, 8] { + std::thread::sleep(Duration::from_millis(ms)); + if switches() > before { + break; + } + } + let mut buf = [0u64; 58]; + // SAFETY: `buf` is 464 writable bytes, the size of `rusage_info_v6`. + let rc = unsafe { libc::proc_pid_rusage(libc::getpid(), 6, buf.as_mut_ptr() as *mut libc::rusage_info_t) }; + assert_eq!(rc, 0, "proc_pid_rusage"); + buf[336 / 8] +} + +#[test] +fn a_pass_is_one_reading_divided_among_its_steps_by_cpu_time() { + let meter = Meter::detect(); + assert_eq!(meter.provenance(), Provenance::HwShunt, "this host's meter does not read a counter: {:?}", meter.rung()); + let steward = SigningKey::from_bytes(&[7u8; 32]); + let mut misses = 0; + for round in 0..10 { + let mut c = Chain::new(steward.clone(), "did:jcp:test-intercept-pass") + .with_meter(Meter::detect()) + .with(Box::new(Burner { name: "one", cpu: Duration::from_millis(2) })) + .with(Box::new(Burner { name: "two", cpu: Duration::from_millis(4) })) + .with(Box::new(Burner { name: "four", cpu: Duration::from_millis(8) })); + let before = oracle_nj(); + assert!(c.before_model(&mut ModelRequest::default()).proceeds()); + let oracle = oracle_nj() - before; + + let recs: Vec<_> = + c.receipts().iter().map(|e| verify(e, c.steward(), &c.steward_pubkey()).expect("a sealed step verifies")).collect(); + assert_eq!(recs.iter().map(|r| r.interceptor.as_str()).collect::>(), ["one", "two", "four"]); + let figures: Vec = recs.iter().map(|r| r.energy_uj).collect(); + let sum: u64 = figures.iter().sum(); + eprintln!( + "intercept pass: round {round}: steps {figures:?} µJ ({:?}), sum {sum} µJ, chain {} µJ, oracle window {:.0} µJ", + recs.iter().map(|r| r.provenance.as_str()).collect::>(), + c.energy_uj(), + oracle as f64 / 1e3 + ); + assert_eq!(sum, c.energy_uj(), "the steps' shares are not the chain's figure"); + if recs.iter().any(|r| r.provenance != "ModelBased") { + // A pass whose ends could not be read is the estimate, divided as one. + assert!(recs.iter().all(|r| r.provenance == "Estimator"), "{recs:?}"); + misses += 1; + continue; + } + // The shares follow the steps' own CPU time: 2 : 4 : 8 ms. + assert!(figures[0] < figures[1] && figures[1] < figures[2], "{figures:?}"); + // The pass is the process's own energy over it: within the oracle's + // window, and most of it. + assert!(sum * 1_000 <= oracle + 1_000, "the pass ({sum} µJ) exceeds the oracle's window ({oracle} nJ)"); + assert!(sum * 1_000 * 3 >= oracle * 2, "the pass ({sum} µJ) is under two thirds of the oracle's window ({oracle} nJ)"); + } + assert!(misses <= 1, "{misses} of 10 passes could not be read"); +} -- 2.54.0 (Apple Git-157) From e00db437c6cc92e3e2e20d79b0e534d5bb37f392 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Tue, 13 Oct 2026 02:43:59 -0400 Subject: [PATCH 2/2] jcp README: how an intercept step's joules are metered where the meter reads a counter Co-Authored-By: Claude Opus 5.5 --- jcp/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jcp/README.md b/jcp/README.md index 0ac850d4d..011474d60 100644 --- a/jcp/README.md +++ b/jcp/README.md @@ -43,7 +43,7 @@ jcp/ - **`jcp-orchestrate`** — a **cross-harness orchestration profile**: a deterministic reconcile loop (poll → route → dispatch → reconcile → retry) that runs work items across a *heterogeneous* fleet of `Harness` backends, where every dispatch is **capability-gated** (a harness is granted only the task kinds it may handle), **joule-budgeted** (an exhausted harness fails over to the next one with capacity), and **sealed** as a JCR-1 decision receipt *before* it runs. A task whose kind no harness is granted is denied; a flaky harness retries up to a bound; the whole run is an ordered, verifiable ledger of sealed decisions. The compose-many-harnesses shape a meta-harness offers — with the capability, energy budget, and audit it leaves to the platform. - **`jcp-prompt`** — a **signed, content-addressed prompt registry**: immutable prompt **versions** (double-brace `{{ variable }}` templates) with mutable **aliases** (`dev` / `staging` / `production`) resolved by `prompts:/@` (or `prompts://`) URIs — the registry shape the serving ecosystem already uses. The difference: each registered version and each alias **promotion** seals as a hash-chained **`prompt/1`** receipt, and a version's id *is* its template, so swapping the text under a name changes the id and re-pointing an alias is a signed entry in an ordered log. The exact instructions an agent ran under — and which version was live when — become tamper-evident artifacts, not a row someone with write access can edit. - **`jcp-trace`** — ingests an agent run's **OpenTelemetry GenAI trace** (`gen_ai.*` semantic conventions: `chat` / `tool` / `retriever` spans with token usage) and its **evaluation assessments** (judge / human / code feedback), attaches **per-span joules** (measured where a meter is present, else `Estimator` from token counts — provenance never silently upgrades), and seals one **`eval/1` JCR-1 receipt**: the per-span token + joule breakdown with the headline `uj_per_accepted_token` at the **weakest** provenance present, the assessments, a deterministic **quality-gate** verdict (the sealed, declarative analog of a CI assert), and a **readiness projection** an ARL validation-axis claim consumes. The observability stacks *record* a trace and *log* a judge score in a table; this is the interop projection that **seals** the same trace, puts a joule on it, and hash-chains it via `EvalLog` — no new cryptography. The **gate is content-addressed**, so the sealed receipt commits to exactly which bar it was judged against (a tamper-evident artifact, not a mutable in-code `assert`), and **`align_threshold` / `Judge`** calibrate a judge's decision threshold to human pass/fail labels — judge alignment as a reproducible number, not a UI click. Behind the `server` feature, a std-only **OTLP `/v1/traces` receiver** lets any OpenTelemetry exporter POST a `gen_ai.*` trace (with its assessments) and get the sealed `eval/1` receipt back the instant the run ends. -- **`jcp-intercept`** — **attested interceptor chains**: a composable, ordered pipeline of transforms *around* each model and tool call — the `before_model` / `after_model` / `before_tool` seams an agent's middleware stack runs, composed onion-order (forward in, reverse out) and stopping at the first refusal or human-approval hold. An interceptor may inspect or **rewrite** the request (redact a secret before it reaches the model, trim stale context), **refuse** it, or **hold** it for a human. The difference from the incumbent's arbitrary in-process hooks: every step seals an **`intercept/1`** JCR-1 receipt carrying the request digest **before and after** the step (so a redaction or a trim is provable — the content itself never sealed, only its hash), the seam, the outcome, and the **metered joules** at their honest provenance, hash-chained — the exact transform pipeline that produced a model request becomes reconstructable and verifiable by anyone, and a step cannot run un-sealed. Three reference interceptors — `Redact` (a content transform), `CallLimit` (a guardrail), `Approve` (a human-in-the-loop hold) — each reuse a primitive the family already holds rather than reinventing it. +- **`jcp-intercept`** — **attested interceptor chains**: a composable, ordered pipeline of transforms *around* each model and tool call — the `before_model` / `after_model` / `before_tool` seams an agent's middleware stack runs, composed onion-order (forward in, reverse out) and stopping at the first refusal or human-approval hold. An interceptor may inspect or **rewrite** the request (redact a secret before it reaches the model, trim stale context), **refuse** it, or **hold** it for a human. The difference from the incumbent's arbitrary in-process hooks: every step seals an **`intercept/1`** JCR-1 receipt carrying the request digest **before and after** the step (so a redaction or a trim is provable — the content itself never sealed, only its hash), the seam, the outcome, and the **metered joules** at their honest provenance (where the meter reads a counter, a seam pass is one reading divided among its steps by each step's thread CPU time, so the steps sum to it exactly and each share is graded `ModelBased`), hash-chained — the exact transform pipeline that produced a model request becomes reconstructable and verifiable by anyone, and a step cannot run un-sealed. Three reference interceptors — `Redact` (a content transform), `CallLimit` (a guardrail), `Approve` (a human-in-the-loop hold) — each reuse a primitive the family already holds rather than reinventing it. - **`jcp-efficiency`** — the **energy-efficiency spec**, surfaced as a sealed receipt. The dark-factory economics turn on four figures — **joules per accepted token, tokens per second, value per token, and the tokens/joules/dollars spent per COMPLETED, VERIFIED unit of work** — and the incumbents publish, at best, a *rate* (cost per token) that can't tell you whether the spend bought a result. An **`efficiency/1`** JCR-1 receipt records all four at honest provenance, hash-chained, with the one thing the rates omit: the **outcome anchor** (`completed` + a verdict). A measurement that didn't complete is spend that bought nothing, and the receipt says so. `aggregate` then splits value-delivered from total spend across a chain — charging *every* attempt (including the failed ones) against the results actually delivered — so `tokens_per_completed_unit` / `uj_per_completed_unit` / `value_per_joule` become the honest efficiency a "cost per token" number is designed to hide. Integers and strings only (JCR-1 forbids bools/floats); the rate figures recompute from the sealed inputs on read. - **`jcp-energy`** — the metering ladder (SPEC §5.1): RAPL package counters (Linux powercap, wraparound-corrected, `HwShunt`, only while every package counter is live — `rapl_status()`), NVML GPU energy behind the `nvml` feature (`HwShunt` and `Partial`, since it does not read the CPU's share; only while live — `nvml_status()`), the macOS kernel's per-process CPU energy count (`proc_pid_rusage`'s `ri_energy_nj`; `HwShunt`, whole for in-process CPU work, only while live — `process_energy_status()`; it covers this process's threads, not children, the GPU or DRAM, so a span that reaps a child process or ends with one is `Partial`; each end of a span is read only once the metering thread is seen to have left its core, because the kernel counts a thread's energy into its process's only then, and a span with an end not so checked is the estimate, never a reading; it is live on macOS 27.0.1 where IOReport's CPU rails are not), Apple Silicon IOReport (`HwShunt`, only while every CPU energy rail, each die's on a multi-die part, is live — on macOS 27.0.1 that rail is republished in lumps, so there the rung is declined and `ioreport_status()` says why; `Meter::detect_explained()` names the rung selected and why each one above it was declined), a time×power model at a power the host calibrated (`ModelBased`, `Meter::time_power`), and estimates no host calibrated (`Estimator`): time × the documented default power — the rung `Meter::detect()` falls to, always available — and a constant per-op figure. A vendor counter's reading is real but vendor-opaque and relayed by the platform, and it counts as a reading only while the counter is live (SPEC §5). Measured when the platform exposes it, estimated when it doesn't — and the provenance always says which; a hardware backend that fails mid-reading degrades honestly to the default-power estimate. - **`jcp-gateway`** — the **energy-native MCP gateway**: a JCP enforcement point that sits in front of an MCP server so an existing client/server pair keeps its transport while every `tools/call` becomes capability-scoped, joule-budgeted, and sealed. The novel mechanic is two-phase metered enforcement: the call is *authorized* before it runs (no debit, because the cost isn't yet known), the upstream call is **metered** by the sibling Sandbox standard's energy probe (Apple Silicon IOReport, RAPL, NVML — vendor energy counters, whose readings are real but vendor-opaque and relayed by the platform, and which count as readings only while the counter is live for the work, SPEC §5; otherwise the figure is an estimate and is graded as one), and the metered joules are debited from the grant — so the budget that stops a runaway agent is denominated in joules, not a token estimate. Where the upstream server is a local process whose CPU energy the host's kernel counts live (macOS `proc_pid_rusage`), each call is read on that server's own process tree, the work's own process rather than the machine (SPEC §5.1, rung 3): sealed `HwShunt`, and `Whole` only when nothing was seen missing from the tree (no descendant reaped or exited in the span, no GPU client held, no in-process Neural Engine memory, every member read and quiet at both ends) and the operator has declared, with `--upstream-self-contained`, that the server hands no work to a process outside its tree; otherwise `Partial`, with the response's `_meta` naming why. Every decision, allow and deny, seals into a **hash-chained receipt log**: excise, reorder, or splice a receipt and the chain breaks at that point. A denied call never reaches the server (the live 2025–26 MCP exploits land on tool *output*, which the gateway frames as untrusted `data` that can never exercise a capability), and the `jcp-gateway` binary runs it over stdio in front of any MCP-server subprocess. This is the chokepoint the AI-gateway field occupies, on the two axes nobody there holds: the unit of account is the joule, and the audit is cryptographic and tamper-evident rather than a dollar figure and a span. -- 2.54.0 (Apple Git-157)