From fdb1c47896071d216b6a67265a1e2b4ed4035668 Mon Sep 17 00:00:00 2001 From: Yichen Jiang Date: Wed, 12 Aug 2026 21:41:23 +0800 Subject: [PATCH] docs(settings): scope the ordering claim and record the deferred wire gaps Card registration order is stable only for cards one package installs together: apply order between packages is unconstrained, so several external cards can still reorder between boots. The note and README said otherwise. Recorded alongside it: the redactor returns a secret reachable only through a union, intersection, or transform verbatim, and serving every registered namespace widens that gap to third-party schemas; and the headline capability still lacks an assembled-composition test. publish() now keeps its snapshot reference when neither the loaded flag nor the dispatched namespaces moved, so an unrelated settings commit no longer re-renders the card list. --- .../2026-08-12-plugin-owned-settings-surface.i18n.yaml | 4 ++-- .../2026-08-12-plugin-owned-settings-surface.md | 4 +++- .../2026-08-12-plugin-owned-settings-surface.zh.md | 4 +++- packages/client/ui-plugin-config/README.i18n.yaml | 4 ++-- packages/client/ui-plugin-config/README.md | 2 +- packages/client/ui-plugin-config/README.zh.md | 2 +- .../client/ui-plugin-config/src/client/section-store.ts | 8 ++++++++ 7 files changed, 20 insertions(+), 8 deletions(-) diff --git a/.agents/notes/implemented/architecture/2026-08-12-plugin-owned-settings-surface.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-12-plugin-owned-settings-surface.i18n.yaml index 9b3c3a967c..7dead37b13 100644 --- a/.agents/notes/implemented/architecture/2026-08-12-plugin-owned-settings-surface.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-08-12-plugin-owned-settings-surface.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/architecture/2026-08-12-plugin-owned-settings-surface.md -2026-08-12-plugin-owned-settings-surface.md: dd044659bb336a5a8add19650b968c069ce24ecc -2026-08-12-plugin-owned-settings-surface.zh.md: ebe7dc87cb595628b5a072ec923768a07797eff0 +2026-08-12-plugin-owned-settings-surface.md: 2cc78986906e83132f4401fc80c0cd601b71827c +2026-08-12-plugin-owned-settings-surface.zh.md: 8f076e2690f5eaf0129ff209883b57fe44ffedb6 diff --git a/.agents/notes/implemented/architecture/2026-08-12-plugin-owned-settings-surface.md b/.agents/notes/implemented/architecture/2026-08-12-plugin-owned-settings-surface.md index dd044659bb..2cc7898690 100644 --- a/.agents/notes/implemented/architecture/2026-08-12-plugin-owned-settings-surface.md +++ b/.agents/notes/implemented/architecture/2026-08-12-plugin-owned-settings-surface.md @@ -52,7 +52,9 @@ So the exposure this change actually adds, in this repository, is one namespace: ## Consequences -A plugin distributed outside this repository is configurable from the settings page with no change here: it registers its namespace on the Host and its card under that key in the browser, and the section pairs the two. Cards now appear in card registration order rather than by hand-assigned `order`; the Host's description order is deliberately not the display order, because plugin activation can reorder it between boots and a settings page whose cards move between visits is worse than one whose order a registrant chose. +A plugin distributed outside this repository is configurable from the settings page with no change here: it registers its namespace on the Host and its card under that key in the browser, and the section pairs the two. Cards now appear in card registration order rather than by hand-assigned `order`. That is stable for the cards this package registers, which install from one generator, and **not** stable across plugins: apply order between packages is unconstrained (`packages/client/AGENTS.md`), so several external cards can still reorder between boots. Ordering them needs an explicit key the section can sort on, which the keyed registration does not carry today. + +Deferred, and larger than this change: the redactor returns a `role('secret')` reachable only through a union, intersection, or transform verbatim (its own `TODO(settings-wire-redaction)`), and `schema.toJSON()` carries a secret's default. That gap predates this change, but serving every registered namespace widens its blast radius from schemas audited in this repository to any third-party schema, so the wire should refuse a namespace it cannot prove it can redact. Also deferred: an assembled-composition test of the headline capability — an overlay-mounted fixture plugin whose Host half registers a namespace and whose `dsh.client` half registers a card, asserted end-to-end. The current coverage proves each half separately; the shipped cards' unchanged output cannot prove the new path. The wire read the section adds is one `settings.describe` beside the per-scope reads the cards already make. Its invalidation is imprecise in one direction: the wire announces document commits and connection resets, not registrations, so a namespace registered after the section's read joins on the next commit or reconnect. diff --git a/.agents/notes/implemented/architecture/2026-08-12-plugin-owned-settings-surface.zh.md b/.agents/notes/implemented/architecture/2026-08-12-plugin-owned-settings-surface.zh.md index ebe7dc87cb..8f076e2690 100644 --- a/.agents/notes/implemented/architecture/2026-08-12-plugin-owned-settings-surface.zh.md +++ b/.agents/notes/implemented/architecture/2026-08-12-plugin-owned-settings-surface.zh.md @@ -52,7 +52,9 @@ Status: implemented ## Consequences -在本仓库之外分发的插件无需改动这里即可从设置页配置:它在 Host 上注册自己的命名空间、在浏览器里把卡片注册在该键上,由分区把两者配对。卡片现在按卡片注册顺序出现,而不再依赖手工指定的 `order`;Host 的描述顺序被刻意排除在展示顺序之外,因为插件激活时序会让它在不同次启动之间变化,而一个卡片会在两次访问之间移位的设置页,比一个顺序由注册方选定的设置页更糟。 +在本仓库之外分发的插件无需改动这里即可从设置页配置:它在 Host 上注册自己的命名空间、在浏览器里把卡片注册在该键上,由分区把两者配对。卡片现在按卡片注册顺序出现,而不再依赖手工指定的 `order`。对本包注册的这几张卡它是稳定的——它们从同一个 generator 安装;对**跨插件**的卡片它并不稳定:包与包之间的 apply 顺序是无约束的(`packages/client/AGENTS.md`),因此多个外部卡片仍可能在不同次启动之间重排。要为它们定序,需要一个 section 可排序的显式键,而 keyed 注册今天并不携带。 + +以下延后,且都大于本次改动:脱敏器对只能经由 union、intersection 或 transform 抵达的 `role('secret')` 原样返回(其自身的 `TODO(settings-wire-redaction)`),而 `schema.toJSON()` 会携带 secret 的默认值。该缺口早于本次改动,但服务每一个已注册命名空间,把它的影响面从本仓库内经审计的 schema 扩大到任意第三方 schema,因此协议应当拒绝服务它无法证明可安全脱敏的命名空间。同样延后的还有:对本次头号能力的组装态测试——用 overlay 挂载一个 fixture 插件(Host 半注册命名空间、`dsh.client` 半注册卡片)并在端到端断言。当前覆盖分别证明了两个半侧;已发卡片输出未变这一点,证明不了新路径。 分区新增的协议读取是一次 `settings.describe`,与卡片各自已有的 per-scope 读取并列。它的失效通知在一个方向上不精确:协议通告的是文档提交与连接重置,而非注册行为,因此在分区读取之后才被注册的命名空间,要等下一次提交或重连才会加入。 diff --git a/packages/client/ui-plugin-config/README.i18n.yaml b/packages/client/ui-plugin-config/README.i18n.yaml index 68ae116abf..9b9205c825 100644 --- a/packages/client/ui-plugin-config/README.i18n.yaml +++ b/packages/client/ui-plugin-config/README.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 packages/client/ui-plugin-config/README.md -README.md: 569f3a404f2b94fd6fb8dc5a4191cf66f37d55e2 -README.zh.md: 5eb96dc07ba7a438824ce6c6c3da707dd3d26285 +README.md: c4cda446b03d98eca01264a6a476999572f9e21f +README.zh.md: 9ddef246f1f80b310ac6f1b292cbd85f5306da49 diff --git a/packages/client/ui-plugin-config/README.md b/packages/client/ui-plugin-config/README.md index 569f3a404f..c4cda446b0 100644 --- a/packages/client/ui-plugin-config/README.md +++ b/packages/client/ui-plugin-config/README.md @@ -6,7 +6,7 @@ The **Plugins** settings section: one expandable card per Host plugin whose conf ## What appears here -The section reads which settings namespaces the Host serves and dispatches one slot key per namespace, so what renders is the intersection of two ledgers: the namespaces a live Host plugin registered, and the cards registered under those keys. A served namespace no card claims renders nothing — another surface owns it, or this deployment ships no browser half for it — and a card whose namespace this deployment does not serve is never dispatched, so an uncomposed plugin leaves no trace and does not hold the section back from its empty line. Cards appear in the order they registered, not the order the Host describes their namespaces — plugin activation can reorder the description between boots. The empty line waits for the Host's first answer, so an unanswered read never reads as "this deployment configures no plugin". +The section reads which settings namespaces the Host serves and dispatches one slot key per namespace, so what renders is the intersection of two ledgers: the namespaces a live Host plugin registered, and the cards registered under those keys. A served namespace no card claims renders nothing — another surface owns it, or this deployment ships no browser half for it — and a card whose namespace this deployment does not serve is never dispatched, so an uncomposed plugin leaves no trace and does not hold the section back from its empty line. Cards appear in the order they registered, which is stable for the cards one package installs together and not stable across plugins: apply order between packages is unconstrained. The empty line waits for the Host's first answer, so an unanswered read never reads as "this deployment configures no plugin". The cards this package ships cover the shell executor (`bash`), the agent loop's tool-call parallelism (`agent-loop`), and the DeepSeek search provider (`web-search-deepseek`). diff --git a/packages/client/ui-plugin-config/README.zh.md b/packages/client/ui-plugin-config/README.zh.md index 5eb96dc07b..9ddef246f1 100644 --- a/packages/client/ui-plugin-config/README.zh.md +++ b/packages/client/ui-plugin-config/README.zh.md @@ -6,7 +6,7 @@ ## 这里会出现什么 -本分区读取 Host 服务了哪些 settings 命名空间,并为每个命名空间派发一个 slot 键,因此渲染出来的是两份账本的交集:存活 Host 插件注册的命名空间,以及注册在这些键上的卡片。被服务却无人认领的命名空间什么都不渲染——它归别的界面所有,或本部署没有为它提供浏览器半侧;而命名空间未被本部署服务的卡片根本不会被派发,因此未组装的插件不留任何痕迹,也不会挡住那行空态文案。卡片按自身注册的顺序出现,而非 Host 描述其命名空间的顺序——插件激活时序会让后者在不同次启动之间变化。空态文案要等 Host 的第一次答复,因此一次尚未答复的读取绝不会被读成"本部署没有可配置的插件"。 +本分区读取 Host 服务了哪些 settings 命名空间,并为每个命名空间派发一个 slot 键,因此渲染出来的是两份账本的交集:存活 Host 插件注册的命名空间,以及注册在这些键上的卡片。被服务却无人认领的命名空间什么都不渲染——它归别的界面所有,或本部署没有为它提供浏览器半侧;而命名空间未被本部署服务的卡片根本不会被派发,因此未组装的插件不留任何痕迹,也不会挡住那行空态文案。卡片按自身注册的顺序出现;对同一个包一起安装的卡片这是稳定的,对跨插件的卡片则不稳定:包与包之间的 apply 顺序是无约束的。空态文案要等 Host 的第一次答复,因此一次尚未答复的读取绝不会被读成"本部署没有可配置的插件"。 本包自带的卡片覆盖 shell 执行器(`bash`)、agent 循环的工具调用并行度(`agent-loop`)以及 DeepSeek 搜索提供方(`web-search-deepseek`)。 diff --git a/packages/client/ui-plugin-config/src/client/section-store.ts b/packages/client/ui-plugin-config/src/client/section-store.ts index 367243822e..75e1c51aa4 100644 --- a/packages/client/ui-plugin-config/src/client/section-store.ts +++ b/packages/client/ui-plugin-config/src/client/section-store.ts @@ -110,6 +110,14 @@ export class PluginConfigSectionController { const served = new Set(this.served) const namespaces = this.entries().flatMap(entry => entry.options.key !== undefined && served.has(entry.options.key) ? [entry.options.key] : []) + const previous = this.store.getSnapshot() + // Every settings-document commit re-reads, and most of them change nothing + // this section shows. An observable source must keep its snapshot + // reference until the fact moves, or each unrelated save re-renders the + // whole card list (packages/client/AGENTS.md reactive rule 5). + if (previous.loaded === this.loaded + && previous.namespaces.length === namespaces.length + && previous.namespaces.every((ns, index) => ns === namespaces[index])) return this.store.set({ loaded: this.loaded, namespaces }) } }