Repository navigation
Description
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 fromsocialOf(flybot)?.canInvite?.(bot.id)only; it never asks whether this window is read-only, sovisibleistruein both windows.plugins/social/client.js:3259-3262—canInvite(botId)returnsrequires status.connected && bots.includes(botId); it readssocial/status(a read) and cannot see the roster.plugins/social/src/social.js:2756-2757— bothsocial/create-inviteandsocial.createInviteare 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
- Read-only window: the item is not visible (
visible === false) andrun()is never reached, so nosocial.createInviteis sent. - Writable window (
readOnlyabsent, a non-string, or whitespace-only): the item stays visible and sends exactly onesocial.createInviteper activation. - 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:208callsstructure.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).ContextMenualready receivesroster(:11), so both pass it.core/src/client/flybot-slots.js:9— the documented shapemenuItems({ 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 tocanInvite.
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 === falseandrun()produces nosocial.createInvite; - (b) writable roster (
undefined,'', and' 'each) →visible === trueand exactly onesocial.createInvite; - (c) the contract's other consumers keep working: a
menuItemsthat ignores the new argument still renders (backward compatible), and a throwingmenuItemsstill only logs.
Scope
Code:
core/src/client/flybot-registry.js(thepluginItemssignature plus its contract comment at:201-204);core/src/client/context-menu.js(both call sites,:42and: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— themenuItems?(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 existingpluginItems(...)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.mdanddocs/subsystems/plugin-runtime.mdboth describemenuItems; update the shape where they do;docs/subsystems/workflow.md:57already 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.jsis regenerated bynpm run build-client;build-client --checkmust be part of the run;- the
patches/ds-bot/files/…mirrors carry their own copies ofsrc/client/flybot-registry.js,src/client/flybot-slots.js,types/flybot.d.tsandtest/bot-tree.test.js, so the patch chain has to be regenerated andds-bot-patches checkhas to pass — a PR that edits onlycore/srcfails 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
HugoXK commentedon Nov 3, 2026
Release
Written-by: FlyBot Lead
Released: 474e5c209d8f414fae2ee8d3da17d621
Reason: merged in #2239 (5a21d5b)
At: 2026-11-03T01:07:27Z