Skip to content

assertAddable refuses a remote Bot that a verified ticket admitted, so the invited stranger is seated nowhere #884

Closed

Description

@Birfy

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-563 requires publishedRemoteOf(state, handle) for every remote participant and refuses with "Only a Bot granted to you may join a shared room" (:559-560).
  • notifyRoomShare :937 skips a remote that is not published, so even a seat that somehow got created would never send the join the guest needs to learn about it.
  • The accept path that would seat the invitee is answerLocalInvite :1019; it calls addRoomParticipant(room.id, { kind: 'bot', handle: asker }, …) (:1046) and, on any throw, rolls the claim back (:1027 keeps acceptedBy single-use) and answers not-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:

  1. 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).
  2. 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.
  3. notifyRoomShare :937 must send join for a seat admitted this way — otherwise the guest's side never learns it is in, and the ticket has been spent for nothing.
  4. 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 (publishedRemoteOf stays 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/#864 and every assertAddable refusal 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-addable for 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

  1. added
    enhancementNew feature or request
    agent-readyMaintainer-approved spec: an agent may take it. Only maintainers set this; never in a template
    area:socialFriends, invites, grants, Bot Social
    area:securityAuth, grants, approvals, secrets
    on Oct 23, 2026
  2. Birfy commented on Oct 23, 2026

    @Birfy
    CollaboratorAuthor

    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

    { ticket, roomId, home, handle, claimedAt, expiresAt }
    

    What the seat gate reads (for #884)

    1. The claim exists and is unexpired.
    2. event.fromHome === claim.home and event.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.
    3. The envelope's room id equals claim.roomId.
    4. The seat handle written onto the room participant is claim.handle, and the grant is the one admission already writes — no per-identity grant, no new room_id on the line.

    Any one of these false → not-addable, and the ticket is not consumed.

    Guest side: which local Bot takes the seat

    claim.handle is 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 recordInboundSharedRoom needs a localBotId for: it is the resolved owner of claim.handle, not a field of the knock.

    Relation to #878

    #878 does not change this record. It changes (a) the host's room.localInvite from one object to a list of tickets so several invites can be live at once, and (b) the redemption so one ask may carry several handles of the redeeming home — each attributable to event.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, a join-invite naming two Bots of one home already seats both, writes one grant pair each, and each of them delivers with room_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.

  3. Birfy commented on Oct 23, 2026

    @Birfy
    CollaboratorAuthor

    Written-by: Dev

    Taking #884 together with #878 (they both rewrite answerLocalInvite and the room's ticket record, so splitting them would mean two PRs editing the same function at once). Branch feat/884-ticket-seat, from main after #887 lands. #878 has carried agent-working since 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) names event.fromHome and lists event.fromHandle; the seat handles are exactly the claimed set, all or none, under maxMembers. On the guest side the projected room's localBotId is the local Bot that owns the claimed handle the join names — never a field of the envelope.

    Seats admitted this way are admittedBy: 'ticket': they get the room's join/line fanout, they are not published remotes (publishedRemoteOf stays 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).

  4. added
    agent-workingAn agent has claimed this issue (the claim comment names the branch)
    on Oct 23, 2026
  5. self-assigned this
    on Oct 23, 2026
  6. removed
    agent-readyMaintainer-approved spec: an agent may take it. Only maintainers set this; never in a template
    agent-workingAn agent has claimed this issue (the claim comment names the branch)
    on Oct 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:securityAuth, grants, approvals, secretsarea:socialFriends, invites, grants, Bot SocialenhancementNew feature or request

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions