@openchambery/web 2.0.0-beta.9 → 2.0.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 (238) hide show
  1. package/README.md +14 -0
  2. package/dist/assets/{AgentAvatar-CzTtVMnI.js → AgentAvatar-DGyF86jy.js} +1 -1
  3. package/dist/assets/AssistantView-ByGdaGiF.js +3 -0
  4. package/dist/assets/{DiagramView-oeXj4YHn.js → DiagramView-PB0pBILb.js} +1 -1
  5. package/dist/assets/{FadeInOnReveal-DCqrQkkz.js → FadeInOnReveal-B_a083XJ.js} +1 -1
  6. package/dist/assets/Icon-BE2hkaYc.js +1 -0
  7. package/dist/assets/MarkdownRenderer-H1MIHXzt.js +2 -0
  8. package/dist/assets/{MarkdownRendererImpl-CahUctxC.js → MarkdownRendererImpl-CZ8GeckU.js} +3 -3
  9. package/dist/assets/{MarkstreamRendererImpl-5JUaTSdl.js → MarkstreamRendererImpl-BKE6YZ2N.js} +1 -1
  10. package/dist/assets/MobileDetailNavigation-D9KkIrWy.js +1 -0
  11. package/dist/assets/{MobileShareBridge-yCN95dYg.js → MobileShareBridge-1yCjq-60.js} +3 -3
  12. package/dist/assets/MobileSurface-Dqn3OPVX.js +2 -0
  13. package/dist/assets/{MultiRunWindow-C0HqMFpp.js → MultiRunWindow-YCFM6or1.js} +1 -1
  14. package/dist/assets/{OnboardingScreen-O4cGdz5p.js → OnboardingScreen-CTmsNR65.js} +1 -1
  15. package/dist/assets/{OverlayScrollbar-CRfGtcmG.js → OverlayScrollbar-CVgW_Z9B.js} +1 -1
  16. package/dist/assets/SettingsView-CbiuXtYG.js +25 -0
  17. package/dist/assets/{SettingsWindow-B_d5Aqq3.js → SettingsWindow-jOZ7L6K8.js} +1 -1
  18. package/dist/assets/{TerminalView-DdNQQb8W.js → TerminalView-CI-KsDdH.js} +1 -1
  19. package/dist/assets/{ToolOutputDialog-4DC5OE1J.js → ToolOutputDialog-izZjgkkg.js} +1 -1
  20. package/dist/assets/appThemeRegistry-MiWlVk4e.js +1 -0
  21. package/dist/assets/{assistant-attachment-cache-DphJleHr.js → assistant-attachment-cache-sG8YQsVs.js} +1 -1
  22. package/dist/assets/{button-DIIrsMjW.js → button-CcOoDLE1.js} +1 -1
  23. package/dist/assets/{collapsible-CnvUeYst.js → collapsible-CSa0Ndgm.js} +1 -1
  24. package/dist/assets/{context-menu-BGBUpZEQ.js → context-menu-DoTwmMb5.js} +1 -1
  25. package/dist/assets/{desktopHosts-JGFqhlaM.js → desktopHosts-BsbquPqS.js} +1 -1
  26. package/dist/assets/desktopRelayRestore-yyvjztgj.js +1 -0
  27. package/dist/assets/{diagram-rvn7_qBM.js → diagram-CZKDkADZ.js} +1 -1
  28. package/dist/assets/dialog-Csvrld51.js +1 -0
  29. package/dist/assets/dropdown-menu.styles-ayLWk2gf.js +1 -0
  30. package/dist/assets/{es-Dsmc0A_3.js → es-BBbon7SL.js} +4 -4
  31. package/dist/assets/fileMentionTouchSelection-Bn0f83D3.js +2 -0
  32. package/dist/assets/{fileQueries-DN6gbLhM.js → fileQueries-B0hhXD86.js} +1 -1
  33. package/dist/assets/{fr-C_ZlZDI2.js → fr-BsKj2CTi.js} +4 -4
  34. package/dist/assets/{gitApi-da8etTTe.js → gitApi-DpwH7xTg.js} +4 -4
  35. package/dist/assets/{imageSource-BMztRu0c.js → imageSource-BpUsiwFd.js} +1 -1
  36. package/dist/assets/{input-BVu-iykl.js → input-C6Y7mytw.js} +1 -1
  37. package/dist/assets/insertionBoundaries-Cst5Z6Cd.js +1 -0
  38. package/dist/assets/{ja-BqqTaWTi.js → ja-1mgxunoM.js} +4 -4
  39. package/dist/assets/{ko-DP2XPM37.js → ko-Ra9znPcL.js} +4 -4
  40. package/dist/assets/{main-KwlAomA8.js → main-CEs8ipuu.js} +2 -2
  41. package/dist/assets/main-Dkzuy7EG.js +169 -0
  42. package/dist/assets/main-qSeeJRW1.css +1 -0
  43. package/dist/assets/{markdownHeightCache-D9-B1wdY.js → markdownHeightCache-DhLhFBWX.js} +1 -1
  44. package/dist/assets/{mcpQueries-3GTWeMU-.js → mcpQueries-BBpCJgvK.js} +1 -1
  45. package/dist/assets/miniChat-CR7k42bZ.js +2 -0
  46. package/dist/assets/mobile-rvypjUpd.js +2 -0
  47. package/dist/assets/multirun-DqmJk0mh.js +3 -0
  48. package/dist/assets/persistence-CwHY3fDg.js +3 -0
  49. package/dist/assets/pl-B714sdql.js +15 -0
  50. package/dist/assets/platform-Bxaalnc8.js +6 -0
  51. package/dist/assets/projectMeta-BhKfv5cj.js +1 -0
  52. package/dist/assets/{pt-BR-W09Q2pvn.js → pt-BR-DVoT_I7_.js} +4 -4
  53. package/dist/assets/{relay-image-stream-DrdVj99c.js → relay-image-stream-D2XooXKZ.js} +1 -1
  54. package/dist/assets/renderElectronMiniChatApp-BLay6f9j.js +2 -0
  55. package/dist/assets/renderMobileApp-BrikirqO.js +28 -0
  56. package/dist/assets/{runtime-fetch-Fs9ZV2V8.js → runtime-fetch-CiqQd__Y.js} +1 -1
  57. package/dist/assets/runtime-switch-BJM2k_uH.js +1 -0
  58. package/dist/assets/runtimeConfig-DjKsteH0.js +2 -0
  59. package/dist/assets/runtimeEndpointReset-D_vfNOQz.css +1 -0
  60. package/dist/assets/runtimeEndpointReset-DbfaE3dL.js +148 -0
  61. package/dist/assets/sessionChangeSummary-Cgq9RPUq.js +2 -0
  62. package/dist/assets/{sessionLookup-0Z4e-J1g.js → sessionLookup-BiE36BTy.js} +1 -1
  63. package/dist/assets/{sortable-tabs-strip-CE7W7lJ9.js → sortable-tabs-strip-CKKp4p3Y.js} +1 -1
  64. package/dist/assets/src-BZ0tISrf.css +2 -0
  65. package/dist/assets/{store-jaP79mqI.js → store-l_X_jQNz.js} +4 -4
  66. package/dist/assets/{terminalApi-mDR2jXTo.js → terminalApi-DSJOXNRQ.js} +1 -1
  67. package/dist/assets/{textarea-XD2oX_Ee.js → textarea-DUjc3iDJ.js} +1 -1
  68. package/dist/assets/{tooltip-DYeQ_Hvy.js → tooltip-CBHR50fA.js} +1 -1
  69. package/dist/assets/{uk-BgFzz8m2.js → uk-ChNnTgKP.js} +4 -4
  70. package/dist/assets/{url-CGbJbiJq.js → url-CEnO45_n.js} +1 -1
  71. package/dist/assets/{useAppFontEffects-y_GTVXhJ.js → useAppFontEffects-CWzjhjXr.js} +55 -55
  72. package/dist/assets/{useAssistantUIStore-Bz4tRflF.js → useAssistantUIStore-CXj8c_tt.js} +1 -1
  73. package/dist/assets/useDesktopWindowControlsLayout-Dtqco-Um.js +1 -0
  74. package/dist/assets/{useEffectiveDirectory-BW9jNyuK.js → useEffectiveDirectory-DNbDaaNV.js} +1 -1
  75. package/dist/assets/{useFontPreferences-Bjx8zkGM.js → useFontPreferences-DGbTdGy3.js} +1 -1
  76. package/dist/assets/useMobileNavigationStore-DYKC2swj.js +415 -0
  77. package/dist/assets/useSessionAutoCleanup-BI0xQcio.js +2 -0
  78. package/dist/assets/{useUIStore-syVmbuDA.js → useUIStore-DNkuudYO.js} +1 -1
  79. package/dist/assets/{useWorkerHighlightedLines-BUcSLKlL.js → useWorkerHighlightedLines-BoyYALWC.js} +1 -1
  80. package/dist/assets/vendor-opencode-client-MF2GBrFx.js +12 -0
  81. package/dist/assets/{vendor-zustand-B4dlGEM2.js → vendor-zustand-DzRqcymP.js} +1 -1
  82. package/dist/assets/{zh-CN-BhqSLgUY.js → zh-CN-DHEFf4u1.js} +4 -4
  83. package/dist/assets/{zh-TW-BsaBgY-p.js → zh-TW-rdSa8atW.js} +4 -4
  84. package/dist/index.html +13 -13
  85. package/dist/mini-chat.html +13 -13
  86. package/dist/mobile.html +13 -13
  87. package/package.json +3 -3
  88. package/server/index.js +8 -20
  89. package/server/lib/client-auth/remote-clients.js +15 -0
  90. package/server/lib/client-auth/remote-clients.test.js +42 -1
  91. package/server/lib/git/DOCUMENTATION.md +1 -0
  92. package/server/lib/git/routes.js +7 -4
  93. package/server/lib/git/routes.test.js +34 -0
  94. package/server/lib/git/service.js +3 -1
  95. package/server/lib/llm/DOCUMENTATION.md +6 -4
  96. package/server/lib/llm/generate.js +8 -9
  97. package/server/lib/llm/generate.test.js +33 -6
  98. package/server/lib/message-queue/service.js +1 -1
  99. package/server/lib/opencode/DOCUMENTATION.md +33 -22
  100. package/server/lib/opencode/agents.js +13 -22
  101. package/server/lib/opencode/auth-state-runtime.js +3 -3
  102. package/server/lib/opencode/auth-state-runtime.test.js +59 -0
  103. package/server/lib/opencode/auth.d.ts +13 -0
  104. package/server/lib/opencode/auth.js +74 -64
  105. package/server/lib/opencode/auth.test.js +57 -0
  106. package/server/lib/opencode/commands.js +1 -1
  107. package/server/lib/opencode/config-entities-layering.test.js +72 -0
  108. package/server/lib/opencode/config-entity-routes.js +10 -123
  109. package/server/lib/opencode/config-entity-routes.test.js +23 -11
  110. package/server/lib/opencode/config-files.d.ts +17 -0
  111. package/server/lib/opencode/config-files.js +187 -0
  112. package/server/lib/opencode/config-files.test.js +201 -0
  113. package/server/lib/opencode/core-routes.js +17 -1
  114. package/server/lib/opencode/core-routes.test.js +43 -15
  115. package/server/lib/opencode/credential-boundary.d.ts +1 -0
  116. package/server/lib/opencode/credential-boundary.js +11 -0
  117. package/server/lib/opencode/ensure-cli.js +4 -1
  118. package/server/lib/opencode/env-config.js +4 -1
  119. package/server/lib/opencode/env-config.test.js +28 -0
  120. package/server/lib/opencode/env-runtime.test.js +18 -1
  121. package/server/lib/opencode/feature-routes-runtime.js +9 -7
  122. package/server/lib/opencode/hmr-state-runtime.js +4 -4
  123. package/server/lib/opencode/lifecycle.js +1 -49
  124. package/server/lib/opencode/lifecycle.test.js +30 -4
  125. package/server/lib/opencode/location-release.d.ts +8 -0
  126. package/server/lib/opencode/location-release.js +32 -0
  127. package/server/lib/opencode/location-release.test.js +43 -0
  128. package/server/lib/opencode/mcp.js +1 -1
  129. package/server/lib/opencode/opencode2-pin.js +2 -1
  130. package/server/lib/opencode/owned-runtime-upgrade.test.js +5 -5
  131. package/server/lib/opencode/plugin-routes.test.js +4 -2
  132. package/server/lib/opencode/plugins.js +12 -60
  133. package/server/lib/opencode/plugins.test.js +5 -3
  134. package/server/lib/opencode/providers.js +6 -3
  135. package/server/lib/opencode/proxy.js +7 -0
  136. package/server/lib/opencode/proxy.test.js +13 -0
  137. package/server/lib/opencode/remote-upgrade.integration.test.js +66 -0
  138. package/server/lib/opencode/routes.js +10 -8
  139. package/server/lib/opencode/routes.test.js +51 -10
  140. package/server/lib/opencode/runtime-contract.js +3 -2
  141. package/server/lib/opencode/runtime-contract.test.js +12 -12
  142. package/server/lib/opencode/settings-normalization-runtime.js +3 -1
  143. package/server/lib/opencode/settings-normalization-runtime.test.js +36 -0
  144. package/server/lib/opencode/settings-runtime.test.js +58 -1
  145. package/server/lib/opencode/shared.js +6 -175
  146. package/server/lib/opencode/update-discovery.js +39 -0
  147. package/server/lib/opencode/update-discovery.test.js +39 -0
  148. package/server/lib/opencode/upgrade-screen.js +4 -11
  149. package/server/lib/opencode/upgrade-screen.test.js +25 -25
  150. package/server/lib/opencode/version-alignment.test.js +40 -0
  151. package/server/lib/question-auto-delegate/DOCUMENTATION.md +21 -6
  152. package/server/lib/question-auto-delegate/core.d.ts +5 -1
  153. package/server/lib/question-auto-delegate/core.js +21 -13
  154. package/server/lib/question-auto-delegate/form-io.d.ts +6 -0
  155. package/server/lib/question-auto-delegate/form-io.js +39 -0
  156. package/server/lib/question-auto-delegate/runtime.js +4 -24
  157. package/server/lib/question-auto-delegate/runtime.test.js +138 -0
  158. package/server/lib/quota/DOCUMENTATION.md +9 -0
  159. package/server/lib/quota/provider-credentials.test.js +29 -0
  160. package/server/lib/quota/providers/claude.js +4 -4
  161. package/server/lib/quota/providers/codex.js +4 -4
  162. package/server/lib/quota/providers/codex.test.js +1 -1
  163. package/server/lib/quota/providers/copilot.js +5 -5
  164. package/server/lib/quota/providers/google/auth.js +3 -3
  165. package/server/lib/quota/providers/google/index.js +2 -2
  166. package/server/lib/quota/providers/index.js +3 -7
  167. package/server/lib/quota/providers/index.test.js +2 -2
  168. package/server/lib/quota/providers/kimi.js +4 -4
  169. package/server/lib/quota/providers/minimax-shared.js +4 -4
  170. package/server/lib/quota/providers/nanogpt.js +4 -4
  171. package/server/lib/quota/providers/openai.js +4 -4
  172. package/server/lib/quota/providers/openrouter.js +4 -4
  173. package/server/lib/quota/providers/wafer.js +4 -4
  174. package/server/lib/quota/providers/zai.js +4 -4
  175. package/server/lib/quota/providers/zhipuai-coding-plan.js +6 -20
  176. package/server/lib/quota/routes.js +1 -1
  177. package/server/lib/relay/DOCUMENTATION.md +3 -1
  178. package/server/lib/relay/service.js +1 -0
  179. package/server/lib/relay/service.test.js +13 -0
  180. package/server/lib/scheduled-tasks/DOCUMENTATION.md +8 -1
  181. package/server/lib/scheduled-tasks/runtime.js +12 -4
  182. package/server/lib/scheduled-tasks/runtime.test.js +81 -6
  183. package/server/lib/session-goal/DOCUMENTATION.md +11 -0
  184. package/server/lib/session-goal/runtime.js +4 -4
  185. package/server/lib/session-goal/runtime.test.js +32 -3
  186. package/server/lib/session-index/DOCUMENTATION.md +7 -0
  187. package/server/lib/session-index/event-ingest.js +13 -0
  188. package/server/lib/session-index/event-ingest.test.js +7 -0
  189. package/server/lib/session-index/service.js +45 -0
  190. package/server/lib/session-index/service.test.js +47 -0
  191. package/server/lib/session-metadata/DOCUMENTATION.md +6 -0
  192. package/server/lib/session-metadata/session-metadata-store.test.js +23 -0
  193. package/server/lib/session-metadata/session-projection.d.ts +1 -0
  194. package/server/lib/session-metadata/session-projection.js +23 -0
  195. package/server/lib/session-metadata/session-projection.test.js +35 -0
  196. package/server/lib/session-title/DOCUMENTATION.md +52 -14
  197. package/server/lib/session-title/native-runtime.test.js +59 -5
  198. package/server/lib/session-title/runtime.js +63 -17
  199. package/server/lib/session-title/session-access.js +108 -3
  200. package/server/lib/session-title/session-access.test.js +125 -7
  201. package/server/lib/tts/DOCUMENTATION.md +7 -5
  202. package/server/lib/tts/credential-source.test.js +23 -0
  203. package/server/lib/tts/routes.js +2 -2
  204. package/server/lib/tts/service.js +16 -37
  205. package/dist/assets/AssistantView-CLFPj5Ki.js +0 -3
  206. package/dist/assets/Icon-B8fSTLdP.js +0 -1
  207. package/dist/assets/MarkdownRenderer-BK69MEHQ.js +0 -2
  208. package/dist/assets/MobileDetailNavigation-agQ-Yhpd.js +0 -1
  209. package/dist/assets/MobileSurface-D0HP_lTS.js +0 -2
  210. package/dist/assets/SettingsView-xLYb22wV.js +0 -25
  211. package/dist/assets/appThemeRegistry-LkVH62uF.js +0 -1
  212. package/dist/assets/desktopRelayRestore-CHEwrI93.js +0 -1
  213. package/dist/assets/dialog-CQ63sc9E.js +0 -1
  214. package/dist/assets/dropdown-menu.styles-Bzo6L3FH.js +0 -1
  215. package/dist/assets/fileMentionTouchSelection-CBzDuQxP.js +0 -3
  216. package/dist/assets/insertionBoundaries-DQrE5qG2.js +0 -1
  217. package/dist/assets/main-D2BzpnUt.css +0 -1
  218. package/dist/assets/main-DYISNvWY.js +0 -169
  219. package/dist/assets/miniChat-DYIqdWi7.js +0 -2
  220. package/dist/assets/mobile-CwiCcmWt.js +0 -2
  221. package/dist/assets/multirun-C3snjTFs.js +0 -4
  222. package/dist/assets/persistence-D0y7226-.js +0 -3
  223. package/dist/assets/pl-BCf5uLlK.js +0 -15
  224. package/dist/assets/platform-CurqO28D.js +0 -6
  225. package/dist/assets/projectMeta-BkhYFMaM.js +0 -3
  226. package/dist/assets/renderElectronMiniChatApp-7iyh_419.js +0 -2
  227. package/dist/assets/renderMobileApp-y0_NeckR.js +0 -28
  228. package/dist/assets/runtime-switch-LvWDU-SL.js +0 -1
  229. package/dist/assets/runtimeConfig-12C5j02f.js +0 -2
  230. package/dist/assets/runtimeEndpointReset-DTY2ZXXb.js +0 -149
  231. package/dist/assets/sessionChangeSummary-CwPBIb3Z.js +0 -2
  232. package/dist/assets/src-lS1keDVV.css +0 -2
  233. package/dist/assets/useDesktopWindowControlsLayout-CR_DcY77.js +0 -1
  234. package/dist/assets/useMobileNavigationStore-DwupvLSA.js +0 -414
  235. package/dist/assets/useSessionAutoCleanup-DkGO_usj.js +0 -2
  236. package/dist/assets/vendor-opencode-client-C12gfjag.js +0 -12
  237. package/server/lib/opencode/snippets.js +0 -242
  238. package/server/lib/opencode/snippets.test.js +0 -97
