sense §11: what a running agent receives when an observation surfaces to it #584
8 changed files with 606 additions and 3 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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<usize, usize>, Result<usize, usize>) {
|
|||
(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<InjectedObservation, InjectError> {
|
||||
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<InjectError> {
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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::<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"
|
||||
);
|
||||
}
|
||||
|
|
|
|||
343
sense/sense-rs/crates/sense-receipt/src/injected.rs
Normal file
343
sense/sense-rs/crates/sense-receipt/src/injected.rs
Normal file
|
|
@ -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<Cid>,
|
||||
/// 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<String>,
|
||||
author: impl Into<String>,
|
||||
delivery: Delivery,
|
||||
seq: u64,
|
||||
parked: bool,
|
||||
woken_under: Option<Cid>,
|
||||
charged_uj: u64,
|
||||
) -> Result<Self, InjectError> {
|
||||
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<Vec<u8>, 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<Cid, ReceiptError> {
|
||||
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<Cid>,
|
||||
uj: u64,
|
||||
) -> Result<InjectedObservation, InjectError> {
|
||||
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"));
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue