jcp-energy: IOReport only when its CPU rail keeps advancing, never on a batch refresh #110

Merged
dcharlot merged 10 commits from jcp-energy/cpu-rail-liveness into main 2026-10-01 05:45:02 -04:00
7 changed files with 5352 additions and 156 deletions

View file

@ -45,8 +45,8 @@ jcp/
- **`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-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`), NVML GPU energy behind the `nvml` feature (`HwShunt`), a time×calibrated-power model (`ModelBased`, always available), and a constant per-op estimator (`Estimator`). 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 model.
- **`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 on real hardware** (the sibling Sandbox standard's energy probe — Apple Silicon IOReport, RAPL, NVML), and the **measured** joules are debited from the grant — so the budget that stops a runaway agent is denominated in energy actually spent, not a token estimate. 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.
- **`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()`), 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), a time×calibrated-power model (`ModelBased`, always available), and a constant per-op estimator (`Estimator`). 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 model.
- **`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. 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.
- **`jcp-wire`** — the `jcp/1` wire binding (SPEC §11): 4-byte length prefix + canonical det-CBOR over any byte stream, with TCP and Unix-socket endpoints and a client. The decoder rejects non-canonical bodies, so a message has exactly one wire form. Requests carry the full delegation chain and are **caller-signed** — the endpoint authenticates from the request itself and denies an impersonation `bad-signature`, sealed like any other decision. Responses carry the deterministic decision and the sealed receipt.
- **`jcp-threshold`** — threshold actors (SPEC §2.2): a `did:key` whose private key is split k-of-n with FROST (RFC 9591); the group key is an ordinary Ed25519 key, so a 2-of-3 team issues, holds, and signs wire requests with nothing else in JCP changing. Shares come from a trusted dealer or **distributed key generation** — with DKG the group's private key never exists anywhere, not even momentarily. The two-agent rule for irreversible actions, refused by arithmetic rather than policy.
- **`jcp-channel`** — receipt-settled budget channels (SPEC §4.5, the OPTIONAL profile): N enforcement points spend optimistically against one grant and settle by exchanging signed statements of cumulative spend — forged, stale, and regressing statements refused; between settlements the exposure is bounded by the window, and after settlement any overdraft is provable from the signed statements. Where bounded overdraft is unacceptable, `shard()` remains the baseline.

View file