@@ -5,7 +5,7 @@ This module provides OpenCode server integration utilities for the web server ru
5
5
 
6
6
  ## Entrypoints and structure
7
7
  - `packages/web/server/lib/opencode/index.js`: public entrypoint (currently baseline placeholder).
8
- - `packages/web/server/lib/opencode/auth.js`: provider authentication file operations.
8
+ - `packages/web/server/lib/opencode/auth.js`: host-only authoritative provider credential API reads and deletion.
9
9
  - `packages/web/server/lib/opencode/auth-state-runtime.js`: managed OpenCode server auth password/header runtime.
10
10
  - `packages/web/server/lib/opencode/cli-options.js`: CLI/environment option parsing for server startup arguments.
11
11
  - `packages/web/server/lib/opencode/cli-entry-runtime.js`: CLI entrypoint runtime that detects direct execution, forces `OPENCHAMBER_RUNTIME=web` unless already `ssh-remote` (so `dev:server` cannot inherit a leftover desktop env and host a relay, while SSH-managed remotes may), parses CLI options, and starts server bootstrap.
@@ -15,7 +15,7 @@ This module provides OpenCode server integration utilities for the web server ru
15
15
  - `packages/web/server/lib/opencode/managed-capabilities-runtime.js`: managed-child scheduled-task resources, config injection, rotating bridge identity, and bridge authorization.
