All checks were successful
energy meters on real hardware / RAPL and NVML on jetson-hub (pull_request) Successful in 34s
standards workspaces build + test / jouleclaw-rs (pull_request) Successful in 13m6s
standards workspaces build + test / aac-rs (pull_request) Successful in 8m58s
standards workspaces build + test / joule-code-rs (pull_request) Successful in 2m17s
standards workspaces build + test / joulecontract-rs (pull_request) Successful in 1m30s
standards workspaces build + test / map-rs (pull_request) Successful in 1m7s
standards workspaces build + test / mesh-rs (pull_request) Successful in 1m24s
standards workspaces build + test / openpay (pull_request) Successful in 7m38s
wai cross-architecture byte-equality / linux-aarch64 (pull_request) Successful in 1m9s
WAI conformance / cargo-class corpora · linux-arm64 (pull_request) Successful in 16m8s
WAI conformance / cross-arch byte-equality · linux-arm64 (pull_request) Successful in 51s
WAI energy receipts / energy receipts · linux-arm64 (pull_request) Successful in 4m37s
wai-pack / wai-pack · linux-arm64 (pull_request) Successful in 3m10s
joule-code conformance gate / Build + round-trip C1-C4 vectors (pull_request) Successful in 1m51s
no-vanity-metrics / Scan committed Markdown for vanity metrics (pull_request) Successful in 3s
no-vanity-metrics / WAI public text names no comparison (pull_request) Successful in 4s
no-vanity-metrics / WAI public text names no company or product (pull_request) Successful in 3s
standards workspaces build + test / arl-rs (pull_request) Successful in 1m13s
standards workspaces build + test / eoc-rs (pull_request) Successful in 2m0s
standards workspaces build + test / jcp-rs (pull_request) Successful in 1m27s
standards workspaces build + test / joule-ui-rs (pull_request) Successful in 1m9s
standards workspaces build + test / joulehook-rs (pull_request) Successful in 1m12s
standards workspaces build + test / proof-rs (pull_request) Successful in 1m16s
standards workspaces build + test / sandbox-rs (pull_request) Successful in 2m15s
standards workspaces build + test / sense-rs (pull_request) Successful in 1m5s
standards workspaces build + test / smart-byte-rs (pull_request) Successful in 3m37s
wai cross-architecture byte-equality / linux-x86_64 (pull_request) Successful in 27s
WAI conformance / cargo-class corpora · linux (pull_request) Successful in 6m40s
WAI conformance / v2 vocabulary · manifests validate as stated (pull_request) Successful in 13s
WAI conformance / independent verifiers · claims, JWP, key discovery and integer payloads (Python) (pull_request) Successful in 12m59s
WAI conformance / delivery telemetry · CMCD and CMSD against an independent library (pull_request) Successful in 9s
WAI conformance / cross-arch byte-equality · linux (pull_request) Successful in 24s
WAI conformance / C2PA manifest emission · signed, read back, validated (pull_request) Successful in 46s
WAI energy receipts / energy receipts · linux (pull_request) Successful in 1m44s
WAI HTTP delivery / HTTP corpus · falsification and differential (pull_request) Successful in 1h19m13s
WAI HTTP delivery / cache rules · controls and labelled caches (pull_request) Successful in 1m4s
WAI JPEG-AI whole decode (pinned inputs, required) / fetch pinned JPEG-AI inputs → whole decode + pixel hash, required (pull_request) Successful in 1m3s
WAI neural sink gates / model-free refusals of the neural sink (pull_request) Successful in 1m7s
wai-pack / wai-pack · linux (pull_request) Successful in 1m4s
wai-pack / wai-pack · independent verifier (Python) (pull_request) Successful in 1m40s
wai-pack / wai-pack · the floor through a media prober (pull_request) Successful in 22s
wai-pack / wai-pack · dependency licences (pull_request) Successful in 1m6s
WAI section references / cited sections exist (pull_request) Successful in 10s
WAI mbt2018_mean integer decode (regenerated vectors) / regenerate mbt2018_mean integer vectors → Rust decode, required (pull_request) Successful in 4m21s
wai-web wasm32 / wai-web wasm32 build · corpus, menu and writers (pull_request) Successful in 2m42s
workflow hygiene / every workflow supersedes its own queue (pull_request) Successful in 4s
standards workspaces build + test / jouledesk-rs (pull_request) Successful in 3m3s
standards workspaces build + test / macOS energy backends (pull_request) Successful in 5m1s
wai cross-architecture byte-equality / macos-arm64 (reference) (pull_request) Successful in 30s
WAI conformance / cargo-class corpora · macos (pull_request) Successful in 10m35s
WAI conformance / cross-arch byte-equality · macos (pull_request) Successful in 29s
WAI energy receipts / energy receipts · macos (pull_request) Successful in 3m33s
wai-pack / wai-pack · macos (pull_request) Successful in 6m13s
Both `amend` and `verify_amendment` compared the amended budget against `grant.budget_uj` — the budget as ISSUED. On a 1 000 µJ grant whose run had already spent 900, an amendment of 1 000 passed as a "tightening" while handing the run ten times its remainder, contradicting the module's own guarantee that "steering can never escalate a running agent's authority". The hole was not that the check was missing; it was that the checkpoint had no way to say what had been spent, so there was nothing for the check to measure against. `Checkpoint` now carries `spent: Option<Spend>` — a cumulative microjoule figure with its §5 provenance grade. Cumulative rather than per-interval, so one checkpoint answers "how much is left" without the verifier holding the whole chain and without a missing link silently understating the total. `remaining_uj` derives the remainder and returns `None` when the checkpoint is silent: unknown is not zero, and conflating them is how an amendment hands back a whole grant. The ceiling is now the remainder where the checkpoint states its spend, and the issued budget where it does not. The fallback is deliberately the old, weaker rule rather than a refusal — a checkpoint written before the field existed is still a valid checkpoint, and rejecting it would break running deployments to fix a hole it cannot express. Enforced on both sides: `amend` refusing is not enough, because an enforcement point handed a signed amendment never calls `amend`. What the figure is worth, stated rather than implied: the checkpoint is authored by the agent, so its declared spend is a signed claim, not an independent measurement. Two things make it hard to shrink — it is attributable, and `checkpoint_follows` catches a later checkpoint that declares less than an earlier one or drops a field it once carried, which would otherwise let an agent recover its full ceiling by going quiet. Wire change is additive: `spent: None` is skipped, so `Checkpoint::id()` and every sealed vector are byte-identical to before the field existed. Regenerating the pack proves it — `standing.json` gains one entry and no published id or signature moves. The new `checkpoint_with_spend` vector publishes the issued budget alongside the spend so a third party can redo the arithmetic, and its id necessarily differs from `checkpoint`'s, which is what makes the additivity claim non-vacuous. The remainder rule itself is a refusal, and a refusal has nothing to sign, so it is pinned by C21 and unit tests instead. SPEC §4.7 and C21 updated: the remainder is normative, both sides MUST enforce it, an absent spend MUST NOT be read as zero, and a declared spend MUST NOT decrease. Co-Authored-By: Claude Opus 5 <[email protected]>
294 lines
13 KiB
Rust
294 lines
13 KiB
Rust
//! Consume the published `jcp` conformance pack — the files nothing read.
|
|
//!
|
|
//! jcp was already the best-covered standard here: `jcp-action-receipt`'s
|
|
//! `tests/conformance.rs` genuinely reads `action_receipt.json` and
|
|
//! `action_receipt_meta.json` from disk. But three of the pack's files had no
|
|
//! reader at all:
|
|
//!
|
|
//! - **`legacy_jcp1.json`** — receipts sealed under the superseded `jcp/1`
|
|
//! profile, published, in the pack's own words, "so a verifier can prove the
|
|
//! jcp/2 migration path: these MUST still verify".
|
|
//!
|
|
//! The guarantee is *not* untested: `jcp-receipt`'s own
|
|
//! `published_v1_receipts_still_verify` checks it. But it checks **one inlined
|
|
//! hex constant**, which is a copy of this pack's *allow* receipt. The pack also
|
|
//! publishes a **legacy deny** (`untrusted-control`) that no inlined constant
|
|
//! covers, and the file itself is read by nothing — so the published bytes, and
|
|
//! the legacy refusal path, were unverified. (`jcp-receipt`'s doc comment titled
|
|
//! *The migration guarantee* is also misplaced: it sits ~80 lines above, over an
|
|
//! unrelated test about environment bindings.)
|
|
//!
|
|
//! The mechanism is simpler than that comment suggests, too. There is no version
|
|
//! dispatch in `jcr1.rs`; `payload_hash` is merely **optional**, so its absence
|
|
//! leaves a legacy preimage byte-identical — which is what the neighbouring
|
|
//! comment ("Absent, the field changes nothing") actually says.
|
|
//! - **`dual_ceilings.json`** — §4.8's money-beside-joules ceiling, including the
|
|
//! two refusals that make it real (`funds-exceeded`, `currency-mismatch`). Those
|
|
//! are negative vectors, and nothing executed them.
|
|
//! - **`standing.json`** — the standing-grant lineage, and (added with §4.7's
|
|
//! remainder rule) the spend-bearing checkpoint whose arithmetic a third
|
|
//! party can redo.
|
|
//!
|
|
//! Note `legacy_jcp1.json` is deliberately absent from the drift check below: it is
|
|
//! a frozen historical artifact ("not minted any more"), so regenerating it would
|
|
//! destroy the very thing it exists to prove.
|
|
|
|
use jcp_receipt::{extract, replay_spend};
|
|
use serde_json::Value;
|
|
use std::path::PathBuf;
|
|
|
|
fn pack_dir() -> PathBuf {
|
|
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
|
.join("../../../conformance/v1")
|
|
.canonicalize()
|
|
.expect("the conformance pack must exist next to the spec")
|
|
}
|
|
|
|
fn doc(name: &str) -> Value {
|
|
serde_json::from_slice(&std::fs::read(pack_dir().join(name)).expect("read pack file"))
|
|
.unwrap_or_else(|e| panic!("{name} must parse: {e}"))
|
|
}
|
|
|
|
/// The steward identity and key the pack publishes.
|
|
fn steward_and_key() -> (String, [u8; 32]) {
|
|
let m = doc("meta.json");
|
|
let steward = m["steward"].as_str().expect("meta names the steward").to_string();
|
|
let key: [u8; 32] = hex::decode(m["pubkey_hex"].as_str().expect("meta publishes a key"))
|
|
.expect("hex")
|
|
.try_into()
|
|
.expect("a 32-byte Ed25519 key");
|
|
(steward, key)
|
|
}
|
|
|
|
fn envelopes(d: &Value) -> Vec<Vec<u8>> {
|
|
d["receipts"]
|
|
.as_array()
|
|
.expect("the pack lists receipts")
|
|
.iter()
|
|
.map(|r| hex::decode(r["envelope_hex"].as_str().expect("receipt carries an envelope")).unwrap())
|
|
.collect()
|
|
}
|
|
|
|
/// The migration guarantee, over the **published** bytes rather than an inlined copy.
|
|
///
|
|
/// Every receipt sealed under the superseded `jcp/1` profile must still verify under
|
|
/// today's implementation, or the `payload_hash` bump silently invalidated every
|
|
/// receipt in the wild. `jcp-receipt` already checks the allow receipt from an
|
|
/// inlined constant; what was missing is the file itself and the legacy **deny**,
|
|
/// which no inlined constant covers. Both verdicts are required below so the
|
|
/// refusal cannot quietly disappear from the pack.
|
|
#[test]
|
|
fn receipts_sealed_under_the_superseded_profile_still_verify() {
|
|
let d = doc("legacy_jcp1.json");
|
|
let (steward, key) = steward_and_key();
|
|
assert_eq!(d["profile"], "jcp/1", "this pack is the superseded profile's evidence");
|
|
|
|
let receipts = d["receipts"].as_array().expect("the pack lists legacy receipts");
|
|
assert!(!receipts.is_empty(), "a wrong path must not look like a pass");
|
|
|
|
let verdicts: Vec<&str> = receipts.iter().map(|r| r["verdict"].as_str().unwrap()).collect();
|
|
assert!(verdicts.contains(&"allow"), "the legacy pack must keep an allow: {verdicts:?}");
|
|
assert!(
|
|
verdicts.contains(&"deny"),
|
|
"the legacy pack must keep a deny — the refusal path is the half no inlined \
|
|
constant covers: {verdicts:?}"
|
|
);
|
|
|
|
for (i, r) in receipts.iter().enumerate() {
|
|
let env = hex::decode(r["envelope_hex"].as_str().unwrap()).unwrap();
|
|
let core = extract(&env, &steward, &key).unwrap_or_else(|e| {
|
|
panic!("legacy receipt {i} no longer verifies ({e:?}) — the profile bump broke migration")
|
|
});
|
|
assert_eq!(
|
|
core.verdict,
|
|
r["verdict"].as_str().unwrap(),
|
|
"legacy receipt {i}: verdict differs from the published one"
|
|
);
|
|
assert_eq!(core.steward, steward, "legacy receipt {i}: steward differs");
|
|
}
|
|
}
|
|
|
|
/// The migration check is not vacuous: tampering with a legacy envelope, or
|
|
/// verifying it under another key, must both fail.
|
|
#[test]
|
|
fn a_tampered_legacy_receipt_does_not_verify() {
|
|
let d = doc("legacy_jcp1.json");
|
|
let (steward, key) = steward_and_key();
|
|
let mut env = hex::decode(d["receipts"][0]["envelope_hex"].as_str().unwrap()).unwrap();
|
|
|
|
let stranger = ed25519_dalek::SigningKey::from_bytes(&[0x42; 32]).verifying_key().to_bytes();
|
|
assert!(
|
|
extract(&env, &steward, &stranger).is_err(),
|
|
"a legacy receipt verified under a key that did not seal it"
|
|
);
|
|
assert!(
|
|
extract(&env, "did:example:not-this-steward", &key).is_err(),
|
|
"a legacy receipt verified against the wrong steward"
|
|
);
|
|
|
|
let mid = env.len() / 2;
|
|
env[mid] ^= 0x01;
|
|
assert!(extract(&env, &steward, &key).is_err(), "a tampered legacy receipt verified");
|
|
}
|
|
|
|
/// §4.8's dual ceilings, with the refusals actually executed.
|
|
///
|
|
/// The pack's value is in its two denials: a spend that exceeds the money ceiling,
|
|
/// and a spend in the wrong currency. Both are compared as the whole recorded
|
|
/// verdict — verdict *and* reason — because "it denied" would pass even if it
|
|
/// denied for the wrong reason.
|
|
#[test]
|
|
fn the_dual_ceiling_refusals_replay_with_their_recorded_reasons() {
|
|
let d = doc("dual_ceilings.json");
|
|
let (steward, key) = steward_and_key();
|
|
let receipts = d["receipts"].as_array().expect("the pack lists receipts");
|
|
assert_eq!(receipts.len(), 3, "the scenario is one allow and two refusals");
|
|
|
|
let mut reasons = Vec::new();
|
|
for (i, r) in receipts.iter().enumerate() {
|
|
let env = hex::decode(r["envelope_hex"].as_str().unwrap()).unwrap();
|
|
let core = extract(&env, &steward, &key)
|
|
.unwrap_or_else(|e| panic!("dual-ceiling receipt {i} does not verify: {e:?}"));
|
|
assert_eq!(core.verdict, r["verdict"].as_str().unwrap(), "receipt {i}: verdict differs");
|
|
|
|
let want_reason = r["wire"]["deny_reason"].as_str();
|
|
assert_eq!(
|
|
core.deny_reason.as_deref(),
|
|
want_reason,
|
|
"receipt {i}: denied for a different reason than the pack records"
|
|
);
|
|
if let Some(reason) = want_reason {
|
|
reasons.push(reason.to_string());
|
|
}
|
|
}
|
|
|
|
// The two rules §4.8 exists to enforce: a money ceiling, and no invented FX.
|
|
assert!(reasons.iter().any(|r| r == "funds-exceeded"), "no funds-exceeded refusal: {reasons:?}");
|
|
assert!(
|
|
reasons.iter().any(|r| r == "currency-mismatch"),
|
|
"no currency-mismatch refusal: {reasons:?}"
|
|
);
|
|
|
|
// Replaying the spend must land on the recorded total: only the allow debits.
|
|
let envs = envelopes(&d);
|
|
let spent: u64 = replay_spend(envs.iter().map(|e| e.as_slice()), &steward, &key)
|
|
.into_iter()
|
|
.map(|(_grant, uj)| uj)
|
|
.sum();
|
|
assert_eq!(
|
|
spent,
|
|
d["spent_after"]["uj"].as_u64().expect("the pack records the joules spent"),
|
|
"replaying the receipts does not reproduce the recorded joule spend"
|
|
);
|
|
assert!(
|
|
spent <= d["grant"]["budget_uj"].as_u64().unwrap(),
|
|
"the replayed spend exceeds the grant's own joule ceiling"
|
|
);
|
|
}
|
|
|
|
/// The standing-grant lineage, as far as the pack makes it checkable.
|
|
///
|
|
/// One link closes: `minted_child` names `standing_grant` as its parent, and its
|
|
/// budget is within the amended ceiling. One does not: `continuation_grant` names a
|
|
/// `parent_hex` that is **not among the ids the pack publishes**. Its budget matches
|
|
/// the amendment's `new_budget_uj`, so the parent is plausibly the post-amendment
|
|
/// grant — but that grant is not in the pack, so a third party cannot close the
|
|
/// link from the published files alone. Asserted as the current state, with a
|
|
/// message to replace it once the parent is published.
|
|
#[test]
|
|
fn the_standing_lineage_links_as_far_as_the_pack_allows() {
|
|
let d = doc("standing.json");
|
|
let id = |k: &str| d[k]["id_hex"].as_str().unwrap_or_default().to_string();
|
|
|
|
for k in ["standing_grant", "mint_policy", "minted_child", "amendment", "checkpoint", "continuation_grant"] {
|
|
let i = id(k);
|
|
assert_eq!(i.len(), 64, "{k} has no 32-byte id");
|
|
assert!(i.bytes().all(|b| b.is_ascii_hexdigit()), "{k} id is not hex");
|
|
}
|
|
|
|
assert_eq!(
|
|
d["minted_child"]["parent_hex"].as_str().unwrap(),
|
|
id("standing_grant"),
|
|
"the minted child does not descend from the standing grant"
|
|
);
|
|
|
|
let amended = d["amendment"]["new_budget_uj"].as_u64().expect("the amendment sets a budget");
|
|
assert_eq!(
|
|
d["continuation_grant"]["budget_uj"].as_u64().unwrap(),
|
|
amended,
|
|
"the continuation does not carry the amended budget"
|
|
);
|
|
assert!(
|
|
d["minted_child"]["budget_uj"].as_u64().unwrap() <= amended,
|
|
"a minted child may not exceed the amended ceiling"
|
|
);
|
|
|
|
// The spend-bearing checkpoint (§4.7's remainder rule). Two things are
|
|
// checkable from the published files alone: the remainder is arithmetic a
|
|
// third party can redo, and the entry's id differs from `checkpoint`'s —
|
|
// which is what makes the additivity claim non-vacuous. `checkpoint` was
|
|
// generated with no spend and keeps the id it had before the field existed;
|
|
// this one carries the field and therefore cannot.
|
|
let sp = &d["checkpoint_with_spend"];
|
|
let issued = sp["grant_budget_uj"].as_u64().expect("the entry publishes the issued budget");
|
|
let spent = sp["spent_uj"].as_u64().expect("the entry publishes the spend");
|
|
let remaining = sp["remaining_uj"].as_u64().expect("the entry publishes the remainder");
|
|
assert_eq!(
|
|
issued.saturating_sub(spent), remaining,
|
|
"the published remainder is not the issued budget minus the spend"
|
|
);
|
|
assert!(remaining < issued, "a vector for the remainder rule must actually have spent something");
|
|
assert_ne!(
|
|
id("checkpoint_with_spend"), id("checkpoint"),
|
|
"a declared spend must change the checkpoint's id — it is signed, not advisory"
|
|
);
|
|
|
|
let published: Vec<String> = ["standing_grant", "mint_policy", "minted_child", "amendment", "checkpoint", "continuation_grant"]
|
|
.iter()
|
|
.map(|k| id(k))
|
|
.collect();
|
|
let cont_parent = d["continuation_grant"]["parent_hex"].as_str().unwrap();
|
|
assert!(
|
|
!published.contains(&cont_parent.to_string()),
|
|
"the continuation's parent is now published — replace this with a real lineage \
|
|
assertion linking it to that grant"
|
|
);
|
|
}
|
|
|
|
/// The generator still emits the files it owns.
|
|
///
|
|
/// `legacy_jcp1.json` is excluded on purpose: it is frozen pre-`payload_hash`
|
|
/// evidence, so regenerating it would erase what it exists to prove.
|
|
#[test]
|
|
fn the_generator_still_emits_the_files_it_owns() {
|
|
let tmp = std::env::temp_dir().join(format!("jcp-pack-{}", std::process::id()));
|
|
let _ = std::fs::remove_dir_all(&tmp);
|
|
std::fs::create_dir_all(&tmp).expect("create temp dir");
|
|
|
|
let out = std::process::Command::new(env!("CARGO_BIN_EXE_gen-vectors"))
|
|
.arg(&tmp)
|
|
.output()
|
|
.expect("gen-vectors must run");
|
|
assert!(out.status.success(), "gen-vectors failed: {}", String::from_utf8_lossy(&out.stderr));
|
|
|
|
let mut checked = 0;
|
|
for f in ["meta.json", "vectors.json", "dual_ceilings.json", "standing.json"] {
|
|
let regenerated = tmp.join(f);
|
|
if !regenerated.exists() {
|
|
continue; // the generator does not own this one
|
|
}
|
|
assert_eq!(
|
|
std::fs::read(®enerated).unwrap(),
|
|
std::fs::read(pack_dir().join(f)).unwrap(),
|
|
"{f} has drifted from what the generator emits"
|
|
);
|
|
checked += 1;
|
|
}
|
|
assert!(checked >= 3, "expected the generator to own at least three files, saw {checked}");
|
|
assert!(
|
|
!tmp.join("legacy_jcp1.json").exists(),
|
|
"the generator now rewrites the frozen legacy pack — that would erase the \
|
|
pre-payload_hash evidence the migration guarantee rests on"
|
|
);
|
|
let _ = std::fs::remove_dir_all(&tmp);
|
|
}
|