Keep live configuration in the owning profile
将实时配置保存在所属 profile 中
Separate settings registrations duplicated schemas, defaults, persistence, and notifications. They also placed stored user settings above deployment overlays without preserving which composition source supplied a value. Using the Loader's resolved Config gives validation and consumers one owner and preserves its lifecycle decisions.
English
Problem
Separate settings registrations duplicated schemas, defaults, persistence, and notifications. They also placed stored user settings above deployment overlays without preserving which composition source supplied a value. Using the Loader's resolved Config gives validation and consumers one owner and preserves its lifecycle decisions.
Decision
A plugin declares every configurable value in its Cordis Config. Fields that can change without remounting use .volatile(); consumers read their references during operations. Settings enumerates those fields for forms and never supplies business configuration. The profile editor writes cordis.patch.yml and applies the ordinary Loader reconciliation path.
Form writes target the active profile. Home patches and command-line overlays remain higher-priority deployment inputs; an edit they would shadow fails before persistence. Two plugin instances are addressed by their distinct profile entry ids. Nested Includes retain independent ownership and are not writable through the profile form.
Cordis config patches replace complete entry configs. An edit preserves ordinary fields and unedited secrets, including raw configuration expressions. A field reset restores the currently inherited value; resetting the complete entry removes its config overrides so later bundle changes are inherited. Other edits store the entry's complete config in the profile row: the ordinary fields as composed at write time plus every volatile field. Later bundle changes to those fields do not reach that profile until the entry's config override is removed. In the shipped bundles this pins permission.presets after a default-preset change, agent-presets after a preset choice, agent-loop.agents after a parallelism edit, and web-search-deepseek.apiKeyEnv after a Web Search edit, and the client marks every volatile field of such a row as overridden, not only the edited one. Narrowing a write to the edited fields needs merge semantics for patch config, which Include does not provide.
The removed $DSH_HOME/settings.yaml is imported once into the active profile when the Loader has settled every entry after Settings starts: section ids are entry ids, with ui-developer-tools → ui-settings, ui-onboarding → ui-settings-general, and shell → the platform's shell executor entry. The file is renamed to settings.yaml.imported before the first write, so the import never repeats; a rejected section is logged and stays in the renamed file.
Alternatives considered
The rejected alternative is retaining settings as a second store behind a configuration adapter: that still requires reconciling two durable documents and deciding which wins after concurrent edits or restart. The removed global settings document offered cross-profile preferences; this design deliberately makes persisted form values profile-specific. Reintroducing shared preferences requires an explicit shared Cordis layer and a defined write destination.
Consequences
This supersedes only the non-secret settings tier in configuration source ownership; its environment restrictions remain applicable, and stored credential precedence supersedes its credential ordering. Volatile references own reference lifetime and ordinary update behavior.
Real profile tests cover multiple instances, persisted restart, invalid edits, stale revisions, higher-layer rejection, secret redaction, and configuration expressions. Browser tests exercise form writes and reset through the assembled application. Runtime reference identity is checked across live edits.
中文
问题
独立的 settings 注册重复了 schema、默认值、持久化和通知,也让已存用户设置高于部署 overlay,却未保留值来自哪个组合来源。采用 Loader 已解析的 Config,使校验与消费者共享同一所有者,并保留已有生命周期判定。
决定
插件在 Cordis Config 中声明全部可配置值。无需重新挂载即可变化的字段使用 .volatile();消费者在操作时读取其引用。Settings 只枚举这些字段并生成表单,不向业务提供配置。profile 编辑器写入 cordis.patch.yml,并通过普通 Loader 协调路径应用变化。
表单写入当前 profile。home patch 和命令行 overlay 仍是优先级更高的部署输入;如果它们会遮蔽某次修改,编辑器会在持久化前拒绝写入。两个插件实例通过各自不同的 profile entry id 寻址。嵌套 Include 保留独立所有权,不能通过该 profile 表单写入。
Cordis config patch 替换完整 entry config。编辑会保留普通字段、未编辑的秘密值和原始配置表达式。字段重置恢复当前继承值;整个 entry 重置后会移除其 config override,从而继承后续 bundle 变化。其他编辑会把该 entry 的完整 config 存入 profile 行:写入时组合出的普通字段加上全部 volatile 字段。此后 bundle 对这些字段的修改在该 entry 的 config override 被移除之前不会到达该 profile。在已发布的 bundle 中,修改默认权限预设会钉住 permission.presets,选择预设会钉住 agent-presets,编辑并行度会钉住 agent-loop.agents,编辑网页搜索会钉住 web-search-deepseek.apiKeyEnv;客户端会把这类行的每个 volatile 字段都标为已覆盖,而不只是被编辑的那个。要把写入收窄到被编辑字段需要 patch config 的合并语义,Include 不提供。
被移除的 $DSH_HOME/settings.yaml 会在 Settings 启动后、Loader 完成所有条目加载时一次性导入当前 profile:section id 即 entry id,其中 ui-developer-tools → ui-settings、ui-onboarding → ui-settings-general、shell → 当前平台的 shell 执行器条目。文件在第一次写入前改名为 settings.yaml.imported,因此导入不会重复;被拒绝的 section 会记录日志并保留在改名后的文件中。
考虑过的替代方案
保留 settings 作为配置适配器背后的第二份存储仍需要协调两份持久化文档,并决定并发修改或重启后哪份优先,因此不采用。删除的全局 settings 文档曾提供跨 profile 偏好;本设计明确将表单值持久化到各 profile。若重新引入共享偏好,必须采用显式共享 Cordis 层并明确写入目标。
后果
本决定只替代 配置来源所有权 中的非秘密 settings 层;其中的环境限制仍适用,其凭据优先级已由存储凭据优先级替代。Volatile 引用 负责引用生命周期和普通更新行为。
真实 profile 测试覆盖多实例、持久化重启、非法编辑、过期修订、高优先级层拒绝、秘密脱敏及配置表达式。浏览器测试通过完整应用执行表单写入和重置,并检查实时编辑前后的运行时引用身份。