fix(client-ui-plugin-config): declare the browser half under dsh.client

Merging master's rename of the client manifest field left this package on the
old `dshClient` name. The row still composed and its empty node half still
activated, but the browser roster scan never matched it, so the whole settings
section vanished with no error anywhere.

verify-cordis-config now requires a packages/client package's "./client"
export and its dsh.client declaration to agree in both directions; the
composition file cannot tell a surface plugin from a Host plugin, so the
manifests are where this is checkable. The check is scoped to that group
because a Host package's "./client" export is the typed wire face its browser
consumers import, not a plugin the roster serves.
This commit is contained in:
Yichen Jiang
2026-08-10 23:56:12 +08:00
parent 0b42259b82
commit ed4d7e7784
6 changed files with 47 additions and 11 deletions
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-10-web-plugin-configuration.md
2026-08-10-web-plugin-configuration.md: cfc7108d3f241ba91c102e32f58b0b4fc2966f0c
2026-08-10-web-plugin-configuration.zh.md: cb6c5e0903a2f3034ff365fc50dd78e4fb655a06
2026-08-10-web-plugin-configuration.md: ed81f94ac7dcc66236907b6994b850a6731bce1d
2026-08-10-web-plugin-configuration.zh.md: 61b63903d5a9974469b0acb6dd1f680aad8f95b0
@@ -45,3 +45,5 @@ A user edits the shell's command timeout and output cap, the agent loop's parall
Two costs are real. Adding a fourth plugin still requires an entry in the apiproxy allowlist, so the page's reach is a Host decision rather than a plugin's. And the plugins the web deployment moved into the agent plane — the file tools, the skills, compaction, the todo tool — appear nowhere here, which is most of what a user might expect to find; their configuration remains the preset editor's.
The bash and pwsh executors now expose `config` as a getter over a source thunk rather than a readonly field. Every read site was already per-call, so nothing else changed, but a subclass that captured `this.config` at construction would silently pin the composition entry.
`verify-cordis-config` gained one check, paid for by this branch: merging master's rename of the client manifest field (`dshClient``dsh.client`) left this package declaring the old name, and the whole section vanished from the browser with no error anywhere — the row composed, the empty node half activated, and the browser roster scan simply never matched it. Nothing could catch that, because the composition file cannot tell a surface plugin from a Host plugin: the difference lives in the manifest. The gate now requires a `packages/client` package's `./client` export and its `dsh.client` declaration to agree in both directions. The check is scoped to that group because a Host package's `./client` export is the typed wire face its browser consumers import, not a plugin the roster serves.
@@ -45,3 +45,5 @@ Status: implemented
有两项真实代价。加入第四个插件仍需要在 apiproxy 白名单里添一条,因此本页的覆盖面是 Host 的决定而非插件的决定。而 web 部署移入 agent 平面的那些插件——文件工具、技能、压缩、todo 工具——在这里一个都不出现,而它们恰恰是用户最可能期待找到的;它们的配置仍归 preset 编辑器。
bash 与 pwsh 执行器现在把 `config` 暴露为 source thunk 之上的 getter,而不再是 readonly 字段。所有读取点本就是按次读取,因此别无变化;但若某个子类在构造期捕获 `this.config`,就会悄然把组装条目钉死。
`verify-cordis-config` 新增一项检查,代价由本分支付过:合并 master 对客户端清单字段的重命名(`dshClient``dsh.client`)后,本包仍声明旧名,于是整个分区从浏览器上消失,且任何地方都不报错——行照常组装、空的 node 半侧照常激活,只是浏览器 roster 扫描永远匹配不到它。这一点无从被既有门禁发现,因为组装文件区分不了 surface 插件与 Host 插件:差别在清单里。现在门禁要求 `packages/client` 包的 `./client` 导出与 `dsh.client` 声明双向一致。之所以只限这一组:Host 包的 `./client` 导出是给浏览器消费方 import 的类型化 wire face,不是 roster 要服务的插件。
+10 -8
View File
@@ -22,14 +22,16 @@
"./src/*": "./src/*",
"./package.json": "./package.json"
},
"dshClient": {
"inject": [
"@deepseek-ai/dsh-client-connection",
"@deepseek-ai/dsh-client-locale",
"@deepseek-ai/dsh-client-runtime",
"@deepseek-ai/dsh-client-ui-settings"
],
"platform": "web"
"dsh": {
"client": {
"inject": [
"@deepseek-ai/dsh-client-connection",
"@deepseek-ai/dsh-client-locale",
"@deepseek-ai/dsh-client-runtime",
"@deepseek-ai/dsh-client-ui-settings"
],
"platform": "web"
}
},
"scripts": {
"bundle": "tsdown",
@@ -2,7 +2,7 @@
* Plugin configuration surface, node half. The empty apply exists so the
* plugin appears in the host cordis.yml / Loader; the browser half ships the
* settings section through exports["./client"], discovered from the
* package.json dshClient declaration. Every section this page edits is owned
* package.json dsh.client declaration. Every section this page edits is owned
* by the Host plugin that registered it, so this package registers no
* namespace of its own.
*/
+30
View File
@@ -79,6 +79,7 @@ errors.push(...validateExampleResolution())
errors.push(...validateAppResolution())
errors.push(...validateSourcePlaneResolution())
errors.push(...validatePresetPlaneSeparation())
errors.push(...validateClientHalvesDeclared())
if (errors.length > 0) {
console.error('verify-cordis-config: invalid Loader metadata or plugin package resolution:')
@@ -88,6 +89,35 @@ if (errors.length > 0) {
console.log(`verify-cordis-config: ${files.length} config files passed.`)
}
/**
* A browser plugin must declare the browser half it ships.
*
* The browser roster is discovered by scanning composed packages for a
* `dsh.client` block, and the node half of a surface plugin is an empty
* `apply`. A `packages/client` package that exports `./client` without that
* block therefore composes, activates, and contributes nothing — its bundle is
* never served and no error is raised anywhere. The mismatch is invisible in
* the composition file, so it is checked against the manifests instead. Only
* this group is checked: a Host package's `./client` export is the typed wire
* face its browser consumers import, not a plugin the roster serves.
* @returns one violation per client package whose `./client` export and
* `dsh.client` declaration disagree.
*/
function validateClientHalvesDeclared(): string[] {
return globSync('packages/client/*/package.json', { cwd: root }).flatMap((manifestPath) => {
const manifest = readManifest(manifestPath) as PackageManifest & {
exports?: Record<string, unknown>
dsh?: { client?: unknown }
}
const shipsClient = manifest.exports !== undefined && Object.hasOwn(manifest.exports, './client')
const declaresClient = manifest.dsh?.client !== undefined
if (shipsClient === declaresClient) return []
return [shipsClient
? `${manifestPath}: exports "./client" but declares no dsh.client, so its browser half is never served`
: `${manifestPath}: declares dsh.client but exports no "./client" entry to serve`]
})
}
/**
* No shipped agent preset may repeat a row the host composition still runs.
*