Skip to content

A local-room invite link cannot be redeemed by a home that cannot already reach the inviter (cross-repo: claim + connector exception) #853

Closed

Description

@Birfy

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 — assertAddable requires a remote Bot to be publishedRemoteOf ("Only a Bot granted to you may join a shared room"), and notifyRoomShare skips 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_conversations is 1:1 and the group rides a FLYBOT-ROOM-SHARE envelope on send_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

Acceptance

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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

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


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 home
  • name: stored for the preview, cleaned the way a face-room name is
  • expires_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 / revoked
  • lead is 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 sees claimed: 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_claimed when 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 is 409

DELETE /v1/local-room-invites/{ticket} — auth: the owner credential of the minting account

  • 204; 404 once it is gone, 403 from 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

  1. added
    enhancementNew feature or request
    area:socialFriends, invites, grants, Bot Social
    area:automationScheduled tasks and automation
    area:securityAuth, grants, approvals, secrets
    on Oct 23, 2026
  2. Birfy commented on Oct 23, 2026

    @Birfy
    CollaboratorAuthor

    Status: not startable yet, so it carries no agent-ready. Two gates: the local ticket contract from #852 must be in main, and the claim contract on the bot-social side must be agreed. Triage will add agent-ready when the first gate is closed and the second has an answer, not before.

  3. Birfy commented on Oct 23, 2026

    @Birfy
    CollaboratorAuthor

    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

  4. Birfy commented on Oct 23, 2026

    @Birfy
    CollaboratorAuthor

    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, tip 0d1f5db8) the comparison is invite.acceptedBy !== asker where asker = event.fromHandle — a Bot handle. So a second Bot from the same home is a different asker and is refused consumed; 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.

  5. Birfy commented on Oct 23, 2026

    @Birfy
    CollaboratorAuthor

    P0 chain opened (2026-10-23). #853 stays 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:

    One correction to this body's contract, on the record: claim must 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.

  6. Birfy commented on Oct 23, 2026

    @Birfy
    CollaboratorAuthor

    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.

  7. Birfy commented on Oct 23, 2026

    @Birfy
    CollaboratorAuthor

    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:

    1. Only the claimed ticket's ask gets 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 whole FLYBOT-ROOM-SHARE envelope 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.
    2. Claims cannot be reused or forged. At the host, 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' in room-membership.js) and at the server (bot-social test/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").
    3. The admitted Bot sits in that one room only. It is not published as a friend's Bot, and the roster sync leaves it alone (feat(rooms): Invite by link in the Members card, with its QR, and Join for a group link (#876) #891, the "seat stays" assertion in the The connector denies a stranger's room-share "ask", so a redeemed link never reaches the inviter's host #883 test). Its access ends on withdraw, room delete or leave: "assertAddable refuses a remote Bot that a verified ticket admitted, so the invited stranger is seated nowhere #884: withdrawing the link ends the seat at both homes and at the server" and "assertAddable refuses a remote Bot that a verified ticket admitted, so the invited stranger is seated nowhere #884: the guest leaving gives the claim back; deleting the room withdraws its links". Claim expiry ends the claim, not the seat, per the decision above.
    4. Negative controls: not claimed, expired, claimed by another home, and lookup failure each refuse on their own, in the tests listed under 1 and 2.

    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.

  8. Birfy commented on Oct 23, 2026

    @Birfy
    CollaboratorAuthor

    Owner decisions (2026-10-24, Asia/Shanghai):

    How each acceptance point is met:

    1. Only the claimed ticket's ask gets 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 whole FLYBOT-ROOM-SHARE envelope 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.
    2. Claims cannot be reused or forged. At the host, 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).
    3. The admitted Bot sits in that one room only. It is not published as a friend's Bot, and the roster sync leaves it alone (feat(rooms): Invite by link in the Members card, with its QR, and Join for a group link (#876) #891, the "seat stays" assertion in the The connector denies a stranger's room-share "ask", so a redeemed link never reaches the inviter's host #883 test). Its access ends when the owner removes it, the room is deleted, or it leaves: "A group joined by link is listed as "Shared room from Ann" instead of its own name #892: withdrawing the link stops new joins and keeps the seat; removing the guest ends its reach" and "assertAddable refuses a remote Bot that a verified ticket admitted, so the invited stranger is seated nowhere #884: the guest leaving gives the claim back; deleting the room withdraws its links" (core/test/room-ticket-seat.test.js).
    4. Negative controls: not claimed, withdrawn, a Bot no claim brought, another room, and lookup failure each refuse on their own, in the tests listed under 1 and 2.

    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).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:automationScheduled tasks and automationarea: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