From a5db765d178d441f88c1834da4023f15b2f6b841 Mon Sep 17 00:00:00 2001 From: Danyang Chen Date: Wed, 28 Oct 2026 21:50:56 +0800 Subject: [PATCH 1/2] fix(core): name the switch that turned a run Bot's connector off (#1543) (replay of #1554) Docs-not-needed: refusal wording only; the connector access rule and its subsystem page are unchanged. --- core/src/host/connector-access.js | 6 ++++++ core/test/connector-access.test.js | 21 +++++++++++++++++++ core/test/workflow-node-connectors.test.js | 3 ++- .../ds-bot/files/src/host/connector-access.js | 6 ++++++ .../files/test/connector-access.test.js | 21 +++++++++++++++++++ .../test/workflow-node-connectors.test.js | 3 ++- 6 files changed, 58 insertions(+), 2 deletions(-) diff --git a/core/src/host/connector-access.js b/core/src/host/connector-access.js index 1c1ca4ab1..d5cec32b7 100644 --- a/core/src/host/connector-access.js +++ b/core/src/host/connector-access.js @@ -144,6 +144,12 @@ export function install(rt) { if (allowed(bot, id)) return undefined if (bot.remote !== undefined) return `The ${id} connector is not available to a friend's Bot.` if (bot.ephemeral !== undefined) { + // #1543: a run Bot's own switch is the honest reason when the step did name the connector. The + // settings sentence below is wrong for it — a run Bot is hidden, so its owner has no page to + // flip the switch on — and the tools list cannot either; say both instead of blaming the graph. + if (turnedOff(bot, id)) { + return `The ${id} connector is turned off for ${bot.name}. A step's tools list can narrow what a run may use, but it cannot turn a connector back on.` + } // #1544: the step did name it — a Bot's switch is what refuses it, and naming it again in the // graph cannot turn it back on. const cap = capOff(bot, id) diff --git a/core/test/connector-access.test.js b/core/test/connector-access.test.js index e7479823a..708566d14 100644 --- a/core/test/connector-access.test.js +++ b/core/test/connector-access.test.js @@ -609,3 +609,24 @@ test('a run Bot does not reopen a connector the Bot that started the run switche // A Bot that no longer exists has no switch list to read: the answer stays what it was. assert.equal(guard(GITHUB[0], 'caller-gone'), undefined) }) + +// #1543: when a run Bot's own connector switch is off, the refusal must name that switch. The step's +// tools list did name the connector, so blaming it sends the reader to fix the graph instead of the +// toggle. The sentence also has to say the tools list cannot turn it back on — a run Bot is +// hidden: true and the owner cannot find it on the settings page to flip the switch there. +test('a run Bot names the switch that turned a connector off, not the step tools', async (t) => { + const state = team() + state.bots.switched = { + id: 'switched', sessionId: 'switched', name: 'Coder Off', role: 'Run', instructions: '', createdAt: 4, + hidden: true, ephemeral: { plugin: 'flybot-workflow' }, stepTools: ['github'], connectors: { github: false }, + } + const { agentFor, guard, stop } = await setup({ state }) + t.after(stop) + await agentFor('switched') + const reason = guard(GITHUB[0], 'switched') + assert.ok(reason, 'the call is still refused: this fixes the reason, not the decision') + assert.match(reason, /The github connector is turned off for Coder Off\./, 'the Bot whose switch is off is named') + assert.match(reason, /A step's tools list can narrow what a run may use, but it cannot turn a connector back on\./) + assert.doesNotMatch(reason, /not granted to this run/, 'the step tools did name it, so that is not the reason') + assert.doesNotMatch(reason, /settings/, 'a run Bot is hidden from the settings page, so do not send the reader there') +}) diff --git a/core/test/workflow-node-connectors.test.js b/core/test/workflow-node-connectors.test.js index 7fd3f4dd4..742a4e278 100644 --- a/core/test/workflow-node-connectors.test.js +++ b/core/test/workflow-node-connectors.test.js @@ -168,6 +168,7 @@ test('a node Bot still keeps full file/shell rights and the team tools stay refu ctx.emit('tools/change') await new Promise(resolve => setImmediate(resolve)) await new Promise(resolve => setImmediate(resolve)) - assert.match(guardFor(id, GITHUB[0]), /not granted to this run/) + assert.match(guardFor(id, GITHUB[0]), /The github connector is turned off for Runner ab\./) + assert.match(guardFor(id, GITHUB[0]), /cannot turn a connector back on/) assert.deepEqual(listedNow(), [], 'both connectors are hidden once the Bot switch is off') }) diff --git a/patches/ds-bot/files/src/host/connector-access.js b/patches/ds-bot/files/src/host/connector-access.js index 1c1ca4ab1..d5cec32b7 100644 --- a/patches/ds-bot/files/src/host/connector-access.js +++ b/patches/ds-bot/files/src/host/connector-access.js @@ -144,6 +144,12 @@ export function install(rt) { if (allowed(bot, id)) return undefined if (bot.remote !== undefined) return `The ${id} connector is not available to a friend's Bot.` if (bot.ephemeral !== undefined) { + // #1543: a run Bot's own switch is the honest reason when the step did name the connector. The + // settings sentence below is wrong for it — a run Bot is hidden, so its owner has no page to + // flip the switch on — and the tools list cannot either; say both instead of blaming the graph. + if (turnedOff(bot, id)) { + return `The ${id} connector is turned off for ${bot.name}. A step's tools list can narrow what a run may use, but it cannot turn a connector back on.` + } // #1544: the step did name it — a Bot's switch is what refuses it, and naming it again in the // graph cannot turn it back on. const cap = capOff(bot, id) diff --git a/patches/ds-bot/files/test/connector-access.test.js b/patches/ds-bot/files/test/connector-access.test.js index e7479823a..708566d14 100644 --- a/patches/ds-bot/files/test/connector-access.test.js +++ b/patches/ds-bot/files/test/connector-access.test.js @@ -609,3 +609,24 @@ test('a run Bot does not reopen a connector the Bot that started the run switche // A Bot that no longer exists has no switch list to read: the answer stays what it was. assert.equal(guard(GITHUB[0], 'caller-gone'), undefined) }) + +// #1543: when a run Bot's own connector switch is off, the refusal must name that switch. The step's +// tools list did name the connector, so blaming it sends the reader to fix the graph instead of the +// toggle. The sentence also has to say the tools list cannot turn it back on — a run Bot is +// hidden: true and the owner cannot find it on the settings page to flip the switch there. +test('a run Bot names the switch that turned a connector off, not the step tools', async (t) => { + const state = team() + state.bots.switched = { + id: 'switched', sessionId: 'switched', name: 'Coder Off', role: 'Run', instructions: '', createdAt: 4, + hidden: true, ephemeral: { plugin: 'flybot-workflow' }, stepTools: ['github'], connectors: { github: false }, + } + const { agentFor, guard, stop } = await setup({ state }) + t.after(stop) + await agentFor('switched') + const reason = guard(GITHUB[0], 'switched') + assert.ok(reason, 'the call is still refused: this fixes the reason, not the decision') + assert.match(reason, /The github connector is turned off for Coder Off\./, 'the Bot whose switch is off is named') + assert.match(reason, /A step's tools list can narrow what a run may use, but it cannot turn a connector back on\./) + assert.doesNotMatch(reason, /not granted to this run/, 'the step tools did name it, so that is not the reason') + assert.doesNotMatch(reason, /settings/, 'a run Bot is hidden from the settings page, so do not send the reader there') +}) diff --git a/patches/ds-bot/files/test/workflow-node-connectors.test.js b/patches/ds-bot/files/test/workflow-node-connectors.test.js index 7fd3f4dd4..742a4e278 100644 --- a/patches/ds-bot/files/test/workflow-node-connectors.test.js +++ b/patches/ds-bot/files/test/workflow-node-connectors.test.js @@ -168,6 +168,7 @@ test('a node Bot still keeps full file/shell rights and the team tools stay refu ctx.emit('tools/change') await new Promise(resolve => setImmediate(resolve)) await new Promise(resolve => setImmediate(resolve)) - assert.match(guardFor(id, GITHUB[0]), /not granted to this run/) + assert.match(guardFor(id, GITHUB[0]), /The github connector is turned off for Runner ab\./) + assert.match(guardFor(id, GITHUB[0]), /cannot turn a connector back on/) assert.deepEqual(listedNow(), [], 'both connectors are hidden once the Bot switch is off') }) From fa932f86900353bc0d916d0836f3a4060d38ed0a Mon Sep 17 00:00:00 2001 From: Danyang Chen Date: Wed, 28 Oct 2026 13:53:26 +0000 Subject: [PATCH 2/2] fix(workflow): a run started by a remote Bot gets no connectors (#1555) (#1590) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #1555 Owner: Dev2 Reviewer: Reviewer Written-by: Chief Replay of #1558: the original head is on a fork we cannot push to (maintainer_can_modify=false), so this PR carries the same change replayed onto current main; the original is closed as superseded. ## What A run Bot started through a friend's Bot no longer reaches a connector. `connector-access.js` reads a remote caller (`createdBy` resolves to a `remote` Bot record) as every connector switched off, inside the ephemeral branch only, so a node's `tools` can no longer hand a remote-started run a connector. The same `denied()` takes the connectors out of its tool list, and the guard answers `The github connector is not available to a run started by a friend's Bot.` Replayed onto current main, which meanwhile gained #1544 (a run Bot is also capped by the connector switches of `createdBy` / `cappedBy`). Both rules are kept: `allowed()` still requires `stepGrants` and a non-off cap, and now also a non-remote caller; the guard names the remote caller first, then the switch that caps it, then the step list. ## Verification At tip: `TZ=UTC node --import ./test/harness-resolve.mjs --test test/connector-access.test.js` — **23/23 pass** (main's #1544 test and the #1555 test). `node scripts/ds-bot-patches.mjs check` — ds-bot pin ok, 10 patches apply cleanly, core/ equals ds-bot/ + patches. `node core/scripts/build-client.mjs --check` — clean. Regenerated with `node scripts/ds-bot-patches.mjs generate` after editing core/. Not run: no live instance, no browser; CI not waited on. ## Checkpoint ```json { "pr": 1590, "claim": "Dev2", "issue": 1555, "summary": "A run Bot started by a friend's Bot now reaches no connector. connector-access.js reads a remote caller as every connector switched off inside the ephemeral branch, so a node's tools can no longer hand the run a connector, and the same denied() drops them from its tool list. Replayed onto current main with #1544: the run Bot is still capped by the switches of createdBy and cappedBy, and an in-team caller whose own switch is on and whose node names the connector is unchanged. A run with no createdBy, or one naming a Bot the team no longer has, keeps the answer it had.", "files": [ "core/src/host/connector-access.js", "core/test/connector-access.test.js", "docs/subsystems/core-host.md", "changelog.d/1555.fixed.md", "patches/ds-bot/files/src/host/connector-access.js" ], "checks": { "ran": "TZ=UTC node --import ./test/harness-resolve.mjs --test test/connector-access.test.js (23/23 pass); node scripts/ds-bot-patches.mjs check; node core/scripts/build-client.mjs --check", "result": "green", "at": "040de207c60cde9001a87ef0210b992760d77b0b" }, "next": "none — ready for review; original #1558 is closed as superseded" } ``` --- changelog.d/1555.fixed.md | 1 + core/src/host/connector-access.js | 17 +++++++++-- core/test/connector-access.test.js | 29 +++++++++++++++++++ docs/subsystems/core-host.md | 1 + .../ds-bot/files/src/host/connector-access.js | 17 +++++++++-- .../files/test/connector-access.test.js | 29 +++++++++++++++++++ 6 files changed, 90 insertions(+), 4 deletions(-) create mode 100644 changelog.d/1555.fixed.md diff --git a/changelog.d/1555.fixed.md b/changelog.d/1555.fixed.md new file mode 100644 index 000000000..1405898bc --- /dev/null +++ b/changelog.d/1555.fixed.md @@ -0,0 +1 @@ +A run Bot started through a friend's Bot no longer reaches a connector: the two Workflow tools have no `visibleTo`, so a remote caller could start a run whose node `tools` alone decided what it reached, and one that started a run is now read as "every connector switched off" — the guard refuses with `… not available to a run started by a friend's Bot`, and the tool mask takes the connectors out of the run Bot's list. An in-team caller whose own switch for a connector is on, and whose node names it, is unchanged, and a run with no caller (the owner, the board) keeps the answer it had. ([#1555](https://github.com/ThinkFlowLab/FlyBot/issues/1555)) diff --git a/core/src/host/connector-access.js b/core/src/host/connector-access.js index 1c1ca4ab1..2bc9f17ab 100644 --- a/core/src/host/connector-access.js +++ b/core/src/host/connector-access.js @@ -9,7 +9,8 @@ // - The owner can turn a connector off for one Bot: `bot.connectors = { composio: false }`. Absent // means on, so every Bot, old or new, has the connected connector. // - A friend's Bot (`remote`) never gets them. A run Bot (`ephemeral`) gets a connector -// only when its `stepTools` name it (`github` covers `mcp__github__*`). A Session whose +// only when its `stepTools` name it (`github` covers `mcp__github__*`), and never when a +// friend's Bot is the one that started the run (#1555). A Session whose // Bot or parent chain cannot be resolved fails closed. Sessions that belong to no Bot // (the DSH Agent, plain DSH sessions) are not touched: they are the owner's own, and no // friend can reach them. @@ -69,8 +70,17 @@ export function install(rt) { return caps } const capOff = (bot, id) => capsOf(bot).find(cap => turnedOff(cap, id)) + // #1555: `run_workflow`/`define_workflow` have no `visibleTo`, so a friend's Bot can call them and a run + // Bot can name it as the one that started the run (`createdBy`). A remote Bot has no switch table of ours + // to intersect with, so it reads as "every connector switched off": the node's `tools` cannot hand the run + // Bot a connector. A record with no `createdBy` (the owner, the board, an older run) keeps the answer it + // had, and a `createdBy` that names a Bot we do not have leaves it as it was too. + const remoteCaller = (bot) => { + const id = bot?.createdBy + return (typeof id === 'string' && id !== '' ? rt.botOf(id) : undefined)?.remote !== undefined + } const allowed = (bot, id) => bot.remote === undefined && !turnedOff(bot, id) - && (bot.ephemeral === undefined || (stepGrants(bot, id) && capOff(bot, id) === undefined)) + && (bot.ephemeral === undefined || (stepGrants(bot, id) && capOff(bot, id) === undefined && !remoteCaller(bot))) // The Bot an Agent works for: its own chat, a group Session and an earlier part resolve through // selfOf; a sub-agent Session is followed up to its parent. The answer has three shapes, and only @@ -144,6 +154,9 @@ export function install(rt) { if (allowed(bot, id)) return undefined if (bot.remote !== undefined) return `The ${id} connector is not available to a friend's Bot.` if (bot.ephemeral !== undefined) { + // #1555: the caller is the reason, not the step list — a friend's Bot started this run, and a remote + // Bot has no switch table of ours to intersect with, so it reads as every connector switched off. + if (remoteCaller(bot)) return `The ${id} connector is not available to a run started by a friend's Bot.` // #1544: the step did name it — a Bot's switch is what refuses it, and naming it again in the // graph cannot turn it back on. const cap = capOff(bot, id) diff --git a/core/test/connector-access.test.js b/core/test/connector-access.test.js index e7479823a..92a6c396a 100644 --- a/core/test/connector-access.test.js +++ b/core/test/connector-access.test.js @@ -609,3 +609,32 @@ test('a run Bot does not reopen a connector the Bot that started the run switche // A Bot that no longer exists has no switch list to read: the answer stays what it was. assert.equal(guard(GITHUB[0], 'caller-gone'), undefined) }) + +// #1555: `run_workflow` / `define_workflow` have no `visibleTo`, so a friend's Bot can call them. A run +// Bot then names that remote Bot as the one that started it, and a remote Bot has no switch table of ours +// to intersect with — so the caller reads as "every connector switched off", and the step list cannot hand +// the run Bot anything. Nothing changes for a team Bot: its own switch and the node's `tools` still decide. +test('a run Bot started by a friend\'s Bot reaches no connector', async (t) => { + const state = team() + state.bots.friend = { id: 'friend', sessionId: 'friend', name: 'Bob', role: 'Bot', instructions: '', createdAt: 5, remote: { handle: '@bob.home' } } + const run = (id, { createdBy, stepTools = ['github'] } = {}) => { + state.bots[id] = { + id, sessionId: id, name: id, role: 'Run', instructions: '', createdAt: 6, + hidden: true, ephemeral: { plugin: 'flybot-workflow' }, stepTools, + ...(createdBy === undefined ? {} : { createdBy }), + } + } + run('remote-run', { createdBy: 'friend' }) + run('team-run', { createdBy: 'chief' }) + const { agentFor, hiddenFrom, guard, stop } = await setup({ state }) + t.after(stop) + await Promise.all(['remote-run', 'team-run'].map(id => agentFor(id))) + + // Red before this change: the node names github, so the step list alone let the run Bot through. + assert.match(guard(GITHUB[0], 'remote-run'), /started by a friend's Bot/, 'the caller is the reason, not the step list') + assert.deepEqual(sorted(hiddenFrom('remote-run')), sorted([...COMPOSIO, ...GITHUB]), 'and both connectors are out of its list too') + assert.match(guard(COMPOSIO[0], 'remote-run'), /started by a friend's Bot/) + // The no-over-refusal case: a team Bot started this run, its switch is on, the node names github. + assert.equal(guard(GITHUB[0], 'team-run'), undefined, 'a team caller keeps what the node names') + assert.deepEqual(sorted(hiddenFrom('team-run')), sorted(COMPOSIO), 'only the connector the node did not name stays out') +}) diff --git a/docs/subsystems/core-host.md b/docs/subsystems/core-host.md index 93f5ef486..a70906475 100644 --- a/docs/subsystems/core-host.md +++ b/docs/subsystems/core-host.md @@ -440,6 +440,7 @@ The connector tools are mounted once for the whole DSH profile, so which Bot may **Decision (the owner, 2026-10-22, #601: 「放开,主要应该用连接器工具」; it supersedes #369's "off by default for other people's use"; one exception, #1237, is the group room's composio and schedule tools, below).** Every turn of every one of the owner's own Bots may use the connected connectors, whatever started it: the owner, a group round of any origin (also a room with a friend or another person in it), a Bot-to-Bot relay, a scheduled turn, a friend talking to a shared Bot, a turn with no recorded origin. The connector tools no longer read `turn-origin.js`. - **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. +- **A run Bot started by a friend's Bot reaches no connector (#1555).** `run_workflow` and `define_workflow` have no `visibleTo`, so a friend's Bot can call them; the run Bot's `createdBy` then names that remote Bot, which has no switch table of ours to intersect with. The ephemeral branch reads that as "every connector switched off": `allowed()` requires the step tools to name the connector **and** a caller that is not remote, so the guard refuses with `The github connector is not available to a run started by a friend's Bot.` and the same `denied()` takes the connectors out of the run Bot's tool list. A run with no `createdBy` (the owner, the board, an older run) and a `createdBy` that names a Bot the team no longer has keep the answer they had, and an in-team caller whose own switch is on, whose node names the connector, still gets it. `visibleTo` on the two tools stays backlog. - **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 mask that cannot be rebuilt keeps the previous one (#1271).** `hideConnectorTools` lifts the old mask before it reads `schemas()` again, so a `restrict()` that refuses a name would otherwise leave the Agent with no mask and every tool listed again. The rebuild now names only what the global view (`schemas()` with no Agent) can restrict, and if a restriction still fails the catch puts the previous deny list back: the guard refuses a call either way, and a list that widens by itself is a wrong answer. A denied name the Agent itself serves stays listed — `restrict()` cannot name a scope's own tool — and the warn line says so. diff --git a/patches/ds-bot/files/src/host/connector-access.js b/patches/ds-bot/files/src/host/connector-access.js index 1c1ca4ab1..2bc9f17ab 100644 --- a/patches/ds-bot/files/src/host/connector-access.js +++ b/patches/ds-bot/files/src/host/connector-access.js @@ -9,7 +9,8 @@ // - The owner can turn a connector off for one Bot: `bot.connectors = { composio: false }`. Absent // means on, so every Bot, old or new, has the connected connector. // - A friend's Bot (`remote`) never gets them. A run Bot (`ephemeral`) gets a connector -// only when its `stepTools` name it (`github` covers `mcp__github__*`). A Session whose +// only when its `stepTools` name it (`github` covers `mcp__github__*`), and never when a +// friend's Bot is the one that started the run (#1555). A Session whose // Bot or parent chain cannot be resolved fails closed. Sessions that belong to no Bot // (the DSH Agent, plain DSH sessions) are not touched: they are the owner's own, and no // friend can reach them. @@ -69,8 +70,17 @@ export function install(rt) { return caps } const capOff = (bot, id) => capsOf(bot).find(cap => turnedOff(cap, id)) + // #1555: `run_workflow`/`define_workflow` have no `visibleTo`, so a friend's Bot can call them and a run + // Bot can name it as the one that started the run (`createdBy`). A remote Bot has no switch table of ours + // to intersect with, so it reads as "every connector switched off": the node's `tools` cannot hand the run + // Bot a connector. A record with no `createdBy` (the owner, the board, an older run) keeps the answer it + // had, and a `createdBy` that names a Bot we do not have leaves it as it was too. + const remoteCaller = (bot) => { + const id = bot?.createdBy + return (typeof id === 'string' && id !== '' ? rt.botOf(id) : undefined)?.remote !== undefined + } const allowed = (bot, id) => bot.remote === undefined && !turnedOff(bot, id) - && (bot.ephemeral === undefined || (stepGrants(bot, id) && capOff(bot, id) === undefined)) + && (bot.ephemeral === undefined || (stepGrants(bot, id) && capOff(bot, id) === undefined && !remoteCaller(bot))) // The Bot an Agent works for: its own chat, a group Session and an earlier part resolve through // selfOf; a sub-agent Session is followed up to its parent. The answer has three shapes, and only @@ -144,6 +154,9 @@ export function install(rt) { if (allowed(bot, id)) return undefined if (bot.remote !== undefined) return `The ${id} connector is not available to a friend's Bot.` if (bot.ephemeral !== undefined) { + // #1555: the caller is the reason, not the step list — a friend's Bot started this run, and a remote + // Bot has no switch table of ours to intersect with, so it reads as every connector switched off. + if (remoteCaller(bot)) return `The ${id} connector is not available to a run started by a friend's Bot.` // #1544: the step did name it — a Bot's switch is what refuses it, and naming it again in the // graph cannot turn it back on. const cap = capOff(bot, id) diff --git a/patches/ds-bot/files/test/connector-access.test.js b/patches/ds-bot/files/test/connector-access.test.js index e7479823a..92a6c396a 100644 --- a/patches/ds-bot/files/test/connector-access.test.js +++ b/patches/ds-bot/files/test/connector-access.test.js @@ -609,3 +609,32 @@ test('a run Bot does not reopen a connector the Bot that started the run switche // A Bot that no longer exists has no switch list to read: the answer stays what it was. assert.equal(guard(GITHUB[0], 'caller-gone'), undefined) }) + +// #1555: `run_workflow` / `define_workflow` have no `visibleTo`, so a friend's Bot can call them. A run +// Bot then names that remote Bot as the one that started it, and a remote Bot has no switch table of ours +// to intersect with — so the caller reads as "every connector switched off", and the step list cannot hand +// the run Bot anything. Nothing changes for a team Bot: its own switch and the node's `tools` still decide. +test('a run Bot started by a friend\'s Bot reaches no connector', async (t) => { + const state = team() + state.bots.friend = { id: 'friend', sessionId: 'friend', name: 'Bob', role: 'Bot', instructions: '', createdAt: 5, remote: { handle: '@bob.home' } } + const run = (id, { createdBy, stepTools = ['github'] } = {}) => { + state.bots[id] = { + id, sessionId: id, name: id, role: 'Run', instructions: '', createdAt: 6, + hidden: true, ephemeral: { plugin: 'flybot-workflow' }, stepTools, + ...(createdBy === undefined ? {} : { createdBy }), + } + } + run('remote-run', { createdBy: 'friend' }) + run('team-run', { createdBy: 'chief' }) + const { agentFor, hiddenFrom, guard, stop } = await setup({ state }) + t.after(stop) + await Promise.all(['remote-run', 'team-run'].map(id => agentFor(id))) + + // Red before this change: the node names github, so the step list alone let the run Bot through. + assert.match(guard(GITHUB[0], 'remote-run'), /started by a friend's Bot/, 'the caller is the reason, not the step list') + assert.deepEqual(sorted(hiddenFrom('remote-run')), sorted([...COMPOSIO, ...GITHUB]), 'and both connectors are out of its list too') + assert.match(guard(COMPOSIO[0], 'remote-run'), /started by a friend's Bot/) + // The no-over-refusal case: a team Bot started this run, its switch is on, the node names github. + assert.equal(guard(GITHUB[0], 'team-run'), undefined, 'a team caller keeps what the node names') + assert.deepEqual(sorted(hiddenFrom('team-run')), sorted(COMPOSIO), 'only the connector the node did not name stays out') +})