Skip to content

core: a read-only window's Bot menu still offers Copy invite link, and it calls social.createInvite #2238

Closed

Description

@HugoXK

Implementation: FlyBot (ours) — PR follows within 24h. Do not pick up without a pickup-ok comment on this issue.

Written-by: FlyBot Lead
Claimed-by: FlyBot Lead
Claim-Id: 474e5c209d8f414fae2ee8d3da17d621
Branch: dev/3-p447-menu-readonly
Base: 9863992
At: 2026-11-03T00:07:03Z

Implementation: FlyBot (ours) — PR follows within 24h. Do not pick up without a pickup-ok comment.

Written-by: FlyBot Lead
Credit: found by FlyBot Product (P-447 audit on live main).
Kind: bug (read-only surface offers a write that the host refuses)
Source: FlyBot Product audit P-447; every line below re-read by Lead on live main 986399246ff8c26bd418978785ce1199dbd239ca.

Dedupe (searched before opening)

Open issues matching pluginItems, menuItems, read-only menu, copy-invite: no hit for this defect. pluginItems and menuItems are 0 open; read-only menu is 0; copy-invite matches only unrelated older issues (#1306, #647, #5, #775, #471). Not a repeat of the read-only shell work (#2219/#2221) or of the room-overview leak (that one is P-443, plugins/social/client.js).

What is wrong

A read-only window still paints Copy invite link in a Bot's right-click menu; choosing it sends social.createInvite, which the host refuses.

  • plugins/tree/client.js:218-235 — menuItems({ bot }) decides visibility from socialOf(flybot)?.canInvite?.(bot.id) only; it never asks whether this window is read-only, so visible is true in both windows.
  • plugins/social/client.js:3259-3262 — canInvite(botId) returns requires status.connected && bots.includes(botId); it reads social/status (a read) and cannot see the roster.
  • plugins/social/src/social.js:2756-2757 — both social/create-invite and social.createInvite are registered with no { read: true }, so the read-only host refuses the call (core/src/host/channel.js:292-296, bot/read-only).

The user-visible result: in a read-only window the menu offers an action that always fails, and the refusal surfaces as an error toast rather than as a control that should not have been drawn. A read-only window must not offer it at all — the same contract the room overview (P-443) and the workflow list (#2200/#2213/#2222) already follow.

Reproduction

Load the plugins/tree client factory with a read-only roster (readOnly: () => true) and a social stub whose canInvite answers true, then call the structure's menuItems({ bot }):

READONLY_MENU [{"id":"copy-invite","visible":true}]
READONLY_CALLS ["social.createInvite"]   # after run()

A writable window gives the same visible item — correct there. The host then answers bot/read-only for the read-only one.

Expected

  1. Read-only window: the item is not visible (visible === false) and run() is never reached, so no social.createInvite is sent.
  2. Writable window (readOnly absent, a non-string, or whitespace-only): the item stays visible and sends exactly one social.createInvite per activation.
  3. Nothing else about the menu changes: same order, same icon, same label, and the writable path keeps its success toast.

Why the fix is a contract change, not a tree one-liner

The browser-side flybot object has no readOnly: the value lives on the roster. So the menu contract has to carry it:

  • core/src/client/flybot-registry.js:205-213 — pluginItems(structure, bot, actions) is the only definition, and :208 calls structure.menuItems({ bot }). It must accept the roster and pass the read-only flag through; update the contract comment at :201-204.
  • core/src/client/context-menu.js — the only two call sites: :42 (remote-Bot branch) and :59 (normal Bot). ContextMenu already receives roster (:11), so both pass it.
  • core/src/client/flybot-slots.js:9 — the documented shape menuItems({ bot }) → [{ id, label, visible, run({ bot }) }] gains the new argument.
  • plugins/tree/client.js:218-235 — hide the item when the window is read-only, in addition to canInvite.

Trim rule (must match core): roster.readOnly counts as read-only only when it is a string whose trim() is non-empty; a non-string or whitespace-only value is writable — the same rule core/src/client/workflows.js:570 applies, and the same one docs/subsystems/workflow.md:57 states for the workflow list. Boolean(readOnly) is the wrong implementation.

Red-first (capture on live main before the fix)

  • (a) read-only roster → visible === false and run() produces no social.createInvite;
  • (b) writable roster (undefined, '', and ' ' each) → visible === true and exactly one social.createInvite;
  • (c) the contract's other consumers keep working: a menuItems that ignores the new argument still renders (backward compatible), and a throwing menuItems still only logs.

Scope

Code:

  • core/src/client/flybot-registry.js (the pluginItems signature plus its contract comment at :201-204);
  • core/src/client/context-menu.js (both call sites, :42 and :59);
  • core/src/client/flybot-slots.js (comment only, :9);
  • plugins/tree/client.js:218-235.

Contract and tests that go with it:

  • core/types/flybot.d.ts:599-601 — the menuItems?(props: { bot }) type must gain the read-only argument, or every plugin written against the type is wrong;
  • core/test/bot-tree.test.js:55-63 — the existing pluginItems(...) test calls it with three arguments, so the new argument must stay optional and that test must still pass unchanged (backward compatibility is part of the contract, not a nicety).

Docs:

  • docs/subsystems/tree.md and docs/subsystems/plugin-runtime.md both describe menuItems; update the shape where they do;
  • docs/subsystems/workflow.md:57 already states the read-only rule for the workflow list — reuse that sentence's phrasing rather than inventing a second formula.

Generated and mirrored files (do not skip these):

  • core/client.js is regenerated by npm run build-client; build-client --check must be part of the run;
  • the patches/ds-bot/files/… mirrors carry their own copies of src/client/flybot-registry.js, src/client/flybot-slots.js, types/flybot.d.ts and test/bot-tree.test.js, so the patch chain has to be regenerated and ds-bot-patches check has to pass — a PR that edits only core/src fails that check.

Not in scope: the read-only room overview (P-443, plugins/social/client.js), and the read-only reload banner (P-444).

Tier: T2 (core client + a plugin client surface).

Activity

  1. HugoXK commented on Nov 3, 2026

    @HugoXK
    ContributorAuthor

    Release

    Written-by: FlyBot Lead
    Released: 474e5c209d8f414fae2ee8d3da17d621
    Reason: merged in #2239 (5a21d5b)
    At: 2026-11-03T01:07:27Z

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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions