open-standards/sense/sense-rs/crates/sense-conformance/tests/pack.rs
Transaction Science 35fc970903
All checks were successful
standards workspaces build + test / which workspaces this change touches (pull_request) Successful in 22s
standards workspaces build + test / macOS energy backends (pull_request) Has been skipped
standards workspaces build + test / sense-rs (sense/sense-rs, sense-rs, linux) (pull_request) Successful in 25s
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (pull_request) Successful in 3m26s
sense §11: what a running agent receives when an observation surfaces to it
§5 decides whether an observation is worth attention and §8 seals that decision.
Neither says what the recipient *receives*. Where the recipient is a person that
is right — the form is a UI concern and a non-goal (§1.6). Where the recipient is
a running agent it is not, because the delivery enters the agent's own record of
what it has been told, and the standard had nothing to say about a frame that
record has to hold.

Three plausible forms each corrupt that record, which is why the form is
normative rather than a matter of taste:

- **as a result on a call the agent made** — the agent made no such call, and a
  record in which every result answers a call cannot hold one that answers
  nothing without ceasing to mean that;
- **as the principal's own words** — the principal did not say it, and an auditor
  reading the record afterwards could no longer separate what the agent was
  *asked* from what it was *told*;
- **outside the order** — what an agent did depends on when it learned a thing,
  so an observation lifted out of order silently changes what the record says it
  knew at each earlier point.

So a surfaced observation delivered to an agent is an **injected observation**: a
distinctly-kinded frame occupying one position in the recipient's order, stating
the surfacing link it carries out (§8, by content address, so the delivery cites
a sealed decision instead of restating it), the event's id and trust level (§2,
bound in so a delivery cannot re-tag what it carries), its destination, the
authority under which a parked recipient was woken, and the joules the delivery
charged — which is not the joules §5 already charged for deciding to surface.

The injected kind may not be offered to the agent as something it can author, and
a host must refuse the recipient authoring one: an agent able to author its own
observations could manufacture a record of having been told something, which is
the one thing the frame exists to witness.

§11.1 — the destination is stated, never inferred

`own` puts the observation in the recipient's own record, so everything it does
afterwards was done knowing it. `derived` puts it in a record that inherits the
recipient's context, and the recipient's own record gains that record's
*outcome*, not the observation — so the recipient never came to know the
observation itself. An auditor reading a recipient's record has to be able to
tell that an observation was handled elsewhere rather than never delivered, so
the two may not be distinguished by shape alone.

§11.2 — four conditions refused rather than recorded

1. **A hold is not deliverable.** §8 chains holds precisely so a decision not to
   surface cannot be quietly forgotten. Delivering one is that fault in the other
   direction.
2. **The recipient is not the author**, as a steering amendment's amender must
   differ from its author (JCP §4.7).
3. **A wake needs an authority** — the fire of a standing grant (JCP §4.6).
   Already-running names none, which is a different fact from a wake nobody
   permitted, and the two do not share a shape: the absent case carries no key
   rather than a null, so the distinction is in the frame's address.
4. **A delivery that happened cannot cost nothing.** Something ran: the recipient
   read the frame. Zero is not a measurement, and where a host cannot measure it
   estimates and labels (§7) rather than reporting nothing. This is the same rule
   as a parked run not being free, applied to the other side of the boundary.

Conformance class C6, and the published pack gains three frame addresses — the
same delivery to each destination, and one that needed a wake against one that
did not — plus the four refusals as stable tokens rather than prose, so a
reworded message does not move a vector. `tests/pack.rs` holds all of it to the
file and asserts the three addresses stay distinct: if the destination or the
wake stopped being bound in, two would collapse into one and a delivery could be
rewritten after the fact without changing what it addresses to. Every published
value from the five existing classes is unchanged.

One implementation note. The harness picks its surfaced and held links from the
C5 log **by what they decided, not by position** — the first attempt indexed
`entries()[1]` as the hold, and that fixture's budget surfaces the first two, so
the refusal silently passed as a success. Selecting by decision means a later
change to the fixture's budget cannot quietly stop testing a refusal.

Evidence, on the Framework box (nothing on the CI host, which is mid-matrix for
another PR): clippy `-D warnings` clean over the workspace, every workspace test
passes including the nine conformance classes and the five published-pack checks.
`check-comparative-prose.sh` and `check-no-vanity-metrics.sh` clean.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-11-02 13:56:29 -05:00

353 lines
14 KiB
Rust

