Repository navigation
feat(rooms): answer a local room's invite link (#852) - #864
Conversation
|
Verdict: INC at 1bc7edd Ran: CI at this tip is red:
The cause is not the new
So this tip would land a regression on To unblock: rebuild the second commit on the current base — start from The envelope half ( |
56dd8c9 to
311f788
Compare
|
Verdict: INC at 56dd8c9 Ran: The previous INC is fixed. This tip is one commit on Blocker: That is not a false positive: the diff touches those five files and does not touch Same push, one more thing the release tooling checks: there is still no Non-blocking
I had not finished the six-point cut review of the envelope half when this tip went red, and a red tip cannot pass; the next tip gets it in full (it is the same core code as |
311f788 to
e1b43d7
Compare
…ed as one (#852) A local room's invite link names the Bot to knock on — `/r/@handle/<ticket>` — and that ticket belongs to another FlyBot. One parser now tells the two shapes apart, and the face-room path never hands a local ticket to Bot Social as one of its own: the join dialog and the host route both answer it in one sentence, before the feature gate, without asking any server anything. Part of #852.
e1b43d7 to
0d1f5db
Compare
|
Verdict: PASS at 0d1f5db Ran: What I checked:
Non-blocking:
|
A Bot's line to a friend's Bot that is only in a room with it was refused with no_relationship: bot-social decides reach from the pair's grants, and a room claim writes a status='room' relationship that findActiveRelationship does not read (#866, bot-social #43/#50). Both send paths now name the room on send_as_member when roomContextFor finds one open room here that holds both Bots, so the server's room-scoped grant is the reach the pair already holds. Room lines (via room/room-share) and a local shared room (#864) name none: a local room has no id on the server, and naming one only earns room_not_shared. A room-scoped refusal is reported as the room's answer, never retried on another Bot of mine.
…card (ThinkFlowLab#885) (ThinkFlowLab#887) Written-by: Dev3 Tip SHA: 4fafe2c Fixes ThinkFlowLab#885 Refs ThinkFlowLab#876 The plumbing behind a local room's own invite link, and the one preview card both surfaces draw. This is the half that was already green: it does not close ThinkFlowLab#876 — the Members card's **Invite by link** row and the redeem dialog stay there. ## Reproduce On `690093bc` there is no way to ask a room for its link from the client: - `grep -rn "room-invite" core/src/host/channel.js` answers nothing: the host has `mintRoomInvite` / `localInviteOf` / `revokeRoomInvite` / the `ask` path, and no endpoint reaches any of them. - There is no way for the invitee's side to knock from the client either: `rt.askLocalInvite` does not exist, so pasting a `/r/@handle/<ticket>` link into the join box can only be refused in one sentence (which is what ThinkFlowLab#864 shipped) and never becomes a request. - The preview card is markup inside `plugins/social/client.js`: `grep -c "face-link-preview-name" plugins/social/client.js` answers `1` on that base, so the second surface that needs a card (the member-invite dialog) would be a copy of it. ## What changed - **Host endpoints** (`core/src/host/channel.js`): `room-invite` returns the room's live ticket through `rt.localInviteOf` (what the card copies and counts down), `room-invite-mint` and `room-invite-revoke` wrap the existing mint/withdraw, and `room-invite-ask` knocks at a link someone else handed over through the new `rt.askLocalInvite`. - **`rt.askLocalInvite`** (`core/src/host/room-membership.js`): reads the handle and ticket out of the link with `parseRoomInviteLink` and sends `{ op: 'ask', ticket }` — and the same knock with `accept: true` for Join — from one of the actor's own Bots whose home has a Bot Social identity. The link cannot name a sender, so the transport does, exactly as on the answering side. A face-room link, a bare ticket and a ticket outside the 16–64 shape are refused in one sentence and send nothing; no knock ever carries a `roomId`. - **Authorization** (`core/src/host/authz.js`): `room-invite` is a member's read; `room-invite-mint` / `room-invite-revoke` need the room owner or a human admin; `room-invite-ask` needs a signed-in actor and no authority over anything here, because the ticket is the inviter's record's business. - **One preview component** (`core/src/client/room-invite-preview.js`): `invitePreviewOf` says only what a server reply can honestly say (a name cleaned at fixed point, the lead, `n / m in the room`, the deadline), `inviteCountdown` words the deadline, and `RoomInvitePreview` draws the tree from a caller-supplied `h`. Registered on `__flybot__` in `flybot-slots.js` with `invitePreviewOf` / `inviteCountdown`, because a classic-script plugin cannot import a core module. `kind` renames only the `data-flybot` hooks, so the face-room paste flow (`face-link-preview`) and the member-invite dialog (`room-invite-preview`) draw one tree. - **`plugins/social/client.js`** loses its own copy of the markup: the `fs-preview*` inline tree is gone and the face-room dialog renders `__flybot__`'s component; only the `fs-preview*` CSS stays. - **Docs**: `docs/subsystems/core-host.md` (the endpoints), `core-client.md` and `plugin-runtime.md` (the shared card), `social.md` (the card has one owner). `changelog.d/876-room-invite-endpoints.added.md`. ## What I ran All on `4fafe2c5` (single commit, 27 files, base `690093bc`): - Core full suite **1314/1314**, 0 fail (`node --import ./test/harness-resolve.mjs --test --test-concurrency=1 "test/*.test.js"`; serial because this machine's pthread limit aborts the parallel run — not a code failure). - `core/test/remote-room.test.js + authz.test.js + room-invite-preview.test.js` → **79/79**. - `plugins/social` full suite **386/386**, including the `hold_reason` assertions ThinkFlowLab#869 merged and the new "no second `face-link-preview-*` markup in `client.js`" case. - `node core/scripts/build-client.mjs --check` → ok; `node scripts/ds-bot-patches.mjs check` → 10 patches apply cleanly, `core/` equals `ds-bot/` + patches; core lint 0 errors; `changelog` 31 fragments; `arch-docs` ok. ## Red without the source change | removed | red | | --- | --- | | the `accept: true` the Join knock carries (`room-membership.js`) | `ThinkFlowLab#876: the invitee knocks on the link, and Join is the same knock with accept` — only that one (53/54) | | the `room-invite-mint` / `room-invite-revoke` authorization rule | `authz: owner cannot touch another owner Bot or room` — only that one (19/20) | Per-file diff against `690093bc`, checked by file sha rather than by eye: `plugins/social/test/social.test.js` **+19/−0**, `docs/subsystems/social.md` **+1/−1** (one sentence of mine replaced), and the only `−1` lines in `core/` are the `import` line and the `ROOM_TARGETED` set line. An earlier tip of this branch carried **+34/−84** on `social.test.js` — the `hold_reason` cases ThinkFlowLab#869 had just merged — and this branch is the rebuild that removed that rollback. ## Not run (and why) - The Members card's **Invite by link** row and the redeem dialog: they are ThinkFlowLab#876, by design. This issue is the plumbing they hang on. - Nothing calls `room-invite-ask`` from the plugin yet, so the offer the inviter sends back (`rt.onLocalInviteOffer`) has no surface to draw on. The seam is there and tested at the host level; the plugin wiring is ThinkFlowLab#876. - No end-to-end run: minting a link needs a second home to knock from, and Bot Social's face-room rate limit is currently exhausted on this machine (SelfTest hit `Too many attempts. Try again in 60 minutes.`). ## Risk - `room-invite-ask` sends from the actor's own Bot, so it needs at least one Bot with a Bot Social identity on this home; without one it refuses with `This machine has no Bot Social identity yet` rather than sending anonymously. - The ticket stays a bearer secret in the room record for ten minutes, as in ThinkFlowLab#861. - `core/client.js` is generated: the `+84` there is `npm run build`, and `build-client --check` is what keeps it honest. - `RoomInvitePreview` is registered with `??=`, so a plugin that loaded first and registered its own stays; nothing in this repo does. --------- Co-authored-by: Birfy <15378933+Birfy@users.noreply.github.com>
…ge the merge tree, one verdict per tip (ThinkFlowLab#906) Owner: Docs Reviewer: Reviewer Written-by: Docs Fixes ThinkFlowLab#905 Tip SHA: cdef24c Supersedes: `ab2cb0885` (INC — two one-line points, both fixed here; no PASS was posted at that tip), which superseded `26f769d2` before any conclusion. Docs only: no code, no behaviour change. One commit on `354f3bc5`; same five files, nothing else touched. **Round 4 — the two INC points** - **`docs/code-standards.md` link fixed.** `[…](.claude/skills/review-pr/SKILL.md)` resolved from `docs/` to a 404; it is now `../.claude/skills/review-pr/SKILL.md`. I re-checked every relative link in the five files the way the review did — path resolved from the file's own directory first, then the anchor against the target file's headings: 84 links, 0 broken. - **No date on the current routine shape.** The paragraph under the table and the "marked *timer*" sentence said 2026-10-21, but 10-21 recorded three timers (self-test, release, and maintenance hourly at `:07`); two timers is today's state. Both now describe the state without a date, so the next change to the schedule cannot leave a wrong date behind. **Round 3**: both timer rows say `timer`; `LESSONS.md` counts the stale-base class 4× (ThinkFlowLab#864, ThinkFlowLab#876, ThinkFlowLab#887, ThinkFlowLab#907 at `d6e143a8` and `b8b582bd`). **Round 2**: `Docs-not-needed:` is why + how to check; the two duplicate copies are pointers; `docs/code-standards.md` gains the list-sync rules and the two kinds of red evidence; the pipeline contract gains one sentence — an `@`-mention must produce one reply (ThinkFlowLab#913). **Round 1**: entry point person → **Chief** → **Triage / Dev / Reviewer**; step 2 proves the base; step 6 is five short checks; four merge-gate rules; "read the board once per round" (ThinkFlowLab#796); `CONTRIBUTING.md` keeps pointers; four dated lessons. **Ran** (at `cdef24ceaec968833da7522d6a4683265fa91b4f`) - `node links.mjs` — every relative link in the five files: path resolved from its own directory, target fetched at `354f3bc5` (or the local copy), anchor matched against that file's headings. 84 links, 0 broken. - `node patch9.cjs` — both edits asserted exactly one match before writing. - GitHub compare `354f3bc5...cdef24c` — ahead 1, behind 0, exactly 5 changed files, all five mine. - `node check_docs.mjs` — code fences even in all five files. **Not run** - `npm run verify` — no code, test, patch or generated file changed; `test`, `plugins (*)`, `package` and `arch-docs` see a docs-only diff. CI confirms. - No smoke bench (no `index.js` or `src/host/` change). Docs-not-needed: process documentation only; no owned path under `core/` or `plugins/` changes. Tier: T3 (`.github/agent/risk-rules.json`), one Reviewer PASS at `cdef24ceaec968833da7522d6a4683265fa91b4f`. The Reviewer enqueues after the PASS; I do not enqueue.
Written-by: Dev3
Tip SHA: 0d1f5db
Part of #852 — this PR does not close it. The Members card's Invite by link row and the paste/join dialog are not in this PR: they move to a follow-up PR, and #852 stays open until they land.
Fixes #877 — the wire half of the umbrella.
The link from #861 can now be used: a knock arrives as
ask, the inviter's host answers from the room record withinvite, and only the accepting knock admits one home. Nothing a knock says creates a room, seats a member, or consumes the ticket. In the plugins, the one invite field now reads both link shapes, and a link into a room on another FlyBot is answered by name instead of being handed to Bot Social as one of its tickets.Reproduce
fdecbfa0the knock is dropped:parseRoomShareanswersundefinedfor{ "v": 1, "op": "ask", "ticket": "AbCdEfGhIjKlMnOpQrStUv" }, becauseaskis not an op and every payload must carry aroomIdan invitee cannot know.applyInboundRoomShareonly knowsjoin|leave|line, and an unmatched op falls into the branch that projects an inbound-share room — so a knock would create a room on the inviter's side.grep -c "op === 'ask'" core/src/host/room-membership.jsanswers0on that base.parseFaceInviteLink('https://social.botawaken.com/r/@chief.alice.home/AbCdEfGhIjKlMnOpQrStUv')answers''on that base, so the join dialog saysThat is not an invite link— the same sentence it uses for a typo — although the link is well formed and means something the server cannot do.What changed
core/src/shared/room-share.js: opsask/invite; fieldsticket,accept,lead,expiresAt.askparses (and encodes) without aroomId,inviterequires the room it is for, and the ticket keeps the 16–64 character shape.core/src/host/room-membership.js:answerLocalInviteanswers the knock from the room record. The offer (askwithoutaccept) sends{ op: 'invite', ticket, roomId, name, lead, expiresAt, inviter* }and writes nothing. Both new ops are dispatched before the inbound-room projection, so neither can create a room.join(addRoomParticipantsends it).acceptedByis written once, at accept. The same home retrying is idempotent success and the lostjoinis sent again; another home is refusedconsumed; an add the room refuses rolls the claim back, so no refusal burns the ticket.event.fromHandle, the transport's sender — nothing in the payload can name it. Refusals are local and change nothing:unknown/expired/consumed/no-sender/not-addable.rt.onLocalInviteOfferis the seam the dialog will draw from (no UI yet).acceptedByis the one name;mintRoomInvitebuilds the URL before persisting (a handle a link cannot carry leaves no ticket behind); the no-identity refusal saysThis machine has no Bot Social identity yet.docs/subsystems/core-host.md(the four feat(rooms): mint and withdraw a local room invite link (#852) #861 follow-ups) anddocs/subsystems/social.md(the two link shapes and the local-link refusal), plus thepatches/ds-bot/files/mirrors this repository'score/requires, andchangelog.d/852-local-room-invites.added.md.plugins/social/src/face-rooms.js+social.js+client.js:parseInviteLinkreads both shapes ({ kind, ticket, handle? });parseFaceInviteLinkanswers only the face-room ticket. A local link is answered in one sentence (That link opens a room on another FlyBot. It does not join a face-to-face room.) by the dialog, by the scanner, and by the host route — the route answers before theface_room_invitesfeature gate, because the ticket was never this server's, so "needs a newer Bot Social" would point at the wrong thing.What I ran
TMPDIRoutside/tmp, which is a full 128M tmpfs here).core/test/remote-room.test.js→ 50/50, including the three#852cases.plugins/socialsuite at this tip → 382/382, 0 fail (face-rooms29/29).node scripts/ds-bot-patches.mjs check→ pin ok, 10 patches apply cleanly,core/equalsds-bot/+ patches; the threepatches/ds-bot/files/mirrors are byte-identical to theircore/sources (cmp).node core/scripts/lint.mjs→ 0 errors;node scripts/lint-plugins.mjs→ ok.1d81580e(currentmain: fix(social): keep the owner name and home when a room-only identity is promoted #867 and test(social): pin the stored owner name over a later whoami on promotion #870 included). Line diff of this tip againstmainforplugins/social/test/social.test.jsis +42/−0 — every test main already has is still there, including test(social): pin the stored owner name over a later whoami on promotion #870's, A user-created group shared to a friend's Bot names its inviter "a friend" #848's and A friend's Bot added to a local group shows the raw FLYBOT-ROOM-SHARE envelope on its approval cards #847's.Red without the source change
Nine knives, each cut on this tip:
ask/invitedispatch before the room projection#852test 3 — a knock projects a roomconsumedcheck#852test 3 — a second home takes the seat#852test 3 — a refusal burns the ticket#852test 1 — an expired ticket opens a room#852test 1 — a refused mint leaves a ticketSign in firstinstead of the identity message#852test 2 — the refusal names the wrong problem#852: a link into a room on another FlyBot is answered in one sentence and calls nobody— only that one#852: the host refuses a local room link before the feature gate, and sends nothing— only that oneparseInviteLink#852: two link shapes in one field— only that oneNot run (and why)
refusedback belongs with the dialog, which does not exist yet.local_room_invitesgate here: a gate needs a consumer. The plugin link work is reachable today (the face-room join box and scanner), and that is what the new tests drive.Risk
saveis the add), so the ticket survives. Once the seat exists the retry is the idempotent path.state.roomsfor ten minutes, as in feat(rooms): mint and withdraw a local room invite link (#852) #861.leadin the offer is the room admin Bot's display name: display only, never authority for the dialog.face-rooms.js) and in the client bundle (client.js), because they are separate bundles — the same duplication the existingINVITE_UNSUPPORTED/INVITE_LINK_COPYpair already has. A test pins both.