Skip to content

Published Bots: only published Bots are reachable from outside (#51) - #87

Merged
Birfy merged 1 commit into
ThinkFlowLab:mainfrom
Birfy:51-published-reach
Oct 15, 2026
Merged

Birfy merged 1 commit into
ThinkFlowLab:mainfrom
Birfy:51-published-reach

Conversation

@Birfy

@Birfy Birfy commented Oct 15, 2026

Copy link
Copy Markdown
Collaborator

Implements #51 (rewritten rule). Builds on #82.

Base: main. #82 is already merged (main = 9034e9e, the merge of #82), so this branch is one commit on top of main; no #82 branch is involved.

Rule

Other owners' and friends' access is by published Bots only. The Chief is just a Bot that is published by default; every other Bot (tree children included) is unpublished until its owner publishes it. Published Bots (Chief included) can be connected directly and pulled into rooms; unpublished Bots are unreachable from outside; a Bot that joined a room by its room code (#83) is reachable in that room only, while a member. Nothing is forced through the Chief.

What changed

Core (ds-bot/ untouched; patches/ds-bot/0013-published-reach.patch added with scripts/ds-bot-patches.mjs generate; nothing hand-edited outside the generate flow). The generator maps one file to one patch, so the new files reach.js and reach-policy.test.js are in 0013 and files that already had a patch (hooks, room-membership, authz, channel, index, types, tests) stay in their existing patches.

  • reach.js: state.bots[id].published (default false; the designated Chief is published unless the owner says otherwise; explicit true/false overrides; a Bot whose owner is not a member is never published), the reach() policy, rt.setPublished / rt.isPublished, the bot.published event, the set-published endpoint, BotView.published, flybot.bot.isPublished.
  • hooks.js: reach() is not a middleware. It runs before the bot.send chain, again on hold release, and on every deliver with a sender or remote origin (resumed, retried, deferred, re-sent), so a plugin allow/bypass cannot skip it and queued messages are judged by what is true at delivery.
  • room-membership.js: a room with a friend's Bot or several owners takes published local Bots only (assertAddable, applyParticipants); unpublishing or replacing the Chief evicts the Bot at once (evictUnshared); fanoutRoomLine checks author and recipient and sends from a Bot that may speak. viaCode ([Feature] Face-to-face group: Bots join one room by entering the same 6-digit number #83) is an internal option, never read from a request body.
  • authz.js: another owner's Bot joins only if published.

social: registers, polls and signs only for published Bots; invites/join name a published Bot; unpublishing keeps the identity unused (not revoked).
tree: a remote sender to an unpublished tree Bot is denied, to a published one it is no opinion (direct, no redirect to the root, any depth); published Bots on both sides are not the tree's call across trees or to non-tree Bots (core decides); parent/child and stopped/report unchanged; the prompt tells a child how to answer outside senders. Tests sit beside the plugin.
Docs: docs/subsystems/{team,ds-bot-core,plugin-runtime,social,tree}.md, plugin READMEs, root README(.zh), CHANGELOG.

Behaviour changes to be aware of

  • Unpublished specialists can no longer message friends' Bots or be messaged from outside; Bot Social identities are registered only for published Bots.
  • Existing identities of Bots that are unpublished are not revoked (destructive); they are just unused.
  • No client UI toggle yet, only the set-published endpoint.
  • FLYBOT_API_VERSION and package versions are not bumped; plugins treat a missing bot.isPublished as unknown (tree: no opinion; social: the older all-Bots behaviour).

Verification

Tip aeaa342 on Birfy:51-published-reach (Node 24). Run from a worktree with core's harness node_modules symlinked from an existing checkout.

  • cd core && npm test: 663 pass, 0 fail (22 new: reach-policy.test.js 14, remote-room.test.js +7, room-membership.test.js +1)
  • plugins/social npm test: 39 pass (1 new, 1 rewritten); plugins/tree npm test: 54 pass (5 new, 1 updated, incl. a real-host test)
  • node scripts/ds-bot-patches.mjs generate then check: ok (12 patches; core/ equals ds-bot/ + patches)
  • cd core && npm run build and node scripts/build-client.mjs --check: ok (no client files changed)
  • npm run lint (core): 0 errors; node scripts/lint-plugins.mjs: 0 errors; ARCH_DOCS_BASE=tfl/main npm run arch-docs: OK
  • node .github/scripts/check-pack.mjs: ok; tsc --noEmit --strict --skipLibCheck types/*.d.ts: ok
  • Fail-without-change: with reach() disabled in hooks.js, 7 of the 14 reach tests and the tree real-host test fail.
  • Not run: the smoke test, Desktop/DSH checks, and the CI workflows themselves.

Refs #51, #82.

…FlowLab#51)

A Bot is reachable from other owners and from friends only if its owner
published it. The Chief is published by default; every other Bot
(including tree children) is not. Published Bots are connected and pulled
into rooms directly, never through the Chief; a Bot that joined a room by
its room code (ThinkFlowLab#83) is reachable in that room only.

Core (patch 0013-published-reach, with the touched files staying in their
patches):
- reach.js: `published` flag, `reach()` owner policy, rt.setPublished,
  rt.isPublished, `bot.published` event, `set-published` endpoint.
- hooks.js runs reach() outside the plugin chain: before bot.send, on hold
  release and on every deliver with a sender or remote origin (resumed,
  retried, deferred, re-sent), so allow/bypass verdicts cannot skip it.
- room-membership.js: a room with a friend's Bot (or several owners) takes
  published local Bots only; unpublishing or replacing the Chief evicts the
  Bot at once; fanoutRoomLine checks author and recipient.
- authz.js: another owner's Bot joins only if published.

Plugins: @flybot/social registers, polls and signs only for published
Bots; @flybot/tree denies a friend's Bot toward an unpublished tree Bot,
reaches a published one directly (no redirect through the root).

Docs: team, ds-bot-core, plugin-runtime, social, tree subsystem pages,
READMEs, CHANGELOG.

Refs ThinkFlowLab#51, ThinkFlowLab#82.
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-15T10:14:49.854761Z aeaa342 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@Birfy
Birfy merged commit 3f957ec into ThinkFlowLab:main Oct 15, 2026
5 checks passed

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: aeaa342aed

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread core/src/host/hooks.js
Comment on lines +383 to 385
const owner = ownerPolicy(msg)
if (owner.action === 'deny') return { action: 'deny', reason: owner.reason, by: owner.by }
const verdict = await evaluate(msg)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Recheck owner reach immediately before dispatch

When a sender or target is unpublished while evaluate() is awaiting an asynchronous send middleware, this pre-chain decision becomes stale before line 395; dispatch() marks local deliveries flybotChecked and calls transports directly, so neither path asks ownerPolicy again and the message can still reach a Bot that is no longer published. Re-run the non-bypassable policy after evaluation and immediately before dispatch, including the approval-release path.

Useful? React with 👍 / 👎.

Comment thread core/src/host/reach.js
}
if (from === undefined || from === null) return ALLOW
const sender = state.bots?.[from]
if (sender === undefined) return ALLOW

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Reject delayed deliveries from removed Bots

When a resumed, retried, deferred, re-sent, or held request still carries the fromId of a Bot deleted after it was queued, sender is undefined and this returns allow. The wrapper then treats the owner policy as passed, while delivery.js accepts the missing sender record and delivers the message as coming from You, so work from a removed Bot can still execute; a non-null unknown local sender must be denied rather than treated like the owner/system no-sender case.

Useful? React with 👍 / 👎.

}
disposers.push(flybot.on('bot.created', bot => { void register(bot).catch(error => warn('register failed', error)) }))
// Publishing gives the Bot its identity; unpublishing only stops it being used.
disposers.push(flybot.on?.('bot.published', bot => { if (bot.published) void register(bot).catch(error => warn('register failed', error)) }) ?? (() => {}))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Feature-detect publication events before subscribing

When this Social plugin runs with a core that satisfies its existing ds-bot >=0.1.5-flybot.1 peer range but predates this commit, bot.isPublished is absent (which published() deliberately supports) while flybot.on still exists and throws for the unknown bot.published event. Optional-call syntax still invokes that method, so createSocial() rejects and the whole plugin becomes unavailable; only subscribe when the publication API/event is actually supported.

Useful? React with 👍 / 👎.

Comment thread core/src/host/reach.js
rt.reach = (msg = {}) => reach(state, {
from: msg.remote?.handle ? { kind: 'remote', handle: msg.remote.handle } : (msg.from ?? msg.fromId),
to: msg.to ?? msg.toId,
roomId: msg.meta?.roomId ?? msg.roomId ?? msg.fromRoomId ?? msg.deliverArgs?.roomId ?? msg.deliverArgs?.fromRoomId,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve room context on room-only replies

When a viaCode Bot sends a request from its group Session to another room member, the initial delivery carries fromRoomId and passes the room-only reach exception, but the answer path in turns.js:455-463 sends only sessionId: back. This mapper therefore supplies no roomId, sees the code-joined recipient as unpublished, and drops the reply; carry the sender room on the reply or derive it from the group Session.

Useful? React with 👍 / 👎.

priority: flybot.bot.send.priority?.APPROVAL ?? 900,
async handle(msg) {
if (msg.target?.remote !== true || (msg.to?.transport ?? 'bot-social') !== 'bot-social') return undefined
if (typeof msg.from === 'string' && !published(msg.from)) return { action: 'deny', reason: `${flybot.bot.get(msg.from)?.name ?? 'this Bot'} is not published, so it cannot message friends' Bots` }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Honor room-code sends in Social middleware

When a previously registered specialist is unpublished, re-enters a shared room via viaCode, and becomes the only local speaker, fanoutRoomLine() sends with meta.roomId and the core owner policy explicitly allows that room-scoped local-to-remote send. This unconditional publication check rejects it before transport (and sendOut() likewise hides the retained identity via activeMember), so valid room lines silently never reach the friend's participant; preserve the room-code exception through the Social layer or disallow that room state.

Useful? React with 👍 / 👎.

cursor Bot pushed a commit to Birfy/FlyBot that referenced this pull request Oct 15, 2026
…wLab#51)

Bot settings (next to Invite to this Bot) gets a Published to friends
checkbox that calls set-published { id, published }. The toggle stays
hidden until PR ThinkFlowLab#87 lands (boolean bot.published, or a missing-endpoint
error). Face-to-face join shows whether the brought Bot is published.

Co-authored-by: Danyang Chen <birfychen@gmail.com>
Birfy added a commit that referenced this pull request Oct 15, 2026
#87 registers only published Bots, so joining a face room with an
unpublished Bot failed with "has no Bot Social identity yet". Register
it on join with the same add_home_member path, keep it room-only
(unpublished, not polled, not listed for friends), and revoke that
identity when the Bot leaves its last face room.

Co-authored-by: Danyang Chen <birfychen@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant