From ed4d7e778445947b30168e9d864bff9a65760abf Mon Sep 17 00:00:00 2001 From: Yichen Jiang Date: Mon, 10 Aug 2026 23:56:12 +0800 Subject: [PATCH] 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. --- ...6-08-10-web-plugin-configuration.i18n.yaml | 4 +-- .../2026-08-10-web-plugin-configuration.md | 2 ++ .../2026-08-10-web-plugin-configuration.zh.md | 2 ++ packages/client/ui-plugin-config/package.json | 18 ++++++----- packages/client/ui-plugin-config/src/index.ts | 2 +- scripts/verify-cordis-config.ts | 30 +++++++++++++++++++ 6 files changed, 47 insertions(+), 11 deletions(-) diff --git a/.agents/notes/implemented/feature/2026-08-10-web-plugin-configuration.i18n.yaml b/.agents/notes/implemented/feature/2026-08-10-web-plugin-configuration.i18n.yaml index 94d2e8fc8e..9295085481 100644 --- a/.agents/notes/implemented/feature/2026-08-10-web-plugin-configuration.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-08-10-web-plugin-configuration.i18n.yaml @@ -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 diff --git a/.agents/notes/implemented/feature/2026-08-10-web-plugin-configuration.md b/.agents/notes/implemented/feature/2026-08-10-web-plugin-configuration.md index cfc7108d3f..ed81f94ac7 100644 --- a/.agents/notes/implemented/feature/2026-08-10-web-plugin-configuration.md +++ b/.agents/notes/implemented/feature/2026-08-10-web-plugin-configuration.md @@ -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. diff --git a/.agents/notes/implemented/feature/2026-08-10-web-plugin-configuration.zh.md b/.agents/notes/implemented/feature/2026-08-10-web-plugin-configuration.zh.md index cb6c5e0903..61b63903d5 100644 --- a/.agents/notes/implemented/feature/2026-08-10-web-plugin-configuration.zh.md +++ b/.agents/notes/implemented/feature/2026-08-10-web-plugin-configuration.zh.md @@ -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 要服务的插件。 diff --git a/packages/client/ui-plugin-config/package.json b/packages/client/ui-plugin-config/package.json index 726dde43a9..6c81334e9f 100644 --- a/packages/client/ui-plugin-config/package.json +++ b/packages/client/ui-plugin-config/package.json @@ -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", diff --git a/packages/client/ui-plugin-config/src/index.ts b/packages/client/ui-plugin-config/src/index.ts index 96ac4efa2e..0bb50fba35 100644 --- a/packages/client/ui-plugin-config/src/index.ts +++ b/packages/client/ui-plugin-config/src/index.ts @@ -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. */ diff --git a/scripts/verify-cordis-config.ts b/scripts/verify-cordis-config.ts index a70dae4d1c..3f760c9ae1 100644 --- a/scripts/verify-cordis-config.ts +++ b/scripts/verify-cordis-config.ts @@ -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 + 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. *