//! Consume the published `sense/surfacing/1` conformance pack.
//!
//! The generator's own doc says "the vectors are exactly what the in-repo tests
//! assert", but the in-crate tests assert the *properties*, not the published
//! values, and coverage across the five classes is uneven:
//!
//! - **C1** — `c1_replay_is_deterministic` is `assert_eq!(f(), f())`. The published
//! replay digest is compared to nothing at all.
//! - **C2 / C3** — real relational checks (changing trust or source changes the id;
//! the burst fires at least one highlight and something surfaces), but the
//! published id, highlights and decisions are never checked.
//! - **C4 / C5** — these do pin their published values, by restating them.
//!
//! So three of the five classes could publish anything and no test would object.
//! This file holds every class to the file on disk, and checks the sealed envelope
//! from the pack and RFC 9052 alone.
use ed25519_dalek::{Signature, Verifier, VerifyingKey};
use sense_conformance as c;
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}"))
}
// --- minimal CBOR (RFC 8949) — independent of the crate's own COSE code ---------
fn head(major: u8, len: u64) -> Vec<u8> {
let m = major << 5;
match len {
0..=23 => vec![m | len as u8],
24..=0xff => vec![m | 24, len as u8],
0x100..=0xffff => {
let mut v = vec![m | 25];
v.extend_from_slice(&(len as u16).to_be_bytes());
v
}
_ => {
let mut v = vec![m | 26];
v.extend_from_slice(&(len as u32).to_be_bytes());
v
}
}
}
fn bstr(b: &[u8]) -> Vec<u8> {
let mut v = head(2, b.len() as u64);
v.extend_from_slice(b);
v
}
fn tstr(s: &str) -> Vec<u8> {
let mut v = head(3, s.len() as u64);
v.extend_from_slice(s.as_bytes());
v
}
fn protected_header(kid: &str) -> Vec<u8> {
let mut v = head(5, 2);
v.push(0x01);
v.push(0x27); // -8, EdDSA
v.push(0x04);
v.extend(bstr(kid.as_bytes()));
v
}
fn sig_structure(protected: &[u8], payload: &[u8]) -> Vec<u8> {
let mut v = head(4, 4);
v.extend(tstr("Signature1"));
v.extend(bstr(protected));
v.extend(bstr(&[]));
v.extend(bstr(payload));
v
}
/// Read one CBOR head, returning `(major, length)` and advancing `i`.
fn read_head(b: &[u8], i: &mut usize) -> (u8, u64) {
let ib = b[*i];
*i += 1;
let (major, ai) = (ib >> 5, ib & 0x1f);
let len = match ai {
0..=23 => ai as u64,
24 => {
let v = b[*i] as u64;
*i += 1;
v
}
25 => {
let v = u16::from_be_bytes([b[*i], b[*i + 1]]) as u64;
*i += 2;
v
}
26 => {
let v = u32::from_be_bytes([b[*i], b[*i + 1], b[*i + 2], b[*i + 3]]) as u64;
*i += 4;
v
}
_ => panic!("unsupported CBOR additional info {ai}"),
};
(major, len)
}
/// Split a `COSE_Sign1` into `(protected, payload, signature)` — reading the
/// published bytes rather than assuming their layout.
fn split_cose_sign1(env: &[u8]) -> (Vec<u8>, Vec<u8>, Vec<u8>) {
let mut i = 0usize;
assert_eq!(env[i], 0xd2, "not tagged as COSE_Sign1 (tag 18)");
i += 1;
let (major, n) = read_head(env, &mut i);
assert_eq!((major, n), (4, 4), "COSE_Sign1 is a four-element array");
let (m, len) = read_head(env, &mut i);
assert_eq!(m, 2, "protected header must be a byte string");
let protected = env[i..i + len as usize].to_vec();
i += len as usize;
let (m, n) = read_head(env, &mut i);
assert_eq!((m, n), (5, 0), "unprotected header must be an empty map");
let (m, len) = read_head(env, &mut i);
assert_eq!(m, 2, "payload must be a byte string");
let payload = env[i..i + len as usize].to_vec();
i += len as usize;
let (m, len) = read_head(env, &mut i);
assert_eq!((m, len), (2, 64), "signature must be a 64-byte string");
let sig = env[i..i + len as usize].to_vec();
(protected, payload, sig)
}
/// The generator still emits exactly the published files, run into a temp directory
/// so the pack itself is never touched.
#[test]
fn the_generator_still_emits_the_published_pack() {
let tmp = std::env::temp_dir().join(format!("sense-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_sense-gen-vectors"))
.arg(&tmp)
.output()
.expect("the generator must run");
assert!(out.status.success(), "generator failed: {}", String::from_utf8_lossy(&out.stderr));
for f in ["meta.json", "vectors.json"] {
assert_eq!(
std::fs::read(tmp.join(f)).unwrap(),
std::fs::read(pack_dir().join(f)).unwrap(),
"{f} has drifted from what the implementation emits"
);
}
let _ = std::fs::remove_dir_all(&tmp);
}
/// C1 and C2 — the two published content addresses, recomputed.
///
/// This is the coverage the in-crate tests do not provide: `c1_replay_digest` is
/// only ever compared to itself there, so a changed replay would go unnoticed.
#[test]
fn the_published_content_addresses_recompute() {
let v = doc("vectors.json");
assert_eq!(
c::c1_replay_digest().to_hex(),
v["c1_replay_digest"].as_str().expect("pack records a replay digest"),
"the scripted event stream no longer replays to the published digest"
);
assert_eq!(
c::c2_event().id().to_hex(),
v["c2_event_id"].as_str().expect("pack records an event id"),
"the canonical event no longer addresses to the published id"
);
}
/// C3 — the highlights and decisions the scripted stream produces, compared to the
/// published lists in full rather than "at least one of them surfaces". Every one
/// surfaces for this idle-ish user; the gate's refusal path is published separately
/// and checked in `the_published_gate_outcomes_are_reproduced`.
#[test]
fn the_published_highlights_and_decisions_are_reproduced() {
let v = doc("vectors.json");
let (_proj, highlights, decisions) = c::c3_run();
let got_decisions: Vec<String> = decisions.iter().map(|d| d.label().to_string()).collect();
let want_decisions: Vec<String> = v["c3_decisions"]
.as_array()
.expect("pack records decisions")
.iter()
.map(|d| d.as_str().unwrap().to_string())
.collect();
assert!(!want_decisions.is_empty(), "a wrong path must not look like a pass");
assert_eq!(got_decisions, want_decisions, "the gate's decisions have changed");
let got_highlights: Vec<String> =
highlights.iter().map(|h| format!("{}:{:.2}", h.kind.as_str(), h.score)).collect();
let want_highlights: Vec<String> = v["c3_highlights"]
.as_array()
.expect("pack records highlights")
.iter()
.map(|h| h.as_str().unwrap().to_string())
.collect();
assert_eq!(got_highlights, want_highlights, "the highlights have changed");
}
/// C3 — the gate's refusal path. The same highlights, gated for a user busy in the
/// modality a surfacing would occupy on a budget that pays for two surfacings, must
/// reproduce the published outcomes and the budget after each: a surfacing debits
/// it, a hold spends nothing. The pack must publish every outcome SPEC §5 names —
/// until it did, only `surface` appeared and the refusals were never checked.
#[test]
fn the_published_gate_outcomes_are_reproduced() {
let v = doc("vectors.json");
let gate = c::c3_gate_run();
let want_decisions: Vec<String> = v["c3_gate_decisions"]
.as_array()
.expect("pack records the gate's decisions")
.iter()
.map(|d| d.as_str().unwrap().to_string())
.collect();
let got_decisions: Vec<String> = gate.iter().map(|x| x.decision.label().to_string()).collect();
assert_eq!(got_decisions, want_decisions, "the gate's decisions have changed");
let want_remaining: Vec<u64> = v["c3_gate_budget_remaining_uj"]
.as_array()
.expect("pack records the budget after each decision")
.iter()
.map(|r| r.as_u64().unwrap())
.collect();
let got_remaining: Vec<u64> = gate.iter().map(|x| x.budget_remaining_uj).collect();
assert_eq!(got_remaining, want_remaining, "the budget moved differently");
for outcome in ["surface", "hold:value", "hold:budget"] {
assert!(
want_decisions.iter().any(|d| d == outcome),
"the pack must publish a `{outcome}` outcome"
);
}
}
/// C4 and C5 — the cascade's answers and the surfacing chain, against the pack.
#[test]
fn the_published_cascade_and_chain_results_hold() {
let v = doc("vectors.json");
let (model, escalated) = c::c4_run();
assert_eq!(model, v["c4_recall_model"].as_str().unwrap(), "recall answered differently");
assert_eq!(
escalated,
v["c4_open_ended_escalated"].as_bool().unwrap(),
"the open-ended query no longer escalates"
);
assert_eq!(model, "deterministic", "recall must still be answered with no model call");
let (ok, broken) = c::c5_verify();
assert_eq!(ok, Ok(v["c5_verify_ok"].as_i64().unwrap() as usize), "the chain no longer verifies");
assert_eq!(
broken,
Err(v["c5_excision_breaks_at"].as_i64().unwrap() as usize),
"an excision no longer breaks the chain where the pack says"
);
}
/// The published sealed link verifies under the published key, checked from the pack
/// and RFC 9052 alone rather than through the crate's own COSE code.
#[test]
fn the_published_envelope_verifies_under_the_published_key() {
let m = doc("meta.json");
let v = doc("vectors.json");
assert_eq!(m["profile"], "sense/surfacing/1");
let steward = m["steward"].as_str().expect("meta names the steward");
let pubkey: [u8; 32] =
hex::decode(m["pubkey_hex"].as_str().unwrap()).unwrap().try_into().expect("32-byte key");
assert_eq!(
pubkey,
ed25519_dalek::SigningKey::from_bytes(&c::STEWARD_SEED).verifying_key().to_bytes(),
"the published key is not the documented seed's"
);
assert_eq!(steward, c::STEWARD);
let envelope = hex::decode(v["c5_first_link_jcr1_envelope_hex"].as_str().unwrap()).unwrap();
let (env_protected, core, sig_bytes) = split_cose_sign1(&envelope);
let sig = Signature::from_slice(&sig_bytes).expect("64-byte signature");
// The envelope's own protected header must be the one RFC 9052 and the pack
// describe — otherwise a verifier reconstructing it from the spec would be
// signing over different bytes than the sealer did.
let protected = protected_header(steward);
assert_eq!(env_protected, protected, "the protected header is not {{1:-8, 4:steward}}");
let vk = VerifyingKey::from_bytes(&pubkey).unwrap();
vk.verify(&sig_structure(&protected, &core), &sig)
.expect("the published envelope must verify under the published key");
// And the payload it commits to is the first surfacing link the implementation
// still produces — sealing it again must reproduce the published bytes exactly.
let (log, _pk) = c::c5_log();
let sk = ed25519_dalek::SigningKey::from_bytes(&c::STEWARD_SEED);
let resealed = sense_receipt::seal_jcr1(&log.entries()[0].core, &sk, steward).expect("seal");
assert_eq!(resealed, envelope, "the first link no longer seals to the published envelope");
// Not vacuous: a flipped payload byte and a stranger's key must both fail.
let mut tampered = core.clone();
let mid = tampered.len() / 2;
tampered[mid] ^= 0x01;
assert!(vk.verify(&sig_structure(&protected, &tampered), &sig).is_err());
let stranger = ed25519_dalek::SigningKey::from_bytes(&[0x42; 32]).verifying_key();
assert!(stranger.verify(&sig_structure(&protected, &core), &sig).is_err());
}
/// C6 — the injected observation (§11), held to the file.
///
/// Three addresses and four refusal tokens. The addresses matter more than they
/// look: if the destination or the presence of a wake stopped being bound into
/// the frame, two of the three would collapse into one, and a delivery could be
/// rewritten after the fact without changing what it addresses to.
#[test]
fn the_published_injected_frames_recompute() {
let v = doc("vectors.json");
let (own_woken, derived_woken, own_running) = c::c6_distinct_addresses();
for (got, key) in [
(own_woken, "c6_injected_id_own_woken"),
(derived_woken, "c6_injected_id_derived_woken"),
(own_running, "c6_injected_id_own_running"),
] {
assert_eq!(
got.to_hex(),
v[key].as_str().unwrap_or_else(|| panic!("pack records {key}")),
"the injected frame no longer addresses to the published {key}"
);
}
// Still three distinct frames, as §11.1 and §11.2 require.
assert_eq!(
[own_woken, derived_woken, own_running].iter().collect::<std::collections::BTreeSet<_>>().len(),
3,
"the destination and the wake must each be visible in the address"
);
let published: Vec<&str> =
v["c6_refusals"].as_array().expect("pack records the refusals")
.iter().map(|x| x.as_str().expect("a refusal token")).collect();
let got: Vec<&str> = c::c6_refusals().iter().map(|e| e.label()).collect();
assert_eq!(got, published, "§11.2's refusals, in order");
assert_eq!(
published,
vec!["not-surfaced", "authored-by-recipient", "no-wake-authority", "unpriced-delivery"],
"a refusal that stopped firing would otherwise publish a shorter list"
);
}