16
16
  - `packages/web/server/lib/opencode/env-runtime.js`: OpenCode CLI/binary resolution and shell environment runtime.
17
17
  - `packages/web/server/lib/opencode/ensure-cli.js`: detect local `opencode` version and install the pinned official V2 into the OpenChamber data dir when missing or too old. Never uses a packaged/bundled binary.
18
- - `packages/web/server/lib/opencode/upgrade-screen.js`: startup gate for a detected OpenCode below `2.0.15`. It does not own process lifecycle. `GET /api/opencode/compatibility` classifies the version already detected by startup. `POST /api/opencode/install-required` installs `max(pin, 2.0.15)` through `installPinnedOpenCode2Cli`, points the managed binary at that cache, and restarts through the existing lifecycle hooks. Success requires the running serve to report `2.0.15` or newer. External serves are not replaced. Failure returns `upgraded: false` and the error; it does not claim the previous process was restored.
18
+ - `packages/web/server/lib/opencode/upgrade-screen.js`: startup gate for the running OpenCode below `2.0.20`. Client/schema dependencies and the default CLI install target are pinned to `2.0.23`; the compatible execution floor is independently `2.0.20` (credential API). An unreadable running version is unavailable even when the local CLI is compatible. `POST /api/opencode/install-required` installs the pin into the owned cache and uses existing lifecycle hooks. Success requires the running service to meet both the floor and selected target. External services are not replaced; compatible newer shared services are not automatically downgraded. Failure restores binary selection and reports `upgraded: false`, without claiming process restoration.
19
19
  - `packages/web/server/lib/opencode/env-config.js`: OpenCode-related environment variable parsing and validation (host/port/hostname).
20
20
  - `packages/web/server/lib/opencode/hmr-state-runtime.js`: HMR-persistent runtime state initialization, auth-state bootstrap, and HMR sync helpers.
21
21
  - `packages/web/server/lib/opencode/bootstrap-runtime.js`: base app bootstrap runtime for status/auth/tts/notification/OpenChamber route wiring.
@@ -23,9 +23,9 @@ This module provides OpenCode server integration utilities for the web server ru
23
23
  - `packages/web/server/lib/opencode/project-directory-runtime.js`: request-scoped and settings-backed project directory resolution/validation runtime.
24
24
  - `packages/web/server/lib/opencode/config-entity-routes.js`: route registration for agent/command/MCP config orchestration and reload semantics.
25
25
  - `packages/web/server/lib/opencode/provider-catalog.js`: fail-closed projection for the safe provider catalog response.
26
- - `packages/web/server/lib/opencode/snippets.js`: opencode-snippets-compatible snippet file CRUD, discovery, and hashtag expansion.
27
26
  - `packages/web/server/lib/opencode/cli-options.js`: CLI/environment option parsing for server startup arguments.
28
27
  - `packages/web/server/lib/opencode/core-routes.js`: server status/system routes, auth/access guard routes, and settings utility route registration.
28
+ - `PATCH /api/client-auth/clients/:id` edits the stored device label (trimmed, 1–80 characters), returning `{ client }` without credentials. Owner UI sessions and local/paired desktop clients can edit any device; other client tokens can edit only themselves. Missing devices return 404, invalid labels 400, unauthorized targets 403. Renaming shares the client store mutation queue and preserves token identity, metadata and revocation. Web, Electron and mobile use the web `ClientAuthAPI`; VS Code does not expose client management.
29
29
  - `packages/web/server/lib/opencode/shutdown-runtime.js`: graceful shutdown orchestration runtime for watcher/session/terminal/process/server teardown.
30
30
  - `packages/web/server/lib/opencode/server-startup-runtime.js`: server listen/startup flow and process/signal handler orchestration runtime.
31
31
  - `packages/web/server/lib/opencode/static-routes-runtime.js`: static asset/SPA fallback route registration and manifest route wiring. The SPA fallback sends `index.html` relative to `root`, so deep links keep working when the install lives under a dot directory.
@@ -55,13 +55,12 @@ This module provides OpenCode server integration utilities for the web server ru
55
55
  - `packages/web/server/lib/ui-auth/ui-passkeys.js`: UI passkey storage and WebAuthn registration/authentication helpers (outside OpenCode module).
56
56
 
57
57
  ## Public exports (auth.js)
58
- - `readAuthFile()`: Reads and parses `~/.local/share/opencode/auth.json`.
59
- - `writeAuthFile(auth)`: Writes auth file with automatic backup.
60
- - `removeProviderAuth(providerId)`: Removes a provider's auth entry.
61
- - `getProviderAuth(providerId)`: Returns auth for a specific provider or null.
62
- - `listProviderAuths()`: Returns list of provider IDs with configured auth.
63
- - `AUTH_FILE`: Auth file path constant.
64
- - `OPENCODE_DATA_DIR`: OpenCode data directory path constant.
58
+ - `configureOpenCodeCredentials(resolveClient)`: host composition injects a call-time SDK client resolver; `null` detaches it. Web/Electron share server wiring; VS Code wires its manager in extension activation. Mobile clients use their host.
59
+ - `openCodeCredentialSource({ buildOpenCodeUrl, getOpenCodeAuthHeaders })`: resolves the current endpoint/auth at each read rather than capturing a previous runtime.
60
+ - `readOpenCodeCredentials()`: async SDK `credential.list()` read of the running service; validates entries and projects only active accounts into the quota/TTS credential shape. OAuth `metadata.accountID` becomes `accountId`. No local `auth.json`, local environment, or local provider-config fallback; external service environment-only keys are not exposed by the credential API.
61
+ - `getProviderAuth(providerId)`: async selected stored credential or authoritative `null`; network/auth/schema failures reject with a sanitized error, never empty success.
62
+ - `removeProviderAuth(providerId)`: deletes every stored account for that integration through SDK `credential.remove`, preserving unrelated integrations. Partial failure rejects explicitly; retry lists remaining accounts. Auth-only disconnect requires no process restart.
63
+ - Credential values remain host-only. `credential-boundary.js` blocks raw credential list/creation responses (GET/HEAD/POST `/api/credential`, including encoded/trailing-slash forms) in Web proxy and VS Code bridge because v2 replies contain keys. Provider-source responses expose only existence; errors never include upstream credential bodies/headers. Explicit legacy config-sync file transfer is not a current-service credential source or login operation.
65
64
 
66
65
  ## Public exports (shared.js)
67
66
  - `OPENCODE_CONFIG_DIR`, `AGENT_DIR`, `COMMAND_DIR`, `SKILL_DIR`, `CONFIG_FILE`, `CUSTOM_CONFIG_FILE`: Path constants.
@@ -87,11 +86,11 @@ This module provides OpenCode server integration utilities for the web server ru
87
86
  - `GET /api/config/opencode-resolution`
88
87
  - `GET /api/behavior/agents-md`: returns an authoritative empty document only when the file is absent (`ENOENT`); permission and I/O failures return HTTP 500.
89
88
  - `PUT /api/behavior/agents-md`
