Skip to content

feat(rooms): seat a stranger's Bots through a link Bot Social records (#884, #878) - #889

Merged
Birfy merged 3 commits into
mainfrom
feat/884-ticket-seat
Oct 23, 2026
Merged

Birfy merged 3 commits into
mainfrom
feat/884-ticket-seat

Conversation

@Birfy

@Birfy Birfy commented Oct 23, 2026

Copy link
Copy Markdown
Collaborator

Written-by: Dev
Tip SHA: a2ed01420cbb3aa8ad64fb45aed180364472dade
Fixes #884
Fixes #878
Refs #852, #883, #876

A room that lives in one FlyBot can now seat a home that is not a friend, with the Bots it brings, through a link Bot Social records (Bot Social #60, merged as b748167). This is the host half: the ticket list, the server seam, the read-back on accept, the seat, and the room traffic to and from it. The social plugin's half (registering the seam, the connector exception for room shares to a seat) is #883 and the next PR; the UI is #876.

Reproduce

On d0cafd5:

What changed

  • A list of links (room-membership.js): room.localInvites, several open at once; rt.localInvitesOf / rt.localInviteOf (newest); rt.revokeRoomInvite(roomId, ticket?) withdraws one link, or every unused one; the old single field is moved into the list on first read.
  • Server-recorded tickets: with rt.roomInviteServer (registered by a plugin through the new flybot.rooms.serveInvites), a mint goes to Bot Social (server: true). A failing server refuses the mint (never a silent local fallback); a server without local_room_invites answers undefined and the friend-only local ticket stays.
  • Join on the guest side: room-invite-ask with accept: true and bots claims the ticket for those Bots (all or none, the actor's own only), then the first of them knocks with { op: 'ask', ticket, accept, roomId, bots }. room-invite-preview reads the server's public record. Server refusals map to one reason each (expired, revoked, claimed-elsewhere, own-invite, busy, lookup-failed).
  • Seating on the host side (seatByTicket): the knock is a request only. The claim is read back with this home's own credential and seats exactly the claim's Bots, from the claiming home, for this room, or nobody: unclaimed / claimed-elsewhere / wrong-room / lookup-failed / expired / revoked / not-addable. A seat is { admittedBy: 'ticket', ticket, hostBotId }: not published, nothing in the sidebar, named from the server's claim.
  • Room traffic: notifyRoomShare / fanoutRoomLine reach a seat from hostBotId only, with meta.scope (the room id) for the transport to send under. Lines and leaves a remote seat sends to a room of this home land in it (ingestSeatShare), fan out to the other seats but not back, and call local Bots through startInboundTurn; a line for a room of mine from a non-seat is dropped (not-seated).
  • Consent: an open or seated Bot Social link is the minting owner's consent to show the room as it is, so that owner's Bots pass mayShareRoom there (reach.js openedByLink). A room with an ungranted Specialist can be shared by link; without a link nothing changes.
  • Seat lifetime: the ten minutes bound the claim, not the seat. A seat ends on withdraw (unseatTicket), removal or the guest leaving (the ticket is spent and revoked at the server once its last seat is gone), and room delete (rt.dropRoomInvites, called from deleteRoom).
  • Guest projection: one inbound-share room for every Bot a ticket seated (sharedFrom.ticket); each join adds or revives only the Bot it names; a seated Bot's line goes from that Bot under the inviter's room id; the last one leaving gives the claim back.
  • Docs: docs/subsystems/core-host.md. changelog.d/884-ticket-seats.added.md.

What I ran

On a2ed01420cbb3aa8ad64fb45aed180364472dade (Node 24.21.0):

  • TZ=UTC npm run verify → pass: core 1321/1321, plugins/social 386/386, tree 125/125, timeline 125/125, .github scripts 332/332, interop 37/37, live-e2e 16/16; build-client check, ds-bot-patches check, lint and arch-docs ok.
  • core/test/room-ticket-seat.test.js (new, 5 tests): two homes in one process, a stand-in for Bot Social's ticket record and reach rule, and a transport that delivers only where that reach lets it through.

Red without the source change

removed red
openedByLink in mayShareRoom 4/5 in room-ticket-seat (the room with an ungranted Writer is not-addable)
the ownSeatOf → ingestSeatShare branch 2/5 (the guest's line opens a phantom room on the host)
the "knocking Bot is one the claim brought, from its home" check the refusal test only
unseatTicket on withdraw the withdraw test only

Not run (and why)

Risk

  • mayShareRoom now passes the minting owner's Bots in a room with an open or seated Bot Social link; that also lets them share that room with a friend's Bot in it. Owner-scoped, and it ends with the link.
  • A seat outlives the ten-minute window (the claim does not): it ends on withdraw, removal, leave or delete. Flagged on Bot Social feat(identity): persist team members and owner-bound bots #60 for the owner to confirm.
  • core/client.js unchanged; patches regenerated, 0001/0003 keep main's generated text.

@Birfy Birfy added the agent-pr Pull request opened by an agent label Oct 23, 2026
@Birfy Birfy self-assigned this Oct 23, 2026
@Birfy

Birfy commented Oct 23, 2026

Copy link
Copy Markdown
Collaborator Author

Verdict: PASS at d5fac4f
Written-by: Reviewer

Fresh pass at the PR head, read cold against origin/main d0cafd5 (21 files: room-membership.js, reach.js, channel.js, authz.js, roster.js, hooks.js, room-share.js, the two test files, two subsystem docs, the regenerated patches).

Ran:

  • node scripts/ds-bot-patches.mjs check (repo root, Node 24.21.0) → pass, core/ equals ds-bot/ + patches/ds-bot/
  • cd core && npm test → pass (1322/1322)
  • cd core && npm run lint → pass (one warning, unicorn/no-useless-spread at room-membership.js:1918, in the participants spread that is on main already)
  • cd core && node scripts/build-client.mjs --check → pass
  • arch-docs check → OK (21 changed files)
  • live two-home pre-check on this box (two local FlyBot instances at a later stacked SHA that holds this head, one local Bot Social at master b748167): mint on home A → preview and Join on home B → B's Bot seated in A's room, as the claim named
    Not run:
  • Desktop / real-model steward: not needed for a host-side change
  • Two-way message traffic in the live pre-check: that needs the plugin half (The connector denies a stranger's room-share "ask", so a redeemed link never reaches the inviter's host #883), which is reviewed on its own PR

Findings, no blocking ones:

  • Security: a seat is only ever made from the claim that Bot Social returns to this home's own credential (seatByTicket → verify). The knocking Bot must be one of the claimed Bots, from the claiming home. All the Bots are seated or none are, with at most 8. A room with a Bot that may not be shown is refused (not-addable). reach() opens only the exact handle pairs under the room's scope (samePeer), never "any Bot of that home". openedByLink widens mayShareRoom only for the minting owner's own Bots, and only while a server link is open or holds a seat.
  • Display names: remote names go through cleanPeerName, and a guest person's authorId is namespaced ${handle}#…, so it cannot pass as a local member.
  • identify (d5fac4f) runs only for the room lead on mint and for the actor's own picked Bots on Join. A failure fails closed with its own message. The new test covers both, plus the failure path.
  • Non-blocking: on the guest side, a join that carries a ticket is not checked against state.roomClaims. Today Bot Social only delivers those messages under a claimed room reach, so this is defence in depth only. Noted for follow-up.

This report is a Verifier result, not merge authority on its own; the merge follows AGENTS.md step 6.

@Birfy
Birfy added this pull request to the merge queue Oct 23, 2026
@github-actions github-actions Bot added the pass label Oct 23, 2026
Merged via the queue into main with commit 924b175 Oct 23, 2026
11 checks passed
@Birfy
Birfy deleted the feat/884-ticket-seat branch October 23, 2026 15:36
@github-actions github-actions Bot removed the agent-pr Pull request opened by an agent label Oct 23, 2026
HugoXK pushed a commit to HugoXK/FlyBot that referenced this pull request Oct 23, 2026
… room (ThinkFlowLab#883) (ThinkFlowLab#890)

Written-by: Dev
Tip SHA: `ca8f82268d4c6eb05216762fd604e23f40276b0e`
Fixes ThinkFlowLab#883
Refs ThinkFlowLab#852, ThinkFlowLab#884, ThinkFlowLab#878, ThinkFlowLab#876

This is the social plugin's half of the stranger link. ThinkFlowLab#889 (merged as
924b175) seats a home that is not a friend through a link Bot Social
records. This PR registers that host seam with the plugin and lets the
room's shares reach exactly that seat, under that room, without making
the stranger a friend.

## Reproduce

On `924b175` with the plugin loaded:

- `rt.roomInviteServer` stays undefined, because no plugin calls
`flybot.rooms.serveInvites`. Every mint falls back to the friend-only
local ticket, and a stranger can never be seated.
- A room share to a seated stranger's Bot is denied by the social send
middleware's roster check (the handle is not a friend), or it is held
for approval.
- Live two-home pre-check on this box (two local FlyBot instances plus a
local Bot Social at master b748167):
- A fresh home's lead Bot has no Bot Social identity, so
`room-invite-mint` answers `This machine has no Bot Social identity
yet`. A room-only identity is not polled, so the room's lines would not
arrive either.
- Withdrawing a link (Bot Social answers `DELETE … 204`) killed the
inviting FlyBot process: `TypeError: Response constructor: Invalid
response status code 204` was thrown inside `socialFetch`'s `end`
handler, where nothing catches it.
- The guest's room read "Shared room from You": the host's own label for
its owner leaked into `sharedFrom.inviterName`.

## What changed

- **The seam** (`social.js`): `flybot.rooms.serveInvites` is registered
as follows:
- `identify` gives a link's own Bots a full identity with `register(bot,
{ forInvite: true })`, which is polled and signs its sends;
- `mint` is gated on the `local_room_invites` feature in `/v1/platform`,
and answers `undefined` on an older server;
- `preview`, `claim` and `verify` read the minting or claiming owner's
view;
  - `revoke`.
  The REST calls live in `face-rooms.js` `localInvites`.
- **Exactly the seat** (`ticketPair`):
- A room share whose pair `{from, to, scope}` is one of
`flybot.rooms.ticketPeers()` skips the roster deny and the hold.
- `sendTicketShare` sends it as `send_as_member` with `room_id: scope`,
so Bot Social uses that room's reach.
- Nothing else is opened: another Bot of the same home, another room, or
a plain DM is still refused.
- **204 is an answer** (`mcp-client.js` `socialFetch`):
  - A 101/204/205/304 Response is built with no body.
- A Response that still throws now rejects the call, so it can no longer
end the process.
- **Inviter name** (`room-membership.js` `shareInviter`): a human
participant named "You" (this home's own label) yields to the Bot Social
owner name.
- **Docs**: `docs/subsystems/social.md`. Changelog fragments
`changelog.d/883-stranger-room-share.added.md` and
`changelog.d/883-link-live-fixes.fixed.md`.

## Tests

- `social.test.js` "ThinkFlowLab#883: …": a SEATED room's share goes to the seat as
`send_as_member` with `room_id`. A non-pair is still refused.
- `face-rooms.test.js` "ThinkFlowLab#884: local room invite calls": each REST shape.
- `connector-scope.test.js` "a 204 answer …": a real HTTP server answers
204. Without the fix this test fails with `Invalid response status code
204`.
- `room-ticket-seat.test.js`: the host's human is "You", and the guest
must read "Alice". This fails without the fix.

## What I ran

- `TZ=UTC npm run verify` at this tree (Node 24.21.0) → pass. The tree
is identical to 0c4c7e0, checked with `git diff 0c4c7e0 ca8f822` =
empty. Counts: core 1322, social 389, tree 125, timeline 125, .github
scripts 332, interop 37, live-e2e 16. `ds-bot-patches check` ok.
- Live pre-check, two non-friend homes on this box:
  - A minted a link (the lead got its identity on demand);
  - B previewed it ("Trip plan", Chief, Ann) and joined with one Bot;
- A seated `@scoutb.ben.home` with `admittedBy: 'ticket'`, and B
projected the room;
- A crashed on the 204 right after. That crash is fixed here, and the
full two-way run is repeated after merge.

## Risk

The plugin's send path for room shares changes, but only for a pair that
`ticketPeers()` lists, under its own scope. Every other send takes the
same path as before.

---------

Co-authored-by: Birfy <15378933+Birfy@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

1 participant