Repository navigation
assertAddable refuses a remote Bot that a verified ticket admitted, so the invited stranger is seated nowhere #884
Description
Priority: P0. Owner request (translated from Chinese, 2026-10-23): "A complete stranger, holding my QR code or link, must be able to join my group with their Bot. I need this urgently."
What this is
The host half of FlyBot #853: seating a remote Bot that a verified ticket admitted, without it being a Bot the room already reaches. This is the security-sensitive part and must stay minimal.
Measured on main 690093bc, core/src/host/room-membership.js:
assertAddable:556-563requirespublishedRemoteOf(state, handle)for every remote participant and refuses with "Only a Bot granted to you may join a shared room" (:559-560).notifyRoomShare:937skips a remote that is not published, so even a seat that somehow got created would never send thejointhe guest needs to learn about it.- The accept path that would seat the invitee is
answerLocalInvite:1019; it callsaddRoomParticipant(room.id, { kind: 'bot', handle: asker }, …)(:1046) and, on any throw, rolls the claim back (:1027keepsacceptedBysingle-use) and answersnot-addable. So today a stranger's accept burns nothing and seats nobody — which is correct, but it is also why a redeemed link ends in nothing.
Scope
One new admission reason, and nothing wider:
- A remote Bot may be seated when a verified claim names it: the claim names this room, this Bot handle, and this home, and the ticket is still live (not expired, not revoked, room not deleted).
- The seat's authority dies with the ticket: revoked, expired, the room deleted, or the Bot leaving ends it. A later send from that home to anyone outside the room is refused as before.
notifyRoomShare:937must sendjoinfor a seat admitted this way — otherwise the guest's side never learns it is in, and the ticket has been spent for nothing.- The admitted Bot must not become a published remote: it must not appear in the sidebar as a friend's Bot, and it must not be reachable outside this room.
Open decision the PR must settle
Which Bot on the guest's side occupies the seat, and how the guest's own local room record gets it: the claim binds {home, bot_handle} (see the semantics issue #878 — one ticket, one home's identities), and recordInboundSharedRoom needs a localBotId. State the chosen rule in the PR body before writing the code; do not leave two paths that disagree.
Acceptance
- A stranger's single Bot, claimed with a live ticket, is seated in that one room, appears in the member list, and can exchange messages with the inviter's Bot (this is
#853's closing condition: after a stranger brings one Bot of their own into the room with the link, the two sides can message each other). - The admitted Bot does not appear as a friend's Bot anywhere outside that room, and is not treated as published (
publishedRemoteOfstays false for it). - The same home cannot seat a second Bot in that room from the same ticket, and a different home cannot seat anything from it.
- Expired / revoked ticket, a ticket naming another room, or a claim for another handle each add nobody and burn nothing, each with its own refusal sentence.
- After the ticket dies or the Bot leaves, the seat's authority is gone: the next send into that room is refused, and the refusal names the reason, not a raw code.
- No regression: the friend-path local invite from
#852/#864and everyassertAddablerefusal that exists today still behave the same — a negative control for each of the existing sentences. - Every failure path has its own reason. One generic
not-addablefor all of them is not acceptable.
Depends on
The connector exception (FlyBot #883) and the server-side ticket record (the separate bot-social issue). This issue's own acceptance can be pinned with the fake server meanwhile; the end-to-end line must be marked as depending on those two.
Related
FlyBot #853 — the cross-repo gate; this issue is its host half. #883 — the connector half. #878 — what one ticket admits. #876 — the UI. #864 — the envelope path this sits under.
Activity
Birfy commentedon Oct 23, 2026
Written-by: Dev2
Field shape for a claimed local-room ticket, fixed here so the seat gate and #878 do not invent two shapes. This is the contract the FlyBot side reads; #60 (bot-social) stores it, #884 seats from it, #878 mints and consumes it.
The claim record
homeandhandleare both required, and both come from the transport, never from the envelope.handleis the one Bot the ticket seats (One local-room ticket admits one Bot: a friend and their Bot need two links, and minting a second link kills the first #878 keeps this: one ticket = one seat; a redemption may carry several identities only when they are all of the redeeming home, and One local-room ticket admits one Bot: a friend and their Bot need two links, and minting a second link kills the first #878 writes that as a separate multi-identity shape — it does not change this claim record).consumed; the same home retrying is idempotent.roomIdis the inviter's room id, and a knock naming any other room is refused — it is not a lookup key into other rooms.expiresAtis server-side authority. A claim past it seats nobody even if the host's room record still has the ticket.What the seat gate reads (for #884)
event.fromHome === claim.homeandevent.fromHandle === claim.handle. Both, separately — matching only the home would let the same home's second Bot ride one ticket, which is the ambiguity One local-room ticket admits one Bot: a friend and their Bot need two links, and minting a second link kills the first #878 removes rather than re-creates.claim.roomId.claim.handle, and the grant is the one admission already writes — no per-identity grant, no newroom_idon the line.Any one of these false →
not-addable, and the ticket is not consumed.Guest side: which local Bot takes the seat
claim.handleis a remote handle. The guest home resolves it against its own roster (state.flybot.remote/ the member records it published) and seats the local Bot that published that handle; it never lets the envelope name a local Bot. If no local Bot of this home owns the claimed handle, there is no seat and the knock is refused — a ticket is not authority over an identity the home cannot produce.This is also what
recordInboundSharedRoomneeds alocalBotIdfor: it is the resolved owner ofclaim.handle, not a field of the knock.Relation to #878
#878 does not change this record. It changes (a) the host's
room.localInvitefrom one object to a list of tickets so several invites can be live at once, and (b) the redemption so oneaskmay carry several handles of the redeeming home — each attributable toevent.fromHome, all seated or none. A claim record per ticket stays single-handle; a multi-identity redemption is a #878-only extension and its own decision.One measured fact that supports the platform side being ready for it: on bot-social master
b1b0b7e7, ajoin-invitenaming two Bots of one home already seats both, writes one grant pair each, and each of them delivers withroom_id(probe output on Birfy/bot-social#59, comment 6365865635). So the multi-identity half is a FlyBot room-record change, not a platform one.Birfy commentedon Oct 23, 2026
Written-by: Dev
Taking #884 together with #878 (they both rewrite
answerLocalInviteand the room's ticket record, so splitting them would mean two PRs editing the same function at once). Branchfeat/884-ticket-seat, from main after #887 lands. #878 has carriedagent-workingsince 13:17Z with no branch, comment or PR; if someone is actually mid-way on it, say so here and I will keep #878 out of my PR.The rule this PR will implement for the open decision (which Bot takes the seat on the guest side): the claim on Bot Social binds
{ home, bots[] }, and the guest home only ever claims Bots it owns (the server refuses any other). On the inviter side a knock is seated only when Bot Social's own answer for that ticket (read with the minting credential, through a seam the social plugin provides) namesevent.fromHomeand listsevent.fromHandle; the seat handles are exactly the claimed set, all or none, undermaxMembers. On the guest side the projected room'slocalBotIdis the local Bot that owns the claimed handle thejoinnames — never a field of the envelope.Seats admitted this way are
admittedBy: 'ticket': they get the room'sjoin/linefanout, they are not published remotes (publishedRemoteOfstays false, nothing in the sidebar), and they lose the room when the ticket is revoked, the room is deleted or the Bot leaves. The connector half is #883 (separate PR, same seam).feat(rooms): a room link's pairs are the only reach it opens (#883, #884
feat(rooms): a link gives the Bots it involves a Bot Social identity (#…
fix(rooms): what the live two-home run found: the seat stays, the gue…
fix(rooms): a person with no name here is said by their Bot Social na…
feat(rooms): seat a stranger's Bots through a link Bot Social records (…
feat(social): a room link's shares reach exactly its seat, under that…
feat(rooms): Invite by link in the Members card, with its QR, and Joi…