@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.
- package/README.md +14 -0
- package/dist/assets/{AgentAvatar-FyTdEOMr.js → AgentAvatar-O2x_1cFU.js} +1 -1
- package/dist/assets/{AssistantView-DvOO2Kf5.js → AssistantView-Bphg-QIn.js} +3 -3
- package/dist/assets/{DiagramView-BqjeSOIg.js → DiagramView-CRD_GFch.js} +1 -1
- package/dist/assets/{FadeInOnReveal-DHDffzHs.js → FadeInOnReveal-CMBd5BTd.js} +1 -1
- package/dist/assets/{Icon-D7NdMmbi.js → Icon-BN87CyS4.js} +1 -1
- package/dist/assets/{MarkdownRenderer-kpkRqS42.js → MarkdownRenderer-D5KEGdBn.js} +2 -2
- package/dist/assets/{MarkdownRendererImpl-CzBos-Gt.js → MarkdownRendererImpl-C143YQsV.js} +2 -2
- package/dist/assets/{MarkstreamRendererImpl-RY_rAgKD.js → MarkstreamRendererImpl-C4rgOuOP.js} +1 -1
- package/dist/assets/{MobileDetailNavigation-DgtRh0zu.js → MobileDetailNavigation-D5tcF2Kg.js} +1 -1
- package/dist/assets/{MobileShareBridge-BM3cN-gG.js → MobileShareBridge-Bm0JZKHc.js} +3 -3
- package/dist/assets/MobileSurface-BzzBCVkG.js +2 -0
- package/dist/assets/{MultiRunWindow-Bq4gz78C.js → MultiRunWindow-B6Sb3aEG.js} +1 -1
- package/dist/assets/{OnboardingScreen-DMbPMwlp.js → OnboardingScreen-BLjZIBOs.js} +1 -1
- package/dist/assets/{OverlayScrollbar-mI9WVa7E.js → OverlayScrollbar-BOqRklf0.js} +1 -1
- package/dist/assets/SettingsView-Ds6x2z0F.js +25 -0
- package/dist/assets/{SettingsWindow-CHPjbEnI.js → SettingsWindow-D6CN5ahJ.js} +1 -1
- package/dist/assets/{TerminalView-DCJuL2nP.js → TerminalView-DkzaJJfh.js} +1 -1
- package/dist/assets/{ToolOutputDialog-Cg_Q1t3X.js → ToolOutputDialog-BegFpg1h.js} +1 -1
- package/dist/assets/{appThemeRegistry-BxuflaQt.js → appThemeRegistry-BMzuqD8_.js} +1 -1
- package/dist/assets/{button-e8fYWIMM.js → button-Ck8ipzBm.js} +1 -1
- package/dist/assets/{collapsible-B6wLXHi3.js → collapsible-nU99prkD.js} +1 -1
- package/dist/assets/{context-menu-Bq7W1d7y.js → context-menu-DMzZN1oi.js} +1 -1
- package/dist/assets/{desktopHosts-C8vf9vRY.js → desktopHosts-DuftEj8B.js} +1 -1
- package/dist/assets/{desktopRelayRestore-DDJtljeu.js → desktopRelayRestore-g378BdZf.js} +1 -1
- package/dist/assets/{diagram-BBL-LfsC.js → diagram-CvO4P8ds.js} +1 -1
- package/dist/assets/{dialog-MRsJpHzu.js → dialog-ByGUkS3c.js} +1 -1
- package/dist/assets/{es-DvPwAj0y.js → es-DCjl24dV.js} +3 -3
- package/dist/assets/fileMentionTouchSelection-Cuk9C_jN.js +2 -0
- package/dist/assets/{fileQueries-CBu-ytXS.js → fileQueries-DuF89nFO.js} +1 -1
- package/dist/assets/{fr-XQsoaNKY.js → fr-DfJ5j0t5.js} +3 -3
- package/dist/assets/{gitApi-CuIUGt04.js → gitApi-BlWYyRdx.js} +3 -3
- package/dist/assets/{input-DbOe1mdT.js → input-CXhFmHcq.js} +1 -1
- package/dist/assets/insertionBoundaries-lAwXo-R-.js +1 -0
- package/dist/assets/{ja-DslzO96s.js → ja-B_9_w2Xf.js} +3 -3
- package/dist/assets/{ko-C_9XOAL0.js → ko-DlGbo3kV.js} +3 -3
- package/dist/assets/{main-Sd4Auh_x.js → main-BmiKRvkB.js} +2 -2
- package/dist/assets/main-D5SdJE5d.js +169 -0
- package/dist/assets/main-qSeeJRW1.css +1 -0
- package/dist/assets/{markdownHeightCache-B5VSLl79.js → markdownHeightCache-g_UsWzbA.js} +1 -1
- package/dist/assets/{mcpQueries-BYNrkehM.js → mcpQueries-0Oe-8vHC.js} +1 -1
- package/dist/assets/{miniChat-Hp9bNmS0.js → miniChat-CSwFHa9K.js} +2 -2
- package/dist/assets/mobile-DzJO7usb.js +2 -0
- package/dist/assets/multirun-Bdd0yE3M.js +3 -0
- package/dist/assets/persistence-Dnj1QOdX.js +3 -0
- package/dist/assets/{pl-Dst9Wk53.js → pl-DjUsS8Ei.js} +3 -3
- package/dist/assets/platform-pfYy32vr.js +6 -0
- package/dist/assets/projectMeta-2dbON46t.js +1 -0
- package/dist/assets/{pt-BR-Dq394BPb.js → pt-BR-B8sV7qo1.js} +3 -3
- package/dist/assets/renderElectronMiniChatApp-Bjf5yS-2.js +2 -0
- package/dist/assets/renderMobileApp-Bv0VxQo1.js +28 -0
- package/dist/assets/{runtimeConfig-pTYamMLy.js → runtimeConfig-BOk04YEP.js} +1 -1
- package/dist/assets/runtimeEndpointReset-DsuP5FPT.css +1 -0
- package/dist/assets/runtimeEndpointReset-Oo4f7T5P.js +148 -0
- package/dist/assets/{sessionChangeSummary-DcnZyuWt.js → sessionChangeSummary-Bf5wJLSv.js} +2 -2
- package/dist/assets/{sessionLookup-B3t-FerN.js → sessionLookup-pga3-Etf.js} +1 -1
- package/dist/assets/{sortable-tabs-strip-B0sZ7mm6.js → sortable-tabs-strip-CHqMkTI3.js} +1 -1
- package/dist/assets/{store-D8fmTWK3.js → store-Cuktref-.js} +3 -3
- package/dist/assets/{terminalApi-CJyWCuc8.js → terminalApi-BgGraKxs.js} +1 -1
- package/dist/assets/{textarea-B5GMntHk.js → textarea-B0Od84ZM.js} +1 -1
- package/dist/assets/{tooltip-C64SdRvR.js → tooltip-BKL7J5hx.js} +1 -1
- package/dist/assets/{uk-8XAR75Qh.js → uk-Bh7HXyLp.js} +3 -3
- package/dist/assets/{url-DF1NFwt_.js → url-DNRMq_0R.js} +1 -1
- package/dist/assets/{useAppFontEffects-BQklk6XB.js → useAppFontEffects-gMl7z7An.js} +53 -53
- package/dist/assets/{useAssistantUIStore-B75cQm_z.js → useAssistantUIStore-DBwfg9nX.js} +1 -1
- package/dist/assets/useDesktopWindowControlsLayout-GOgauAay.js +1 -0
- package/dist/assets/{useEffectiveDirectory-Ddx7sZ5A.js → useEffectiveDirectory-DgAB7HWq.js} +1 -1
- package/dist/assets/{useFontPreferences-CrteQiAp.js → useFontPreferences-Djxu_uRy.js} +1 -1
- package/dist/assets/useMobileNavigationStore-D7uAoBXe.js +415 -0
- package/dist/assets/useSessionAutoCleanup-Cg7KJFGa.js +2 -0
- package/dist/assets/{useUIStore-Brsa2mKs.js → useUIStore-Dfx6TKvi.js} +1 -1
- package/dist/assets/{useWorkerHighlightedLines-VOtVUGkO.js → useWorkerHighlightedLines-DKa2EWBH.js} +1 -1
- package/dist/assets/vendor-opencode-client-MF2GBrFx.js +12 -0
- package/dist/assets/{vendor-zustand-B4dlGEM2.js → vendor-zustand-DzRqcymP.js} +1 -1
- package/dist/assets/{zh-CN-B-ILfrqC.js → zh-CN-Uzia0gk4.js} +3 -3
- package/dist/assets/{zh-TW-5HeTVP4z.js → zh-TW-CiI7DLdB.js} +3 -3
- package/dist/index.html +10 -10
- package/dist/mini-chat.html +10 -10
- package/dist/mobile.html +10 -10
- package/package.json +3 -3
- package/server/index.js +8 -20
- package/server/lib/git/DOCUMENTATION.md +1 -0
- package/server/lib/git/routes.js +7 -4
- package/server/lib/git/routes.test.js +34 -0
- package/server/lib/git/service.js +3 -1
- package/server/lib/llm/DOCUMENTATION.md +6 -4
- package/server/lib/llm/generate.js +8 -9
- package/server/lib/llm/generate.test.js +33 -6
- package/server/lib/opencode/DOCUMENTATION.md +24 -20
- package/server/lib/opencode/agents.js +13 -22
- package/server/lib/opencode/auth-state-runtime.js +3 -3
- package/server/lib/opencode/auth-state-runtime.test.js +59 -0
- package/server/lib/opencode/auth.d.ts +13 -0
- package/server/lib/opencode/auth.js +74 -64
- package/server/lib/opencode/auth.test.js +57 -0
- package/server/lib/opencode/commands.js +1 -1
- package/server/lib/opencode/config-entities-layering.test.js +72 -0
- package/server/lib/opencode/config-entity-routes.js +9 -122
- package/server/lib/opencode/config-entity-routes.test.js +7 -11
- package/server/lib/opencode/config-files.d.ts +17 -0
- package/server/lib/opencode/config-files.js +187 -0
- package/server/lib/opencode/config-files.test.js +201 -0
- package/server/lib/opencode/core-routes.js +0 -1
- package/server/lib/opencode/core-routes.test.js +0 -15
- package/server/lib/opencode/credential-boundary.d.ts +1 -0
- package/server/lib/opencode/credential-boundary.js +11 -0
- package/server/lib/opencode/ensure-cli.js +4 -1
- package/server/lib/opencode/env-config.js +4 -1
- package/server/lib/opencode/env-config.test.js +28 -0
- package/server/lib/opencode/env-runtime.test.js +2 -1
- package/server/lib/opencode/feature-routes-runtime.js +9 -7
- package/server/lib/opencode/hmr-state-runtime.js +4 -4
- package/server/lib/opencode/lifecycle.js +1 -49
- package/server/lib/opencode/lifecycle.test.js +30 -4
- package/server/lib/opencode/location-release.d.ts +8 -0
- package/server/lib/opencode/location-release.js +32 -0
- package/server/lib/opencode/location-release.test.js +43 -0
- package/server/lib/opencode/mcp.js +1 -1
- package/server/lib/opencode/opencode2-pin.js +2 -1
- package/server/lib/opencode/owned-runtime-upgrade.test.js +5 -5
- package/server/lib/opencode/plugin-routes.test.js +4 -2
- package/server/lib/opencode/plugins.js +12 -60
- package/server/lib/opencode/plugins.test.js +5 -3
- package/server/lib/opencode/providers.js +6 -3
- package/server/lib/opencode/proxy.js +7 -0
- package/server/lib/opencode/proxy.test.js +13 -0
- package/server/lib/opencode/routes.js +5 -5
- package/server/lib/opencode/routes.test.js +18 -18
- package/server/lib/opencode/runtime-contract.js +3 -2
- package/server/lib/opencode/runtime-contract.test.js +12 -12
- package/server/lib/opencode/shared.js +6 -175
- package/server/lib/opencode/upgrade-screen.js +4 -11
- package/server/lib/opencode/upgrade-screen.test.js +25 -25
- package/server/lib/opencode/version-alignment.test.js +40 -0
- package/server/lib/quota/DOCUMENTATION.md +9 -0
- package/server/lib/quota/provider-credentials.test.js +29 -0
- package/server/lib/quota/providers/claude.js +4 -4
- package/server/lib/quota/providers/codex.js +4 -4
- package/server/lib/quota/providers/codex.test.js +1 -1
- package/server/lib/quota/providers/copilot.js +5 -5
- package/server/lib/quota/providers/google/auth.js +3 -3
- package/server/lib/quota/providers/google/index.js +2 -2
- package/server/lib/quota/providers/index.js +3 -7
- package/server/lib/quota/providers/index.test.js +2 -2
- package/server/lib/quota/providers/kimi.js +4 -4
- package/server/lib/quota/providers/minimax-shared.js +4 -4
- package/server/lib/quota/providers/nanogpt.js +4 -4
- package/server/lib/quota/providers/openai.js +4 -4
- package/server/lib/quota/providers/openrouter.js +4 -4
- package/server/lib/quota/providers/wafer.js +4 -4
- package/server/lib/quota/providers/zai.js +4 -4
- package/server/lib/quota/providers/zhipuai-coding-plan.js +6 -20
- package/server/lib/quota/routes.js +1 -1
- package/server/lib/scheduled-tasks/DOCUMENTATION.md +1 -0
- package/server/lib/scheduled-tasks/runtime.js +2 -3
- package/server/lib/session-index/DOCUMENTATION.md +7 -0
- package/server/lib/session-index/event-ingest.js +13 -0
- package/server/lib/session-index/event-ingest.test.js +7 -0
- package/server/lib/session-index/service.js +45 -0
- package/server/lib/session-index/service.test.js +47 -0
- package/server/lib/session-metadata/DOCUMENTATION.md +6 -0
- package/server/lib/session-metadata/session-metadata-store.test.js +23 -0
- package/server/lib/session-metadata/session-projection.d.ts +1 -0
- package/server/lib/session-metadata/session-projection.js +23 -0
- package/server/lib/session-metadata/session-projection.test.js +35 -0
- package/server/lib/session-title/DOCUMENTATION.md +36 -8
- package/server/lib/session-title/native-runtime.test.js +42 -0
- package/server/lib/session-title/runtime.js +18 -8
- package/server/lib/session-title/session-access.js +108 -3
- package/server/lib/session-title/session-access.test.js +125 -7
- package/server/lib/tts/DOCUMENTATION.md +7 -5
- package/server/lib/tts/credential-source.test.js +23 -0
- package/server/lib/tts/routes.js +2 -2
- package/server/lib/tts/service.js +16 -37
- package/dist/assets/MobileSurface-ocVWenHA.js +0 -2
- package/dist/assets/SettingsView-CIZz40Jg.js +0 -25
- package/dist/assets/fileMentionTouchSelection-Bj67pPo7.js +0 -3
- package/dist/assets/insertionBoundaries-nzs1Q3YR.js +0 -1
- package/dist/assets/main-D2BzpnUt.css +0 -1
- package/dist/assets/main-DjilfYN7.js +0 -169
- package/dist/assets/mobile-fBLEOx9_.js +0 -2
- package/dist/assets/multirun-D7W7oCj4.js +0 -4
- package/dist/assets/persistence-CMWO3jFo.js +0 -3
- package/dist/assets/platform-B0p43squ.js +0 -6
- package/dist/assets/projectMeta-DrUKrYt0.js +0 -3
- package/dist/assets/renderElectronMiniChatApp-D1xipCO5.js +0 -2
- package/dist/assets/renderMobileApp-DSk7Qs-_.js +0 -28
- package/dist/assets/runtimeEndpointReset-B5HEMuiB.js +0 -149
- package/dist/assets/useDesktopWindowControlsLayout-CrzYwZDQ.js +0 -1
- package/dist/assets/useMobileNavigationStore-i4cXNoOf.js +0 -414
- package/dist/assets/useSessionAutoCleanup-Dzd0fGSe.js +0 -2
- package/dist/assets/vendor-opencode-client-C12gfjag.js +0 -12
- package/server/lib/opencode/snippets.js +0 -242
- 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
|
|
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
|
|
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,7 +23,6 @@ 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.
|
|
29
28
|
- `packages/web/server/lib/opencode/shutdown-runtime.js`: graceful shutdown orchestration runtime for watcher/session/terminal/process/server teardown.
|
|
@@ -55,13 +54,12 @@ This module provides OpenCode server integration utilities for the web server ru
|
|
|
55
54
|
- `packages/web/server/lib/ui-auth/ui-passkeys.js`: UI passkey storage and WebAuthn registration/authentication helpers (outside OpenCode module).
|
|
56
55
|
|
|
57
56
|
## Public exports (auth.js)
|
|
58
|
-
- `
|
|
59
|
-
- `
|
|
60
|
-
- `
|
|
61
|
-
- `getProviderAuth(providerId)`:
|
|
62
|
-
- `
|
|
63
|
-
- `
|
|
64
|
-
- `OPENCODE_DATA_DIR`: OpenCode data directory path constant.
|
|
57
|
+
- `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.
|
|
58
|
+
- `openCodeCredentialSource({ buildOpenCodeUrl, getOpenCodeAuthHeaders })`: resolves the current endpoint/auth at each read rather than capturing a previous runtime.
|
|
59
|
+
- `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.
|
|
60
|
+
- `getProviderAuth(providerId)`: async selected stored credential or authoritative `null`; network/auth/schema failures reject with a sanitized error, never empty success.
|
|
61
|
+
- `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.
|
|
62
|
+
- 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
63
|
|
|
66
64
|
## Public exports (shared.js)
|
|
67
65
|
- `OPENCODE_CONFIG_DIR`, `AGENT_DIR`, `COMMAND_DIR`, `SKILL_DIR`, `CONFIG_FILE`, `CUSTOM_CONFIG_FILE`: Path constants.
|
|
@@ -89,9 +87,9 @@ This module provides OpenCode server integration utilities for the web server ru
|
|
|
89
87
|
- `PUT /api/behavior/agents-md`
|
|
90
88
|
- `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.
|
|
91
89
|
- `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.
|
|
92
|
-
- `GET /api/opencode/compatibility`: startup upgrade-screen status. `
|
|
93
|
-
- `POST /api/opencode/install-required`: installs the
|
|
94
|
-
- `GET /api/opencode/contract
|
|
90
|
+
- `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.
|
|
91
|
+
- `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.
|
|
92
|
+
- `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
93
|
- `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
94
|
- `GET /api/opencode/version`: serve + CLI versions, mismatch flag, and contract snapshot
|
|
97
95
|
- `POST /api/opencode/directory`
|
|
@@ -130,7 +128,7 @@ This module provides OpenCode server integration utilities for the web server ru
|
|
|
130
128
|
- `resolveOpenCode2UpgradeTarget(target)` / `rejectOpenCode1xUpgradeTarget(target)`: upgrade targets default to the pin and refuse 1.x.
|
|
131
129
|
|
|
132
130
|
## Public exports (runtime-contract.js)
|
|
133
|
-
-
|
|
131
|
+
- 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
132
|
- `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
133
|
- `formatRuntimeContractBlockedError(contract)` / `runtimeContractExecutionBlockedBody(contract)`: user-facing upgrade guidance on HTTP 409 execution blocks (below-min / 1.x), not raw JSON dumps.
|
|
136
134
|
- `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 +153,7 @@ This module provides OpenCode server integration utilities for the web server ru
|
|
|
155
153
|
- `fetchV1MigrationGate({ url, headers, signal, fetchImpl })`: GET-only status poll used by lifecycle.
|
|
156
154
|
|
|
157
155
|
## 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.
|
|
156
|
+
- `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
157
|
- 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
158
|
- 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
159
|
- Returned API:
|
|
@@ -210,6 +208,7 @@ This module provides OpenCode server integration utilities for the web server ru
|
|
|
210
208
|
|
|
211
209
|
## Public exports (env-config.js)
|
|
212
210
|
- `resolveOpenCodeEnvConfig(options?)`: resolves and validates OpenCode host/port/hostname environment configuration.
|
|
211
|
+
- `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.
|
|
213
212
|
- Returned object fields:
|
|
214
213
|
- `configuredOpenCodePort`
|
|
215
214
|
- `configuredOpenCodeHost`
|
|
@@ -333,7 +332,6 @@ Failures of either required list still return HTTP 502 for the caller to retry.
|
|
|
333
332
|
- 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`
|
|
334
333
|
- 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
|
|
335
334
|
- MCP servers: `/api/config/mcp` and `/api/config/mcp/:name`
|
|
336
|
-
- Snippets: `/api/config/snippets`, `/api/config/snippets/:name`, and `/api/config/snippets/expand`
|
|
337
335
|
|
|
338
336
|
### Catalog / OpenChamber-owned API change checklist
|
|
339
337
|
|
|
@@ -346,6 +344,7 @@ When adding or changing Host HTTP APIs that mobile/desktop clients reach over Pr
|
|
|
346
344
|
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.
|
|
347
345
|
|
|
348
346
|
## Public exports (auth-state-runtime.js)
|
|
347
|
+
- 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.
|
|
349
348
|
- `createOpenCodeAuthStateRuntime(dependencies)`: creates runtime for managed OpenCode auth password state and request headers.
|
|
350
349
|
- Returned API:
|
|
351
350
|
- `getOpenCodeAuthHeaders()`
|
|
@@ -513,16 +512,21 @@ The V2 skill catalog uses `id` as the existing OpenChamber `name` invocation key
|
|
|
513
512
|
reuse the existing UI-password / CORS path, register before the generic proxy,
|
|
514
513
|
and return 501 when the service is disabled. Logs never include message
|
|
515
514
|
bodies, parts, or tokens.
|
|
516
|
-
- Provider auth:
|
|
517
|
-
-
|
|
518
|
-
-
|
|
515
|
+
- Provider auth: running OpenCode v2 credential API (no direct access to legacy auth files or OpenCode database).
|
|
516
|
+
- `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`.
|
|
517
|
+
- 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.
|
|
518
|
+
- 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`.
|
|
519
519
|
- Custom config: `OPENCODE_CONFIG` env var path.
|
|
520
|
+
- 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.
|
|
521
|
+
- 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.
|
|
522
|
+
- 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.
|
|
523
|
+
- 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.
|
|
520
524
|
- Rate limit config: `OPENCHAMBER_RATE_LIMIT_MAX_ATTEMPTS`, `OPENCHAMBER_RATE_LIMIT_NO_IP_MAX_ATTEMPTS` env vars.
|
|
521
525
|
|
|
522
526
|
## Notes for contributors
|
|
523
527
|
- This module serves as foundation for OpenCode-related server utilities.
|
|
524
528
|
- Route ownership moved to module-level `routes.js`; `index.js` wires dependencies only.
|
|
525
529
|
- All file writes include automatic backup before modification.
|
|
526
|
-
- Config merging follows priority:
|
|
530
|
+
- Config merging follows priority: project `.opencode` > project direct > custom > global JSONC > global JSON.
|
|
527
531
|
- UI auth uses scrypt for password hashing with constant-time comparison.
|
|
528
532
|
- 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
|
|
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
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
760
|
-
|
|
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
|
|
769
|
-
const
|
|
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() ||
|
|
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
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import os from 'os';
|
|
1
|
+
import { makeOpenCodeV2Client } from './v2-client.js';
|
|
4
2
|
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
12
|
+
export function openCodeCredentialSource({ buildOpenCodeUrl, getOpenCodeAuthHeaders }) {
|
|
13
|
+
return () => makeOpenCodeV2Client({
|
|
14
|
+
baseUrl: buildOpenCodeUrl('', '').replace(/\/+$/, ''),
|
|
15
|
+
authHeaders: getOpenCodeAuthHeaders(),
|
|
16
|
+
});
|
|
17
|
+
}
|
|
30
18
|
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
return true;
|
|
52
|
+
function currentClient() {
|
|
53
|
+
if (!getClient) throw credentialError();
|
|
54
|
+
try { return getClient(); } catch { throw credentialError(); }
|
|
61
55
|
}
|
|
62
56
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
|
69
|
-
|
|
70
|
-
return Object.keys(auth);
|
|
74
|
+
export async function getProviderAuth(providerId) {
|
|
75
|
+
return (await readOpenCodeCredentials())[providerId] || null;
|
|
71
76
|
}
|
|
72
77
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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 =
|
|
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: {
|