From ad28c666bddee1e611ff98d482422d74b48d3f15 Mon Sep 17 00:00:00 2001 From: Danyang Chen Date: Mon, 26 Oct 2026 18:30:46 +0800 Subject: [PATCH 1/2] fix(host): the group-room refusal stops pointing a friend's Bot at its own chat (#1271) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The room branch of the connector guard runs before the remote branch, so a friend's Bot holding a room Session was told the connector was "available in your own chat, not in a group room" — while allowed() refuses a remote Bot in every chat. The refusal now says only what is true: not available in a group room. Refs #1271 --- changelog.d/1271-room-refusal-wording.changed.md | 1 + core/src/host/connector-access.js | 2 +- core/test/connector-access.test.js | 11 +++++++---- patches/ds-bot/files/src/host/connector-access.js | 2 +- patches/ds-bot/files/test/connector-access.test.js | 11 +++++++---- 5 files changed, 17 insertions(+), 10 deletions(-) create mode 100644 changelog.d/1271-room-refusal-wording.changed.md diff --git a/changelog.d/1271-room-refusal-wording.changed.md b/changelog.d/1271-room-refusal-wording.changed.md new file mode 100644 index 00000000..bf3d17eb --- /dev/null +++ b/changelog.d/1271-room-refusal-wording.changed.md @@ -0,0 +1 @@ +A connector call refused in a group room now says the tool is not available there, instead of pointing the caller at its own chat — a friend's Bot has no composio tools in its chat either ([#1271](https://github.com/ThinkFlowLab/FlyBot/issues/1271)). diff --git a/core/src/host/connector-access.js b/core/src/host/connector-access.js index d8bcf0c9..ae269491 100644 --- a/core/src/host/connector-access.js +++ b/core/src/host/connector-access.js @@ -101,7 +101,7 @@ export function install(rt) { // a Bot that reads it knows where the tool went, and the refusal is the same for every origin. if (roomHidden(who, name)) { return id === COMPOSIO - ? `The ${COMPOSIO} connector is available in your own chat, not in a group room (#1237).` + ? `The ${COMPOSIO} connector is not available in a group room (#1237).` : `Schedules are managed in your own chat, not in a group room (#1237).` } if (id === undefined) return undefined diff --git a/core/test/connector-access.test.js b/core/test/connector-access.test.js index ebc94db4..96165d56 100644 --- a/core/test/connector-access.test.js +++ b/core/test/connector-access.test.js @@ -139,7 +139,7 @@ test('default: a connected connector is on for every Bot, old or new, in its cha // #1237: it is the one Session that carries neither the composio tools nor the schedule ones. assert.deepEqual([...hiddenFrom('room-writer')].sort(), [...COMPOSIO].sort(), 'the mask lands with the Agent, not with the turn') const done = await turn('room-writer', { kind: 'bot', form: 'relay', role: 'room', roomId: 'room', exchangeId: 'rr', senderName: 'You', hop: 0, origin: 'owner' }) - assert.match(guard(COMPOSIO[0], 'room-writer'), /own chat, not in a group room/) + assert.match(guard(COMPOSIO[0], 'room-writer'), /not available in a group room/) assert.equal(guard(GITHUB[0], 'room-writer'), undefined, 'github stays: a room judges pull requests') done() assert.equal(guard(COMPOSIO[0], 'plain'), undefined) @@ -201,9 +201,11 @@ test('#1237: a room turn drops the composio and schedule tools and keeps github assert.deepEqual([...allHidden('room-writer')].sort(), [...COMPOSIO, ...SCHEDULE].sort(), 'and the four schedule tools, nothing else') for (const name of [...GITHUB, ...OWN]) assert.equal(allHidden('room-writer').has(name), false, `${name} stays`) for (const name of [...GITHUB, ...OWN]) assert.equal(guard(name, 'room-writer'), undefined, `${name}`) - for (const name of [...COMPOSIO, ...SCHEDULE]) assert.match(guard(name, 'room-writer'), /own chat, not in a group room/, name) + for (const name of [...SCHEDULE]) assert.match(guard(name, 'room-writer'), /own chat, not in a group room/, name) assert.equal(guard('schedule_list', 'room-writer'), 'Schedules are managed in your own chat, not in a group room (#1237).') - assert.equal(guard(COMPOSIO[0], 'room-writer'), 'The composio connector is available in your own chat, not in a group room (#1237).') + // #1271: a friend's Bot that holds a room Session is refused here too, and it has no composio tools in + // its own chat either, so the refusal no longer points there. + for (const name of [...COMPOSIO]) assert.equal(guard(name, 'room-writer'), 'The composio connector is not available in a group room (#1237).', name) }) test('#1237: a Bot\'s own chat keeps composio and the schedule tools', async (t) => { @@ -248,7 +250,8 @@ test('#601: a group round of any origin keeps the github tools, also with a frie begin('room-writer') await tick() for (const name of GITHUB) assert.equal(guard(name, 'room-writer'), undefined, `${origin}: ${name}`) - for (const name of [...COMPOSIO, ...SCHEDULE]) assert.match(guard(name, 'room-writer'), /own chat, not in a group room/, `${origin}: ${name}`) + for (const name of [...SCHEDULE]) assert.match(guard(name, 'room-writer'), /own chat, not in a group room/, `${origin}: ${name}`) + for (const name of [...COMPOSIO]) assert.match(guard(name, 'room-writer'), /not available in a group room/, `${origin}: ${name}`) assert.deepEqual([...allHidden('room-writer')].sort(), [...COMPOSIO, ...SCHEDULE].sort(), `${origin}: the mask is the same in every origin`) end('room-writer') } diff --git a/patches/ds-bot/files/src/host/connector-access.js b/patches/ds-bot/files/src/host/connector-access.js index d8bcf0c9..ae269491 100644 --- a/patches/ds-bot/files/src/host/connector-access.js +++ b/patches/ds-bot/files/src/host/connector-access.js @@ -101,7 +101,7 @@ export function install(rt) { // a Bot that reads it knows where the tool went, and the refusal is the same for every origin. if (roomHidden(who, name)) { return id === COMPOSIO - ? `The ${COMPOSIO} connector is available in your own chat, not in a group room (#1237).` + ? `The ${COMPOSIO} connector is not available in a group room (#1237).` : `Schedules are managed in your own chat, not in a group room (#1237).` } if (id === undefined) return undefined diff --git a/patches/ds-bot/files/test/connector-access.test.js b/patches/ds-bot/files/test/connector-access.test.js index ebc94db4..96165d56 100644 --- a/patches/ds-bot/files/test/connector-access.test.js +++ b/patches/ds-bot/files/test/connector-access.test.js @@ -139,7 +139,7 @@ test('default: a connected connector is on for every Bot, old or new, in its cha // #1237: it is the one Session that carries neither the composio tools nor the schedule ones. assert.deepEqual([...hiddenFrom('room-writer')].sort(), [...COMPOSIO].sort(), 'the mask lands with the Agent, not with the turn') const done = await turn('room-writer', { kind: 'bot', form: 'relay', role: 'room', roomId: 'room', exchangeId: 'rr', senderName: 'You', hop: 0, origin: 'owner' }) - assert.match(guard(COMPOSIO[0], 'room-writer'), /own chat, not in a group room/) + assert.match(guard(COMPOSIO[0], 'room-writer'), /not available in a group room/) assert.equal(guard(GITHUB[0], 'room-writer'), undefined, 'github stays: a room judges pull requests') done() assert.equal(guard(COMPOSIO[0], 'plain'), undefined) @@ -201,9 +201,11 @@ test('#1237: a room turn drops the composio and schedule tools and keeps github assert.deepEqual([...allHidden('room-writer')].sort(), [...COMPOSIO, ...SCHEDULE].sort(), 'and the four schedule tools, nothing else') for (const name of [...GITHUB, ...OWN]) assert.equal(allHidden('room-writer').has(name), false, `${name} stays`) for (const name of [...GITHUB, ...OWN]) assert.equal(guard(name, 'room-writer'), undefined, `${name}`) - for (const name of [...COMPOSIO, ...SCHEDULE]) assert.match(guard(name, 'room-writer'), /own chat, not in a group room/, name) + for (const name of [...SCHEDULE]) assert.match(guard(name, 'room-writer'), /own chat, not in a group room/, name) assert.equal(guard('schedule_list', 'room-writer'), 'Schedules are managed in your own chat, not in a group room (#1237).') - assert.equal(guard(COMPOSIO[0], 'room-writer'), 'The composio connector is available in your own chat, not in a group room (#1237).') + // #1271: a friend's Bot that holds a room Session is refused here too, and it has no composio tools in + // its own chat either, so the refusal no longer points there. + for (const name of [...COMPOSIO]) assert.equal(guard(name, 'room-writer'), 'The composio connector is not available in a group room (#1237).', name) }) test('#1237: a Bot\'s own chat keeps composio and the schedule tools', async (t) => { @@ -248,7 +250,8 @@ test('#601: a group round of any origin keeps the github tools, also with a frie begin('room-writer') await tick() for (const name of GITHUB) assert.equal(guard(name, 'room-writer'), undefined, `${origin}: ${name}`) - for (const name of [...COMPOSIO, ...SCHEDULE]) assert.match(guard(name, 'room-writer'), /own chat, not in a group room/, `${origin}: ${name}`) + for (const name of [...SCHEDULE]) assert.match(guard(name, 'room-writer'), /own chat, not in a group room/, `${origin}: ${name}`) + for (const name of [...COMPOSIO]) assert.match(guard(name, 'room-writer'), /not available in a group room/, `${origin}: ${name}`) assert.deepEqual([...allHidden('room-writer')].sort(), [...COMPOSIO, ...SCHEDULE].sort(), `${origin}: the mask is the same in every origin`) end('room-writer') } From 5cd087cfb1d6abd05fb051d24adb09cbb20b5032 Mon Sep 17 00:00:00 2001 From: Danyang Chen Date: Mon, 26 Oct 2026 18:34:05 +0800 Subject: [PATCH 2/2] docs(core-host): the room refusal no longer quotes a chat a friend's Bot lacks (#1271) arch-docs: core/src/host/connector-access.js is core-host's owned file, so the page follows the string. Same sentence the guard now returns. Refs #1271 --- docs/subsystems/core-host.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/subsystems/core-host.md b/docs/subsystems/core-host.md index 44cfb75a..e32ebf71 100644 --- a/docs/subsystems/core-host.md +++ b/docs/subsystems/core-host.md @@ -354,7 +354,7 @@ The connector tools are mounted once for the whole DSH profile, so which Bot may - **Per-Bot record** (only what differs from the default is stored): `bot.connectors = { github?: false, composio?: false }` is an explicit off for that Bot (absent = on; a legacy `true` from #346 reads as on). `bot-connectors { botId, connector, enabled }` writes it; a `scope` an older client still sends is ignored. The retired "also when others use it" offer (`bot.connectorsShared`) means nothing any more and is dropped on the Bot's next switch or on a disconnect. - **Boundaries that stay.** A friend's Bot (`bot.remote`) never gets the tools. An Agent whose Bot cannot be determined (a parent Session that is gone, or a parent chain deeper than eight) gets none either (fail closed), so an unresolved sub-agent never reads as a plain owner Session. A chain that really ends at a Session of no Bot (the DSH Agent, a plain Session) keeps them. A sub-agent Session is followed to its parent Bot (`whoseAgent`, also on `rt.whoseAgent` for `secrets.js`). - **Enforcement is two layers.** A `ctx.tools.guard` refuses every `mcp__github__*` / `mcp__composio__*` call that is not allowed, and `agent.ctx.tools.restrict({ deny })` takes the same tools out of that Agent's tool list. The mask is refreshed when the Agent is created, on every `resolveAgent` (delivery), when a switch changes, and on `tools/change` (a connector that mounts or goes away later). Turns and sharing no longer change it. DSH's hidden "Tool added/removed" notices for these tools are not drawn in the chat (`cells.js`). -- **A group room turn carries no composio tools and no schedule tools (#1237).** The eight tools — `mcp__composio__*` and `schedule_create` / `schedule_list` / `schedule_update` / `schedule_delete` — are 20,355 characters of schema (≈5.2k tokens at the measured 3.92 chars/token) on every call, and a room round uses neither: an outside app is reached from the Bot's own chat, and a schedule belongs to a Session (#628), so managing it from a member's room Session would write to the wrong place. The rule is one function in `connector-access.js`: the same `denied()` that answers the per-Bot switches also names these eight when `rt.groupOwners` — the store's Session → room index — has the Agent's Session, so the mask rides the existing refresh points (Agent created, `resolveAgent` on every delivery, a switch, `tools/change` for a plugin that mounts later) and the guard refuses a call with `… available in your own chat, not in a group room`. The github tools stay (a room judges pull requests), and the Bot's own chat, its earlier parts and its sub-agents are unchanged. Taking the github tools out too is a separate decision, in #1237's 「不做」. +- **A group room turn carries no composio tools and no schedule tools (#1237).** The eight tools — `mcp__composio__*` and `schedule_create` / `schedule_list` / `schedule_update` / `schedule_delete` — are 20,355 characters of schema (≈5.2k tokens at the measured 3.92 chars/token) on every call, and a room round uses neither: an outside app is reached from the Bot's own chat, and a schedule belongs to a Session (#628), so managing it from a member's room Session would write to the wrong place. The rule is one function in `connector-access.js`: the same `denied()` that answers the per-Bot switches also names these eight when `rt.groupOwners` — the store's Session → room index — has the Agent's Session, so the mask rides the existing refresh points (Agent created, `resolveAgent` on every delivery, a switch, `tools/change` for a plugin that mounts later) and the guard refuses a call with `… not available in a group room` — the room answer comes before the friend's-Bot one, and a friend's Bot has no composio tools in its own chat either, so the refusal does not send it there. The github tools stay (a room judges pull requests), and the Bot's own chat, its earlier parts and its sub-agents are unchanged. Taking the github tools out too is a separate decision, in #1237's 「不做」. - **Bots prefer the connector for GitHub.** The Bot rules (`prompt.js`) say to use the GitHub connector tools first and `$DSH_SECRET_GITHUB_TOKEN` with `gh` or `git` in the shell only for what they cannot do, such as `git push`. - **Defaults and disconnect.** New and duplicated Bots (and tree children) start on. The Connectors row reads "On for all N Bots", "On for K of N Bots" or "Off for every Bot"; **Choose Bots** and each Bot's **Connectors** card show one switch per Bot and connector. **Disconnect** clears every Bot's record for that connector. The switches change only through the `bot-connectors` channel endpoint, which Bots do not call; it is in `SHARED_BLOCKED`, so with more than one member it is refused like the other connector writes. - **Whose request a turn answers** (`turn-origin.js`, recorded by `turns.js`, carried by relays, posts and resume records) is still tracked: standing approvals (`approvals.js`) and resumed turns read it. It no longer decides connector tools or secrets.