@vantran-se/openchamber-web 1.24.3 → 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-BjJHrzmU.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-CDOJy5Ay.js → GitView-BWa7e5vw.js} +4 -4
- package/dist/assets/{GuestAttachDialog-D3S0Rjic.js → GuestAttachDialog-DdOuyZzp.js} +1 -1
- package/dist/assets/GuestSurfacePane-DDfzid3U.js +1 -0
- package/dist/assets/{JsonTreeView-D0C9nU7K.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-DnEhjXcC.js → PatchDiff-DS-IMU1j.js} +1 -1
- package/dist/assets/{PierreDiffViewer-DyxZVVVR.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-0pyr_mHY.js → VirtualizedFileDiff-C763hNCR.js} +4 -4
- package/dist/assets/{WalkthroughView-yNj4nOP5.js → WalkthroughView-lap5btCF.js} +2 -2
- package/dist/assets/appThemeRegistry-u4zw1l0B.js +1 -0
- package/dist/assets/{commitAndPush-DvJKa3Ks.js → commitAndPush-CwGJfJG-.js} +1 -1
- package/dist/assets/{computeVirtualFileMetrics-DxTBMuu-.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-D7FkBMP6.js → isDiffPlainText-D47a6hMP.js} +1 -1
- package/dist/assets/{isFilePlainText-CL2vsrIZ.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-Dh7WEsdM.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-Bsba8KFp.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-8_3csmr6.js → shikiHighlight-DNm2Z_Jc.js} +1 -1
- package/dist/assets/{textMateThemeFromAppTheme-CHo2fCgW.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-DkQ_gqpn.js → useInlineCommentController-CuUuakrt.js} +2 -2
- package/dist/assets/useRootScrollLock-CQIsyERq.js +2 -0
- package/dist/assets/{vendor-codemirror-language-data-CdzUmUyZ.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-B8IcVQAA.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-COVQhWGd.js +0 -4
- package/dist/assets/FilesView-B1V5JxFX.js +0 -13
- package/dist/assets/LinearIssuesView-D7FKg1XJ.js +0 -1
- package/dist/assets/MarkdownImageGallery-BrFjAVEf.js +0 -1
- package/dist/assets/MarkdownRendererImpl-DWepTafh.js +0 -12
- package/dist/assets/OnboardingScreen-Dyxxiqdt.js +0 -1
- package/dist/assets/PlanView-LW2z_EAP.js +0 -5
- package/dist/assets/PluginPane-B4tKno2h.js +0 -3
- package/dist/assets/SettingsView-DEaMulN9.js +0 -29
- package/dist/assets/SettingsWindow-FLDJXOgQ.js +0 -1
- package/dist/assets/SubmoduleDiffSummary-CvVwD-Vi.js +0 -1
- package/dist/assets/ToolOutputDialog-BiP0x2Rw.js +0 -15
- package/dist/assets/ToolPartDiffPreview-CkR4msKP.js +0 -8
- package/dist/assets/appThemeRegistry-BuUKhbYQ.js +0 -1
- package/dist/assets/de-DgxgFyGG.js +0 -10
- package/dist/assets/es-BFsZj5F_.js +0 -16
- package/dist/assets/fr-jdZ5KKQi.js +0 -14
- package/dist/assets/index-CKHjPKbq.js +0 -1
- package/dist/assets/index-Cbci4i1W.js +0 -40
- package/dist/assets/index-DqaMWNrC.css +0 -1
- package/dist/assets/ja-C5kKrhO4.js +0 -16
- package/dist/assets/ko-DXDTT_0O.js +0 -16
- package/dist/assets/main-CZALtVek.js +0 -896
- package/dist/assets/main-CgXMBNvG.js +0 -2
- package/dist/assets/miniChat-CwTmDixi.js +0 -2
- package/dist/assets/mobile-CLf2vCtO.js +0 -2
- package/dist/assets/pl-D-7x4Y8Q.js +0 -16
- package/dist/assets/pt-BR-CSBZrHa8.js +0 -16
- package/dist/assets/renderElectronMiniChatApp-C5gAFBk0.js +0 -2
- package/dist/assets/renderMobileApp-V4hYd32H.js +0 -25
- package/dist/assets/runtimeEndpointReset-DhlHCn-2.js +0 -25
- package/dist/assets/scheduledTasksApi-DzjbLd8P.js +0 -1
- package/dist/assets/tr-Dr-oJ22L.js +0 -7
- package/dist/assets/uk-9Nx83UcI.js +0 -16
- package/dist/assets/useAppFontEffects-khZXpFL_.js +0 -7333
- package/dist/assets/useRootScrollLock-DdvHf9Pt.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-Bxy_Hj3r.js +0 -16
- package/dist/assets/zh-TW-0BB4gpHM.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
|
@@ -0,0 +1,587 @@
|
|
|
1
|
+
import { SpaceError } from '../errors.js';
|
|
2
|
+
import { createGatekeeperChannel } from '../gatekeeper-channel.js';
|
|
3
|
+
import {
|
|
4
|
+
buildGatekeeperCreateArgs,
|
|
5
|
+
buildGatekeeperNetworkArgs,
|
|
6
|
+
buildSpaceCreateArgs,
|
|
7
|
+
buildSpaceNetworkArgs,
|
|
8
|
+
buildVolumeOwnershipRunArgs,
|
|
9
|
+
findGatekeeperHardeningViolations,
|
|
10
|
+
findHardeningViolations,
|
|
11
|
+
requireMemoryBytes,
|
|
12
|
+
} from '../hardening.js';
|
|
13
|
+
import {
|
|
14
|
+
ROLE_GATEKEEPER,
|
|
15
|
+
ROLE_NETWORK,
|
|
16
|
+
ROLE_OUTER_NETWORK,
|
|
17
|
+
ROLE_SETUP,
|
|
18
|
+
ROLE_SPACE,
|
|
19
|
+
ROLE_VOLUME,
|
|
20
|
+
buildSpaceLabels,
|
|
21
|
+
labelArgs,
|
|
22
|
+
labelFilterArgs,
|
|
23
|
+
parseSpaceLabels,
|
|
24
|
+
requireOwner,
|
|
25
|
+
requireSpaceId,
|
|
26
|
+
spaceResourceName,
|
|
27
|
+
} from '../labels.js';
|
|
28
|
+
import { SPACE_USER, TOOLS_MOUNT_PATH } from '../layout.js';
|
|
29
|
+
import { createSpaceServerChannel, createSpaceToken } from '../space-server.js';
|
|
30
|
+
import { CHANGE_TIMEOUT_MS, ROLLBACK_SETTLE_MS, createDockerEngine, entryLabels, entryName, isInterrupted, pause } from './docker-engine.js';
|
|
31
|
+
import { createDockerTools } from './docker-tools.js';
|
|
32
|
+
|
|
33
|
+
const DOCKER_PLACE_ID = 'docker';
|
|
34
|
+
|
|
35
|
+
// node:22-bookworm as a multi-arch index digest. DOCUMENTATION.md says how it was verified.
|
|
36
|
+
export const SPACE_BASE_IMAGE = 'node@sha256:dd5847a04b0deee391fa145f1f4c6d214196668b6bcc7988ebed67249f226844';
|
|
37
|
+
|
|
38
|
+
const CHECK_TIMEOUT_MS = 10_000;
|
|
39
|
+
const PULL_TIMEOUT_MS = 20 * 60_000;
|
|
40
|
+
const EXEC_TIMEOUT_MS = 60_000;
|
|
41
|
+
// The bridge option that keeps the space away from services on the Docker host.
|
|
42
|
+
const HOST_ISOLATION_MIN_ENGINE = 28;
|
|
43
|
+
// While a space moves to a new tools volume, its old container waits under this suffix.
|
|
44
|
+
const ASIDE_SUFFIX = 'old';
|
|
45
|
+
|
|
46
|
+
// Removal order. A network cannot go while a container is attached, and a volume cannot go while mounted.
|
|
47
|
+
const KINDS = ['container', 'volume', 'network'];
|
|
48
|
+
|
|
49
|
+
/** Which container an `exec` runs in. The place contract knows two targets and no more. */
|
|
50
|
+
function execRole(target) {
|
|
51
|
+
if (target === undefined || target === ROLE_SPACE) {
|
|
52
|
+
return ROLE_SPACE;
|
|
53
|
+
}
|
|
54
|
+
if (target === ROLE_GATEKEEPER) {
|
|
55
|
+
return ROLE_GATEKEEPER;
|
|
56
|
+
}
|
|
57
|
+
throw new SpaceError('invalid_exec_target', `A command runs in the space or in its gatekeeper, not in '${target}'`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function createDockerPlace({ runCommand, dockerPath, owner, toolsSource, wait = pause, now = () => new Date() }) {
|
|
61
|
+
requireOwner(owner);
|
|
62
|
+
|
|
63
|
+
const engine = createDockerEngine({ runCommand, dockerPath });
|
|
64
|
+
const { run, docker, inspect, removeOne, removeStoppedContainer } = engine;
|
|
65
|
+
const tools = createDockerTools({ engine, owner, toolsSource, image: SPACE_BASE_IMAGE, now, wait });
|
|
66
|
+
|
|
67
|
+
/** Every resource that carries our marker and this owner, optionally for one space. Found by label only. */
|
|
68
|
+
const findResources = async (spaceId) => {
|
|
69
|
+
const filters = labelFilterArgs({ owner, spaceId });
|
|
70
|
+
const resources = [];
|
|
71
|
+
for (const kind of KINDS) {
|
|
72
|
+
for (const entry of await engine.findByLabel(kind, filters)) {
|
|
73
|
+
const labels = parseSpaceLabels(entryLabels(kind, entry));
|
|
74
|
+
if (labels && labels.owner === owner && (spaceId === null || labels.id === spaceId)) {
|
|
75
|
+
resources.push({ kind, name: entryName(entry), labels, entry });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return resources;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
/** The inspect entry of a container of this space, or null. Refuses one without this owner's labels. */
|
|
83
|
+
const inspectOwnContainer = async (spaceId, name) => {
|
|
84
|
+
const entry = await inspect('container', name);
|
|
85
|
+
if (!entry) {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
const labels = parseSpaceLabels(entry.Config?.Labels);
|
|
89
|
+
if (labels?.id !== spaceId || labels?.owner !== owner) {
|
|
90
|
+
throw new SpaceError('space_not_ours', `Container ${name} exists but this OpenChamber installation did not create it`);
|
|
91
|
+
}
|
|
92
|
+
return entry;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* The container of the space, for the operations that only read or stop. They repair nothing.
|
|
97
|
+
* While a move to new tools is under way, or after one died, the container waits under its
|
|
98
|
+
* aside name. Only `start` finishes or undoes a move, so these operations say so and change nothing.
|
|
99
|
+
*/
|
|
100
|
+
const requireSpaceContainer = async (spaceId) => {
|
|
101
|
+
const name = spaceResourceName(spaceId, ROLE_SPACE);
|
|
102
|
+
const entry = await inspectOwnContainer(spaceId, name);
|
|
103
|
+
if (entry) {
|
|
104
|
+
return entry;
|
|
105
|
+
}
|
|
106
|
+
if (await inspectOwnContainer(spaceId, spaceResourceName(spaceId, ROLE_SPACE, ASIDE_SUFFIX))) {
|
|
107
|
+
throw new SpaceError('space_move_unfinished', `Space ${spaceId} is in the middle of a move to new tools, or a move did not finish. Start the space to repair it.`);
|
|
108
|
+
}
|
|
109
|
+
throw new SpaceError('space_not_found', `Space ${spaceId} has no container in Docker`);
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const check = async () => {
|
|
113
|
+
const unavailable = (code, message) => ({ available: false, code, message });
|
|
114
|
+
let version;
|
|
115
|
+
let info;
|
|
116
|
+
try {
|
|
117
|
+
version = await run(['version', '--format', '{{json .}}'], CHECK_TIMEOUT_MS);
|
|
118
|
+
info = await run(['info', '--format', '{{json .SecurityOptions}}'], CHECK_TIMEOUT_MS);
|
|
119
|
+
} catch (error) {
|
|
120
|
+
if (error.code === 'command_spawn_failed' && error.details?.errno === 'ENOENT') {
|
|
121
|
+
return unavailable('docker_cli_missing', 'The docker command was not found. Install Docker Desktop, Colima or Docker Engine, then try again.');
|
|
122
|
+
}
|
|
123
|
+
if (error.code === 'command_spawn_failed') {
|
|
124
|
+
return unavailable('docker_cli_unusable', `The docker command could not be started (${error.details?.errno ?? 'unknown error'}). Check that ${dockerPath} is a program this user may run.`);
|
|
125
|
+
}
|
|
126
|
+
if (error.code === 'command_timeout') {
|
|
127
|
+
return unavailable('docker_daemon_unreachable', 'Docker did not answer in time. Restart Docker Desktop or Colima, then try again.');
|
|
128
|
+
}
|
|
129
|
+
throw error;
|
|
130
|
+
}
|
|
131
|
+
let server = null;
|
|
132
|
+
let securityOptions = [];
|
|
133
|
+
try {
|
|
134
|
+
server = JSON.parse(version.stdout).Server ?? null;
|
|
135
|
+
securityOptions = JSON.parse(info.stdout) ?? [];
|
|
136
|
+
} catch {
|
|
137
|
+
server = null;
|
|
138
|
+
}
|
|
139
|
+
if (version.code !== 0 || info.code !== 0 || !server) {
|
|
140
|
+
return unavailable('docker_daemon_unreachable', 'Docker is installed but not running. Start Docker Desktop or Colima, then try again.');
|
|
141
|
+
}
|
|
142
|
+
// Older engines call the builtin profile `default`.
|
|
143
|
+
if (!securityOptions.some((option) => /^name=seccomp,profile=(builtin|default)$/.test(option))) {
|
|
144
|
+
return unavailable('docker_seccomp_missing', 'This Docker engine runs containers without its builtin seccomp profile. Turn seccomp back on in the Docker daemon settings, then try again.');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
available: true,
|
|
149
|
+
version: server.Version,
|
|
150
|
+
os: server.Os,
|
|
151
|
+
arch: server.Arch,
|
|
152
|
+
// False means a space on this engine could reach services that listen on the Docker host.
|
|
153
|
+
hostIsolation: Number.parseInt(server.Version, 10) >= HOST_ISOLATION_MIN_ENGINE,
|
|
154
|
+
};
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const ensureImage = async () => {
|
|
158
|
+
const present = await inspect('image', SPACE_BASE_IMAGE);
|
|
159
|
+
if (present) {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
const result = await run(['pull', SPACE_BASE_IMAGE], PULL_TIMEOUT_MS);
|
|
163
|
+
if (result.code !== 0) {
|
|
164
|
+
throw new SpaceError(
|
|
165
|
+
'image_pull_failed',
|
|
166
|
+
`Could not download the base image: ${result.stderr.trim() || `exit code ${result.code}`}. Common causes: no internet access on the Docker machine, or Docker's credential helper cannot run in this session.`,
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const verifyContainer = async (spaceId, container) => {
|
|
172
|
+
const network = await inspect('network', spaceResourceName(spaceId, ROLE_NETWORK));
|
|
173
|
+
const toolsMount = (container.Mounts ?? []).find((mount) => mount.Destination === TOOLS_MOUNT_PATH);
|
|
174
|
+
const toolsVolume = toolsMount?.Name ? await inspect('volume', toolsMount.Name) : null;
|
|
175
|
+
return findHardeningViolations({ spaceId, owner, container, network, toolsVolume });
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const verifyGatekeeperContainer = async (spaceId, container) => {
|
|
179
|
+
const outerNetwork = await inspect('network', spaceResourceName(spaceId, ROLE_OUTER_NETWORK));
|
|
180
|
+
return findGatekeeperHardeningViolations({ spaceId, owner, container, outerNetwork });
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Both containers and both networks. A space with no gatekeeper is a space with an
|
|
185
|
+
* uncontrolled way out, so it is a violation like any other and the manager removes it.
|
|
186
|
+
*/
|
|
187
|
+
const verify = async (spaceId) => {
|
|
188
|
+
const violations = await verifyContainer(spaceId, await requireSpaceContainer(spaceId));
|
|
189
|
+
const gatekeeperContainer = await inspectOwnContainer(spaceId, spaceResourceName(spaceId, ROLE_GATEKEEPER));
|
|
190
|
+
if (!gatekeeperContainer) {
|
|
191
|
+
return [...violations, { check: 'gatekeeper_missing', message: 'The space has no gatekeeper container' }];
|
|
192
|
+
}
|
|
193
|
+
return [...violations, ...(await verifyGatekeeperContainer(spaceId, gatekeeperContainer))];
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const requireNoViolations = (violations, what) => {
|
|
197
|
+
if (violations.length > 0) {
|
|
198
|
+
throw new SpaceError(
|
|
199
|
+
'space_verification_failed',
|
|
200
|
+
`The new ${what} does not match the requested restrictions: ${violations.map((violation) => violation.message).join('; ')}`,
|
|
201
|
+
{ violations },
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const requireVerified = async (spaceId, container) => requireNoViolations(await verifyContainer(spaceId, container), 'container');
|
|
207
|
+
|
|
208
|
+
const requireGatekeeperVerified = async (spaceId, container) => requireNoViolations(await verifyGatekeeperContainer(spaceId, container), 'gatekeeper');
|
|
209
|
+
|
|
210
|
+
const remove = async (spaceId) => {
|
|
211
|
+
const resources = await findResources(requireSpaceId(spaceId));
|
|
212
|
+
const removed = [];
|
|
213
|
+
const failed = [];
|
|
214
|
+
for (const { kind, name } of resources) {
|
|
215
|
+
const problem = await removeOne(kind, name);
|
|
216
|
+
if (problem) {
|
|
217
|
+
failed.push(problem);
|
|
218
|
+
} else {
|
|
219
|
+
removed.push({ kind, name });
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return { removed, failed };
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
const rollBack = async (spaceId) => {
|
|
226
|
+
try {
|
|
227
|
+
return (await remove(spaceId)).failed;
|
|
228
|
+
} catch (error) {
|
|
229
|
+
return [{ kind: 'space', name: spaceId, message: error.message }];
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Runs argv as the space user in the space or, with `target: 'gatekeeper'`, in its gatekeeper.
|
|
235
|
+
* It does not look at the container first, so it also serves containers this place just made.
|
|
236
|
+
*/
|
|
237
|
+
const execInContainer = (spaceId, argv, options = {}) => run(
|
|
238
|
+
['exec', '--interactive', '--user', SPACE_USER, spaceResourceName(spaceId, execRole(options.target)), ...argv],
|
|
239
|
+
options.timeoutMs ?? EXEC_TIMEOUT_MS,
|
|
240
|
+
{ stdin: options.stdin ?? '' },
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
const server = createSpaceServerChannel({ exec: execInContainer, wait, now: () => now().getTime() });
|
|
244
|
+
const gatekeeper = createGatekeeperChannel({ exec: execInContainer, wait, now: () => now().getTime() });
|
|
245
|
+
|
|
246
|
+
const create = async ({ id, name, project, created, memoryBytes }) => {
|
|
247
|
+
requireMemoryBytes(memoryBytes);
|
|
248
|
+
// Built first, so a bad spec is rejected before Docker is asked anything.
|
|
249
|
+
const labelArguments = new Map([ROLE_NETWORK, ROLE_OUTER_NETWORK, ROLE_VOLUME, ROLE_SETUP, ROLE_GATEKEEPER, ROLE_SPACE].map((role) => (
|
|
250
|
+
[role, labelArgs(buildSpaceLabels({ id, role, owner, project, name, created }))]
|
|
251
|
+
)));
|
|
252
|
+
const labelsFor = (role) => labelArguments.get(role);
|
|
253
|
+
const resources = {
|
|
254
|
+
spaceId: id,
|
|
255
|
+
network: spaceResourceName(id, ROLE_NETWORK),
|
|
256
|
+
workVolume: spaceResourceName(id, ROLE_VOLUME, 'work'),
|
|
257
|
+
homeVolume: spaceResourceName(id, ROLE_VOLUME, 'home'),
|
|
258
|
+
image: SPACE_BASE_IMAGE,
|
|
259
|
+
};
|
|
260
|
+
const outerNetwork = spaceResourceName(id, ROLE_OUTER_NETWORK);
|
|
261
|
+
const containerName = spaceResourceName(id, ROLE_SPACE);
|
|
262
|
+
const gatekeeperName = spaceResourceName(id, ROLE_GATEKEEPER);
|
|
263
|
+
const taken = [
|
|
264
|
+
['container', containerName],
|
|
265
|
+
['container', spaceResourceName(id, ROLE_SETUP)],
|
|
266
|
+
['container', gatekeeperName],
|
|
267
|
+
['network', resources.network],
|
|
268
|
+
['network', outerNetwork],
|
|
269
|
+
['volume', resources.workVolume],
|
|
270
|
+
['volume', resources.homeVolume],
|
|
271
|
+
];
|
|
272
|
+
|
|
273
|
+
// `docker volume create` succeeds silently on an existing name, so look first.
|
|
274
|
+
// This stays outside the rollback: nothing here is ours to remove yet.
|
|
275
|
+
for (const [kind, resourceName] of taken) {
|
|
276
|
+
if (await inspect(kind, resourceName)) {
|
|
277
|
+
throw new SpaceError('space_name_taken', `Docker already has a ${kind} named ${resourceName}. Remove it or create the space again.`);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
let toolsVolume = null;
|
|
282
|
+
try {
|
|
283
|
+
await ensureImage();
|
|
284
|
+
// The tools volume is shared by every space of this owner. It has a rollback of its own,
|
|
285
|
+
// and the rollback below never touches a labelled one: it removes by space id and by the seven names only.
|
|
286
|
+
toolsVolume = await tools.ensure();
|
|
287
|
+
await docker(buildSpaceNetworkArgs({ network: resources.network, labelArguments: labelsFor(ROLE_NETWORK) }), CHANGE_TIMEOUT_MS);
|
|
288
|
+
await docker(buildGatekeeperNetworkArgs({ network: outerNetwork, labelArguments: labelsFor(ROLE_OUTER_NETWORK) }), CHANGE_TIMEOUT_MS);
|
|
289
|
+
await docker(['volume', 'create', ...labelsFor(ROLE_VOLUME), resources.workVolume], CHANGE_TIMEOUT_MS);
|
|
290
|
+
await docker(['volume', 'create', ...labelsFor(ROLE_VOLUME), resources.homeVolume], CHANGE_TIMEOUT_MS);
|
|
291
|
+
await docker(buildVolumeOwnershipRunArgs({
|
|
292
|
+
...resources,
|
|
293
|
+
containerName: spaceResourceName(id, ROLE_SETUP),
|
|
294
|
+
labelArguments: labelsFor(ROLE_SETUP),
|
|
295
|
+
}), CHANGE_TIMEOUT_MS);
|
|
296
|
+
// The gatekeeper comes first, and its corridor accepts connections before the space
|
|
297
|
+
// container starts, so the proxy in the space environment works from the first moment.
|
|
298
|
+
// It is also the first container on the inner network, which is why it holds the `.1`
|
|
299
|
+
// address there: that address is the gatekeeper and never the Docker host.
|
|
300
|
+
await docker(buildGatekeeperCreateArgs({
|
|
301
|
+
containerName: gatekeeperName,
|
|
302
|
+
labelArguments: labelsFor(ROLE_GATEKEEPER),
|
|
303
|
+
network: resources.network,
|
|
304
|
+
image: SPACE_BASE_IMAGE,
|
|
305
|
+
}), CHANGE_TIMEOUT_MS);
|
|
306
|
+
// A second network needs its own call: Docker refuses `--network-alias` with two `--network`.
|
|
307
|
+
await docker(['network', 'connect', outerNetwork, gatekeeperName], CHANGE_TIMEOUT_MS);
|
|
308
|
+
await requireGatekeeperVerified(id, await inspectOwnContainer(id, gatekeeperName) ?? {});
|
|
309
|
+
await docker(['start', gatekeeperName], CHANGE_TIMEOUT_MS);
|
|
310
|
+
await gatekeeper.writeProgram(id);
|
|
311
|
+
await gatekeeper.waitUntilReady(id);
|
|
312
|
+
// Create, verify, then start: a container that fails the check never runs.
|
|
313
|
+
await docker(buildSpaceCreateArgs({
|
|
314
|
+
...resources,
|
|
315
|
+
containerName,
|
|
316
|
+
labelArguments: labelsFor(ROLE_SPACE),
|
|
317
|
+
toolsVolume,
|
|
318
|
+
memoryBytes,
|
|
319
|
+
}), CHANGE_TIMEOUT_MS);
|
|
320
|
+
await requireVerified(id, await inspectOwnContainer(id, containerName) ?? {});
|
|
321
|
+
await docker(['start', containerName], CHANGE_TIMEOUT_MS);
|
|
322
|
+
// The server inside waits for its token. It arrives on stdin, after the start,
|
|
323
|
+
// so it is in no argument, no container env and no label.
|
|
324
|
+
await server.linkPlugin(id);
|
|
325
|
+
await server.writeToken(id, createSpaceToken());
|
|
326
|
+
await server.waitUntilReady(id);
|
|
327
|
+
} catch (error) {
|
|
328
|
+
let rollbackFailures = await rollBack(id);
|
|
329
|
+
// The CLI was killed, the daemon may still finish the step. Sweep again after a pause.
|
|
330
|
+
const uncertain = isInterrupted(error);
|
|
331
|
+
if (uncertain) {
|
|
332
|
+
await wait(ROLLBACK_SETTLE_MS);
|
|
333
|
+
rollbackFailures = [...(await rollBack(id)), ...(await engine.removeByName(taken))];
|
|
334
|
+
}
|
|
335
|
+
// Last, because a volume cannot go while the failed container still mounts it.
|
|
336
|
+
if (toolsVolume) {
|
|
337
|
+
rollbackFailures = [...rollbackFailures, ...(await tools.removeIfUnlabelled(toolsVolume))];
|
|
338
|
+
}
|
|
339
|
+
const leftovers = rollbackFailures.length > 0
|
|
340
|
+
? ` Clean-up also failed for: ${rollbackFailures.map((item) => `${item.kind} ${item.name}`).join(', ')}.`
|
|
341
|
+
: '';
|
|
342
|
+
const wrapped = new SpaceError(
|
|
343
|
+
error.code ?? 'space_create_failed',
|
|
344
|
+
`Could not create the space. ${error.message}${leftovers}${uncertain ? ' Docker may still finish the interrupted step, so look at the spaces list.' : ''}`,
|
|
345
|
+
{ original: error.details ?? null, rollbackFailures, uncertain },
|
|
346
|
+
);
|
|
347
|
+
wrapped.cause = error;
|
|
348
|
+
throw wrapped;
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
const list = async () => {
|
|
353
|
+
const byId = new Map();
|
|
354
|
+
for (const resource of await findResources(null)) {
|
|
355
|
+
const group = byId.get(resource.labels.id) ?? [];
|
|
356
|
+
group.push(resource);
|
|
357
|
+
byId.set(resource.labels.id, group);
|
|
358
|
+
}
|
|
359
|
+
return Array.from(byId.entries(), ([id, group]) => {
|
|
360
|
+
// A move to new tools that died half way leaves two containers. The one with the plain name is the space.
|
|
361
|
+
const containers = group.filter((resource) => resource.labels.role === ROLE_SPACE);
|
|
362
|
+
const space = containers.find((resource) => resource.name === spaceResourceName(id, ROLE_SPACE)) ?? containers[0];
|
|
363
|
+
const { name, project, created } = (space ?? group[0]).labels;
|
|
364
|
+
let state = 'missing';
|
|
365
|
+
if (space) {
|
|
366
|
+
state = space.entry.State?.Running === true ? 'running' : 'exited';
|
|
367
|
+
}
|
|
368
|
+
const orphans = space ? [] : group.map((resource) => ({ kind: resource.kind, name: resource.name }));
|
|
369
|
+
// A container whose network, volume or gatekeeper is gone. `list` only reports it and repairs nothing.
|
|
370
|
+
const expected = [
|
|
371
|
+
spaceResourceName(id, ROLE_NETWORK),
|
|
372
|
+
spaceResourceName(id, ROLE_OUTER_NETWORK),
|
|
373
|
+
spaceResourceName(id, ROLE_VOLUME, 'work'),
|
|
374
|
+
spaceResourceName(id, ROLE_VOLUME, 'home'),
|
|
375
|
+
];
|
|
376
|
+
const missing = space ? expected.filter((resourceName) => !group.some((resource) => resource.name === resourceName)) : [];
|
|
377
|
+
// `state` stays the state of the space container, because that is what start and stop act on.
|
|
378
|
+
// A gatekeeper that is absent, or that does not run while the space does, is as good as no
|
|
379
|
+
// gatekeeper for the space, and both count as missing here.
|
|
380
|
+
const guard = group.find((resource) => resource.labels.role === ROLE_GATEKEEPER);
|
|
381
|
+
if (space && (!guard || (state === 'running' && guard.entry.State?.Running !== true))) {
|
|
382
|
+
missing.push(spaceResourceName(id, ROLE_GATEKEEPER));
|
|
383
|
+
}
|
|
384
|
+
return { id, name, project, created, state, orphans, damaged: missing.length > 0, missing };
|
|
385
|
+
});
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
// Starts that are under way in this process, by space id.
|
|
389
|
+
const starting = new Map();
|
|
390
|
+
|
|
391
|
+
/** The gatekeeper container of a space. It is never renamed, so there is no move to repair here. */
|
|
392
|
+
const requireGatekeeperContainer = async (spaceId) => {
|
|
393
|
+
const name = spaceResourceName(spaceId, ROLE_GATEKEEPER);
|
|
394
|
+
const entry = await inspectOwnContainer(spaceId, name);
|
|
395
|
+
if (!entry) {
|
|
396
|
+
throw new SpaceError('gatekeeper_missing', `Space ${spaceId} has no gatekeeper container. Apply or discard its work and create the space again.`);
|
|
397
|
+
}
|
|
398
|
+
return entry;
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
const exec = async (spaceId, argv, options = {}) => {
|
|
402
|
+
if (!Array.isArray(argv) || argv.length === 0) {
|
|
403
|
+
throw new SpaceError('invalid_command', 'A command is a non-empty array of arguments');
|
|
404
|
+
}
|
|
405
|
+
if (execRole(options.target) === ROLE_GATEKEEPER) {
|
|
406
|
+
await requireGatekeeperContainer(spaceId);
|
|
407
|
+
} else {
|
|
408
|
+
await requireSpaceContainer(spaceId);
|
|
409
|
+
}
|
|
410
|
+
return execInContainer(spaceId, argv, options);
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* The argv that runs a command in the space with its stdin and stdout attached, for a caller that
|
|
415
|
+
* must start the process itself: git starts it for a push over `ext::`. Only the space, never the
|
|
416
|
+
* gatekeeper. The same ownership check as `exec` runs first, so a missing container or one this
|
|
417
|
+
* installation did not create is refused, and so is a stopped one. The argv carries no secret.
|
|
418
|
+
*/
|
|
419
|
+
const execArgv = async (spaceId) => {
|
|
420
|
+
const entry = await requireSpaceContainer(spaceId);
|
|
421
|
+
// A stopped container would only answer git with the daemon's own words.
|
|
422
|
+
if (entry.State?.Running !== true) {
|
|
423
|
+
throw new SpaceError('space_not_running', `Space ${spaceId} is stopped. Start it, then try again.`);
|
|
424
|
+
}
|
|
425
|
+
return [dockerPath, 'exec', '--interactive', '--user', SPACE_USER, spaceResourceName(spaceId, ROLE_SPACE)];
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* The space stops first and its gatekeeper after it, so a space is never running while its
|
|
430
|
+
* way out is not under the host's control. A stop of the gatekeeper that fails leaves the
|
|
431
|
+
* space stopped, which is the safe side of this order.
|
|
432
|
+
*/
|
|
433
|
+
const stop = async (spaceId) => {
|
|
434
|
+
// A start of this space that is under way finishes first, whatever its outcome. A stop that
|
|
435
|
+
// slipped in between the create and the start of a move would resolve, and the space would run anyway.
|
|
436
|
+
await starting.get(spaceId)?.catch(() => {});
|
|
437
|
+
await requireSpaceContainer(spaceId);
|
|
438
|
+
await docker(['stop', spaceResourceName(spaceId, ROLE_SPACE)], CHANGE_TIMEOUT_MS);
|
|
439
|
+
const gatekeeperName = spaceResourceName(spaceId, ROLE_GATEKEEPER);
|
|
440
|
+
if (await inspectOwnContainer(spaceId, gatekeeperName)) {
|
|
441
|
+
await docker(['stop', gatekeeperName], CHANGE_TIMEOUT_MS);
|
|
442
|
+
}
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* The gatekeeper runs, with its program, before the space container starts. Its tmpfs is empty
|
|
447
|
+
* again after every stop, so the program is written at every start. The network mode, the
|
|
448
|
+
* allowlist and the grants are not restored here: the gatekeeper comes up allowing nothing,
|
|
449
|
+
* and the host says again what this space may reach. No container of ours restarts by itself,
|
|
450
|
+
* so this also holds after the Docker machine restarts.
|
|
451
|
+
*/
|
|
452
|
+
const startGatekeeper = async (spaceId) => {
|
|
453
|
+
const name = spaceResourceName(spaceId, ROLE_GATEKEEPER);
|
|
454
|
+
const container = await requireGatekeeperContainer(spaceId);
|
|
455
|
+
if (container.State?.Running !== true) {
|
|
456
|
+
await requireGatekeeperVerified(spaceId, container);
|
|
457
|
+
await docker(['start', name], CHANGE_TIMEOUT_MS);
|
|
458
|
+
}
|
|
459
|
+
await gatekeeper.writeProgram(spaceId);
|
|
460
|
+
await gatekeeper.waitUntilReady(spaceId);
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Moves a stopped space to the current tools volume. The mounts of a container are fixed
|
|
465
|
+
* at creation, so the container is made again. Everything the space owns lives in its
|
|
466
|
+
* two volumes, so nothing is lost. The old container waits aside until the new one is healthy.
|
|
467
|
+
*
|
|
468
|
+
* The new container is only ever removed by the id that `docker create` printed. A name
|
|
469
|
+
* says nothing here: if anything gave the old container its name back in the meantime, a
|
|
470
|
+
* removal by name would delete the space's only container.
|
|
471
|
+
*/
|
|
472
|
+
const recreate = async (spaceId, old, toolsVolume) => {
|
|
473
|
+
const name = spaceResourceName(spaceId, ROLE_SPACE);
|
|
474
|
+
const aside = spaceResourceName(spaceId, ROLE_SPACE, ASIDE_SUFFIX);
|
|
475
|
+
const labels = buildSpaceLabels({ ...parseSpaceLabels(old.Config?.Labels), role: ROLE_SPACE, owner });
|
|
476
|
+
await docker(['rename', name, aside], CHANGE_TIMEOUT_MS);
|
|
477
|
+
let createdId = '';
|
|
478
|
+
try {
|
|
479
|
+
createdId = (await docker(buildSpaceCreateArgs({
|
|
480
|
+
spaceId,
|
|
481
|
+
containerName: name,
|
|
482
|
+
labelArguments: labelArgs(labels),
|
|
483
|
+
network: spaceResourceName(spaceId, ROLE_NETWORK),
|
|
484
|
+
workVolume: spaceResourceName(spaceId, ROLE_VOLUME, 'work'),
|
|
485
|
+
homeVolume: spaceResourceName(spaceId, ROLE_VOLUME, 'home'),
|
|
486
|
+
toolsVolume,
|
|
487
|
+
memoryBytes: old.HostConfig?.Memory,
|
|
488
|
+
image: SPACE_BASE_IMAGE,
|
|
489
|
+
}), CHANGE_TIMEOUT_MS)).trim().split('\n').pop();
|
|
490
|
+
await requireVerified(spaceId, await inspectOwnContainer(spaceId, createdId) ?? {});
|
|
491
|
+
await docker(['start', createdId], CHANGE_TIMEOUT_MS);
|
|
492
|
+
await server.waitUntilReady(spaceId);
|
|
493
|
+
} catch (error) {
|
|
494
|
+
// Back to the old container. It still has the old tools, and it still works.
|
|
495
|
+
// When the create itself failed there is no id, and nothing is removed.
|
|
496
|
+
const rollbackFailures = [];
|
|
497
|
+
const problem = createdId ? await removeOne('container', createdId) : null;
|
|
498
|
+
if (problem) rollbackFailures.push({ ...problem, name });
|
|
499
|
+
try {
|
|
500
|
+
await docker(['rename', aside, name], CHANGE_TIMEOUT_MS);
|
|
501
|
+
} catch (renameError) {
|
|
502
|
+
// Fine when the old container already has its name back.
|
|
503
|
+
const current = await inspect('container', name).catch(() => null);
|
|
504
|
+
if (current?.Id !== old.Id) rollbackFailures.push({ kind: 'container', name: aside, message: renameError.message });
|
|
505
|
+
}
|
|
506
|
+
rollbackFailures.push(...(await tools.removeIfUnlabelled(toolsVolume)));
|
|
507
|
+
const outcome = rollbackFailures.length === 0
|
|
508
|
+
? 'The space keeps its old container.'
|
|
509
|
+
: `Putting the old container back failed for: ${rollbackFailures.map((item) => `${item.kind} ${item.name}`).join(', ')}. Start the space again to repair it.`;
|
|
510
|
+
const wrapped = new SpaceError(
|
|
511
|
+
error.code ?? 'space_recreate_failed',
|
|
512
|
+
`Could not move the space to the current tools. ${error.message} ${outcome}`,
|
|
513
|
+
{ original: error.details ?? null, rollbackFailures },
|
|
514
|
+
);
|
|
515
|
+
wrapped.cause = error;
|
|
516
|
+
throw wrapped;
|
|
517
|
+
}
|
|
518
|
+
// A leftover here loses nothing. `remove` removes it. The next start of the stopped space takes the
|
|
519
|
+
// container with the plain name for unfinished, goes back to this old one, and moves the space again.
|
|
520
|
+
await removeOne('container', aside);
|
|
521
|
+
await tools.prune();
|
|
522
|
+
};
|
|
523
|
+
|
|
524
|
+
const startOnce = async (spaceId) => {
|
|
525
|
+
const name = spaceResourceName(spaceId, ROLE_SPACE);
|
|
526
|
+
const aside = spaceResourceName(spaceId, ROLE_SPACE, ASIDE_SUFFIX);
|
|
527
|
+
let container = await inspectOwnContainer(spaceId, name);
|
|
528
|
+
const waiting = await inspectOwnContainer(spaceId, aside);
|
|
529
|
+
if (!container && !waiting) {
|
|
530
|
+
throw new SpaceError('space_not_found', `Space ${spaceId} has no container in Docker`);
|
|
531
|
+
}
|
|
532
|
+
if (container?.State?.Running === true) {
|
|
533
|
+
// The space runs, so its own container and its tools are left alone. Its way out is not
|
|
534
|
+
// the space's container: a gatekeeper that died, from its own memory limit or anything
|
|
535
|
+
// else, is brought back here. Otherwise the one action a user would take, "start", would
|
|
536
|
+
// skip the only step that repairs it.
|
|
537
|
+
await startGatekeeper(spaceId);
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
if (waiting) {
|
|
541
|
+
// A move that died half way. A stopped container with the plain name is the unfinished new one.
|
|
542
|
+
// It goes by the id that was just inspected, and without force: if another process started it
|
|
543
|
+
// in the meantime, Docker refuses, and a running container is a space that is already started.
|
|
544
|
+
const problem = container ? await removeStoppedContainer(container.Id) : null;
|
|
545
|
+
if (problem) {
|
|
546
|
+
if ((await inspectOwnContainer(spaceId, name))?.State?.Running === true) {
|
|
547
|
+
await startGatekeeper(spaceId);
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
throw new SpaceError('space_recreate_failed', `Could not remove the unfinished container ${name}: ${problem.message}`);
|
|
551
|
+
}
|
|
552
|
+
await docker(['rename', aside, name], CHANGE_TIMEOUT_MS);
|
|
553
|
+
container = await inspectOwnContainer(spaceId, name);
|
|
554
|
+
if (!container) {
|
|
555
|
+
throw new SpaceError('space_not_found', `Space ${spaceId} lost its container during the repair of a move`);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
await ensureImage();
|
|
559
|
+
const toolsVolume = await tools.ensure();
|
|
560
|
+
// Before either way of starting the space. The move to new tools leaves the gatekeeper
|
|
561
|
+
// alone: it mounts no tools volume, so a new tools version changes nothing about it.
|
|
562
|
+
await startGatekeeper(spaceId);
|
|
563
|
+
const mounted = (container?.Mounts ?? []).find((mount) => mount.Destination === TOOLS_MOUNT_PATH)?.Name;
|
|
564
|
+
if (mounted !== toolsVolume) {
|
|
565
|
+
await recreate(spaceId, container, toolsVolume);
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
await docker(['start', name], CHANGE_TIMEOUT_MS);
|
|
569
|
+
// The token is already in HOME, so the server comes up by itself.
|
|
570
|
+
await server.waitUntilReady(spaceId);
|
|
571
|
+
};
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* A running space is never touched. A stopped one picks up the current tools here, and a
|
|
575
|
+
* half-done move is repaired here and nowhere else. Calls for one space share one run in this
|
|
576
|
+
* process, so two starts never move the same container at once.
|
|
577
|
+
*/
|
|
578
|
+
const start = async (spaceId) => {
|
|
579
|
+
requireSpaceId(spaceId);
|
|
580
|
+
if (!starting.has(spaceId)) {
|
|
581
|
+
starting.set(spaceId, startOnce(spaceId).finally(() => { starting.delete(spaceId); }));
|
|
582
|
+
}
|
|
583
|
+
return starting.get(spaceId);
|
|
584
|
+
};
|
|
585
|
+
|
|
586
|
+
return { id: DOCKER_PLACE_ID, check, create, list, exec, execArgv, stop, start, remove, verify };
|
|
587
|
+
}
|