@orion-agents/orion-code 0.2.2 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (217) hide show
  1. package/CHANGELOG.md +82 -0
  2. package/README.md +89 -14
  3. package/README.zh-CN.md +77 -14
  4. package/bin/orion +15 -4
  5. package/dist/cli.js +51 -172
  6. package/dist/cli.js.map +1 -1
  7. package/dist/commands/model-command-handlers.d.ts +2 -2
  8. package/dist/commands/model-command-handlers.d.ts.map +1 -1
  9. package/dist/commands/model-command-handlers.js +113 -32
  10. package/dist/commands/model-command-handlers.js.map +1 -1
  11. package/dist/commands/session-command-handlers.d.ts.map +1 -1
  12. package/dist/commands/session-command-handlers.js +106 -15
  13. package/dist/commands/session-command-handlers.js.map +1 -1
  14. package/dist/commands/types.d.ts +13 -3
  15. package/dist/commands/types.d.ts.map +1 -1
  16. package/dist/commands/types.js +1 -1
  17. package/dist/commands/types.js.map +1 -1
  18. package/dist/core/tool-artifacts.d.ts +2 -0
  19. package/dist/core/tool-artifacts.d.ts.map +1 -1
  20. package/dist/core/tool-artifacts.js +27 -1
  21. package/dist/core/tool-artifacts.js.map +1 -1
  22. package/dist/index.d.ts +1 -1
  23. package/dist/index.js +1 -1
  24. package/dist/runtime/agent-loop.d.ts.map +1 -1
  25. package/dist/runtime/agent-loop.js +3 -3
  26. package/dist/runtime/agent-loop.js.map +1 -1
  27. package/dist/runtime/agent-runtime-controller.d.ts +32 -1
  28. package/dist/runtime/agent-runtime-controller.d.ts.map +1 -1
  29. package/dist/runtime/agent-runtime-controller.js +241 -27
  30. package/dist/runtime/agent-runtime-controller.js.map +1 -1
  31. package/dist/runtime/agent-runtime-protocol.d.ts +4 -0
  32. package/dist/runtime/agent-runtime-protocol.d.ts.map +1 -1
  33. package/dist/runtime/agent-runtime-protocol.js.map +1 -1
  34. package/dist/runtime/agent-runtime-runner.d.ts +2 -1
  35. package/dist/runtime/agent-runtime-runner.d.ts.map +1 -1
  36. package/dist/runtime/durable-tool-receipt-reader.d.ts +32 -0
  37. package/dist/runtime/durable-tool-receipt-reader.d.ts.map +1 -0
  38. package/dist/runtime/durable-tool-receipt-reader.js +130 -0
  39. package/dist/runtime/durable-tool-receipt-reader.js.map +1 -0
  40. package/dist/runtime/legacy-thread-materializer.d.ts +31 -0
  41. package/dist/runtime/legacy-thread-materializer.d.ts.map +1 -1
  42. package/dist/runtime/legacy-thread-materializer.js +71 -17
  43. package/dist/runtime/legacy-thread-materializer.js.map +1 -1
  44. package/dist/runtime/orion-runtime-v1.d.ts +2 -0
  45. package/dist/runtime/orion-runtime-v1.d.ts.map +1 -1
  46. package/dist/runtime/orion-runtime-v1.js +7 -1
  47. package/dist/runtime/orion-runtime-v1.js.map +1 -1
  48. package/dist/runtime/orion-session-runner.d.ts +7 -2
  49. package/dist/runtime/orion-session-runner.d.ts.map +1 -1
  50. package/dist/runtime/orion-session-runner.js +16 -6
  51. package/dist/runtime/orion-session-runner.js.map +1 -1
  52. package/dist/runtime/product-bootstrap.d.ts +22 -0
  53. package/dist/runtime/product-bootstrap.d.ts.map +1 -0
  54. package/dist/runtime/product-bootstrap.js +374 -0
  55. package/dist/runtime/product-bootstrap.js.map +1 -0
  56. package/dist/runtime/product-orion-runtime.d.ts +2 -1
  57. package/dist/runtime/product-orion-runtime.d.ts.map +1 -1
  58. package/dist/runtime/product-orion-runtime.js +83 -13
  59. package/dist/runtime/product-orion-runtime.js.map +1 -1
  60. package/dist/runtime/protocol/runtime-protocol-v1.d.ts +1 -1
  61. package/dist/runtime/protocol/runtime-protocol-v1.js +1 -1
  62. package/dist/runtime/protocol/runtime-protocol-v1.js.map +1 -1
  63. package/dist/runtime/release-receipts.d.ts +66 -3
  64. package/dist/runtime/release-receipts.d.ts.map +1 -1
  65. package/dist/runtime/release-receipts.js +340 -7
  66. package/dist/runtime/release-receipts.js.map +1 -1
  67. package/dist/runtime/step-snapshot.d.ts.map +1 -1
  68. package/dist/runtime/step-snapshot.js +22 -2
  69. package/dist/runtime/step-snapshot.js.map +1 -1
  70. package/dist/runtime/subagents/runtime-integration.d.ts +1 -1
  71. package/dist/runtime/subagents/runtime-integration.d.ts.map +1 -1
  72. package/dist/runtime/subagents/runtime-integration.js +5 -2
  73. package/dist/runtime/subagents/runtime-integration.js.map +1 -1
  74. package/dist/runtime/thread-event-store.d.ts +80 -0
  75. package/dist/runtime/thread-event-store.d.ts.map +1 -1
  76. package/dist/runtime/thread-event-store.js +456 -22
  77. package/dist/runtime/thread-event-store.js.map +1 -1
  78. package/dist/runtime/thread-projection.d.ts +10 -0
  79. package/dist/runtime/thread-projection.d.ts.map +1 -1
  80. package/dist/runtime/thread-projection.js +40 -0
  81. package/dist/runtime/thread-projection.js.map +1 -1
  82. package/dist/runtime/thread-runtime.d.ts +3 -1
  83. package/dist/runtime/thread-runtime.d.ts.map +1 -1
  84. package/dist/runtime/thread-runtime.js +6 -0
  85. package/dist/runtime/thread-runtime.js.map +1 -1
  86. package/dist/runtime/thread-session-index.d.ts +84 -0
  87. package/dist/runtime/thread-session-index.d.ts.map +1 -0
  88. package/dist/runtime/thread-session-index.js +503 -0
  89. package/dist/runtime/thread-session-index.js.map +1 -0
  90. package/dist/runtime/thread-session-view.d.ts +70 -9
  91. package/dist/runtime/thread-session-view.d.ts.map +1 -1
  92. package/dist/runtime/thread-session-view.js +200 -82
  93. package/dist/runtime/thread-session-view.js.map +1 -1
  94. package/dist/runtime/thread-ui-adapter.d.ts +2 -0
  95. package/dist/runtime/thread-ui-adapter.d.ts.map +1 -1
  96. package/dist/runtime/thread-ui-adapter.js +86 -4
  97. package/dist/runtime/thread-ui-adapter.js.map +1 -1
  98. package/dist/runtime/tool-detail-repository.d.ts.map +1 -1
  99. package/dist/runtime/tool-detail-repository.js +23 -19
  100. package/dist/runtime/tool-detail-repository.js.map +1 -1
  101. package/dist/runtime/tool-receipt-validator.d.ts +21 -0
  102. package/dist/runtime/tool-receipt-validator.d.ts.map +1 -0
  103. package/dist/runtime/tool-receipt-validator.js +157 -0
  104. package/dist/runtime/tool-receipt-validator.js.map +1 -0
  105. package/dist/runtime/ui-events.d.ts +35 -0
  106. package/dist/runtime/ui-events.d.ts.map +1 -1
  107. package/dist/runtime/ui-events.js +1 -0
  108. package/dist/runtime/ui-events.js.map +1 -1
  109. package/dist/runtime/ui-view-model.d.ts +2 -0
  110. package/dist/runtime/ui-view-model.d.ts.map +1 -1
  111. package/dist/runtime/ui-view-model.js +7 -3
  112. package/dist/runtime/ui-view-model.js.map +1 -1
  113. package/dist/services/global-config.d.ts +25 -0
  114. package/dist/services/global-config.d.ts.map +1 -1
  115. package/dist/services/global-config.js +373 -5
  116. package/dist/services/global-config.js.map +1 -1
  117. package/dist/services/redaction.d.ts +4 -0
  118. package/dist/services/redaction.d.ts.map +1 -1
  119. package/dist/services/redaction.js +63 -0
  120. package/dist/services/redaction.js.map +1 -1
  121. package/dist/services/session-index.d.ts +16 -0
  122. package/dist/services/session-index.d.ts.map +1 -1
  123. package/dist/services/session-index.js +45 -36
  124. package/dist/services/session-index.js.map +1 -1
  125. package/dist/services/session-storage.d.ts +59 -0
  126. package/dist/services/session-storage.d.ts.map +1 -1
  127. package/dist/services/session-storage.js +306 -107
  128. package/dist/services/session-storage.js.map +1 -1
  129. package/dist/services/settings-coordinator.d.ts +174 -0
  130. package/dist/services/settings-coordinator.d.ts.map +1 -0
  131. package/dist/services/settings-coordinator.js +605 -0
  132. package/dist/services/settings-coordinator.js.map +1 -0
  133. package/dist/services/settings-document-repository.d.ts +138 -0
  134. package/dist/services/settings-document-repository.d.ts.map +1 -0
  135. package/dist/services/settings-document-repository.js +551 -0
  136. package/dist/services/settings-document-repository.js.map +1 -0
  137. package/dist/services/workspace-registry.d.ts +42 -0
  138. package/dist/services/workspace-registry.d.ts.map +1 -0
  139. package/dist/services/workspace-registry.js +222 -0
  140. package/dist/services/workspace-registry.js.map +1 -0
  141. package/dist/terminal-ui/launch.d.ts.map +1 -1
  142. package/dist/terminal-ui/launch.js +5 -0
  143. package/dist/terminal-ui/launch.js.map +1 -1
  144. package/dist/tui-ui/state.d.ts.map +1 -1
  145. package/dist/tui-ui/state.js +5 -0
  146. package/dist/tui-ui/state.js.map +1 -1
  147. package/dist/web/errors.d.ts +6 -0
  148. package/dist/web/errors.d.ts.map +1 -0
  149. package/dist/web/errors.js +13 -0
  150. package/dist/web/errors.js.map +1 -0
  151. package/dist/web/event-hub.d.ts +43 -0
  152. package/dist/web/event-hub.d.ts.map +1 -0
  153. package/dist/web/event-hub.js +323 -0
  154. package/dist/web/event-hub.js.map +1 -0
  155. package/dist/web/file-read-service.d.ts +55 -0
  156. package/dist/web/file-read-service.d.ts.map +1 -0
  157. package/dist/web/file-read-service.js +399 -0
  158. package/dist/web/file-read-service.js.map +1 -0
  159. package/dist/web/git-read-model-service.d.ts +77 -0
  160. package/dist/web/git-read-model-service.d.ts.map +1 -0
  161. package/dist/web/git-read-model-service.js +631 -0
  162. package/dist/web/git-read-model-service.js.map +1 -0
  163. package/dist/web/index.d.ts +6 -0
  164. package/dist/web/index.d.ts.map +1 -0
  165. package/dist/web/index.js +28 -0
  166. package/dist/web/index.js.map +1 -0
  167. package/dist/web/launch.d.ts +9 -0
  168. package/dist/web/launch.d.ts.map +1 -0
  169. package/dist/web/launch.js +64 -0
  170. package/dist/web/launch.js.map +1 -0
  171. package/dist/web/protocol.d.ts +408 -0
  172. package/dist/web/protocol.d.ts.map +1 -0
  173. package/dist/web/protocol.js +369 -0
  174. package/dist/web/protocol.js.map +1 -0
  175. package/dist/web/review-service.d.ts +38 -0
  176. package/dist/web/review-service.d.ts.map +1 -0
  177. package/dist/web/review-service.js +74 -0
  178. package/dist/web/review-service.js.map +1 -0
  179. package/dist/web/server.d.ts +20 -0
  180. package/dist/web/server.d.ts.map +1 -0
  181. package/dist/web/server.js +744 -0
  182. package/dist/web/server.js.map +1 -0
  183. package/dist/web/terminal-manager.d.ts +155 -0
  184. package/dist/web/terminal-manager.d.ts.map +1 -0
  185. package/dist/web/terminal-manager.js +728 -0
  186. package/dist/web/terminal-manager.js.map +1 -0
  187. package/dist/web/terminal-server.d.ts +8 -0
  188. package/dist/web/terminal-server.d.ts.map +1 -0
  189. package/dist/web/terminal-server.js +275 -0
  190. package/dist/web/terminal-server.js.map +1 -0
  191. package/dist/web/workbench-controller.d.ts +99 -0
  192. package/dist/web/workbench-controller.d.ts.map +1 -0
  193. package/dist/web/workbench-controller.js +1267 -0
  194. package/dist/web/workbench-controller.js.map +1 -0
  195. package/dist/web-client/assets/DiffViewer-DpKoD07C.js +5 -0
  196. package/dist/web-client/assets/FilesPanel-BkoLmgjR.js +2 -0
  197. package/dist/web-client/assets/GitPanel-D20StcdG.js +1 -0
  198. package/dist/web-client/assets/ReviewPanel-3MVWbMQV.js +1 -0
  199. package/dist/web-client/assets/TerminalPanel-BLQ592Fb.js +21 -0
  200. package/dist/web-client/assets/TerminalPanel-DLuoa74B.css +1 -0
  201. package/dist/web-client/assets/index-BkIDl_xk.js +16 -0
  202. package/dist/web-client/assets/index-DXMpnWE8.css +1 -0
  203. package/dist/web-client/index.html +16 -0
  204. package/docs/architecture/v0.3.0-web-api.yaml +1340 -0
  205. package/docs/architecture/v0.3.1-web-api.yaml +2290 -0
  206. package/docs/migration/v0.2.2-to-v0.3.0-settings.md +114 -0
  207. package/docs/migration/v0.2.2-to-v0.3.0.md +55 -0
  208. package/docs/migration/v0.3.0-to-v0.3.1.md +88 -0
  209. package/docs/orion.example.json +9 -2
  210. package/docs/plan/v0.3.0-node-runtime-compatibility-plan.md +57 -0
  211. package/docs/plan/v0.3.0-settings-integration-plan.md +740 -0
  212. package/docs/plan/v0.3.0-web-workbench-plan.md +376 -0
  213. package/docs/plan/v0.3.1-web-workbench-professional-shell-plan.md +848 -0
  214. package/docs/readme.md +22 -1
  215. package/docs/test/v0.3.1-web-workbench-e2e-plan.md +140 -0
  216. package/npm-shrinkwrap.json +1229 -45
  217. package/package.json +47 -11