90
- - `POST /api/opencode/upgrade` (ticket 12): installs the pinned/target `@opencode/cli` into the **owned cache** at `~/.config/openchamber/opencode-cli/<version>/`. Managed owned-cache processes restart on that binary; official shared services use that exact binary's `service restart`, then adopt its registration again. Success requires a fresh running-serve version probe and runtime contract verification. Arbitrary external servers, private processes using global CLI, and private bundled binaries return HTTP 409; user installs are never overwritten. Concurrent upgrades are single-flight (`operation` state). Active sessions require `confirmActiveTasks`/`force`. Failure preserves cached binaries, restores the previous binary selection, and reports the failed phase; it does not claim the old process was restored. Shared-service recovery can subsequently re-ensure the registered service.
91
- - `GET /api/opencode/upgrade-status`: returns `currentVersion`/`serveVersion`/`cliVersion`, `targetVersion`/`latestVersion` (pin), `supplySource`, `ownership`, `canManage`, `management`, `guidance`, `contract`, and `operation`. Official shared services report `ownership: 'shared-service'`, `processOwnership: 'shared'`, and `canManage: true`. `available` is true only when `canManage` and target > current; unmanaged global/external runtimes report `available: null` plus manual guidance.
92
- - `GET /api/opencode/compatibility`: startup upgrade-screen status. `state` is `compatible` at OpenCode `2.0.15` or newer, `incompatible` when a detected version is older or not 2.x, and `unavailable` when no version could be read. `canInstall` is true only for a local (managed or bundled) runtime this platform can install into the owned cache. External serves are reported and not replaced.
93
- - `POST /api/opencode/install-required`: installs the upgrade-screen target (`2.0.15`, or the pin when the pin is newer) into the owned cache and restarts through the existing lifecycle. `upgraded: true` only after the running serve verifies at or above that target. Failure is `upgraded: false` plus `error`. Concurrent installs are single-flight.
94
- - `GET /api/opencode/contract` (ticket 11): runtime contract admission for the **running serve** (not health alone). Dimensions: reachable / authenticated / protocolCompatible / executionAllowed / capabilities / migration / CLI↔serve mismatch. Floor `2.0.12` (acceptable OpenCode 2.x at or above); below-min 2.x and 1.x block execution with upgrade guidance; no upper version cap.
89
+ - `POST /api/opencode/upgrade` (ticket 12): installs the remote latest or explicitly targeted `@opencode/cli` into the **owned cache** at `~/.config/openchamber/opencode-cli/<version>/`. Managed owned-cache processes restart on that binary; official shared services use that exact binary's `service restart`, then adopt its registration again. Success requires a fresh running-serve version probe and runtime contract verification. Arbitrary external servers, private processes using global CLI, and private bundled binaries return HTTP 409; user installs are never overwritten. Concurrent upgrades are single-flight (`operation` state). Active sessions require `confirmActiveTasks`/`force`. Failure preserves cached binaries, restores the previous binary selection, and reports the failed phase; it does not claim the old process was restored. Shared-service recovery can subsequently re-ensure the registered service.
90
+ - `GET /api/opencode/upgrade-status`: returns `currentVersion`/`serveVersion`/`cliVersion`, `targetVersion`/`latestVersion` (official npm `@opencode/cli` latest stable 2.x release), `supplySource`, `ownership`, `canManage`, `management`, `guidance`, `contract`, and `operation`. Official shared services report `ownership: 'shared-service'`, `processOwnership: 'shared'`, and `canManage: true`. `available` is true only when `canManage` and target > current; unmanaged global/external runtimes report `available: null` plus manual guidance. `update-discovery.js` shares one in-flight registry request per host, caches successful checks for five minutes and failed checks for thirty seconds, with a five-second request deadline. Failed registry checks return HTTP 500 with `available: null`, never a false up-to-date result. An upgrade without an explicit target resolves this same remote release. Shared Web/Electron/hosted-mobile/Capacitor clients check after startup, every five minutes while visible, on focus/reconnect, and on runtime changes; query results and dismissed notifications are scoped to the host. Successful upgrade already includes process restart and live version verification, so no second reload is needed.
91
+ - `GET /api/opencode/compatibility`: startup upgrade-screen status. `compatible` requires the running OpenCode 2.x at `2.0.20` or newer; older/invalid versions are `incompatible`; unreadable running versions are `unavailable`. External serves are not replaced.
92
+ - `POST /api/opencode/install-required`: installs the pin (`2.0.23`, never below the `2.0.20` floor) into the owned cache and restarts through the existing lifecycle. `upgraded: true` requires live version verification. Concurrent installs are single-flight; failures remain errors.
93
+ - `GET /api/opencode/contract`: runtime contract admission for the **running serve**. Reachability, auth, execution, migration, and CLI↔serve mismatch are separate. The `2.0.20` floor is independent of the `2.0.23` install pin; compatible newer 2.x services remain admitted.
95
94
  - `GET /api/opencode/health`: probes managed OpenCode `GET /api/info` (Basic auth, 4s timeout). `healthy` is reachability/version shape via `evaluateOpenCodeHealthBody` (admits official 2.x `ServerInfo` and classic `{ healthy: true, version }`; rejects 1.x / missing / `healthy: false`). Response also includes `executionAllowed`, `protocolCompatible`, and `contract` so clients do not treat health success as full execution semantics. Upstream non-OK responses pass through status with `{ healthy: false, error, contract }`; transport failures return HTTP 503.
96
95
  - `GET /api/opencode/version`: serve + CLI versions, mismatch flag, and contract snapshot
97
96
  - `POST /api/opencode/directory`
@@ -130,7 +129,7 @@ This module provides OpenCode server integration utilities for the web server ru
130
129
  - `resolveOpenCode2UpgradeTarget(target)` / `rejectOpenCode1xUpgradeTarget(target)`: upgrade targets default to the pin and refuse 1.x.
131
130
 
132
131
  ## Public exports (runtime-contract.js)
133
- - Ticket 11 pure admission matrix. `RUNTIME_CONTRACT_MIN_VERIFIED` (`2.0.12`) is the documented 2.x floor; capabilities include `core.protocol`, `session.prompt`, `session.queuedInput`, `session.background`, `session.generationFallback`, `migration.v1Read`.
132
+ - Pure admission matrix. `RUNTIME_CONTRACT_MIN_VERIFIED` (`2.0.20`) is the documented 2.x floor, shared with the startup gate through `MINIMUM_OPENCODE2_VERSION`; the VS Code sidecar mirrors this floor. Capabilities include `core.protocol`, `session.prompt`, `session.queuedInput`, `session.background`, `session.generationFallback`, `migration.v1Read`.
134
133
  - `evaluateRuntimeContract(input)`: separate reachable / auth / protocol / execution / migration / CLI↔serve mismatch reasons for UI and diagnostics. **`executionAllowed` requires acceptable OpenCode 2.x at or above the floor** (`versionBand === 'verified'`).
135
134
  - `formatRuntimeContractBlockedError(contract)` / `runtimeContractExecutionBlockedBody(contract)`: user-facing upgrade guidance on HTTP 409 execution blocks (below-min / 1.x), not raw JSON dumps.
136
135
  - `isRuntimeContractExecutionPath` / `isRuntimeContractDiagnosticPath` / `isRuntimeContractStopPath` / `shouldBlockRuntimeContractExecution` / `createRevokedRuntimeContract`: proxy + Host transport gate helpers. **Execution paths require `executionAllowed === true` on the current contract**; null/pending/false all block (no silent bypass). Unit/DI factories may pass `{ allowMissingContract: true }` only when no lifecycle is wired. Interrupt/abort and reads stay available when execution is limited. Health success is not full execution semantics.
@@ -155,7 +154,7 @@ This module provides OpenCode server integration utilities for the web server ru
155
154
  - `fetchV1MigrationGate({ url, headers, signal, fetchImpl })`: GET-only status poll used by lifecycle.
156
155
 
157
156
  ## Public exports (lifecycle.js)
158
- - `createOpenCodeLifecycleRuntime(dependencies)`: creates lifecycle runtime for managed/external OpenCode process orchestration. Bootstrap reuses a healthy v2 `opencode serve` when present; otherwise starts managed `opencode serve`. Before managed spawn, `ensurePinnedOpenCode2CliEnv()` detects the local CLI version and installs the pinned official V2 when missing or too old (skipped for external skip-start). Startup accepts both `server listening on http://127.0.0.1:PORT` and the legacy `opencode server listening on …` line. Readiness/health probes `GET /api/info` first, then `/global/health`, with Basic auth (username `opencode`). A body is admitted only when `evaluateOpenCodeHealthBody` accepts the version (rejects 1.x and missing/unknown). After health ok, `startOpenCode` / `waitForOpenCodeReady` poll `GET /api/experimental/migration/v1` (no POST; backfill is owned by opencode2). Each readiness attempt uses one abort timer that covers **both** health and migration so a hung migration cannot outrun the per-attempt budget. External instances are a read-only mount: restart/refresh only re-probe health + V1 migration (never kill, spawn, clear port, or flip `isExternalOpenCode`); health monitoring skips them; upgrade routes return 409. After readiness admits, lifecycle warms recent directories via optional `getWarmupDirectories` (sequential best-effort `GET /api/session?directory=&limit=1`). External skip-start / auto-detect attach leave `isOpenCodeReady` false until the gate admits. `isOpenCodeReady` means transcript may be fetched only when the gate admits (`completed`, or no V1 library such as HTTP 404). `required` / `running` / `error` keep the ready gate closed; `error` is retried. The last gate result is stored on `state.v1Migration` and published on the OpenChamber `/health` snapshot so UI can render `phase` and running `progress` (`label` / `numerator` / `denominator`) plus `userNotice` (reuse message ids; in-progress tools become interrupted; V1 subtasks do not appear in v2).
157
+ - `createOpenCodeLifecycleRuntime(dependencies)`: creates lifecycle runtime for managed/external OpenCode process orchestration. Bootstrap reuses a healthy v2 `opencode serve` when present; otherwise starts managed `opencode serve`. Before managed spawn, `ensurePinnedOpenCode2CliEnv()` detects the local CLI version and installs the pinned official V2 when missing or too old (skipped for external skip-start). Startup accepts both `server listening on http://127.0.0.1:PORT` and the legacy `opencode server listening on …` line. Readiness/health probes `GET /api/info` first, then `/global/health`, with Basic auth (username `opencode`). A body is admitted only when `evaluateOpenCodeHealthBody` accepts the version (rejects 1.x and missing/unknown). After health ok, `startOpenCode` / `waitForOpenCodeReady` poll `GET /api/experimental/migration/v1` (no POST; backfill is owned by opencode2). Each readiness attempt uses one abort timer that covers **both** health and migration so a hung migration cannot outrun the per-attempt budget. External instances are a read-only mount: restart/refresh only re-probe health + V1 migration (never kill, spawn, clear port, or flip `isExternalOpenCode`); health monitoring skips them; upgrade routes return 409. Startup does not prewarm persisted project directories: v2 session lists are storage-scoped and do not initialize locations, while location-scoped reads must follow actual consumer demand. External skip-start / auto-detect attach leave `isOpenCodeReady` false until the gate admits. `isOpenCodeReady` means transcript may be fetched only when the gate admits (`completed`, or no V1 library such as HTTP 404). `required` / `running` / `error` keep the ready gate closed; `error` is retried. The last gate result is stored on `state.v1Migration` and published on the OpenChamber `/health` snapshot so UI can render `phase` and running `progress` (`label` / `numerator` / `denominator`) plus `userNotice` (reuse message ids; in-progress tools become interrupted; V1 subtasks do not appear in v2).
159
158
  - Shared official service (default for Web and Electron hosts, including mobile clients connected to those hosts): when `sharedService` is injected and neither an OpenCode port nor host is configured, bootstrap runs `opencode service start` before HMR/private-process reuse or any port-4096 probe. It attaches to the registered stable-channel service, adopts its password/base URL/port, and marks it external for shutdown. Closing OpenChamber leaves that service running; no managed capabilities (scheduled-task plugin, bridge token, `v2-plugin-host-shim`) are injected. Attach failure clears URL/port/password, reports an unavailable integration and permits explicit retry, without silently spawning a private server. Explicit host/port and shared-service opt-out retain their existing behavior; VS Code retains its extension-owned sidecar lifecycle.
