@openchambery/web 2.0.0-beta.26 → 2.0.0-beta.29

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 (194) hide show
  1. package/README.md +14 -0
  2. package/dist/assets/{AgentAvatar-FyTdEOMr.js → AgentAvatar-O2x_1cFU.js} +1 -1
  3. package/dist/assets/{AssistantView-DvOO2Kf5.js → AssistantView-Bphg-QIn.js} +3 -3
  4. package/dist/assets/{DiagramView-BqjeSOIg.js → DiagramView-CRD_GFch.js} +1 -1
  5. package/dist/assets/{FadeInOnReveal-DHDffzHs.js → FadeInOnReveal-CMBd5BTd.js} +1 -1
  6. package/dist/assets/{Icon-D7NdMmbi.js → Icon-BN87CyS4.js} +1 -1
  7. package/dist/assets/{MarkdownRenderer-kpkRqS42.js → MarkdownRenderer-D5KEGdBn.js} +2 -2
  8. package/dist/assets/{MarkdownRendererImpl-CzBos-Gt.js → MarkdownRendererImpl-C143YQsV.js} +2 -2
  9. package/dist/assets/{MarkstreamRendererImpl-RY_rAgKD.js → MarkstreamRendererImpl-C4rgOuOP.js} +1 -1
  10. package/dist/assets/{MobileDetailNavigation-DgtRh0zu.js → MobileDetailNavigation-D5tcF2Kg.js} +1 -1
  11. package/dist/assets/{MobileShareBridge-BM3cN-gG.js → MobileShareBridge-Bm0JZKHc.js} +3 -3
  12. package/dist/assets/MobileSurface-BzzBCVkG.js +2 -0
  13. package/dist/assets/{MultiRunWindow-Bq4gz78C.js → MultiRunWindow-B6Sb3aEG.js} +1 -1
  14. package/dist/assets/{OnboardingScreen-DMbPMwlp.js → OnboardingScreen-BLjZIBOs.js} +1 -1
  15. package/dist/assets/{OverlayScrollbar-mI9WVa7E.js → OverlayScrollbar-BOqRklf0.js} +1 -1
  16. package/dist/assets/SettingsView-Ds6x2z0F.js +25 -0
  17. package/dist/assets/{SettingsWindow-CHPjbEnI.js → SettingsWindow-D6CN5ahJ.js} +1 -1
  18. package/dist/assets/{TerminalView-DCJuL2nP.js → TerminalView-DkzaJJfh.js} +1 -1
  19. package/dist/assets/{ToolOutputDialog-Cg_Q1t3X.js → ToolOutputDialog-BegFpg1h.js} +1 -1
  20. package/dist/assets/{appThemeRegistry-BxuflaQt.js → appThemeRegistry-BMzuqD8_.js} +1 -1
  21. package/dist/assets/{button-e8fYWIMM.js → button-Ck8ipzBm.js} +1 -1
  22. package/dist/assets/{collapsible-B6wLXHi3.js → collapsible-nU99prkD.js} +1 -1
  23. package/dist/assets/{context-menu-Bq7W1d7y.js → context-menu-DMzZN1oi.js} +1 -1
  24. package/dist/assets/{desktopHosts-C8vf9vRY.js → desktopHosts-DuftEj8B.js} +1 -1
  25. package/dist/assets/{desktopRelayRestore-DDJtljeu.js → desktopRelayRestore-g378BdZf.js} +1 -1
  26. package/dist/assets/{diagram-BBL-LfsC.js → diagram-CvO4P8ds.js} +1 -1
  27. package/dist/assets/{dialog-MRsJpHzu.js → dialog-ByGUkS3c.js} +1 -1
  28. package/dist/assets/{es-DvPwAj0y.js → es-DCjl24dV.js} +3 -3
  29. package/dist/assets/fileMentionTouchSelection-Cuk9C_jN.js +2 -0
  30. package/dist/assets/{fileQueries-CBu-ytXS.js → fileQueries-DuF89nFO.js} +1 -1
  31. package/dist/assets/{fr-XQsoaNKY.js → fr-DfJ5j0t5.js} +3 -3
  32. package/dist/assets/{gitApi-CuIUGt04.js → gitApi-BlWYyRdx.js} +3 -3
  33. package/dist/assets/{input-DbOe1mdT.js → input-CXhFmHcq.js} +1 -1
  34. package/dist/assets/insertionBoundaries-lAwXo-R-.js +1 -0
  35. package/dist/assets/{ja-DslzO96s.js → ja-B_9_w2Xf.js} +3 -3
  36. package/dist/assets/{ko-C_9XOAL0.js → ko-DlGbo3kV.js} +3 -3
  37. package/dist/assets/{main-Sd4Auh_x.js → main-BmiKRvkB.js} +2 -2
  38. package/dist/assets/main-D5SdJE5d.js +169 -0
  39. package/dist/assets/main-qSeeJRW1.css +1 -0
  40. package/dist/assets/{markdownHeightCache-B5VSLl79.js → markdownHeightCache-g_UsWzbA.js} +1 -1
  41. package/dist/assets/{mcpQueries-BYNrkehM.js → mcpQueries-0Oe-8vHC.js} +1 -1
  42. package/dist/assets/{miniChat-Hp9bNmS0.js → miniChat-CSwFHa9K.js} +2 -2
  43. package/dist/assets/mobile-DzJO7usb.js +2 -0
  44. package/dist/assets/multirun-Bdd0yE3M.js +3 -0
  45. package/dist/assets/persistence-Dnj1QOdX.js +3 -0
  46. package/dist/assets/{pl-Dst9Wk53.js → pl-DjUsS8Ei.js} +3 -3
  47. package/dist/assets/platform-pfYy32vr.js +6 -0
  48. package/dist/assets/projectMeta-2dbON46t.js +1 -0
  49. package/dist/assets/{pt-BR-Dq394BPb.js → pt-BR-B8sV7qo1.js} +3 -3
  50. package/dist/assets/renderElectronMiniChatApp-Bjf5yS-2.js +2 -0
  51. package/dist/assets/renderMobileApp-Bv0VxQo1.js +28 -0
  52. package/dist/assets/{runtimeConfig-pTYamMLy.js → runtimeConfig-BOk04YEP.js} +1 -1
  53. package/dist/assets/runtimeEndpointReset-DsuP5FPT.css +1 -0
  54. package/dist/assets/runtimeEndpointReset-Oo4f7T5P.js +148 -0
  55. package/dist/assets/{sessionChangeSummary-DcnZyuWt.js → sessionChangeSummary-Bf5wJLSv.js} +2 -2
  56. package/dist/assets/{sessionLookup-B3t-FerN.js → sessionLookup-pga3-Etf.js} +1 -1
  57. package/dist/assets/{sortable-tabs-strip-B0sZ7mm6.js → sortable-tabs-strip-CHqMkTI3.js} +1 -1
  58. package/dist/assets/{store-D8fmTWK3.js → store-Cuktref-.js} +3 -3
  59. package/dist/assets/{terminalApi-CJyWCuc8.js → terminalApi-BgGraKxs.js} +1 -1
  60. package/dist/assets/{textarea-B5GMntHk.js → textarea-B0Od84ZM.js} +1 -1
  61. package/dist/assets/{tooltip-C64SdRvR.js → tooltip-BKL7J5hx.js} +1 -1
  62. package/dist/assets/{uk-8XAR75Qh.js → uk-Bh7HXyLp.js} +3 -3
  63. package/dist/assets/{url-DF1NFwt_.js → url-DNRMq_0R.js} +1 -1
  64. package/dist/assets/{useAppFontEffects-BQklk6XB.js → useAppFontEffects-gMl7z7An.js} +53 -53
  65. package/dist/assets/{useAssistantUIStore-B75cQm_z.js → useAssistantUIStore-DBwfg9nX.js} +1 -1
  66. package/dist/assets/useDesktopWindowControlsLayout-GOgauAay.js +1 -0
  67. package/dist/assets/{useEffectiveDirectory-Ddx7sZ5A.js → useEffectiveDirectory-DgAB7HWq.js} +1 -1
  68. package/dist/assets/{useFontPreferences-CrteQiAp.js → useFontPreferences-Djxu_uRy.js} +1 -1
  69. package/dist/assets/useMobileNavigationStore-D7uAoBXe.js +415 -0
  70. package/dist/assets/useSessionAutoCleanup-Cg7KJFGa.js +2 -0
  71. package/dist/assets/{useUIStore-Brsa2mKs.js → useUIStore-Dfx6TKvi.js} +1 -1
  72. package/dist/assets/{useWorkerHighlightedLines-VOtVUGkO.js → useWorkerHighlightedLines-DKa2EWBH.js} +1 -1
  73. package/dist/assets/vendor-opencode-client-MF2GBrFx.js +12 -0
  74. package/dist/assets/{vendor-zustand-B4dlGEM2.js → vendor-zustand-DzRqcymP.js} +1 -1
  75. package/dist/assets/{zh-CN-B-ILfrqC.js → zh-CN-Uzia0gk4.js} +3 -3
  76. package/dist/assets/{zh-TW-5HeTVP4z.js → zh-TW-CiI7DLdB.js} +3 -3
  77. package/dist/index.html +10 -10
  78. package/dist/mini-chat.html +10 -10
  79. package/dist/mobile.html +10 -10
  80. package/package.json +3 -3
  81. package/server/index.js +8 -20
  82. package/server/lib/git/DOCUMENTATION.md +1 -0
  83. package/server/lib/git/routes.js +7 -4
  84. package/server/lib/git/routes.test.js +34 -0
  85. package/server/lib/git/service.js +3 -1
  86. package/server/lib/llm/DOCUMENTATION.md +6 -4
  87. package/server/lib/llm/generate.js +8 -9
  88. package/server/lib/llm/generate.test.js +33 -6
  89. package/server/lib/opencode/DOCUMENTATION.md +24 -20
  90. package/server/lib/opencode/agents.js +13 -22
  91. package/server/lib/opencode/auth-state-runtime.js +3 -3
  92. package/server/lib/opencode/auth-state-runtime.test.js +59 -0
  93. package/server/lib/opencode/auth.d.ts +13 -0
  94. package/server/lib/opencode/auth.js +74 -64
  95. package/server/lib/opencode/auth.test.js +57 -0
  96. package/server/lib/opencode/commands.js +1 -1
  97. package/server/lib/opencode/config-entities-layering.test.js +72 -0
  98. package/server/lib/opencode/config-entity-routes.js +9 -122
  99. package/server/lib/opencode/config-entity-routes.test.js +7 -11
  100. package/server/lib/opencode/config-files.d.ts +17 -0
  101. package/server/lib/opencode/config-files.js +187 -0
  102. package/server/lib/opencode/config-files.test.js +201 -0
  103. package/server/lib/opencode/core-routes.js +0 -1
  104. package/server/lib/opencode/core-routes.test.js +0 -15
  105. package/server/lib/opencode/credential-boundary.d.ts +1 -0
  106. package/server/lib/opencode/credential-boundary.js +11 -0
  107. package/server/lib/opencode/ensure-cli.js +4 -1
  108. package/server/lib/opencode/env-config.js +4 -1
  109. package/server/lib/opencode/env-config.test.js +28 -0
  110. package/server/lib/opencode/env-runtime.test.js +2 -1
  111. package/server/lib/opencode/feature-routes-runtime.js +9 -7
  112. package/server/lib/opencode/hmr-state-runtime.js +4 -4
  113. package/server/lib/opencode/lifecycle.js +1 -49
  114. package/server/lib/opencode/lifecycle.test.js +30 -4
  115. package/server/lib/opencode/location-release.d.ts +8 -0
  116. package/server/lib/opencode/location-release.js +32 -0
  117. package/server/lib/opencode/location-release.test.js +43 -0
  118. package/server/lib/opencode/mcp.js +1 -1
  119. package/server/lib/opencode/opencode2-pin.js +2 -1
  120. package/server/lib/opencode/owned-runtime-upgrade.test.js +5 -5
  121. package/server/lib/opencode/plugin-routes.test.js +4 -2
  122. package/server/lib/opencode/plugins.js +12 -60
  123. package/server/lib/opencode/plugins.test.js +5 -3
  124. package/server/lib/opencode/providers.js +6 -3
  125. package/server/lib/opencode/proxy.js +7 -0
  126. package/server/lib/opencode/proxy.test.js +13 -0
  127. package/server/lib/opencode/routes.js +5 -5
  128. package/server/lib/opencode/routes.test.js +18 -18
  129. package/server/lib/opencode/runtime-contract.js +3 -2
  130. package/server/lib/opencode/runtime-contract.test.js +12 -12
  131. package/server/lib/opencode/shared.js +6 -175
  132. package/server/lib/opencode/upgrade-screen.js +4 -11
  133. package/server/lib/opencode/upgrade-screen.test.js +25 -25
  134. package/server/lib/opencode/version-alignment.test.js +40 -0
  135. package/server/lib/quota/DOCUMENTATION.md +9 -0
  136. package/server/lib/quota/provider-credentials.test.js +29 -0
  137. package/server/lib/quota/providers/claude.js +4 -4
  138. package/server/lib/quota/providers/codex.js +4 -4
  139. package/server/lib/quota/providers/codex.test.js +1 -1
  140. package/server/lib/quota/providers/copilot.js +5 -5
  141. package/server/lib/quota/providers/google/auth.js +3 -3
  142. package/server/lib/quota/providers/google/index.js +2 -2
  143. package/server/lib/quota/providers/index.js +3 -7
  144. package/server/lib/quota/providers/index.test.js +2 -2
  145. package/server/lib/quota/providers/kimi.js +4 -4
  146. package/server/lib/quota/providers/minimax-shared.js +4 -4
  147. package/server/lib/quota/providers/nanogpt.js +4 -4
  148. package/server/lib/quota/providers/openai.js +4 -4
  149. package/server/lib/quota/providers/openrouter.js +4 -4
  150. package/server/lib/quota/providers/wafer.js +4 -4
  151. package/server/lib/quota/providers/zai.js +4 -4
  152. package/server/lib/quota/providers/zhipuai-coding-plan.js +6 -20
  153. package/server/lib/quota/routes.js +1 -1
  154. package/server/lib/scheduled-tasks/DOCUMENTATION.md +1 -0
  155. package/server/lib/scheduled-tasks/runtime.js +2 -3
  156. package/server/lib/session-index/DOCUMENTATION.md +7 -0
  157. package/server/lib/session-index/event-ingest.js +13 -0
  158. package/server/lib/session-index/event-ingest.test.js +7 -0
  159. package/server/lib/session-index/service.js +45 -0
  160. package/server/lib/session-index/service.test.js +47 -0
  161. package/server/lib/session-metadata/DOCUMENTATION.md +6 -0
  162. package/server/lib/session-metadata/session-metadata-store.test.js +23 -0
  163. package/server/lib/session-metadata/session-projection.d.ts +1 -0
  164. package/server/lib/session-metadata/session-projection.js +23 -0
  165. package/server/lib/session-metadata/session-projection.test.js +35 -0
  166. package/server/lib/session-title/DOCUMENTATION.md +36 -8
  167. package/server/lib/session-title/native-runtime.test.js +42 -0
  168. package/server/lib/session-title/runtime.js +18 -8
  169. package/server/lib/session-title/session-access.js +108 -3
  170. package/server/lib/session-title/session-access.test.js +125 -7
  171. package/server/lib/tts/DOCUMENTATION.md +7 -5
  172. package/server/lib/tts/credential-source.test.js +23 -0
  173. package/server/lib/tts/routes.js +2 -2
  174. package/server/lib/tts/service.js +16 -37
  175. package/dist/assets/MobileSurface-ocVWenHA.js +0 -2
  176. package/dist/assets/SettingsView-CIZz40Jg.js +0 -25
  177. package/dist/assets/fileMentionTouchSelection-Bj67pPo7.js +0 -3
  178. package/dist/assets/insertionBoundaries-nzs1Q3YR.js +0 -1
  179. package/dist/assets/main-D2BzpnUt.css +0 -1
  180. package/dist/assets/main-DjilfYN7.js +0 -169
  181. package/dist/assets/mobile-fBLEOx9_.js +0 -2
  182. package/dist/assets/multirun-D7W7oCj4.js +0 -4
  183. package/dist/assets/persistence-CMWO3jFo.js +0 -3
  184. package/dist/assets/platform-B0p43squ.js +0 -6
  185. package/dist/assets/projectMeta-DrUKrYt0.js +0 -3
  186. package/dist/assets/renderElectronMiniChatApp-D1xipCO5.js +0 -2
  187. package/dist/assets/renderMobileApp-DSk7Qs-_.js +0 -28
  188. package/dist/assets/runtimeEndpointReset-B5HEMuiB.js +0 -149
  189. package/dist/assets/useDesktopWindowControlsLayout-CrzYwZDQ.js +0 -1
  190. package/dist/assets/useMobileNavigationStore-i4cXNoOf.js +0 -414
  191. package/dist/assets/useSessionAutoCleanup-Dzd0fGSe.js +0 -2
  192. package/dist/assets/vendor-opencode-client-C12gfjag.js +0 -12
  193. package/server/lib/opencode/snippets.js +0 -242
  194. package/server/lib/opencode/snippets.test.js +0 -97
