From 35fc9709031c71fc5fb7a51c198589c522edb366 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Mon, 2 Nov 2026 13:56:29 -0500 Subject: [PATCH] =?UTF-8?q?sense=20=C2=A711:=20what=20a=20running=20agent?= =?UTF-8?q?=20receives=20when=20an=20observation=20surfaces=20to=20it?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit §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 --- sense/SPEC.md | 87 +++++ sense/conformance/v1/README.md | 2 +- sense/conformance/v1/vectors.json | 11 +- .../sense-conformance/src/bin/gen_vectors.rs | 9 + .../crates/sense-conformance/src/lib.rs | 116 +++++- .../crates/sense-conformance/tests/pack.rs | 40 ++ .../crates/sense-receipt/src/injected.rs | 343 ++++++++++++++++++ .../sense-rs/crates/sense-receipt/src/lib.rs | 1 + 8 files changed, 606 insertions(+), 3 deletions(-) create mode 100644 sense/sense-rs/crates/sense-receipt/src/injected.rs 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 -- 2.54.0 (Apple Git-157)