160
159
  - While attached, ordinary `restartOpenCode()` re-runs `service start` and adopts the latest registration before re-probing; a failed ensure propagates instead of probing stale credentials as success. Upgrade-only `restartOpenCode({ binaryPath })` bypasses settings rediscovery and runs the verified target's `service restart`. Configuration refresh uses `POST /api/location/reload` instead of process replacement (returns `{ reloaded: true, external: true }`). Health monitoring uses registration-PID liveness. Normal attachment/recovery does not version-pin or downgrade a healthy registered service; explicit in-app upgrade is the version-replacement operation.
161
160
  - Returned API:
@@ -180,6 +179,7 @@ This module provides OpenCode server integration utilities for the web server ru
180
179
  - `startSharedOpenCodeService({ binary, args, env, spawnImpl, timeoutMs, replace })`: runs `<binary> [...args] service start`, or `service restart` for explicit `replace: true`; rejects with the stderr tail on non-zero exit or timeout. The launcher has hidden Windows console and does not own the detached service lifetime.
181
180
  - `isProcessAlive(pid)`, `createSharedOpenCodeService({ envLike, fsLike, spawnImpl })`: the lifecycle dependency (`start`, `readRegistration`, `buildStartEnv`).
182
181
  - `shared-service.integration.test.js` is opt-in via `OPENCHAMBER_TEST_OPENCODE_BINARY`; it isolates HOME/XDG/config/database and verifies launcher exit survival, PID reuse, explicit replacement and authenticated health, then stops only its isolated service.
182
+ - `remote-upgrade.integration.test.js` is opt-in via `OPENCHAMBER_TEST_REMOTE_UPGRADE=1`; it downloads official binaries, starts an isolated 2.0.16 shared service, discovers the remote latest release through the status route, upgrades through the HTTP route, and verifies the running version and cleared update availability. It never restarts the user's shared service.
183
183
 
184
184
  ## Public exports (env-runtime.js)
185
185
  - `createOpenCodeEnvRuntime(dependencies)`: creates runtime that owns OpenCode CLI environment and binary discovery state. Discovery order: settings/env, then PATH `opencode`, then known install locations, then a previously installed pin under the OpenChamber data dir. Automatic discovery and bundled resources use only `opencode` (`opencode.exe` on Windows); 1.x binaries are skipped by `--version`, not by basename. `resolveOpencodeCliPath()` never prefers a packaged/bundled Electron extraResource. Managed startup calls `ensurePinnedOpenCode2CliEnv()` and installs the pin only when no usable 2.x CLI exists; if pin install fails (or auto-install is disabled), it may last-resort to a 2.x binary under `OPENCHAMBER_BUNDLED_OPENCODE_CLI_DIR` or `process.resourcesPath/opencode-cli` (source `bundled`). Explicit `OPENCODE_BINARY` that reports 1.x fails closed with `OPENCODE_BINARY_INVALID`. Bootstrap reuses a healthy v2 `opencode serve` on port 4096 when present; otherwise it starts generic `opencode serve`.
@@ -209,6 +209,7 @@ This module provides OpenCode server integration utilities for the web server ru
209
209
 
210
210
  ## Public exports (env-config.js)
211
211
  - `resolveOpenCodeEnvConfig(options?)`: resolves and validates OpenCode host/port/hostname environment configuration.
212
+ - `OPENCODE_HOST` still requires an explicit port; HTTP `:80` and HTTPS `:443` retain their effective port even when URL normalization removes it. Host precedence, IPv6 and path/query/hash validation are unchanged.
212
213
  - Returned object fields:
213
214
  - `configuredOpenCodePort`
214
215
  - `configuredOpenCodeHost`
@@ -265,6 +266,7 @@ This module provides OpenCode server integration utilities for the web server ru
265
266
 
266
267
  ## Public exports (settings-normalization-runtime.js)
267
268
  - `createSettingsNormalizationRuntime(dependencies)`: creates normalization/sanitization runtime for shared settings helper logic.
269
+ - Filesystem path normalization prefers `realpathSync.native` (falling back to the injected function when native is unavailable), so case-insensitive volumes use the same spelling as Git/OpenCode. It never lowercases directory names; inaccessible paths keep their original value. Existing settings read/write normalization repairs project paths and the existing deterministic-ID migration carries project storage/icons and active selection to the resulting ID. Web, in-process Electron, and hosted/native mobile use this Host boundary; extension-only VS Code retains its workspace-folder ownership.
268
270
  - Returned API:
269
271
  - `normalizeDirectoryPath(value)`
270
272
  - `normalizePathForPersistence(value)`
@@ -321,6 +323,10 @@ restart, then terminates only its own child and removes its temporary files.
321
323
  It makes no model requests and never reads real user credentials.
322
324
 
323
325
  ## Public exports (config-entity-routes.js)
326
+ The provider catalog's optional `model.default` lookup may fail independently:
327
+ available provider/model lists still return successfully without defaults.
328
+ Failures of either required list still return HTTP 502 for the caller to retry.
329
+
324
330
  - `registerConfigEntityRoutes(app, dependencies)`: registers configuration entity routes:
325
331
  - `GET /api/config/catalog/providers` resolves the request project directory, composes official v2 `provider.list` + `model.list` (+ `model.default`), and returns schema version `1` with an allowlisted provider/model catalog. Catalog model ids and defaults use `ModelInfo.id` (the external generate/session id). `ModelInfo.modelID` is an internal field and is not the catalog key. The route is available through the shared web host used by Web, Electron, hosted mobile, and Capacitor mobile, before generic OpenCode proxy handling.
326
332
  - Provider catalog responses include only provider `id`, `name`, and safe models. Safe models include `id`, `name`, fixed text/audio/image/video/pdf capability modalities, bounded cost/limit fields, `release_date`, and variant names with `{}` values. v2 `ModelInfo.variants` is an array of `{ id, settings?, headers?, body? }`; projection keeps ids in array order as empty-object keys and drops request payloads. v2 `capabilities.tools` maps to `toolcall`, and `input`/`output` modality name arrays map to boolean maps. v2 `cost` tier arrays keep the untiered base price. Identifiers are bounded to 512 characters, display names to 1024, release dates to 64, numeric values to absolute 1e9, and catalog collections to 200 providers, 500 models per provider, and 100 defaults or variants. Provider credentials/configuration and unallowlisted model fields stay server-side. Malformed catalog roots and SDK error envelopes return HTTP 502. `partial: true` is structural only: provider/model/default truncation, dropped providers/models/defaults, or truncated variant dictionaries. Soft allowlist stripping of optional metadata (unknown modalities, non-boolean capability flags, out-of-range cost/limit numbers, empty/null/invalid `release_date`, invalid variant ids) keeps valid models and must not set `partial: true`, or UI refresh retains a stale complete snapshot after OpenCode provider updates. The same input shapes are accepted by the VS Code projector and `packages/ui/src/lib/configCatalogParser.ts`.
@@ -328,7 +334,6 @@ It makes no model requests and never reads real user credentials.
328
334
  - Commands: batched metadata via `POST /api/config/commands/metadata`; `{ catalog: true }` returns the compact autocomplete catalog without templates, plus CRUD at `/api/config/commands/:name`
329
335
  - Global raw configs: `GET /api/config/global` discovers existing config targets; `GET/PUT /api/config/global/:target` reads and writes `opencode`, `oh-my-opencode-slim`, and `oh-my-openagent` JSON or JSONC files
330
336
  - MCP servers: `/api/config/mcp` and `/api/config/mcp/:name`
331
- - Snippets: `/api/config/snippets`, `/api/config/snippets/:name`, and `/api/config/snippets/expand`
332
337
 
