@openchambery/web 2.0.0-beta.8 → 2.0.0

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 (237) hide show
  1. package/README.md +14 -0
  2. package/dist/assets/{AgentAvatar-CzTtVMnI.js → AgentAvatar-C1MGZ7qC.js} +1 -1
  3. package/dist/assets/AssistantView-vqCFvVo0.js +3 -0
  4. package/dist/assets/{DiagramView-BBPsLqLq.js → DiagramView-CYO5iVVm.js} +1 -1
  5. package/dist/assets/{FadeInOnReveal-DCqrQkkz.js → FadeInOnReveal-BMSk_tY-.js} +1 -1
  6. package/dist/assets/Icon-BdzPRCSy.js +1 -0
  7. package/dist/assets/MarkdownRenderer-FMynjkGR.js +2 -0
  8. package/dist/assets/{MarkdownRendererImpl-CD0RYNWX.js → MarkdownRendererImpl-DoWvw5jj.js} +3 -3
  9. package/dist/assets/{MarkstreamRendererImpl-CECVYTH6.js → MarkstreamRendererImpl-DoeYajmw.js} +1 -1
  10. package/dist/assets/{MobileDetailNavigation-DOwW-F0i.js → MobileDetailNavigation-BzBz-co_.js} +1 -1
  11. package/dist/assets/{MobileShareBridge-d0hQ2FSQ.js → MobileShareBridge-D1Uoj6l_.js} +3 -3
  12. package/dist/assets/MobileSurface-8YpZ51Qo.js +2 -0
  13. package/dist/assets/{MultiRunWindow-DOdFEgMg.js → MultiRunWindow-BxcYUPta.js} +1 -1
  14. package/dist/assets/{OnboardingScreen-O4cGdz5p.js → OnboardingScreen-yRdQTR1M.js} +1 -1
  15. package/dist/assets/{OverlayScrollbar-S-7EePsU.js → OverlayScrollbar-BUjtqnxn.js} +1 -1
  16. package/dist/assets/SettingsView-D7KrvqvH.js +25 -0
  17. package/dist/assets/{SettingsWindow-vKn-MWGU.js → SettingsWindow-DX2w-cDc.js} +1 -1
  18. package/dist/assets/{TerminalView-BgPjY0CO.js → TerminalView-CiUcCQPm.js} +1 -1
  19. package/dist/assets/{ToolOutputDialog-DtT3wbie.js → ToolOutputDialog-DQvXtK71.js} +1 -1
  20. package/dist/assets/appThemeRegistry-BXC6cIDE.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-CUwjpv-i.js} +1 -1
  23. package/dist/assets/{collapsible-CnvUeYst.js → collapsible-paItOH3A.js} +1 -1
  24. package/dist/assets/{context-menu-BGBUpZEQ.js → context-menu-K6zNumqN.js} +1 -1
  25. package/dist/assets/{desktopHosts-JGFqhlaM.js → desktopHosts-DuftEj8B.js} +1 -1
  26. package/dist/assets/desktopRelayRestore-g378BdZf.js +1 -0
  27. package/dist/assets/{diagram-rvn7_qBM.js → diagram-D6nY5hbD.js} +1 -1
  28. package/dist/assets/dialog-PjKWayIU.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-78v5mNTE.js +2 -0
  32. package/dist/assets/{fileQueries-DN6gbLhM.js → fileQueries-DuF89nFO.js} +1 -1
  33. package/dist/assets/{fr-C_ZlZDI2.js → fr-BsKj2CTi.js} +4 -4
  34. package/dist/assets/{gitApi-D9ygh31C.js → gitApi-eoKSJcvW.js} +4 -4
  35. package/dist/assets/{imageSource-BMztRu0c.js → imageSource-BpUsiwFd.js} +1 -1
  36. package/dist/assets/{input-BVu-iykl.js → input-BeVw4D0W.js} +1 -1
  37. package/dist/assets/insertionBoundaries-BI3RHOpf.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-CQpUPjPK.js +169 -0
  41. package/dist/assets/{main-B97oNWoh.js → main-MN0YKJ0_.js} +2 -2
  42. package/dist/assets/main-qSeeJRW1.css +1 -0
  43. package/dist/assets/{markdownHeightCache-D9-B1wdY.js → markdownHeightCache-CGCWmHgq.js} +1 -1
  44. package/dist/assets/{mcpQueries-zC4-V5b8.js → mcpQueries-X424Iq2z.js} +1 -1
  45. package/dist/assets/miniChat-DFIl76ku.js +2 -0
  46. package/dist/assets/mobile-C3fWXGQJ.js +2 -0
  47. package/dist/assets/multirun-DuSX3YRs.js +3 -0
  48. package/dist/assets/persistence-Dnj1QOdX.js +3 -0
  49. package/dist/assets/pl-B714sdql.js +15 -0
  50. package/dist/assets/platform-jCZ3KfMQ.js +6 -0
  51. package/dist/assets/projectMeta-Cn08pTNi.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-CRPdctIn.js +2 -0
  55. package/dist/assets/renderMobileApp-CVURQbeh.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-xivE1wxC.js +2 -0
  59. package/dist/assets/runtimeEndpointReset-B8DzKzwg.js +148 -0
  60. package/dist/assets/runtimeEndpointReset-D_vfNOQz.css +1 -0
  61. package/dist/assets/sessionChangeSummary-yBsR2pyd.js +2 -0
  62. package/dist/assets/{sessionLookup-DcyGP3xg.js → sessionLookup-1Z1FWZRB.js} +1 -1
  63. package/dist/assets/{sortable-tabs-strip-BsObV7Pl.js → sortable-tabs-strip-D611LPyd.js} +1 -1
  64. package/dist/assets/src-TBM7scxy.css +2 -0
  65. package/dist/assets/{store-jaP79mqI.js → store-l_X_jQNz.js} +4 -4
  66. package/dist/assets/{terminalApi-Dhqqp4xi.js → terminalApi-BZW_rAc3.js} +1 -1
  67. package/dist/assets/{textarea-BoXpPjTd.js → textarea-QtZjf4L8.js} +1 -1
  68. package/dist/assets/{tooltip-DYeQ_Hvy.js → tooltip-DDNnmcXI.js} +1 -1
  69. package/dist/assets/{uk-BgFzz8m2.js → uk-ChNnTgKP.js} +4 -4
  70. package/dist/assets/{url-CGbJbiJq.js → url-DNRMq_0R.js} +1 -1
  71. package/dist/assets/{useAppFontEffects-BauzQXF0.js → useAppFontEffects-DJy8P5By.js} +51 -51
  72. package/dist/assets/{useAssistantUIStore-Bz4tRflF.js → useAssistantUIStore-DBwfg9nX.js} +1 -1
  73. package/dist/assets/useDesktopWindowControlsLayout-DlwSomVK.js +1 -0
  74. package/dist/assets/{useEffectiveDirectory-qDoROBC_.js → useEffectiveDirectory-eJorVTT9.js} +1 -1
  75. package/dist/assets/{useFontPreferences-Xw1vSkmC.js → useFontPreferences-Bjgt_lzl.js} +1 -1
  76. package/dist/assets/useMobileNavigationStore-A_ZNTfpl.js +415 -0
  77. package/dist/assets/useSessionAutoCleanup-CqC3dEou.js +2 -0
  78. package/dist/assets/{useUIStore-syVmbuDA.js → useUIStore-Dfx6TKvi.js} +1 -1
  79. package/dist/assets/{useWorkerHighlightedLines-D8luxgem.js → useWorkerHighlightedLines-B0vSL6jr.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-CxAzKd78.js +0 -3
  206. package/dist/assets/Icon-B8fSTLdP.js +0 -1
  207. package/dist/assets/MarkdownRenderer-yq2DPZUG.js +0 -2
  208. package/dist/assets/MobileSurface-qalyCN7X.js +0 -2
  209. package/dist/assets/SettingsView-DVxMbeKC.js +0 -25
  210. package/dist/assets/appThemeRegistry-DC5NRV_i.js +0 -1
  211. package/dist/assets/desktopRelayRestore-CHEwrI93.js +0 -1
  212. package/dist/assets/dialog-CQ63sc9E.js +0 -1
  213. package/dist/assets/dropdown-menu.styles-Bzo6L3FH.js +0 -1
  214. package/dist/assets/fileMentionTouchSelection-B8Blr6EI.js +0 -3
  215. package/dist/assets/insertionBoundaries-DYu8VdgE.js +0 -1
  216. package/dist/assets/main-D2BzpnUt.css +0 -1
  217. package/dist/assets/main-SXMSe99Y.js +0 -169
  218. package/dist/assets/miniChat-CIi4PmQO.js +0 -2
  219. package/dist/assets/mobile-BK0Ov-k5.js +0 -2
  220. package/dist/assets/multirun-UIu_EbC2.js +0 -4
  221. package/dist/assets/persistence-D0y7226-.js +0 -3
  222. package/dist/assets/pl-BCf5uLlK.js +0 -15
  223. package/dist/assets/platform-D_c-T5nO.js +0 -6
  224. package/dist/assets/projectMeta-BPi16qGY.js +0 -3
  225. package/dist/assets/renderElectronMiniChatApp-CnTCJpBd.js +0 -2
  226. package/dist/assets/renderMobileApp-BFDrOvfv.js +0 -28
  227. package/dist/assets/runtime-switch-LvWDU-SL.js +0 -1
  228. package/dist/assets/runtimeConfig-023C-il6.js +0 -2
  229. package/dist/assets/runtimeEndpointReset-Bdx3CutT.js +0 -149
  230. package/dist/assets/sessionChangeSummary-B8xrgW5-.js +0 -2
  231. package/dist/assets/src-lS1keDVV.css +0 -2
  232. package/dist/assets/useDesktopWindowControlsLayout-kZ4wtPnO.js +0 -1
  233. package/dist/assets/useMobileNavigationStore-BDXu5FSG.js +0 -414
  234. package/dist/assets/useSessionAutoCleanup-BHloMhRp.js +0 -2
  235. package/dist/assets/vendor-opencode-client-C12gfjag.js +0 -12
  236. package/server/lib/opencode/snippets.js +0 -242
  237. package/server/lib/opencode/snippets.test.js +0 -97
@@ -10,6 +10,8 @@ vi.mock('node:child_process', () => ({
10
10
 
11
11
  const { createOpenCodeLifecycleRuntime } = await import('./lifecycle.js');
12
12
  const { createOpenCodeNetworkRuntime } = await import('./network-runtime.js');
13
+ const { resolveOpenCodeEnvConfig } = await import('./env-config.js');
14
+ const { RUNTIME_CONTRACT_MIN_VERIFIED } = await import('./runtime-contract.js');
13
15
 
14
16
  const originalOpencodeBinary = process.env.OPENCODE_BINARY;
15
17
  const originalPath = process.env.PATH;
@@ -106,6 +108,7 @@ const createRuntime = (overrides = {}, stateRef = null) => {
106
108
  getManagedOpenCodeShellEnvSnapshot: vi.fn(() => ({
107
109
  PATH: '/home/user/.bun/bin:/usr/local/bin:/usr/bin',
108
110
  SHELL_ONLY: 'yes',
111
+ OPENCODE_PASSWORD: 'shell-v2-password',
109
112
  OPENCODE_SERVER_PASSWORD: 'shell-password',
110
113
  })),
111
114
  ...overrides,
@@ -160,6 +163,28 @@ const startListeningChild = () => {
160
163
  };
161
164
 
162
165
  describe('OpenCode lifecycle', () => {
166
+ it.each(['http://example.test:80', 'https://example.test:443', 'http://[::1]:80'])('attaches explicit default-port host %s without spawning', async (host) => {
167
+ const config = resolveOpenCodeEnvConfig({ env: { OPENCODE_HOST: host, OPENCODE_PORT: '9999' } });
168
+ const stateRef = {};
169
+ const runtime = createRuntime({
170
+ env: {
171
+ ENV_CONFIGURED_OPENCODE_PORT: config.configuredOpenCodePort,
172
+ ENV_CONFIGURED_OPENCODE_HOST: config.configuredOpenCodeHost,
173
+ ENV_EFFECTIVE_PORT: config.effectivePort,
174
+ ENV_CONFIGURED_OPENCODE_HOSTNAME: '127.0.0.1',
175
+ ENV_SKIP_OPENCODE_START: true,
176
+ },
177
+ buildOpenCodeUrl: (route) => new URL(route, config.configuredOpenCodeHost.origin).href,
178
+ }, stateRef);
179
+ const fetchMock = stubOpenCodeFetch({ health: { ...DEFAULT_V2_HEALTH, version: '2.0.23' } });
180
+ await runtime.bootstrapOpenCodeAtStartup();
181
+ await runtime.waitForOpenCodeReady(1000, 1);
182
+ expect(stateRef.current.isExternalOpenCode).toBe(true);
183
+ expect(stateRef.current.openCodePort).toBe(config.effectivePort);
184
+ expect(fetchMock.mock.calls.some(([url]) => String(url) === `${config.configuredOpenCodeHost.origin}/api/info`)).toBe(true);
185
+ expect(spawnMock).not.toHaveBeenCalled();
186
+ });
187
+
163
188
  it('launches managed OpenCode with the managed PATH', async () => {
164
189
  delete process.env.OPENCODE_BINARY;
165
190
  const child = createMockChild();
@@ -179,6 +204,7 @@ describe('OpenCode lifecycle', () => {
179
204
  expect(options.env.PATH).toBe('/home/user/.bun/bin:/usr/local/bin:/usr/bin');
180
205
  expect(options.env.SHELL_ONLY).toBe('yes');
181
206
  expect(options.env.OPENCODE_SERVER_PASSWORD).toBe('password');
207
+ expect(options.env.OPENCODE_PASSWORD).toBe('password');
182
208
 
183
209
  await server.close();
184
210
  });
@@ -847,7 +873,7 @@ describe('OpenCode lifecycle', () => {
847
873
  stateRef.current.runtimeContract = {
848
874
  executionAllowed: true,
849
875
  phase: 'ready',
850
- serveVersion: '2.0.12',
876
+ serveVersion: RUNTIME_CONTRACT_MIN_VERIFIED,
851
877
  instanceGeneration: 1,
852
878
  };
853
879
 
@@ -861,7 +887,7 @@ describe('OpenCode lifecycle', () => {
861
887
  runtime.refreshRuntimeContractFromProbe(
862
888
  {
863
889
  ok: true,
864
- version: '2.0.12',
890
+ version: RUNTIME_CONTRACT_MIN_VERIFIED,
865
891
  authenticated: true,
866
892
  healthOk: true,
867
893
  },
@@ -875,7 +901,7 @@ describe('OpenCode lifecycle', () => {
875
901
  runtime.refreshRuntimeContractFromProbe(
876
902
  {
877
903
  ok: true,
878
- version: '2.0.12',
904
+ version: RUNTIME_CONTRACT_MIN_VERIFIED,
879
905
  authenticated: true,
880
906
  healthOk: true,
881
907
  },
@@ -884,7 +910,7 @@ describe('OpenCode lifecycle', () => {
884
910
  );
885
911
  expect(stateRef.current.runtimeContract.executionAllowed).toBe(true);
886
912
  expect(stateRef.current.runtimeContract.instanceGeneration).toBe(2);
887
- expect(stateRef.current.runtimeContract.serveVersion).toBe('2.0.12');
913
+ expect(stateRef.current.runtimeContract.serveVersion).toBe(RUNTIME_CONTRACT_MIN_VERIFIED);
888
914
  });
889
915
 
890
916
  it('does not mark external skip-start ready before version and migration admit', async () => {
@@ -0,0 +1,8 @@
1
+ import type { OpenCodeClient } from '@opencode/client';
2
+
3
+ export type LocationReleaseResult = { state: 'released' | 'timeout' | 'failed' | 'unavailable' };
4
+ export function releaseDeletedWorktreeLocation(input: {
5
+ getClient: () => OpenCodeClient | null;
6
+ directory: string;
7
+ timeoutMs?: number;
8
+ }): Promise<LocationReleaseResult>;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Explicit worktree deletion only. An eviction acknowledgement detaches cached
3
+ * services; it does not prove that borrowers have released every OS handle.
4
+ * Never use this operation for automatic navigation cleanup on a shared serve.
5
+ *
6
+ * @param {{ getClient: () => import('@opencode/client').OpenCodeClient | null, directory: string, timeoutMs?: number }} input
7
+ * @returns {Promise<{ state: 'released' | 'timeout' | 'failed' | 'unavailable' }>}
8
+ */
9
+ export async function releaseDeletedWorktreeLocation({ getClient, directory, timeoutMs = 2000 }) {
10
+ const controller = new AbortController();
11
+ let timer;
12
+ try {
13
+ if (!directory || !directory.trim()) return { state: 'unavailable' };
14
+ const client = getClient();
15
+ if (!client) return { state: 'unavailable' };
16
+ const deadline = new Promise((resolve) => {
17
+ timer = setTimeout(() => {
18
+ controller.abort();
19
+ resolve({ state: 'timeout' });
20
+ }, timeoutMs);
21
+ });
22
+ const request = Promise.resolve().then(() => client.debug.location.evict(
23
+ { location: { directory } },
24
+ { signal: controller.signal },
25
+ )).then(() => ({ state: 'released' }), () => ({ state: controller.signal.aborted ? 'timeout' : 'failed' }));
26
+ return await Promise.race([request, deadline]);
27
+ } catch {
28
+ return { state: 'failed' };
29
+ } finally {
30
+ clearTimeout(timer);
31
+ }
32
+ }
@@ -0,0 +1,43 @@
1
+ import { afterEach, describe, expect, it, vi } from 'vitest';
2
+ import { OpenCode } from '@opencode/client';
3
+ import { releaseDeletedWorktreeLocation } from './location-release.js';
4
+
5
+ afterEach(() => vi.useRealTimers());
6
+
7
+ describe('explicit worktree location release', () => {
8
+ it('uses the official SDK exact target, current origin and auth', async () => {
9
+ const requests = [];
10
+ let origin = 'https://old.invalid';
11
+ const getClient = () => OpenCode.make({ baseUrl: origin, headers: { Authorization: 'test-auth' }, fetch: async (input, init) => {
12
+ requests.push(new Request(input, init));
13
+ return new Response(null, { status: 204 });
14
+ } });
15
+ origin = 'https://current.invalid';
16
+ expect(await releaseDeletedWorktreeLocation({ getClient, directory: '/repo/work tree' })).toEqual({ state: 'released' });
17
+ expect(requests).toHaveLength(1);
18
+ const url = new URL(requests[0].url);
19
+ expect(url.origin + url.pathname).toBe('https://current.invalid/api/debug/location');
20
+ expect(url.searchParams.get('location[directory]')).toBe('/repo/work tree');
21
+ expect(requests[0].method).toBe('DELETE');
22
+ expect(requests[0].headers.get('Authorization')).toBe('test-auth');
23
+ });
24
+
25
+ it('reports failure without claiming successful disposal', async () => {
26
+ const getClient = () => ({ debug: { location: { evict: async () => { throw new Error('unavailable'); } } } });
27
+ expect(await releaseDeletedWorktreeLocation({ getClient, directory: '/repo' })).toEqual({ state: 'failed' });
28
+ expect(await releaseDeletedWorktreeLocation({ getClient: () => null, directory: '/repo' })).toEqual({ state: 'unavailable' });
29
+ });
30
+
31
+ it('bounds even a transport that ignores abort and never settles', async () => {
32
+ vi.useFakeTimers();
33
+ let signal;
34
+ const getClient = () => ({ debug: { location: { evict: (_input, options) => {
35
+ signal = options.signal;
36
+ return new Promise(() => {});
37
+ } } } });
38
+ const result = releaseDeletedWorktreeLocation({ getClient, directory: '/repo', timeoutMs: 50 });
39
+ await vi.advanceTimersByTimeAsync(50);
40
+ expect(await result).toEqual({ state: 'timeout' });
41
+ expect(signal.aborted).toBe(true);
42
+ });
43
+ });
@@ -29,7 +29,7 @@ function validateMcpName(name) {
29
29
  */
30
30
  function resolveMcpScopeFromPath(layers, sourcePath) {
31
31
  if (!sourcePath) return null;
32
- return sourcePath === layers.paths.projectPath ? AGENT_SCOPE.PROJECT : AGENT_SCOPE.USER;
32
+ return layers.projectLayers.some((layer) => layer.path === sourcePath) ? AGENT_SCOPE.PROJECT : AGENT_SCOPE.USER;
33
33
  }
34
34
 
35
35
  function ensureProjectMcpConfigPath(workingDirectory) {
@@ -1,5 +1,6 @@
1
1
  // Pinned desktop/runtime opencode2. Upgrade and prepare must not fall back to 1.18.x.
2
- export const PINNED_OPENCODE2_VERSION = '2.0.12';
2
+ export const PINNED_OPENCODE2_VERSION = '2.0.23';
3
+ export const MINIMUM_OPENCODE2_VERSION = '2.0.20';
3
4
 
4
5
  // Global npm/bun package that installs the opencode v2 binary (not 1.x opencode-ai).
5
6
  export const OPENCODE2_NPM_PACKAGE = '@opencode/cli';
@@ -86,15 +86,15 @@ describe('owned runtime upgrade (ticket 12)', () => {
86
86
 
87
87
  it('verifies running serve identity before upgrade success', () => {
88
88
  const contractOk = evaluateRuntimeContract({
89
- serveVersion: '2.0.14',
89
+ serveVersion: '2.0.23',
90
90
  reachable: true,
91
91
  authenticated: true,
92
92
  healthOk: true,
93
93
  migrationAdmitTranscript: true,
94
94
  });
95
95
  expect(evaluateOwnedUpgradeResult({
96
- targetVersion: '2.0.14',
97
- serveVersion: '2.0.14',
96
+ targetVersion: '2.0.23',
97
+ serveVersion: '2.0.23',
98
98
  binaryPath: ownedBinary,
99
99
  expectedBinaryPath: ownedBinary,
100
100
  contract: contractOk,
@@ -116,8 +116,8 @@ describe('owned runtime upgrade (ticket 12)', () => {
116
116
  });
117
117
 
118
118
  expect(evaluateOwnedUpgradeResult({
119
- targetVersion: '2.0.14',
120
- serveVersion: '2.0.14',
119
+ targetVersion: '2.0.23',
120
+ serveVersion: '2.0.23',
121
121
  binaryPath: '/usr/bin/opencode',
122
122
  expectedBinaryPath: ownedBinary,
123
123
  contract: contractOk,
@@ -1,4 +1,5 @@
1
1
  import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, mock, test } from 'bun:test';
2
+ import { vi } from 'vitest';
2
3
  import express from 'express';
3
4
  import fs from 'fs';
4
5
  import os from 'os';
@@ -67,7 +68,8 @@ describe('opencode plugin routes', () => {
67
68
  beforeAll(async () => {
68
69
  rootDir = fs.mkdtempSync(path.join(os.tmpdir(), 'openchamber-plugin-routes-'));
69
70
  userConfigPath = path.join(rootDir, 'user-opencode.json');
70
- process.env.OPENCODE_CONFIG = userConfigPath;
71
+ vi.stubEnv('OPENCODE_CONFIG', userConfigPath);
72
+ vi.stubEnv('OPENCODE_CONFIG_DIR', path.join(rootDir, 'global-config'));
71
73
  plugins = await import('./plugins.js');
72
74
  });
73
75
 
@@ -91,7 +93,7 @@ describe('opencode plugin routes', () => {
91
93
 
92
94
  afterAll(() => {
93
95
  fs.rmSync(rootDir, { recursive: true, force: true });
94
- delete process.env.OPENCODE_CONFIG;
96
+ vi.unstubAllEnvs();
95
97
  });
96
98
 
97
99
  test('GET /api/config/plugins empty returns entries and files arrays', async () => {
@@ -4,6 +4,8 @@ import path from 'path';
4
4
  import {
5
5
  AGENT_SCOPE,
6
6
  readConfigFile,
7
+ readConfigLayers,
8
+ getConfigForPath,
7
9
  writeConfig,
8
10
  } from './shared.js';
9
11
  import { isPathSpec } from './plugin-spec.js';
@@ -74,54 +76,8 @@ function getActiveOpencodeConfigDir() {
74
76
  return path.join(os.homedir(), '.config', 'opencode');
75
77
  }
76
78
 
77
- function getActiveUserConfigPaths() {
78
- const configDir = getActiveOpencodeConfigDir();
79
- return [
80
- path.join(configDir, 'config.json'),
81
- path.join(configDir, 'opencode.json'),
82
- path.join(configDir, 'opencode.jsonc'),
83
- ];
84
- }
85
-
86
- function getActiveCustomConfigPath() {
87
- return process.env.OPENCODE_CONFIG ? path.resolve(process.env.OPENCODE_CONFIG) : null;
88
- }
89
-
90
- function getPrimaryUserConfigPath() {
91
- const [defaultPath, ...fallbackPaths] = getActiveUserConfigPaths();
92
- for (const userPath of [defaultPath, ...fallbackPaths]) {
93
- if (fs.existsSync(userPath)) {
94
- return userPath;
95
- }
96
- }
97
- return defaultPath;
98
- }
99
-
100
- function getProjectConfigPath(workingDirectory) {
101
- if (!workingDirectory) return null;
102
- const candidates = [
103
- path.join(workingDirectory, 'opencode.json'),
104
- path.join(workingDirectory, 'opencode.jsonc'),
105
- path.join(workingDirectory, '.opencode', 'opencode.json'),
106
- path.join(workingDirectory, '.opencode', 'opencode.jsonc'),
107
- ];
108
- return candidates.find((candidate) => fs.existsSync(candidate)) || candidates[0];
109
- }
110
-
111
79
  function readPluginConfigLayers(workingDirectory) {
112
- const customPath = getActiveCustomConfigPath();
113
- const userPath = getPrimaryUserConfigPath();
114
- const projectPath = getProjectConfigPath(workingDirectory);
115
- return {
116
- userConfig: readConfigFile(userPath),
117
- projectConfig: readConfigFile(projectPath),
118
- customConfig: readConfigFile(customPath),
119
- paths: {
120
- userPath,
121
- projectPath,
122
- customPath,
123
- },
124
- };
80
+ return readConfigLayers(workingDirectory);
125
81
  }
126
82
 
127
83
  function validateFileName(fileName) {
@@ -144,16 +100,10 @@ function ensureProjectConfigPath(workingDirectory) {
144
100
  }
145
101
 
146
102
  function configSources(layers) {
147
- const sources = [];
148
- if (layers.paths.customPath) {
149
- sources.push({ config: layers.customConfig, filePath: layers.paths.customPath, scope: AGENT_SCOPE.USER });
150
- } else {
151
- sources.push({ config: layers.userConfig, filePath: layers.paths.userPath, scope: AGENT_SCOPE.USER });
152
- }
153
- if (layers.paths.projectPath) {
154
- sources.push({ config: layers.projectConfig, filePath: layers.paths.projectPath, scope: AGENT_SCOPE.PROJECT });
155
- }
156
- return sources;
103
+ return [...layers.documents.filter((layer) => layer.scope !== 'project').reverse(), ...layers.projectLayers.toReversed()].map((layer) => ({
104
+ config: layer.config, filePath: layer.path,
105
+ scope: layer.scope === 'project' ? AGENT_SCOPE.PROJECT : AGENT_SCOPE.USER,
106
+ }));
157
107
  }
158
108
 
159
109
  function splitScopedValue(value) {
@@ -175,7 +125,9 @@ function getPluginTarget(id, workingDirectory) {
175
125
  const { scope, value: spec } = splitScopedValue(decoded.value);
176
126
  validateScope(scope);
177
127
  const layers = readPluginConfigLayers(workingDirectory);
178
- const source = configSources(layers).find((candidate) => candidate.scope === scope);
128
+ const source = configSources(layers).find((candidate) => candidate.scope === scope
129
+ && Array.isArray(candidate.config?.plugin)
130
+ && candidate.config.plugin.some((raw) => parsePluginRaw(raw).spec === spec));
179
131
  const plugin = Array.isArray(source?.config?.plugin) ? source.config.plugin : [];
180
132
  const index = plugin.findIndex((raw) => parsePluginRaw(raw).spec === spec);
181
133
  if (!source || index === -1) {
@@ -281,14 +233,14 @@ function createPluginEntry(entry, workingDirectory) {
281
233
  throw codedError(`Plugin "${spec}" already exists`, 'ENTRY_EXISTS');
282
234
  }
283
235
 
284
- let targetPath = getPrimaryUserConfigPath();
236
+ let targetPath = layers.paths.userPath;
285
237
  let config = {};
286
238
  if (scope === AGENT_SCOPE.PROJECT) {
287
239
  targetPath = ensureProjectConfigPath(workingDirectory);
288
240
  config = fs.existsSync(targetPath) ? readConfigFile(targetPath) : {};
289
241
  } else {
290
242
  targetPath = layers.paths.customPath || layers.paths.userPath;
291
- config = layers.paths.customPath ? layers.customConfig : layers.userConfig;
243
+ config = getConfigForPath(layers, targetPath);
292
244
  }
293
245
 
294
246
  if (!Array.isArray(config.plugin)) {
@@ -1,4 +1,5 @@
1
1
  import { afterAll, beforeAll, beforeEach, describe, expect, test } from 'bun:test';
2
+ import { vi } from 'vitest';
2
3
  import fs from 'fs';
3
4
  import os from 'os';
4
5
  import path from 'path';
@@ -30,19 +31,20 @@ describe('opencode plugins data layer', () => {
30
31
  beforeAll(async () => {
31
32
  rootDir = fs.mkdtempSync(path.join(os.tmpdir(), 'openchamber-plugins-'));
32
33
  userConfigPath = path.join(rootDir, 'user-opencode.json');
33
- process.env.OPENCODE_CONFIG = userConfigPath;
34
+ vi.stubEnv('OPENCODE_CONFIG', userConfigPath);
35
+ vi.stubEnv('OPENCODE_CONFIG_DIR', path.join(rootDir, 'global-config'));
34
36
  plugins = await import('./plugins.js');
35
37
  });
36
38
 
37
39
  beforeEach(() => {
38
- process.env.OPENCODE_CONFIG = userConfigPath;
40
+ vi.stubEnv('OPENCODE_CONFIG', userConfigPath);
39
41
  projectDir = fs.mkdtempSync(path.join(rootDir, 'project-'));
40
42
  fs.rmSync(userConfigPath, { force: true });
41
43
  });
42
44
 
43
45
  afterAll(() => {
44
46
  fs.rmSync(rootDir, { recursive: true, force: true });
45
- delete process.env.OPENCODE_CONFIG;
47
+ vi.unstubAllEnvs();
46
48
  });
47
49
 
48
50
  test('parses raw plugin entries', () => {
@@ -30,8 +30,8 @@ function getProviderSources(providerId, workingDirectory) {
30
30
  return {
31
31
  sources: {
32
32
  auth: { exists: false },
33
- user: { exists: userExists, path: paths.userPath },
34
- project: { exists: projectExists, path: paths.projectPath || null },
33
+ user: { exists: userExists, path: layers.userLayers.findLast((layer) => layer.config?.provider?.[providerId] !== undefined || layer.config?.providers?.[providerId] !== undefined)?.path || paths.userPath },
34
+ project: { exists: projectExists, path: layers.projectLayers.findLast((layer) => layer.config?.provider?.[providerId] !== undefined || layer.config?.providers?.[providerId] !== undefined)?.path || paths.projectPath || null },
35
35
  custom: { exists: customExists, path: paths.customPath }
36
36
  }
37
37
  };
@@ -44,17 +44,20 @@ function removeProviderConfig(providerId, workingDirectory, scope = 'user') {
44
44
 
45
45
  const layers = readConfigLayers(workingDirectory);
46
46
  let targetPath = layers.paths.userPath;
47
+ const sources = getProviderSources(providerId, workingDirectory).sources;
47
48
 
48
49
  if (scope === 'project') {
49
50
  if (!workingDirectory) {
50
51
  throw new Error('Working directory is required for project scope');
51
52
  }
52
- targetPath = layers.paths.projectPath || targetPath;
53
+ targetPath = sources.project.path || targetPath;
53
54
  } else if (scope === 'custom') {
54
55
  if (!layers.paths.customPath) {
55
56
  return false;
56
57
  }
57
58
  targetPath = layers.paths.customPath;
59
+ } else {
60
+ targetPath = sources.user.path;
58
61
  }
59
62
 
60
63
  const targetConfig = getConfigForPath(layers, targetPath);
@@ -1,4 +1,5 @@
1
1
  import { createProxyMiddleware } from 'http-proxy-middleware';
2
+ import { isSensitiveCredentialResponse } from './credential-boundary.js';
2
3
 
3
4
  import {
4
5
  applyForwardProxyResponseHeaders,
@@ -244,6 +245,12 @@ export const resolveSessionTurnAdmissionRequest = (req) => {
244
245
  };
245
246
 
246
247
  export const registerOpenCodeProxy = (app, deps) => {
248
+ app.use((req, res, next) => {
249
+ if (isSensitiveCredentialResponse(req.method, req.originalUrl || req.url)) {
250
+ return res.status(403).json({ code: 'credential_response_refused', error: 'Provider credential values are host-only.' });
251
+ }
252
+ return next();
253
+ });
247
254
  const {
248
255
  fs,
249
256
  os,
@@ -57,6 +57,19 @@ const mountProxy = () => {
57
57
  return app;
58
58
  };
59
59
 
60
+ describe('credential response boundary', () => {
61
+ it.each(['/api/credential', '/api/credential/', '/api/%63redential?directory=/repo'])('refuses raw credential list %s before upstream', async (path) => {
62
+ const fetchSpy = vi.fn();
63
+ globalThis.fetch = fetchSpy;
64
+ const response = await request(mountProxy()).get(path).expect(403);
65
+ expect(response.body.code).toBe('credential_response_refused');
66
+ expect(fetchSpy).not.toHaveBeenCalled();
67
+ });
68
+ it('refuses credential create replies which echo values', async () => {
69
+ await request(mountProxy()).post('/api/credential').send({}).expect(403);
70
+ });
71
+ });
72
+
60
73
  const sseBody = (blocks) => {
61
74
  const encoder = new TextEncoder();
62
75
  const text = blocks.join('');
@@ -0,0 +1,66 @@
1
+ import { expect, test } from 'vitest';
2
+ import express from 'express';
3
+ import request from 'supertest';
4
+ import { execFile, spawn } from 'node:child_process';
5
+ import { promisify } from 'node:util';
6
+ import fs from 'node:fs/promises';
7
+ import os from 'node:os';
8
+ import path from 'node:path';
9
+ import net from 'node:net';
10
+ import { createOpenCodeUpdateDiscovery } from './update-discovery.js';
11
+ import { installPinnedOpenCode2Cli } from './ensure-cli.js';
12
+ import { createSharedOpenCodeService } from './shared-service.js';
13
+ import { registerOpenCodeRoutes } from './routes.js';
14
+
15
+ // Downloads real official binaries; never touches the user's shared service.
16
+ test.skipIf(process.env.OPENCHAMBER_TEST_REMOTE_UPGRADE !== '1')('remote discovery upgrades an isolated shared service and verifies its running version', async () => {
17
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), 'opencode-upgrade-'));
18
+ const env = {
19
+ PATH: process.env.PATH, HOME: root, TMPDIR: os.tmpdir(),
20
+ XDG_CONFIG_HOME: path.join(root, 'config'), XDG_DATA_HOME: path.join(root, 'data'),
21
+ XDG_STATE_HOME: path.join(root, 'state'), XDG_CACHE_HOME: path.join(root, 'cache'),
22
+ OPENCODE_CONFIG_DIR: path.join(root, 'config', 'opencode'), OPENCODE_DISABLE_MODELS_FETCH: '1',
23
+ };
24
+ await fs.mkdir(env.OPENCODE_CONFIG_DIR, { recursive: true });
25
+ const listener = net.createServer();
26
+ await new Promise((resolve) => listener.listen(0, '127.0.0.1', resolve));
27
+ const port = listener.address().port;
28
+ await new Promise((resolve) => listener.close(resolve));
29
+ await fs.writeFile(path.join(env.OPENCODE_CONFIG_DIR, 'service.json'), JSON.stringify({ hostname: '127.0.0.1', port }));
30
+ const service = createSharedOpenCodeService({ envLike: env, spawnImpl: (command, args, options) => spawn(command, args, { ...options, cwd: root }) });
31
+ let binary;
32
+ let registration;
33
+ try {
34
+ const target = await createOpenCodeUpdateDiscovery()();
35
+ expect(target).not.toBe('2.0.16');
36
+ binary = await installPinnedOpenCode2Cli({ version: '2.0.16', dataDir: root });
37
+ await service.start({ binary, env });
38
+ registration = await service.readRegistration();
39
+ expect(registration.version).toBe('2.0.16');
40
+ const app = express();
41
+ app.use(express.json());
42
+ registerOpenCodeRoutes(app, {
43
+ getIsExternalOpenCode: () => true, getIsSharedOpenCodeService: () => true,
44
+ getResolvedOpenCodeBinary: () => binary, getResolvedOpenCodeBinarySource: () => 'installed',
45
+ readSettingsFromDiskMigrated: async () => ({}),
46
+ getOpenCodeResolutionSnapshot: async () => ({ source: 'installed', resolved: binary }),
47
+ buildOpenCodeUrl: (pathname) => `${registration.origin}${pathname}`,
48
+ getOpenCodeAuthHeaders: () => ({ Authorization: `Basic ${Buffer.from(`opencode:${registration.password}`).toString('base64')}` }),
49
+ forceResolvedOpenCodeBinary: (value) => { binary = value; },
50
+ restartOpenCode: async ({ binaryPath }) => {
51
+ await service.start({ binary: binaryPath, env, replace: true });
52
+ registration = await service.readRegistration();
53
+ },
54
+ openchamberDataDir: root,
55
+ });
56
+ const before = await request(app).get('/api/opencode/upgrade-status').expect(200);
57
+ expect(before.body).toMatchObject({ available: true, targetVersion: target });
58
+ const upgrade = await request(app).post('/api/opencode/upgrade').send({ target }).expect(200);
59
+ expect(upgrade.body).toMatchObject({ success: true, restarted: true, version: target });
60
+ const after = await request(app).get('/api/opencode/upgrade-status').expect(200);
61
+ expect(after.body).toMatchObject({ available: false, serveVersion: target });
62
+ } finally {
63
+ if (binary) await promisify(execFile)(binary, ['service', 'stop'], { env, cwd: root, timeout: 20_000, windowsHide: true });
64
+ await fs.rm(root, { recursive: true, force: true });
65
+ }
66
+ }, 180_000);
@@ -1,7 +1,6 @@
1
1
  import { createProjectIdFromPath } from '../projects/project-id.js';
2
2
  import { projectBootstrapSettingsResponse } from './settings-helpers.js';
3
3
  import {
4
- PINNED_OPENCODE2_VERSION,
5
4
  evaluateOpenCodeHealthBody,
6
5
  isOpenCode1xVersion,
7
6
  resolveOpenCode2UpgradeTarget,
@@ -13,6 +12,7 @@ import {
13
12
  } from './ensure-cli.js';
14
13
  import { registerUpgradeScreenRoutes } from './upgrade-screen.js';
15
14
  import { evaluateRuntimeContract } from './runtime-contract.js';
15
+ import { createOpenCodeUpdateDiscovery } from './update-discovery.js';
16
16
  import {
17
17
  buildUpgradeStatusSnapshot,
18
18
  classifyRuntimeOwnership,
@@ -54,6 +54,7 @@ export const registerOpenCodeRoutes = (app, dependencies) => {
54
54
  getResolvedOpenCodeBinarySource = () => null,
55
55
  getActiveSessionCount = () => 0,
56
56
  openchamberDataDir = null,
57
+ discoverOpenCodeUpdate = createOpenCodeUpdateDiscovery(),
57
58
  } = dependencies;
58
59
 
59
60
  const upgradeOperation = createUpgradeOperationState();
@@ -156,12 +157,13 @@ export const registerOpenCodeRoutes = (app, dependencies) => {
156
157
  const buildUpgradeStatus = async () => {
157
158
  const ownership = await resolveOwnershipContext();
158
159
  const contract = await buildLiveContract();
160
+ const targetVersion = await discoverOpenCodeUpdate();
159
161
  const activeCount = typeof getActiveSessionCount === 'function' ? Number(getActiveSessionCount()) || 0 : 0;
160
162
  return buildUpgradeStatusSnapshot({
161
163
  ownership,
162
164
  serveVersion: contract.serveVersion,
163
165
  cliVersion: contract.cliVersion,
164
- targetVersion: PINNED_OPENCODE2_VERSION,
166
+ targetVersion,
165
167
  contract,
166
168
  operation: upgradeOperation.getState(),
167
169
  hasActiveTasks: activeCount > 0,
@@ -342,7 +344,7 @@ export const registerOpenCodeRoutes = (app, dependencies) => {
342
344
  errorCode: 'OPENCODE_UPGRADE_1X_REFUSED',
343
345
  });
344
346
  }
345
- const target = resolveOpenCode2UpgradeTarget(rawTarget);
347
+ const target = resolveOpenCode2UpgradeTarget(rawTarget ?? (await buildUpgradeStatus()).targetVersion);
346
348
  const confirmActive = req.body?.confirmActiveTasks === true || req.body?.force === true;
347
349
  const activeCount = typeof getActiveSessionCount === 'function' ? Number(getActiveSessionCount()) || 0 : 0;
348
350
  if (activeCount > 0 && !confirmActive) {
@@ -707,7 +709,7 @@ export const registerOpenCodeRoutes = (app, dependencies) => {
707
709
 
708
710
  const sources = getProviderSources(providerId, directory);
709
711
  const { getProviderAuth } = await getAuthLibrary();
710
- const auth = getProviderAuth(providerId);
712
+ const auth = await getProviderAuth(providerId);
711
713
  sources.sources.auth.exists = Boolean(auth);
712
714
 
713
715
  return res.json({
@@ -751,12 +753,12 @@ export const registerOpenCodeRoutes = (app, dependencies) => {
751
753
  let removed = false;
752
754
  if (scope === 'auth') {
753
755
  const { removeProviderAuth } = await getAuthLibrary();
754
- removed = removeProviderAuth(providerId);
756
+ removed = await removeProviderAuth(providerId);
755
757
  } else if (scope === 'user' || scope === 'project' || scope === 'custom') {
756
758
  removed = removeProviderConfig(providerId, directory, scope);
757
759
  } else if (scope === 'all') {
758
760
  const { removeProviderAuth } = await getAuthLibrary();
759
- const authRemoved = removeProviderAuth(providerId);
761
+ const authRemoved = await removeProviderAuth(providerId);
760
762
  const userRemoved = removeProviderConfig(providerId, directory, 'user');
761
763
  const projectRemoved = directory ? removeProviderConfig(providerId, directory, 'project') : false;
762
764
  const customRemoved = removeProviderConfig(providerId, directory, 'custom');
@@ -765,14 +767,14 @@ export const registerOpenCodeRoutes = (app, dependencies) => {
765
767
  return res.status(400).json({ error: 'Invalid scope' });
766
768
  }
767
769
 
768
- if (removed) {
770
+ if (removed && scope !== 'auth') {
769
771
  await refreshOpenCodeAfterConfigChange(`provider ${providerId} disconnected (${scope})`);
770
772
  }
771
773
 
772
774
  return res.json({
773
775
  success: true,
774
776
  removed,
775
- requiresReload: removed,
777
+ requiresReload: removed && scope !== 'auth',
776
778
  message: removed ? 'Provider disconnected successfully' : 'Provider was not connected',
777
779
  reloadDelayMs: removed ? clientReloadDelayMs : undefined,
778
780
  });