From dbcea23150c8e823e7a3f636a91f7e0c35217ba2 Mon Sep 17 00:00:00 2001 From: Danyang Chen Date: Wed, 28 Oct 2026 21:35:07 +0800 Subject: [PATCH] docs(workflow): finish the Tools wording left over from #1535 (#1548) (replay of #1560) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replay of #1560 onto main ac7a916b: the original head lives on a fork we cannot push to. Docs-not-needed: docs/subsystems/workflow.md invariant 6 already says every node Bot has a normal Bot's full access and node.tools names connectors only (#1535). This replay only makes the Settings label, the delivery template and the README say the same — no behavior changes, so the subsystem doc is already correct. --- .../1548-workflow-tools-wording.changed.md | 1 + core/test/workflow-node-connectors.test.js | 4 +-- .../test/workflow-node-connectors.test.js | 4 +-- plugins/workflow/README.md | 2 +- plugins/workflow/client.js | 4 +-- plugins/workflow/src/template.js | 3 +- plugins/workflow/test/client.test.js | 36 ++++++++++++++++++- plugins/workflow/test/ephemeral.test.js | 4 +-- 8 files changed, 46 insertions(+), 12 deletions(-) create mode 100644 changelog.d/1548-workflow-tools-wording.changed.md diff --git a/changelog.d/1548-workflow-tools-wording.changed.md b/changelog.d/1548-workflow-tools-wording.changed.md new file mode 100644 index 000000000..b17d87814 --- /dev/null +++ b/changelog.d/1548-workflow-tools-wording.changed.md @@ -0,0 +1 @@ +Workflow Settings says the run's full access is a normal Bot's instead of a per-Bot permission choice, and the delivery template lists only connectors a node really calls (`github`). The README says plainly that naming `bash` grants nothing. diff --git a/core/test/workflow-node-connectors.test.js b/core/test/workflow-node-connectors.test.js index 7fd3f4dd4..8ba5bb1cf 100644 --- a/core/test/workflow-node-connectors.test.js +++ b/core/test/workflow-node-connectors.test.js @@ -132,9 +132,9 @@ test('the built-in delivery nodes make github available exactly where the node n assert.match(guardFor(bot.id, COMPOSIO[0]), /not granted to this run/, `${nodeId} may not call composio`) } - // The tester names bash only, and planner/writer name nothing: no connector for either. + // The tester, planner and writer name nothing, so none of them reaches a connector. const tester = byNode.get('tester') - assert.deepEqual(tester.stepTools, ['bash']) + assert.equal(tester.stepTools, undefined, 'the tester names no connector') assert.match(guardFor(tester.id, GITHUB[0]), /not granted to this run/) for (const nodeId of ['planner', 'writer']) { const bot = byNode.get(nodeId) 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..8ba5bb1cf 100644 --- a/patches/ds-bot/files/test/workflow-node-connectors.test.js +++ b/patches/ds-bot/files/test/workflow-node-connectors.test.js @@ -132,9 +132,9 @@ test('the built-in delivery nodes make github available exactly where the node n assert.match(guardFor(bot.id, COMPOSIO[0]), /not granted to this run/, `${nodeId} may not call composio`) } - // The tester names bash only, and planner/writer name nothing: no connector for either. + // The tester, planner and writer name nothing, so none of them reaches a connector. const tester = byNode.get('tester') - assert.deepEqual(tester.stepTools, ['bash']) + assert.equal(tester.stepTools, undefined, 'the tester names no connector') assert.match(guardFor(tester.id, GITHUB[0]), /not granted to this run/) for (const nodeId of ['planner', 'writer']) { const bot = byNode.get(nodeId) diff --git a/plugins/workflow/README.md b/plugins/workflow/README.md index 1e95a3448..dc18c7319 100644 --- a/plugins/workflow/README.md +++ b/plugins/workflow/README.md @@ -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`. Coder, Reviewer and Deployer name `github`; the Tester names none, because `bash` is not a connector and grants nothing here (#1535). The weekly template passes `task` → `notes` → `metrics` → `report`. ## Branches and loops diff --git a/plugins/workflow/client.js b/plugins/workflow/client.js index 21c1a8f61..100bce15e 100644 --- a/plugins/workflow/client.js +++ b/plugins/workflow/client.js @@ -68,7 +68,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.': '失败的节点停住,等你处理。', @@ -879,7 +879,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))) diff --git a/plugins/workflow/src/template.js b/plugins/workflow/src/template.js index 1c7411581..55f238904 100644 --- a/plugins/workflow/src/template.js +++ b/plugins/workflow/src/template.js @@ -30,7 +30,7 @@ const writer = { const coder = { id: 'coder', name: 'Coder', role: '代码实现', instructions: '根据 Planner 的子任务和 Writer 的文案修改代码,提交 PR,并在 PR 描述中列出变更。', - tools: ['github', 'bash', 'read', 'edit'], + tools: ['github'], inputs: [{ name: 'copy', type: 'text', required: true }], outputs: [{ name: 'patch', type: 'text' }], } @@ -44,7 +44,6 @@ const reviewer = { const tester = { id: 'tester', name: 'Tester', role: '自动化测试', failOnce: true, instructions: '运行单元、快照和 E2E。失败时自动重试 1 次,仍失败则停在本节点。', - tools: ['bash'], inputs: [{ name: 'patch', type: 'text', required: true }], outputs: [{ name: 'report', type: 'text' }], } diff --git a/plugins/workflow/test/client.test.js b/plugins/workflow/test/client.test.js index 3d209dd62..48c03ff71 100644 --- a/plugins/workflow/test/client.test.js +++ b/plugins/workflow/test/client.test.js @@ -1721,4 +1721,38 @@ test('#1493: a failed sibling run stays on the stage, and its row carries time a assert.ok(older, 'a history row was dropped') assert.equal(textOf(older).includes('Done'), true) view.unmount() -}) \ No newline at end of file +}) + +test('the drawer details name the permission every node Bot actually runs with', async () => { + // #1535 made every node Bot danger-full-access, so a row claiming each Bot follows + // its own permission was stale (#1548): the details must say the run's full access + // is the same as a normal Bot's, and must not claim a per-Bot choice. + const graph = delivery() + const call = async (name) => { + if (name === 'workflow/state') return { workflows: [graph], templates: [{ id: 'delivery', name: '发布交付流水线' }] } + if (name === 'workflow/messages') return { messages: graph.messages } + return { ok: true, workflows: [graph] } + } + const view = mount(client.Section, { roster: { bots: [] }, call, actions: {} }) + await settle() + findAll(view.tree, node => node.type === 'button' && textOf(node).includes('发布交付流水线'))[0].props.onClick() + findAll(view.tree, node => node.type === 'button' && textOf(node) === 'Flow')[0].props.onClick() + await settle() + findAll(view.tree, node => node.type === 'button' && textOf(node) === 'Details')[0].props.onClick() + await settle() + const rows = findAll(view.tree, node => node.props?.className === 'wf-kv') + const permission = rows.find(row => textOf(row.children?.[0] ?? '') === client.tr('Permissions')) + assert.ok(permission, 'the details list a Permissions row') + const value = textOf(permission.children[1]) + assert.doesNotMatch(value, /own permission/i, 'a node Bot does not pick its own permission') + assert.match(value, /full access/i, 'the row names the full access a node Bot runs with') + assert.match(value, /same as a normal Bot/i, 'and says it is a normal Bot\'s full access') + const prev = globalThis.__flybot__ + globalThis.__flybot__ = { t: key => (key === 'Tasks' ? '任务' : key) } + try { + assert.equal(client.tr('Full access, same as a normal Bot'), '完全访问,与普通 Bot 相同', 'the row is translated') + } finally { + globalThis.__flybot__ = prev + } + view.unmount() +}) diff --git a/plugins/workflow/test/ephemeral.test.js b/plugins/workflow/test/ephemeral.test.js index ffa4eb4cf..46ee8569e 100644 --- a/plugins/workflow/test/ephemeral.test.js +++ b/plugins/workflow/test/ephemeral.test.js @@ -789,10 +789,10 @@ test('the built-in delivery nodes hand their connector list to the host', async assert.notEqual(state?.ok, false, `${nodeId} advanced: ${state?.error ?? ''}`) } const byNode = new Map(created.map(spec => [spec.fields.nodeId, spec])) - assert.deepEqual(byNode.get('coder').tools, ['github', 'bash', 'read', 'edit'], 'Coder names github') + assert.deepEqual(byNode.get('coder').tools, ['github'], 'Coder names github') assert.deepEqual(byNode.get('reviewer').tools, ['github'], 'Reviewer names github') assert.deepEqual(byNode.get('deployer').tools, ['github'], 'Deployer names github') - assert.deepEqual(byNode.get('tester').tools, ['bash'], 'Tester names no connector') + assert.deepEqual(byNode.get('tester').tools, [], 'Tester names no connector') assert.deepEqual(byNode.get('planner').tools, [], 'Planner names no connector') for (const spec of created) assert.equal(spec.permission, 'danger-full-access', 'file and shell rights are a normal Bot\'s') })