333
338
  ### Catalog / OpenChamber-owned API change checklist
334
339
 
@@ -341,6 +346,7 @@ When adding or changing Host HTTP APIs that mobile/desktop clients reach over Pr
341
346
  5. Coordinate `partial` semantics between Host projection, VS Code projection (`packages/vscode/src/provider-catalog-runtime.ts`), and `packages/ui/src/lib/configCatalogParser.ts`. Reserve `partial: true` for structural incompleteness (dropped/truncated providers, models, defaults, variants). Soft metadata stripping must stay non-partial; a false `partial: true` blocks UI refresh of an existing complete snapshot and can leave model pickers on stale catalogs after OpenCode provider updates.
342
347
 
343
348
  ## Public exports (auth-state-runtime.js)
349
+ - Initial user auth prefers `OPENCODE_PASSWORD` over `OPENCODE_SERVER_PASSWORD`; HMR preserves the captured choice and active password. Private child environments set both names to the active header password after merging shell/host env. Registered shared-service auth is authoritative (including no password), uses username `opencode`, and never falls back to private env credentials.
344
350
  - `createOpenCodeAuthStateRuntime(dependencies)`: creates runtime for managed OpenCode auth password state and request headers.
345
351
  - Returned API:
346
352
  - `getOpenCodeAuthHeaders()`
@@ -508,16 +514,21 @@ The V2 skill catalog uses `id` as the existing OpenChamber `name` invocation key
508
514
  reuse the existing UI-password / CORS path, register before the generic proxy,
509
515
  and return 501 when the service is disabled. Logs never include message
510
516
  bodies, parts, or tokens.
511
- - Provider auth: `~/.local/share/opencode/auth.json`.
512
- - User config: `~/.config/opencode/opencode.json` or `~/.config/opencode/opencode.jsonc`.
513
- - Project config: `<workingDirectory>/.opencode/opencode.json` or `opencode.json`.
517
+ - Provider auth: running OpenCode v2 credential API (no direct access to legacy auth files or OpenCode database).
518
+ - `config-files.js` owns physical JSON/JSONC discovery, layer provenance and structural writes for Web/Electron and the VS Code extension. Global directory: `OPENCODE_CONFIG_DIR`, otherwise `$XDG_CONFIG_HOME/opencode` or `~/.config/opencode`.
519
+ - Global documents load `opencode.json` then `opencode.jsonc`; V2 does not load legacy `config.json`. Both are retained as separate writable documents. `userConfig` / `projectConfig` are merged read views; mutations must use `getConfigForPath`, `getJsonEntrySource`, or `getJsonWriteTarget`, never persist a merged view into one file.
520
+ - Project documents load from filesystem root to the working directory, all direct JSON/JSONC files first, then all `.opencode` files. `documents` records low-to-high precedence; `userLayers` / `projectLayers` retain physical sources. Custom files sit above global files and below project documents, matching V2 `ConfigDiscovery.names` and `Config.load`.
514
521
  - Custom config: `OPENCODE_CONFIG` env var path.
522
+ - Raw global editors in Web and VS Code edit the explicit custom file when set, otherwise the highest-precedence existing global document (JSONC wins). New global documents default to `opencode.jsonc`. Raw content is one physical document, never a synthetic merged serialization.
523
+ - Missing, whitespace-only and comment-only files are empty layers. Malformed/non-object JSONC and IO failures throw; partial parser results never become empty success. Both raw and structured writers validate the existing document before replacement.
524
+ - Structured writes diff object fields, preserve untouched comments/formatting (including BOM/CRLF), and remove properties with adjacent separator commas without consuming surrounding comments. Arrays are replaced as owned values. A failed round-trip aborts instead of falling back to whole-file serialization.
525
+ - Writes stage a same-directory temporary file, preserve file mode, create `.openchamber.backup`, then atomically rename; failed staging/backup/rename leaves the original intact and cleans the temporary file. Existing symlinks are resolved first: staging/rename target the real file, the link remains unchanged, and the backup stays beside the supplied config path. Dangling symlinks fail before staging; existing target permissions are restored on the staged file regardless of umask. Deleting an entity removes it from its selected source document; lower-precedence definitions may become visible afterward.
515
526
  - Rate limit config: `OPENCHAMBER_RATE_LIMIT_MAX_ATTEMPTS`, `OPENCHAMBER_RATE_LIMIT_NO_IP_MAX_ATTEMPTS` env vars.
516
527
 
517
528
  ## Notes for contributors
518
529
  - This module serves as foundation for OpenCode-related server utilities.
519
530
  - Route ownership moved to module-level `routes.js`; `index.js` wires dependencies only.
520
531
  - All file writes include automatic backup before modification.
521
- - Config merging follows priority: custom > project > user.
532
+ - Config merging follows priority: project `.opencode` > project direct > custom > global JSONC > global JSON.
522
533
  - UI auth uses scrypt for password hashing with constant-time comparison.
523
534
  - Tunnel auth treats `host.docker.internal` as local-only when the socket remote IP is private/loopback.
@@ -197,23 +197,12 @@ function getAgentPermissionSource(agentName, workingDirectory, lookupCache = nul
197
197
  }
198
198
  }
199
199
 
200
- // Check JSON layers in effective override order. readConfigLayers merges
201
- // user -> project -> custom, so custom wins over project, project over user.
200
+ // Check physical documents in effective override order, including both global files.
202
201
  const layers = readConfigLayers(workingDirectory);