@@ -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('');
@@ -709,7 +709,7 @@ export const registerOpenCodeRoutes = (app, dependencies) => {
709
709
 
710
710
  const sources = getProviderSources(providerId, directory);
711
711
  const { getProviderAuth } = await getAuthLibrary();
712
- const auth = getProviderAuth(providerId);
712
+ const auth = await getProviderAuth(providerId);
713
713
  sources.sources.auth.exists = Boolean(auth);
714
714
 
715
715
  return res.json({
@@ -753,12 +753,12 @@ export const registerOpenCodeRoutes = (app, dependencies) => {
753
753
  let removed = false;
754
754
  if (scope === 'auth') {
755
755
  const { removeProviderAuth } = await getAuthLibrary();
756
- removed = removeProviderAuth(providerId);
756
+ removed = await removeProviderAuth(providerId);
757
757
  } else if (scope === 'user' || scope === 'project' || scope === 'custom') {
758
758
  removed = removeProviderConfig(providerId, directory, scope);
759
759
  } else if (scope === 'all') {
760
760
  const { removeProviderAuth } = await getAuthLibrary();
761
- const authRemoved = removeProviderAuth(providerId);
761
+ const authRemoved = await removeProviderAuth(providerId);
762
762
  const userRemoved = removeProviderConfig(providerId, directory, 'user');
763
763
  const projectRemoved = directory ? removeProviderConfig(providerId, directory, 'project') : false;
764
764
  const customRemoved = removeProviderConfig(providerId, directory, 'custom');
@@ -767,14 +767,14 @@ export const registerOpenCodeRoutes = (app, dependencies) => {
767
767
  return res.status(400).json({ error: 'Invalid scope' });
768
768
  }
769
769
 
770
- if (removed) {
770
+ if (removed && scope !== 'auth') {
771
771
  await refreshOpenCodeAfterConfigChange(`provider ${providerId} disconnected (${scope})`);
772
772
  }
773
773
 
774
774
  return res.json({
775
775
  success: true,
776
776
  removed,
777
- requiresReload: removed,
777
+ requiresReload: removed && scope !== 'auth',
778
778
  message: removed ? 'Provider disconnected successfully' : 'Provider was not connected',
779
779
  reloadDelayMs: removed ? clientReloadDelayMs : undefined,
780
780
  });
@@ -233,39 +233,39 @@ describe('opencode2 upgrade pin (ticket 12)', () => {
233
233
  });
234
234
 
235
235
  it('upgrades a shared service using the owned cache and verifies the running version', async () => {
236
- const binary = '/cache/opencode-cli/2.0.14/opencode';
236
+ const binary = '/cache/opencode-cli/2.0.23/opencode';
237
237
  vi.mocked(installPinnedOpenCode2Cli).mockResolvedValueOnce(binary);
238
- vi.mocked(readOpenCode2BinaryVersion).mockReturnValueOnce('2.0.14');
239
- vi.stubGlobal('fetch', vi.fn(async () => new Response(JSON.stringify({ version: '2.0.14', pid: 42 }))));
238
+ vi.mocked(readOpenCode2BinaryVersion).mockReturnValueOnce('2.0.23');
239
+ vi.stubGlobal('fetch', vi.fn(async () => new Response(JSON.stringify({ version: '2.0.23', pid: 42 }))));
240
240
  let resolvedBinary = '/usr/local/bin/opencode';
241
241
  const restartOpenCode = vi.fn(async () => {});
242
242
  const { app } = createUpgradeApp({
243
243
  getIsExternalOpenCode: () => true,
244
244
  getIsSharedOpenCodeService: () => true,
245
245
  getResolvedOpenCodeBinary: () => resolvedBinary,
246
- getOpenCodeCliVersion: () => '2.0.14',
246
+ getOpenCodeCliVersion: () => '2.0.23',
247
247
  forceResolvedOpenCodeBinary: (value) => { resolvedBinary = value; },
248
248
  restartOpenCode,
249
249
  waitForOpenCodeReady: vi.fn(),
250
250
  openchamberDataDir: '/cache',
251
251
  });
252
252
 
253
- const response = await request(app).post('/api/opencode/upgrade').send({ target: '2.0.14' }).expect(200);
253
+ const response = await request(app).post('/api/opencode/upgrade').send({ target: '2.0.23' }).expect(200);
254
254
  expect(restartOpenCode).toHaveBeenCalledWith({ binaryPath: binary });
255
- expect(response.body).toMatchObject({ success: true, version: '2.0.14', ownership: 'shared-service' });
255
+ expect(response.body).toMatchObject({ success: true, version: '2.0.23', ownership: 'shared-service' });
256
256
  });
257
257
 
258
- it('discovers the remote release and updates a shared 2.0.16 service to 2.0.18', async () => {
258
+ it('discovers the remote release and updates a shared 2.0.16 service to 2.0.23', async () => {
259
259
  let serveVersion = '2.0.16';
260
260
  let binary = '/usr/local/bin/opencode';
261
- const installed = '/cache/opencode-cli/2.0.18/opencode';
261
+ const installed = '/cache/opencode-cli/2.0.23/opencode';
262
262
  const fetchMock = vi.fn(async (url) => new Response(JSON.stringify({
263
- version: String(url).includes('registry.npmjs.org') ? '2.0.18' : serveVersion,
263
+ version: String(url).includes('registry.npmjs.org') ? '2.0.23' : serveVersion,
264
264
  })));
265
265
  vi.stubGlobal('fetch', fetchMock);
266
- vi.mocked(readOpenCode2BinaryVersion).mockReturnValue('2.0.18');
266
+ vi.mocked(readOpenCode2BinaryVersion).mockReturnValue('2.0.23');
267
267
  vi.mocked(installPinnedOpenCode2Cli).mockResolvedValueOnce(installed);
268
- const restartOpenCode = vi.fn(async () => { serveVersion = '2.0.18'; });
268
+ const restartOpenCode = vi.fn(async () => { serveVersion = '2.0.23'; });
269
269
  const { app } = createUpgradeApp({
270
270
  discoverOpenCodeUpdate: createOpenCodeUpdateDiscovery(),
271
271
  getIsExternalOpenCode: () => true,
@@ -277,12 +277,12 @@ describe('opencode2 upgrade pin (ticket 12)', () => {
277
277
  openchamberDataDir: '/cache',
278
278
  });
279
279
  const before = await request(app).get('/api/opencode/upgrade-status').expect(200);
280
- expect(before.body).toMatchObject({ available: true, serveVersion: '2.0.16', targetVersion: '2.0.18', canManage: true });
280
+ expect(before.body).toMatchObject({ available: true, serveVersion: '2.0.16', targetVersion: '2.0.23', canManage: true });
281
281
  const result = await request(app).post('/api/opencode/upgrade').send({}).expect(200);
282
- expect(result.body).toMatchObject({ success: true, restarted: true, version: '2.0.18' });
282
+ expect(result.body).toMatchObject({ success: true, restarted: true, version: '2.0.23' });
283
283
  expect(restartOpenCode).toHaveBeenCalledWith({ binaryPath: installed });
284
284
  const after = await request(app).get('/api/opencode/upgrade-status').expect(200);
285
- expect(after.body).toMatchObject({ available: false, serveVersion: '2.0.18' });
285
+ expect(after.body).toMatchObject({ available: false, serveVersion: '2.0.23' });
286
286
  expect(fetchMock.mock.calls.filter(([url]) => String(url).includes('registry.npmjs.org'))).toHaveLength(1);
287
287
  vi.mocked(readOpenCode2BinaryVersion).mockReset();
288
288
  });
@@ -385,7 +385,7 @@ describe('runtime contract routes (ticket 11)', () => {
385
385
  ok: true,
386
386
  status: 200,
387
387
  statusText: 'OK',
388
- json: async () => ({ version: '2.0.12', pid: 1 }),
388
+ json: async () => ({ version: '2.0.20', pid: 1 }),
389
389
  })));
390
390
  const { app } = createContractApp();
391
391
 
@@ -395,7 +395,7 @@ describe('runtime contract routes (ticket 11)', () => {
395
395
  expect(health.body.executionAllowed).toBe(true);
396
396
 
397
397
  const contract = await request(app).get('/api/opencode/contract').expect(200);
398
- expect(contract.body.serveVersion).toBe('2.0.12');
398
+ expect(contract.body.serveVersion).toBe('2.0.20');
399
399
  expect(contract.body.capabilities['core.protocol'].available).toBe(true);
400
400
  });
401
401
 
@@ -439,7 +439,7 @@ describe('GET /api/opencode/health', () => {
439
439
  status: 200,
440
440
  statusText: 'OK',
441
441
  json: async () => ({
442
- version: '2.0.12',
442
+ version: '2.0.20',
443
443
  pid: 4242,
444
444
  urls: { api: 'http://127.0.0.1:4096' },
445
445
  paths: { config: '/tmp/config', data: '/tmp/data' },
@@ -453,7 +453,7 @@ describe('GET /api/opencode/health', () => {
453
453
 
454
454
  expect(response.body).toMatchObject({
455
455
  healthy: true,
456
- version: '2.0.12',
456
+ version: '2.0.20',
457
457
  executionAllowed: true,
458
458
  protocolCompatible: true,
459
459
  });
@@ -7,20 +7,21 @@
7
7
  * execution semantics.
8
8
  *
9
9
  * Evidence bounds (ticket 11):
10
- * - MIN_VERIFIED: pin / installed verification (`2.0.12`)
10
+ * - MIN_VERIFIED: credential API availability (`2.0.20`)
11
11
  * Any acceptable OpenCode 2.x at or above MIN_VERIFIED may execute. Below-min
12
12
  * 2.x and 1.x are refused with upgrade guidance; there is no upper version cap.
13
13
  */
14
14
 
15
15
  import {
16
16
  PINNED_OPENCODE2_VERSION,
17
+ MINIMUM_OPENCODE2_VERSION,
17
18
  compareOpenCode2Versions,
18
19
  isAcceptableOpenCode2HealthVersion,
19
20
  isOpenCode1xVersion,
20
21
  } from './opencode2-pin.js';
21
22
 
22
23
  /** Lowest version with documented OpenChamber + OpenCode 2 contract tests. */
23
- export const RUNTIME_CONTRACT_MIN_VERIFIED = PINNED_OPENCODE2_VERSION;
24
+ export const RUNTIME_CONTRACT_MIN_VERIFIED = MINIMUM_OPENCODE2_VERSION;
24
25
 
25
26
  /**
26
27
  * Optional / core capabilities and the minimum serve version that admits them.
@@ -13,14 +13,14 @@ import {
13
13
  } from './runtime-contract.js';
14
14
 
15
15
  describe('runtime contract admission (ticket 11)', () => {
16
- it('records minimum verified bound from pin', () => {
17
- expect(RUNTIME_CONTRACT_MIN_VERIFIED).toBe('2.0.12');
16
+ it('records the credential API floor independently of the install pin', () => {
17
+ expect(RUNTIME_CONTRACT_MIN_VERIFIED).toBe('2.0.20');
18
18
  });
19
19
 
20
20
  it('classifies version bands with a 2.x floor only', () => {
21
- expect(classifyRuntimeVersionBand('2.0.12')).toBe('verified');
22
- expect(classifyRuntimeVersionBand('2.0.14')).toBe('verified');
23
- expect(classifyRuntimeVersionBand('2.0.15')).toBe('verified');
21
+ expect(classifyRuntimeVersionBand('2.0.19')).toBe('below-min');
22
+ expect(classifyRuntimeVersionBand('2.0.20')).toBe('verified');
23
+ expect(classifyRuntimeVersionBand('2.0.23')).toBe('verified');
24
24
  expect(classifyRuntimeVersionBand('2.1.0')).toBe('verified');
25
25
  expect(classifyRuntimeVersionBand('2.0.5')).toBe('below-min');
26
26
  expect(classifyRuntimeVersionBand('1.18.18')).toBe('1x');
@@ -30,8 +30,8 @@ describe('runtime contract admission (ticket 11)', () => {
30
30
 
31
31
  it('admits verified serve for core protocol and optional caps', () => {
32
32
  const result = evaluateRuntimeContract({
33
- serveVersion: '2.0.12',
34
- cliVersion: '2.0.12',
33
+ serveVersion: '2.0.20',
34
+ cliVersion: '2.0.20',
35
35
  reachable: true,
36
36
  authenticated: true,
37
37
  healthOk: true,
@@ -65,7 +65,7 @@ describe('runtime contract admission (ticket 11)', () => {
65
65
 
66
66
  it('admits newer 2.x above the documented floor for execution', () => {
67
67
  const result = evaluateRuntimeContract({
68
- serveVersion: '2.0.15',
68
+ serveVersion: '2.0.23',
69
69
  reachable: true,
70
70
  authenticated: true,
71
71
  healthOk: true,
@@ -105,12 +105,12 @@ describe('runtime contract admission (ticket 11)', () => {
105
105
  expect(evaluateRuntimeContract({
106
106
  reachable: true,
107
107
  authenticated: false,
108
- serveVersion: '2.0.12',
108
+ serveVersion: '2.0.20',
109
109
  healthOk: true,
110
110
  }).phase).toBe('unauthenticated');
111
111
 
112
112
  const migration = evaluateRuntimeContract({
113
- serveVersion: '2.0.12',
113
+ serveVersion: '2.0.20',
114
114
  reachable: true,
115
115
  authenticated: true,
116
116
  healthOk: true,
@@ -122,7 +122,7 @@ describe('runtime contract admission (ticket 11)', () => {
122
122
  expect(migration.migrationExecutable).toBe(false);
123
123
 
124
124
  const mismatch = evaluateRuntimeContract({
125
- serveVersion: '2.0.12',
125
+ serveVersion: '2.0.20',
126
126
  cliVersion: '2.0.14',
127
127
  reachable: true,
128
128
  authenticated: true,
@@ -179,7 +179,7 @@ describe('runtime contract admission (ticket 11)', () => {
179
179
  migrationAdmitTranscript: true,
180
180
  });
181
181
  const body = runtimeContractExecutionBlockedBody(contract);
182
- expect(body.error).toContain('2.0.12');
182
+ expect(body.error).toContain('2.0.20');
183
183
  expect(body.error).toMatch(/Settings → About/i);
184
184
  expect(formatRuntimeContractBlockedError(contract)).toBe(body.error);
185
185
  });