@@ -0,0 +1,740 @@
1
+ # Orion Code v0.3.0 Settings 集成开发计划
2
+
3
+ > 状态:Implemented;历史 20/22/24 Settings Web E2E 与当前 22/24/26 本地候选矩阵均为
4
+ > `GO`,仍待 source-clean 发布候选获得新的远端 CI receipt
5
+ >
6
+ > 目标版本:`v0.3.0`
7
+ >
8
+ > Orion 基线:`v0.2.2@40d02f687b70a648b452022ebf0e2f114021ea2e`
9
+ >
10
+ > DSH 参考基线:`deepseek-harness@b150a551b8d465e31e418e1b2eaf5e79bbb7d28e`
11
+ >
12
+ > 编制日期:2026-08-27
13
+ >
14
+ > 完成日期:2026-08-27
15
+ >
16
+ > 历史本地验证:同一 `@orion-agents/orion-code@0.3.0` tgz 通过 3 次 22 场景
17
+ > primary 与 Node 20/22/24 各 18 场景 critical 矩阵;该历史 Web E2E receipt 为 `GO`。
18
+ > 候选包是在本计划随包内容冻结后生成的,因此精确 tarball/receipt digest 只记录在
19
+ > 不参与 npm 打包的 [Web E2E Qualification Report](../test/v0.3.0-web-e2e-report.md),
20
+ > 避免包内文档对自身 tarball 哈希形成不可复现的循环引用。
21
+ > 源码工作树仍为 dirty,本计划不宣称 commit/tag/push/npm publication 已完成。
22
+ >
23
+ > 当前页面基线:[Settings 截图](../assets/screenshots/v0.3.0-web/13-settings.png);
24
+ > [27 状态真实 Chrome 图册](../assets/screenshots/v0.3.0-web/README.md)
25
+
26
+ ## 1. 结论与交付目标
27
+
28
+ v0.3.0 要把当前“能改三个值的弹窗”升级为真正的 Settings 子系统:Host 是唯一真源,
29
+ 设置具备明确作用域、来源、生效时机、磁盘持久化、原子批量保存、跨标签/跨进程冲突检测、
30
+ 外部文件编辑收敛、错误恢复和秘密隔离;Web、TUI、命令与 Runtime 不能再绕过同一套设置协调器。
31
+
32
+ 完成后,用户可以可靠地完成以下操作:
33
+
34
+ - 设置主题与动效,并在刷新、端口变化和 Host 重启后保留;
35
+ - 设置全局默认模型,且不会把“当前会话临时切换”误认为默认值;
36
+ - 设置当前工作区的 Effort 默认值,重启后仍按 `project > global > model default` 生效;
37
+ - 设置全局工具确认策略,明确知道它的作用域和开始生效的时间;
38
+ - 看见每个字段的有效值、显式覆盖值、来源、作用域和生效时机,并可“重置为继承值”;
39
+ - 在两个标签页或外部编辑 `orion.json` 时得到可解释冲突,不丢草稿、不静默覆盖;
40
+ - 查看 Provider 凭证的 `ready / missing / unavailable` 状态,但浏览器永远拿不到密钥值;
41
+ - 通过“打开配置文件”进入高级编辑;请求不接收、响应不返回任意本地路径。
42
+
43
+ v0.3.0 的发布结论只有两种:上述 P0 合同和真实 E2E 全部通过则 `GO`;任一持久化、
44
+ CAS、秘密泄漏或恢复用例失败则 `NO_GO`。不能以当前单字段 CAS 测试代替完整 Settings 证明。
45
+
46
+ ## 2. 源码审计结论
47
+
48
+ ### 2.1 DSH 的真实能力
49
+
50
+ DSH 值得参考的是设置生命周期,不是界面外形或插件系统。
51
+
52
+ | 能力 | 固定源码证据 | 对 Orion 的意义 |
53
+ | -------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- |
54
+ | defaults → composition base → user 覆盖 | `packages/settings/settings/src/index.ts:1-6,290-305,686-709` | 显式呈现来源与继承,不能只返回一个 resolved value |
55
+ | descriptor 含 `value/base/user/applies/secrets/revision` | `packages/settings/settings/src/index.ts:64-90` | Web 只消费脱敏描述,不直接读取配置文件 |
56
+ | 最小 `set/unset` path-op + revision CAS | `packages/settings/settings/src/index.ts:192-203,577-648` | reset 必须 unset;不能 replace 脱敏后的整份文档 |
57
+ | 0700 目录、0600 文件、跨进程锁、原子写 | `packages/settings/settings-file/src/index.ts:104-229` | Orion 可复用已有安全文件原语,但仍需 durable revision |
58
+ | 外部合法编辑热加载;非法编辑保留 last-good | `packages/settings/settings-file/src/index.ts:232-338` | watcher 不得用默认值覆盖坏文件 |
59
+ | 共享 SettingsMirror 合并读取并防止旧响应回写 | `packages/client/ui-settings/src/client/settings-mirror.ts:109-207` | 多组件不能各自维护一份设置真相 |
60
+ | 写入串行、冲突后回读 Host | `packages/client/ui-settings/src/client/settings-scope.ts:124-158` | 浏览器草稿与服务器快照必须分离 |
61
+ | loopback-only、pathless `openDocument` | `packages/host/apiproxy/src/api/settings.ts:52-105` | 保留 Orion 的本地安全边界,不暴露绝对路径 |
62
+ | 默认模型/权限与当前 Session 控制分离 | `packages/core/agent-default-model/src/index.ts:20-103`、`packages/interaction/permission-presets/src/index.ts:270-293,395-445` | Settings 编辑默认值;会话控制继续属于 Runtime |
63
+ | Provider 配置与凭证分域,key write-only | `packages/client/ui-settings-models/src/client/ProviderEditor.tsx:243-317`、`packages/credentials/credentials-local/src/index.ts:557-570` | Orion v0.3.0 只暴露凭证状态,不返回秘密 |
64
+
65
+ DSH 当前 Web 传输是 unary `POST /api/<method>` 加两条仅下行 WebSocket,见
66
+ `packages/host/apiproxy/src/fetch/client.ts:303-349` 与
67
+ `packages/client/connection/src/client/web-api-client.ts:1-32`。Orion 已采用同源 JSON API 加一条可重放 SSE,
68
+ 本方案只借鉴状态投影和恢复原则,不改变 v0.3.0 的传输决策。
69
+
70
+ ### 2.2 Orion 当前可复用基础
71
+
72
+ | 已有基础 | 源码证据 | 结论 |
73
+ | ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------- |
74
+ | `orion.json` 是单一结构化配置源 | `src/services/config.ts:1-16,316-384` | 继续作为唯一配置文档 |
75
+ | 全局与项目设置已经在同一文档中 | `src/services/global-config.ts:204-242,512-531` | 可原子更新 global 与 active project override |
76
+ | 文件锁、0600、原子 rename 与 fsync | `src/services/global-config.ts:475-505`、`src/services/atomic-write.ts:64-99`、`src/services/file-lock.ts:146-180` | 直接复用,不再造一套写盘原语 |
77
+ | Effort 已有 session/project/global 解析 | `src/commands/model-command-handlers.ts:594-709` | 补齐 Web bootstrap 的 project 读取即可形成闭环 |
78
+ | Web mutation 已有 requestId、Origin、nonce、JSON 检查 | `src/web/server.ts:213-230,296-310` | Settings 沿用同一幂等与浏览器安全边界 |
79
+ | Settings DTO 是正向白名单 | `src/web/protocol.ts:13-25` | 当前不会返回 key/header,但缺少来源与状态元数据 |
80
+
81
+ `src/product/paths.ts:43-46` 虽保留 `getSettingsPath()`,当前生产链没有消费它;迁移代码只把历史
82
+ `settings.json` 当作遗留文件复制。v0.3.0 **不启用它作为第二真源**,避免 `orion.json` 与
83
+ `settings.json` 双写和漂移。后续若要清理该历史路径,应另立迁移,不与本功能混做。
84
+
85
+ ### 2.3 当前 P0 缺口
86
+
87
+ 1. `src/web/workbench-controller.ts:73,453-515` 的 revision 是从 `1` 开始的内存整数;Host
88
+ 重启、其他进程、slash command 或外部文件编辑都可绕过 CAS。两个不同 requestId 的并发异步
89
+ mutation 也可能同时通过同一 revision。
90
+ 2. `web/src/useWorkbench.ts:426-448` 把一次保存拆成最多三次 PATCH;第二步失败时第一步已经提交,
91
+ 但 UI 仍把它表现为一个事务。
92
+ 3. Web 模型设置仅执行 `/model` 并改 LLM/Store,未写 `defaultModel` 或 Session metadata:
93
+ `src/web/workbench-controller.ts:482-497`、`src/commands/model-command-handlers.ts:417-470`。
94
+ 4. Effort 虽写到 `projects[cwd].defaultEffort`,启动只读取全局 `defaultEffort`:
95
+ `src/commands/model-command-handlers.ts:650-671`、`src/services/config.ts:367-373`。
96
+ 5. Theme/Motion 只在当前浏览器 localStorage:`web/src/App.tsx:17,27-53,249-271`。
97
+ 6. 当前 reducer 只有 baseline/本地 PATCH 回写,没有 `settings_changed` 事件:
98
+ `web/src/reducer.ts:78-93,133-134`。
99
+ 7. 任意 HTTP 409 都被显示成“其他客户端更新”,无法区分 revision conflict、runtime busy 与
100
+ requestId/body conflict:`web/src/useWorkbench.ts:449-455`。
101
+ 8. 当前测试只证明顺序单字段 stale CAS;未证明并发、Host 重启、项目 Effort 恢复、UI 草稿冲突、
102
+ 部分失败、外部文件热加载或 Settings 专项 secret-negative。
103
+
104
+ ## 3. 范围与非目标
105
+
106
+ ### 3.1 v0.3.0 必须交付
107
+
108
+ - `SettingsCoordinatorV1`:所有持久设置写入口的唯一协调器;
109
+ - `orion.json` 严格读取、持久 revision、进程内队列、文件锁内 CAS、原子批量 mutation;
110
+ - General、Models & Reasoning、Permissions、Advanced 四个静态页面;
111
+ - Host-backed Theme/Motion、全局默认模型、项目 Effort、全局工具确认;
112
+ - 每字段的 scope/source/applies/override/writable 元数据及 reset-to-inherited;
113
+ - Provider/credential 的脱敏就绪状态与 pathless “打开配置文件”;
114
+ - secret-free `settings_invalidated` SSE 与浏览器 SettingsMirror;
115
+ - inline dirty/saving/conflict/invalid/read-only/restart-required 状态;
116
+ - 单元、合同、集成、真实浏览器 E2E、重启与安全门禁。
117
+
118
+ ### 3.2 明确不做
119
+
120
+ - 不引入 DSH/Cordis 式插件注册、动态 Settings section、Plugin SDK、插件市场或外部 JavaScript;
121
+ - 不把 Skills、MCP、Model 合并成“插件”。它们继续保留现有独立领域边界;
122
+ - 不提供任意 JSON Pointer、任意文件路径或任意环境变量编辑能力;
123
+ - 不在浏览器返回已有 API key、Authorization/Header、cookie、credential value 或环境变量名;
124
+ - 不在 v0.3.0 提供 MCP `env/headers` 的 Web CRUD;Inspector 继续只读展示;
125
+ - 不伪造 locale 切换。Orion 当前没有完整 i18n 资源,语言设置等 i18n 完成后再加入;
126
+ - 不宣称 DSH 使用 SSE,也不复制其 UI、品牌或源码。
127
+
128
+ ### 3.3 后续扩展,不阻塞 v0.3.0
129
+
130
+ - Provider/model 可视化 CRUD 与 model discovery;
131
+ - 独立 `CredentialStore` 的 write-only set/delete API;
132
+ - project-scoped tool policy、sandbox 和 agent-loop 高级项;
133
+ - locale、键位、状态栏等跨 TUI/Web 偏好;
134
+ - 经真实认证后的非 loopback Settings。未实现认证前始终仅绑定 loopback。
135
+
136
+ ## 4. 产品信息架构
137
+
138
+ Settings 使用静态导航;字段少时移动端退化为单列,不做动态 slot。
139
+
140
+ | 页面 | v0.3.0 字段/能力 | 作用域 | 生效时机 |
141
+ | ------------------ | ------------------------------------------------------------------------------------ | ----------------------------------- | -------------------------------------- |
142
+ | General | Theme:`system/light/dark`;Motion:`system/reduced` | global user | `live` |
143
+ | Models & Reasoning | 全局默认模型;当前工作区 Effort;当前 Session 模型/effort 只读摘要与“转到会话控制” | global / project / session-readonly | `new-session` / `next-logical-request` |
144
+ | Permissions | 全局 `toolConfirmation=ask/allow/deny`;硬拒绝与 allowlist 边界说明 | global | Runtime idle 后的下一逻辑请求 |
145
+ | Advanced | 配置状态、last-good/错误、打开配置文件、Provider 凭证 readiness、Skills/MCP 只读入口 | read-only/action | n/a |
146
+
147
+ 必须避免两个现有语义错误:
148
+
149
+ - “当前会话模型”不是“默认模型”。Settings 写 `GlobalConfig.defaultModel`;当前会话仍通过 `/model`
150
+ 或会话控制切换,保存默认值不偷偷改正在进行的会话。
151
+ - Effort 的 Settings 字段是 active workspace 的 project default。若 Session 已有显式 effort,页面显示
152
+ “被会话覆盖”,并提供转到会话控制,不覆盖该 Session 值。
153
+
154
+ 工具确认保持 Orion 既有的 global + live Runtime 语义,但 Settings mutation 只允许在 Runtime idle 时提交;
155
+ 选择 `allow` 需要二次确认,并明确“不会越过硬拒绝、sandbox、workspace boundary 或风险上限”。审批
156
+ `once/project/global` allowlist 是另一个授权领域,不混入默认策略单选项。
157
+
158
+ ### 4.1 页面状态机
159
+
160
+ ```text
161
+ closed
162
+ -> loading
163
+ -> ready(clean)
164
+ -> ready(dirty)
165
+ -> saving
166
+ -> ready(clean)
167
+ -> conflict(draft preserved)
168
+ -> rejected(draft preserved)
169
+ -> stale(auto refresh when clean)
170
+ -> invalid(last-good visible, writes disabled)
171
+ -> unavailable/read-only
172
+ ```
173
+
174
+ 交互要求:
175
+
176
+ - 只有 Host-backed 字段进入 dirty;外观也改为 Host-backed 后统一进入原子保存;
177
+ - 未修改时禁用按钮;按钮文案为“应用 N 项”;
178
+ - 关闭/Escape/遮罩遇到 dirty 时确认丢弃,clean 时直接关闭并恢复触发器焦点;
179
+ - conflict 内联展示“服务器最新值 / 我的草稿”,提供“采用服务器值”和“基于最新值重试”;
180
+ - `runtime_busy` 预先禁用相关字段并解释,不得误报为 revision conflict;
181
+ - invalid external edit 时展示 last-good,只允许刷新/打开文档,不允许覆盖坏文件;
182
+ - 成功、冲突和错误都在 dialog 内用 `role=status/alert` 宣告,不依赖 dialog 外 toast。
183
+
184
+ ## 5. 目标领域模型
185
+
186
+ ### 5.1 单一配置真源与覆盖链
187
+
188
+ 继续使用 `$ORION_CODE_CONFIG_DIR/orion.json`:
189
+
190
+ ```text
191
+ internal/model default
192
+ -> GlobalConfig (orion.json root)
193
+ -> ProjectConfig (orion.json.projects[canonical active workspace])
194
+ -> Session override / current Runtime state
195
+ ```
196
+
197
+ v0.3.0 新增:
198
+
199
+ ```ts
200
+ interface WebAppearanceConfigV1 {
201
+ theme?: 'system' | 'light' | 'dark';
202
+ motion?: 'system' | 'reduced';
203
+ }
204
+
205
+ interface GlobalConfig {
206
+ web?: { appearance?: WebAppearanceConfigV1 };
207
+ }
208
+ ```
209
+
210
+ 字段到现有存储的映射:
211
+
212
+ | Settings key | 持久化位置 | effective 解析 |
213
+ | ------------------------------ | --------------------------------------- | ----------------------------------------------------- |
214
+ | `appearance.theme` | `web.appearance.theme` | explicit global → `system` |
215
+ | `appearance.motion` | `web.appearance.motion` | explicit global → `system` |
216
+ | `defaults.model` | `defaultModel` | explicit global → internal default |
217
+ | `defaults.effort` | `projects[activeProject].defaultEffort` | session → project → global → model capability default |
218
+ | `permissions.toolConfirmation` | `toolConfirmation` | explicit global → internal default |
219
+
220
+ Settings API 不暴露 `projects[/absolute/path]`。Host 根据已激活且 canonicalized 的 workspace 解析 project key,
221
+ 避免客户端构造跨 workspace 写入。
222
+
223
+ ### 5.2 字段描述
224
+
225
+ ```ts
226
+ type SettingsSourceV1 = 'internal' | 'model' | 'global' | 'project' | 'session';
227
+ type SettingsScopeV1 = 'global' | 'project' | 'session';
228
+ type SettingsAppliesV1 = 'live' | 'next-logical-request' | 'new-session' | 'restart';
229
+
230
+ interface SettingsFieldViewV1<T> {
231
+ effectiveValue: T;
232
+ explicitValue?: T;
233
+ inheritedValue?: T;
234
+ source: SettingsSourceV1;
235
+ scope: SettingsScopeV1;
236
+ applies: SettingsAppliesV1;
237
+ overridden: boolean;
238
+ writable: boolean;
239
+ blockedReason?: 'runtime_busy' | 'read_only' | 'invalid_document';
240
+ }
241
+ ```
242
+
243
+ `explicitValue` 只适用于非秘密字段。Provider credential 仅返回:
244
+
245
+ ```ts
246
+ interface CredentialSlotViewV1 {
247
+ providerId: string;
248
+ state: 'ready' | 'missing' | 'unavailable';
249
+ source: 'environment' | 'legacy' | 'none';
250
+ writable: false; // v0.3.0 Web 不写 secret
251
+ }
252
+ ```
253
+
254
+ 不得返回具体环境变量名、密钥长度、前后缀或可用于枚举凭证的信息。
255
+
256
+ ### 5.3 持久 revision
257
+
258
+ 现有内存整数替换为不透明、带本机密钥的字符串:
259
+
260
+ ```text
261
+ revision = "hmac-sha256:" + HMAC-SHA256(local revision key, exact current orion.json bytes)
262
+ ```
263
+
264
+ - revision key 是独立的 32-byte 随机值,保存为配置目录内 0600 文件;它不是设置真源,也不得进入
265
+ API、日志、trace 或错误消息;
266
+ - 文件不存在时,对明确的 `missing-document-v1` sentinel 计算 revision;
267
+ - revision 在 Host 重启后稳定,并可发现任何外部字节变化;
268
+ - 客户端只做相等比较,不解析或递增;
269
+ - mutation 在持有文件锁后重新读取并计算 revision,再比较 `expectedRevision`;
270
+ - 不把 revision 写进 `orion.json`,避免手工编辑忘记递增;
271
+ - 不直接把含凭证文档的裸 SHA-256 暴露给浏览器,避免形成低熵秘密的离线猜测 oracle;
272
+ - watcher 只用于快速失效,正确性来自每次锁内重读和 CAS。
273
+
274
+ ## 6. 写入与运行时一致性
275
+
276
+ ### 6.1 `SettingsCoordinatorV1`
277
+
278
+ 新增唯一协调链:
279
+
280
+ ```text
281
+ Web/TUI/command
282
+ -> SettingsCoordinatorV1.prepare()
283
+ -> in-process serialized queue
284
+ -> withFileLockSync(orion.json)
285
+ -> strict reread + exact-byte revision
286
+ -> expectedRevision CAS
287
+ -> apply bounded typed operations to raw clone
288
+ -> validate full candidate + model registry
289
+ -> atomicWriteFileSync(mode=0600, fsync=true)
290
+ -> apply prepared Runtime transition
291
+ -> emit settings_invalidated
292
+ ```
293
+
294
+ 关键不变量:
295
+
296
+ 1. 所有持久设置写入口都调用 Coordinator。`/settings`、`/model --default`、
297
+ `/effort --project|--global`、`permission_mode_change` 不再各自直接写文件;
298
+ 2. 普通 `/model <id>` 仍是 Session 控制,不改 default settings,但必须更新 Session metadata;
299
+ 3. 一个 PATCH 的全部 operations 在同一次锁与一次 atomic rename 中提交;
300
+ 4. 候选文档严格校验失败时零写入;未知设置 key、越界值、重复 key、超限 operation 均 400;
301
+ 5. 初次读取就是非法 JSON 时 fail closed,不能沿用 `loadGlobalConfig()` 的“静默回默认值”路径覆盖原文件;
302
+ 6. watcher 遇到非法外部编辑时保留 last-good Runtime,发布诊断并禁用 Web 写入;
303
+ 7. write queue 的一次失败不能 poison 后续请求;
304
+ 8. requestId 幂等缓存仍校验 body digest;同 requestId 不同 body 返回 409。
305
+
306
+ ### 6.2 prepare → persist → apply
307
+
308
+ - `prepare` 验证 model 是否存在、Effort 是否受当前 model 支持、Runtime 是否 idle,并构造新配置和
309
+ Runtime transition,不产生副作用;
310
+ - `persist` 在锁内 CAS 后一次写盘;
311
+ - `apply` 更新共享 runtime config/store,并在需要时 rebind Session Runtime;
312
+ - apply 失败时,仅在磁盘 revision 仍等于刚提交 revision 时用 CAS 恢复旧字节;若期间出现外部写入,
313
+ 进入 `settings_recovery_required`,暂停新的 agent turn 并要求 reload,不覆盖第三方更改;
314
+ - tool policy 变更必须同步重建/刷新 ExecutionPolicy 投影,保证实际行为与 durable receipt 的 policy
315
+ digest 一致;
316
+ - project Effort 必须接入 `product-bootstrap.ts`,启动解析与 `/effort status` 使用同一 resolver。
317
+
318
+ ## 7. Web API 与事件合同
319
+
320
+ 当前 Web API 尚未发布,v0.3.0 在发布前直接收紧现有 `/api/v1/settings` 合同;不保留错误的内存整数
321
+ revision 兼容层。
322
+
323
+ ### 7.1 查询
324
+
325
+ `GET /api/v1/settings`
326
+
327
+ ```ts
328
+ interface WebSettingsDocumentV1 {
329
+ schemaVersion: 1;
330
+ revision: string;
331
+ state: 'ready' | 'invalid' | 'read-only' | 'unavailable';
332
+ writable: boolean;
333
+ hasDocument: boolean;
334
+ workspace: string;
335
+ sections: {
336
+ appearance: {
337
+ theme: SettingsFieldViewV1<'system' | 'light' | 'dark'>;
338
+ motion: SettingsFieldViewV1<'system' | 'reduced'>;
339
+ };
340
+ defaults: {
341
+ model: SettingsFieldViewV1<string>;
342
+ effort: SettingsFieldViewV1<string>;
343
+ };
344
+ permissions: {
345
+ toolConfirmation: SettingsFieldViewV1<'ask' | 'allow' | 'deny'>;
346
+ };
347
+ };
348
+ models: readonly WebModelSummaryV1[];
349
+ credentials: readonly CredentialSlotViewV1[];
350
+ currentSession?: {
351
+ model: string;
352
+ effort: string;
353
+ overridesProjectEffort: boolean;
354
+ };
355
+ diagnostic?: { code: string; message: string }; // secret/path-redacted
356
+ }
357
+ ```
358
+
359
+ 响应不包含配置路径、raw document、headers、env、apiKey 或 Authorization 数据。
360
+
361
+ ### 7.2 原子 mutation
362
+
363
+ `PATCH /api/v1/settings`
364
+
365
+ ```ts
366
+ interface UpdateSettingsRequestV1 {
367
+ requestId: string; // UUID
368
+ expectedRevision: string;
369
+ operations: readonly SettingsOperationV1[]; // 1..20, total body <= 64 KiB
370
+ }
371
+
372
+ type SettingsOperationV1 =
373
+ | { op: 'set'; key: 'appearance.theme'; value: 'system' | 'light' | 'dark' }
374
+ | { op: 'unset'; key: 'appearance.theme' }
375
+ | { op: 'set'; key: 'appearance.motion'; value: 'system' | 'reduced' }
376
+ | { op: 'unset'; key: 'appearance.motion' }
377
+ | { op: 'set'; key: 'defaults.model'; value: string }
378
+ | { op: 'unset'; key: 'defaults.model' }
379
+ | { op: 'set'; key: 'defaults.effort'; value: EffortPreference }
380
+ | { op: 'unset'; key: 'defaults.effort' }
381
+ | { op: 'set'; key: 'permissions.toolConfirmation'; value: 'ask' | 'allow' | 'deny' }
382
+ | { op: 'unset'; key: 'permissions.toolConfirmation' };
383
+ ```
384
+
385
+ 同一 key 在一个请求中只能出现一次。`unset` 恢复继承,不把当前 effective value 写成新的 override。
386
+ 成功返回 `{requestId, revision, appliedKeys, settings}`,exact retry 返回第一次的相同结果。
387
+
388
+ ### 7.3 Problem codes
389
+
390
+ | HTTP | code | 语义 |
391
+ | ---- | ---------------------------- | -------------------------------------- |
392
+ | 400 | `settings_invalid_operation` | key/value/数量/组合非法 |
393
+ | 403 | `settings_write_forbidden` | nonce/origin/loopback/read-only 失败 |
394
+ | 409 | `settings_revision_conflict` | 文件 revision 已变化,零字段提交 |
395
+ | 409 | `request_id_conflict` | 同 requestId、不同 body |
396
+ | 409 | `runtime_busy` | 当前 turn/transition 不允许应用 |
397
+ | 422 | `settings_rejected` | 候选文档或 model/effort 语义校验失败 |
398
+ | 503 | `settings_document_invalid` | 外部文件非法;last-good 可读、写入禁用 |
399
+ | 503 | `settings_recovery_required` | persist 后 Runtime apply 无法安全收敛 |
400
+
401
+ 客户端必须按 `code` 分支,不再把所有 409 解释成并发冲突。
402
+
403
+ ### 7.4 Pathless advanced action
404
+
405
+ `POST /api/v1/settings/open-document`
406
+
407
+ - body 只有 `requestId`;
408
+ - 只在 loopback Host 开启;
409
+ - Server 自己解析 `getGlobalConfigPath()` 并调用受控 native opener;
410
+ - 不接受 path,不在响应或日志返回 path;
411
+ - 无文档时可创建权限 0600 的最小合法模板,创建同样走 Coordinator/CAS。
412
+
413
+ ### 7.5 SSE invalidation
414
+
415
+ 扩展 `WebEventEnvelope` 的判别联合:
416
+
417
+ ```ts
418
+ interface SettingsInvalidatedEventV1 {
419
+ type: 'settings_invalidated';
420
+ eventId: string;
421
+ cursor: number;
422
+ durable: false;
423
+ payload: {
424
+ revision: string;
425
+ reason: 'local-write' | 'external-edit' | 'workspace-change';
426
+ state: 'ready' | 'invalid';
427
+ };
428
+ }
429
+ ```
430
+
431
+ 事件不携带值、operation、绝对路径或秘密。SSE cursor 只负责连接内去重;断线/丢事件/Host 重启后的
432
+ 最终真相始终由 `GET /bootstrap` + `GET /settings` 恢复。
433
+
434
+ ## 8. Browser SettingsMirror 与草稿
435
+
436
+ 新增 `web/src/settings/`,不要继续把 Settings 生命周期散落在 `App.tsx`、Dialog local state 和
437
+ `useWorkbench.ts`:
438
+
439
+ ```text
440
+ SettingsMirror
441
+ - one shared Host snapshot
442
+ - one in-flight read + rerun flag + generation guard
443
+ - last-good preservation
444
+ - settings_invalidated / connection reset refresh
445
+
446
+ SettingsDraft
447
+ - openedAtRevision
448
+ - base values
449
+ - typed operations
450
+ - dirty keys
451
+ - server-latest snapshot on conflict
452
+ ```
453
+
454
+ 规则:
455
+
456
+ - 打开 dialog 时从 Mirror hydrate 草稿;切 workspace 清空旧草稿;
457
+ - clean 状态收到 invalidation:合并重复刷新并自动采用新快照;
458
+ - dirty 状态收到 invalidation:抓取最新快照,保留草稿并进入 conflict-pending;
459
+ - 保存只发送一次 batch PATCH;成功后把服务端响应直接 fold 到 Mirror,防止旧 GET 覆盖新值;
460
+ - 409 revision conflict 不丢草稿;rebase 后仅重试用户仍确认的 key;
461
+ - Host 重启后的 Recover 必须重新 bootstrap 取得新 nonce,再取 settings,不能只刷新 session snapshot;
462
+ - localStorage migration:若 Host appearance 没有显式值而 legacy localStorage 有值,以一次 CAS batch
463
+ 导入;确认落盘后删除 legacy key。Host 已有值时 Host 获胜并清理 legacy key。
464
+
465
+ ## 9. 实施工作包与文件映射
466
+
467
+ ### WP0:合同与安全不变量
468
+
469
+ - 修改 `docs/architecture/v0.3.0-web-api.yaml`:新 Settings document、batch operation、problem code、
470
+ `settings_invalidated` discriminator;
471
+ - 修改 `src/web/protocol.ts` 与 `web/src/types.ts`,协议与 UI 类型一一对应;
472
+ - 扩展 `tests/web-api-contract.test.ts`,验证所有 ref、oneOf、限制、secret-negative 和 operationId;
473
+ - 在编码前冻结字段/作用域/生效矩阵,后续实现不得改变产品语义。
474
+
475
+ 退出条件:OpenAPI、TypeScript type test 和 secret key denylist 全通过。
476
+
477
+ ### WP1:Settings repository 与 Coordinator
478
+
479
+ - 新增 `src/services/settings-document-repository.ts`:strict read、keyed revision、raw-preserving typed
480
+ patch、watcher、last-good;
481
+ - 新增 `src/services/settings-coordinator.ts`:queue、lock 内 CAS、prepare/persist/apply/rollback;
482
+ - 扩展 `src/services/global-config.ts`:`web.appearance` schema 与 strict validator;现有宽容启动 loader 保留,
483
+ 但不能用于 Settings 写入前校验;
484
+ - 复用 `src/services/atomic-write.ts`、`src/services/file-lock.ts`;
485
+ - 修改 `src/commands/model-command-handlers.ts`、`src/runtime/agent-runtime-controller.ts`,持久 mutation
486
+ 统一进入 Coordinator。
487
+
488
+ 退出条件:并发同 revision 只有一个成功;坏 JSON 零覆盖;Host 重启 revision 稳定。
489
+
490
+ ### WP2:Runtime 解析与一致性
491
+
492
+ - 修改 `src/runtime/product-bootstrap.ts`:读取 active project Effort,并按统一 resolver 初始化 Store/LLM;
493
+ - 修改 `src/services/config.ts`:暴露 global/project effective settings builder,不复制解析规则;
494
+ - 修改 session model 写入链,使普通 `/model` 更新当前 Session metadata,但不改全局 default;
495
+ - 修改 Runtime policy 创建/重绑链,toolConfirmation 行为和 receipt digest 同步;
496
+ - 增加 `SettingsCoordinator` 到 `src/runtime/product-bootstrap.ts` 的产品装配,不在 Web 层私自 new。
497
+
498
+ 退出条件:重建 Runtime/Host 后 model default、project effort、tool policy 与字段声明完全一致。
499
+
500
+ ### WP3:Host API、事件和恢复
501
+
502
+ - 修改 `src/web/workbench-controller.ts`:删除 `settingsRevision` 和命令字符串拼接式设置写入;委托
503
+ Coordinator;
504
+ - 修改 `src/web/server.ts`:batch PATCH、结构化 Problem、pathless open-document;
505
+ - 修改 `src/web/event-hub.ts`:`settings_invalidated` 投影与递归脱敏;
506
+ - 修改 `src/runtime/product-bootstrap.ts`、`src/web/server.ts` 与 `src/web/launch.ts` 的装配/关闭链,
507
+ 启动和关闭 watcher;
508
+ - 保留 exact Origin、nonce、same-origin、loopback、content-type 和 body limit;高级 action 同样受保护。
509
+
510
+ 退出条件:两个 Controller/进程修改同一配置可正确 CAS;旧页面在 Host 重启后重新 bootstrap。
511
+
512
+ ### WP4:SettingsMirror 与 UI
513
+
514
+ - 新增 `web/src/settings/settings-mirror.ts`、`settings-draft.ts`、`settings-reducer.ts`;
515
+ - 修改 `web/src/api.ts`:一次 batch mutation、problem code、open-document;
516
+ - 修改 `web/src/useWorkbench.ts`:baseline/recover/workspace switch/settings invalidation;
517
+ - 重构 `web/src/components/Dialogs.tsx` 为静态导航和分区组件;
518
+ - 修改 `web/src/App.tsx`:移除长期 localStorage 真源,仅保留一次迁移;
519
+ - 修改 `web/src/styles.css`:desktop、390×844、320 CSS px、200% zoom、safe-area、inline live region、
520
+ high contrast 与 reduced motion;
521
+ - Settings 触发器增加 `aria-haspopup="dialog"`、`aria-expanded`、`aria-controls`。
522
+
523
+ 退出条件:dirty、save、conflict、invalid、read-only、busy、reset、discard 全状态可键盘完成。
524
+
525
+ ### WP5:文档与迁移
526
+
527
+ - 更新 `README.md`、`README.zh-CN.md` 和 `docs/orion.example.json`;
528
+ - 新增 `docs/migration/v0.2.2-to-v0.3.0-settings.md`,说明 localStorage appearance 一次迁移、
529
+ 模型默认值与会话模型的语义拆分;
530
+ - 更新 `docs/plan/v0.3.0-web-workbench-plan.md` 和 Web API 文档;
531
+ - 记录实现差异、测试命令、已知限制和最终 GO/NO_GO 到 `docs/test/`。
532
+
533
+ ## 10. 测试方案与真实 E2E
534
+
535
+ ### 10.1 单元测试
536
+
537
+ 新增或扩展:
538
+
539
+ - `tests/settings-document-repository.test.ts`
540
+ - keyed exact-byte revision;revision key/file 0600 且不外泄;missing file;atomic write;
541
+ - global/project layering、array replacement、unset inheritance;
542
+ - 非法 JSON/非法 schema 保留 last-good 且拒绝写;
543
+ - watcher boot/read race、合法/非法/删除事件;
544
+ - raw 未知字段保留、受控 key 之外不可写;
545
+ - recursive secret redaction。
546
+ - `tests/settings-coordinator.test.ts`
547
+ - 同 revision 并发请求只提交一个;不同进程锁内重读;
548
+ - batch 全成或全不成;requestId exact replay/body conflict;
549
+ - prepare failure、persist failure、apply failure rollback;
550
+ - 失败队列不 poison 后续写入。
551
+ - `tests/web-settings-draft.test.ts`
552
+ - hydrate/dirty/reset/discard;
553
+ - invalidation clean auto-refresh;dirty preserve/rebase;
554
+ - conflict、busy、rejected 分支;旧 response generation 丢弃。
555
+
556
+ ### 10.2 Controller/API 集成
557
+
558
+ - 重建 Controller/Host 后读取相同 revision 与 effective values;
559
+ - active project Effort 写入、workspace 切换、重启恢复;
560
+ - default model 只影响新 Session,普通 `/model` 只影响当前 Session;
561
+ - tool policy 只在 idle 提交,下一真实 ToolGateway 评估生效,receipt policy digest 一致;
562
+ - slash command、TUI 和 Web 写入都触发同一 revision 与 invalidation;
563
+ - 所有 Problem code 精确,不使用消息字符串判断;
564
+ - GET/PATCH/SSE/problem/network log 对 secret denylist 递归扫描为零命中。
565
+
566
+ ### 10.3 真实浏览器 E2E
567
+
568
+ 使用 Playwright + 真实 Chrome、真实打包 tarball、隔离的 HOME/`ORION_CODE_CONFIG_DIR`/TMPDIR、
569
+ deterministic loopback OpenAI-compatible provider。禁止 fake Runtime 作为发布证据。
570
+
571
+ | ID | 真实旅程 | 通过标准 |
572
+ | --------- | ---------------------------------------------------------------- | ------------------------------------------------------------------- |
573
+ | SET-P0-01 | 打开 Settings,修改 Theme/Motion,保存,刷新、换端口、重启 Host | 值与 UI 都恢复;legacy localStorage 被安全迁移 |
574
+ | SET-P0-02 | 修改 default model,新建 Session;旧 Session 保持原模型 | 默认/当前语义无串线,Session metadata 正确 |
575
+ | SET-P0-03 | 修改 project Effort,发真实 LLM 请求,切 workspace、重启再请求 | wire effort 与 `project > global > model` 解析一致 |
576
+ | SET-P0-04 | ask → allow/deny,触发真实 ToolGateway write/exec | idle gate、生效时机、硬拒绝和 receipt digest 正确 |
577
+ | SET-P0-05 | 一个 UI 同时修改三个字段 | 网络只有一次 PATCH;全部成功或零字段提交 |
578
+ | SET-P0-06 | 两个真实 page 从同 revision 编辑并依次保存 | 第二页内联 conflict;草稿保留;首方值不被覆盖;rebase 可完成 |
579
+ | SET-P0-07 | Settings 打开且 dirty 时外部合法编辑 `orion.json` | invalidation 到达;server latest 与草稿并存;无静默覆盖 |
580
+ | SET-P0-08 | 外部写入非法 JSON,再尝试保存 | Runtime 保留 last-good;UI 显示 invalid;原坏文件字节不被覆盖 |
581
+ | SET-P0-09 | Host 保存响应后立即断线,再以同 requestId 重试 | exact result 重放;副作用和文件写入只有一次 |
582
+ | SET-P0-10 | Host 重启但保留旧页面,再 Recover 后保存 | 新 nonce/bootstrap/settings 生效;无旧 nonce 403 或 revision 假冲突 |
583
+ | SET-P0-11 | hostile Origin、缺 nonce、错 content-type、任意 path/open 请求 | 全部 fail closed;合法 pathless action 可用 |
584
+ | SET-P0-12 | 捕获 bootstrap/settings/SSE/problem/log/trace | secret、header、credential value、env 名和绝对 config path 零命中 |
585
+ | SET-P0-13 | desktop、390×844、320 CSS px、200% zoom、键盘、Escape、axe | 无阻断 a11y、横向溢出或焦点丢失;inline alert 可宣告 |
586
+ | SET-P0-14 | 从 npm tarball 安装,在 Node 22/24/26 启动并跑关键 Settings 旅程 | 三矩阵均使用同一 tarball SHA,不能 skip 当 pass |
587
+
588
+ 每个 E2E 证据记录:tarball SHA、Node/Chrome 版本、相对截图/trace 路径、requestId、revision 前后值、
589
+ 事件 cursor/eventId、真实 provider 请求摘要和 secret-scan 计数。证据不得包含 prompt body、
590
+ 密钥、Header、绝对用户路径或完整配置文件。
591
+
592
+ ### 10.4 必跑命令
593
+
594
+ 实现后以当时 `package.json` 的最终脚本为准,至少执行:
595
+
596
+ ```bash
597
+ npm run lint
598
+ npm run build
599
+ npm run build:web
600
+ npx jest --runInBand \
601
+ tests/settings-document-repository.test.ts \
602
+ tests/settings-coordinator.test.ts \
603
+ tests/web-api-contract.test.ts \
604
+ tests/web-protocol.test.ts \
605
+ tests/web-server.test.ts \
606
+ tests/web-workbench-controller.test.ts
607
+ npm run test:web-e2e -- --grep @settings
608
+ npm run release:check
609
+ ```
610
+
611
+ CI 的 critical Web gate 必须显式包含 `@settings`,不能继续只按旧的 `P0-01..04` grep;最终 release
612
+ receipt 要绑定同一 tarball 的 Settings E2E receipt,而不是只记录 HTTP health。
613
+
614
+ ## 11. 发布验收清单
615
+
616
+ ### 数据与一致性
617
+
618
+ - [x] `orion.json` 是唯一真源;没有 `settings.json` 双写;
619
+ - [x] revision 在 Host 重启后稳定,任何外部修改都会使旧 CAS 失败;
620
+ - [x] batch mutation 全成或全不成;
621
+ - [x] model default、current model、project effort、session effort、tool policy 的作用域没有混淆;
622
+ - [x] 外部非法编辑保留 last-good 且绝不被 Web 覆盖;
623
+ - [x] Web/TUI/slash command 进入同一 Coordinator。
624
+
625
+ ### 安全
626
+
627
+ - [x] API、SSE、错误、日志、trace、截图不含秘密或配置绝对路径;
628
+ - [x] 凭证只有 readiness,没有读取值的 endpoint;
629
+ - [x] open-document 不接收 path;
630
+ - [x] Host 仍只绑定 loopback,mutation 保留 exact Origin + nonce + JSON;
631
+ - [x] `allow` 不越过硬拒绝、sandbox、workspace 或风险上限。
632
+
633
+ ### UX 与恢复
634
+
635
+ - [x] 每字段显示 effective/source/scope/applies;reset 使用 unset;
636
+ - [x] dirty、busy、conflict、invalid、read-only、saving、success/error 均有明确状态;
637
+ - [x] 冲突和断线不丢用户草稿;
638
+ - [x] Host 重启恢复会重新 bootstrap nonce 与 settings;
639
+ - [x] desktop/mobile/zoom/keyboard/axe 均通过。
640
+
641
+ ### 发布证据
642
+
643
+ - [x] 单元、合同、集成、真实 UI、重启、安全 E2E 全绿;
644
+ - [ ] Node 22/24/26 对同一新 npm tarball 的关键旅程全绿;
645
+ - [x] receipt 绑定 tarball SHA、browser/version、runner digest 与 secret scan;
646
+ - [x] 只在所有 P0 证据可复核时标记 v0.3.0 Settings `GO`。
647
+
648
+ ## 12. 风险、缓解与回滚
649
+
650
+ | 风险 | 缓解 | 回滚策略 |
651
+ | ------------------------------ | -------------------------------------------------------- | ------------------------------------------------------ |
652
+ | 新 Coordinator 与旧命令双写 | 先做调用图测试,禁止生产代码直接调用旧写函数 | 保留旧读取,feature flag 关闭 Web 写入,不回滚磁盘格式 |
653
+ | apply 已失败但磁盘已提交 | prepare 预验证 + committed revision 条件回滚 | 进入 recovery-required,暂停 turn,绝不覆盖第三方写入 |
654
+ | watcher 抖动/重复事件 | debounce + exact revision 去重 + SettingsMirror 合并读取 | 停 watcher 后仍靠 GET/锁内 CAS 保证正确性 |
655
+ | localStorage 迁移多标签竞争 | CAS 导入,冲突后 Host 值优先 | legacy key 仅在 Host 确认落盘后删除 |
656
+ | 默认/当前模型语义迁移造成困惑 | 页面同时显示两者并提供会话控制入口 | 保留 `/model` 行为,不自动改当前 Session |
657
+ | Provider 配置包含历史明文 key | v0.3.0 只返回 readiness,所有投影递归扫描 | 禁用 Advanced readiness,仍允许 pathless 本地编辑 |
658
+ | 修改 `orion.json` 破坏未知字段 | raw-preserving typed patch + full candidate validation | 旧字节备份仅用于条件回滚,不做整文档 replace |
659
+
660
+ ## 13. 推荐实施顺序
661
+
662
+ 严格按 `WP0 → WP1 → WP2 → WP3 → WP4 → WP5` 推进。WP1/2 未通过持久化与 Runtime 恢复测试前,
663
+ 不要扩展 UI;WP3 合同未冻结前,不写并行的客户端适配;真实 E2E 从 WP1 起持续接入,而不是最后补。
664
+
665
+ 最小可验收纵向切片是:`appearance.theme` 一项走完整的 strict read → durable revision → atomic CAS →
666
+ SSE invalidation → SettingsMirror → UI conflict → Host restart E2E。该切片通过后,再按同一模式接入
667
+ default model、project effort 和 tool policy,能最早暴露架构错误,同时避免先做一套无法可靠保存的页面。
668
+
669
+ ## 14. 实施与验证记录
670
+
671
+ ### 14.1 已落地架构
672
+
673
+ - `src/services/settings-document-repository.ts` 实现 strict read、exact-byte HMAC revision、
674
+ raw-preserving typed patch、文件锁内 CAS、atomic write、last-good 与 watcher;revision key
675
+ 与配置文件均使用 0600 权限。
676
+ - `src/services/settings-coordinator.ts` 实现无 poison 串行队列、prepare/persist/apply、
677
+ requestId 幂等、条件回滚、external edit 合并与 busy 边界收敛。
678
+ - `src/runtime/product-bootstrap.ts`、`src/runtime/agent-runtime-controller.ts` 与命令入口共享
679
+ 同一 Coordinator;model default/current Session、project/global/session Effort 和 tool policy
680
+ 保持不同作用域。
681
+ - `src/web/` 提供 source-aware Settings document、单次 batch PATCH、结构化 Problem、
682
+ pathless open-document 与 secret-free `settings_invalidated`;重启恢复会重取 bootstrap
683
+ nonce 与 Settings。
684
+ - `web/src/settings/` 实现共享 SettingsMirror 和独立 draft;`SettingsDialog.tsx` 提供
685
+ General、Models & Reasoning、Permissions、Advanced 四个静态分区及 dirty/busy/
686
+ conflict/invalid/read-only 恢复界面。
687
+ - `src/runtime/thread-ui-adapter.ts` 只在 canonical durable receipt 与 `tool.receipt` fact
688
+ 同时校验通过时投影 policy/receipt digest 和授权来源;`src/web/event-hub.ts`
689
+ 只对该 typed event 放行白名单字段,任意 `authorization` 键仍被删除。
690
+
691
+ ### 14.2 测试层
692
+
693
+ - Repository/Coordinator/Runtime 回归覆盖 exact-byte revision、两进程竞争、非法
694
+ JSON 零覆盖、未知字段保留、persist/apply 失败、条件回滚、external sync、
695
+ model/effort/policy 重绑。
696
+ - Host/contract 回归覆盖 OpenAPI refs/discriminator、Problem code、Origin/nonce/JSON、
697
+ revision CAS、pathless action、SSE 脱敏与重启 bootstrap。
698
+ - `tests/e2e/web-settings-integration.spec.ts` 包含唯一且不 skip 的
699
+ `SET-P0-01..14`;只替换 loopback OpenAI-compatible provider,Host、Runtime、
700
+ ToolGateway、文件/命令工具、MCP 与 durable store 均为生产链。
701
+ - SET-P0-13 使用 W3C Reflow 所需的等效 320 CSS px viewport;自动化的
702
+ 200% 证据为 320 CSS px + DPR 2,而不是只改 visual viewport、不触发
703
+ reflow 的 CDP pinch zoom。参见
704
+ [W3C Understanding Reflow](https://www.w3.org/WAI/WCAG22/Understanding/reflow.html)。
705
+
706
+ ### 14.3 失败—修复记录
707
+
708
+ | ID | 失败证据 | 修复与最终保护 |
709
+ | -------- | --------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
710
+ | SET-F001 | 新建 Session 后 rename locator 被旧 row 拦截 | 等待 row count/active identity/rename enabled 后重新定位,使用真实键盘操作 |
711
+ | SET-F002 | Host 重启时早期 null bootstrap 使 composer 永久禁用 | diagnostics 不再隐式创建 Session;恢复后以新 bootstrap/workbench state 收敛 |
712
+ | SET-F003 | tool card 丢失真实授权来源与 receipt digest | durable receipt 校验后投影 typed authorization;Host sanitizer 只放行已知白名单 |
713
+ | SET-F004 | 拒绝工具后测试错等 provider 第二请求 | 对齐生产语义:拒绝终止 tool turn,证明 pending cleared、文件不存在与 idle |
714
+ | SET-F005 | 真实 409 与主动 SSE 替换被证据器当作未解释错误 | exact one-shot console expectation 与 method/path/error 精确断言;任何额外错误仍 fail |
715
+ | SET-F006 | `setPageScaleFactor(2)` 保留 640 px layout,错把 pinch zoom 当 reflow | 改用 320 CSS px + DPR 2 等效视口,保留普通 pointer、横向溢出、焦点与 axe 断言 |
716
+
717
+ 所有失败都保留为 `NO_GO` 证据后再修复;未使用 force click、skip、retry
718
+ 或删除产品断言来获得绿色结果。
719
+
720
+ ### 14.4 最终可复核证据
721
+
722
+ | 证据 | 结果 |
723
+ | ---------------------------- | ----------------------------------------------------------------- |
724
+ | Artifact | `@orion-agents/orion-code@0.3.0`,单一冻结 tgz |
725
+ | Primary | Node 22.22.3,3 次 fresh,每次 22/22,共 66/66,`GO` |
726
+ | Historical runtime matrix | Node 20.19.5 / 22.22.3 / 24.14.1,每个 18/18,共 54/54,`GO` |
727
+ | Current runtime matrix | Node 22.12/24.0/26.0,同一 tgz;各 18/18,共 54/54,`GO` |
728
+ | Browser | Google Chrome `151.0.7922.174` |
729
+ | Secret/evidence clean checks | 6/6 runs,0 findings;console/page/HTTP 5xx/dropped events 均为 0 |
730
+
731
+ 精确 tarball SHA-256、npm integrity、artifact receipt、installed target、runner、每次 run
732
+ manifest 与 Web E2E receipt digest 记录在仓库侧的
733
+ [Web E2E Qualification Report](../test/v0.3.0-web-e2e-report.md)。该报告不进入 npm tarball,
734
+ 因此能够在包内容冻结、生成候选并完成浏览器验证后安全记录候选身份;忽略的原始结构化证据位于
735
+ `tests/tmp/web-e2e/`,不包含在源码或发布包中。
736
+
737
+ 历史矩阵与当前 22/24/26 本地候选矩阵共同证明 Settings 功能和当前 Runtime 合同;精确
738
+ digest 记录在 [Node Runtime Compatibility Report](../test/v0.3.0-node-runtime-compatibility-report.md)。
739
+ 但 artifact receipt 依然记录 `source.dirty=true`;source-clean commit 生成的新 tarball 必须由远端
740
+ CI 重新绑定 receipt,在此之前干净源码发布门禁仍为 `NO_GO`。