203
-
204
- const customJsonPermission = layers.customConfig?.agent?.[agentName]?.permission;
205
- if (customJsonPermission !== undefined && layers.paths.customPath) {
206
- return { source: 'json', scope: 'custom', path: layers.paths.customPath };
207
- }
208
-
209
- const projectJsonPermission = layers.projectConfig?.agent?.[agentName]?.permission;
210
- if (projectJsonPermission !== undefined && layers.paths.projectPath) {
211
- return { source: 'json', scope: AGENT_SCOPE.PROJECT, path: layers.paths.projectPath };
212
- }
213
-
214
- const userJsonPermission = layers.userConfig?.agent?.[agentName]?.permission;
215
- if (userJsonPermission !== undefined) {
216
- return { source: 'json', scope: AGENT_SCOPE.USER, path: layers.paths.userPath };
202
+ for (const layer of layers.documents.toReversed()) {
203
+ if (layer.config?.agent?.[agentName]?.permission !== undefined) {
204
+ return { source: 'json', scope: layer.scope, path: layer.path };
205
+ }
217
206
  }
218
207
 
219
208
  return { source: null, scope: null, path: null };
@@ -300,7 +289,7 @@ function getAgentSources(agentName, workingDirectory, lookupCache = createAgentL
300
289
  const jsonSource = getJsonEntrySource(layers, 'agent', agentName);
301
290
  const jsonSection = jsonSource.section;
302
291
  const jsonPath = jsonSource.path || layers.paths.customPath || layers.paths.projectPath || layers.paths.userPath;
303
- const jsonScope = jsonSource.path === layers.paths.projectPath ? AGENT_SCOPE.PROJECT : AGENT_SCOPE.USER;
292
+ const jsonScope = layers.projectLayers.some((layer) => layer.path === jsonSource.path) ? AGENT_SCOPE.PROJECT : AGENT_SCOPE.USER;
304
293
 
305
294
  const sources = {
306
295
  md: {
@@ -444,7 +433,7 @@ function getAgentConfig(agentName, workingDirectory, lookupCache = createAgentLo
444
433
  const jsonSource = getJsonEntrySource(layers, 'agent', agentName);
445
434
 
446
435
  if (jsonSource.exists && jsonSource.section) {
447
- const scope = jsonSource.path === layers.paths.projectPath ? AGENT_SCOPE.PROJECT : AGENT_SCOPE.USER;
436
+ const scope = layers.projectLayers.some((layer) => layer.path === jsonSource.path) ? AGENT_SCOPE.PROJECT : AGENT_SCOPE.USER;
448
437
  return {
449
438
  source: 'json',
450
439
  scope,
@@ -756,8 +745,9 @@ function deleteAgent(agentName, workingDirectory, scope) {
756
745
  const layers = readConfigLayers(workingDirectory);
757
746
 
758
747
  if (requestedScope === AGENT_SCOPE.PROJECT) {
759
- if (layers.paths.projectPath && deleteJsonAgentEntry(layers.projectConfig, agentName)) {
760
- writeConfig(layers.projectConfig, layers.paths.projectPath);
748
+ const target = layers.projectLayers.toReversed().find((layer) => layer.config?.agent?.[agentName] !== undefined);
749
+ if (target && deleteJsonAgentEntry(target.config, agentName)) {
750
+ writeConfig(target.config, target.path);
761
751
  console.log(`Removed project-level agent from opencode.json: ${agentName}`);
762
752
  return;
763
753
  }
@@ -765,8 +755,9 @@ function deleteAgent(agentName, workingDirectory, scope) {
765
755
  }
766
756
 
767
757
  if (requestedScope === AGENT_SCOPE.USER) {
768
- const userJsonPath = layers.paths.customPath || layers.paths.userPath;
769
- const userJsonConfig = layers.paths.customPath ? layers.customConfig : layers.userConfig;
758
+ const target = layers.documents.toReversed().find((layer) => layer.scope !== 'project' && layer.config?.agent?.[agentName] !== undefined);
759
+ const userJsonPath = target?.path;
760
+ const userJsonConfig = target?.config;
770
761
  if (userJsonPath && deleteJsonAgentEntry(userJsonConfig, agentName)) {
771
762
  writeConfig(userJsonConfig, userJsonPath);
772
763
  console.log(`Removed user-level agent from opencode.json: ${agentName}`);
@@ -31,7 +31,7 @@ export const createOpenCodeAuthStateRuntime = (dependencies) => {
31
31
  const normalized = normalizeOpenCodePassword(password);
32
32
  if (!isValidOpenCodePassword(normalized)) {
33
33
  setAuthPassword(null);
34
- setAuthSource(null);
34
+ setAuthSource(source || null);
35
35
  delete process.env.OPENCODE_SERVER_PASSWORD;
36
36
  syncToHmrState();
37
37
  return null;
@@ -45,13 +45,13 @@ export const createOpenCodeAuthStateRuntime = (dependencies) => {
45
45
  };
46
46
 
47
47
  const getOpenCodeAuthHeaders = () => {
48
- const password = normalizeOpenCodePassword(getAuthPassword() || process.env.OPENCODE_SERVER_PASSWORD || '');
48
+ const password = normalizeOpenCodePassword(getAuthPassword() || '');
49
49
 
50
50
  if (!password) {
51
51
  return {};
52
52
  }
53
53
 
54
- const username = process.env.OPENCODE_SERVER_USERNAME?.trim() || 'opencode';
54
+ const username = getAuthSource() === 'shared-service' ? 'opencode' : process.env.OPENCODE_SERVER_USERNAME?.trim() || 'opencode';
55
55
  const credentials = Buffer.from(`${username}:${password}`).toString('base64');
56
56
  return { Authorization: `Basic ${credentials}` };
57
57
  };
@@ -0,0 +1,59 @@
1
+ import crypto from 'node:crypto';
2
+ import { describe, expect, it } from 'vitest';
3
+ import { createOpenCodeAuthStateRuntime } from './auth-state-runtime.js';
4
+ import { createHmrStateRuntime } from './hmr-state-runtime.js';
5
+
6
+ const setup = (env = {}) => {
7
+ const processLike = { env: { ...env } };
8
+ const hmr = createHmrStateRuntime({ globalThisLike: {}, os: { homedir: () => '/example' }, processLike, stateKey: 'test' });
9
+ const state = hmr.getOrCreateHmrState();
10
+ hmr.ensureUserProvidedOpenCodePassword(state);
11
+ const restore = () => hmr.restoreRuntimeFromState({ hmrState: state, userProvidedOpenCodePassword: hmr.getUserProvidedOpenCodePassword(state) });
12
+ const runtimeState = restore();
13
+ const auth = createOpenCodeAuthStateRuntime({ crypto, process: processLike,
14
+ getAuthPassword: () => runtimeState.openCodeAuthPassword,
15
+ setAuthPassword: (value) => { runtimeState.openCodeAuthPassword = value; },
16
+ getAuthSource: () => runtimeState.openCodeAuthSource,
17
+ setAuthSource: (value) => { runtimeState.openCodeAuthSource = value; },
18
+ getUserProvidedPassword: () => hmr.getUserProvidedOpenCodePassword(state),
19
+ syncToHmrState: () => hmr.syncStateFromRuntime(state, runtimeState),
20
+ });
21
+ return { auth, restore, runtimeState, processLike, hmr, state };
22
+ };
23
+
24
+ describe('OpenCode auth ownership', () => {
25
+ it('uses v2 before legacy credentials and preserves the captured choice through HMR/restart', async () => {
26
+ const test = setup({ OPENCODE_PASSWORD: 'v2-fixture', OPENCODE_SERVER_PASSWORD: 'legacy-fixture' });
27
+ const password = await test.auth.ensureLocalOpenCodeServerPassword({ rotateManaged: true });
28
+ expect(password === 'v2-fixture').toBe(true);
29
+ expect(test.auth.getOpenCodeAuthHeaders().Authorization === `Basic ${Buffer.from(`opencode:${password}`).toString('base64')}`).toBe(true);
30
+ test.processLike.env.OPENCODE_PASSWORD = 'later-shell-fixture';
31
+ test.hmr.ensureUserProvidedOpenCodePassword(test.state);
32
+ expect((await test.auth.ensureLocalOpenCodeServerPassword({ rotateManaged: true })) === password).toBe(true);
33
+ expect(test.restore().openCodeAuthPassword === password).toBe(true);
34
+ });
35
+
36
+ it('rotates generated credentials while old credentials fail a controlled auth check', async () => {
37
+ const { auth, restore } = setup();
38
+ const old = await auth.ensureLocalOpenCodeServerPassword();
39
+ const previousHeader = auth.getOpenCodeAuthHeaders().Authorization;
40
+ const password = await auth.ensureLocalOpenCodeServerPassword({ rotateManaged: true });
41
+ const accepts = (header) => header === `Basic ${Buffer.from(`opencode:${password}`).toString('base64')}`;
42
+ expect(password !== old).toBe(true);
43
+ expect(accepts(previousHeader)).toBe(false);
44
+ expect(accepts(auth.getOpenCodeAuthHeaders().Authorization)).toBe(true);
45
+ expect(restore().openCodeAuthPassword === password).toBe(true);
46
+ });
47
+
48
+ it('adopts only shared registration credentials and never falls back to private env after clearing', () => {
49
+ const { auth, processLike, restore } = setup({ OPENCODE_PASSWORD: 'private-fixture', OPENCODE_SERVER_USERNAME: 'private-user' });
50
+ auth.adoptOpenCodeServerPassword('registration-fixture', 'shared-service');
51
+ expect(auth.getOpenCodeAuthHeaders().Authorization === `Basic ${Buffer.from('opencode:registration-fixture').toString('base64')}`).toBe(true);
52
+ auth.adoptOpenCodeServerPassword(null, 'shared-service');
53
+ expect(restore().openCodeAuthPassword).toBeNull();
54
+ expect(restore().openCodeAuthSource).toBe('shared-service');
55
+ auth.adoptOpenCodeServerPassword(null, null);
56
+ processLike.env.OPENCODE_SERVER_PASSWORD = 'stale-fixture';
57
+ expect(auth.getOpenCodeAuthHeaders()).toEqual({});
58
+ });
59
+ });
@@ -0,0 +1,13 @@
1
+ import type { OpenCodeClient } from '@opencode/client';
2
+
3
+ export type ProviderCredential =
4
+ | { type: 'api'; key: string }
5
+ | { type: 'oauth'; access: string; refresh: string; expires: number; accountId?: string; enterpriseUrl?: string };
6
+ export function configureOpenCodeCredentials(resolveClient: (() => OpenCodeClient) | null): void;
7
+ export function openCodeCredentialSource(deps: {
8
+ buildOpenCodeUrl: (path: string, prefix?: string) => string;
9
+ getOpenCodeAuthHeaders: () => Record<string, string>;
10
+ }): () => OpenCodeClient;
11
+ export function readOpenCodeCredentials(): Promise<Record<string, ProviderCredential>>;
12
+ export function getProviderAuth(providerId: string): Promise<ProviderCredential | null>;
13
+ export function removeProviderAuth(providerId: string): Promise<boolean>;
@@ -1,81 +1,91 @@
1
- import fs from 'fs';
2
- import path from 'path';
3
- import os from 'os';
1
+ import { makeOpenCodeV2Client } from './v2-client.js';
4
2
 
5
- const OPENCODE_DATA_DIR = path.join(os.homedir(), '.local', 'share', 'opencode');
6
- const AUTH_FILE = path.join(OPENCODE_DATA_DIR, 'auth.json');
3
+ /** @typedef {{ type: 'api', key: string } | { type: 'oauth', access: string, refresh: string, expires: number, accountId?: string, enterpriseUrl?: string }} ProviderCredential */
4
+ /** @type {null | (() => import('@opencode/client').OpenCodeClient)} */
5
+ let getClient = null;
7
6
 
8
- function readAuthFile() {
9
- if (!fs.existsSync(AUTH_FILE)) {
10
- return {};
11
- }
12
- try {
13
- const content = fs.readFileSync(AUTH_FILE, 'utf8');
14
- const trimmed = content.trim();
15
- if (!trimmed) {
16
- return {};
17
- }
18
- return JSON.parse(trimmed);
19
- } catch (error) {
20
- console.error('Failed to read auth file:', error);
21
- throw new Error('Failed to read OpenCode auth configuration');
22
- }
7
+ /** Configure only in the trusted host. Resolve endpoint and authentication on every call. */
8
+ export function configureOpenCodeCredentials(resolveClient) {
9
+ getClient = resolveClient;
23
10
  }
24
11
 
25
- function writeAuthFile(auth) {
26
- try {
27
- if (!fs.existsSync(OPENCODE_DATA_DIR)) {
28
- fs.mkdirSync(OPENCODE_DATA_DIR, { recursive: true });
29
- }
12
+ export function openCodeCredentialSource({ buildOpenCodeUrl, getOpenCodeAuthHeaders }) {
13
+ return () => makeOpenCodeV2Client({
14
+ baseUrl: buildOpenCodeUrl('', '').replace(/\/+$/, ''),
15
+ authHeaders: getOpenCodeAuthHeaders(),
16
+ });
17
+ }
30
18
 
31
- if (fs.existsSync(AUTH_FILE)) {
32
- const backupFile = `${AUTH_FILE}.openchamber.backup`;
33
- fs.copyFileSync(AUTH_FILE, backupFile);
34
- console.log(`Created auth backup: ${backupFile}`);
35
- }
19
+ const credentialError = () => new Error('Unable to read OpenCode credentials');
20
+ const record = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
36
21
 
37
- fs.writeFileSync(AUTH_FILE, JSON.stringify(auth, null, 2), 'utf8');
38
- console.log('Successfully wrote auth file');
39
- } catch (error) {
40
- console.error('Failed to write auth file:', error);
41
- throw new Error('Failed to write OpenCode auth configuration');
22
+ /** Validate before projection: malformed/unsupported responses are never empty success. */
23
+ function validateCredentialEntries(entries) {
24
+ if (!Array.isArray(entries)) throw credentialError();
25
+ const active = new Set();
26
+ for (const entry of entries) {
27
+ if (!record(entry) || typeof entry.id !== 'string' || !entry.id || typeof entry.integrationID !== 'string'
28
+ || !entry.integrationID || typeof entry.active !== 'boolean' || !record(entry.value)) throw credentialError();
29
+ const value = entry.value;
30
+ if (value.type === 'key') {
31
+ if (typeof value.key !== 'string') throw credentialError();
32
+ } else if (value.type === 'oauth') {
33
+ if (typeof value.access !== 'string' || typeof value.refresh !== 'string' || !Number.isFinite(value.expires)) throw credentialError();
34
+ } else throw credentialError();
35
+ if (entry.active) {
36
+ if (active.has(entry.integrationID)) throw credentialError();
37
+ active.add(entry.integrationID);
38
+ }
42
39
  }
40
+ return entries;
43
41
  }
44
42
 
45
- function removeProviderAuth(providerId) {
46
- if (!providerId || typeof providerId !== 'string') {
47
- throw new Error('Provider ID is required');
48
- }
49
-
50
- const auth = readAuthFile();
51
-
52
- if (!auth[providerId]) {
53
- console.log(`Provider ${providerId} not found in auth file, nothing to remove`);
54
- return false;
43
+ async function listCredentials(client) {
44
+ try {
45
+ return validateCredentialEntries(await client.credential.list({ signal: AbortSignal.timeout(8000) }));
46
+ } catch {
47
+ // Upstream errors can contain response bodies or request headers with secrets.
48
+ throw credentialError();
55
49
  }
50
+ }
56
51
 
57
- delete auth[providerId];
58
- writeAuthFile(auth);
59
- console.log(`Removed provider auth: ${providerId}`);
60
- return true;
52
+ function currentClient() {
53
+ if (!getClient) throw credentialError();
54
+ try { return getClient(); } catch { throw credentialError(); }
61
55
  }
62
56
 
63
- function getProviderAuth(providerId) {
64
- const auth = readAuthFile();
65
- return auth[providerId] || null;
57
+ /** Host-only selected provider credentials. Never serialize this result to a client. */
58
+ export async function readOpenCodeCredentials() {
59
+ const entries = await listCredentials(currentClient());
60
+ /** @type {Record<string, ProviderCredential>} */
61
+ const result = Object.create(null);
62
+ for (const entry of entries) {
63
+ if (!entry.active) continue;
64
+ const value = entry.value;
65
+ result[entry.integrationID] = value.type === 'key' ? { type: 'api', key: value.key } : {
66
+ type: 'oauth', access: value.access, refresh: value.refresh, expires: value.expires,
67
+ ...(typeof value.metadata?.accountID === 'string' ? { accountId: value.metadata.accountID } : {}),
68
+ ...(typeof value.metadata?.enterpriseUrl === 'string' ? { enterpriseUrl: value.metadata.enterpriseUrl } : {}),
69
+ };
70
+ }
71
+ return result;
66
72
  }
67
73
 
68
- function listProviderAuths() {
69
- const auth = readAuthFile();
70
- return Object.keys(auth);
74
+ export async function getProviderAuth(providerId) {
75
+ return (await readOpenCodeCredentials())[providerId] || null;
71
76
  }
72
77
 
73
- export {
74
- readAuthFile,
75
- writeAuthFile,
76
- removeProviderAuth,
77
- getProviderAuth,
78
- listProviderAuths,
79
- AUTH_FILE,
80
- OPENCODE_DATA_DIR
81
- };
78
+ /** Disconnect every saved account for the provider, so deleting active cannot select another. */
79
+ export async function removeProviderAuth(providerId) {
80
+ if (typeof providerId !== 'string' || !providerId.trim()) throw new Error('Provider ID is required');
81
+ const client = currentClient();
82
+ const entries = (await listCredentials(client)).filter((entry) => entry.integrationID === providerId);
83
+ for (const entry of entries) {
84
+ try {
85
+ await client.credential.remove({ credentialID: entry.id }, { signal: AbortSignal.timeout(8000) });
86
+ } catch {
87
+ throw new Error('Unable to remove OpenCode credentials; some accounts may already have been removed. Retry to finish disconnecting.');
88
+ }
89
+ }
90
+ return entries.length > 0;
91
+ }
@@ -0,0 +1,57 @@
1
+ import { afterEach, describe, expect, it, vi } from 'vitest';
2
+ import { configureOpenCodeCredentials, getProviderAuth, readOpenCodeCredentials, removeProviderAuth, openCodeCredentialSource } from './auth.js';
3
+
4
+ const key = (id, active, value = id) => ({ id, integrationID: 'openai', label: id, active, value: { type: 'key', key: value } });
5
+ afterEach(() => { configureOpenCodeCredentials(null); vi.unstubAllGlobals(); });
6
+
7
+ describe('authoritative host credential API', () => {
8
+ it('observes active account switching and logout without cached or legacy fallback', async () => {
9
+ let entries = [key('old', true), key('new', false)];
10
+ configureOpenCodeCredentials(() => ({ credential: { list: async () => entries } }));
11
+ await expect(getProviderAuth('openai')).resolves.toEqual({ type: 'api', key: 'old' });
12
+ entries = [key('old', false), key('new', true)];
13
+ await expect(getProviderAuth('openai')).resolves.toEqual({ type: 'api', key: 'new' });
14
+ entries = [];
15
+ await expect(getProviderAuth('openai')).resolves.toBeNull();
16
+ });
17
+ it('maps OAuth metadata without copying arbitrary secret-bearing fields', async () => {
18
+ configureOpenCodeCredentials(() => ({ credential: { list: async () => [{ ...key('a', true), value: { type: 'oauth', access: 'access', refresh: 'refresh', expires: 123, metadata: { accountID: 'account', enterpriseUrl: 'https://example.com', unrelated: 'private' } } }] } }));
19
+ await expect(readOpenCodeCredentials()).resolves.toEqual({ openai: { type: 'oauth', access: 'access', refresh: 'refresh', expires: 123, accountId: 'account', enterpriseUrl: 'https://example.com' } });
20
+ });
21
+ it.each([null, {}, { data: [] }, [{ ...key('a', true), value: { type: 'unknown' } }], [key('a', true), key('b', true)]])('rejects unsupported payloads without inventing empty success', async (entries) => {
22
+ configureOpenCodeCredentials(() => ({ credential: { list: async () => entries } }));
23
+ await expect(readOpenCodeCredentials()).rejects.toThrow('Unable to read OpenCode credentials');
24
+ });
25
+ it('redacts transport/auth failures and leaves them retryable', async () => {
26
+ const list = vi.fn().mockRejectedValueOnce(new Error('Authorization: private-token')).mockResolvedValueOnce([]);
27
+ configureOpenCodeCredentials(() => ({ credential: { list } }));
28
+ await expect(readOpenCodeCredentials()).rejects.toThrow(/^Unable to read OpenCode credentials$/);
29
+ await expect(readOpenCodeCredentials()).resolves.toEqual({});
30
+ });
31
+ it('removes all provider accounts, preserves other integrations and supports retry after partial failure', async () => {
32
+ let entries = [key('old', false), key('new', true), { ...key('other', true), integrationID: 'anthropic' }];
33
+ const remove = vi.fn(async ({ credentialID }) => { entries = entries.filter((e) => e.id !== credentialID); });
34
+ configureOpenCodeCredentials(() => ({ credential: { list: async () => entries, remove } }));
35
+ remove.mockImplementationOnce(async ({ credentialID }) => { entries = entries.filter((e) => e.id !== credentialID); });
36
+ remove.mockRejectedValueOnce(new Error('private-token'));
37
+ await expect(removeProviderAuth('openai')).rejects.toThrow('some accounts may already have been removed');
38
+ await expect(removeProviderAuth('openai')).resolves.toBe(true);
39
+ expect(entries.map((e) => e.id)).toEqual(['other']);
40
+ await expect(removeProviderAuth('openai')).resolves.toBe(false);
41
+ });
42
+ it('uses the current endpoint/auth through the real SDK for every read', async () => {
43
+ let origin = 'http://first.invalid';
44
+ let authorization = 'Basic first';
45
+ const requests = [];
46
+ vi.stubGlobal('fetch', vi.fn(async (input, init) => {
47
+ const req = new Request(input, init);
48
+ requests.push([req.url, req.headers.get('authorization')]);
49
+ return Response.json({ data: [key('current', true)] });
50
+ }));
51
+ configureOpenCodeCredentials(openCodeCredentialSource({ buildOpenCodeUrl: () => origin, getOpenCodeAuthHeaders: () => ({ Authorization: authorization }) }));
52
+ await readOpenCodeCredentials();
53
+ origin = 'http://second.invalid'; authorization = 'Basic second';
54
+ await readOpenCodeCredentials();
55
+ expect(requests).toEqual([['http://first.invalid/api/credential', 'Basic first'], ['http://second.invalid/api/credential', 'Basic second']]);
56
+ });
57
+ });
@@ -114,7 +114,7 @@ function getCommandSources(commandName, workingDirectory) {
114
114
  const jsonSource = getJsonEntrySource(layers, 'command', commandName);
115
115
  const jsonSection = jsonSource.section;
116
116
  const jsonPath = jsonSource.path || layers.paths.customPath || layers.paths.projectPath || layers.paths.userPath;
117
- const jsonScope = jsonSource.path === layers.paths.projectPath ? COMMAND_SCOPE.PROJECT : COMMAND_SCOPE.USER;
117
+ const jsonScope = layers.projectLayers.some((layer) => layer.path === jsonSource.path) ? COMMAND_SCOPE.PROJECT : COMMAND_SCOPE.USER;
118
118
 
119
119
  const sources = {
120
120
  md: {