fix(server): root a room only on a write it retains (C99) - #403
Open
vieiralucas wants to merge 6 commits into
Open
fix(server): root a room only on a write it retains (C99)#403vieiralucas wants to merge 6 commits into
vieiralucas wants to merge 6 commits into
Conversation
`ensure_creator` fired on any `Ok` from `Hub::ingest`, and an empty batch answers `Ok(vec![])` while the ingest materialises the room on the way through — so the first authenticated actor to send a no-op `Ops` frame at an unestablished room became its doc-ACL authority root, owning `/` without authoring a byte the room retains. The rule is stated once, in `may_stand_as_root`, for every seam that installs a root: the client write, a peer's `Replicate`/`ReplicateMeta`, a snapshot install, and the record read back off the store. It is a rule about the room rather than the batch — no actor roots a room at sequence zero — because `ReplicateMeta` carries no batch to judge by construction. A deduped write still roots a room that already holds ops, deliberately.
…s (C99) A first cut applied `may_stand_as_root` at every seam that installs a root. Falsification measured two of them as worse than the defect: a state full of content whose frame names floor zero came up rootless, leaving every doc-ACL tuple in it with no authority to be decided under, and a room a state transfer left at sequence zero lost a legitimately established root on its next reload — after which the next authenticated writer takes `/`. Both are C29's inert-deny hole from the other side. The rule now sits only at `Hub::ensure_creator`, which a client write, a peer's `Replicate` and the metadata-only `ReplicateMeta` all compose through — the seam whose sequence is this node's own count of what it took, rather than a number that arrived beside the root. Both measured states are pinned as tests so it cannot creep back. The consequence test proved the wrong thing: a deny beats a schema-tier grant from any grantor, so its ACL half held whoever owned `/`. What the root actually carries is the creator's exemption, so the deny now names the squatter and the squatter reads.
The room-level rule closed only half the defect. Being a rule about the room, it says nothing once the room has reached a sequence — and a room holding content with no root is a real state, left by an anonymous establishing commit or by a replica whose best-effort metadata write was lost, which are C55's own two routes. There a frame carrying nothing satisfied the rule and took `/` over content its sender had no part in, then read straight through the room author's deny by the creator's exemption, permanently: set-once locks the content's authors out. The condition that refuses it is a statement about what the batch presented, so it sits at the client write seam. The replication callers state no such condition and must not: they adopt a root established elsewhere, and `ReplicateMeta` is by construction the frame with no batch beneath it. A resend the room dedups whole still roots a room that holds ops — presenting the room's content is something only a replica holding it can do, where an empty frame presents nothing. Also corrects the reload test, whose fixture installed an empty state and so pinned a root over nothing rather than the inert-deny harm it cited.
vieiralucas
force-pushed
the
server/c99-empty-batch-creator
branch
from
August 9, 2026 19:10
d2a8b4d to
3059301
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
ensure_creatorfired on anyOkfromHub::ingest, and an empty batch answersOk(vec![])— while the ingest'sentry(room).or_insert_with(Room::new)materialises the room on the way through. The first authenticated actor to send a no-opOpsframe at an unestablished room became its creator: the doc-ACL authority root owning/, over a room at sequence zero, having authored no byte the room retains. Reserve-by-no-op, the shape C23 (#389) closed one tier down for the replica-identity claim.Two conditions close it, at different seams, because they are different statements.
may_stand_as_root, inHub::ensure_creator, which a client write, a peer'sReplicateand the metadata-onlyReplicateMetaall compose through, so the replication path is judged by it rather than left with the hole. This forces the shape: "only a batch that landed" (C23's rule) is not statable atReplicateMeta, which has no batch beneath it by construction (C55, fix(server): carry a room's authority root where no op batch is going that way (C55) #397)./over content its sender had no part in, then read through the room author's deny by the creator's exemption — permanently, since set-once locks the content's authors out.The line is what the batch presented, not what landed. A resend the room dedups whole still roots a room that holds ops: presenting the room's content is something only a replica holding it can do, where an empty frame presents nothing. Refusing the resend too would buy attributability, at the cost of C55's route 1, against an attacker who pays one op to route around it.
Where the room rule stops was measured, not chosen. A first cut applied it at the snapshot install and the store load too; falsification measured both as worse than the defect — content installed at floor zero came up rootless (every tuple inert), and a room a state transfer left at sequence zero lost a legitimately-established root on reload, over content it still holds. Both are C29's inert-deny hole from the other side. Both are now pinned as tests.
crates/server/tests/creator_establishment.rs(14). ARCHITECTURE §Owner; DECISIONS 2026-08-09. Residue: C140, C141.