Repository navigation
A local-room invite link cannot be redeemed by a home that cannot already reach the inviter (cross-repo: claim + connector exception) #853
Description
What this tracks
The cross-repo half of #852: a /r/<ticket> link to a local room that a home with no prior reach can redeem. #852 (in-repo half) covers homes that can already reach each other — a friend, or a Bot already shared. The same link handed to a stranger cannot work today for two measured reasons:
plugins/social/src/social.js:1339-1357— the send middleware denies any target that is not a Bot the server roster already shared with us (:1340, "is not a Bot a friend shared with you") and denies once the friendship is gone (:1357). A cold handle never reaches the transport.core/src/host/room-membership.js:555-563—assertAddablerequires a remote Bot to bepublishedRemoteOf("Only a Bot granted to you may join a shared room"), andnotifyRoomShareskips unpublished remotes at:923. "Ticket verified, admit them" is not expressible today.- Bot Social has no record of a local room:
docs/subsystems/social.md:185-187—list_conversationsis 1:1 and the group rides aFLYBOT-ROOM-SHAREenvelope onsend_as_member. So a stranger's home cannot look the link up the way a face-room ticket is looked up (GET /v1/face-rooms/invites/{ticket}).
Measured on main edb9b7cd.
Decision (owner, via Chief, 2026-10-23)
Path 1: a connector exception scoped to the ticket. send_as_member is allowed for a FLYBOT-ROOM-SHARE op:"ask" only when all of these hold: the ticket has been claimed, the claiming home is the sender, the ticket has not expired, and the room id in the envelope is the room bound to that ticket. Everything else keeps the existing deny.
Rejected: a server-pushed inbox event on claim. It would add a new polling mechanism in this repo and gains nothing over checking at send time.
Host side: a way for a remote Bot to be in a room because a verified ticket admitted it, without being publishedRemoteOf. This is the security-sensitive part and must stay minimal: only the room the ticket names, only the Bot the claim names, only while the ticket lives.
Depends on
- The local ticket contract landed in A local room has no invite of its own: no link to hand over, only a picker of Bots the account already knows #852: 22-character ticket, 10-minute life, one per room, re-mint replaces, cleared on revoke / room delete / leave, and fail-closed on unknown ticket, expiry, a claim by another home, an impersonated home, or a failed lookup.
claimsemantics on the Bot Social side (ThinkFlowLab/bot-social): who may claim, how long a claim lives, whether a claim can be redeemed twice, and whatclaimreturns (the inviter's home plus the room binding).- The
ask/invite/joinenvelopes from A local room has no invite of its own: no link to hand over, only a picker of Bots the account already knows #852 — this issue adds no new envelope shape unless the claim result needs one.
Acceptance
- A home with no friend relationship and no shared Bot, holding a live claimed ticket, sends
op:"ask"— exactly that one message passes; the same home sending any other text or any other op is still denied with the existing sentence. - Claims cannot be reused or forged: a ticket claimed by home X is denied for home Y at the connector and at the host; a ticket whose envelope names a different room adds nobody; an expired ticket is denied at both layers.
- The admitted remote Bot appears in that one room and is not published to the sidebar as a friend's Bot; access ends when the ticket is revoked, the room is deleted, or the claim expires.
- Negative controls — each of the four conditions (not claimed / expired / claimed by another home / lookup failure) forced on its own produces a denial, not a silent success.
- No regression: face-room
/r/<ticket>and the friend-path local invite from A local room has no invite of its own: no link to hand over, only a picker of Bots the account already knows #852 both still work, and the connector's default deny for non-friends is unchanged for everything outside this one envelope.
Out of scope
- The in-repo link itself (A local room has no invite of its own: no link to hand over, only a picker of Bots the account already knows #852) and the hold clearing (A friend's Bot added to a local group shows the raw FLYBOT-ROOM-SHARE envelope on its approval cards #847).
- Making Bot Social store FlyBot group state. The ticket is the only record; there is no room mirror to keep in sync.
Where the work lives
ThinkFlowLab/bot-social: the claim endpoint and the resolver that lets a link be redeemed without a friend relationship.- This repo: the host participant that a verified ticket admits, and the connector exception above.
Related
- A local room has no invite of its own: no link to hand over, only a picker of Bots the account already knows #852 — the in-repo link this depends on
- A friend's Bot added to a local group shows the raw FLYBOT-ROOM-SHARE envelope on its approval cards #847 — the hold that currently parks the invite envelope
- A user-created group shared to a friend's Bot names its inviter "a friend" #848 — the inviter fields the link's preview needs to name
Endpoint contract — the bot-social half (moved here from #852 by Dev3; the Chief approved this split on 2026-10-23)
The link invitation for a local room needs these four endpoints in the bot-social repository. Nothing in this repository can ship them, so the local half stays inert behind a local_room_invites entry in GET /v1/platform features until they exist. The room itself is not part of the contract: it stays local to each home, and the server records only the ticket.
POST /v1/local-room-invites — auth: the owner credential (the one the face-room invite paths already use)
- body
{ room_id, name } 201 { ticket, url, expires_at }ticket: 16–64 characters of[A-Za-z0-9_-], the shape the client already accepts; one room, one homename: stored for the preview, cleaned the way a face-room name isexpires_at: 10 minutes after mint, the face-room window
GET /v1/local-room-invites/{ticket} — public, like GET /v1/face-rooms/invites/{ticket}
200 { kind: 'local_room', room_id, name, lead: { handle, display_name, owner_name }, expires_at, claimed }404 invite_not_found,410 expired/revokedleadis the object shape the face-room preview already returns (Face-room invite preview renders the room lead as "object Object" #846)- with the minting account's credential the answer also carries
claimer: { handle } | null, which is what the inviter's host re-reads before it adds anyone; an anonymous caller only seesclaimed: true | false
POST /v1/local-room-invites/{ticket}/claim — auth: the owner credential of the joining account
200 { claimed: true, room_id, inviter_home, expires_at }409 already_claimedwhen another home claimed it first,410 expired/revoked,404 invite_not_found- the claim is single-use: repeating it from the same home is idempotent (
200), from a different home it is409
DELETE /v1/local-room-invites/{ticket} — auth: the owner credential of the minting account
204;404once it is gone,403from another account
In this repository the resolve order is GET /v1/face-rooms/invites/{ticket} first and this endpoint on 404, so a server without the new routes behaves exactly as today. The client never parses the ticket; the answer's kind decides.
Activity
Birfy commentedon Oct 23, 2026
Status: not startable yet, so it carries no
agent-ready. Two gates: the local ticket contract from #852 must be inmain, and theclaimcontract on thebot-socialside must be agreed. Triage will addagent-readywhen the first gate is closed and the second has an answer, not before.Birfy commentedon Oct 23, 2026
Tracked upstream: Birfy/bot-social#43 — a valid room invite ticket should write a room-scoped grant on claim, instead of requiring an existing friendship. That issue also records the two alternatives we weighed (connector exception keyed on claim; server-authored inbox event) and why a connector-side bypass is the wrong shape.
Written-by: Chief
Birfy commentedon Oct 23, 2026
Recorded while answering a product question (2026-10-23): one ticket admits one Bot identity, not one home and not one person. In #864's accept (
room-membership.js:1027, tip0d1f5db8) the comparison isinvite.acceptedBy !== askerwhereasker = event.fromHandle— a Bot handle. So a second Bot from the same home is a different asker and is refusedconsumed; to seat two Bots you mint twice (one ticket per room, re-mint replaces).That is also why this issue's "who may claim" question cannot be answered by "the home": the local record binds a handle. If the cross-repo claim should bind a home instead (a home redeeming once and then seating several of its Bots), say so explicitly here — the local half would have to change with it, and this issue is the only place that decision is visible.
Birfy commentedon Oct 23, 2026
P0 chain opened (2026-10-23).
#853stays the cross-repo gate; its closing condition is unchanged and quoted in each child: a stranger, holding the link, brings one Bot of their own into the room, and the two sides can then message each other.The work is now four buildable issues plus the UI split:
room_invitestable, mint / public preview / single-useclaim(binds home and Bot handle) / revoke, plusfeatures.local_room_invites. It adopts the endpoint contract that currently lives in this issue's "Endpoint contract" section, with the deltas measured by Dev4 (new table, claim, 16-64 ticket shape)./r/:handle/:ticketis a 404 today becauseserver.ts:51only registers/r/:ticket, while the local link is two segments.op:"ask"envelope, four conditions, fail-closed, one sentence per refusal. Measured deny points on main690093bcaresocial.js:1436/:1453(the1339-1357in this body has drifted).assertAddable:556-563,notifyRoomShare:937, one seat for one verified claim, and the seat dies with the ticket.One correction to this body's contract, on the record:
claimmust bind the Bot handle as well as the home (#878's semantics), and the ticket shape is^[A-Za-z0-9_-]{16,64}$, not the face room's 43 characters.Birfy commentedon Oct 23, 2026
Status after #887, #889, #890, #891 (main c90af02) and bot-social #60, #62, #64 (master 8c64f33):
Local two-home run on c90af02 + bot-social 8c64f33 (homes not friends, fresh Bot Social): A minted the link from the Members card, B previewed and joined with its Bot, B and B's Bot were seated in A's group, lines flowed both ways, and B's Bot answered in A's group. Acceptance 1, 2 and 4 are covered by tests in #889/#890/#891 (connector exception only for a claimed ticket's ask; claim by another home, other room, expiry and lookup failure each refused at both layers; a guest join on a ticket this home never claimed opens nothing).
Left open for one owner decision on acceptance 3: the 10 minutes bound the claim, not the seat. Today the admitted Bot stays in the room after the ticket's 10 minutes; it leaves when the owner removes it, the link is revoked before the claim, the room is deleted, or the guest leaves. If "access ends when the claim expires" should mean the seat itself expires, that is a small follow-up.
Also: Bot Social server changes (#62, #64) are merged but not deployed to production, so real homes on the hosted server cannot redeem a stranger's link until that deploy.
Birfy commentedon Oct 23, 2026
Owner decision (2026-10-24 00:32 Asia/Shanghai) on acceptance point 3: keep the current behaviour. The stranger's seat stays until the owner removes it, the group is deleted, or the stranger leaves. The 10 minutes bound only the claim, not the seat. Withdrawing the link while it is still live also ends the seat.
How each acceptance point is met:
askgets through. The connector exception is in feat(rooms): seat a stranger's Bots through a link Bot Social records (#884, #878) #889 (feat(rooms): seat a stranger's Bots through a link Bot Social records (#884, #878) #889) and feat(social): a room link's shares reach exactly its seat, under that room (#883) #890 (feat(social): a room link's shares reach exactly its seat, under that room (#883) #890), with the server side in bot-social feat(auth): member sign-in and revocable invitations #62 and feat(authz): scope bot resources and administration to their owner #64. feat(rooms): Invite by link in the Members card, with its QR, and Join for a group link (#876) #891 (feat(rooms): Invite by link in the Members card, with its QR, and Join for a group link (#876) #891) adds that the text must be a wholeFLYBOT-ROOM-SHAREenvelope for that same room. Test:plugins/social/test/social.test.js"The connector denies a stranger's room-share "ask", so a redeemed link never reaches the inviter's host #883: a room share reaches a seat a link opened, under that room only, and nothing else gets through". Any other target, text or room is denied, and nothing reaches Bot Social.core/test/room-ticket-seat.test.js"assertAddable refuses a remote Bot that a verified ticket admitted, so the invited stranger is seated nowhere #884: the accept is read back from Bot Social, and each way it fails has its own reason" covers: unclaimed, a Bot the claim did not bring, the right handle from another home (claimed-elsewhere), a different room (wrong-room), lookup-failed, and a withdrawn link (unknown). A second home's claim on a spent link is refused. The guest side, from feat(rooms): Invite by link in the Members card, with its QR, and Join for a group link (#876) #891: "assertAddable refuses a remote Bot that a verified ticket admitted, so the invited stranger is seated nowhere #884: a join that names a ticket this home never claimed opens nothing". Expiry is refused at the host (refused: 'expired'inroom-membership.js) and at the server (bot-socialtest/local_room_invites.test.ts"feat(identity): persist team members and owner-bound bots #60: an expired or withdrawn ticket refuses a claim and writes nothing").Live proof: two homes that are not friends, on main c90af02 with bot-social master 8c64f33, joined by link. Both people and the guest's Bot talked in the group.
Closing as done by #887, #889, #890 and #891 (plus bot-social #62 and #64). The Bot Social production deploy is still pending.
Birfy commentedon Oct 23, 2026
Owner decisions (2026-10-24, Asia/Shanghai):
929d390) and feat(rooms): group link with no deadline for anyone until withdrawn; Members-card layout; Join-or-create entry; link rooms keep their name (#892) #893 (mergedb54f38a).How each acceptance point is met:
askgets through. The connector exception is in feat(rooms): seat a stranger's Bots through a link Bot Social records (#884, #878) #889 (feat(rooms): seat a stranger's Bots through a link Bot Social records (#884, #878) #889) and feat(social): a room link's shares reach exactly its seat, under that room (#883) #890 (feat(social): a room link's shares reach exactly its seat, under that room (#883) #890), with the server side in bot-social feat(auth): member sign-in and revocable invitations #62 and feat(authz): scope bot resources and administration to their owner #64. feat(rooms): Invite by link in the Members card, with its QR, and Join for a group link (#876) #891 (feat(rooms): Invite by link in the Members card, with its QR, and Join for a group link (#876) #891) adds that the text must be a wholeFLYBOT-ROOM-SHAREenvelope for that same room. Test:plugins/social/test/social.test.js"The connector denies a stranger's room-share "ask", so a redeemed link never reaches the inviter's host #883: a room share reaches a seat a link opened, under that room only, and nothing else gets through". Any other target, text or room is denied, and nothing reaches Bot Social.core/test/room-ticket-seat.test.js"assertAddable refuses a remote Bot that a verified ticket admitted, so the invited stranger is seated nowhere #884: the accept is read back from Bot Social, and each way it fails has its own reason" covers: unclaimed, a Bot the claim did not bring, the right handle from another home (claimed-elsewhere), a different room (wrong-room), lookup-failed, and a withdrawn link (unknown). A second home's claim on a spent link is refused. The guest side, from feat(rooms): Invite by link in the Members card, with its QR, and Join for a group link (#876) #891: "assertAddable refuses a remote Bot that a verified ticket admitted, so the invited stranger is seated nowhere #884: a join that names a ticket this home never claimed opens nothing". A withdrawn link is refused at the host (revoked) and at the server; a ticket dated before the change still expires at both. One home claims once (same Bots again is a retry; other Bots are refused).core/test/room-ticket-seat.test.js).Live proof: two homes that are not friends joined by link, on main c90af02 with bot-social 8c64f33, and again on #893's head with bot-social 929d390 (mint answers no deadline). Both people talked in the group, and the guest's Bot answered in the first run.
Closing as done by #887, #889, #890, #891 and #893 (plus bot-social #62, #64 and #66). The Bot Social production deploy is still pending (#60–#66 are not deployed).
feat(rooms): group link with no deadline for anyone until withdrawn; …