@vantran-se/openchamber-web 1.24.4 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -8
- package/bin/lib/commands-connect-url.js +3 -8
- package/bin/lib/commands-tunnel.js +3 -0
- package/dist/assets/{CodeMirrorCommentWidgets-B4a0JkUz.js → CodeMirrorCommentWidgets-ntXIbhV2.js} +1 -1
- package/dist/assets/DiffView-CjxUovbM.js +4 -0
- package/dist/assets/FilesView-DFWD0RQR.js +17 -0
- package/dist/assets/{GitView-Byu2DY7s.js → GitView-BWa7e5vw.js} +4 -4
- package/dist/assets/{GuestAttachDialog-B808E0-d.js → GuestAttachDialog-DdOuyZzp.js} +1 -1
- package/dist/assets/GuestSurfacePane-DDfzid3U.js +1 -0
- package/dist/assets/{JsonTreeView-VW_7czo3.js → JsonTreeView-3sGwqexh.js} +1 -1
- package/dist/assets/LinearIssuesView-C6dpIdjw.js +1 -0
- package/dist/assets/MarkdownImageGallery-Q7YR_tl4.js +1 -0
- package/dist/assets/MarkdownRendererImpl-DxDV56bL.js +12 -0
- package/dist/assets/OnboardingScreen-rK4Stg__.js +1 -0
- package/dist/assets/{PatchDiff-DCjGhlYV.js → PatchDiff-DS-IMU1j.js} +1 -1
- package/dist/assets/{PierreDiffViewer-CEDZNkU2.js → PierreDiffViewer-DW23KWKZ.js} +1 -1
- package/dist/assets/PlanView-BCk_VobP.js +5 -0
- package/dist/assets/PluginPane-BPP1dbck.js +3 -0
- package/dist/assets/SettingsView-kMmrpn43.js +29 -0
- package/dist/assets/SettingsWindow-BpvD6IYQ.js +1 -0
- package/dist/assets/SubmoduleDiffSummary-BLttSS1X.js +1 -0
- package/dist/assets/ToolOutputDialog-CtqDqbyr.js +15 -0
- package/dist/assets/ToolPartDiffPreview-5FcKdyRy.js +8 -0
- package/dist/assets/{VirtualizedFileDiff-NU0FHFhT.js → VirtualizedFileDiff-C763hNCR.js} +4 -4
- package/dist/assets/{WalkthroughView-BZyjhqHv.js → WalkthroughView-lap5btCF.js} +2 -2
- package/dist/assets/appThemeRegistry-u4zw1l0B.js +1 -0
- package/dist/assets/{commitAndPush-BDftrjr6.js → commitAndPush-CwGJfJG-.js} +1 -1
- package/dist/assets/{computeVirtualFileMetrics-DlWlEM3x.js → computeVirtualFileMetrics-DW5jjX4n.js} +1 -1
- package/dist/assets/de-CndEg67T.js +10 -0
- package/dist/assets/es-DglvPlaC.js +16 -0
- package/dist/assets/fr-DejPCpbS.js +14 -0
- package/dist/assets/index-C2r64QfD.js +1 -0
- package/dist/assets/index-CLcM8HKx.css +1 -0
- package/dist/assets/index-CvLgnuNI.js +41 -0
- package/dist/assets/{isDiffPlainText-CO4e5J_S.js → isDiffPlainText-D47a6hMP.js} +1 -1
- package/dist/assets/{isFilePlainText-ue6kCou7.js → isFilePlainText-B-mGgeKl.js} +1 -1
- package/dist/assets/ja-CVpQawCd.js +16 -0
- package/dist/assets/ko-Bw446h9j.js +16 -0
- package/dist/assets/main-Bz4WWXbr.js +2 -0
- package/dist/assets/main-oreqDsOV.js +896 -0
- package/dist/assets/{markdownCore-CK48LG0z.js → markdownCore-D2e5sp9y.js} +4 -4
- package/dist/assets/miniChat-D1v2gsBI.js +2 -0
- package/dist/assets/mobile-DBj3lCf2.js +2 -0
- package/dist/assets/{patchFileDiff-D7QVlovD.js → patchFileDiff-BpeUOV91.js} +3 -3
- package/dist/assets/pl-BYNlPdhm.js +16 -0
- package/dist/assets/pt-BR-CL-Yfv9H.js +16 -0
- package/dist/assets/renderElectronMiniChatApp-DVtfEB2z.js +2 -0
- package/dist/assets/renderMobileApp-DFp9-xbG.js +25 -0
- package/dist/assets/runtimeEndpointReset-CsIpUep1.js +27 -0
- package/dist/assets/scheduledTasksApi-B5OHVpPM.js +1 -0
- package/dist/assets/{shikiHighlight-BPu3JiQs.js → shikiHighlight-DNm2Z_Jc.js} +1 -1
- package/dist/assets/{textMateThemeFromAppTheme-DWqRPo9A.js → textMateThemeFromAppTheme-DGF2kG7g.js} +1 -1
- package/dist/assets/tr-DzmbxLy-.js +7 -0
- package/dist/assets/uk-C7vQnwro.js +16 -0
- package/dist/assets/useAppFontEffects-Ci3FEaPf.js +7337 -0
- package/dist/assets/{useInlineCommentController-CgS0fnw_.js → useInlineCommentController-CuUuakrt.js} +2 -2
- package/dist/assets/useRootScrollLock-CQIsyERq.js +2 -0
- package/dist/assets/{vendor-codemirror-language-data-Bx2EHc1O.js → vendor-codemirror-language-data-CQa7_1cr.js} +2 -2
- package/dist/assets/vendor-cron-parser-CjvYVigk.js +2 -0
- package/dist/assets/vendor-opencode-client-_BAHQo4R.js +12 -0
- package/dist/assets/{vendor-remixicon-react-BDmGmFCA.js → vendor-remixicon-react-D6dZ0UwF.js} +1 -1
- package/dist/assets/{vendor-zod-CgAdXWZO.js → vendor-zod-Gea9uBlY.js} +1 -1
- package/dist/assets/{web-components-DxX8bZ1o.js → web-components-B6tKQyJN.js} +2 -2
- package/dist/assets/zh-CN-Cl0srS-z.js +16 -0
- package/dist/assets/zh-TW-DtII5bDb.js +16 -0
- package/dist/index.html +13 -7
- package/dist/mini-chat.html +5 -5
- package/dist/mobile.html +5 -5
- package/package.json +6 -5
- package/server/index.d.ts +1 -0
- package/server/index.js +246 -91
- package/server/lib/agent-tool/DOCUMENTATION.md +32 -15
- package/server/lib/agent-tool/runtime.js +135 -54
- package/server/lib/agent-tool/runtime.test.js +155 -87
- package/server/lib/browser-control/DOCUMENTATION.md +37 -2
- package/server/lib/browser-control/provider.js +217 -0
- package/server/lib/browser-control/provider.test.js +251 -0
- package/server/lib/context-obligatory/runtime.js +61 -50
- package/server/lib/context-obligatory/runtime.test.js +62 -162
- package/server/lib/event-stream/DOCUMENTATION.md +12 -1
- package/server/lib/event-stream/delta-coalescer.js +0 -0
- package/server/lib/event-stream/delta-coalescer.test.js +68 -57
- package/server/lib/event-stream/directory-ws-bridge.js +1 -1
- package/server/lib/event-stream/global-hub.js +10 -1
- package/server/lib/event-stream/global-hub.test.js +27 -26
- package/server/lib/event-stream/index.js +6 -0
- package/server/lib/event-stream/protocol.js +26 -19
- package/server/lib/event-stream/protocol.test.js +18 -7
- package/server/lib/event-stream/rebind.test.js +5 -5
- package/server/lib/event-stream/runtime.test.js +17 -15
- package/server/lib/event-stream/translate-v2.js +313 -0
- package/server/lib/event-stream/translate-v2.test.js +209 -0
- package/server/lib/event-stream/upstream-reader.test.js +16 -16
- package/server/lib/fs/DOCUMENTATION.md +4 -2
- package/server/lib/fs/byte-range.js +35 -0
- package/server/lib/fs/byte-range.test.js +31 -0
- package/server/lib/fs/routes.js +47 -42
- package/server/lib/fs/routes.test.js +123 -18
- package/server/lib/git/service.js +12 -0
- package/server/lib/github/DOCUMENTATION.md +1 -1
- package/server/lib/github/pr-status.js +22 -2
- package/server/lib/github/pr-status.test.js +27 -14
- package/server/lib/guests/DOCUMENTATION.md +13 -0
- package/server/lib/guests/catalog.js +12 -0
- package/server/lib/guests/catalog.test.js +33 -0
- package/server/lib/guests/routes.js +10 -1
- package/server/lib/guests/service.js +192 -14
- package/server/lib/guests/service.test.js +201 -1
- package/server/lib/guests/surface.js +630 -0
- package/server/lib/guests/surface.test.js +486 -0
- package/server/lib/markdown-image-grants/routes.js +18 -5
- package/server/lib/markdown-image-grants/routes.test.js +9 -6
- package/server/lib/message-queue/DOCUMENTATION.md +33 -22
- package/server/lib/message-queue/runtime.js +155 -100
- package/server/lib/message-queue/runtime.test.js +168 -82
- package/server/lib/notifications/DOCUMENTATION.md +28 -0
- package/server/lib/notifications/control-stream.http.test.js +68 -0
- package/server/lib/notifications/emitter-runtime.js +15 -0
- package/server/lib/notifications/emitter-runtime.test.js +32 -0
- package/server/lib/notifications/push-runtime.js +5 -1
- package/server/lib/notifications/push-runtime.test.js +2 -2
- package/server/lib/notifications/routes.js +1 -1
- package/server/lib/notifications/runtime.js +115 -26
- package/server/lib/notifications/template-runtime.js +13 -15
- package/server/lib/notifications/template-runtime.test.js +16 -9
- package/server/lib/notifications/trigger-runtime.test.js +187 -0
- package/server/lib/openchamber-control/DOCUMENTATION.md +10 -0
- package/server/lib/openchamber-control/actions.js +3 -2
- package/server/lib/openchamber-control/error.js +1 -1
- package/server/lib/openchamber-control/file-open.js +59 -0
- package/server/lib/openchamber-control/file-open.test.js +57 -0
- package/server/lib/openchamber-control/routes.test.js +12 -0
- package/server/lib/openchamber-control/service.js +102 -49
- package/server/lib/openchamber-control/service.test.js +84 -44
- package/server/lib/openchamber-sessions/archive-store.js +224 -0
- package/server/lib/openchamber-sessions/archive-store.test.js +187 -0
- package/server/lib/openchamber-sessions/opencode-client.js +22 -0
- package/server/lib/openchamber-sessions/routes.js +398 -328
- package/server/lib/openchamber-sessions/routes.test.js +777 -740
- package/server/lib/openchamber-sessions/session-metadata-store.js +285 -0
- package/server/lib/openchamber-sessions/session-metadata-store.test.js +259 -0
- package/server/lib/opencode/DOCUMENTATION.md +449 -29
- package/server/lib/opencode/agents.js +233 -327
- package/server/lib/opencode/auth.js +41 -44
- package/server/lib/opencode/auth.test.js +92 -0
- package/server/lib/opencode/cli-upgrade.d.ts +4 -0
- package/server/lib/opencode/cli-upgrade.js +18 -0
- package/server/lib/opencode/cli-upgrade.test.js +36 -0
- package/server/lib/opencode/commands.js +164 -145
- package/server/lib/opencode/compatibility.d.ts +16 -0
- package/server/lib/opencode/compatibility.js +88 -0
- package/server/lib/opencode/compatibility.test.js +62 -0
- package/server/lib/opencode/config-entity-routes.js +58 -29
- package/server/lib/opencode/config-entity-v2.test.js +290 -0
- package/server/lib/opencode/config-mutation-response.js +17 -7
- package/server/lib/opencode/config-mutation-response.test.js +4 -7
- package/server/lib/opencode/config-paths.test.js +2 -2
- package/server/lib/opencode/config-v2.d.ts +183 -0
- package/server/lib/opencode/config-v2.js +801 -0
- package/server/lib/opencode/config-v2.test.js +466 -0
- package/server/lib/opencode/credential-db.js +144 -0
- package/server/lib/opencode/credential-db.test.js +104 -0
- package/server/lib/opencode/env-runtime.js +48 -9
- package/server/lib/opencode/env-runtime.test.js +45 -0
- package/server/lib/opencode/feature-routes-runtime.js +15 -5
- package/server/lib/opencode/lifecycle-process.test.js +7 -4
- package/server/lib/opencode/lifecycle.js +85 -30
- package/server/lib/opencode/lifecycle.test.js +80 -8
- package/server/lib/opencode/login-shell-env.js +27 -0
- package/server/lib/opencode/managed-config-file.js +119 -0
- package/server/lib/opencode/managed-config-file.test.js +152 -0
- package/server/lib/opencode/managed-plugin-config.js +33 -13
- package/server/lib/opencode/mcp.js +43 -149
- package/server/lib/opencode/network-runtime.js +6 -7
- package/server/lib/opencode/openchamber-routes.test.js +2 -5
- package/server/lib/opencode/plugin-routes.js +3 -3
- package/server/lib/opencode/plugin-routes.test.js +6 -24
- package/server/lib/opencode/plugins.js +91 -40
- package/server/lib/opencode/plugins.test.js +28 -15
- package/server/lib/opencode/providers.js +63 -83
- package/server/lib/opencode/providers.test.js +128 -59
- package/server/lib/opencode/proxy-agent-wiring.test.js +4 -2
- package/server/lib/opencode/proxy.js +179 -129
- package/server/lib/opencode/pwa-manifest-routes.js +4 -2
- package/server/lib/opencode/pwa-manifest-routes.test.js +3 -2
- package/server/lib/opencode/response-envelope.js +14 -0
- package/server/lib/opencode/response-envelope.test.js +23 -0
- package/server/lib/opencode/routes-upgrade.test.js +73 -116
- package/server/lib/opencode/routes.js +121 -428
- package/server/lib/opencode/server-utils-runtime.js +10 -4
- package/server/lib/opencode/session-activity.js +78 -0
- package/server/lib/opencode/session-runtime.js +21 -10
- package/server/lib/opencode/session-runtime.test.js +9 -9
- package/server/lib/opencode/settings-helpers.js +9 -9
- package/server/lib/opencode/settings-helpers.test.js +7 -12
- package/server/lib/opencode/settings-registry.json +6 -8
- package/server/lib/opencode/settings-runtime.js +18 -1
- package/server/lib/opencode/settings-runtime.test.js +31 -2
- package/server/lib/opencode/shared.js +54 -22
- package/server/lib/opencode/shared.test.js +8 -4
- package/server/lib/opencode/shutdown-runtime.js +54 -7
- package/server/lib/opencode/shutdown-runtime.test.js +224 -30
- package/server/lib/opencode/skill-routes.js +23 -24
- package/server/lib/opencode/skills.js +7 -5
- package/server/lib/opencode/tunnel-wiring-runtime.js +2 -1
- package/server/lib/opencode/upgrade-capability.test.js +1 -1
- package/server/lib/opencode/v1-migration-topup.js +288 -0
- package/server/lib/opencode/v1-migration-topup.test.js +266 -0
- package/server/lib/opencode/v2-install.d.ts +8 -0
- package/server/lib/opencode/v2-install.js +158 -0
- package/server/lib/opencode/v2-install.test.js +124 -0
- package/server/lib/opencode/watcher.js +10 -3
- package/server/lib/opencode/watcher.test.js +17 -21
- package/server/lib/opencode/websearch-config.js +66 -0
- package/server/lib/package-manager.js +55 -68
- package/server/lib/package-manager.test.js +241 -75
- package/server/lib/permission-auto-accept/runtime.js +38 -22
- package/server/lib/permission-auto-accept/runtime.test.js +39 -17
- package/server/lib/quota/DOCUMENTATION.md +4 -0
- package/server/lib/quota/providers/xai.js +4 -4
- package/server/lib/relay/tunnel-host.js +4 -1
- package/server/lib/relay/tunnel-host.test.js +7 -0
- package/server/lib/routing/DOCUMENTATION.md +52 -21
- package/server/lib/routing/defaults.js +23 -6
- package/server/lib/routing/jev.js +17 -7
- package/server/lib/routing/routes.http.test.js +68 -33
- package/server/lib/routing/routes.js +65 -22
- package/server/lib/routing/runtime.js +145 -48
- package/server/lib/routing/runtime.test.js +79 -55
- package/server/lib/scheduled-tasks/DOCUMENTATION.md +4 -0
- package/server/lib/scheduled-tasks/issue-2710-double-execution.test.js +51 -46
- package/server/lib/scheduled-tasks/runtime.js +81 -74
- package/server/lib/scheduled-tasks/runtime.test.js +53 -1
- package/server/lib/session-assist/DOCUMENTATION.md +8 -3
- package/server/lib/session-assist/context.js +122 -46
- package/server/lib/session-assist/context.test.js +117 -49
- package/server/lib/session-assist/runtime.js +52 -22
- package/server/lib/session-assist/runtime.test.js +145 -173
- package/server/lib/session-goal/DOCUMENTATION.md +23 -15
- package/server/lib/session-goal/create.js +9 -12
- package/server/lib/session-goal/create.test.js +27 -28
- package/server/lib/session-goal/runtime.js +164 -77
- package/server/lib/session-goal/runtime.test.js +271 -519
- package/server/lib/session-knowledge/DOCUMENTATION.md +5 -0
- package/server/lib/session-knowledge/runtime.js +59 -41
- package/server/lib/session-knowledge/runtime.test.js +90 -20
- package/server/lib/small-model/DOCUMENTATION.md +154 -208
- package/server/lib/small-model/client.js +113 -0
- package/server/lib/small-model/index.js +296 -158
- package/server/lib/small-model/index.test.js +423 -256
- package/server/lib/small-model/routes.js +1 -1
- package/server/lib/spaces/DOCUMENTATION.md +807 -0
- package/server/lib/spaces/code-in-bait.js +185 -0
- package/server/lib/spaces/code-in.js +641 -0
- package/server/lib/spaces/code-in.test.js +1048 -0
- package/server/lib/spaces/errors.js +8 -0
- package/server/lib/spaces/exec-http.js +94 -0
- package/server/lib/spaces/gatekeeper-channel.js +202 -0
- package/server/lib/spaces/gatekeeper-channel.test.js +215 -0
- package/server/lib/spaces/gatekeeper-program.cjs +914 -0
- package/server/lib/spaces/gatekeeper-program.test.js +1391 -0
- package/server/lib/spaces/hardening.js +457 -0
- package/server/lib/spaces/hardening.test.js +368 -0
- package/server/lib/spaces/host-git.js +127 -0
- package/server/lib/spaces/host-git.test.js +137 -0
- package/server/lib/spaces/labels.js +234 -0
- package/server/lib/spaces/labels.test.js +219 -0
- package/server/lib/spaces/layout.js +184 -0
- package/server/lib/spaces/layout.test.js +30 -0
- package/server/lib/spaces/manager.js +82 -0
- package/server/lib/spaces/manager.test.js +114 -0
- package/server/lib/spaces/places/code-in.docker.live.test.js +216 -0
- package/server/lib/spaces/places/contract-suite.js +156 -0
- package/server/lib/spaces/places/contract.docker.live.test.js +7 -0
- package/server/lib/spaces/places/contract.test.js +6 -0
- package/server/lib/spaces/places/docker-engine.js +126 -0
- package/server/lib/spaces/places/docker-live-support.js +202 -0
- package/server/lib/spaces/places/docker-tools.js +186 -0
- package/server/lib/spaces/places/docker.js +587 -0
- package/server/lib/spaces/places/docker.test.js +1610 -0
- package/server/lib/spaces/places/escape-suite.js +1018 -0
- package/server/lib/spaces/places/escape.docker.live.test.js +7 -0
- package/server/lib/spaces/places/fake-docker.js +375 -0
- package/server/lib/spaces/places/memory-place.js +103 -0
- package/server/lib/spaces/places/packed.docker.live.test.js +77 -0
- package/server/lib/spaces/places/registry.js +43 -0
- package/server/lib/spaces/places/registry.test.js +50 -0
- package/server/lib/spaces/places/server.docker.live.test.js +225 -0
- package/server/lib/spaces/run-command.js +261 -0
- package/server/lib/spaces/run-command.test.js +396 -0
- package/server/lib/spaces/space-server.js +190 -0
- package/server/lib/spaces/space-server.test.js +335 -0
- package/server/lib/spaces/tools-filler.js +82 -0
- package/server/lib/spaces/tools-filler.test.js +148 -0
- package/server/lib/spaces/tools-pack.js +49 -0
- package/server/lib/spaces/tools-pack.test.js +57 -0
- package/server/lib/spaces/tools.js +138 -0
- package/server/lib/spaces/tools.test.js +153 -0
- package/server/lib/tts/openai-sdk.js +11 -0
- package/server/lib/tts/service.js +7 -5
- package/server/lib/tts/stt.js +3 -2
- package/server/lib/tunnels/DOCUMENTATION.md +2 -0
- package/server/lib/tunnels/routes.js +7 -0
- package/server/lib/tunnels/routes.test.js +55 -0
- package/server/lib/ui-auth/ui-auth.js +38 -2
- package/server/lib/ui-auth/ui-auth.test.js +42 -0
- package/server/lib/ui-auth/ui-passkeys.js +12 -10
- package/server/lib/walkthrough/DOCUMENTATION.md +10 -1
- package/server/lib/walkthrough/pull-request.js +61 -8
- package/server/lib/walkthrough/pull-request.test.js +54 -1
- package/server/lib/walkthrough/routes.js +20 -0
- package/server/lib/walkthrough/routes.test.js +19 -0
- package/server/opencode-proxy.test.js +190 -166
- package/server/opencode-worktree-readiness.test.js +5 -4
- package/dist/assets/DiffView-B2CD9wC8.js +0 -4
- package/dist/assets/FilesView-C4vMFclc.js +0 -13
- package/dist/assets/LinearIssuesView-EzGNCGrd.js +0 -1
- package/dist/assets/MarkdownImageGallery-B2YUDFaB.js +0 -1
- package/dist/assets/MarkdownRendererImpl-B4ikIzR3.js +0 -12
- package/dist/assets/OnboardingScreen-CQHBSTEi.js +0 -1
- package/dist/assets/PlanView-D3x-pTkZ.js +0 -5
- package/dist/assets/PluginPane-DEcgDQ9h.js +0 -3
- package/dist/assets/SettingsView-CSinuH-1.js +0 -29
- package/dist/assets/SettingsWindow-B5CHoJFZ.js +0 -1
- package/dist/assets/SubmoduleDiffSummary-oxeACCWs.js +0 -1
- package/dist/assets/ToolOutputDialog-tLNWmfcW.js +0 -15
- package/dist/assets/ToolPartDiffPreview-CflzmK-x.js +0 -8
- package/dist/assets/appThemeRegistry-DLicw4dH.js +0 -1
- package/dist/assets/de-Bro6_zyl.js +0 -10
- package/dist/assets/es-Bb9XQZvd.js +0 -16
- package/dist/assets/fr-Dgfzn-IL.js +0 -14
- package/dist/assets/index-CtQNmqUL.js +0 -1
- package/dist/assets/index-DqaMWNrC.css +0 -1
- package/dist/assets/index-Dqc6USl3.js +0 -40
- package/dist/assets/ja-BYsakGFh.js +0 -16
- package/dist/assets/ko-BSaQz9aN.js +0 -16
- package/dist/assets/main-B62_pjL5.js +0 -2
- package/dist/assets/main-BEdcOI2P.js +0 -896
- package/dist/assets/miniChat-LT8Rqofr.js +0 -2
- package/dist/assets/mobile-CqarEynh.js +0 -2
- package/dist/assets/pl-OIviPdoj.js +0 -16
- package/dist/assets/pt-BR-Cltg-E6s.js +0 -16
- package/dist/assets/renderElectronMiniChatApp-COyLTtZx.js +0 -2
- package/dist/assets/renderMobileApp-BC1MHg6g.js +0 -25
- package/dist/assets/runtimeEndpointReset-DdrazOVH.js +0 -25
- package/dist/assets/scheduledTasksApi-B2SWMGEU.js +0 -1
- package/dist/assets/tr-CQI-KPre.js +0 -7
- package/dist/assets/uk-CqW7R4DA.js +0 -16
- package/dist/assets/useAppFontEffects-ne84JBVL.js +0 -7333
- package/dist/assets/useRootScrollLock-Cpf1qoGp.js +0 -2
- package/dist/assets/vendor-cron-parser-CUcP7OfX.js +0 -2
- package/dist/assets/vendor-opencode-sdk-BJ73u-bH.js +0 -7
- package/dist/assets/zh-CN-CVZG_o_J.js +0 -16
- package/dist/assets/zh-TW-D5ZxzCZp.js +0 -16
- package/server/lib/opencode/mcp-oauth-callback.test.js +0 -111
- package/server/lib/routing/feature-flag.js +0 -11
- package/server/lib/small-model/call.js +0 -851
- package/server/lib/small-model/call.test.js +0 -1292
- package/server/lib/small-model/catalog.js +0 -13
- package/server/lib/small-model/resolve.js +0 -131
- package/server/lib/small-model/resolve.test.js +0 -197
- package/server/lib/small-model/runtime-providers.js +0 -164
- package/server/lib/small-model/runtime-providers.test.js +0 -118
- package/server/lib/system-prompt/DOCUMENTATION.md +0 -36
- package/server/lib/system-prompt/runtime.js +0 -49
- package/server/lib/system-prompt/runtime.test.js +0 -76
- package/server/lib/walkthrough/reproduce-2607.test.js +0 -148
|
@@ -195,6 +195,7 @@ export const registerGuestRoutes = (app, {
|
|
|
195
195
|
resolveGitBinaryForSpawn,
|
|
196
196
|
resolveOptionalProjectDirectory,
|
|
197
197
|
getSmallModelService,
|
|
198
|
+
onGuestDeactivated = async () => false,
|
|
198
199
|
}) => {
|
|
199
200
|
const persistPath = extensionsPersistPath(openchamberDataDir);
|
|
200
201
|
const authPath = guestAuthPersistPath(openchamberDataDir);
|
|
@@ -309,6 +310,7 @@ export const registerGuestRoutes = (app, {
|
|
|
309
310
|
if (!isGuestPanelId(id)) {
|
|
310
311
|
return res.status(404).json({ error: 'not-found' });
|
|
311
312
|
}
|
|
313
|
+
const removed = await loadGuest(id);
|
|
312
314
|
const result = await uninstallGuest(id, persistPath);
|
|
313
315
|
if (!result.ok) {
|
|
314
316
|
const status = result.code === 'bundled' ? 400 : 404;
|
|
@@ -316,6 +318,9 @@ export const registerGuestRoutes = (app, {
|
|
|
316
318
|
}
|
|
317
319
|
// Remove means forget: tokens, client secret, and settings go with the package.
|
|
318
320
|
await forgetGuestAuth(id, authPath);
|
|
321
|
+
// A role this package stood in for (the agent's browser) goes back to
|
|
322
|
+
// the host's own, and the user is told rather than finding out mid-task.
|
|
323
|
+
await onGuestDeactivated({ guestId: id, guestName: removed?.name ?? id });
|
|
319
324
|
res.status(204).end();
|
|
320
325
|
} catch (error) {
|
|
321
326
|
console.error('Failed to uninstall guest:', error);
|
|
@@ -561,7 +566,7 @@ export const registerGuestRoutes = (app, {
|
|
|
561
566
|
res.json(result);
|
|
562
567
|
} catch (error) {
|
|
563
568
|
if (error instanceof GuestServiceError) {
|
|
564
|
-
const status = error.code === 'SERVICE_FAILED' ? 502 : 400;
|
|
569
|
+
const status = error.code === 'SERVICE_FAILED' || error.code === 'REQUEST_FAILED' ? 502 : 400;
|
|
565
570
|
return res.status(status).json({ error: error.code, message: error.message });
|
|
566
571
|
}
|
|
567
572
|
console.error('Failed to proxy guest service request:', error);
|
|
@@ -716,6 +721,7 @@ export const registerGuestRoutes = (app, {
|
|
|
716
721
|
await setCapabilityGrants(guest.id, persistPath, granted, granted.length > 0 ? scope : null);
|
|
717
722
|
if (granted.length === 0) {
|
|
718
723
|
await stopGuestService(guest.id);
|
|
724
|
+
await onGuestDeactivated({ guestId: guest.id, guestName: guest.name });
|
|
719
725
|
}
|
|
720
726
|
// Credentials were stored for one API origin and one pair of OAuth
|
|
721
727
|
// endpoints. When a newer version points the integration somewhere
|
|
@@ -745,6 +751,9 @@ export const registerGuestRoutes = (app, {
|
|
|
745
751
|
return res.status(400).json({ error: 'invalid-request' });
|
|
746
752
|
}
|
|
747
753
|
await setGuestEnabled(guest.id, persistPath, parsed.data.enabled);
|
|
754
|
+
if (!parsed.data.enabled) {
|
|
755
|
+
await onGuestDeactivated({ guestId: guest.id, guestName: guest.name });
|
|
756
|
+
}
|
|
748
757
|
const next = await loadGuest(guest.id);
|
|
749
758
|
if (!next) {
|
|
750
759
|
return res.status(404).json({ error: 'not-found' });
|
|
@@ -37,10 +37,39 @@ const runtimes = new Map();
|
|
|
37
37
|
/** @type {Map<string, Promise<ServiceRuntime>>} */
|
|
38
38
|
const startingByGuest = new Map();
|
|
39
39
|
|
|
40
|
+
/** @type {Map<string, Promise<void>>} */
|
|
41
|
+
const stoppingByGuest = new Map();
|
|
42
|
+
|
|
43
|
+
// Requests retain their lifecycle object across awaits. A later host start
|
|
44
|
+
// cannot reopen admission for work that belonged to the previous host.
|
|
45
|
+
let hostLifecycle = { accepting: true };
|
|
46
|
+
|
|
47
|
+
export const beginGuestServiceHost = () => {
|
|
48
|
+
if (runtimes.size || startingByGuest.size || stoppingByGuest.size) {
|
|
49
|
+
throw new Error('Guest services must finish stopping before a new host starts.');
|
|
50
|
+
}
|
|
51
|
+
hostLifecycle.accepting = false;
|
|
52
|
+
hostLifecycle = { accepting: true };
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export const beginGuestServiceShutdown = () => {
|
|
56
|
+
hostLifecycle.accepting = false;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const assertGuestHostActive = (lifecycle) => {
|
|
60
|
+
if (!lifecycle.accepting) {
|
|
61
|
+
throw new GuestServiceError('The host is shutting down.', 'NO_SERVICE');
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
40
65
|
export class GuestServiceError extends Error {
|
|
41
66
|
/**
|
|
42
67
|
* @param {string} message
|
|
43
|
-
* @param {'NO_SERVICE' | 'SERVICE_FAILED' | 'BAD_PATH' | 'BAD_METHOD'} code
|
|
68
|
+
* @param {'NO_SERVICE' | 'SERVICE_FAILED' | 'REQUEST_FAILED' | 'BAD_PATH' | 'BAD_METHOD' | 'DISABLED' | 'CANCELLED'} code
|
|
69
|
+
* `SERVICE_FAILED` is a service that never became ready: nothing was sent
|
|
70
|
+
* to it. `REQUEST_FAILED` is a request that was sent and got no usable
|
|
71
|
+
* answer (timeout, dropped connection, unreadable body): the service may
|
|
72
|
+
* have acted on it.
|
|
44
73
|
*/
|
|
45
74
|
constructor(message, code) {
|
|
46
75
|
super(message);
|
|
@@ -175,6 +204,72 @@ const stopEpochs = new Map();
|
|
|
175
204
|
|
|
176
205
|
const stopEpochOf = (guestId) => stopEpochs.get(guestId) ?? 0;
|
|
177
206
|
|
|
207
|
+
/**
|
|
208
|
+
* Services the host starts on its own (a provider role) have no panel whose
|
|
209
|
+
* closing would end them, so they end themselves: a request arms a timer, the
|
|
210
|
+
* next request re-arms it, and when it fires with nothing in flight the
|
|
211
|
+
* process goes away. The next request starts it again. A panel-driven service
|
|
212
|
+
* passes no `idleStopMs` and keeps today's lifetime.
|
|
213
|
+
* @type {Map<string, { timer: ReturnType<typeof setTimeout>, inflight: number, idleMs: number }>}
|
|
214
|
+
*/
|
|
215
|
+
const idleStops = new Map();
|
|
216
|
+
|
|
217
|
+
const clearIdleStop = (guestId) => {
|
|
218
|
+
const entry = idleStops.get(guestId);
|
|
219
|
+
if (!entry) return;
|
|
220
|
+
clearTimeout(entry.timer);
|
|
221
|
+
idleStops.delete(guestId);
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
/** Long-lived consumers (a surface viewer) that keep an idle-stopping service up. */
|
|
225
|
+
const holds = new Map();
|
|
226
|
+
|
|
227
|
+
const armIdleStop = (guestId, idleMs) => {
|
|
228
|
+
const previous = idleStops.get(guestId);
|
|
229
|
+
const inflight = previous?.inflight ?? 0;
|
|
230
|
+
if (previous) clearTimeout(previous.timer);
|
|
231
|
+
const timer = setTimeout(() => {
|
|
232
|
+
const current = idleStops.get(guestId);
|
|
233
|
+
if (!current || current.inflight > 0 || (holds.get(guestId) ?? 0) > 0) {
|
|
234
|
+
// A request is running or a viewer is attached; whichever ends last
|
|
235
|
+
// re-arms the timer.
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
idleStops.delete(guestId);
|
|
239
|
+
void discardRuntime(guestId);
|
|
240
|
+
}, idleMs);
|
|
241
|
+
timer.unref?.();
|
|
242
|
+
idleStops.set(guestId, { timer, inflight, idleMs });
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Keeps an idle-stopping service alive while the returned release function
|
|
247
|
+
* has not been called. Releasing the last hold restarts the idle window.
|
|
248
|
+
* @param {string} guestId
|
|
249
|
+
*/
|
|
250
|
+
export const holdGuestService = (guestId) => {
|
|
251
|
+
holds.set(guestId, (holds.get(guestId) ?? 0) + 1);
|
|
252
|
+
let released = false;
|
|
253
|
+
return () => {
|
|
254
|
+
if (released) return;
|
|
255
|
+
released = true;
|
|
256
|
+
const remaining = (holds.get(guestId) ?? 1) - 1;
|
|
257
|
+
if (remaining > 0) {
|
|
258
|
+
holds.set(guestId, remaining);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
holds.delete(guestId);
|
|
262
|
+
const entry = idleStops.get(guestId);
|
|
263
|
+
if (entry && runtimes.get(guestId)) armIdleStop(guestId, entry.idleMs);
|
|
264
|
+
};
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
const trackInflight = (guestId, delta) => {
|
|
268
|
+
const entry = idleStops.get(guestId);
|
|
269
|
+
if (!entry) return;
|
|
270
|
+
entry.inflight = Math.max(0, entry.inflight + delta);
|
|
271
|
+
};
|
|
272
|
+
|
|
178
273
|
/**
|
|
179
274
|
* A user-facing stop (Pause, Remove, withdrawn approval, socket change,
|
|
180
275
|
* host quit): ends the process and cancels any request or start in flight.
|
|
@@ -182,6 +277,7 @@ const stopEpochOf = (guestId) => stopEpochs.get(guestId) ?? 0;
|
|
|
182
277
|
*/
|
|
183
278
|
export const stopGuestService = async (guestId) => {
|
|
184
279
|
stopEpochs.set(guestId, stopEpochOf(guestId) + 1);
|
|
280
|
+
clearIdleStop(guestId);
|
|
185
281
|
await discardRuntime(guestId);
|
|
186
282
|
};
|
|
187
283
|
|
|
@@ -192,6 +288,8 @@ export const stopGuestService = async (guestId) => {
|
|
|
192
288
|
* @param {string} guestId
|
|
193
289
|
*/
|
|
194
290
|
const discardRuntime = async (guestId) => {
|
|
291
|
+
const stopping = stoppingByGuest.get(guestId);
|
|
292
|
+
if (stopping) return stopping;
|
|
195
293
|
const runtime = runtimes.get(guestId);
|
|
196
294
|
if (!runtime) {
|
|
197
295
|
return;
|
|
@@ -202,7 +300,7 @@ const discardRuntime = async (guestId) => {
|
|
|
202
300
|
if (child.exitCode !== null || child.signalCode) {
|
|
203
301
|
return;
|
|
204
302
|
}
|
|
205
|
-
|
|
303
|
+
const stopped = new Promise((resolve) => {
|
|
206
304
|
const timer = setTimeout(() => {
|
|
207
305
|
child.kill('SIGKILL');
|
|
208
306
|
resolve(undefined);
|
|
@@ -213,14 +311,24 @@ const discardRuntime = async (guestId) => {
|
|
|
213
311
|
});
|
|
214
312
|
child.kill('SIGTERM');
|
|
215
313
|
});
|
|
314
|
+
stoppingByGuest.set(guestId, stopped);
|
|
315
|
+
try {
|
|
316
|
+
await stopped;
|
|
317
|
+
} finally {
|
|
318
|
+
stoppingByGuest.delete(guestId);
|
|
319
|
+
}
|
|
216
320
|
};
|
|
217
321
|
|
|
218
322
|
/** Test seam: the pid of a guest's running service, or `null`. */
|
|
219
323
|
export const readServicePid = (guestId) => runtimes.get(guestId)?.child.pid ?? null;
|
|
220
324
|
|
|
221
325
|
export const stopAllGuestServices = async () => {
|
|
222
|
-
const
|
|
223
|
-
|
|
326
|
+
const starts = [...startingByGuest.values()];
|
|
327
|
+
const ids = new Set([...runtimes.keys(), ...startingByGuest.keys(), ...stoppingByGuest.keys()]);
|
|
328
|
+
const stops = [...ids].map((id) => stopGuestService(id));
|
|
329
|
+
// Cancelled starts reject normally. Await their cleanup as well as children
|
|
330
|
+
// already registered when shutdown began.
|
|
331
|
+
await Promise.allSettled([...starts, ...stops]);
|
|
224
332
|
};
|
|
225
333
|
|
|
226
334
|
/**
|
|
@@ -266,7 +374,14 @@ const startGuestService = async ({
|
|
|
266
374
|
socketBindings = [],
|
|
267
375
|
socketOverrides = {},
|
|
268
376
|
epoch,
|
|
377
|
+
lifecycle,
|
|
269
378
|
}) => {
|
|
379
|
+
assertGuestHostActive(lifecycle);
|
|
380
|
+
// A replacement must not occupy the same guest slot while its previous
|
|
381
|
+
// child is still stopping; host shutdown must be able to drain both.
|
|
382
|
+
const previousStop = stoppingByGuest.get(guestId);
|
|
383
|
+
if (previousStop) await previousStop;
|
|
384
|
+
assertGuestHostActive(lifecycle);
|
|
270
385
|
const existing = runtimes.get(guestId);
|
|
271
386
|
if (existing?.status === 'ready' && existing.child.exitCode === null && !existing.child.signalCode) {
|
|
272
387
|
return existing;
|
|
@@ -276,7 +391,7 @@ const startGuestService = async ({
|
|
|
276
391
|
}
|
|
277
392
|
// The request's epoch, read before its first store access: a Pause that
|
|
278
393
|
// finished anywhere since then is a cancellation, spawn included.
|
|
279
|
-
const cancelled = () => stopEpochOf(guestId) !== epoch;
|
|
394
|
+
const cancelled = () => !lifecycle.accepting || stopEpochOf(guestId) !== epoch;
|
|
280
395
|
const stoppedError = () => new GuestServiceError('The service was stopped before it became ready.', 'NO_SERVICE');
|
|
281
396
|
|
|
282
397
|
const absoluteEntry = path.resolve(packageRoot, entry);
|
|
@@ -294,6 +409,7 @@ const startGuestService = async ({
|
|
|
294
409
|
throw stoppedError();
|
|
295
410
|
}
|
|
296
411
|
const port = await reserveLoopbackPort();
|
|
412
|
+
if (cancelled()) throw stoppedError();
|
|
297
413
|
const token = crypto.randomBytes(24).toString('hex');
|
|
298
414
|
const socketEnv = socketBindings.length > 0
|
|
299
415
|
? await resolveServiceSocketEnv(socketBindings, socketOverrides)
|
|
@@ -418,8 +534,15 @@ const ensureGuestService = async (params) => {
|
|
|
418
534
|
};
|
|
419
535
|
|
|
420
536
|
/**
|
|
537
|
+
* Starts the guest's service if needed and sends it one HTTP request, giving
|
|
538
|
+
* back the raw `Response`. This is the one place a host-driven caller (a
|
|
539
|
+
* provider role, a shared surface) or a panel proxy talks to the loopback:
|
|
540
|
+
* the enabled check, the grant, the spawn, the epoch, and the bearer live
|
|
541
|
+
* here. The caller reads the body as it needs it (text, JSON, image bytes).
|
|
542
|
+
*
|
|
421
543
|
* @param {{
|
|
422
544
|
* guestId: string,
|
|
545
|
+
* guestName?: string,
|
|
423
546
|
* packageRoot: string,
|
|
424
547
|
* service: { entry: string, permissions?: { sockets?: Array<{ id: string, candidatesByPlatform?: Partial<Record<'linux' | 'darwin' | 'win32', string[]>> }>, exec?: string[] } },
|
|
425
548
|
* granted: string[],
|
|
@@ -428,9 +551,16 @@ const ensureGuestService = async (params) => {
|
|
|
428
551
|
* path: string,
|
|
429
552
|
* query?: Record<string, string>,
|
|
430
553
|
* body?: string,
|
|
431
|
-
*
|
|
554
|
+
* accept?: string,
|
|
555
|
+
* timeoutMs?: number,
|
|
556
|
+
* idleStopMs?: number,
|
|
557
|
+
* signal?: AbortSignal,
|
|
558
|
+
* }} params `timeoutMs` and `idleStopMs` are for host-driven calls; a
|
|
559
|
+
* panel's `serviceRequest` keeps the SDK limits. With `idleStopMs` the
|
|
560
|
+
* service stops itself after that long without a request; `holdGuestService`
|
|
561
|
+
* suspends that while a long-lived consumer (a surface viewer) is attached.
|
|
432
562
|
*/
|
|
433
|
-
export const
|
|
563
|
+
export const openGuestServiceRequest = async ({
|
|
434
564
|
guestId,
|
|
435
565
|
guestName,
|
|
436
566
|
packageRoot,
|
|
@@ -441,7 +571,13 @@ export const proxyGuestServiceRequest = async ({
|
|
|
441
571
|
path: requestPath,
|
|
442
572
|
query,
|
|
443
573
|
body,
|
|
574
|
+
accept = 'application/json',
|
|
575
|
+
timeoutMs = GUEST_REQUEST_TIMEOUT_MS,
|
|
576
|
+
idleStopMs,
|
|
577
|
+
signal,
|
|
444
578
|
}) => {
|
|
579
|
+
const lifecycle = hostLifecycle;
|
|
580
|
+
assertGuestHostActive(lifecycle);
|
|
445
581
|
if (!METHODS.has(method)) {
|
|
446
582
|
throw new GuestServiceError('Unsupported request method.', 'BAD_METHOD');
|
|
447
583
|
}
|
|
@@ -453,6 +589,7 @@ export const proxyGuestServiceRequest = async ({
|
|
|
453
589
|
// enabled flag it read before the pause.
|
|
454
590
|
const epoch = stopEpochOf(guestId);
|
|
455
591
|
const store = await readExtensionStore(persistPath);
|
|
592
|
+
assertGuestHostActive(lifecycle);
|
|
456
593
|
if (store.disabledGuests?.[guestId]) {
|
|
457
594
|
const label = typeof guestName === 'string' && guestName.trim() ? guestName.trim() : 'This extension';
|
|
458
595
|
throw new GuestServiceError(
|
|
@@ -481,9 +618,10 @@ export const proxyGuestServiceRequest = async ({
|
|
|
481
618
|
socketBindings,
|
|
482
619
|
socketOverrides,
|
|
483
620
|
epoch,
|
|
621
|
+
lifecycle,
|
|
484
622
|
});
|
|
485
623
|
}
|
|
486
|
-
if (stopEpochOf(guestId) !== epoch) {
|
|
624
|
+
if (!lifecycle.accepting || stopEpochOf(guestId) !== epoch) {
|
|
487
625
|
if (runtimes.get(guestId) === runtime) {
|
|
488
626
|
await discardRuntime(guestId);
|
|
489
627
|
}
|
|
@@ -507,13 +645,24 @@ export const proxyGuestServiceRequest = async ({
|
|
|
507
645
|
|
|
508
646
|
/** @type {Record<string, string>} */
|
|
509
647
|
const headers = {
|
|
510
|
-
Accept:
|
|
648
|
+
Accept: accept,
|
|
511
649
|
[OPENCHAMBER_SERVICE_AUTH_HEADER]: `Bearer ${runtime.token}`,
|
|
512
650
|
};
|
|
513
651
|
if (body !== undefined && method !== 'GET') {
|
|
514
652
|
headers['Content-Type'] = 'application/json';
|
|
515
653
|
}
|
|
516
654
|
|
|
655
|
+
if (idleStopMs) {
|
|
656
|
+
armIdleStop(guestId, idleStopMs);
|
|
657
|
+
trackInflight(guestId, 1);
|
|
658
|
+
}
|
|
659
|
+
const settleIdle = () => {
|
|
660
|
+
if (!idleStopMs) return;
|
|
661
|
+
trackInflight(guestId, -1);
|
|
662
|
+
// Measured from the end of the last request, not its start: a long
|
|
663
|
+
// action must not be cut short by a timer armed before it began.
|
|
664
|
+
if (runtimes.get(guestId) === runtime) armIdleStop(guestId, idleStopMs);
|
|
665
|
+
};
|
|
517
666
|
let response;
|
|
518
667
|
try {
|
|
519
668
|
response = await fetch(url, {
|
|
@@ -521,18 +670,47 @@ export const proxyGuestServiceRequest = async ({
|
|
|
521
670
|
headers,
|
|
522
671
|
body: method === 'GET' || body === undefined ? undefined : body,
|
|
523
672
|
redirect: 'manual',
|
|
524
|
-
signal: AbortSignal.timeout(
|
|
673
|
+
signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(timeoutMs)]) : AbortSignal.timeout(timeoutMs),
|
|
525
674
|
});
|
|
526
675
|
} catch {
|
|
676
|
+
settleIdle();
|
|
677
|
+
if (signal?.aborted) {
|
|
678
|
+
throw new GuestServiceError('The request was cancelled.', 'CANCELLED');
|
|
679
|
+
}
|
|
527
680
|
runtime.status = 'failed';
|
|
528
|
-
throw new GuestServiceError('Guest service request failed.', '
|
|
681
|
+
throw new GuestServiceError('Guest service request failed.', 'REQUEST_FAILED');
|
|
529
682
|
}
|
|
683
|
+
// The body is still streaming when we return; the idle window starts once
|
|
684
|
+
// the caller has read it (or dropped it), which is what `finished` marks.
|
|
685
|
+
return {
|
|
686
|
+
response,
|
|
687
|
+
finished: settleIdle,
|
|
688
|
+
};
|
|
689
|
+
};
|
|
530
690
|
|
|
531
|
-
|
|
691
|
+
/**
|
|
692
|
+
* Panel-shaped proxy: text answer, capped. Same params as
|
|
693
|
+
* `openGuestServiceRequest` plus `responseMax`.
|
|
694
|
+
*/
|
|
695
|
+
export const proxyGuestServiceRequest = async ({ responseMax = GUEST_REQUEST_RESPONSE_MAX, ...params }) => {
|
|
696
|
+
const { response, finished } = await openGuestServiceRequest(params);
|
|
697
|
+
let text;
|
|
698
|
+
try {
|
|
699
|
+
text = await response.text();
|
|
700
|
+
} catch {
|
|
701
|
+
finished();
|
|
702
|
+
if (params.signal?.aborted) {
|
|
703
|
+
throw new GuestServiceError('The request was cancelled.', 'CANCELLED');
|
|
704
|
+
}
|
|
705
|
+
const runtime = runtimes.get(params.guestId);
|
|
706
|
+
if (runtime) runtime.status = 'failed';
|
|
707
|
+
throw new GuestServiceError('Guest service request failed.', 'REQUEST_FAILED');
|
|
708
|
+
}
|
|
709
|
+
finished();
|
|
532
710
|
return {
|
|
533
711
|
status: response.status,
|
|
534
|
-
body: text.length <=
|
|
712
|
+
body: text.length <= responseMax
|
|
535
713
|
? text
|
|
536
|
-
: text.slice(0,
|
|
714
|
+
: text.slice(0, responseMax),
|
|
537
715
|
};
|
|
538
716
|
};
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import { afterEach, describe, expect, test } from 'bun:test';
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, spyOn, test } from 'bun:test';
|
|
2
2
|
import fs from 'node:fs/promises';
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
|
|
6
6
|
import {
|
|
7
7
|
getServiceStatus,
|
|
8
|
+
beginGuestServiceHost,
|
|
9
|
+
beginGuestServiceShutdown,
|
|
8
10
|
proxyGuestServiceRequest,
|
|
9
11
|
readServicePid,
|
|
10
12
|
stopAllGuestServices,
|
|
@@ -45,6 +47,8 @@ http.createServer((req, res) => {
|
|
|
45
47
|
return { dir, persistPath, packageRoot };
|
|
46
48
|
};
|
|
47
49
|
|
|
50
|
+
beforeEach(() => beginGuestServiceHost());
|
|
51
|
+
|
|
48
52
|
afterEach(async () => {
|
|
49
53
|
await stopAllGuestServices();
|
|
50
54
|
});
|
|
@@ -194,6 +198,86 @@ describe('guest service proxy', () => {
|
|
|
194
198
|
});
|
|
195
199
|
});
|
|
196
200
|
|
|
201
|
+
describe('host-driven services', () => {
|
|
202
|
+
const request = (extra) => ({
|
|
203
|
+
guestId: 'docker',
|
|
204
|
+
packageRoot: extra.packageRoot,
|
|
205
|
+
service: { entry: 'service/main.js' },
|
|
206
|
+
granted: ['service'],
|
|
207
|
+
persistPath: extra.persistPath,
|
|
208
|
+
method: 'GET',
|
|
209
|
+
path: '/ping',
|
|
210
|
+
...extra,
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test('stops itself after the idle window and restarts on the next request', async () => {
|
|
214
|
+
const { dir, persistPath, packageRoot } = await writeFixture();
|
|
215
|
+
try {
|
|
216
|
+
await setCapabilityGrants('docker', persistPath, ['service']);
|
|
217
|
+
const first = await proxyGuestServiceRequest(request({ packageRoot, persistPath, idleStopMs: 150 }));
|
|
218
|
+
expect(first.status).toBe(200);
|
|
219
|
+
const pid = readServicePid('docker');
|
|
220
|
+
expect(pid).not.toBeNull();
|
|
221
|
+
await new Promise((resolve) => setTimeout(resolve, 400));
|
|
222
|
+
expect(getServiceStatus('docker')).toBe('stopped');
|
|
223
|
+
expect(readServicePid('docker')).toBeNull();
|
|
224
|
+
|
|
225
|
+
const second = await proxyGuestServiceRequest(request({ packageRoot, persistPath, idleStopMs: 150 }));
|
|
226
|
+
expect(second.status).toBe(200);
|
|
227
|
+
expect(readServicePid('docker')).not.toBe(pid);
|
|
228
|
+
} finally {
|
|
229
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test('a request keeps re-arming the window, and a panel request never arms one', async () => {
|
|
234
|
+
const { dir, persistPath, packageRoot } = await writeFixture();
|
|
235
|
+
try {
|
|
236
|
+
await setCapabilityGrants('docker', persistPath, ['service']);
|
|
237
|
+
await proxyGuestServiceRequest(request({ packageRoot, persistPath, idleStopMs: 200 }));
|
|
238
|
+
const pid = readServicePid('docker');
|
|
239
|
+
for (let i = 0; i < 3; i += 1) {
|
|
240
|
+
await new Promise((resolve) => setTimeout(resolve, 120));
|
|
241
|
+
await proxyGuestServiceRequest(request({ packageRoot, persistPath, idleStopMs: 200 }));
|
|
242
|
+
}
|
|
243
|
+
expect(readServicePid('docker')).toBe(pid);
|
|
244
|
+
|
|
245
|
+
await stopGuestService('docker');
|
|
246
|
+
await proxyGuestServiceRequest(request({ packageRoot, persistPath }));
|
|
247
|
+
await new Promise((resolve) => setTimeout(resolve, 350));
|
|
248
|
+
expect(getServiceStatus('docker')).toBe('ready');
|
|
249
|
+
} finally {
|
|
250
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
test('a longer timeout and a bigger response cap are honoured', async () => {
|
|
255
|
+
const { dir, persistPath, packageRoot } = await writeFixture();
|
|
256
|
+
try {
|
|
257
|
+
await setCapabilityGrants('docker', persistPath, ['service']);
|
|
258
|
+
const result = await proxyGuestServiceRequest(request({ packageRoot, persistPath, timeoutMs: 45_000, responseMax: 5 }));
|
|
259
|
+
expect(result).toEqual({ status: 200, body: '{"pon' });
|
|
260
|
+
} finally {
|
|
261
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
test('an aborted request reports cancellation, not a failed service', async () => {
|
|
266
|
+
const { dir, persistPath, packageRoot } = await writeFixture();
|
|
267
|
+
try {
|
|
268
|
+
await setCapabilityGrants('docker', persistPath, ['service']);
|
|
269
|
+
await proxyGuestServiceRequest(request({ packageRoot, persistPath }));
|
|
270
|
+
const controller = new AbortController();
|
|
271
|
+
controller.abort();
|
|
272
|
+
await expect(proxyGuestServiceRequest(request({ packageRoot, persistPath, signal: controller.signal })))
|
|
273
|
+
.rejects.toMatchObject({ code: 'CANCELLED' });
|
|
274
|
+
expect(getServiceStatus('docker')).toBe('ready');
|
|
275
|
+
} finally {
|
|
276
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
|
|
197
281
|
describe('pause during startup', () => {
|
|
198
282
|
test('a stop that lands while the service is coming up wins', async () => {
|
|
199
283
|
const { dir, persistPath, packageRoot } = await writeFixture();
|
|
@@ -275,3 +359,119 @@ describe('restart after the process died', () => {
|
|
|
275
359
|
}
|
|
276
360
|
});
|
|
277
361
|
});
|
|
362
|
+
|
|
363
|
+
describe('host shutdown', () => {
|
|
364
|
+
const request = (fixture) => ({
|
|
365
|
+
guestId: 'docker', packageRoot: fixture.packageRoot,
|
|
366
|
+
service: { entry: 'service/main.js' }, granted: ['service'],
|
|
367
|
+
persistPath: fixture.persistPath, method: 'GET', path: '/ping',
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
test('a replacement waits for the previous child and cannot escape host shutdown', async () => {
|
|
371
|
+
const fixture = await writeFixture();
|
|
372
|
+
await fs.appendFile(path.join(fixture.packageRoot, 'service/main.js'), '\nprocess.on("SIGTERM", () => {});\n');
|
|
373
|
+
let pid;
|
|
374
|
+
let reading;
|
|
375
|
+
let accessing;
|
|
376
|
+
try {
|
|
377
|
+
await proxyGuestServiceRequest(request(fixture));
|
|
378
|
+
pid = readServicePid('docker');
|
|
379
|
+
const previousStop = stopGuestService('docker');
|
|
380
|
+
accessing = spyOn(fs, 'access');
|
|
381
|
+
const storeRead = Promise.withResolvers();
|
|
382
|
+
const readFile = fs.readFile.bind(fs);
|
|
383
|
+
reading = spyOn(fs, 'readFile').mockImplementation(async (...args) => {
|
|
384
|
+
const result = await readFile(...args);
|
|
385
|
+
if (args[0] === fixture.persistPath) storeRead.resolve();
|
|
386
|
+
return result;
|
|
387
|
+
});
|
|
388
|
+
const replacement = proxyGuestServiceRequest(request(fixture)).catch((error) => error);
|
|
389
|
+
await storeRead.promise;
|
|
390
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
391
|
+
expect(accessing).not.toHaveBeenCalledWith(path.join(fixture.packageRoot, 'service/main.js'));
|
|
392
|
+
expect(readServicePid('docker')).toBeNull();
|
|
393
|
+
beginGuestServiceShutdown();
|
|
394
|
+
const stopping = stopAllGuestServices();
|
|
395
|
+
process.kill(pid, 'SIGKILL');
|
|
396
|
+
await Promise.all([previousStop, stopping]);
|
|
397
|
+
expect(await replacement).toMatchObject({ code: 'NO_SERVICE' });
|
|
398
|
+
expect(readServicePid('docker')).toBeNull();
|
|
399
|
+
expect(() => process.kill(pid, 0)).toThrow();
|
|
400
|
+
} finally {
|
|
401
|
+
reading?.mockRestore();
|
|
402
|
+
accessing?.mockRestore();
|
|
403
|
+
if (pid) { try { process.kill(pid, 'SIGKILL'); } catch {} }
|
|
404
|
+
await stopAllGuestServices();
|
|
405
|
+
await fs.rm(fixture.dir, { recursive: true, force: true });
|
|
406
|
+
}
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
test('rejects new requests, drains a live child, and only reopens on a new host lifecycle', async () => {
|
|
410
|
+
const fixture = await writeFixture();
|
|
411
|
+
try {
|
|
412
|
+
expect((await proxyGuestServiceRequest(request(fixture))).status).toBe(200);
|
|
413
|
+
const pid = readServicePid('docker');
|
|
414
|
+
beginGuestServiceShutdown();
|
|
415
|
+
const stopping = stopAllGuestServices();
|
|
416
|
+
await expect(proxyGuestServiceRequest(request(fixture))).rejects.toMatchObject({ code: 'NO_SERVICE' });
|
|
417
|
+
await stopping;
|
|
418
|
+
expect(readServicePid('docker')).toBeNull();
|
|
419
|
+
expect(() => process.kill(pid, 0)).toThrow();
|
|
420
|
+
await expect(proxyGuestServiceRequest(request(fixture))).rejects.toMatchObject({ code: 'NO_SERVICE' });
|
|
421
|
+
beginGuestServiceHost();
|
|
422
|
+
expect((await proxyGuestServiceRequest(request(fixture))).status).toBe(200);
|
|
423
|
+
} finally {
|
|
424
|
+
await stopAllGuestServices();
|
|
425
|
+
await fs.rm(fixture.dir, { recursive: true, force: true });
|
|
426
|
+
}
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
test('drains a first start paused before spawn and prevents it from surviving shutdown', async () => {
|
|
430
|
+
const fixture = await writeFixture();
|
|
431
|
+
const entered = Promise.withResolvers();
|
|
432
|
+
const release = Promise.withResolvers();
|
|
433
|
+
const access = fs.access.bind(fs);
|
|
434
|
+
const pausedAccess = spyOn(fs, 'access').mockImplementation(async (...args) => {
|
|
435
|
+
if (args[0] === path.join(fixture.packageRoot, 'service/main.js')) {
|
|
436
|
+
entered.resolve();
|
|
437
|
+
await release.promise;
|
|
438
|
+
}
|
|
439
|
+
return access(...args);
|
|
440
|
+
});
|
|
441
|
+
try {
|
|
442
|
+
const pending = proxyGuestServiceRequest(request(fixture)).catch((error) => error);
|
|
443
|
+
await entered.promise;
|
|
444
|
+
expect(readServicePid('docker')).toBeNull();
|
|
445
|
+
beginGuestServiceShutdown();
|
|
446
|
+
let drained = false;
|
|
447
|
+
const stopping = stopAllGuestServices().then(() => { drained = true; });
|
|
448
|
+
await Promise.resolve();
|
|
449
|
+
expect(drained).toBe(false);
|
|
450
|
+
expect(() => beginGuestServiceHost()).toThrow();
|
|
451
|
+
release.resolve();
|
|
452
|
+
expect(await pending).toMatchObject({ code: 'NO_SERVICE' });
|
|
453
|
+
await stopping;
|
|
454
|
+
expect(readServicePid('docker')).toBeNull();
|
|
455
|
+
expect(getServiceStatus('docker')).toBe('stopped');
|
|
456
|
+
} finally {
|
|
457
|
+
release.resolve();
|
|
458
|
+
pausedAccess.mockRestore();
|
|
459
|
+
await stopAllGuestServices();
|
|
460
|
+
await fs.rm(fixture.dir, { recursive: true, force: true });
|
|
461
|
+
}
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
test('a request still reading the store cannot enter a later host lifecycle', async () => {
|
|
465
|
+
const fixture = await writeFixture();
|
|
466
|
+
try {
|
|
467
|
+
const pending = proxyGuestServiceRequest(request(fixture));
|
|
468
|
+
beginGuestServiceShutdown();
|
|
469
|
+
await stopAllGuestServices();
|
|
470
|
+
beginGuestServiceHost();
|
|
471
|
+
await expect(pending).rejects.toMatchObject({ code: 'NO_SERVICE' });
|
|
472
|
+
expect(readServicePid('docker')).toBeNull();
|
|
473
|
+
} finally {
|
|
474
|
+
await fs.rm(fixture.dir, { recursive: true, force: true });
|
|
475
|
+
}
|
|
476
|
+
});
|
|
477
|
+
});
|