diff --git a/changelog.d/1520.fixed.md b/changelog.d/1520.fixed.md new file mode 100644 index 000000000..134a8fd17 --- /dev/null +++ b/changelog.d/1520.fixed.md @@ -0,0 +1 @@ +A Workflow node's `tools` field names the connectors its run Bot may use, and the Settings node inspector now says so: the editable `Tools` field stays and a note under it explains that `github` grants `mcp__github__*`, `composio` grants the Composio connector, file and shell access is always full like a normal Bot, and a node that names no connector gets none. The README, the `define_workflow` tool description and the subsystem doc say the same; applying `tools` in the spawn path is a separate change that is not merged yet, so until it lands every connector stays refused ([#1520](https://github.com/ThinkFlowLab/FlyBot/issues/1520)). diff --git a/docs/subsystems/workflow.md b/docs/subsystems/workflow.md index bd9ec9331..dff33496d 100644 --- a/docs/subsystems/workflow.md +++ b/docs/subsystems/workflow.md @@ -29,11 +29,11 @@ Everything in `plugins/workflow/`: the graph rules, the run store, the matcher, | `plugins/workflow/index.js` | Plugin entry (`flybot-workflow`). Endpoints, `run_workflow`, `define_workflow`, the board listener, and the reply middleware that ingests a step result and denies delivery to persistent Bot chats. | | `plugins/workflow/src/graph.js` | Nodes, edges, positions, and named inputs and outputs. A node starts when its forward predecessors are settled and a taken edge reaches it. Same-rank nodes run together. A forward cycle is refused. A loop edge is the only cycle. A `when` edge chooses a branch; the other is skipped. A required input with no upstream provider is refused before a run starts. | | `plugins/workflow/src/service.js` | The store, lanes (active runs, FIFO queue, terminal history), `maxConcurrent`, `rename`, `delete`, `saveGraph`, `define`, `runNamed`, `ingest`, `stepResult`. `rename` updates a workflow's name and optional description; `delete` removes the workflow, releases ephemeral Bots, and cleans logs/runs/routes. Factory template ids (e.g. `delivery`) are normal saved rows and may be deleted. A spawn hook makes one run Bot per node. Disposing the plugin ends every live run still marked `running` (nodes in flight are failed, the run is archived) and then releases every run Bot and drops those ids from the saved record only once the release has finished, handing the release back as a promise the host awaits, so a shutdown cannot persist a run as `running` with no ephemeral map and cannot leave hidden Bots that no state names (#1387, #1414). A released run Bot also gives its runner name back to the allocator instead of keeping it for the lifetime of the process. A dispatch that does not land is not marked handed. | -| `plugins/workflow/src/ephemeral.js` | Builds each node's run Bot from its prompt, role, model and tools, and releases the ids the host returns. The `tools` field is **always omitted** from `fb.bot.create` so every node Bot inherits all registered tools (GitHub, Composio, file/shell tools, and team tools limited by `EPHEMERAL_DENIED_TOOLS` at the guard layer). `node.tools` is stored as documentation only in `fields.tools` but does not restrict the grant. All node Bots get `danger-full-access` permission. | +| `plugins/workflow/src/ephemeral.js` | Builds each node's run Bot from its prompt, role, model and tools, and releases the ids the host returns. Every node Bot gets `danger-full-access`, the same file, shell and workspace-write permission a normal Bot has. `spawn` passes the node's `tools` to `fb.bot.create` once the spawn change lands, so the host records `stepTools` from them and a node that names nothing gets no connector; until that change merges the field is still omitted and every connector is refused. `node.tools` is stored in `fields.tools` and selects connectors only — never the file or shell permission. | | `plugins/workflow/src/match.js` | Which open card matches a Workflow or a local Bot. | | `plugins/workflow/src/template.js` | 发布交付流水线 (a parallel branch) and the weekly report, each node with the inputs and outputs that edge carries. | | `plugins/workflow/src/narrate.js` | Progress lines for a step. A live run quotes the Bot reply when present; otherwise delivery-template nodes may use canned copy. A join line is only for a rank of more than one node. | -| `plugins/workflow/client.js` | Sidebar section (with mini-flowcharts), workflow stage (view/control: one card per live run with its own title, node line, segment progress, status chip, and Pause / Resume / Stop; click a card or its Flow button to focus that run in the drawer flowchart; optional aggregate chip when several run or the queue is non-empty — no message composer), assign dialog (scrollable with sticky footer), flowchart drawer (collapsed by default, per-node inspector), and Settings → Workflow graphs (canvas-first with inspector panel). The product UI starts only live runs (assign, ingest). The stage shows the queue and refreshes state while runs are active; a running node uses a breathing border animation. Visual flowcharts replace text chains across all surfaces. Sidebar rows (including factory ids such as `delivery`) support Delete with confirm. The stage and dialogs portal onto `document.body`; a missing container stays in the section tree. | +| `plugins/workflow/client.js` | Sidebar section (with mini-flowcharts), workflow stage (view/control: one card per live run with its own title, node line, segment progress, status chip, and Pause / Resume / Stop; click a card or its Flow button to focus that run in the drawer flowchart; optional aggregate chip when several run or the queue is non-empty — no message composer), assign dialog (scrollable with sticky footer), flowchart drawer (collapsed by default, per-node inspector), and Settings → Workflow graphs (canvas-first with inspector panel). The product UI starts only live runs (assign, ingest). The stage shows the queue and refreshes state while runs are active; a running node uses a breathing border animation. Visual flowcharts replace text chains across all surfaces. Sidebar rows (including factory ids such as `delivery`) support Delete with confirm. The per-node inspector edits the system prompt, the role and the `Tools` field: a note under it says `tools` names the connectors the run Bot may use (`github` → `mcp__github__*`, `composio` → the Composio connector), that file and shell access is always full like a normal Bot, and that a node naming no connector gets none. The stage Details row shows `Permissions` as full access, same as a normal Bot. The stage and dialogs portal onto `document.body`; a missing container stays in the section tree. | ## Entry points @@ -49,13 +49,13 @@ Everything in `plugins/workflow/`: the graph rules, the run store, the matcher, 3. **A live run advances only when its ephemeral Bot replies, following the graph's edges in order.** That reply advances the run and may appear on the workflow stage log; it is not delivered into a persistent sidebar Bot conversation (for example Chief). The `workflow-step` send middleware denies those replies after `stepResult` runs. Nodes start only when their forward predecessors finish and edges reach them. There is no scripted or seeded run path: `workflow/state` exposes only `live === true` runs. A run does not create a schedule. `scheduleLabel` is display text. When the host can create Bots, starting a live run (`assign`, `run_workflow`, `ingest`) spawns one ephemeral Bot per node from that node's prompt, role, model and tools, sends it the step from the Main Bot, and removes every instance when the run finishes, fails for good, or is reset. A `failOnce` retry keeps them. They are hidden and left out of the sidebar. Loading the plugin does not spawn Bots for the default workflows. Plugin unload (`dispose`) and the load-time `reap` both end a live run still marked `running`: in-flight nodes are failed, the run is archived as `failed`, and any ephemeral Bot ids are released. A run already written as `running` with no ephemeral map (an older unload) is healed the same way on the next load (#1414). Loading is also the one moment a run the host left behind is noticed when its ephemeral map is still present: `reap` releases those instances and records the run as `failed`. Nothing resumes it. A run that already ended keeps its own status (#1388). `workflow/reset` stopping one live run ends it the same way before its Bots are released: nodes still `running` become `failed`, so the strip cannot keep painting a run that is over (#1424). A dispatch that does not land is not marked handed. The chat names the code, and the next hand-off tries that node again. It is not sent twice in the same hand-off. A reply that says `could not answer your message` or `model call failed` fails that node and does not start its successor. A 429 or rate-limit mention in any other reply does not. 4. Each workflow keeps a FIFO queue and up to `maxConcurrent` live runs (default 2, an integer from 1 to 8 on the record or via `workflow/concurrency`). The host runs at most 6 live runs at once. A task that does not fit is enqueued. When a run finishes, every workflow with a free slot is drained, oldest enqueue first, until the host cap. A new assign on a workflow that already has a queue waits behind it. A task does not replace a running run or release that run's Bots. Migrating an old store keeps only runs that were still `running` with ephemeral Bot ids; other legacy scripted rows are dropped from active state (finished tasks may remain in lane history). History records a finished or failed live run. Lowering the cap does not stop a run that has already started. Assigning `{ botId }` claims the card for that local Bot and does not enqueue. A remote Bot is refused. `activeCount` is `runningOf(lane)` — live runs with `live === true` and `status === 'running'`. The stage aggregate chip and the Runs tab (运行记录) must agree: every such run appears as its own running row and progress card, and the chip count does not exceed that list. 5. A node declares up to eight inputs and eight outputs. A required input is an output of the same name on an ancestor, or `task`, `title`, or `brief` on a root. A start with no provider is refused, and Bots spawned for that attempt are released. On a Bot-backed run a successor stays queued until its required inputs are present. If every forward predecessor is done, failed or skipped and an input is still missing, that node fails and the run stops. The dispatch text carries `Input : `. The Bot reports `OUTPUT : `; a single declared output may be the reply body. Nodes start in edge order. -6. Each node is a real ephemeral Bot for that run, then it is removed. A permanent sidebar Bot (`botId`) is optional and is not required for the step to run. **Every node Bot gets the same full permissions a normal sidebar Bot gets:** the `tools` field is always omitted from `fb.bot.create`, letting the Bot inherit all registered tools — file/shell tools, GitHub (`mcp__github__*`: pull requests, reviews, comments), Composio, and team tools — limited only by `EPHEMERAL_DENIED_TOOLS` at the guard/approval layer (#1466, 29 team-admin tools). `node.tools` is stored as documentation in `fields.tools` but does not restrict the grant. All node Bots get `danger-full-access` permission. A hidden run Bot cannot show an approval card, so a granted tool is allowed and a connector it did not name is refused. The host stores this on the run Bot as `stepTools` and `permission`. +6. Each node is a real ephemeral Bot for that run, then it is removed. A permanent sidebar Bot (`botId`) is optional and is not required for the step to run. **Every node Bot gets the same file, shell and workspace-write permission a normal sidebar Bot gets** (`danger-full-access`); the node's `tools` select connectors only, never that permission. `spawn` passes the node's tools, so the host records `stepTools` from them; a node that names nothing gets no connector. That spawn change is not merged at this tip yet: until it lands, no tools are passed and every connector is refused. `EPHEMERAL_DENIED_TOOLS` refuses the team tools at the guard/approval layer (#1466, 29 team-admin tools). `node.tools` is stored in `fields.tools`. A hidden run Bot cannot show an approval card, so a granted tool is allowed and a connector it did not name is refused. The host stores `permission` on the run Bot. 7. A loop edge (`loop: true`, `max` from 1 to 8, default 3, optional `until: { output, eq }`) is the only cycle. The body runs again until `until` matches or `max` runs have finished, and the forward edges wait until then. Restarting the body drops the branch choice made inside it and queues a sibling that the previous pass skipped, so the next pass can take the other side. A `when: { output, eq }` edge is taken only when that output equals `eq`. `when: { else: true }` is taken only when no other `when` matched. A node whose forward edges are all closed is `skipped` and does not block a join. A run cannot spin: every loop has a max. Settings → Workflow graphs can add a link, a condition or a loop. 8. Auto-assign considers an `open` card only. A saved Bot claim beats a workflow trigger. Otherwise first hit wins: a `workflow:` token, then `workflowId` / `workflow`, then an assignee that names a workflow, then a `bot:` token, then an assignee that names a local Bot, then the longest trigger contained in the title, note or criteria. A miss returns no route. A second ingest of the same task and target is a no-op, and a live run for that task id is not restarted. 9. The browser draws text only. Names and messages are React text children. The unit renderer asserts a hostile name is not markup. 10. `enabled: false` registers no endpoint, tool or listener and writes nothing. 11. A sidebar click opens the stage or a dialog and leaves the Workflows section in place. The stage and the dialogs portal onto `document.body`. When that container is missing they stay in the section tree; the click does not throw. -12. A node's prompt and the graph are one record. Settings → Workflow graphs (canvas-first with per-node inspector), the flowchart drawer (collapsed by default, inspector on select), and `define_workflow` all write `workflow/graph`. A run reads that record; it does not define it. After a live run finishes, the stage shows the last finished live run (`lane.last` with `live === true`) until a new live run starts. +12. A node's prompt and the graph are one record. Settings → Workflow graphs (canvas-first with per-node inspector), the flowchart drawer (collapsed by default, inspector on select), and `define_workflow` all write `workflow/graph`. A run reads that record; it does not define it. After a live run finishes, the stage shows the last finished live run (`lane.last` with `live === true`) until a new live run starts. The inspector keeps an editable `Tools` field that names the connectors the run Bot may use; it never changes the file or shell permission, and a node that names no connector gets none. ## Tests diff --git a/plugins/workflow/README.md b/plugins/workflow/README.md index ee1dc3451..8b48a26ba 100644 --- a/plugins/workflow/README.md +++ b/plugins/workflow/README.md @@ -20,7 +20,7 @@ This is not the workflow-definition engine (`workflows/.json`, #1272). This ## A live run -Assigning a task, `run_workflow`, or a Workflow match starts a live run. One hidden ephemeral Bot is created per node, used for that step, and removed when the run finishes. A node does not need a permanent sidebar Bot. `tools` on the node is what that Bot can use: `github` grants the GitHub connector (pull requests, reviews, comments), `bash` grants a shell at full access, and every step gets at least workspace write. The Main Bot sends that node its step. The node stays `running` until that Bot replies. The reply is the chat line for the step. The next node starts only after that reply, and only along the edges: every predecessor is done first, and nodes that share a rank run together. +Assigning a task, `run_workflow`, or a Workflow match starts a live run. One hidden ephemeral Bot is created per node, used for that step, and removed when the run finishes. A node does not need a permanent sidebar Bot. Every node Bot runs with `danger-full-access`, the same file, shell and workspace-write permission a normal sidebar Bot has. `tools` names the connectors its run Bot may use: `github` for `mcp__github__*`, `composio` for the Composio connector, and a node that names none gets none. The spawn change that passes `tools` to the host is not merged at this tip yet, so until it lands every connector is refused. The Main Bot sends that node its step. The node stays `running` until that Bot replies. The reply is the chat line for the step. The next node starts only after that reply, and only along the edges: every predecessor is done first, and nodes that share a rank run together. A second task does not replace a run that is already going, and it does not release that run's Bots. It waits in a FIFO queue. The stage shows a chip when more than one run is active or the queue is not empty, and it refreshes `workflow/state` while a live run exists. That refresh does not advance the graph. The product UI does not offer a canned demo replay; only live runs advance the graph. @@ -34,7 +34,7 @@ A required input must be an output of the same name on an ancestor. A root node On a live run, a successor stays queued until its required inputs are present. If every predecessor is already done or failed and an input is still missing, that node fails and the run stops. The dispatch text includes `Input : `. The Bot reports outputs as `OUTPUT : `. A node with one declared output may put that value in the reply body. -The delivery template passes `task` → `plan` → `copy` → `patch`, then `review` and `report` into `release`. Coder and Tester name `bash`; Coder, Reviewer and Deployer name `github`. The weekly template passes `task` → `notes` → `metrics` → `report`. +The delivery template passes `task` → `plan` → `copy` → `patch`, then `review` and `report` into `release`. Its Coder and Tester carry a `bash` note, and its Coder, Reviewer and Deployer carry a `github` note; only a connector name takes effect, so the `github` note is what lets those nodes reach GitHub once the spawn change lands, and a file or shell word such as `bash` decides nothing. The weekly template passes `task` → `notes` → `metrics` → `report`. ## Branches and loops @@ -53,7 +53,7 @@ A reply that says the model could not answer (`could not answer your message` or A local Bot uses the same store: - `run_workflow` — `workflow` (id or exact name) and `brief` (the task). Optional `task` id. This joins the same queue a manual assign uses. -- `define_workflow` — `name`, `nodes` (`{ id, name, prompt?, role?, model?, tools?, botId?, inputs?, outputs?, x?, y? }`), `edges` (`{ from, to, when?, loop?, max?, until? }`). `prompt` is that node's Bot system prompt. `tools` equips the ephemeral Bot (`github`, `bash`, `read`, `edit`). `botId` is optional. `inputs` and `outputs` are `{ name, type?, required? }`. `when` chooses an edge; `loop: true` with `max` is the only cycle. Pass `workflow` to replace one. A friend's Bot is refused. This writes the same graph Settings and the flowchart edit. +- `define_workflow` — `name`, `nodes` (`{ id, name, prompt?, role?, model?, tools?, botId?, inputs?, outputs?, x?, y? }`), `edges` (`{ from, to, when?, loop?, max?, until? }`). `prompt` is that node's Bot system prompt. `tools` names the connectors the ephemeral Bot may use (`github`, `composio`); file and shell access is always full, like a normal Bot, and a node that names no connector gets none. `botId` is optional. `inputs` and `outputs` are `{ name, type?, required? }`. `when` chooses an edge; `loop: true` with `max` is the only cycle. Pass `workflow` to replace one. A friend's Bot is refused. This writes the same graph Settings and the flowchart edit. ## Install @@ -79,7 +79,7 @@ Workflow 是一组本地 Bot 的群聊式运行。侧栏打开同一次运行: 把看板任务交给一个 Workflow,会开始一次真实运行,或在已有运行占满时排进 FIFO 队列。交给一个本地 Bot,只把卡片的负责人设成该 Bot,不会启动 Workflow。远程 Bot 会被拒绝。 -一次真实运行会为每个节点创建一个隐藏的临时 Bot,用完即删。节点不必绑定侧栏里的永久 Bot。`tools` 决定这个 Bot 能用什么:`github` 授予 GitHub 连接器(拉取请求、评审、评论),`bash` 授予完整权限的 shell,其余步骤至少可以写工作区。节点一直处于运行中,直到该 Bot 回复。回复写入 Workflow 聊天,下一节点才沿边开始。正在进行的运行不会被新任务替换,也不会因此释放它的 Bot。历史里只留下已完成或已失败的运行。 +一次真实运行会为每个节点创建一个隐藏的临时 Bot,用完即删。节点不必绑定侧栏里的永久 Bot。每个节点 Bot 以 `danger-full-access` 运行,与普通 Bot 同样有文件、shell 与工作区写入权限;`tools` 指定这次运行的 Bot 可用的连接器(`github` 对应 `mcp__github__*`、`composio` 对应 Composio 连接器),未声明任何连接器的节点不会得到连接器。把 `tools` 交给宿主的那处 spawn 改动尚未合入,未合入前连接器仍全部被拒。节点一直处于运行中,直到该 Bot 回复。回复写入 Workflow 聊天,下一节点才沿边开始。正在进行的运行不会被新任务替换,也不会因此释放它的 Bot。历史里只留下已完成或已失败的运行。 每个 Workflow 同时运行的上限是 `maxConcurrent`,默认 2,取 1 到 8 的整数,在创建时传入,或调用 `workflow/concurrency`。全部 Workflow 加在一起最多 6 个正在运行的任务。某个运行结束后,所有 Workflow 里最早排队、且自己还有空位的任务会开始。新的分配排在已经排队的任务后面。降低上限不会停掉已经开始的运行。舞台在详情里显示当前上限;有排队或同时多于一个运行时显示队列条。有真实运行时,页面只刷新状态。产品界面不提供预置演示重放,只有真实运行会推进图。 diff --git a/plugins/workflow/client.js b/plugins/workflow/client.js index fe7aa0254..168889133 100644 --- a/plugins/workflow/client.js +++ b/plugins/workflow/client.js @@ -63,7 +63,7 @@ window.__ModuleLoader__.load({ 'Each Bot\'s own model': '跟随各个 Bot 的模型', '{count} models': '{count} 个模型', Permissions: '权限', - 'Each Bot\'s own permission': '跟随各个 Bot 的权限', + 'Full access, same as a normal Bot': '完全访问,与普通 Bot 相同', 'On failure': '失败策略', 'Tester retries once. Any other failed step stops the run.': 'Tester 失败后自动重试 1 次。其他节点失败则停住。', 'A failed step stops the run.': '失败的节点停住,等你处理。', @@ -98,6 +98,7 @@ window.__ModuleLoader__.load({ 'Node settings': '节点设置', Role: '角色', Tools: '工具', + 'Names the connectors this run Bot may use: github grants mcp__github__*, composio grants the Composio connector. File and shell access is always full, like a normal Bot. A node that names no connector gets none. Until the spawn change that passes these tools to the host lands, every connector is still refused.': '指定这次运行的 Bot 可用的连接器:github 授予 mcp__github__*,composio 授予 Composio 连接器。文件与 shell 权限始终全量,与普通 Bot 相同;未声明任何连接器的节点不会得到连接器。在把这些 tools 传给宿主的 spawn 改动合入之前,所有连接器仍会被拒绝。', 'Inputs and outputs': '输入和输出', Delete: '删除', Rename: '重命名', @@ -678,7 +679,7 @@ window.__ModuleLoader__.load({ return h('div', { className: 'wf-box' }, h('div', { className: 'wf-kv' }, h('span', null, tr('Trigger')), h('span', null, trigger)), h('div', { className: 'wf-kv' }, h('span', null, tr('Model')), h('span', null, modelLabel)), - h('div', { className: 'wf-kv' }, h('span', null, tr('Permissions')), h('span', null, tr('Each Bot\'s own permission'))), + h('div', { className: 'wf-kv' }, h('span', null, tr('Permissions')), h('span', null, tr('Full access, same as a normal Bot'))), h('div', { className: 'wf-kv' }, h('span', null, tr('On failure')), h('span', null, tr(retries ? 'Tester retries once. Any other failed step stops the run.' : 'A failed step stops the run.'))), h('div', { className: 'wf-kv' }, h('span', null, tr('Concurrent runs')), h('span', null, String(workflow.maxConcurrent ?? 2))), h('div', { className: 'wf-kv' }, h('span', null, tr('Members')), h('span', null, workflow.chain))) @@ -927,13 +928,14 @@ window.__ModuleLoader__.load({ value: selected.role || '', onChange: (event) => updateNode(selected.id, { role: event.target.value }), })), - h('label', { className: 'wf-field' }, + h('label', { className: 'wf-field', 'data-flybot': 'node-tools' }, h('span', null, tr('Tools')), h('input', { value: (selected.tools ?? []).join(', '), - placeholder: 'github, bash, read, edit...', + placeholder: 'github, composio', onChange: (event) => updateNode(selected.id, { tools: event.target.value.split(',').map(s => s.trim()).filter(Boolean) }), - })))) : null, + }), + h('span', { className: 'wf-muted', 'data-flybot': 'node-tools-note' }, tr('Names the connectors this run Bot may use: github grants mcp__github__*, composio grants the Composio connector. File and shell access is always full, like a normal Bot. A node that names no connector gets none. Until the spawn change that passes these tools to the host lands, every connector is still refused.'))))) : null, editing && nodes.length >= 2 ? h('div', { className: 'wf-edge-form' }, h('h3', null, tr('Add link')), h('div', { className: 'wf-edge-fields' }, diff --git a/plugins/workflow/index.js b/plugins/workflow/index.js index 02c50bf57..9d3d14070 100644 --- a/plugins/workflow/index.js +++ b/plugins/workflow/index.js @@ -31,10 +31,10 @@ export function apply(ctx, config = {}) { const liveNames = new Map() const lifecycle = typeof fb.bot?.create === 'function' ? { async spawn(spec) { - // Every Workflow node Bot gets the same full tool/connector grant a normal sidebar Bot - // gets (GitHub, Composio, file/shell, team tools), limited only by EPHEMERAL_DENIED_TOOLS - // at the guard layer. spec.tools is documentation only; the tools field is always omitted - // from fb.bot.create so the Bot inherits all available tools. + // Every Workflow node Bot gets the file, shell and workspace-write permission a normal + // sidebar Bot has. spec.tools names the connectors the run Bot may use; the spawn change + // that passes them to fb.bot.create is not merged yet, so until it lands the field is + // omitted and connector-access refuses every connector. const prompt = spec.prompt let last for (let n = 0; n < 4; n += 1) { @@ -120,11 +120,11 @@ export function apply(ctx, config = {}) { })) offs.push(fb.tools.register({ name: 'define_workflow', - description: 'Create or replace a Workflow graph. nodes are { id, name, prompt?, role?, model?, tools?, botId?, inputs?, outputs?, x?, y? } and edges are { from, to, when?, loop?, max?, until? }. prompt is that node\'s Bot system prompt. tools name what the ephemeral run Bot may use (github grants mcp__github__*; bash grants a shell). A node does not need a permanent botId. inputs and outputs are { name, type?, required? }. A required input must be produced upstream, or be task, title, or brief on a root. when chooses an edge; loop with max (and optional until) is the only cycle. The Settings page and the flowchart edit the same graph. A friend\'s Bot is refused.', + description: 'Create or replace a Workflow graph. nodes are { id, name, prompt?, role?, model?, tools?, botId?, inputs?, outputs?, x?, y? } and edges are { from, to, when?, loop?, max?, until? }. prompt is that node\'s Bot system prompt. tools names the connectors the ephemeral run Bot may use (github, composio); file and shell access is always full, like a normal Bot, and a node that names no connector gets none. The spawn change that passes tools to the host is not merged yet, so until it lands every connector is refused. A node does not need a permanent botId. inputs and outputs are { name, type?, required? }. A required input must be produced upstream, or be task, title, or brief on a root. when chooses an edge; loop with max (and optional until) is the only cycle. The Settings page and the flowchart edit the same graph. A friend\'s Bot is refused.', parameters: { name: { type: 'string', required: true, description: 'Workflow name' }, workflow: { type: 'string', description: 'Existing id or name to replace. Omit to create.' }, - nodes: { type: 'array', required: true, description: 'Nodes. Each may carry prompt (system prompt), role, model, tools (github, bash, read, edit, …), inputs, outputs, and an optional local botId. Real work uses an ephemeral Bot per run, not a seated botId.' }, + nodes: { type: 'array', required: true, description: 'Nodes. Each may carry prompt (system prompt), role, model, tools (the connectors the run Bot may use, such as github or composio; file and shell access is always full, and a node that names no connector gets none — not passed to the host yet, so every connector is refused until that change lands), inputs, outputs, and an optional local botId. Real work uses an ephemeral Bot per run, not a seated botId.' }, edges: { type: 'array', required: true, description: 'Hand-offs { from, to }. A fork is two edges out; a join is two edges in. A condition is when: { output, eq } or { else: true }. A loop is loop: true with max (1–8, default 3) and optional until: { output, eq }.' }, }, execute: args => service.define(args ?? {}), diff --git a/plugins/workflow/test/client.test.js b/plugins/workflow/test/client.test.js index 8dbd6b92a..dc15e3955 100644 --- a/plugins/workflow/test/client.test.js +++ b/plugins/workflow/test/client.test.js @@ -354,6 +354,32 @@ test('settings and the flowchart save a node prompt and a link on the same graph flow.unmount() }) +test('the node inspector keeps the Tools field and explains what it selects', async () => { + const graph = delivery() + graph.nodes = graph.nodes.map(node => node.id === 'coder' ? { ...node, tools: ['github'] } : node) + const calls = [] + const call = async (name, body) => { + calls.push({ name, body }) + if (name === 'workflow/state') return { workflows: [graph] } + return { ok: true, workflows: [graph] } + } + const settings = mount(client.WorkflowSettings, { call, roster: { bots: [] } }) + await settle() + button(settings.tree, 'Edit').props.onClick() + await settle() + findAll(settings.tree, node => node.props?.['data-node'] === 'coder')[0].props.onClick() + await settle() + const tools = findAll(settings.tree, node => node.type === 'input' && /github/.test(String(node.props?.placeholder ?? '')))[0] + assert.ok(tools, 'the inspector still has an editable Tools field') + const note = findAll(settings.tree, node => node.props?.['data-flybot'] === 'node-tools-note')[0] + assert.equal(note ? textOf(note) : '', 'Names the connectors this run Bot may use: github grants mcp__github__*, composio grants the Composio connector. File and shell access is always full, like a normal Bot. A node that names no connector gets none. Until the spawn change that passes these tools to the host lands, every connector is still refused.', 'the Tools field explains it selects connectors, not the permission') + tools.props.onChange({ target: { value: 'github, composio' } }) + await settle() + const saved = calls.filter(item => item.name === 'workflow/graph').at(-1) + assert.deepEqual(saved.body.nodes.find(node => node.id === 'coder').tools, ['github', 'composio'], 'the inspector still writes node.tools') + settings.unmount() +}) + test('apply registers the sidebar section and the settings page', () => { const entries = [] const off = client.apply({