diff --git a/sense/SPEC.md b/sense/SPEC.md index 8c1795d30..d991a287a 100644 --- a/sense/SPEC.md +++ b/sense/SPEC.md @@ -225,6 +225,11 @@ A conforming implementation reproduces, from the published vectors alone (`deterministic`); an open-ended query escalates. - **C5 — chain integrity.** A surfacing log verifies (`verify_chain → n`), and excising the middle link breaks it at the published index. +- **C6 — injected observations (§11).** A sealed surfacing delivers into a running + agent's record and the frame carries the link's own facts; a hold, a + self-authored observation, a wake with no authority, and a delivery priced at + zero are each refused; and neither the destination nor the presence of a wake + can change without changing the frame's address. ## 10. Recognized edges @@ -244,3 +249,85 @@ standard, not a dependency it owns: - **joule-ui** — the surface a highlight is rendered on; a surfaced highlight is itself a metered, sealed joule-ui interaction. - **WAI** — the container for multimodal sensor payloads referenced by a `Cid`. + +--- + +## 11. Surfacing into a running agent (REQUIRED where the recipient is an agent) + +§5 decides whether an observation is worth attention; §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 three plausible forms each corrupt that record. + +- **As a result on a call the agent made.** The agent made no such call. 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. An auditor + reading the record afterwards could no longer separate what the agent was + *asked* from what it was *told*, and that distinction is most of why the record + is worth keeping. +- **Outside the order.** What an agent did depends on when it learned a thing. An + observation lifted out of the order silently changes what the record says the + agent knew at each point before it. + +A surfaced observation delivered to an agent is therefore an **injected +observation**: a distinctly-kinded frame occupying one position in the recipient's +order, which **MUST** state + +- the **surfacing link** it carries out (§8), by the content address of that + link's signed core — so the delivery cites a sealed decision instead of + restating it, and the two cannot drift apart; +- the observed event's content id and its declared **trust level** (§2), bound in + so a delivery cannot re-tag what it carries; +- its **destination** (below); +- the **authority** under which a parked recipient was woken, where one was + needed; +- the **joules** the delivery charged — the cost of the recipient reading the + frame, which is not the cost §5 already charged for deciding to surface it. + +A host **MUST NOT** present an injected observation as a result, as the +principal's words, or outside the order. The injected kind **MUST NOT** be offered +to the agent as something it may author, and a host **MUST** refuse an attempt by +the recipient to author one: an agent able to author its own observations could +manufacture a record of having been told something, which is the one thing this +frame exists to witness. + +### 11.1 Destination + +A delivery states where it landed, because the two destinations say different +things about what the recipient knows. + +- **`own`** — into the recipient's own record. Everything the recipient does + afterwards was done knowing it. +- **`derived`** — into a derived record that inherits the recipient's context. The + recipient's own record gains that record's *outcome*, not the observation, so + the recipient never came to know the observation itself. + +The destination **MUST** be stated rather than inferred from shape. An auditor +reading a recipient's record has to be able to tell that an observation was +handled elsewhere rather than never delivered. + +### 11.2 What is refused + +Four conditions are 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 the same fault in the + other direction, and **MUST** be refused. +2. **The recipient is not the author.** Author and recipient **MUST** be distinct + principals, as a steering amendment's amender must differ from its author + (JCP §4.7). +3. **A wake needs an authority.** Where delivery wakes a parked run, the frame + **MUST** name the authority that permitted it — the fire of a standing grant + (JCP §4.6) — and delivery **MUST** be refused where none is held. An + already-running recipient names none, which is a different fact from a wake + nobody permitted, and the two **MUST NOT** share a shape. +4. **A delivery that happened is not free.** Something ran: the recipient read the + frame. Zero is therefore not a measurement, and a delivery priced at zero + **MUST** be refused. Where a host cannot measure, it estimates and labels + (§7); it does not report nothing. + +Conformance class **C6** exercises the delivery, each of the four refusals, and +that neither the destination nor the presence of a wake can be changed without +changing the frame's address. diff --git a/sense/conformance/v1/README.md b/sense/conformance/v1/README.md index 6fc0d1641..4a1303b2f 100644 --- a/sense/conformance/v1/README.md +++ b/sense/conformance/v1/README.md @@ -1,6 +1,6 @@ # Sense conformance vectors — v1 -Reproducible fixtures for the five conformance classes (SPEC §9). A third-party +Reproducible fixtures for the six conformance classes (SPEC §9). A third-party implementation is conforming if it reproduces these values from the spec alone. Regenerate with: diff --git a/sense/conformance/v1/vectors.json b/sense/conformance/v1/vectors.json index 0b1593e5f..23ad35435 100644 --- a/sense/conformance/v1/vectors.json +++ b/sense/conformance/v1/vectors.json @@ -29,5 +29,14 @@ "c4_recall_model": "deterministic", "c5_excision_breaks_at": 1, "c5_first_link_jcr1_envelope_hex": "d2845821a2012704581b6469643a6578616d706c653a73656e73652d7265666572656e6365a0590160ad6373657100646b696e646873616c69656e636564707265767840303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030306574727573746a466972737450617274796773746577617264781b6469643a6578616d706c653a73656e73652d7265666572656e6365686465636973696f6e6773757266616365686576656e745f69647840653232356166356230306161306632643562663765666437623061383833633962376433623763356636313537366438616334633930353564663631383035376a636861726765645f756a1903e86a636f73745f6d6963726f006b73636f72655f6d6963726f1a000e7ef06b76616c75655f6d6963726f1a000e7ef06d73656e73655f726563656970747173656e73652f737572666163696e672f31736275646765745f72656d61696e696e675f756a1903e85840e3f034e6f4d38ac68a1075e04b26a3e86297b19e40cdedeba2ed81999e25c00de1bd4841771a5e3687719b9855ed644669da85d1f8c6c27c95d2a365ee983700", - "c5_verify_ok": 3 + "c5_verify_ok": 3, + "c6_injected_id_derived_woken": "1968d0a2acc40ebe7074cfc355c83f3914dddf338cb34a721265446641e66344", + "c6_injected_id_own_running": "23feae15df140457b12b9a4add128bd67fbacb5aa0c65904a64f830dadd71094", + "c6_injected_id_own_woken": "d892422446e0a8588657fe5ef8b3eacfba081c28a34b22b82985632ec3fa4fb2", + "c6_refusals": [ + "not-surfaced", + "authored-by-recipient", + "no-wake-authority", + "unpriced-delivery" + ] } diff --git a/sense/sense-rs/crates/sense-conformance/src/bin/gen_vectors.rs b/sense/sense-rs/crates/sense-conformance/src/bin/gen_vectors.rs index 4b1650a23..f7e1fd05d 100644 --- a/sense/sense-rs/crates/sense-conformance/src/bin/gen_vectors.rs +++ b/sense/sense-rs/crates/sense-conformance/src/bin/gen_vectors.rs @@ -45,6 +45,11 @@ fn main() { let first = &log.entries()[0].core; let envelope_hex = hex::encode(seal_jcr1(first, &sk, c::STEWARD).expect("seal")); + // C6 — the injected observation (§11): the frame's address under each + // destination and with/without a wake, and the four refusals by token. + let (c6_own_woken, c6_derived_woken, c6_own_running) = c::c6_distinct_addresses(); + let c6_refusals: Vec<&str> = c::c6_refusals().iter().map(|e| e.label()).collect(); + let vectors = serde_json::json!({ "c1_replay_digest": c::c1_replay_digest().to_hex(), "c2_event_id": c::c2_event().id().to_hex(), @@ -57,6 +62,10 @@ fn main() { "c5_verify_ok": c5_ok.map(|n| n as i64).unwrap_or(-1), "c5_excision_breaks_at": c5_broken.err().map(|i| i as i64).unwrap_or(-1), "c5_first_link_jcr1_envelope_hex": envelope_hex, + "c6_injected_id_own_woken": c6_own_woken.to_hex(), + "c6_injected_id_derived_woken": c6_derived_woken.to_hex(), + "c6_injected_id_own_running": c6_own_running.to_hex(), + "c6_refusals": c6_refusals, }); std::fs::write(out_dir.join("meta.json"), format!("{}\n", serde_json::to_string_pretty(&meta).unwrap())).expect("write meta"); diff --git a/sense/sense-rs/crates/sense-conformance/src/lib.rs b/sense/sense-rs/crates/sense-conformance/src/lib.rs index a36cc5bd7..375d9e484 100644 --- a/sense/sense-rs/crates/sense-conformance/src/lib.rs +++ b/sense/sense-rs/crates/sense-conformance/src/lib.rs @@ -25,7 +25,8 @@ use sense_cascade::{ CascadeClient, CascadeStage, DeterministicStage, Envelope, RecorderClient, RetrievedSpan, StageDecision, SynthesisClient, }; -use sense_receipt::{verify_chain, SurfacingLog}; +use sense_receipt::injected::{Delivery, InjectError, InjectedObservation}; +use sense_receipt::{verify_chain, SealedSurfacing, SurfacingLog}; use sense_schema::{Cid, Event, Hlc, Highlight, SourceClass, StateProjection, TrustLevel}; /// The fixed steward key the conformance receipts seal with. A vector pack must @@ -33,6 +34,9 @@ use sense_schema::{Cid, Event, Hlc, Highlight, SourceClass, StateProjection, Tru /// nothing. pub const STEWARD_SEED: [u8; 32] = [0x5e; 32]; pub const STEWARD: &str = "did:example:sense-reference"; +/// The agent a surfaced observation is delivered to (§11). Distinct from +/// [`STEWARD`], which authors the delivery: the two may never be one. +pub const RECIPIENT: &str = "did:example:sense-recipient"; /// The shared scripted signal stream: a quiet baseline of shell activity, then a /// burst of file changes, then a low-trust log entry. Each tuple is @@ -189,6 +193,82 @@ pub fn c5_verify() -> (Result, Result) { (ok, broken) } +/// A link from the C5 log, chosen by what it decided rather than by position, +/// so a change to the fixture's budget cannot silently turn a "held" case into +/// a surfaced one and quietly stop testing the refusal. +fn c6_pick(log: &SurfacingLog, surfaced: bool) -> SealedSurfacing { + log.entries() + .iter() + .find(|e| (e.core.decision == "surface") == surfaced) + .unwrap_or_else(|| { + panic!( + "the C5 log has no {} link", + if surfaced { "surfaced" } else { "held" } + ) + }) + .clone() +} + +/// C6 — an injected observation (§11): the frame a surfaced decision takes +/// when its recipient is a running agent, and each condition that is refused +/// rather than recorded. +pub fn c6_injected() -> Result { + let (log, _pk) = c5_log(); + let surfaced = c6_pick(&log, true); + InjectedObservation::deliver( + &surfaced, + RECIPIENT, + STEWARD, + Delivery::Own, + 0, + true, + Some(Cid::of(b"sense-conformance-standing-fire")), + 4_200, + ) +} + +/// Each of §11.2's four refusals, in order, as the harness asserts them. +pub fn c6_refusals() -> Vec { + let (log, _pk) = c5_log(); + let surfaced = c6_pick(&log, true); + let held = c6_pick(&log, false); + let fire = Cid::of(b"sense-conformance-standing-fire"); + let d = |s: &SealedSurfacing, to: &str, parked, woken, uj| { + InjectedObservation::deliver(s, to, STEWARD, Delivery::Own, 0, parked, woken, uj) + .expect_err("each case is a refusal") + }; + vec![ + // 1. a decision that was a hold + d(&held, RECIPIENT, false, None, 1), + // 2. the recipient authoring its own observation + d(&surfaced, STEWARD, false, None, 1), + // 3. a parked recipient woken with no authority named + d(&surfaced, RECIPIENT, true, None, 1), + // 4. a delivery priced at nothing + d(&surfaced, RECIPIENT, false, Some(fire), 0), + ] +} + +/// The addresses of the same delivery to each destination, and of one that +/// needed a wake against one that did not. Neither distinction may be +/// invisible in the frame's address. +pub fn c6_distinct_addresses() -> (Cid, Cid, Cid) { + let (log, _pk) = c5_log(); + let s = &c6_pick(&log, true); + let fire = Cid::of(b"sense-conformance-standing-fire"); + let mk = |dest, parked, woken| { + InjectedObservation::deliver(s, RECIPIENT, STEWARD, dest, 0, parked, woken, 4_200) + .expect("delivers") + .content_id() + .expect("canonicalizes") + }; + ( + mk(Delivery::Own, true, Some(fire)), + mk(Delivery::Derived, true, Some(fire)), + mk(Delivery::Own, false, None), + ) +} + #[cfg(test)] mod tests { use super::*; @@ -245,4 +325,38 @@ mod tests { assert_eq!(ok, Ok(3)); assert_eq!(broken, Err(1)); } + + #[test] + fn c6_a_surfaced_decision_delivers_with_the_links_own_facts() { + let (log, _pk) = c5_log(); + let surfaced = &log.entries()[0]; + assert_eq!(surfaced.core.decision, "surface", "the first link really surfaced"); + let i = c6_injected().expect("a surfaced decision delivers"); + assert_eq!(i.surfacing_id, surfaced.content_id); + assert_eq!(i.event_id, surfaced.core.event_id); + assert_eq!(i.trust, surfaced.core.trust); + assert_ne!(i.author, i.recipient); + assert!(i.woken_under.is_some(), "a parked recipient names its authority"); + assert!(i.charged_uj > 0, "the delivery is priced"); + } + + #[test] + fn c6_every_refusal_fires_for_its_own_reason() { + assert_eq!( + c6_refusals(), + vec![ + InjectError::NotSurfaced, + InjectError::AuthoredByRecipient, + InjectError::NoWakeAuthority, + InjectError::UnpricedDelivery, + ] + ); + } + + #[test] + fn c6_destination_and_wake_are_both_visible_in_the_address() { + let (own_woken, derived_woken, own_running) = c6_distinct_addresses(); + assert_ne!(own_woken, derived_woken, "the destination is bound in"); + assert_ne!(own_woken, own_running, "whether a wake happened is bound in"); + } } diff --git a/sense/sense-rs/crates/sense-conformance/tests/pack.rs b/sense/sense-rs/crates/sense-conformance/tests/pack.rs index 61f1c6d1a..9f23be13a 100644 --- a/sense/sense-rs/crates/sense-conformance/tests/pack.rs +++ b/sense/sense-rs/crates/sense-conformance/tests/pack.rs @@ -311,3 +311,43 @@ fn the_published_envelope_verifies_under_the_published_key() { 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::>().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" + ); +} diff --git a/sense/sense-rs/crates/sense-receipt/src/injected.rs b/sense/sense-rs/crates/sense-receipt/src/injected.rs new file mode 100644 index 000000000..8f8f29367 --- /dev/null +++ b/sense/sense-rs/crates/sense-receipt/src/injected.rs @@ -0,0 +1,343 @@ +//! Surfacing into a running agent (SPEC §11) — the form an observation takes +//! when the thing it is surfaced *to* is an agent rather than a person. +//! +//! §5 decides whether an observation is worth attention and §8 seals that +//! decision. Neither says what the recipient receives, and where the recipient +//! is a person that is right: the form is a UI concern, out of scope (§1.6). +//! Where the recipient is a running agent it is not out of scope, because the +//! delivery enters the agent's own record of what it has been told, and three +//! plausible forms each corrupt that record: +//! +//! - **as a result on a call the agent made.** The agent made no such call. 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. An auditor +//! reading the record afterwards could no longer separate what the agent was +//! *asked* from what it was *told*, which is the distinction that makes the +//! record worth keeping. +//! - **outside the order.** What an agent did depends on when it learned a +//! thing. An observation lifted out of order silently changes what the record +//! says it knew at each point. +//! +//! So a surfaced observation is delivered as a distinctly-kinded frame that +//! occupies one position in the recipient's order and cites the sealed decision +//! it carries out. Four things are refused rather than recorded: delivering a +//! decision that was a hold, an agent authoring its own observation, waking a +//! parked recipient with no authority, and a delivery priced at nothing. + +use crate::{ReceiptError, SealedSurfacing}; +use sense_schema::{Cid, TrustLevel}; +use serde_json::{json, Value}; + +/// The profile tag bound into a delivery's canonical bytes. The `/1` is the +/// shape version: changing the preimage bumps it so two versions never collide. +pub const PROFILE: &str = "sense/injected/1"; + +/// Where a delivered observation landed. +/// +/// Stated, not inferred, because the two say different things about what the +/// recipient knows — and an auditor reading a recipient's record must be able +/// to tell that an observation was handled elsewhere rather than missed. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Delivery { + /// Into the recipient's own record. It is thereafter part of what the + /// recipient knows, and everything the recipient does afterwards was done + /// knowing it. + Own, + /// Into a derived record that inherits the recipient's context. The + /// recipient's own record gains that record's outcome, not the + /// observation — so the recipient did not come to know the observation + /// itself, only what was concluded from it. + Derived, +} + +impl Delivery { + pub fn label(&self) -> &'static str { + match self { + Delivery::Own => "own", + Delivery::Derived => "derived", + } + } +} + +/// Why a delivery was refused. +#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)] +pub enum InjectError { + /// The cited decision was a hold. A held highlight is exactly the record + /// §8 chains so it cannot be quietly forgotten; delivering one would be + /// the same fault in the other direction — a decision not to surface, + /// surfaced anyway. + #[error("the cited decision was a hold, not a surfacing")] + NotSurfaced, + /// The recipient authored the observation it received. An agent able to + /// author its own observations could manufacture a record of having been + /// told something, which is the one thing this frame exists to witness. + #[error("an agent may not author an observation delivered to itself")] + AuthoredByRecipient, + /// The recipient was parked and the delivery named no authority to wake + /// it. Waking a parked run is work someone must have permitted. + #[error("a parked recipient was woken with no authority named")] + NoWakeAuthority, + /// The delivery charged nothing. Something ran — the recipient read the + /// frame — so zero is not a measurement. Where a host cannot measure it + /// estimates and labels (§7); it does not report nothing. + #[error("a delivery that happened cannot be priced at zero")] + UnpricedDelivery, +} + +impl InjectError { + /// A stable token for the pack. The `Display` text is prose and may be + /// reworded; a published vector must not move when it is. + pub fn label(&self) -> &'static str { + match self { + InjectError::NotSurfaced => "not-surfaced", + InjectError::AuthoredByRecipient => "authored-by-recipient", + InjectError::NoWakeAuthority => "no-wake-authority", + InjectError::UnpricedDelivery => "unpriced-delivery", + } + } +} + +/// One observation delivered into a running agent's record (§11). +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct InjectedObservation { + /// The surfacing link (§8) whose decision this delivery carries out, by + /// the content address of its signed core. The delivery cites a sealed + /// decision rather than restating it, so the two cannot drift apart. + pub surfacing_id: Cid, + /// Content id of the observed event (§2). + pub event_id: Cid, + /// Its declared trust level, bound in so a delivery cannot re-tag what it + /// carries. + pub trust: TrustLevel, + /// Where it landed. + pub delivery: Delivery, + /// The principal the observation was delivered to. + pub recipient: String, + /// The principal that delivered it — never the recipient. + pub author: String, + /// The position this frame occupies in the recipient's order. + pub seq: u64, + /// The authority under which a parked recipient was woken: the fire of a + /// standing grant. `None` where the recipient was already running, which + /// is a different fact from a wake nobody permitted. + pub woken_under: Option, + /// Microjoules this delivery charged — the cost of the recipient reading + /// the frame, which is not the cost §5 already charged for deciding to + /// surface it. + pub charged_uj: u64, +} + +impl InjectedObservation { + /// Deliver a sealed surfacing decision into a recipient's record, or + /// refuse. Every field the frame carries is taken from the sealed link or + /// from the delivery itself; nothing is restated by the caller, so a frame + /// cannot disagree with the decision it cites. + #[allow(clippy::too_many_arguments)] + pub fn deliver( + surfacing: &SealedSurfacing, + recipient: impl Into, + author: impl Into, + delivery: Delivery, + seq: u64, + parked: bool, + woken_under: Option, + charged_uj: u64, + ) -> Result { + if surfacing.core.decision != "surface" { + return Err(InjectError::NotSurfaced); + } + let (recipient, author) = (recipient.into(), author.into()); + if recipient == author { + return Err(InjectError::AuthoredByRecipient); + } + if parked && woken_under.is_none() { + return Err(InjectError::NoWakeAuthority); + } + if charged_uj == 0 { + return Err(InjectError::UnpricedDelivery); + } + Ok(Self { + surfacing_id: surfacing.content_id, + event_id: surfacing.core.event_id, + trust: surfacing.core.trust, + delivery, + recipient, + author, + seq, + woken_under, + charged_uj, + }) + } + + /// The canonical wire JSON — a flat uint/text map, as §3 requires of + /// anything content-addressed. `woken_under` is absent rather than null + /// when no wake was needed, so "already running" and "woken" do not share + /// a shape. + pub fn to_wire(&self) -> Value { + let mut m = json!({ + "sense_injected": PROFILE, + "surfacing_id": self.surfacing_id.to_hex(), + "event_id": self.event_id.to_hex(), + "trust": self.trust.label(), + "delivery": self.delivery.label(), + "recipient": self.recipient, + "author": self.author, + "seq": self.seq, + "charged_uj": self.charged_uj, + }); + if let Some(w) = &self.woken_under { + m["woken_under"] = json!(w.to_hex()); + } + m + } + + /// Canonical bytes, under the family's JCR-1 canonicalization. + pub fn canonical_bytes(&self) -> Result, ReceiptError> { + Ok(crate::jcr1::signed_core_bytes(&self.to_wire())?) + } + + /// This frame's content id, so a recipient's record can cite it. + pub fn content_id(&self) -> Result { + Ok(Cid::of(&self.canonical_bytes()?)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::SurfacingLog; + use ed25519_dalek::SigningKey; + use sense_attention::{AttentionBudget, EngagementState, Modality}; + use sense_schema::{Event, Highlight, HighlightKind, Hlc, SourceClass}; + + fn event_id() -> Cid { + Event::new(Hlc::new(1, 0), SourceClass::FileChange, TrustLevel::FirstParty, b"d", "/a.md") + .id() + } + + /// One sealed link. `surface` picks a budget that can afford the + /// interruption; otherwise the gate holds on budget, which is the record + /// a delivery must refuse to act on. + fn sealed(surface: bool) -> SealedSurfacing { + let mut log = SurfacingLog::new(SigningKey::from_bytes(&[7u8; 32]), "did:example:sense"); + let h = Highlight { + kind: HighlightKind::Salience, + score: 0.95, + headline: "x".into(), + triggered_at_unix_ns: 0, + }; + let mut budget = + AttentionBudget::new(if surface { 10_000 } else { 100 }, 1_000); + let v = budget.decide(&h, EngagementState::default(), Modality::Visual); + log.record(event_id(), TrustLevel::FirstParty, &h, &v).unwrap(); + log.entries()[0].clone() + } + + fn deliver( + s: &SealedSurfacing, + recipient: &str, + d: Delivery, + parked: bool, + woken: Option, + uj: u64, + ) -> Result { + InjectedObservation::deliver(s, recipient, "did:example:sense", d, 0, parked, woken, uj) + } + + #[test] + fn a_delivery_carries_the_sealed_decisions_own_facts() { + let s = sealed(true); + assert_eq!(s.core.decision, "surface", "the fixture really did surface"); + let i = InjectedObservation::deliver( + &s, + "did:example:agent", + "did:example:sense", + Delivery::Own, + 3, + false, + None, + 4_200, + ) + .expect("a surfaced decision delivers"); + // Nothing is restated by the caller, so the frame cannot disagree with + // the link it cites. + assert_eq!(i.surfacing_id, s.content_id); + assert_eq!(i.event_id, s.core.event_id); + assert_eq!(i.trust, s.core.trust); + assert_eq!(i.seq, 3); + assert!(i.content_id().is_ok()); + } + + /// A held highlight delivered anyway is the §8 fault in the other + /// direction: a decision not to surface, surfaced. + #[test] + fn a_hold_is_not_deliverable() { + let s = sealed(false); + assert_ne!(s.core.decision, "surface", "the fixture really did hold"); + assert_eq!( + deliver(&s, "did:example:agent", Delivery::Own, false, None, 1), + Err(InjectError::NotSurfaced) + ); + } + + #[test] + fn the_recipient_may_not_be_the_author() { + let s = sealed(true); + assert_eq!( + deliver(&s, "did:example:sense", Delivery::Own, false, None, 1), + Err(InjectError::AuthoredByRecipient) + ); + } + + #[test] + fn waking_a_parked_recipient_needs_an_authority() { + let s = sealed(true); + assert_eq!( + deliver(&s, "did:example:agent", Delivery::Own, true, None, 1), + Err(InjectError::NoWakeAuthority) + ); + let fire = Cid::of(b"standing-fire"); + let ok = deliver(&s, "did:example:agent", Delivery::Own, true, Some(fire), 1).unwrap(); + assert_eq!(ok.woken_under, Some(fire)); + } + + #[test] + fn a_delivery_that_happened_cannot_cost_nothing() { + let s = sealed(true); + assert_eq!( + deliver(&s, "did:example:agent", Delivery::Own, false, None, 0), + Err(InjectError::UnpricedDelivery) + ); + } + + /// The two destinations say different things about what the recipient + /// knows, so they must not share an address. + #[test] + fn the_two_destinations_are_distinguishable() { + let s = sealed(true); + let own = deliver(&s, "did:example:agent", Delivery::Own, false, None, 9).unwrap(); + let derived = deliver(&s, "did:example:agent", Delivery::Derived, false, None, 9).unwrap(); + assert_ne!(own.content_id().unwrap(), derived.content_id().unwrap()); + } + + /// "Already running" and "woken under an authority" are different facts, + /// so they must not canonicalize alike — and the absent case carries no + /// key at all rather than a null. + #[test] + fn a_wake_is_visible_in_the_address() { + let s = sealed(true); + let running = deliver(&s, "did:example:agent", Delivery::Own, false, None, 9).unwrap(); + let woken = deliver( + &s, + "did:example:agent", + Delivery::Own, + true, + Some(Cid::of(b"standing-fire")), + 9, + ) + .unwrap(); + assert_ne!(running.content_id().unwrap(), woken.content_id().unwrap()); + assert!(!running.to_wire().as_object().unwrap().contains_key("woken_under")); + } +} diff --git a/sense/sense-rs/crates/sense-receipt/src/lib.rs b/sense/sense-rs/crates/sense-receipt/src/lib.rs index 9987f1dc4..ac9f3aec8 100644 --- a/sense/sense-rs/crates/sense-receipt/src/lib.rs +++ b/sense/sense-rs/crates/sense-receipt/src/lib.rs @@ -21,6 +21,7 @@ use sense_attention::AttentionVerdict; use sense_schema::{Cid, Highlight, StateProjection, TrustLevel}; use serde_json::{json, Value}; +pub mod injected; pub mod jcr1; /// The profile tag bound into every signed core. The `/1` is the canonical-core