Desk feed door: account ids are lowercase D-format UUIDs (doc, fixtures, grammar test) #56
27 changed files with 255 additions and 26 deletions
|
|
@ -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<ListStatus>, cursor: Option<u64>, 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<ListStatus>, cursor: Option<u64>, 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<String>,
|
||||
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<String>,
|
||||
retracts: Option<String>,
|
||||
code: Option<huginn_mind::desk_wire::ForkAnswerCode>,
|
||||
options_hash: String,
|
||||
choice: Option<String>,
|
||||
note: Option<String>,
|
||||
answered_at: String,
|
||||
received_at: String,
|
||||
claim_in_future: bool,
|
||||
answerer: Option<String>,
|
||||
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<String>,
|
||||
answerer_count: u64,
|
||||
answers: Vec<WithheldAnswer>,
|
||||
}
|
||||
|
||||
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<u8>)> {
|
||||
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<u8> {
|
||||
use base64::Engine;
|
||||
|
|
@ -170,6 +277,10 @@ fn fixtures() -> Vec<(String, Vec<u8>)> {
|
|||
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");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
1
crates/huginn-mind/tests/fixtures/desk-feed/card-viewers-decider.synthetic.payload.hex
vendored
Normal file
1
crates/huginn-mind/tests/fixtures/desk-feed/card-viewers-decider.synthetic.payload.hex
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
82a8646563696465727392d92435663063326231652d386134642d346333622d396537662d316132623363346435653666d92430623964376533612d366332312d346635382d613064342d396538663761366235633464ad646563696465725f636f756e7402
|
||||
1
crates/huginn-mind/tests/fixtures/desk-feed/card-viewers-decider.synthetic.payload.msgpack
vendored
Normal file
1
crates/huginn-mind/tests/fixtures/desk-feed/card-viewers-decider.synthetic.payload.msgpack
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
‚¨deciders’Ù$5f0c2b1e-8a4d-4c3b-9e7f-1a2b3c4d5e6fÙ$0b9d7e3a-6c21-4f58-a0d4-9e8f7a6b5c4ddecider_count
|
||||
1
crates/huginn-mind/tests/fixtures/desk-feed/card-viewers-non-decider.synthetic.payload.hex
vendored
Normal file
1
crates/huginn-mind/tests/fixtures/desk-feed/card-viewers-non-decider.synthetic.payload.hex
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
82a8646563696465727390ad646563696465725f636f756e7402
|
||||
|
|
@ -0,0 +1 @@
|
|||
‚¨deciders<EFBFBD>decider_count
|
||||
Binary file not shown.
|
|
@ -1 +1 @@
|
|||
0000011287a96d6573736167654964a36d2d33a96f7065726174696f6ea5666f726b73a77061796c6f6164d9606761564762334a726334616b62576c755a4b5a6c64584a6c6132476e63335669616d566a644b45337147526c59326c6b5a584a7a6b7145336f6a4579706e4e30595852316336527663475675706d4e31636e4e766373436c62476c7461585141af7061796c6f6164456e636f64696e67b26d6573736167657061636b2d626173653634ad7061796c6f6164536368656d61b6687567696e6e2e6465736b5f726571756573742e7631ad736368656d6156657273696f6ebc63756c746e65742e6f7065726174696f6e5f726571756573742e7630a9736572766963654964ab687567696e6e2e6465736b
|
||||
000001a287a96d6573736167654964a36d2d33a96f7065726174696f6ea5666f726b73a77061796c6f6164d9f06761564762334a726334616b62576c755a4b5a6c64584a6c6132476e63335669616d566a644e6b6b4e575977597a4a694d5755744f4745305a433030597a4e694c546c6c4e3259744d574579596a4e6a4e4751315a545a6d7147526c59326c6b5a584a7a6b746b6b4e575977597a4a694d5755744f4745305a433030597a4e694c546c6c4e3259744d574579596a4e6a4e4751315a545a6d32535177596a6c6b4e32557a59533032597a49784c54526d4e5467745954426b4e4330355a54686d4e324532596a566a4e47536d633352686448567a704739775a57366d5933567963323979774b5673615731706441413daf7061796c6f6164456e636f64696e67b26d6573736167657061636b2d626173653634ad7061796c6f6164536368656d61b6687567696e6e2e6465736b5f726571756573742e7631ad736368656d6156657273696f6ebc63756c746e65742e6f7065726174696f6e5f726571756573742e7630a9736572766963654964ab687567696e6e2e6465736b
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
81a5466f726b7386a46d696e64a6657572656b61a77375626a656374a137a8646563696465727392a137a23132a6737461747573a46f70656ea6637572736f72c0a56c696d697400
|
||||
81a5466f726b7386a46d696e64a6657572656b61a77375626a656374d92435663063326231652d386134642d346333622d396537662d316132623363346435653666a8646563696465727392d92435663063326231652d386134642d346333622d396537662d316132623363346435653666d92430623964376533612d366332312d346635382d613064342d396538663761366235633464a6737461747573a46f70656ea6637572736f72c0a56c696d697400
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
|
|
@ -1 +1 @@
|
|||
0000011687a96d6573736167654964a36d2d32a96f7065726174696f6ea5666f726b73a77061796c6f6164d9646761564762334a726334616b62576c755a4b5a6c64584a6c6132476e63335669616d566a644b45337147526c59326c6b5a584a7a6b7145336f6a4579706e4e30595852316336527663475675706d4e31636e4e7663714e6a4e444b6c62476c7461585179af7061796c6f6164456e636f64696e67b26d6573736167657061636b2d626173653634ad7061796c6f6164536368656d61b6687567696e6e2e6465736b5f726571756573742e7631ad736368656d6156657273696f6ebc63756c746e65742e6f7065726174696f6e5f726571756573742e7630a9736572766963654964ab687567696e6e2e6465736b
|
||||
000001a687a96d6573736167654964a36d2d32a96f7065726174696f6ea5666f726b73a77061796c6f6164d9f46761564762334a726334616b62576c755a4b5a6c64584a6c6132476e63335669616d566a644e6b6b4e575977597a4a694d5755744f4745305a433030597a4e694c546c6c4e3259744d574579596a4e6a4e4751315a545a6d7147526c59326c6b5a584a7a6b746b6b4e575977597a4a694d5755744f4745305a433030597a4e694c546c6c4e3259744d574579596a4e6a4e4751315a545a6d32535177596a6c6b4e32557a59533032597a49784c54526d4e5467745954426b4e4330355a54686d4e324532596a566a4e47536d633352686448567a704739775a57366d59335679633239796f324d304d715673615731706444493daf7061796c6f6164456e636f64696e67b26d6573736167657061636b2d626173653634ad7061796c6f6164536368656d61b6687567696e6e2e6465736b5f726571756573742e7631ad736368656d6156657273696f6ebc63756c746e65742e6f7065726174696f6e5f726571756573742e7630a9736572766963654964ab687567696e6e2e6465736b
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
81a5466f726b7386a46d696e64a6657572656b61a77375626a656374a137a8646563696465727392a137a23132a6737461747573a46f70656ea6637572736f72a3633432a56c696d697432
|
||||
81a5466f726b7386a46d696e64a6657572656b61a77375626a656374d92435663063326231652d386134642d346333622d396537662d316132623363346435653666a8646563696465727392d92435663063326231652d386134642d346333622d396537662d316132623363346435653666d92430623964376533612d366332312d346635382d613064342d396538663761366235633464a6737461747573a46f70656ea6637572736f72a3633432a56c696d697432
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
<EFBFBD>¥Forks†¤mind¦eureka§subject¡7¨deciders’¡7¢12¦status¤open¦cursor£c42¥limit2
|
||||
<EFBFBD>¥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
|
||||
Binary file not shown.
|
|
@ -1 +1 @@
|
|||
0000011287a96d6573736167654964a36d2d31a96f7065726174696f6ea5666f726b73a77061796c6f6164d9606761564762334a726334616b62576c755a4b5a6c64584a6c6132476e63335669616d566a644b45337147526c59326c6b5a584a7a6b7145336f6a4579706e4e30595852316336527663475675706d4e31636e4e766373436c62476c7461585179af7061796c6f6164456e636f64696e67b26d6573736167657061636b2d626173653634ad7061796c6f6164536368656d61b6687567696e6e2e6465736b5f726571756573742e7631ad736368656d6156657273696f6ebc63756c746e65742e6f7065726174696f6e5f726571756573742e7630a9736572766963654964ab687567696e6e2e6465736b
|
||||
000001a287a96d6573736167654964a36d2d31a96f7065726174696f6ea5666f726b73a77061796c6f6164d9f06761564762334a726334616b62576c755a4b5a6c64584a6c6132476e63335669616d566a644e6b6b4e575977597a4a694d5755744f4745305a433030597a4e694c546c6c4e3259744d574579596a4e6a4e4751315a545a6d7147526c59326c6b5a584a7a6b746b6b4e575977597a4a694d5755744f4745305a433030597a4e694c546c6c4e3259744d574579596a4e6a4e4751315a545a6d32535177596a6c6b4e32557a59533032597a49784c54526d4e5467745954426b4e4330355a54686d4e324532596a566a4e47536d633352686448567a704739775a57366d5933567963323979774b5673615731706444493daf7061796c6f6164456e636f64696e67b26d6573736167657061636b2d626173653634ad7061796c6f6164536368656d61b6687567696e6e2e6465736b5f726571756573742e7631ad736368656d6156657273696f6ebc63756c746e65742e6f7065726174696f6e5f726571756573742e7630a9736572766963654964ab687567696e6e2e6465736b
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
81a5466f726b7386a46d696e64a6657572656b61a77375626a656374a137a8646563696465727392a137a23132a6737461747573a46f70656ea6637572736f72c0a56c696d697432
|
||||
81a5466f726b7386a46d696e64a6657572656b61a77375626a656374d92435663063326231652d386134642d346333622d396537662d316132623363346435653666a8646563696465727392d92435663063326231652d386134642d346333622d396537662d316132623363346435653666d92430623964376533612d366332312d346635382d613064342d396538663761366235633464a6737461747573a46f70656ea6637572736f72c0a56c696d697432
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
<EFBFBD>¥Forks†¤mind¦eureka§subject¡7¨deciders’¡7¢12¦status¤open¦cursorÀ¥limit2
|
||||
<EFBFBD>¥Forks†¤mind¦eureka§subjectÙ$5f0c2b1e-8a4d-4c3b-9e7f-1a2b3c4d5e6f¨deciders’Ù$5f0c2b1e-8a4d-4c3b-9e7f-1a2b3c4d5e6fÙ$0b9d7e3a-6c21-4f58-a0d4-9e8f7a6b5c4d¦status¤open¦cursorÀ¥limit2
|
||||
Binary file not shown.
|
|
@ -1 +1 @@
|
|||
0000013987a96d6573736167654964a36d2d34a96f7065726174696f6ea4666f726ba77061796c6f6164d9886761524762334a72684b52746157356b706d5631636d56725961647a64574a715a574e306f54656f5a47566a6157526c636e4f536f5465694d544b6e5a6d3979613139705a4e6b775a5856795a5774684c584e31596e4e30636d46305a5470786457567a64476c76626a706b5a584e724c57787063335270626d6374595856306147397961585235af7061796c6f6164456e636f64696e67b26d6573736167657061636b2d626173653634ad7061796c6f6164536368656d61b6687567696e6e2e6465736b5f726571756573742e7631ad736368656d6156657273696f6ebc63756c746e65742e6f7065726174696f6e5f726571756573742e7630a9736572766963654964ab687567696e6e2e6465736b
|
||||
000001ca87a96d6573736167654964a36d2d34a96f7065726174696f6ea4666f726ba77061796c6f6164da01186761524762334a72684b52746157356b706d5631636d56725961647a64574a715a574e30325351315a6a426a4d6d49785a5330345954526b4c54526a4d3249744f5755335a69307859544a694d324d305a44566c4e6d616f5a47566a6157526c636e4f53325351315a6a426a4d6d49785a5330345954526b4c54526a4d3249744f5755335a69307859544a694d324d305a44566c4e6d625a4a4442694f5751335a544e684c545a6a4d6a45744e4759314f4331684d4751304c546c6c4f47593359545a694e574d305a4b646d62334a7258326c6b3254426c64584a6c6132457463335669633352795958526c4f6e46315a584e30615739754f6d526c6332737462476c7a64476c755a7931686458526f62334a7064486b3daf7061796c6f6164456e636f64696e67b26d6573736167657061636b2d626173653634ad7061796c6f6164536368656d61b6687567696e6e2e6465736b5f726571756573742e7631ad736368656d6156657273696f6ebc63756c746e65742e6f7065726174696f6e5f726571756573742e7630a9736572766963654964ab687567696e6e2e6465736b
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
81a4466f726b84a46d696e64a6657572656b61a77375626a656374a137a8646563696465727392a137a23132a7666f726b5f6964d930657572656b612d7375627374726174653a7175657374696f6e3a6465736b2d6c697374696e672d617574686f72697479
|
||||
81a4466f726b84a46d696e64a6657572656b61a77375626a656374d92435663063326231652d386134642d346333622d396537662d316132623363346435653666a8646563696465727392d92435663063326231652d386134642d346333622d396537662d316132623363346435653666d92430623964376533612d366332312d346635382d613064342d396538663761366235633464a7666f726b5f6964d930657572656b612d7375627374726174653a7175657374696f6e3a6465736b2d6c697374696e672d617574686f72697479
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
<EFBFBD>¤Fork„¤mind¦eureka§subject¡7¨deciders’¡7¢12§fork_idÙ0eureka-substrate:question:desk-listing-authority
|
||||
<EFBFBD>¤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
|
||||
1
crates/huginn-mind/tests/fixtures/desk-feed/record-non-decider.proposed.payload.hex
vendored
Normal file
1
crates/huginn-mind/tests/fixtures/desk-feed/record-non-decider.proposed.payload.hex
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
86ad736368656d6156657273696f6eb767616d6563756c742e666f726b2e7265636f72642e7630a7666f726b5f6964d930657572656b612d7375627374726174653a7175657374696f6e3a6465736b2d6c697374696e672d617574686f72697479ac6f7074696f6e735f68617368d94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438b1636f756e7465645f616e737765725f6964a26e31ae616e7377657265725f636f756e7402a7616e737765727392de0010ad736368656d6156657273696f6eb767616d6563756c742e6465736b5f616e737765722e7630a9616e737765725f6964a26e31a7666f726b5f6964d930657572656b612d7375627374726174653a7175657374696f6e3a6465736b2d6c697374696e672d617574686f72697479a76f7574636f6d65a7436f756e746564a26f66c0a87265747261637473c0a4636f6465c0ac6f7074696f6e735f68617368d94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438a663686f696365ab61736b696e672d6d696e64a46e6f7465be5468652061736b6572206c6973747320686572206f776e20666f726b732eab616e7377657265645f6174b4323032362d31312d31315431373a30303a30305aab72656365697665645f6174b4323032362d31312d31315431373a30303a30305aaf636c61696d5f696e5f667574757265c2a8616e737765726572c0a9617574686f72697479a64d656d626572aa726571756573745f6964d92430303030303030302d303030302d343030302d383030302d303030303030303030303031de0010ad736368656d6156657273696f6eb767616d6563756c742e6465736b5f616e737765722e7630a9616e737765725f6964a26e32a7666f726b5f6964d930657572656b612d7375627374726174653a7175657374696f6e3a6465736b2d6c697374696e672d617574686f72697479a76f7574636f6d65a85265766973696f6ea26f66a26e31a87265747261637473c0a4636f6465c0ac6f7074696f6e735f68617368d94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438a663686f696365ab61736b696e672d6d696e64a46e6f7465a74167726565642eab616e7377657265645f6174b4323032362d31312d31315431373a30353a30305aab72656365697665645f6174b4323032362d31312d31315431373a30353a30305aaf636c61696d5f696e5f667574757265c2a8616e737765726572c0a9617574686f72697479a64d656d626572aa726571756573745f6964d92430303030303030302d303030302d343030302d383030302d303030303030303030303032
|
||||
BIN
crates/huginn-mind/tests/fixtures/desk-feed/record-non-decider.proposed.payload.msgpack
vendored
Normal file
BIN
crates/huginn-mind/tests/fixtures/desk-feed/record-non-decider.proposed.payload.msgpack
vendored
Normal file
Binary file not shown.
|
|
@ -1 +1 @@
|
|||
85ad736368656d6156657273696f6eb767616d6563756c742e666f726b2e7265636f72642e7630a7666f726b5f6964d930657572656b612d7375627374726174653a7175657374696f6e3a6465736b2d6c697374696e672d617574686f72697479ac6f7074696f6e735f68617368d94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438b1636f756e7465645f616e737765725f6964a26e31a7616e737765727392de0010ad736368656d6156657273696f6eb767616d6563756c742e6465736b5f616e737765722e7630a9616e737765725f6964a26e31a7666f726b5f6964d930657572656b612d7375627374726174653a7175657374696f6e3a6465736b2d6c697374696e672d617574686f72697479a76f7574636f6d65a7436f756e746564a26f66c0a87265747261637473c0a4636f6465c0ac6f7074696f6e735f68617368d94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438a663686f696365ab61736b696e672d6d696e64a46e6f7465be5468652061736b6572206c6973747320686572206f776e20666f726b732eab616e7377657265645f6174b4323032362d31312d31315431373a30303a30305aab72656365697665645f6174b4323032362d31312d31315431373a30303a30305aaf636c61696d5f696e5f667574757265c2a8616e737765726572a137a9617574686f72697479a64d656d626572aa726571756573745f6964d92430303030303030302d303030302d343030302d383030302d303030303030303030303031de0010ad736368656d6156657273696f6eb767616d6563756c742e6465736b5f616e737765722e7630a9616e737765725f6964a26e32a7666f726b5f6964d930657572656b612d7375627374726174653a7175657374696f6e3a6465736b2d6c697374696e672d617574686f72697479a76f7574636f6d65a85265766973696f6ea26f66a26e31a87265747261637473c0a4636f6465c0ac6f7074696f6e735f68617368d94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438a663686f696365ab61736b696e672d6d696e64a46e6f7465a74167726565642eab616e7377657265645f6174b4323032362d31312d31315431373a30353a30305aab72656365697665645f6174b4323032362d31312d31315431373a30353a30305aaf636c61696d5f696e5f667574757265c2a8616e737765726572a23132a9617574686f72697479a64d656d626572aa726571756573745f6964d92430303030303030302d303030302d343030302d383030302d303030303030303030303032
|
||||
85ad736368656d6156657273696f6eb767616d6563756c742e666f726b2e7265636f72642e7630a7666f726b5f6964d930657572656b612d7375627374726174653a7175657374696f6e3a6465736b2d6c697374696e672d617574686f72697479ac6f7074696f6e735f68617368d94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438b1636f756e7465645f616e737765725f6964a26e31a7616e737765727392de0010ad736368656d6156657273696f6eb767616d6563756c742e6465736b5f616e737765722e7630a9616e737765725f6964a26e31a7666f726b5f6964d930657572656b612d7375627374726174653a7175657374696f6e3a6465736b2d6c697374696e672d617574686f72697479a76f7574636f6d65a7436f756e746564a26f66c0a87265747261637473c0a4636f6465c0ac6f7074696f6e735f68617368d94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438a663686f696365ab61736b696e672d6d696e64a46e6f7465be5468652061736b6572206c6973747320686572206f776e20666f726b732eab616e7377657265645f6174b4323032362d31312d31315431373a30303a30305aab72656365697665645f6174b4323032362d31312d31315431373a30303a30305aaf636c61696d5f696e5f667574757265c2a8616e737765726572d92435663063326231652d386134642d346333622d396537662d316132623363346435653666a9617574686f72697479a64d656d626572aa726571756573745f6964d92430303030303030302d303030302d343030302d383030302d303030303030303030303031de0010ad736368656d6156657273696f6eb767616d6563756c742e6465736b5f616e737765722e7630a9616e737765725f6964a26e32a7666f726b5f6964d930657572656b612d7375627374726174653a7175657374696f6e3a6465736b2d6c697374696e672d617574686f72697479a76f7574636f6d65a85265766973696f6ea26f66a26e31a87265747261637473c0a4636f6465c0ac6f7074696f6e735f68617368d94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438a663686f696365ab61736b696e672d6d696e64a46e6f7465a74167726565642eab616e7377657265645f6174b4323032362d31312d31315431373a30353a30305aab72656365697665645f6174b4323032362d31312d31315431373a30353a30305aaf636c61696d5f696e5f667574757265c2a8616e737765726572d92430623964376533612d366332312d346635382d613064342d396538663761366235633464a9617574686f72697479a64d656d626572aa726571756573745f6964d92430303030303030302d303030302d343030302d383030302d303030303030303030303032
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue