diff --git a/crates/huginn-mind/tests/desk_feed_fixtures.rs b/crates/huginn-mind/tests/desk_feed_fixtures.rs index 9c6b64a..399c82b 100644 --- a/crates/huginn-mind/tests/desk_feed_fixtures.rs +++ b/crates/huginn-mind/tests/desk_feed_fixtures.rs @@ -11,6 +11,8 @@ use std::path::PathBuf; +use serde::Serialize; + use cultnet_rs::{CultNetMessage, CultNetWireContract, encode_cultnet_message_to_vec, encode_frame}; use huginn_mind::MindRefusal; use huginn_mind::desk_wire::{ @@ -21,6 +23,11 @@ use huginn_mind::envelope::{OperationFailure, encode_desk_failure, encode_desk_r use huginn_mind::eureka_pipeline::{DeskAuthority, Short, Slug}; const RUNTIME: &str = "huginn-eureka"; +/// Account ids as Bifrost sends them: `UserAccount.Id` rendered lowercase D-format +/// (`DeskEndpoints.cs` `accountId.Value.ToString("D")`; `StartupConfigurationValidator` refuses any +/// other decider). Synthetic values. +const READER: &str = "5f0c2b1e-8a4d-4c3b-9e7f-1a2b3c4d5e6f"; +const OTHER_DECIDER: &str = "0b9d7e3a-6c21-4f58-a0d4-9e8f7a6b5c4d"; /// A question Eureka's mind holds (asked 2026-11-11). The answers, notes and hash in `record()` are /// synthetic: the hash is CultLib's sample digest, not this question's. const FORK: &str = "eureka-substrate:question:desk-listing-authority"; @@ -32,8 +39,8 @@ fn s(text: &str) -> Short { fn forks(status: Option, cursor: Option, limit: u8) -> DeskRequest { DeskRequest::Forks { mind: Slug("eureka".into()), - subject: s("7"), - deciders: vec![s("7"), s("12")], + subject: s(READER), + deciders: vec![s(READER), s(OTHER_DECIDER)], status, cursor: cursor.and_then(ForkCursor::new), limit: ForkLimit::new(limit).unwrap(), @@ -41,7 +48,7 @@ fn forks(status: Option, cursor: Option, limit: u8) -> DeskRequ } fn read() -> DeskRequest { - DeskRequest::Fork { mind: Slug("eureka".into()), subject: s("7"), deciders: vec![s("7"), s("12")], fork_id: FORK.into() } + DeskRequest::Fork { mind: Slug("eureka".into()), subject: s(READER), deciders: vec![s(READER), s(OTHER_DECIDER)], fork_id: FORK.into() } } fn list(open_count: u64, recorded_count: u64) -> DeskResponse { @@ -93,12 +100,112 @@ fn record() -> ForkRecord { options_hash: "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48".into(), counted_answer_id: Some("n1".into()), answers: vec![ - answer("n1", "Counted", None, "7", "The asker lists her own forks.", "2026-11-11T17:00:00Z"), - answer("n2", "Revision", Some("n1"), "12", "Agreed.", "2026-11-11T17:05:00Z"), + answer("n1", "Counted", None, READER, "The asker lists her own forks.", "2026-11-11T17:00:00Z"), + answer("n2", "Revision", Some("n1"), OTHER_DECIDER, "Agreed.", "2026-11-11T17:05:00Z"), ], } } +// ---- PROPOSED reader views (ruling: the door withholds account ids from readers who are not +// deciders, and returns counts). CultLib's card and desk_answer cannot carry this yet (card +// `deciders` has minItems 1 and no count key; `answerer` is a required non-null id), so these are +// fragments in the shape proposed to Libby (follow-up `cultlib-fork-card-reader-counts`), built from +// mirror types that exist only here. They are not Huginn's output until that contract lands and +// `cut-desk-feed-listing` builds it. ---- + +/// The two card keys the proposal changes, as a fragment: a decider gets the ids, anyone else `[]` +/// and the count. Not a card. +#[derive(Serialize)] +struct CardViewers { + deciders: Vec, + decider_count: u64, +} + +fn card_viewers(decider: bool) -> CardViewers { + let ids = vec![READER.to_string(), OTHER_DECIDER.to_string()]; + CardViewers { decider_count: ids.len() as u64, deciders: if decider { ids } else { Vec::new() } } +} + +/// `gamecult.desk_answer.v0` as proposed for a reader who is not a decider: `answerer` nil, key kept. +#[derive(Serialize)] +struct WithheldAnswer { + #[serde(rename = "schemaVersion")] + schema_version: AnswerSchema, + answer_id: String, + fork_id: String, + outcome: String, + of: Option, + retracts: Option, + code: Option, + options_hash: String, + choice: Option, + note: Option, + answered_at: String, + received_at: String, + claim_in_future: bool, + answerer: Option, + authority: DeskAuthority, + request_id: String, +} + +/// `gamecult.fork.record.v0` as proposed for a reader who is not a decider: every `answerer` nil and +/// `answerer_count`, the number of distinct answerers, in their place. +#[derive(Serialize)] +struct WithheldRecord { + #[serde(rename = "schemaVersion")] + schema_version: RecordSchema, + fork_id: String, + options_hash: String, + counted_answer_id: Option, + answerer_count: u64, + answers: Vec, +} + +fn withheld(record: ForkRecord) -> WithheldRecord { + let mut answerers: Vec<&str> = record.answers.iter().map(|answer| answer.answerer.as_str()).collect(); + answerers.sort_unstable(); + answerers.dedup(); + let answerer_count = answerers.len() as u64; + WithheldRecord { + schema_version: record.schema_version, + fork_id: record.fork_id, + options_hash: record.options_hash, + counted_answer_id: record.counted_answer_id, + answerer_count, + answers: record + .answers + .into_iter() + .map(|answer| WithheldAnswer { + schema_version: answer.schema_version, + answer_id: answer.answer_id, + fork_id: answer.fork_id, + outcome: answer.outcome, + of: answer.of, + retracts: answer.retracts, + code: answer.code, + options_hash: answer.options_hash, + choice: answer.choice, + note: answer.note, + answered_at: answer.answered_at, + received_at: answer.received_at, + claim_in_future: answer.claim_in_future, + answerer: None, + authority: answer.authority, + request_id: answer.request_id, + }) + .collect(), + } +} + +/// The proposed fragments by reader kind: (file stem, bytes). +fn reader_views() -> Vec<(&'static str, Vec)> { + vec![ + ("card-viewers-decider.synthetic", rmp_serde::to_vec_named(&card_viewers(true)).unwrap()), + ("card-viewers-non-decider.synthetic", rmp_serde::to_vec_named(&card_viewers(false)).unwrap()), + ("record-non-decider.proposed", rmp_serde::to_vec_named(&withheld(record())).unwrap()), + ] +} + /// The named MessagePack payload a CultNet operation message carries (base64 in the envelope). fn payload(message: &CultNetMessage) -> Vec { use base64::Engine; @@ -170,6 +277,10 @@ fn fixtures() -> Vec<(String, Vec)> { let whole = rmp_serde::to_vec_named(&record()).unwrap(); files.push(("record-whole.payload.msgpack".into(), whole.clone())); files.push(("record-whole.payload.hex".into(), hex(&whole).into_bytes())); + for (name, bytes) in reader_views() { + files.push((format!("{name}.payload.msgpack"), bytes.clone())); + files.push((format!("{name}.payload.hex"), hex(&bytes).into_bytes())); + } files } @@ -190,3 +301,74 @@ fn desk_feed_fixtures_are_what_the_encoders_write() { } } } + +/// The one grammar for `subject`, every `deciders` entry and every `answerer`: a lowercase +/// D-format UUID (8-4-4-4-12 lowercase hex), the grammar Huginn already names for `RequestId` +/// (`eureka-pipeline` `uuid_text`). A Matrix id, an email or a handle never fits it. +fn account_id(text: &str) -> bool { + text.len() == 36 + && text.bytes().enumerate().all(|(at, byte)| match at { + 8 | 13 | 18 | 23 => byte == b'-', + _ => matches!(byte, b'0'..=b'9' | b'a'..=b'f'), + }) +} + +/// Every account id the fixtures carry is a Bifrost account id, never a Matrix id, email or +/// handle. The door does not refuse other shapes yet (`Short`, any 200 bytes); that refusal is owed +/// by `cut-desk-feed-listing.r6`, and this pins the grammar it must enforce. +#[test] +fn every_account_id_in_the_fixtures_is_a_lowercase_d_format_uuid() { + for request in [forks(Some(ListStatus::Open), None, 50), read()] { + let subject = match &request { + DeskRequest::Forks { subject, .. } | DeskRequest::Fork { subject, .. } | DeskRequest::Answer { subject, .. } => subject, + }; + assert!(account_id(&subject.0), "subject"); + assert!(request.deciders().iter().all(|decider| account_id(&decider.0)), "deciders"); + } + assert!(record().answers.iter().all(|answer| account_id(&answer.answerer)), "answerer"); + for shape in [ + "@heimdall:gamecult.org", + "meta@gamecult.org", + "heimdall", + "7", + "5F0C2B1E-8A4D-4C3B-9E7F-1A2B3C4D5E6F", + "{5f0c2b1e-8a4d-4c3b-9e7f-1a2b3c4d5e6f}", + "", + // 36 hex digits and no dash: the length alone does not make an id. + "5f0c2b1e08a4d04c3b09e7f01a2b3c4d5e6f", + // 37 characters: a valid id with one more digit. + "5f0c2b1e-8a4d-4c3b-9e7f-1a2b3c4d5e6f0", + // A dash moved off its position. + "5f0c2b1e8-a4d-4c3b-9e7f-1a2b3c4d5e6f", + ] { + assert!(!account_id(shape), "{shape} is not an account id"); + } +} + +fn holds(bytes: &[u8], text: &str) -> bool { + bytes.windows(text.len()).any(|window| window == text.as_bytes()) +} + +/// This test pins MIRROR TYPES ONLY (`CardViewers`, `WithheldRecord`), the shapes proposed to CultLib. +/// It does not exercise Huginn's projection, which does not withhold ids yet. The tests on real list and +/// read responses land with `cut-desk-feed-listing` (r6). +/// +/// A reader who is not a decider gets counts and no account id at all: neither UUID appears +/// anywhere in its fragments' bytes. A decider gets every id. (Proposed shapes; the build cut pins +/// the same on Huginn's real list and read responses.) +#[test] +fn a_non_decider_gets_counts_and_no_ids_a_decider_gets_ids() { + let views = reader_views(); + let bytes = |name: &str| views.iter().find(|(stem, _)| *stem == name).unwrap().1.clone(); + for name in ["card-viewers-non-decider.synthetic", "record-non-decider.proposed"] { + let view = bytes(name); + assert!(!holds(&view, READER) && !holds(&view, OTHER_DECIDER), "{name} carries an account id"); + } + let non_decider = card_viewers(false); + assert_eq!((non_decider.deciders.len(), non_decider.decider_count), (0, 2)); + assert_eq!(withheld(record()).answerer_count, 2); + let decider = bytes("card-viewers-decider.synthetic"); + assert!(holds(&decider, READER) && holds(&decider, OTHER_DECIDER)); + let whole = rmp_serde::to_vec_named(&record()).unwrap(); + assert!(holds(&whole, READER) && holds(&whole, OTHER_DECIDER), "a decider's record keeps the answerer ids"); +} diff --git a/crates/huginn-mind/tests/fixtures/desk-feed/README.md b/crates/huginn-mind/tests/fixtures/desk-feed/README.md index b018191..0525883 100644 --- a/crates/huginn-mind/tests/fixtures/desk-feed/README.md +++ b/crates/huginn-mind/tests/fixtures/desk-feed/README.md @@ -35,7 +35,10 @@ lands (see `docs/desk-feed-door.md`). | `unavailable.response` | Huginn to Bifrost | `Refused` `Unavailable` (Bifrost: 503 for this Mind) | | `failure.response` | Huginn to Bifrost | operation failure, `payloadSchema` `gamecult.cultnet.operation_failure.v1` (Bifrost: 503) | | `error.response` | Huginn to Bifrost | CultNet `Error` message, no payload (Bifrost: 503) | -| `record-whole` | fragment | `gamecult.fork.record.v0` with a counted answer and a revision, answerer ids and notes raw, as the door sends them (Bifrost masks for a reader who is not a decider) | +| `record-whole` | fragment | `gamecult.fork.record.v0` with a counted answer and a revision, answerer ids and notes whole: what the door sends today, and a decider's view | +| `record-non-decider.proposed` | fragment | PROPOSED non-decider record: every `answerer` nil, `answerer_count` 2. Needs CultLib change `cultlib-fork-card-reader-counts`; not Huginn output yet | +| `card-viewers-decider.synthetic` | fragment | SYNTHETIC: only `{deciders: [two ids], decider_count: 2}`. Not a card | +| `card-viewers-non-decider.synthetic` | fragment | SYNTHETIC: only `{deciders: [], decider_count: 2}`. Not a card | No case carries a card (`gamecult.fork.card.v0`). The card's bytes wait on CultLib's re-pinned card vectors for `raised_at`, so no entry-bearing list page or read is pinned yet. diff --git a/crates/huginn-mind/tests/fixtures/desk-feed/card-viewers-decider.synthetic.payload.hex b/crates/huginn-mind/tests/fixtures/desk-feed/card-viewers-decider.synthetic.payload.hex new file mode 100644 index 0000000..1624ae5 --- /dev/null +++ b/crates/huginn-mind/tests/fixtures/desk-feed/card-viewers-decider.synthetic.payload.hex @@ -0,0 +1 @@ +82a8646563696465727392d92435663063326231652d386134642d346333622d396537662d316132623363346435653666d92430623964376533612d366332312d346635382d613064342d396538663761366235633464ad646563696465725f636f756e7402 diff --git a/crates/huginn-mind/tests/fixtures/desk-feed/card-viewers-decider.synthetic.payload.msgpack b/crates/huginn-mind/tests/fixtures/desk-feed/card-viewers-decider.synthetic.payload.msgpack new file mode 100644 index 0000000..71b15b3 --- /dev/null +++ b/crates/huginn-mind/tests/fixtures/desk-feed/card-viewers-decider.synthetic.payload.msgpack @@ -0,0 +1 @@ +‚¨deciders’Ù$5f0c2b1e-8a4d-4c3b-9e7f-1a2b3c4d5e6fÙ$0b9d7e3a-6c21-4f58-a0d4-9e8f7a6b5c4d­decider_count \ No newline at end of file diff --git a/crates/huginn-mind/tests/fixtures/desk-feed/card-viewers-non-decider.synthetic.payload.hex b/crates/huginn-mind/tests/fixtures/desk-feed/card-viewers-non-decider.synthetic.payload.hex new file mode 100644 index 0000000..29a1257 --- /dev/null +++ b/crates/huginn-mind/tests/fixtures/desk-feed/card-viewers-non-decider.synthetic.payload.hex @@ -0,0 +1 @@ +82a8646563696465727390ad646563696465725f636f756e7402 diff --git a/crates/huginn-mind/tests/fixtures/desk-feed/card-viewers-non-decider.synthetic.payload.msgpack b/crates/huginn-mind/tests/fixtures/desk-feed/card-viewers-non-decider.synthetic.payload.msgpack new file mode 100644 index 0000000..252f158 --- /dev/null +++ b/crates/huginn-mind/tests/fixtures/desk-feed/card-viewers-non-decider.synthetic.payload.msgpack @@ -0,0 +1 @@ +‚¨deciders�­decider_count \ No newline at end of file diff --git a/crates/huginn-mind/tests/fixtures/desk-feed/list-counts.request.frame.bin b/crates/huginn-mind/tests/fixtures/desk-feed/list-counts.request.frame.bin index b5c855d..87fae5a 100644 Binary files a/crates/huginn-mind/tests/fixtures/desk-feed/list-counts.request.frame.bin and b/crates/huginn-mind/tests/fixtures/desk-feed/list-counts.request.frame.bin differ diff --git a/crates/huginn-mind/tests/fixtures/desk-feed/list-counts.request.frame.hex b/crates/huginn-mind/tests/fixtures/desk-feed/list-counts.request.frame.hex index d0554ad..f11745c 100644 --- a/crates/huginn-mind/tests/fixtures/desk-feed/list-counts.request.frame.hex +++ b/crates/huginn-mind/tests/fixtures/desk-feed/list-counts.request.frame.hex @@ -1 +1 @@ -0000011287a96d6573736167654964a36d2d33a96f7065726174696f6ea5666f726b73a77061796c6f6164d9606761564762334a726334616b62576c755a4b5a6c64584a6c6132476e63335669616d566a644b45337147526c59326c6b5a584a7a6b7145336f6a4579706e4e30595852316336527663475675706d4e31636e4e766373436c62476c7461585141af7061796c6f6164456e636f64696e67b26d6573736167657061636b2d626173653634ad7061796c6f6164536368656d61b6687567696e6e2e6465736b5f726571756573742e7631ad736368656d6156657273696f6ebc63756c746e65742e6f7065726174696f6e5f726571756573742e7630a9736572766963654964ab687567696e6e2e6465736b +000001a287a96d6573736167654964a36d2d33a96f7065726174696f6ea5666f726b73a77061796c6f6164d9f06761564762334a726334616b62576c755a4b5a6c64584a6c6132476e63335669616d566a644e6b6b4e575977597a4a694d5755744f4745305a433030597a4e694c546c6c4e3259744d574579596a4e6a4e4751315a545a6d7147526c59326c6b5a584a7a6b746b6b4e575977597a4a694d5755744f4745305a433030597a4e694c546c6c4e3259744d574579596a4e6a4e4751315a545a6d32535177596a6c6b4e32557a59533032597a49784c54526d4e5467745954426b4e4330355a54686d4e324532596a566a4e47536d633352686448567a704739775a57366d5933567963323979774b5673615731706441413daf7061796c6f6164456e636f64696e67b26d6573736167657061636b2d626173653634ad7061796c6f6164536368656d61b6687567696e6e2e6465736b5f726571756573742e7631ad736368656d6156657273696f6ebc63756c746e65742e6f7065726174696f6e5f726571756573742e7630a9736572766963654964ab687567696e6e2e6465736b diff --git a/crates/huginn-mind/tests/fixtures/desk-feed/list-counts.request.payload.hex b/crates/huginn-mind/tests/fixtures/desk-feed/list-counts.request.payload.hex index 60cb15a..804b47e 100644 --- a/crates/huginn-mind/tests/fixtures/desk-feed/list-counts.request.payload.hex +++ b/crates/huginn-mind/tests/fixtures/desk-feed/list-counts.request.payload.hex @@ -1 +1 @@ -81a5466f726b7386a46d696e64a6657572656b61a77375626a656374a137a8646563696465727392a137a23132a6737461747573a46f70656ea6637572736f72c0a56c696d697400 +81a5466f726b7386a46d696e64a6657572656b61a77375626a656374d92435663063326231652d386134642d346333622d396537662d316132623363346435653666a8646563696465727392d92435663063326231652d386134642d346333622d396537662d316132623363346435653666d92430623964376533612d366332312d346635382d613064342d396538663761366235633464a6737461747573a46f70656ea6637572736f72c0a56c696d697400 diff --git a/crates/huginn-mind/tests/fixtures/desk-feed/list-counts.request.payload.msgpack b/crates/huginn-mind/tests/fixtures/desk-feed/list-counts.request.payload.msgpack index 931ad8d..d79df5f 100644 Binary files a/crates/huginn-mind/tests/fixtures/desk-feed/list-counts.request.payload.msgpack and b/crates/huginn-mind/tests/fixtures/desk-feed/list-counts.request.payload.msgpack differ diff --git a/crates/huginn-mind/tests/fixtures/desk-feed/list-open-cursor.request.frame.bin b/crates/huginn-mind/tests/fixtures/desk-feed/list-open-cursor.request.frame.bin index 55963c3..f2479d5 100644 Binary files a/crates/huginn-mind/tests/fixtures/desk-feed/list-open-cursor.request.frame.bin and b/crates/huginn-mind/tests/fixtures/desk-feed/list-open-cursor.request.frame.bin differ diff --git a/crates/huginn-mind/tests/fixtures/desk-feed/list-open-cursor.request.frame.hex b/crates/huginn-mind/tests/fixtures/desk-feed/list-open-cursor.request.frame.hex index 74bfc3c..466d859 100644 --- a/crates/huginn-mind/tests/fixtures/desk-feed/list-open-cursor.request.frame.hex +++ b/crates/huginn-mind/tests/fixtures/desk-feed/list-open-cursor.request.frame.hex @@ -1 +1 @@ -0000011687a96d6573736167654964a36d2d32a96f7065726174696f6ea5666f726b73a77061796c6f6164d9646761564762334a726334616b62576c755a4b5a6c64584a6c6132476e63335669616d566a644b45337147526c59326c6b5a584a7a6b7145336f6a4579706e4e30595852316336527663475675706d4e31636e4e7663714e6a4e444b6c62476c7461585179af7061796c6f6164456e636f64696e67b26d6573736167657061636b2d626173653634ad7061796c6f6164536368656d61b6687567696e6e2e6465736b5f726571756573742e7631ad736368656d6156657273696f6ebc63756c746e65742e6f7065726174696f6e5f726571756573742e7630a9736572766963654964ab687567696e6e2e6465736b +000001a687a96d6573736167654964a36d2d32a96f7065726174696f6ea5666f726b73a77061796c6f6164d9f46761564762334a726334616b62576c755a4b5a6c64584a6c6132476e63335669616d566a644e6b6b4e575977597a4a694d5755744f4745305a433030597a4e694c546c6c4e3259744d574579596a4e6a4e4751315a545a6d7147526c59326c6b5a584a7a6b746b6b4e575977597a4a694d5755744f4745305a433030597a4e694c546c6c4e3259744d574579596a4e6a4e4751315a545a6d32535177596a6c6b4e32557a59533032597a49784c54526d4e5467745954426b4e4330355a54686d4e324532596a566a4e47536d633352686448567a704739775a57366d59335679633239796f324d304d715673615731706444493daf7061796c6f6164456e636f64696e67b26d6573736167657061636b2d626173653634ad7061796c6f6164536368656d61b6687567696e6e2e6465736b5f726571756573742e7631ad736368656d6156657273696f6ebc63756c746e65742e6f7065726174696f6e5f726571756573742e7630a9736572766963654964ab687567696e6e2e6465736b diff --git a/crates/huginn-mind/tests/fixtures/desk-feed/list-open-cursor.request.payload.hex b/crates/huginn-mind/tests/fixtures/desk-feed/list-open-cursor.request.payload.hex index cd0c5b0..4004719 100644 --- a/crates/huginn-mind/tests/fixtures/desk-feed/list-open-cursor.request.payload.hex +++ b/crates/huginn-mind/tests/fixtures/desk-feed/list-open-cursor.request.payload.hex @@ -1 +1 @@ -81a5466f726b7386a46d696e64a6657572656b61a77375626a656374a137a8646563696465727392a137a23132a6737461747573a46f70656ea6637572736f72a3633432a56c696d697432 +81a5466f726b7386a46d696e64a6657572656b61a77375626a656374d92435663063326231652d386134642d346333622d396537662d316132623363346435653666a8646563696465727392d92435663063326231652d386134642d346333622d396537662d316132623363346435653666d92430623964376533612d366332312d346635382d613064342d396538663761366235633464a6737461747573a46f70656ea6637572736f72a3633432a56c696d697432 diff --git a/crates/huginn-mind/tests/fixtures/desk-feed/list-open-cursor.request.payload.msgpack b/crates/huginn-mind/tests/fixtures/desk-feed/list-open-cursor.request.payload.msgpack index d9b0991..c0349ca 100644 --- a/crates/huginn-mind/tests/fixtures/desk-feed/list-open-cursor.request.payload.msgpack +++ b/crates/huginn-mind/tests/fixtures/desk-feed/list-open-cursor.request.payload.msgpack @@ -1 +1 @@ -�¥Forks†¤mind¦eureka§subject¡7¨deciders’¡7¢12¦status¤open¦cursor£c42¥limit2 \ No newline at end of file +�¥Forks†¤mind¦eureka§subjectÙ$5f0c2b1e-8a4d-4c3b-9e7f-1a2b3c4d5e6f¨deciders’Ù$5f0c2b1e-8a4d-4c3b-9e7f-1a2b3c4d5e6fÙ$0b9d7e3a-6c21-4f58-a0d4-9e8f7a6b5c4d¦status¤open¦cursor£c42¥limit2 \ No newline at end of file diff --git a/crates/huginn-mind/tests/fixtures/desk-feed/list-open.request.frame.bin b/crates/huginn-mind/tests/fixtures/desk-feed/list-open.request.frame.bin index 0da51f3..8ea0611 100644 Binary files a/crates/huginn-mind/tests/fixtures/desk-feed/list-open.request.frame.bin and b/crates/huginn-mind/tests/fixtures/desk-feed/list-open.request.frame.bin differ diff --git a/crates/huginn-mind/tests/fixtures/desk-feed/list-open.request.frame.hex b/crates/huginn-mind/tests/fixtures/desk-feed/list-open.request.frame.hex index 304b63b..c351ca8 100644 --- a/crates/huginn-mind/tests/fixtures/desk-feed/list-open.request.frame.hex +++ b/crates/huginn-mind/tests/fixtures/desk-feed/list-open.request.frame.hex @@ -1 +1 @@ -0000011287a96d6573736167654964a36d2d31a96f7065726174696f6ea5666f726b73a77061796c6f6164d9606761564762334a726334616b62576c755a4b5a6c64584a6c6132476e63335669616d566a644b45337147526c59326c6b5a584a7a6b7145336f6a4579706e4e30595852316336527663475675706d4e31636e4e766373436c62476c7461585179af7061796c6f6164456e636f64696e67b26d6573736167657061636b2d626173653634ad7061796c6f6164536368656d61b6687567696e6e2e6465736b5f726571756573742e7631ad736368656d6156657273696f6ebc63756c746e65742e6f7065726174696f6e5f726571756573742e7630a9736572766963654964ab687567696e6e2e6465736b +000001a287a96d6573736167654964a36d2d31a96f7065726174696f6ea5666f726b73a77061796c6f6164d9f06761564762334a726334616b62576c755a4b5a6c64584a6c6132476e63335669616d566a644e6b6b4e575977597a4a694d5755744f4745305a433030597a4e694c546c6c4e3259744d574579596a4e6a4e4751315a545a6d7147526c59326c6b5a584a7a6b746b6b4e575977597a4a694d5755744f4745305a433030597a4e694c546c6c4e3259744d574579596a4e6a4e4751315a545a6d32535177596a6c6b4e32557a59533032597a49784c54526d4e5467745954426b4e4330355a54686d4e324532596a566a4e47536d633352686448567a704739775a57366d5933567963323979774b5673615731706444493daf7061796c6f6164456e636f64696e67b26d6573736167657061636b2d626173653634ad7061796c6f6164536368656d61b6687567696e6e2e6465736b5f726571756573742e7631ad736368656d6156657273696f6ebc63756c746e65742e6f7065726174696f6e5f726571756573742e7630a9736572766963654964ab687567696e6e2e6465736b diff --git a/crates/huginn-mind/tests/fixtures/desk-feed/list-open.request.payload.hex b/crates/huginn-mind/tests/fixtures/desk-feed/list-open.request.payload.hex index e8564d6..2006332 100644 --- a/crates/huginn-mind/tests/fixtures/desk-feed/list-open.request.payload.hex +++ b/crates/huginn-mind/tests/fixtures/desk-feed/list-open.request.payload.hex @@ -1 +1 @@ -81a5466f726b7386a46d696e64a6657572656b61a77375626a656374a137a8646563696465727392a137a23132a6737461747573a46f70656ea6637572736f72c0a56c696d697432 +81a5466f726b7386a46d696e64a6657572656b61a77375626a656374d92435663063326231652d386134642d346333622d396537662d316132623363346435653666a8646563696465727392d92435663063326231652d386134642d346333622d396537662d316132623363346435653666d92430623964376533612d366332312d346635382d613064342d396538663761366235633464a6737461747573a46f70656ea6637572736f72c0a56c696d697432 diff --git a/crates/huginn-mind/tests/fixtures/desk-feed/list-open.request.payload.msgpack b/crates/huginn-mind/tests/fixtures/desk-feed/list-open.request.payload.msgpack index 90f32b7..0b18a68 100644 --- a/crates/huginn-mind/tests/fixtures/desk-feed/list-open.request.payload.msgpack +++ b/crates/huginn-mind/tests/fixtures/desk-feed/list-open.request.payload.msgpack @@ -1 +1 @@ -�¥Forks†¤mind¦eureka§subject¡7¨deciders’¡7¢12¦status¤open¦cursorÀ¥limit2 \ No newline at end of file +�¥Forks†¤mind¦eureka§subjectÙ$5f0c2b1e-8a4d-4c3b-9e7f-1a2b3c4d5e6f¨deciders’Ù$5f0c2b1e-8a4d-4c3b-9e7f-1a2b3c4d5e6fÙ$0b9d7e3a-6c21-4f58-a0d4-9e8f7a6b5c4d¦status¤open¦cursorÀ¥limit2 \ No newline at end of file diff --git a/crates/huginn-mind/tests/fixtures/desk-feed/read.request.frame.bin b/crates/huginn-mind/tests/fixtures/desk-feed/read.request.frame.bin index 9b114db..181317a 100644 Binary files a/crates/huginn-mind/tests/fixtures/desk-feed/read.request.frame.bin and b/crates/huginn-mind/tests/fixtures/desk-feed/read.request.frame.bin differ diff --git a/crates/huginn-mind/tests/fixtures/desk-feed/read.request.frame.hex b/crates/huginn-mind/tests/fixtures/desk-feed/read.request.frame.hex index 64c88a6..f581179 100644 --- a/crates/huginn-mind/tests/fixtures/desk-feed/read.request.frame.hex +++ b/crates/huginn-mind/tests/fixtures/desk-feed/read.request.frame.hex @@ -1 +1 @@ -0000013987a96d6573736167654964a36d2d34a96f7065726174696f6ea4666f726ba77061796c6f6164d9886761524762334a72684b52746157356b706d5631636d56725961647a64574a715a574e306f54656f5a47566a6157526c636e4f536f5465694d544b6e5a6d3979613139705a4e6b775a5856795a5774684c584e31596e4e30636d46305a5470786457567a64476c76626a706b5a584e724c57787063335270626d6374595856306147397961585235af7061796c6f6164456e636f64696e67b26d6573736167657061636b2d626173653634ad7061796c6f6164536368656d61b6687567696e6e2e6465736b5f726571756573742e7631ad736368656d6156657273696f6ebc63756c746e65742e6f7065726174696f6e5f726571756573742e7630a9736572766963654964ab687567696e6e2e6465736b +000001ca87a96d6573736167654964a36d2d34a96f7065726174696f6ea4666f726ba77061796c6f6164da01186761524762334a72684b52746157356b706d5631636d56725961647a64574a715a574e30325351315a6a426a4d6d49785a5330345954526b4c54526a4d3249744f5755335a69307859544a694d324d305a44566c4e6d616f5a47566a6157526c636e4f53325351315a6a426a4d6d49785a5330345954526b4c54526a4d3249744f5755335a69307859544a694d324d305a44566c4e6d625a4a4442694f5751335a544e684c545a6a4d6a45744e4759314f4331684d4751304c546c6c4f47593359545a694e574d305a4b646d62334a7258326c6b3254426c64584a6c6132457463335669633352795958526c4f6e46315a584e30615739754f6d526c6332737462476c7a64476c755a7931686458526f62334a7064486b3daf7061796c6f6164456e636f64696e67b26d6573736167657061636b2d626173653634ad7061796c6f6164536368656d61b6687567696e6e2e6465736b5f726571756573742e7631ad736368656d6156657273696f6ebc63756c746e65742e6f7065726174696f6e5f726571756573742e7630a9736572766963654964ab687567696e6e2e6465736b diff --git a/crates/huginn-mind/tests/fixtures/desk-feed/read.request.payload.hex b/crates/huginn-mind/tests/fixtures/desk-feed/read.request.payload.hex index 68e7538..efe17b0 100644 --- a/crates/huginn-mind/tests/fixtures/desk-feed/read.request.payload.hex +++ b/crates/huginn-mind/tests/fixtures/desk-feed/read.request.payload.hex @@ -1 +1 @@ -81a4466f726b84a46d696e64a6657572656b61a77375626a656374a137a8646563696465727392a137a23132a7666f726b5f6964d930657572656b612d7375627374726174653a7175657374696f6e3a6465736b2d6c697374696e672d617574686f72697479 +81a4466f726b84a46d696e64a6657572656b61a77375626a656374d92435663063326231652d386134642d346333622d396537662d316132623363346435653666a8646563696465727392d92435663063326231652d386134642d346333622d396537662d316132623363346435653666d92430623964376533612d366332312d346635382d613064342d396538663761366235633464a7666f726b5f6964d930657572656b612d7375627374726174653a7175657374696f6e3a6465736b2d6c697374696e672d617574686f72697479 diff --git a/crates/huginn-mind/tests/fixtures/desk-feed/read.request.payload.msgpack b/crates/huginn-mind/tests/fixtures/desk-feed/read.request.payload.msgpack index bda8f77..27941b5 100644 --- a/crates/huginn-mind/tests/fixtures/desk-feed/read.request.payload.msgpack +++ b/crates/huginn-mind/tests/fixtures/desk-feed/read.request.payload.msgpack @@ -1 +1 @@ -�¤Fork„¤mind¦eureka§subject¡7¨deciders’¡7¢12§fork_idÙ0eureka-substrate:question:desk-listing-authority \ No newline at end of file +�¤Fork„¤mind¦eureka§subjectÙ$5f0c2b1e-8a4d-4c3b-9e7f-1a2b3c4d5e6f¨deciders’Ù$5f0c2b1e-8a4d-4c3b-9e7f-1a2b3c4d5e6fÙ$0b9d7e3a-6c21-4f58-a0d4-9e8f7a6b5c4d§fork_idÙ0eureka-substrate:question:desk-listing-authority \ No newline at end of file diff --git a/crates/huginn-mind/tests/fixtures/desk-feed/record-non-decider.proposed.payload.hex b/crates/huginn-mind/tests/fixtures/desk-feed/record-non-decider.proposed.payload.hex new file mode 100644 index 0000000..4053399 --- /dev/null +++ b/crates/huginn-mind/tests/fixtures/desk-feed/record-non-decider.proposed.payload.hex @@ -0,0 +1 @@ +86ad736368656d6156657273696f6eb767616d6563756c742e666f726b2e7265636f72642e7630a7666f726b5f6964d930657572656b612d7375627374726174653a7175657374696f6e3a6465736b2d6c697374696e672d617574686f72697479ac6f7074696f6e735f68617368d94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438b1636f756e7465645f616e737765725f6964a26e31ae616e7377657265725f636f756e7402a7616e737765727392de0010ad736368656d6156657273696f6eb767616d6563756c742e6465736b5f616e737765722e7630a9616e737765725f6964a26e31a7666f726b5f6964d930657572656b612d7375627374726174653a7175657374696f6e3a6465736b2d6c697374696e672d617574686f72697479a76f7574636f6d65a7436f756e746564a26f66c0a87265747261637473c0a4636f6465c0ac6f7074696f6e735f68617368d94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438a663686f696365ab61736b696e672d6d696e64a46e6f7465be5468652061736b6572206c6973747320686572206f776e20666f726b732eab616e7377657265645f6174b4323032362d31312d31315431373a30303a30305aab72656365697665645f6174b4323032362d31312d31315431373a30303a30305aaf636c61696d5f696e5f667574757265c2a8616e737765726572c0a9617574686f72697479a64d656d626572aa726571756573745f6964d92430303030303030302d303030302d343030302d383030302d303030303030303030303031de0010ad736368656d6156657273696f6eb767616d6563756c742e6465736b5f616e737765722e7630a9616e737765725f6964a26e32a7666f726b5f6964d930657572656b612d7375627374726174653a7175657374696f6e3a6465736b2d6c697374696e672d617574686f72697479a76f7574636f6d65a85265766973696f6ea26f66a26e31a87265747261637473c0a4636f6465c0ac6f7074696f6e735f68617368d94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438a663686f696365ab61736b696e672d6d696e64a46e6f7465a74167726565642eab616e7377657265645f6174b4323032362d31312d31315431373a30353a30305aab72656365697665645f6174b4323032362d31312d31315431373a30353a30305aaf636c61696d5f696e5f667574757265c2a8616e737765726572c0a9617574686f72697479a64d656d626572aa726571756573745f6964d92430303030303030302d303030302d343030302d383030302d303030303030303030303032 diff --git a/crates/huginn-mind/tests/fixtures/desk-feed/record-non-decider.proposed.payload.msgpack b/crates/huginn-mind/tests/fixtures/desk-feed/record-non-decider.proposed.payload.msgpack new file mode 100644 index 0000000..d184479 Binary files /dev/null and b/crates/huginn-mind/tests/fixtures/desk-feed/record-non-decider.proposed.payload.msgpack differ diff --git a/crates/huginn-mind/tests/fixtures/desk-feed/record-whole.payload.hex b/crates/huginn-mind/tests/fixtures/desk-feed/record-whole.payload.hex index ecf6f3f..aa5f793 100644 --- a/crates/huginn-mind/tests/fixtures/desk-feed/record-whole.payload.hex +++ b/crates/huginn-mind/tests/fixtures/desk-feed/record-whole.payload.hex @@ -1 +1 @@ -85ad736368656d6156657273696f6eb767616d6563756c742e666f726b2e7265636f72642e7630a7666f726b5f6964d930657572656b612d7375627374726174653a7175657374696f6e3a6465736b2d6c697374696e672d617574686f72697479ac6f7074696f6e735f68617368d94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438b1636f756e7465645f616e737765725f6964a26e31a7616e737765727392de0010ad736368656d6156657273696f6eb767616d6563756c742e6465736b5f616e737765722e7630a9616e737765725f6964a26e31a7666f726b5f6964d930657572656b612d7375627374726174653a7175657374696f6e3a6465736b2d6c697374696e672d617574686f72697479a76f7574636f6d65a7436f756e746564a26f66c0a87265747261637473c0a4636f6465c0ac6f7074696f6e735f68617368d94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438a663686f696365ab61736b696e672d6d696e64a46e6f7465be5468652061736b6572206c6973747320686572206f776e20666f726b732eab616e7377657265645f6174b4323032362d31312d31315431373a30303a30305aab72656365697665645f6174b4323032362d31312d31315431373a30303a30305aaf636c61696d5f696e5f667574757265c2a8616e737765726572a137a9617574686f72697479a64d656d626572aa726571756573745f6964d92430303030303030302d303030302d343030302d383030302d303030303030303030303031de0010ad736368656d6156657273696f6eb767616d6563756c742e6465736b5f616e737765722e7630a9616e737765725f6964a26e32a7666f726b5f6964d930657572656b612d7375627374726174653a7175657374696f6e3a6465736b2d6c697374696e672d617574686f72697479a76f7574636f6d65a85265766973696f6ea26f66a26e31a87265747261637473c0a4636f6465c0ac6f7074696f6e735f68617368d94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438a663686f696365ab61736b696e672d6d696e64a46e6f7465a74167726565642eab616e7377657265645f6174b4323032362d31312d31315431373a30353a30305aab72656365697665645f6174b4323032362d31312d31315431373a30353a30305aaf636c61696d5f696e5f667574757265c2a8616e737765726572a23132a9617574686f72697479a64d656d626572aa726571756573745f6964d92430303030303030302d303030302d343030302d383030302d303030303030303030303032 +85ad736368656d6156657273696f6eb767616d6563756c742e666f726b2e7265636f72642e7630a7666f726b5f6964d930657572656b612d7375627374726174653a7175657374696f6e3a6465736b2d6c697374696e672d617574686f72697479ac6f7074696f6e735f68617368d94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438b1636f756e7465645f616e737765725f6964a26e31a7616e737765727392de0010ad736368656d6156657273696f6eb767616d6563756c742e6465736b5f616e737765722e7630a9616e737765725f6964a26e31a7666f726b5f6964d930657572656b612d7375627374726174653a7175657374696f6e3a6465736b2d6c697374696e672d617574686f72697479a76f7574636f6d65a7436f756e746564a26f66c0a87265747261637473c0a4636f6465c0ac6f7074696f6e735f68617368d94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438a663686f696365ab61736b696e672d6d696e64a46e6f7465be5468652061736b6572206c6973747320686572206f776e20666f726b732eab616e7377657265645f6174b4323032362d31312d31315431373a30303a30305aab72656365697665645f6174b4323032362d31312d31315431373a30303a30305aaf636c61696d5f696e5f667574757265c2a8616e737765726572d92435663063326231652d386134642d346333622d396537662d316132623363346435653666a9617574686f72697479a64d656d626572aa726571756573745f6964d92430303030303030302d303030302d343030302d383030302d303030303030303030303031de0010ad736368656d6156657273696f6eb767616d6563756c742e6465736b5f616e737765722e7630a9616e737765725f6964a26e32a7666f726b5f6964d930657572656b612d7375627374726174653a7175657374696f6e3a6465736b2d6c697374696e672d617574686f72697479a76f7574636f6d65a85265766973696f6ea26f66a26e31a87265747261637473c0a4636f6465c0ac6f7074696f6e735f68617368d94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438a663686f696365ab61736b696e672d6d696e64a46e6f7465a74167726565642eab616e7377657265645f6174b4323032362d31312d31315431373a30353a30305aab72656365697665645f6174b4323032362d31312d31315431373a30353a30305aaf636c61696d5f696e5f667574757265c2a8616e737765726572d92430623964376533612d366332312d346635382d613064342d396538663761366235633464a9617574686f72697479a64d656d626572aa726571756573745f6964d92430303030303030302d303030302d343030302d383030302d303030303030303030303032 diff --git a/crates/huginn-mind/tests/fixtures/desk-feed/record-whole.payload.msgpack b/crates/huginn-mind/tests/fixtures/desk-feed/record-whole.payload.msgpack index 2405433..1c8b4ed 100644 Binary files a/crates/huginn-mind/tests/fixtures/desk-feed/record-whole.payload.msgpack and b/crates/huginn-mind/tests/fixtures/desk-feed/record-whole.payload.msgpack differ diff --git a/docs/desk-feed-door.md b/docs/desk-feed-door.md index 8f233ec..e440feb 100644 --- a/docs/desk-feed-door.md +++ b/docs/desk-feed-door.md @@ -94,6 +94,45 @@ Request (`huginn.desk_request.v1`): Huginn keeps no copy. `subject` is the verified Bifrost account id of the reader. The reads carry no `authority`, and a payload that names it is refused whole. +**Account ids.** `subject`, every `deciders` entry and every stored `answerer` are Bifrost account ids: +`UserAccount.Id` in lowercase D-format, 8-4-4-4-12 lowercase hex (Bifrost renders it in +`DeskEndpoints.cs` with `accountId.Value.ToString("D")`, and `StartupConfigurationValidator.cs` +refuses any decider not in that form). That is the same grammar Huginn names for `RequestId` +(`uuid_text`). These ids are opaque random UUIDs. A Matrix id, an email, a display name or any other +handle is never one of them. Bifrost enforces the grammar; Huginn's door does not yet: it reads these +fields as `Short` (any text up to 200 bytes) and would accept `@x:server` or an email. A subject +is stored as `answerer` on an answer, so a wrong value would enter the mind. Refusing every other +shape at decode is owed by `cut-desk-feed-listing.r6`. The test +`every_account_id_in_the_fixtures_is_a_lowercase_d_format_uuid` pins the grammar. The fixtures use +synthetic UUIDs. + +## Account ids by reader (planned; needs a CultLib change) + +A reader is a **decider** when the call's `subject` is in the call's `deciders`, compared byte for +byte; both values are in the account-id format above. Any other reader is a non-decider. + +- **On `main` today**, every reader gets the ids. `card.deciders` carries the call's decider list on + every card, and every answer carries its `answerer` id. This is not live yet: no receiver is + deployed. +- **Ruled (Persona, 2026-11-11; follow-up `chat-mind-20261111T150417226Z-t167`):** the door, not + Bifrost, withholds account ids from a non-decider and returns a count instead. Bifrost cannot do + it, because the card is opaque to it. A decider sees the ids. The superseding ruling is drafted + as `desk-read-visibility-withheld-ids`. +- **CultLib cannot carry this yet.** The card has 16 required keys in closed map-mode (17 once `raised_at` lands), and + `deciders` must hold at least one item. `answerer` is a required, non-null id. The proposal to + Libby (follow-up `cultlib-fork-card-reader-counts`): + - the card gains a required `decider_count` (an integer). A non-decider gets `deciders: []`; a + decider gets the ids. The count is the same for both. + - `answerer` becomes nullable: nil for a non-decider, with the key kept, as `note` is stripped. + - the record gains a required `answerer_count`, the number of distinct answerers. +- **Proposed fixtures**, built from mirror types that exist only in the test: + - `card-viewers-decider.synthetic` and `card-viewers-non-decider.synthetic` hold only the two card + keys the proposal changes. They are not cards. + - `record-non-decider.proposed` is a whole record as the proposal shapes it. + + The test `a_non_decider_gets_counts_and_no_ids_a_decider_gets_ids` checks that neither account id + appears anywhere in a non-decider's bytes. `record-whole` is the decider's view. + Response (`huginn.desk_response.v1`): | variant | meaning | Bifrost answers | @@ -158,16 +197,16 @@ every pinned response carries no entry. The entry-bearing fixtures (an open list with `raised_at` as a string and as nil) are added by `cut-desk-feed-raised-at` against that CultLib commit. -## Trust boundary: what Huginn sends raw, what Bifrost does +## Trust boundary: what Huginn sends, what Bifrost does -- Huginn sends every record whole over the peer-checked socket: every answer's `answerer` id and - every note, unmasked (fixture `record-whole`). The peer-uid check means only Bifrost's uid reads - them. A second reader of the socket would leak answerer ids and notes, so the socket must belong - to Bifrost's uid alone. +- The peer-uid check is the whole authentication, so the socket must belong to Bifrost's uid alone. +- Today Huginn sends every record whole: every `answerer` id, every note and the card's + `deciders` (fixture `record-whole`). +- Once `cut-desk-feed-listing` and the CultLib change land, Huginn withholds the account ids from a + non-decider (above). Notes stay whole on the wire. - Bifrost owns three things: the per-request membership check, the `desk.read` token, and - masking on what the door returns. It masks answerer ids for a reader who is not a decider (X3, - Heimdall 2026-11-11) and strips notes (ruling `desk-read-visibility-listed`, `DeskNotes`). - Huginn masks nothing. + stripping notes for a reader who is not a decider (`DeskNotes`). It no longer masks answerer + ids. ## Listing: which forks a reader sees (Huginn's; NOT live, see the status above)