/** * A room whose members' stances are sealed into its policy's custody, and * whose policy releases only what one function of its own module projects * from them, once per instance. * * This is `custody-projector.tsx` with the release rule a room over sealed * custody is meant to take, and showing the answer the seal publishes rather * than its reactive projection. That room stays as it was recorded, with the * weaker rule, because the rule is part of the policy its `policy` cell * declares: a room of it keeps its sealed values under that policy. * * A member reviews a stance in the trusted host's `cf-custody-seal` dialog, * which a room binds as ``, with the member's own draft and * source policy from the member's home space. The host seals the stance into * the instance's box: one document in this room's space, which every * member's seal writes an entry into and which names no member. The pattern * never holds a member's DID, the box's address, or its own policy's * reference, and needs none of them: * * - The terms name each seat by a cell whose stored label attests one * principal, as a member's profile does. The host resolves each to the DID * it seals. * - `policy` is declared `PolicyOf` the room's rules, so its label carries * the policy's reference with this room as its subject, and the host reads * the reference from there. * - When a member seals, the seal writes a link to the box into `box`, in * the transaction that writes the member's entry. * * Every entry of the box is labeled with the room's policy, and the box * itself with the policy or the room's readers, so what the pattern computes * from it leaves the policy only through `releaseChoice`. That rule releases * what `projectChoice` returns, one of the listed answers, to the seal alone * (`Builtin{cfc-custody-seal}`, a reader no member holds), when everything * confidential `projectChoice` read was written by the seal * (`TransformedBy{builtin cfc-custody-seal}` as its input witness): the box * the seal linked into `box`, and not a document other code put there, which * could repeat another member's entry or mix it with entries it made up. A * member's rating, and anything else computed from the box, stays sealed. * The stances hold a bounded array of closed ratings, one per option, which * is the shape the seal admits for a multiple choice. * * `choice`, the projection, is read by no member. It is reactive, and a * member's code can point the projector at input of its own; an answer that * does not change keeps its earlier stamp, so were `choice` readable, it * would say whether that input yields the released answer. The room shows the * host's `cf-custody-answer` instead, which asks the seal to publish `choice` * once every seat has sealed and the rule releases it to the seal. The seal * declassifies it once into the instance's answer slot, which the room's * readers can read, and the component shows what the slot holds, verified to * be the seal's write. The slot is create-only and the seal's alone to * write, so the answer published for an instance never changes, whatever * later points the projector at other input and whoever writes what. * * Which instance the room shows is not held against a member's own code. The * component shows the slot of the instance its bound `terms` digest to, under * the policy its bound `policy` names, and those bindings sit in the room's * result document and UI, which the room space's members can write and no * writer claim covers. A member's code can point them at another instance's * terms, such as those of a one-seat room the member sealed and published * alone, whose answer the room then shows as its own, or at terms whose slot * is empty. * * `terms` and `policy` are write-once here as defense in depth: each carries a * writer claim naming `propose`, and `propose` writes each only while it reads * as unwritten. That refuses a write to the room's argument document by any * other code, through any schema, and nothing more. Write authority is keyed * by code, not by piece (normative CFC ยง8.15.8), so a member's own instance of * this pattern bound beneath these cells runs an authorized `propose`, and a * source update's successor inherits this one's authority. * `docs/specs/cfc-custody-seal.md` says what closing this would take, and * what else the room does not cover. * * Whoever runs `propose` first writes the terms, and anyone who can write in * the room space can run it, with seats of their choosing, before the room's * own proposal does. The room does not guard against that: it records no * creator to check the sender against, and a guard requiring the sender to * hold a seat would admit any sender that seats itself. What that costs is * the room, not a member's consent. The seal's confirmation shows every seat, * and a member seals only under terms that seat them, so terms the members * did not agree to leave the room with no answer, and the remedy is another * room. */ import { type Confidential, Default, handler, lift, NAME, pattern, Stream, UI, type VNode, Writable, type WriteAuthorizedBy, } from "commonfabric"; import { exchangeRule, exchangeRules, type PolicyOf, THIS_POLICY, } from "commonfabric/cfc"; export const releaseChoice = exchangeRule({ appliesTo: THIS_POLICY, pre: { integrity: [{ type: "https://commonfabric.org/cfc/atom/TransformedBy", identity: { kind: "verified", moduleIdentity: THIS_POLICY.moduleIdentity, symbol: "projectChoice", }, inputWitness: { type: "https://commonfabric.org/cfc/atom/TransformedBy", identity: { kind: "builtin", builtinId: "cfc-custody-seal" }, }, }], }, // Released to the seal alone, which publishes it once per instance; no // member reads the projection itself. post: { addAlternatives: [{ type: "https://commonfabric.org/cfc/atom/Builtin", name: "cfc-custody-seal", }], }, }); export const custodyRules = exchangeRules([releaseChoice]); /** Labeled with the room's policy: only `projectChoice`'s output leaves. */ export type Sealed = Confidential< T, readonly [PolicyOf] >; /** The options a stance rates, in the order its ratings list them. */ export const OPTIONS = ["pizza", "sushi", "tacos"] as const; /** The answer when no option suits every seat, or the room is incomplete. */ export const NO_AGREEMENT = "no agreement"; export type Rating = "no" | "maybe" | "yes"; /** One member's sealed stance: a rating per option. */ export interface Stance { ratings: Rating[]; } /** One entry of the box, as the seal writes it. */ export interface BoxEntry { instance: string; terms: string; stance: Stance; } /** The instance's box: entries keyed by a blinded key that names no member. */ export type Box = Record; /** * The stance schema the terms carry: a closed rating per option, which the * seal admits as an array whose one element schema is closed and whose * length is bounded. */ export const STANCE_SCHEMA = { type: "object", properties: { ratings: { type: "array", items: { enum: ["no", "maybe", "yes"] }, minItems: OPTIONS.length, maxItems: OPTIONS.length, }, }, required: ["ratings"], additionalProperties: false, }; /** * The released computation: the option no seat rated `no` that the most * seats rated `yes`, the earliest listed winning a tie. It answers * {@link NO_AGREEMENT} unless every entry was sealed under the same terms and * there is one entry per seat, and it never throws. */ export const projectChoice = lift((box: Box | undefined): string => { const entries = Object.values(box ?? {}); const terms = entries[0]?.terms; if ( terms === undefined || entries.some((entry) => entry?.terms !== terms) ) return NO_AGREEMENT; let seats: unknown; try { seats = (JSON.parse(terms) as { seats?: unknown }).seats; } catch { return NO_AGREEMENT; } if (!Array.isArray(seats) || seats.length !== entries.length) { return NO_AGREEMENT; } let best: string = NO_AGREEMENT; let bestYes = -1; OPTIONS.forEach((option, index) => { const ratings = entries.map((entry) => entry?.stance?.ratings?.[index]); if (ratings.some((rating) => rating !== "yes" && rating !== "maybe")) { return; } const yes = ratings.filter((rating) => rating === "yes").length; if (yes > bestYes) { best = option; bestYes = yes; } }); return best; }); /** Defined beside `projectChoice` but not named by the rule, so not released. */ export const firstRating = lift((box: Box | undefined): string => Object.values(box ?? {})[0]?.stance?.ratings?.[0] ?? "" ); /** The room's terms, as `propose` writes them. */ export interface CustodyTerms { question: string; answers: string[]; /** A cell per seat, whose stored label attests the seat's member. */ seats: unknown[]; stanceSchema: typeof STANCE_SCHEMA; } /** * The room's terms, which only the code of `propose` may write, and which it * writes only while it reads them as unwritten. The host shows the slot of the * instance the terms digest to, so terms rewritten after the answer is * published would have the room show another instance's slot, or an empty one. */ export type ProposedTerms = WriteAuthorizedBy; /** * The room's policy cell, which only the code of `propose` may write, and * which it writes only while it reads it as unwritten, for the same reason: the * host reads the slot under the policy this cell names. */ export type DeclaredPolicy = WriteAuthorizedBy, typeof propose>; interface CustodyAnswerRoomInput { /** * Absent until `propose` writes it. It has no default: on * `CustodyTerms | null`, the writer claim would sit on the object branch * alone and not refuse a write of `null`, which would let other code clear * the terms for `propose` to write again. */ terms: Writable; /** * Declared with the room's policy, so that once written its label carries * the policy's reference, with this room as its subject, for the host to * read. It has no default: `propose` writes it. */ policy: Writable; /** * Receives a link to the instance's box, which the seal writes in the * transaction that writes a member's entry. It declares no label of its * own: every entry of the box already carries the room's policy, and a * read through the link carries it. * * TODO(custody-box-link): declaring `Record>` here * is refused. Repro: give `box` that type and seal into the room, as * `integration/cfc-custody-projector.test.ts` does; the seal's commit * preparation crashes with `type changed incompatibly at /box: ["object"] * -> ["array"]`, as the schema merge sets the input's record schema * against an array schema. */ box: Writable>>; } export interface CustodyAnswerRoomOutput { [NAME]: string; [UI]: VNode; terms?: CustodyTerms; policy: Sealed; box: Box; /** * The projected answer, which moves with what the projector reads. It is * released to the seal alone, which publishes it; no member reads it, and * the room shows the answer the seal published instead. */ choice: string; /** One member's sealed rating, which no rule releases. */ rating: string; propose: Stream<{ seats: unknown[] }>; } /** * Writes the room's terms, naming each seat by the cell the event carries, * and declares the room's policy on `policy`, each only while it is * unwritten. Each guard reads the cell it writes and nothing else, so the * room's own stream, run again, finds what the first proposal wrote and * writes nothing. An instance of this pattern bound elsewhere reads its own * binding instead; the doc comment at the top says what the claims leave * open. */ const propose = handler< { seats: unknown[] }, { terms: Writable; policy: Writable> } >(({ seats }, { terms, policy }) => { if (terms.get() === undefined) { terms.set({ question: "Where should we eat?", answers: [...OPTIONS, NO_AGREEMENT], seats, stanceSchema: STANCE_SCHEMA, }); } if (policy.get() === undefined) policy.set(true as Sealed); }); const CustodyAnswerRoom = pattern< CustodyAnswerRoomInput, CustodyAnswerRoomOutput >(({ terms, policy, box }) => { const choice = projectChoice(box); const rating = firstRating(box); return { [NAME]: "Sealed custody answer room", [UI]: ( The room's choice ), terms, policy, box, choice, rating, propose: propose({ terms, policy }), }; }); export default CustodyAnswerRoom;