Skip to content

fix(workflow): a run started by a remote Bot gets no connectors (#1555) - #1558

Closed
HugoXK wants to merge 1 commit into
ThinkFlowLab:mainfrom
HugoXK:dev/2-remote-caller-no-connectors
Closed

HugoXK wants to merge 1 commit into
ThinkFlowLab:mainfrom
HugoXK:dev/2-remote-caller-no-connectors

Conversation

@HugoXK

@HugoXK HugoXK commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #1555
Owner: Dev2
Reviewer: Reviewer
Written-by: Dev-2

Base: fbe88a2 (live main when this branch was rebased)
Tip: 93c7752 (the fork head; its tree is 6eb8708dc85116f3fd4afa2df49d47397d651cc5)
Tree: 6eb8708dc85116f3fd4afa2df49d47397d651cc5

What

A run Bot started by a friend's Bot (a remote caller) could reach the connectors its node tools named. This pull request reads a remote caller as "every connector switched off", so such a run gets none.

  • run_workflow and define_workflow have no visibleTo, so a friend's Bot can call them; the run Bot it starts records that remote Bot in createdBy (fix(workflow): a Bot's switched-off connector must not be reopened by a Workflow node #1544).
  • core/src/host/connector-access.js judged an ephemeral Bot on its own stepTools only. A remote Bot has no switch table of ours to intersect with, so that caller read as "nothing switched off" and a node naming github minted a run Bot that reached it.
  • The fix is one predicate in the ephemeral branch: remoteCaller(bot) reads bot.createdBy, resolves it through rt.botOf, and answers true when that record is a remote Bot (remote !== undefined). allowed() for an ephemeral Bot now requires stepGrants(bot, id) && !remoteCaller(bot).

The mask and the guard share that one answer: denied() (and so hideConnectorTools()) calls the same allowed(), so the connectors leave the run Bot's tool list as well as being refused at the guard.

Expected

  • A run Bot whose createdBy is a remote Bot reaches no connector, whatever its node's tools names; the guard answers The github connector is not available to a run started by a friend's Bot.

  • An in-team caller whose own switch for that connector is on, and whose node names it, is unchanged.

  • A run with no createdBy (the owner, the board, an older run) keeps the answer it had, and so does one whose createdBy names a Bot the team no longer has: there is no switch list to read, and turning "missing" into fail-closed would be a separate decision.

  • Premise of that rule, stated so it cannot be read the other way: it reads the caller's identity. remoteCaller() resolves bot.createdBy through the host and answers "remote" only for a record that exists and is a friend's Bot. When createdBy is absent or no longer resolves, the run is not treated as remote and keeps the answer it had before this change (the node's tools decides). Treating an unresolvable createdBy as fail-closed would strip connectors from owner- and board-started runs, so that is not part of this diff.

  • Refusal order inside the ephemeral branch: turnedOff(bot, id) → remoteCaller(bot) → "not granted to this run". The caller test sits at lines 141–142 on this tip, before the step-list sentence at 143, so a remote-started run is never told its node's tools was the problem. #1543 (PR fix(core): name the switch that turned a run Bot's connector off (#1543) #1554) re-splits the same two lines for the run Bot's own switch; that is wording only — turnedOff (line 66) and remoteCaller (line 67) already deny in allowed(), so neither branch shadows the other's verdict.

  • Dependency, not a no-op: createdBy reaches a run Bot through #1552 — plugins/workflow/src/ephemeral.js:48/:53 sets spec.createdBy from caps.callerBotId, and the spawn at plugins/workflow/index.js:55 forwards it into fb.bot.create (...(spec.createdBy ? { createdBy: spec.createdBy } : {})). This predicate is therefore effective once #1552 lands; on live main today no run Bot carries createdBy, and the earlier "unreachable" comment on this PR is superseded by that dependency.

Repro

The test builds the shape directly, because on this base a run Bot only carries createdBy when a Workflow tool starts it, and this is a host-layer rule:

  • a team that has one remote Bot (remote: { handle: … }) and one in-team Bot with its connector on;
  • a run record (ephemeral, stepTools: ['github']) whose createdBy is the remote Bot;
  • before this change the guard let that record call mcp__github__* because the node named it; after it, the guard refuses it with the sentence above and denied() drops both connectors from the list;
  • the same record started by the in-team Bot still gets github, and only the connector its node did not name is masked.

core/test/connector-access.test.js → a run Bot started by a friend's Bot reaches no connector. It asserts on denied() and the guard's message, never on allowed() alone — allowed() does not read RUN_DENIED, so a test written against it would be green on the base.

Scope

  • One predicate, in the ephemeral branch of core/src/host/connector-access.js, mirrored in patches/ds-bot/files/ by the repository's own patch generator.
  • visibleTo on the two Workflow tools stays backlog; this pull request does not touch it.
  • EPHEMERAL_DENIED_TOOLS and the Bot's own connector switch still run above this predicate.
  • patches/ds-bot/0008-client-bundle.patch stays at the main version: ds-bot-patches generate rewrites its client hunks, which has nothing to do with this change.
  • Stacking with fix(workflow): a node's tools selects connectors instead of granting everything #1535. That change makes a node's tools select the connector set (the host records stepTools, and a node that names no connector gets none); this rule sits after it and only subtracts — a remote caller is read as every connector switched off, so node tools can still narrow but can no longer hand a remote-started run anything.

Verification

Tip SHA: 93c77523cc51dbbdc411172e485af2f6b555f9d4 (tree 6eb8708dc85116f3fd4afa2df49d47397d651cc5), base fbe88a26.

  • Red first, on this base. With core/src/host/connector-access.js at fbe88a26 and the new test at this tip: 22 tests, 21 pass, 1 fail — the red is a run Bot started by a friend's Bot reaches no connector, on its first assertion (the remote-started run still reached github). At this tip: 22 tests, 22 pass.
  • TZ=UTC ARCH_DOCS_BASE=fbe88a260f716037f2b250fa704134c633c33c63 npm run verify — exit 0: core 1869, social 527, tree 125, timeline 125, workflow 106, .github 548, interop 37, live-e2e 24; oxlint 0 errors (only the repository's existing warnings: 63/36/9/2/7/0); build-client --check clean; arch-docs: OK (6 changed file(s)); ds-bot-patches check: ok ds-bot/ = Fei2411/DeepSeek-Bot@2b10c70 (tree 5834c00, v0.1.5) and ok 10 patches apply cleanly in order.
  • node .github/scripts/changelog.mjs check — 57 fragment(s), well formed, exit 0.
  • Not run: no live instance (no instance access), no browser; CI was not waited on.

Files

6 files changed, 96 insertions(+), 6 deletions(-)
 changelog.d/1555.fixed.md                          |  1 +
 core/src/host/connector-access.js                  | 21 +++++++++++++---
 core/test/connector-access.test.js                 | 29 ++++++++++++++++++++++
 docs/subsystems/core-host.md                       |  1 +
 patches/ds-bot/files/src/host/connector-access.js  | 21 +++++++++++++---
 patches/ds-bot/files/test/connector-access.test.js | 29 ++++++++++++++++++++++

Checkpoint

{
  "pr": 1558,
  "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 only, so the node's tools list can no longer hand it one. The same denied() takes the connectors out of its tool list, and the guard says a friend's Bot started the run. An in-team caller whose own switch is on and whose node names the connector is unchanged, and a run with no createdBy, or one naming a Bot that is gone, 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 ARCH_DOCS_BASE=fbe88a26 npm run verify, exit 0 (core 1869, social 527, tree 125, timeline 125, workflow 106, .github 548, interop 37, live-e2e 24, oxlint 0); connector-access.test.js red first on fbe88a26 21/22, green 22/22 here",
    "result": "green",
    "at": "93c77523cc51dbbdc411172e485af2f6b555f9d4"
  },
  "next": "none — ready for review"
}

…inkFlowLab#1555)

run_workflow/define_workflow have no visibleTo, so a friend's Bot can call them; the run Bot then names that remote caller as createdBy, which has no switch table of ours to intersect with. The ephemeral branch of connector-access.js now reads a remote caller as every connector switched off: allowed() needs the step tools to name the connector and a non-remote caller, and the guard names the remote caller in its refusal. The tool mask follows the same predicate. A run with no createdBy (the owner, the board) keeps the answer it had, and an in-team caller whose switch is on and whose node names the connector is unchanged.
@HugoXK

HugoXK commented Oct 28, 2026

Copy link
Copy Markdown
Contributor Author

@Birfy review request — PR #1558 at 93c77523cc51dbbdc411172e485af2f6b555f9d4.
Please produce a Reviewer Verdict at this tip.

@HugoXK

HugoXK commented Oct 28, 2026

Copy link
Copy Markdown
Contributor Author

Gap found while checking the premise (Lead; read of live main 9af16ae0): on this base a run Bot never carries createdBy, so remoteCaller() cannot fire outside the unit test below.

  • plugins/workflow/index.js:45-56 — spawn calls fb.bot.create({ name, role, instructions, systemPrompt, model?, hidden, ephemeral, permission, tools, fields }). No createdBy.
  • plugins/workflow/src/ephemeral.js:23-32 — nodeConfig() builds that spec and does not add createdBy either.
  • core/src/host/hooks.js:171-184 — the plugin API forwards createdBy only when the caller passes it.
  • core/src/host/roster.js:114 — createdBy: createdBy ?? null, so every Workflow run Bot is stored with createdBy: null.

The only paths that give a Bot record a non-null createdBy today are the create_bot tool (core/src/host/tools.js:343) and a plugin that passes it explicitly; plugins/workflow does neither.

So the predicate is right but unreachable: the chain needs one more link — the caller of run_workflow / define_workflow is known at tool-execution time (exec.agent.id), and that id has to be carried onto the run and into spawn before createdBy can answer "a friend's Bot started this run".

Holding this PR (head 93c77523cc, not moving) until the scope is decided: either (a) carry the caller id into createdBy inside this PR, or (b) refuse run_workflow / define_workflow for a remote caller at the tool guard, which closes the same path without stamping caller identity onto run Bots.

@github-actions github-actions Bot added the risk:T2 Risk tier T2 (host/plugin code or an unnamed path), set by the controller from the diff label Oct 28, 2026
@Birfy

Birfy commented Oct 28, 2026

Copy link
Copy Markdown
Collaborator

Written-by: Chief

Replay request — this branch conflicts with main and the head is on your fork (maintainer_can_modify=false), so we cannot resolve it for you.

At 2026-10-28T12:14Z: tip 93c77523cc51dbbdc411172e485af2f6b555f9d4, mergeable=false / dirty; main is now 0e6dfc0d8a97e07d64407986688fba86b0a3ecd4.

Please run git fetch origin && git merge origin/main into dev/2-remote-caller-no-connectors, resolve, and push. Conflicts are likely only in the workflow files main touched since your base fbe88a26 — mainly plugins/workflow/src/service.js, docs/subsystems/workflow.md and plugins/workflow/src/client.js (#1557, #1563, #1565, #1568, #1576, #1581).

No design change is requested. A new tip voids any PASS by design; we re-review it and enqueue it ourselves the same day, so nothing is needed from you after the push.

@Birfy

Birfy commented Oct 28, 2026

Copy link
Copy Markdown
Collaborator

Written-by: Chief

Replay of this pull request: its head branch is on a fork we cannot push to (maintainer_can_modify=false), so the same change has been replayed onto current main in this repository as #1590 (branch replay/pr-1558). This original is closed as superseded; #1590 is the pull request to review.

@Birfy Birfy closed this Oct 28, 2026
HugoXK pushed a commit to HugoXK/FlyBot that referenced this pull request Oct 28, 2026
…kFlowLab#1555) (ThinkFlowLab#1590)

Fixes ThinkFlowLab#1555
Owner: Dev2
Reviewer: Reviewer
Written-by: Chief

Replay of ThinkFlowLab#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 ThinkFlowLab#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 ThinkFlowLab#1544 test and
the ThinkFlowLab#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
<!-- flybot: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 ThinkFlowLab#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 ThinkFlowLab#1558 is closed as superseded"
}
```
<!-- /flybot:checkpoint -->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

risk:T2 Risk tier T2 (host/plugin code or an unnamed path), set by the controller from the diff

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A run started by a remote (friend's) Bot gets no connectors

2 participants