jcp-energy: IOReport only when its CPU rail keeps advancing, never on a batch refresh #110
7 changed files with 5352 additions and 156 deletions
|
|
@ -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.
|
||||
|
|
|
|||
70
jcp/SPEC.md
70
jcp/SPEC.md
|
|
@ -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)
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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
68
jcp/jcp-rs/crates/jcp-energy/tests/ioreport_probed_once.rs
Normal file
68
jcp/jcp-rs/crates/jcp-energy/tests/ioreport_probed_once.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue