Desk feed door: the door withholds account ids from non-deciders (proposed shapes, CultLib change owed)

Persona's ruling (follow-up chat-mind-20261111T150417226Z-t167): a
reader is a decider when subject is in the call's deciders; any other
reader gets no decider or answerer ids, only counts, and Huginn does it
because the card is opaque to Bifrost. CultLib's closed card (deciders
minItems 1, no count) and non-null answerer cannot carry it yet, so the
doc names the proposal to Libby and the fixtures add clearly synthetic
card fragments and a proposed non-decider record, with a test that no
account id appears in a non-decider's bytes and a decider keeps them.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Eureka 2026-11-11 17:38:52 +00:00
commit d08b8880ff
9 changed files with 174 additions and 13 deletions

View file

@ -102,13 +102,36 @@ refuses any decider not in that form). That is the same grammar Huginn names for
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.r4`. The test
shape at decode is owed by `cut-desk-feed-listing.r5`. The test
`every_account_id_in_the_fixtures_is_a_lowercase_d_format_uuid` pins the grammar. The fixtures use
synthetic UUIDs.
`card.deciders` echoes the call's decider list on every card to every reader. Bifrost masks
`answerer` for a reader who is not a decider (X3), but it must also mask `card.deciders`, or the
same account ids reach that reader through the card.
## 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 17 required keys in closed map-mode, 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`):
@ -174,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)