@ -383,6 +383,31 @@ absent). The runtime debits `cost.uj()` from a grant's remaining budget on every
allow; the coupling MUST be enforced. The energy axis is what no existing
agentic protocol standardizes — JCP makes it a precondition, not a report.
`HwShunt` covers a vendor energy counter as well as a dedicated shunt. A vendor
counter's count is a real reading, not an estimate; what it lacks is an
independent root — how the vendor derives it is undisclosed, and the
platform's own software relays it — which is a gap in provenance, not in
accuracy. A reading earns the grade only while its counter is live:
**Liveness (REQUIRED).** A reading from a vendor energy counter (RAPL, NVML,
Apple IOReport, ...) MAY be graded as a hardware reading only while the counter
is demonstrably live for the domain the figure claims: the implementation MUST
have observed the counter covering that domain advance under load (for Apple
IOReport: the CPU energy rail itself advancing in at least 5 sample intervals,
in at least 4 of every 5, over at least 300 ms while the probing thread burns
CPU). Where the figure sums several counters of the domain — a RAPL package
counter per package, an IOReport CPU energy rail per die — every one of them
MUST be observed live: a frozen one beside live ones would be left out of a
figure that claims the whole domain. A counter that is present but not live —
frozen, or republished in lumps — MUST NOT produce a hardware-graded figure; the implementation falls back to
its estimator or reports the work unmetered, and SHOULD record why. A figure
MUST NOT claim coverage of a domain its counter does not read; where the format
has a coverage field, such a figure is Partial.
The record of why is the implementation's to choose; the reference meter
exposes the IOReport liveness status it was selected or declined on. Coverage
is defined in §5.2.
### 5.1 The metering ladder (REQUIRED behavior, RECOMMENDED backends)
The rule: **measured when the platform exposes it, estimated when it doesn't —
@ -395,11 +420,47 @@ hardware backend fails mid-reading and the figure degrades to a model.
Recommended ladder, best first:
1. **RAPL** (Linux `powercap`) — package energy counters, wraparound-corrected
→ `HwShunt`.
2. **NVML** — the GPU total-energy counter → `HwShunt`.
3. **Time × calibrated power** — wall-clock duration times a host-calibrated
→ `HwShunt`, selected only while every package counter is live under the
probe's CPU burn, by the rule above as IOReport's CPU rails are. A span over
which no package counter advanced is metered by a lower rung. The package
counters are the top-level zones whose `name` is `package-<n>`, every
socket's, each corrected at its own `max_energy_range_uj`; no other zone is
summed or probed. A `psys` zone already includes the packages, so summing it
beside them counts their energy twice, and a `dram` zone is not CPU energy.
2. **NVML** — the GPU total-energy counter → `HwShunt`, `Partial` (§5.2): it
reads the GPU and not the CPU's share of the work. Selected only while the
counter is live by the same rule; the probe cannot load the GPU, so for
this counter live means published continuously, advancing on the device's
own draw. A span over which it did not advance is metered by a lower rung.
3. **Apple IOReport** (the Energy Model group) — the SoC energy counters
→ `HwShunt`, selected only while every CPU energy rail (one per die on a
multi-die part) is live by the rule above. A span over which that rail did not advance is metered by the next
rung, never graded `HwShunt`. Where the CPU rail is not live but the GPU rail
is, the rung is declined rather than reporting the GPU rail as a `Partial`
reading of the work: the GPU rail is machine-wide (display compositing
moves it), so booking it to a task misattributes energy the task did not
spend, and the partial figure would under-debit the budget, the CPU's share
going unmetered. The whole-task estimate below is the honest figure.
4. **Time × calibrated power** — wall-clock duration times a host-calibrated
active power → `ModelBased`. Always available.
4. **Constant per-operation estimate** → `Estimator`.
5. **Constant per-operation estimate** → `Estimator`.
### 5.2 Coverage (REQUIRED where a figure is partial)
Provenance says how a figure was obtained; **coverage** says how much of the
work it accounts for: `Whole` (the default) or `Partial`. A reading can be
hardware-rooted and still cover only part of the work — an accelerator counter
on a host whose CPU has none — and its remainder is unmeasured, not zero. A
`Partial` figure MUST NOT be presented as a task total.
On the wire coverage is carried only when it is `Partial`, and omitted when
`Whole`, so a figure sealed before the field existed keeps its exact bytes; a
verifier reads an absent field as `Whole`. The `jcp/2` receipt carries it as
`energy_coverage` (§8); the `intercept/1` step receipt of the reference
interceptor chain (`jcp-intercept`) carries it as `coverage`, beside its
`provenance`. In `intercept/1` the field, when present, has exactly one value,
`"Partial"`; a verifier MUST refuse any other value, `"Whole"` included, rather
than read it either way.
## 6. The Frame (REQUIRED)
@ -473,6 +534,7 @@ verdict = "allow" | "deny"
deny_reason = string ; omitted on allow
cost_uj = uint
energy_provenance = "HwShunt" | "ModelBased" | "Estimator"
energy_coverage = "Partial" ; omitted when Whole (§5.2)
budget_remaining_uj, budget_uj = uint
steward = the signer's id (the COSE kid)
```

View file

@ -97,10 +97,15 @@ here the entire field — TX included — sits on an **unsolved problem, one of
provenance and tamper-evidence rather than accuracy**:
- **Vendor counters are opaque and platform-relayed, not independently rooted**:
RAPL/NVML/IOReport are *real* power-sensor readings, not fabricated estimates — but
their derivation and error bounds are undisclosed, nothing binds the number to the
sensor, and the reading reaches a verifier through the platform's own (untrusted)
software. RAPL has separately been an unprivileged-readable power side-channel
RAPL/NVML/IOReport are *real* readings of the vendor's own energy counter, not
fabricated estimates — which is why JCP grades a live one `HwShunt` (SPEC §5) — but
how the vendor derives the count (a sensor, a model, or both) and its error bounds
are undisclosed, nothing binds the number to its source, and the reading reaches a
verifier through the platform's own (untrusted) software. And a counter is a reading
only while it is live: one that is present but frozen, or republished in lumps (as
Apple's CPU energy rails are to an unprivileged reader on macOS 27.0.1), reads
nothing of the work and must not yield a hardware-graded figure.
RAPL has separately been an unprivileged-readable power side-channel
(PLATYPUS), and its accuracy against an external meter is itself contested
(arXiv:2505.09375).
- **NVML / `nvidia-smi`** is a real on-board sensor, but its coarse sampling misses
@ -157,11 +162,12 @@ re-verifies both from scratch, and derives `IntegrityAttested`.
**What `PhysicallyRooted` actually requires (an honest clarification).** It is *not*
reached by standing up a cloud confidential VM. A cloud SEV-SNP/TDX instance gives a
hardware-grounded *report*, but its energy counter is still RAPL — a model, not a
meter — so the derived level is `IntegrityAttested`, not `PhysicallyRooted`. That is
the whole "confidential computing attests integrity, not energy" thesis, enforced at
the type level: the ladder refuses to promote a non-physical counter no matter how
genuine the TEE. `PhysicallyRooted` needs *both* a hardware-grounded report *and* a
hardware-grounded *report*, but its energy counter is still RAPL — a vendor counter
the platform relays, not a reading signed at its source — so the derived level is
`IntegrityAttested`, not `PhysicallyRooted`. That is the whole "confidential
computing attests integrity, not energy" thesis, enforced at the type level: the
ladder refuses to promote a counter not signed at its source, no matter how genuine
the TEE. `PhysicallyRooted` needs *both* a hardware-grounded report *and* a
**physical counter whose reading is signed at the source** — a current shunt with a
secure element — co-located with confidential compute. That is a custom-hardware
build, not an instance type. Its software-reachable precursor — deriving

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,68 @@
//! The IOReport liveness probe runs once per PROCESS, whoever asks first and
//! however many ask at once.
//!
//! This file holds one test, so it runs in a process of its own, and its
//! concurrent calls ARE the first use: the cache starts cold, whatever order
//! other tests run in. Counted, not timed: how long a probe takes depends on
//! the rail and on the host's load, so a clock cannot tell one probe from two.
use std::sync::Barrier;
use jcp_energy::{ioreport_status, IoReportStatus, Meter};
#[test]
fn the_liveness_probe_runs_once_per_process() {
const THREADS: usize = 8;
let (runs_before, opens_before) = (jcp_energy::ioreport_probe_runs(), jcp_energy::ioreport_energy_model_opens());
assert_eq!(
(runs_before, opens_before),
(0, 0),
"something probed before this test began, so its calls are not the first use"
);
let barrier = Barrier::new(THREADS);
let statuses: Vec<IoReportStatus> = std::thread::scope(|s| {
let workers: Vec<_> = (0..THREADS)
.map(|i| {
let barrier = &barrier;
s.spawn(move || {
barrier.wait();
// Half the threads build the meter first and half ask for
// the status first, so either path can be the first use.
let first = if i % 2 == 0 {
let _ = Meter::detect();
ioreport_status()
} else {
let status = ioreport_status();
let _ = Meter::detect();
status
};
for _ in 0..3 {
assert_eq!(ioreport_status(), first, "the cached status changed between calls");
let _ = Meter::detect();
}
first
})
})
.collect();
workers.into_iter().map(|w| w.join().expect("a thread panicked")).collect()
});
let (runs, opens) = (jcp_energy::ioreport_probe_runs(), jcp_energy::ioreport_energy_model_opens());
eprintln!(
"ioreport cache: {THREADS} threads, first use concurrent, each asked for the status 4 times and built the \
meter 4 times; they saw {statuses:?}; probe runs in this process = {runs}, Energy Model subscriptions \
opened = {opens}"
);
assert!(statuses.iter().all(|s| *s == statuses[0]), "threads saw different statuses: {statuses:?}");
assert_eq!(statuses[0], ioreport_status(), "the spawned threads and this one disagree");
if cfg!(target_os = "macos") {
assert_eq!(runs, 1, "the liveness probe did not run exactly once in this process");
assert_eq!(
opens, 1,
"{opens} Energy Model subscriptions were opened in this process: something probed besides the one cached \
probe"
);
} else {
assert_eq!((runs, opens), (0, 0), "a probe ran on a platform with no IOReport");
assert_eq!(statuses[0], IoReportStatus::Unavailable);
}
}

View file

@ -18,7 +18,9 @@
//! that a call was refused, without ever sealing the content itself (only its
//! hash). A step cannot run un-sealed.
//! - **Honest joule accounting.** Each step is metered ([`jcp_energy`]); the
//! receipt records the figure at its true provenance, never overclaiming.
//! receipt records the figure at its true provenance, never overclaiming —
//! and a figure that covers only part of the step says so (`"coverage":
//! "Partial"`), rather than being sealed as the whole step's energy.
//!
//! A [`Chain`] runs its interceptors forward through the `before_*` seams and in
//! reverse through `after_model` (the onion unwinds), stopping at the first
@ -30,7 +32,7 @@
use ed25519_dalek::SigningKey;
use jcp_energy::Meter;
use jcp_receipt::jcr1;
use jcp_schema::{ContentId, Energy};
use jcp_schema::{ContentId, Coverage, Energy};
use serde_json::{json, Value};
// ───────────────── the request/response/tool an interceptor transforms ─────────────────
@ -352,7 +354,8 @@ impl Chain {
&self.receipts
}
/// Total metered energy across every sealed step, microjoules.
/// Total metered energy across every sealed step, microjoules. Where a
/// step's figure was `Partial` (its receipt says so), so is this total.
pub fn energy_uj(&self) -> u64 {
self.energy_uj
}
@ -409,6 +412,13 @@ fn intercept_wire(
"prev": prev.to_hex(),
"steward": steward,
});
// Coverage rides beside provenance only when the figure is `Partial`. The
// whole-step case is the default and is omitted, so a receipt for a whole
// figure is byte-identical to one sealed before the field existed. Without
// it a partial figure would be sealed as the step's whole energy.
if !energy.is_whole() {
o["coverage"] = json!(energy.coverage.label());
}
if let Some(r) = step.reason() {
o["reason"] = json!(r);
}
@ -450,6 +460,10 @@ pub struct InterceptReceipt {
pub energy_uj: u64,
/// The energy provenance label.
pub provenance: String,
/// How much of the step the energy figure covers: `Partial` when the
/// receipt carries `"coverage": "Partial"`, `Whole` when it carries no
/// coverage (the only other encoding a receipt may have).
pub coverage: Coverage,
/// The reason, for a halt/hold outcome.
pub reason: Option<String>,
/// The chain sequence number.
@ -480,6 +494,13 @@ pub fn verify(envelope: &[u8], steward: &str, pubkey: &[u8; 32]) -> Result<Inter
.and_then(Value::as_u64)
.ok_or_else(|| InterceptError::Malformed(format!("missing uint '{k}'")))
};
// Present only for a partial figure; any other value, `"Whole"` included,
// is not an encoding a sealer produces, so it is refused rather than read.
let coverage = match v.get("coverage") {
None => Coverage::Whole,
Some(c) if c.as_str() == Some(Coverage::Partial.label()) => Coverage::Partial,
Some(c) => return Err(InterceptError::Malformed(format!("coverage {c} is not \"Partial\""))),
};
Ok(InterceptReceipt {
interceptor: s("interceptor")?,
seam: s("seam")?,
@ -489,6 +510,7 @@ pub fn verify(envelope: &[u8], steward: &str, pubkey: &[u8; 32]) -> Result<Inter
transformed: u("transformed")? != 0,
energy_uj: u("energy_uj")?,
provenance: s("provenance")?,
coverage,
reason: v.get("reason").and_then(Value::as_str).map(str::to_string),
seq: u("seq")?,
prev: s("prev")?,
@ -781,4 +803,127 @@ mod tests {
assert_eq!(rec.energy_uj, 1_000);
assert_eq!(rec.provenance, "Estimator"); // Meter::constant → Estimator
}
/// The `intercept/1` envelopes the chain in
/// [`a_whole_figure_seals_byte_identically_to_before`] sealed at 3fd35db5,
/// before coverage was carried: a `Redact` then `CallLimit` step at the
/// constant meter's 1 000 µJ, under the tests' key and steward.
const WHOLE_BEFORE_COVERAGE: [&str; 2] = [
concat!(
"d284581ba2012704566469643a6a63703a746573742d696e74657263657074a0590190ac637365710064707265767840",
"303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030",
"30303030303030303030303030303030647365616d6c6265666f72655f6d6f64656c676f7574636f6d6568636f6e7469",
"6e75656773746577617264766469643a6a63703a746573742d696e7465726365707469656e657267795f756a1903e86a",
"70726f76656e616e636569457374696d61746f726b696e746572636570746f72667265646163746b7472616e73666f72",
"6d6564006d726571756573745f6166746572784061393861313863336263643239363363613966363839633334396531",
"3364636464303237366532343566303239623034353639626232376331316635653963636e726571756573745f626566",
"6f7265784061393861313863336263643239363363613966363839633334396531336463646430323736653234356630",
"32396230343536396262323763313166356539636371696e746572636570745f726563656970746b696e746572636570",
"742f315840b128f5ec05886ebc7207c2e8687c04f5c8b452e2bde4f2ba4f241b0c9b86bdcb9319aba3815f10fd9572fe",
"d52bc91c848e33276f09cd7cf75426a5f81313c903",
),
concat!(
"d284581ba2012704566469643a6a63703a746573742d696e74657263657074a059019aac637365710164707265767840",
"653934313538363465383261656238303162363036626233663836366235356661613863313330353761613463626232",
"66663035653564643730616632306264647365616d6c6265666f72655f6d6f64656c676f7574636f6d6568636f6e7469",
"6e75656773746577617264766469643a6a63703a746573742d696e7465726365707469656e657267795f756a1903e86a",
"70726f76656e616e636569457374696d61746f726b696e746572636570746f72706d6f64656c2d63616c6c2d6c696d69",
"746b7472616e73666f726d6564006d726571756573745f61667465727840613938613138633362636432393633636139",
"663638396333343965313364636464303237366532343566303239623034353639626232376331316635653963636e72",
"6571756573745f6265666f72657840613938613138633362636432393633636139663638396333343965313364636464",
"3032373665323435663032396230343536396262323763313166356539636371696e746572636570745f726563656970",
"746b696e746572636570742f3158401bb080b73a487db157a7375884d9983a13725e45e79e5532726a17cd5c0ed2dfb0",
"541a67e23371d456099130ae763c96bc0f8c2f29e3b68340d5b8a2ccd9ba08",
),
];
fn payload(env: &[u8], c: &Chain) -> Value {
let core = jcr1::verify_cose_sign1(env, c.steward(), &c.steward_pubkey()).expect("a sealed receipt verifies");
jcp_schema::from_canonical_cbor(&core).expect("the core decodes")
}
/// A whole figure seals exactly the bytes it sealed before coverage
/// existed: no `coverage` key, the same envelopes, byte for byte.
#[test]
fn a_whole_figure_seals_byte_identically_to_before() {
let mut c = chain()
.with(Box::new(Redact::secrets()))
.with(Box::new(CallLimit::models(5)));
c.before_model(&mut req());
assert_eq!(c.receipts().len(), 2);
for (i, env) in c.receipts().iter().enumerate() {
assert!(payload(env, &c).get("coverage").is_none(), "receipt {i} carries a coverage for a whole figure");
assert_eq!(verify(env, c.steward(), &c.steward_pubkey()).unwrap().coverage, Coverage::Whole);
assert_eq!(hex::encode(env), WHOLE_BEFORE_COVERAGE[i], "receipt {i} is not the envelope sealed before coverage");
}
}
/// The same for a whole hardware reading, `HwShunt`, which the
/// constant meter cannot produce: its object is the `intercept/1` object
/// as it was, key for key, and canonicalizes to the same core bytes.
#[test]
fn a_whole_reading_seals_the_object_it_sealed_before() {
let (before, after) = (ContentId::of_bytes(b"a"), ContentId::of_bytes(b"b"));
let prev = ContentId([3u8; 32]);
let wire = intercept_wire(
"redact", Seam::BeforeTool, &Step::Continue, before, after, &Energy::measured(7), 4, &prev, "did:jcp:s",
);
let as_before = json!({
"intercept_receipt": "intercept/1",
"interceptor": "redact",
"seam": "before_tool",
"outcome": "continue",
"request_before": before.to_hex(),
"request_after": after.to_hex(),
"transformed": 1u64,
"energy_uj": 7u64,
"provenance": "HwShunt",
"seq": 4u64,
"prev": prev.to_hex(),
"steward": "did:jcp:s",
});
assert_eq!(wire, as_before);
assert_eq!(jcr1::signed_core_bytes(&wire).unwrap(), jcr1::signed_core_bytes(&as_before).unwrap());
}
/// A partial figure is sealed as one: `"coverage": "Partial"` beside its
/// provenance, recovered by `verify`, and that key is the only difference
/// from the same figure sealed whole.
#[test]
fn a_partial_figure_seals_coverage_partial() {
let mut c = chain();
let d = req().digest();
c.seal("meter", Seam::BeforeModel, &Step::Continue, d, d, Energy::measured_partial(500));
c.seal("meter", Seam::BeforeModel, &Step::Continue, d, d, Energy::measured(500));
let (partial, whole) = (payload(&c.receipts()[0], &c), payload(&c.receipts()[1], &c));
assert_eq!(partial["coverage"], json!("Partial"), "a partial figure sealed without its coverage: {partial}");
let rec = verify(&c.receipts()[0], c.steward(), &c.steward_pubkey()).unwrap();
assert_eq!((rec.coverage, rec.provenance.as_str(), rec.energy_uj), (Coverage::Partial, "HwShunt", 500));
let strip = |mut v: Value| {
let o = v.as_object_mut().unwrap();
o.remove("coverage");
o.remove("seq");
o.remove("prev");
v
};
assert_eq!(strip(partial), strip(whole), "a partial figure's receipt differs from a whole one's beyond its coverage");
}
/// `coverage` has one encoding, `"Partial"`, and absence means whole. A
/// receipt carrying any other value, an explicit `"Whole"` included, is
/// refused rather than read either way.
#[test]
fn verify_refuses_a_coverage_other_than_partial() {
let sk = SigningKey::from_bytes(&[7u8; 32]);
let steward = "did:jcp:test-intercept";
let d = req().digest();
let whole = intercept_wire("m", Seam::BeforeModel, &Step::Continue, d, d, &Energy::measured(1), 0, &d, steward);
for bad in [json!("Whole"), json!("partial"), json!(1u64)] {
let mut wire = whole.clone();
wire["coverage"] = bad.clone();
let env = jcr1::cose_sign1_envelope(&jcr1::signed_core_bytes(&wire).unwrap(), &sk, steward);
let got = verify(&env, steward, &sk.verifying_key().to_bytes());
assert!(matches!(got, Err(InterceptError::Malformed(_))), "coverage {bad} was read: {got:?}");
}
}
}

View file

@ -107,7 +107,14 @@ impl TryFrom<String> for Did {
/// How honest an energy reading is (SPEC §5) — the family's provenance model.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum Provenance {
/// Read from a hardware energy interface (RAPL/NVML). Requires a measured figure.
/// Read from a hardware energy interface: a dedicated shunt, or a vendor
/// energy counter (RAPL, NVML, Apple IOReport, ...). A vendor counter's
/// count is a real reading, not an estimate; how the vendor derives it is
/// undisclosed and the platform relays it, which is a gap in provenance,
/// not in accuracy. It earns this grade only while it is live for the domain
/// the figure claims (SPEC §5): observed advancing under load. A counter
/// that is present but frozen, or republished in lumps, never yields an
/// `HwShunt` figure. Requires a measured figure.
HwShunt,
/// A calibrated per-operation model. A measured figure may be absent.
ModelBased,