fix(subagents): apply the delegation opt-in to collaboration spawn (#386) - #407
Conversation
…astsa#386) `session/collaboration/spawn` picked its worker model like this: const chosen = requested ? models.find((item) => item.key === requested) : models.find((item) => item.availableForSubagents) ?? models.find((item) => item.isDefault); The `availableForSubagents` in the inheritance branch is the tell: the flag was meant to govern here. The `requested` branch never consults it, so a `modelKey` naming any configured model was honored — including one the user deliberately left unticked in model settings. `listReadyPluginModels` reports the flag but does not filter, and a plugin is not where this is decided, so nothing enforced it at all. The named branch now needs the model's own opt-in and answers `PERMISSION_DENIED` before a worker exists. Inheritance is untouched: omitting `modelKey` still takes the first opted-in model and otherwise the default, and naming the default model's own key is that inheritance written out, the way repeating a definition's own pin is on the Task path. The Task path this issue reports was closed by vastsa#305 and vastsa#318 and ships in v0.14.8; this is the one remaining entry point the report's second expectation covers. Specs, both zh-CN mirrors, the E2E scenario and the ADR record the widened scope.
|
On the red CI here, before anyone spends time on it:
I am flagging it rather than staying quiet because this PR does touch that file and its zh-CN mirror, so the honest thing is to say so up front. The edits add table rows to neither side, and the failing list is one entry long on both trees — the same one. I deliberately did not fold an unrelated docs repair into a behaviour fix; say the word if you would rather I did. The other red mark, For what it is worth on the JS side of that job: |
|
Brought up to date with
On the merged head: |
Closes #386
Summary
The Task path this issue reports is already closed. #305 and #318 landed the
subagentModelKeysseparation on 2026-09-13 and ship in v0.14.8: a pin theuser never ticked stays out of the delegation catalog and is refused as another
delegate's
Task.model. I re-verified that onmain@4e2e0055beforetouching anything — evidence below.
What is still open is the report's second expectation — 「即使通过参数或其他入口
手动传入该模型,运行时也应拒绝请求」 — for one other entry point.
session/collaboration/spawnpicks its worker model like this(
apps/desktop/electron/main/services/session-collaboration.ts):The
availableForSubagentsin the inheritance branch is the tell: the flag wasmeant to govern here. The
requestedbranch never consults it, so amodelKeynaming any configured, ready model is honored — including one the user
deliberately left unticked in model settings.
listReadyPluginModelsreportsthe flag but does not filter, and a plugin is not where this can be decided, so
in practice nothing enforced it.
That is the same bypass #386 describes, through the second door: a worker the
agent starts is AI-driven delegation, and the Session Orchestrator asks for it
on the agent's behalf.
The change
The named branch now needs the model's own opt-in and answers
PERMISSION_DENIEDbefore a worker exists.Inheritance is deliberately untouched, so nobody who was not bypassing the
toggle sees a difference:
modelKeystill takes the first opted-in model, and otherwise thedefault;
same allowance the Task path already makes for repeating a definition's own
pin, and for the exact parent provider/model.
Eight lines in one file. Everything else in the diff is the specs, both zh-CN
mirrors, the E2E scenario and the ADR recording the widened scope, per
AGENTS.md§11.Not changed
listReadyPluginModelsstill returns every ready model with itsavailableForSubagentsflag. It backsmodels.listandagent.completetoo,where the flag is irrelevant, and filtering there would only move the decision
to a caller that cannot be trusted with it. Main is the authoritative side, so
main is where the gate went.
Evidence
Four tests in
apps/desktop/test/session-collaboration-service.test.mjs. Theyload the real
plugin-agent-completehelpers rather than a stub, so the flag isproved to survive the whole way from
providers.listthrough the plugin modellist into the spawn decision.
The refusal test fails on the merge base and passes here; the other three are
controls that pass on both, which is what shows the change costs no existing
behaviour.
Base source (
origin/main@4e2e0055, this branch's tests):This branch:
That the Task path is already fixed, re-verified on
main@4e2e0055:Validation
pnpm build:jspnpm typecheckpnpm lintpnpm -r --if-present --no-bail testpnpm test:e2e:subagent-modelspnpm test:e2e:collaborationpnpm test:e2ecargo fmt/cargo test -p host-core/cargo clippypnpm test:e2e:collaborationandpnpm test:e2eboth stop athost binary missing … pi-desktop-host-core, and there is no Rust toolchain on the machine Ivalidated on, so the host cannot be built here. Both are the suites that would
exercise this change end to end, and neither ran. The unit coverage above drives
the real
createSessionCollaborationServiceagainst the real model helpers,which is the whole decision under change, but it does not replace those runs.
Remaining risk sits there and with the post-integration E2E
AGENTS.md§15requires.
docs testfails oncheck-locales:06-delivery/04-e2e-test-plan.mdhas 97table rows in English and 95 in Chinese, first diverging at row 90. This
reproduces unchanged on
main@4e2e0055— same file, same counts, samedivergence index — and my edits to that pair add no table rows. Not fixed here:
it is an unrelated pre-existing drift and folding it in would bury this change.
中文摘要
Issue 里报告的 Task 路径已经修好了:#305 和 #318 于 2026-09-13 合并,随 v0.14.8
发布,未勾选的固定模型既不会进入委托目录,也不会被接受为其他子代理的
Task.model。我在动手之前先在main@4e2e0055上复验了这一点。仍然存在的是本 issue 第二条预期所说的「其他入口」:
session/collaboration/spawn显式传入modelKey时完全不检查availableForSubagents,因此未勾选的模型照样能被派发。同一个表达式的继承分支是查了这个标志的,说明作者本来就打算让它在这里生效。
本 PR 让显式指定的
modelKey必须带有该模型自己的许可,否则在创建 worker 之前返回
PERMISSION_DENIED。继承行为不变:省略modelKey仍然优先取已勾选的模型、其次取默认模型;显式写出默认模型自己的键同样按继承处理,与 Task 路径上「重复定义
自己的固定键等于省略」的规则一致。