@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,368 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { findGatekeeperHardeningViolations, findHardeningViolations, requireMemoryBytes } from './hardening.js';
|
|
4
|
+
import { buildSpaceLabels, buildToolsLabels, hashProjectDirectory } from './labels.js';
|
|
5
|
+
import { bridgeNetworkEntry, hardenedContainerEntry, internalNetworkEntry } from './places/fake-docker.js';
|
|
6
|
+
|
|
7
|
+
const ID = 'a1b2c3d4e5f6';
|
|
8
|
+
const OWNER = 'install-a';
|
|
9
|
+
const NETWORK = `openchamber-space-${ID}-network`;
|
|
10
|
+
const WORK = `openchamber-space-${ID}-volume-work`;
|
|
11
|
+
const HOME = `openchamber-space-${ID}-volume-home`;
|
|
12
|
+
const GATEWAY_MODE = 'com.docker.network.bridge.gateway_mode_ipv4';
|
|
13
|
+
const KEY = '0123456789abcdef';
|
|
14
|
+
const TOOLS = `openchamber-tools-${OWNER}-${KEY}`;
|
|
15
|
+
const TOOLS_PATH = '/opt/openchamber-tools';
|
|
16
|
+
|
|
17
|
+
const labels = (role, change = {}) => buildSpaceLabels({
|
|
18
|
+
id: ID,
|
|
19
|
+
role,
|
|
20
|
+
owner: OWNER,
|
|
21
|
+
project: hashProjectDirectory('/home/me/project'),
|
|
22
|
+
name: 'Fix login',
|
|
23
|
+
created: '2026-09-19T10:00:00.000Z',
|
|
24
|
+
...change,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const toolsLabels = (change = {}) => buildToolsLabels({ role: 'tools', owner: OWNER, key: KEY, description: 'web 1.24.2, opencode 1.18.31', created: '2026-09-19T10:00:00.000Z', ...change });
|
|
28
|
+
|
|
29
|
+
const goodContainer = () => hardenedContainerEntry({ name: `openchamber-space-${ID}-space`, labels: labels('space'), network: NETWORK, volumes: [WORK, HOME], toolsVolume: TOOLS });
|
|
30
|
+
const goodNetwork = () => internalNetworkEntry({ name: NETWORK, labels: labels('network') });
|
|
31
|
+
const goodToolsVolume = () => ({ Name: TOOLS, Labels: toolsLabels() });
|
|
32
|
+
|
|
33
|
+
const withHost = (change) => {
|
|
34
|
+
const container = goodContainer();
|
|
35
|
+
return { ...container, HostConfig: { ...container.HostConfig, ...change } };
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const withMount = (mount) => {
|
|
39
|
+
const container = goodContainer();
|
|
40
|
+
return { ...container, Mounts: [...container.Mounts, mount] };
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const withLog = (change) => {
|
|
44
|
+
const log = goodContainer().HostConfig.LogConfig;
|
|
45
|
+
return withHost({ LogConfig: { ...log, ...change, Config: { ...log.Config, ...change.Config } } });
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/** The good container with its tools mount changed, or replaced by `mounts`. */
|
|
49
|
+
const withToolsMount = (change) => {
|
|
50
|
+
const container = goodContainer();
|
|
51
|
+
return { ...container, Mounts: container.Mounts.map((mount) => (mount.Destination === TOOLS_PATH ? { ...mount, ...change } : mount)) };
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const withEnv = (env) => {
|
|
55
|
+
const container = goodContainer();
|
|
56
|
+
return { ...container, Config: { ...container.Config, Env: [...container.Config.Env, ...env] } };
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const checksFor = ({ container = goodContainer(), network = goodNetwork(), toolsVolume = goodToolsVolume() }) => (
|
|
60
|
+
findHardeningViolations({ spaceId: ID, owner: OWNER, container, network, toolsVolume }).map((violation) => violation.check)
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
describe('findHardeningViolations', () => {
|
|
64
|
+
it('finds nothing in a container created with the hardening flags', () => {
|
|
65
|
+
expect(findHardeningViolations({ spaceId: ID, owner: OWNER, container: goodContainer(), network: goodNetwork(), toolsVolume: goodToolsVolume() })).toEqual([]);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('accepts the long spelling of no-new-privileges', () => {
|
|
69
|
+
expect(checksFor({ container: withHost({ SecurityOpt: ['no-new-privileges:true'] }) })).toEqual([]);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('accepts an engine that reports no runtime, no namespace fields and empty maps', () => {
|
|
73
|
+
const container = withHost({ Runtime: undefined, CgroupnsMode: undefined, UsernsMode: undefined, Sysctls: {}, VolumesFrom: [] });
|
|
74
|
+
expect(checksFor({ container })).toEqual([]);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it.each([
|
|
78
|
+
['user', { ...goodContainer(), Config: { User: '' } }],
|
|
79
|
+
['user', { ...goodContainer(), Config: { User: '0:0' } }],
|
|
80
|
+
['environment', withEnv(['OPENCHAMBER_UI_PASSWORD=secret'])],
|
|
81
|
+
['environment', withEnv(['OPENCODE_AUTH_CONTENT={}'])],
|
|
82
|
+
['environment', withEnv(['OPENCHAMBER_UI_PASSWORD='])],
|
|
83
|
+
['read_only', withHost({ ReadonlyRootfs: false })],
|
|
84
|
+
['privileged', withHost({ Privileged: true })],
|
|
85
|
+
['cap_drop', withHost({ CapDrop: ['NET_RAW'] })],
|
|
86
|
+
['cap_drop', withHost({ CapDrop: null })],
|
|
87
|
+
['cap_add', withHost({ CapAdd: ['SYS_ADMIN'] })],
|
|
88
|
+
['no_new_privileges', withHost({ SecurityOpt: null })],
|
|
89
|
+
['security_options', withHost({ SecurityOpt: ['no-new-privileges', 'seccomp=unconfined'] })],
|
|
90
|
+
['security_options', withHost({ SecurityOpt: ['no-new-privileges', 'apparmor=unconfined'] })],
|
|
91
|
+
['security_options', withHost({ SecurityOpt: ['no-new-privileges', 'label=disable'] })],
|
|
92
|
+
['init', withHost({ Init: null })],
|
|
93
|
+
['pids_limit', withHost({ PidsLimit: null })],
|
|
94
|
+
['pids_limit', withHost({ PidsLimit: 0 })],
|
|
95
|
+
['pids_limit', withHost({ PidsLimit: -1 })],
|
|
96
|
+
['memory_swap', withHost({ MemorySwap: -1 })],
|
|
97
|
+
['memory_swap', withHost({ MemorySwap: 8589934592 })],
|
|
98
|
+
['shm_size', withHost({ ShmSize: 1073741824 })],
|
|
99
|
+
['shm_size', withHost({ ShmSize: undefined })],
|
|
100
|
+
['log_limit', withLog({ Type: 'json-file' })],
|
|
101
|
+
['log_limit', withLog({ Config: { 'max-size': '1g' } })],
|
|
102
|
+
['log_limit', withLog({ Config: { 'max-file': '100' } })],
|
|
103
|
+
['log_limit', withHost({ LogConfig: { Type: 'local', Config: {} } })],
|
|
104
|
+
['tmpfs', withHost({ Tmpfs: null })],
|
|
105
|
+
['tmpfs', withHost({ Tmpfs: { '/tmp': 'rw,exec,nosuid,size=256m', '/run': 'rw' } })],
|
|
106
|
+
['tmpfs', withHost({ Tmpfs: { '/tmp': 'rw,exec,size=256m' } })],
|
|
107
|
+
['tmpfs', withHost({ Tmpfs: { '/tmp': 'rw,exec,nosuid' } })],
|
|
108
|
+
['tmpfs', withHost({ Tmpfs: { '/tmp': 'rw,exec,nosuid,size=64g' } })],
|
|
109
|
+
['binds', withHost({ Binds: ['/Users/me:/host'] })],
|
|
110
|
+
['volumes_from', withHost({ VolumesFrom: ['some-other-container'] })],
|
|
111
|
+
['mounts', withMount({ Type: 'bind', Source: '/Users/me', Destination: '/host' })],
|
|
112
|
+
['mounts', withMount({ Type: 'volume', Name: 'someone-elses-volume', Source: '/var/lib/docker/volumes/x/_data', Destination: '/x' })],
|
|
113
|
+
['mounts', withMount({ Type: 'tmpfs', Source: '', Destination: '/run' })],
|
|
114
|
+
['namespace', withHost({ PidMode: 'host' })],
|
|
115
|
+
['namespace', withHost({ PidMode: 'container:abc123' })],
|
|
116
|
+
['namespace', withHost({ IpcMode: 'host' })],
|
|
117
|
+
['namespace', withHost({ IpcMode: 'shareable' })],
|
|
118
|
+
['namespace', withHost({ UTSMode: 'host' })],
|
|
119
|
+
['namespace', withHost({ UsernsMode: 'host' })],
|
|
120
|
+
['namespace', withHost({ CgroupnsMode: 'host' })],
|
|
121
|
+
['sysctls', withHost({ Sysctls: { 'net.ipv4.ip_forward': '1' } })],
|
|
122
|
+
['runtime', withHost({ Runtime: 'sysbox-runc' })],
|
|
123
|
+
['devices', withHost({ Devices: [{ PathOnHost: '/dev/kvm', PathInContainer: '/dev/kvm' }] })],
|
|
124
|
+
['port_bindings', withHost({ PortBindings: { '3000/tcp': [{ HostIp: '127.0.0.1', HostPort: '3000' }] } })],
|
|
125
|
+
])('reports %s', (check, container) => {
|
|
126
|
+
expect(checksFor({ container })).toEqual([check]);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('reports a missing memory limit', () => {
|
|
130
|
+
expect(checksFor({ container: withHost({ Memory: 0, MemorySwap: 0 }) })).toEqual(['memory']);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('reports a missing no-new-privileges next to an extra security option', () => {
|
|
134
|
+
expect(checksFor({ container: withHost({ SecurityOpt: ['no-new-privileges:false'] }) })).toEqual(['no_new_privileges', 'security_options']);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('reports a mounted runtime socket by source and by destination', () => {
|
|
138
|
+
const bySource = withMount({ Type: 'volume', Name: `${WORK}-x`, Source: '/var/run/docker.sock', Destination: '/x' });
|
|
139
|
+
const byDestination = withMount({ Type: 'volume', Name: `${WORK}-x`, Source: '/x', Destination: '/run/docker.sock' });
|
|
140
|
+
|
|
141
|
+
expect(checksFor({ container: bySource })).toEqual(['runtime_socket']);
|
|
142
|
+
expect(checksFor({ container: byDestination })).toEqual(['runtime_socket']);
|
|
143
|
+
expect(checksFor({ container: withHost({ Binds: ['/var/run/docker.sock:/var/run/docker.sock'] }) })).toEqual(['binds', 'runtime_socket']);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('accepts harmless variables next to the ones a space gets', () => {
|
|
147
|
+
expect(checksFor({ container: withEnv(['OPENCODE_DISABLE_AUTOUPDATE=1', 'NOT_OPENCHAMBER_UI_PASSWORD=x']) })).toEqual([]);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe('tools mount', () => {
|
|
151
|
+
it('reports a writable tools mount', () => {
|
|
152
|
+
expect(checksFor({ container: withToolsMount({ RW: true }) })).toEqual(['tools_read_only']);
|
|
153
|
+
expect(checksFor({ container: withToolsMount({ RW: undefined }) })).toEqual(['tools_read_only']);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('reports a space without a tools mount', () => {
|
|
157
|
+
const container = goodContainer();
|
|
158
|
+
const withoutTools = { ...container, Mounts: container.Mounts.filter((mount) => mount.Destination !== TOOLS_PATH) };
|
|
159
|
+
expect(checksFor({ container: withoutTools, toolsVolume: null })).toEqual(['tools_mount']);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('gives a tools volume at another destination no exception, read-only or not', () => {
|
|
163
|
+
expect(checksFor({ container: withToolsMount({ Destination: '/opt/other' }) })).toEqual(['mounts', 'tools_mount']);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('reports a second tools mount', () => {
|
|
167
|
+
const second = { Type: 'volume', Name: `openchamber-tools-${OWNER}-ffffffffffffffff`, Source: '/var/lib/docker/volumes/x/_data', Destination: '/opt/more-tools', RW: false };
|
|
168
|
+
expect(checksFor({ container: withMount(second) })).toEqual(['mounts']);
|
|
169
|
+
// Docker allows one mount per destination. An inspect result that shows two is not a space of ours.
|
|
170
|
+
expect(checksFor({ container: withMount({ ...second, Destination: TOOLS_PATH }) })).toEqual(['tools_mount']);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('reports the tools volume of another installation', () => {
|
|
174
|
+
const name = `openchamber-tools-install-b-${KEY}`;
|
|
175
|
+
const toolsVolume = { Name: name, Labels: toolsLabels({ owner: 'install-b' }) };
|
|
176
|
+
expect(checksFor({ container: withToolsMount({ Name: name }), toolsVolume })).toEqual(['tools_mount', 'tools_labels']);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('reports a bind mount or a volume of this space at the tools path', () => {
|
|
180
|
+
expect(checksFor({ container: withToolsMount({ Type: 'bind', Name: undefined, Source: '/Users/me/tools' }), toolsVolume: null })).toEqual(['tools_mount', 'tools_labels']);
|
|
181
|
+
expect(checksFor({ container: withToolsMount({ Name: `${WORK}-tools` }), toolsVolume: { Name: `${WORK}-tools`, Labels: labels('volume') } })).toEqual(['tools_mount', 'tools_labels']);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it.each([
|
|
185
|
+
['no labels', null],
|
|
186
|
+
['the labels of another installation', toolsLabels({ owner: 'install-b' })],
|
|
187
|
+
['another key than its name says', toolsLabels({ key: 'ffffffffffffffff' })],
|
|
188
|
+
['the role of a filler', toolsLabels({ role: 'tools-fill' })],
|
|
189
|
+
['the labels of a space volume', labels('volume')],
|
|
190
|
+
])('reports a volume with a matching name and %s', (title, volumeLabels) => {
|
|
191
|
+
expect(checksFor({ toolsVolume: { Name: TOOLS, Labels: volumeLabels } })).toEqual(['tools_labels']);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('reports a tools volume that was not inspected, or another volume than the mounted one', () => {
|
|
195
|
+
expect(checksFor({ toolsVolume: null })).toEqual(['tools_labels']);
|
|
196
|
+
expect(checksFor({ toolsVolume: { Name: `openchamber-tools-${OWNER}-ffffffffffffffff`, Labels: toolsLabels({ key: 'ffffffffffffffff' }) } })).toEqual(['tools_labels']);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('still reports a read-only mount that is not the tools mount', () => {
|
|
200
|
+
const mount = { Type: 'volume', Name: 'someone-elses-volume', Source: '/var/lib/docker/volumes/x/_data', Destination: '/x', RW: false };
|
|
201
|
+
expect(checksFor({ container: withMount(mount) })).toEqual(['mounts']);
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it.each(['host', 'bridge', 'none', 'container:abc123', `${NETWORK}-other`])('reports NetworkMode %s', (mode) => {
|
|
206
|
+
expect(checksFor({ container: withHost({ NetworkMode: mode }) })).toEqual(['network_mode']);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('reports a second network, another network, and no network', () => {
|
|
210
|
+
const attachedTo = (networks) => ({ ...goodContainer(), NetworkSettings: { Networks: networks } });
|
|
211
|
+
|
|
212
|
+
expect(checksFor({ container: attachedTo({ [NETWORK]: {}, bridge: {} }) })).toEqual(['networks']);
|
|
213
|
+
expect(checksFor({ container: attachedTo({ bridge: {} }) })).toEqual(['networks']);
|
|
214
|
+
expect(checksFor({ container: attachedTo({}) })).toEqual(['networks']);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('reports a network that is not internal', () => {
|
|
218
|
+
expect(checksFor({ network: { ...goodNetwork(), Internal: false } })).toEqual(['network_internal']);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('reports a network with IPv6', () => {
|
|
222
|
+
expect(checksFor({ network: { ...goodNetwork(), EnableIPv6: true } })).toEqual(['network_ipv6']);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('reports a network without the isolated gateway option', () => {
|
|
226
|
+
expect(checksFor({ network: { ...goodNetwork(), Options: {} } })).toEqual(['network_host_isolation']);
|
|
227
|
+
expect(checksFor({ network: { ...goodNetwork(), Options: { [GATEWAY_MODE]: 'nat' } } })).toEqual(['network_host_isolation']);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('reports an engine that took the option but still gave the bridge a gateway address', () => {
|
|
231
|
+
const network = { ...goodNetwork(), IPAM: { Config: [{ Subnet: '172.19.0.0/16', Gateway: '172.19.0.1' }] } };
|
|
232
|
+
expect(checksFor({ network })).toEqual(['network_host_isolation']);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it.each([
|
|
236
|
+
['no labels', null],
|
|
237
|
+
['the labels of another space', labels('network', { id: 'ffffffffffff' })],
|
|
238
|
+
['the labels of another installation', labels('network', { owner: 'install-b' })],
|
|
239
|
+
['a different role', labels('volume')],
|
|
240
|
+
])('reports a network with %s', (title, networkLabels) => {
|
|
241
|
+
expect(checksFor({ network: { ...goodNetwork(), Labels: networkLabels } })).toEqual(['network_labels']);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('reports everything for an empty inspect result', () => {
|
|
245
|
+
expect(checksFor({ container: {}, network: null })).toEqual([
|
|
246
|
+
'user', 'read_only', 'privileged', 'cap_drop', 'no_new_privileges', 'init', 'pids_limit', 'memory',
|
|
247
|
+
'shm_size', 'log_limit', 'tmpfs', 'tools_mount', 'network_mode',
|
|
248
|
+
'networks', 'network_internal', 'network_host_isolation', 'network_labels',
|
|
249
|
+
]);
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
describe('findGatekeeperHardeningViolations', () => {
|
|
254
|
+
const OUTER = `openchamber-space-${ID}-outer-network`;
|
|
255
|
+
const GATEKEEPER_MEMORY = 268435456;
|
|
256
|
+
|
|
257
|
+
const goodGatekeeper = () => {
|
|
258
|
+
const container = hardenedContainerEntry({
|
|
259
|
+
name: `openchamber-space-${ID}-gatekeeper`,
|
|
260
|
+
labels: labels('gatekeeper'),
|
|
261
|
+
network: NETWORK,
|
|
262
|
+
mounts: [],
|
|
263
|
+
env: ['HOME=/tmp'],
|
|
264
|
+
memoryBytes: GATEKEEPER_MEMORY,
|
|
265
|
+
tmpfs: '/tmp:rw,noexec,nosuid,size=16m',
|
|
266
|
+
aliases: ['gatekeeper'],
|
|
267
|
+
});
|
|
268
|
+
container.NetworkSettings.Networks[OUTER] = {};
|
|
269
|
+
return container;
|
|
270
|
+
};
|
|
271
|
+
const goodOuterNetwork = () => bridgeNetworkEntry({ name: OUTER, labels: labels('outer-network') });
|
|
272
|
+
|
|
273
|
+
const gatekeeperChecks = ({ container = goodGatekeeper(), outerNetwork = goodOuterNetwork() }) => (
|
|
274
|
+
findGatekeeperHardeningViolations({ spaceId: ID, owner: OWNER, container, outerNetwork }).map((violation) => violation.check)
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
const withGatekeeperHost = (change) => {
|
|
278
|
+
const container = goodGatekeeper();
|
|
279
|
+
return { ...container, HostConfig: { ...container.HostConfig, ...change } };
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
const attachedTo = (networks) => ({ ...goodGatekeeper(), NetworkSettings: { Networks: networks } });
|
|
283
|
+
|
|
284
|
+
it('finds nothing in a gatekeeper created with its flags', () => {
|
|
285
|
+
expect(findGatekeeperHardeningViolations({ spaceId: ID, owner: OWNER, container: goodGatekeeper(), outerNetwork: goodOuterNetwork() })).toEqual([]);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it('names every check and every message as the gatekeeper\'s, so a caller can tell the two containers apart', () => {
|
|
289
|
+
const violations = findGatekeeperHardeningViolations({ spaceId: ID, owner: OWNER, container: {}, outerNetwork: null });
|
|
290
|
+
for (const violation of violations) {
|
|
291
|
+
expect(violation.check).toMatch(/^gatekeeper_/);
|
|
292
|
+
expect(violation.message).toMatch(/^Gatekeeper: /);
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it.each([
|
|
297
|
+
['gatekeeper_environment', { ...goodGatekeeper(), Config: { ...goodGatekeeper().Config, Env: ['HOME=/tmp', 'OPENCHAMBER_UI_PASSWORD=secret'] } }],
|
|
298
|
+
['gatekeeper_user', { ...goodGatekeeper(), Config: { ...goodGatekeeper().Config, User: '0:0' } }],
|
|
299
|
+
['gatekeeper_read_only', withGatekeeperHost({ ReadonlyRootfs: false })],
|
|
300
|
+
['gatekeeper_privileged', withGatekeeperHost({ Privileged: true })],
|
|
301
|
+
['gatekeeper_cap_drop', withGatekeeperHost({ CapDrop: [] })],
|
|
302
|
+
['gatekeeper_cap_add', withGatekeeperHost({ CapAdd: ['NET_ADMIN'] })],
|
|
303
|
+
['gatekeeper_no_new_privileges', withGatekeeperHost({ SecurityOpt: [] })],
|
|
304
|
+
['gatekeeper_security_options', withGatekeeperHost({ SecurityOpt: ['no-new-privileges', 'seccomp=unconfined'] })],
|
|
305
|
+
['gatekeeper_init', withGatekeeperHost({ Init: false })],
|
|
306
|
+
['gatekeeper_pids_limit', withGatekeeperHost({ PidsLimit: 0 })],
|
|
307
|
+
['gatekeeper_memory', withGatekeeperHost({ Memory: 0, MemorySwap: 0 })],
|
|
308
|
+
['gatekeeper_memory_swap', withGatekeeperHost({ MemorySwap: -1 })],
|
|
309
|
+
['gatekeeper_shm_size', withGatekeeperHost({ ShmSize: 1073741824 })],
|
|
310
|
+
['gatekeeper_log_limit', withGatekeeperHost({ LogConfig: { Type: 'json-file', Config: {} } })],
|
|
311
|
+
// The space's tmpfs options are not the gatekeeper's: it needs no `exec` and 16 MiB.
|
|
312
|
+
['gatekeeper_tmpfs', withGatekeeperHost({ Tmpfs: { '/tmp': 'rw,exec,nosuid,size=256m' } })],
|
|
313
|
+
['gatekeeper_binds', withGatekeeperHost({ Binds: ['/Users/me:/host'] })],
|
|
314
|
+
['gatekeeper_volumes_from', withGatekeeperHost({ VolumesFrom: ['another'] })],
|
|
315
|
+
['gatekeeper_namespace', withGatekeeperHost({ PidMode: 'host' })],
|
|
316
|
+
['gatekeeper_sysctls', withGatekeeperHost({ Sysctls: { 'net.ipv4.ip_forward': '1' } })],
|
|
317
|
+
['gatekeeper_runtime', withGatekeeperHost({ Runtime: 'sysbox-runc' })],
|
|
318
|
+
['gatekeeper_devices', withGatekeeperHost({ Devices: [{ PathOnHost: '/dev/kvm' }] })],
|
|
319
|
+
['gatekeeper_port_bindings', withGatekeeperHost({ PortBindings: { '3128/tcp': [{ HostPort: '3128' }] } })],
|
|
320
|
+
['gatekeeper_network_mode', withGatekeeperHost({ NetworkMode: 'bridge' })],
|
|
321
|
+
['gatekeeper_memory_limit', withGatekeeperHost({ Memory: 4294967296, MemorySwap: 4294967296 })],
|
|
322
|
+
])('reports %s', (check, container) => {
|
|
323
|
+
expect(gatekeeperChecks({ container })).toEqual([check]);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it('reports any mount at all, because the gatekeeper has none', () => {
|
|
327
|
+
const container = goodGatekeeper();
|
|
328
|
+
const mounted = { ...container, Mounts: [{ Type: 'volume', Name: TOOLS, Source: '/x', Destination: TOOLS_PATH, RW: false }] };
|
|
329
|
+
expect(gatekeeperChecks({ container: mounted })).toEqual(['gatekeeper_mounts']);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it('reports a mounted runtime socket', () => {
|
|
333
|
+
const container = goodGatekeeper();
|
|
334
|
+
const mounted = { ...container, Mounts: [{ Type: 'bind', Source: '/var/run/docker.sock', Destination: '/var/run/docker.sock' }] };
|
|
335
|
+
expect(gatekeeperChecks({ container: mounted })).toEqual(['gatekeeper_mounts', 'gatekeeper_runtime_socket']);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it('demands exactly the two networks of this space, by name', () => {
|
|
339
|
+
expect(gatekeeperChecks({ container: attachedTo({ [NETWORK]: { Aliases: ['gatekeeper'] } }) })).toEqual(['gatekeeper_networks']);
|
|
340
|
+
expect(gatekeeperChecks({ container: attachedTo({ [NETWORK]: { Aliases: ['gatekeeper'] }, [OUTER]: {}, bridge: {} }) })).toEqual(['gatekeeper_networks']);
|
|
341
|
+
expect(gatekeeperChecks({ container: attachedTo({ [NETWORK]: { Aliases: ['gatekeeper'] }, 'openchamber-space-ffffffffffff-outer-network': {} }) })).toEqual(['gatekeeper_networks']);
|
|
342
|
+
expect(gatekeeperChecks({ container: attachedTo({}) })).toEqual(['gatekeeper_networks']);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it('reports a gatekeeper the space cannot reach by name', () => {
|
|
346
|
+
expect(gatekeeperChecks({ container: attachedTo({ [NETWORK]: {}, [OUTER]: {} }) })).toEqual(['gatekeeper_alias']);
|
|
347
|
+
expect(gatekeeperChecks({ container: attachedTo({ [NETWORK]: { Aliases: ['something-else'] }, [OUTER]: {} }) })).toEqual(['gatekeeper_alias']);
|
|
348
|
+
// Newer engines report the same thing under DNSNames.
|
|
349
|
+
expect(gatekeeperChecks({ container: attachedTo({ [NETWORK]: { DNSNames: ['gatekeeper', 'abc123'] }, [OUTER]: {} }) })).toEqual([]);
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
it('reports an outer network that cannot reach outside, or that is not ours', () => {
|
|
353
|
+
expect(gatekeeperChecks({ outerNetwork: { ...goodOuterNetwork(), Internal: true } })).toEqual(['gatekeeper_outer_network']);
|
|
354
|
+
expect(gatekeeperChecks({ outerNetwork: null })).toEqual(['gatekeeper_outer_network_labels']);
|
|
355
|
+
expect(gatekeeperChecks({ outerNetwork: { ...goodOuterNetwork(), Labels: labels('network') } })).toEqual(['gatekeeper_outer_network_labels']);
|
|
356
|
+
expect(gatekeeperChecks({ outerNetwork: { ...goodOuterNetwork(), Labels: labels('outer-network', { owner: 'install-b' }) } })).toEqual(['gatekeeper_outer_network_labels']);
|
|
357
|
+
});
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
describe('requireMemoryBytes', () => {
|
|
361
|
+
it('accepts a whole number of bytes from 64 MiB up', () => {
|
|
362
|
+
expect(requireMemoryBytes(64 * 1024 * 1024)).toBe(67108864);
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
it.each([0, -1, 1024, 1.5 * 1024 * 1024 * 1024 + 0.5, Number.NaN, undefined, null, '4g'])('rejects %j', (value) => {
|
|
366
|
+
expect(() => requireMemoryBytes(value)).toThrow(expect.objectContaining({ code: 'invalid_memory_limit' }));
|
|
367
|
+
});
|
|
368
|
+
});
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// How the host runs git against the user's repository. Every host git call of the module goes
|
|
2
|
+
// through here, so the environment rule lives in one place.
|
|
3
|
+
//
|
|
4
|
+
// The user's normal config applies, on purpose: their global ignore file must keep its files out
|
|
5
|
+
// of a space. What must not apply is anything inherited that points git at another repository,
|
|
6
|
+
// index or object store, so those variables never reach our git.
|
|
7
|
+
|
|
8
|
+
import { SpaceError } from './errors.js';
|
|
9
|
+
import { tail } from './exec-http.js';
|
|
10
|
+
|
|
11
|
+
const QUERY_TIMEOUT_MS = 2 * 60_000;
|
|
12
|
+
const QUERY_MAX_OUTPUT_BYTES = 4 * 1024 * 1024;
|
|
13
|
+
|
|
14
|
+
// Inherited from a parent process, each of these would make our commands read or write another
|
|
15
|
+
// repository, index, object store or ref namespace than the one we name with `-C`, or would add
|
|
16
|
+
// config the user did not write. `GIT_CONFIG_PARAMETERS` is how a parent git passes its `-c`
|
|
17
|
+
// options on. `GIT_ALLOW_PROTOCOL` would override the one protocol switch the transfer sets.
|
|
18
|
+
// The `GIT_TRACE` family writes to stderr, which a push counts against its output cap: an inherited
|
|
19
|
+
// `GIT_TRACE_PACKET` would turn every push into a failure.
|
|
20
|
+
const REMOVED_VARIABLES = new Set([
|
|
21
|
+
'GIT_DIR',
|
|
22
|
+
'GIT_WORK_TREE',
|
|
23
|
+
'GIT_INDEX_FILE',
|
|
24
|
+
'GIT_OBJECT_DIRECTORY',
|
|
25
|
+
'GIT_ALTERNATE_OBJECT_DIRECTORIES',
|
|
26
|
+
'GIT_NAMESPACE',
|
|
27
|
+
'GIT_COMMON_DIR',
|
|
28
|
+
'GIT_PREFIX',
|
|
29
|
+
'GIT_SHALLOW_FILE',
|
|
30
|
+
'GIT_GRAFT_FILE',
|
|
31
|
+
'GIT_REPLACE_REF_BASE',
|
|
32
|
+
'GIT_NO_REPLACE_OBJECTS',
|
|
33
|
+
'GIT_QUARANTINE_PATH',
|
|
34
|
+
'GIT_INTERNAL_SUPER_PREFIX',
|
|
35
|
+
'GIT_DEFAULT_HASH',
|
|
36
|
+
'GIT_DEFAULT_REF_FORMAT',
|
|
37
|
+
'GIT_CONFIG_PARAMETERS',
|
|
38
|
+
'GIT_CONFIG_COUNT',
|
|
39
|
+
'GIT_ALLOW_PROTOCOL',
|
|
40
|
+
// These change how every pathspec is read. With GIT_LITERAL_PATHSPECS, `:(exclude,literal)<dir>`
|
|
41
|
+
// would be a path, `add` would fail, and `:(attr:filter=lfs)` would match nothing.
|
|
42
|
+
'GIT_LITERAL_PATHSPECS',
|
|
43
|
+
'GIT_GLOB_PATHSPECS',
|
|
44
|
+
'GIT_NOGLOB_PATHSPECS',
|
|
45
|
+
'GIT_ICASE_PATHSPECS',
|
|
46
|
+
'GIT_PROTOCOL_FROM_USER',
|
|
47
|
+
'GIT_AUTHOR_NAME',
|
|
48
|
+
'GIT_AUTHOR_EMAIL',
|
|
49
|
+
'GIT_AUTHOR_DATE',
|
|
50
|
+
'GIT_COMMITTER_NAME',
|
|
51
|
+
'GIT_COMMITTER_EMAIL',
|
|
52
|
+
'GIT_COMMITTER_DATE',
|
|
53
|
+
]);
|
|
54
|
+
const REMOVED_PATTERN = /^(?:GIT_CONFIG_(?:KEY|VALUE)_\d+|GIT_TRACE.*)$/;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The environment of every host git call: the given one without the variables above, plus
|
|
58
|
+
* `GIT_OPTIONAL_LOCKS=0`, so a status read never rewrites the index, and `GIT_TERMINAL_PROMPT=0`,
|
|
59
|
+
* so nothing waits for a password. Variable names are compared without case on Windows, where
|
|
60
|
+
* the system treats `git_dir` as `GIT_DIR`.
|
|
61
|
+
*/
|
|
62
|
+
export function hostGitEnvironment(environment, platform = process.platform) {
|
|
63
|
+
const normalize = platform === 'win32' ? (name) => name.toUpperCase() : (name) => name;
|
|
64
|
+
const kept = Object.entries(environment).filter(([name]) => {
|
|
65
|
+
const key = normalize(name);
|
|
66
|
+
return !REMOVED_VARIABLES.has(key) && !REMOVED_PATTERN.test(key);
|
|
67
|
+
});
|
|
68
|
+
return { ...Object.fromEntries(kept), GIT_OPTIONAL_LOCKS: '0', GIT_TERMINAL_PROMPT: '0' };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* `{ major, minor, patch }` from the output of `git version`, or null. Builds add their own suffix,
|
|
73
|
+
* such as `2.50.1 (Apple Git-155)` or `2.54.0.windows.1`, and only the first three numbers count.
|
|
74
|
+
*/
|
|
75
|
+
export function parseGitVersion(text) {
|
|
76
|
+
const match = /^git version (\d+)\.(\d+)(?:\.(\d+))?/.exec(String(text ?? '').trim());
|
|
77
|
+
if (!match) return null;
|
|
78
|
+
return { major: Number(match[1]), minor: Number(match[2]), patch: Number(match[3] ?? 0) };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Whether `version` is `floor` or newer. Both are `{ major, minor, patch }`. */
|
|
82
|
+
export const isGitVersionAtLeast = (version, floor) => (
|
|
83
|
+
version.major !== floor.major ? version.major > floor.major
|
|
84
|
+
: version.minor !== floor.minor ? version.minor > floor.minor
|
|
85
|
+
: version.patch >= floor.patch
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
// The git subcommand in an argv, for messages: the first argument that is not an option or the value of `-c`.
|
|
89
|
+
const subcommandOf = (args) => {
|
|
90
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
91
|
+
if (args[index] === '-c') {
|
|
92
|
+
index += 1;
|
|
93
|
+
} else if (!args[index].startsWith('-')) {
|
|
94
|
+
return args[index];
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return '';
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* `run(directory, args, options)` resolves `{ code, stdout, stderr }` for any exit code, and
|
|
102
|
+
* `output(directory, args, options)` resolves stdout and rejects with `git_command_failed` for a
|
|
103
|
+
* non-zero one. Both run `git -C <directory> ...` through `runCommand`, with an argv and no shell.
|
|
104
|
+
* `options.env` adds variables for one call, such as `GIT_INDEX_FILE`. `options.killTree` goes to
|
|
105
|
+
* `runCommand` unchanged.
|
|
106
|
+
*
|
|
107
|
+
* `gitPath` is the git program, `environment` the environment to start from.
|
|
108
|
+
*/
|
|
109
|
+
export function createHostGit({ runCommand, gitPath = 'git', environment = process.env }) {
|
|
110
|
+
const baseEnvironment = hostGitEnvironment(environment);
|
|
111
|
+
|
|
112
|
+
const run = (directory, args, { env = {}, stdin, timeoutMs = QUERY_TIMEOUT_MS, maxOutputBytes = QUERY_MAX_OUTPUT_BYTES, killTree = false } = {}) => runCommand(
|
|
113
|
+
gitPath,
|
|
114
|
+
['-C', directory, ...args],
|
|
115
|
+
{ env: { ...baseEnvironment, ...env }, stdin, timeoutMs, maxOutputBytes, killTree },
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
const output = async (directory, args, options) => {
|
|
119
|
+
const result = await run(directory, args, options);
|
|
120
|
+
if (result.code !== 0) {
|
|
121
|
+
throw new SpaceError('git_command_failed', `git ${subcommandOf(args)} failed: ${tail(result.stderr) || `exit code ${result.code}`}`, { exitCode: result.code });
|
|
122
|
+
}
|
|
123
|
+
return result.stdout;
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
return { run, output };
|
|
127
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { createHostGit, hostGitEnvironment, isGitVersionAtLeast, parseGitVersion } from './host-git.js';
|
|
4
|
+
|
|
5
|
+
// Written out here rather than imported, so that a name dropped from the module's list turns this red.
|
|
6
|
+
const REMOVED = [
|
|
7
|
+
'GIT_DIR', 'GIT_WORK_TREE', 'GIT_INDEX_FILE', 'GIT_OBJECT_DIRECTORY', 'GIT_ALTERNATE_OBJECT_DIRECTORIES',
|
|
8
|
+
'GIT_NAMESPACE', 'GIT_COMMON_DIR', 'GIT_PREFIX', 'GIT_SHALLOW_FILE', 'GIT_GRAFT_FILE', 'GIT_REPLACE_REF_BASE',
|
|
9
|
+
'GIT_NO_REPLACE_OBJECTS', 'GIT_QUARANTINE_PATH', 'GIT_INTERNAL_SUPER_PREFIX', 'GIT_DEFAULT_HASH', 'GIT_DEFAULT_REF_FORMAT',
|
|
10
|
+
'GIT_CONFIG_PARAMETERS', 'GIT_CONFIG_COUNT', 'GIT_ALLOW_PROTOCOL', 'GIT_PROTOCOL_FROM_USER',
|
|
11
|
+
'GIT_AUTHOR_NAME', 'GIT_AUTHOR_EMAIL', 'GIT_AUTHOR_DATE', 'GIT_COMMITTER_NAME', 'GIT_COMMITTER_EMAIL', 'GIT_COMMITTER_DATE',
|
|
12
|
+
'GIT_CONFIG_KEY_0', 'GIT_CONFIG_VALUE_0', 'GIT_CONFIG_KEY_12', 'GIT_CONFIG_VALUE_12',
|
|
13
|
+
'GIT_TRACE', 'GIT_TRACE_PACKET', 'GIT_TRACE_PACK_ACCESS', 'GIT_TRACE_PERFORMANCE', 'GIT_TRACE_SETUP', 'GIT_TRACE_CURL',
|
|
14
|
+
'GIT_TRACE_SHALLOW', 'GIT_TRACE2', 'GIT_TRACE2_EVENT', 'GIT_TRACE2_PERF', 'GIT_TRACE2_CONFIG_PARAMS',
|
|
15
|
+
'GIT_LITERAL_PATHSPECS', 'GIT_GLOB_PATHSPECS', 'GIT_NOGLOB_PATHSPECS', 'GIT_ICASE_PATHSPECS',
|
|
16
|
+
];
|
|
17
|
+
const ADDED = { GIT_OPTIONAL_LOCKS: '0', GIT_TERMINAL_PROMPT: '0' };
|
|
18
|
+
const mixedCase = (name) => name.toLowerCase().replace(/^git/, 'Git');
|
|
19
|
+
|
|
20
|
+
describe('hostGitEnvironment, every removed variable', () => {
|
|
21
|
+
it.each(REMOVED)('removes %s and keeps the rest', (name) => {
|
|
22
|
+
expect(hostGitEnvironment({ PATH: '/usr/bin', [name]: 'x' }, 'linux')).toEqual({ PATH: '/usr/bin', ...ADDED });
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it.each(REMOVED)('removes %s in any case on Windows', (name) => {
|
|
26
|
+
for (const spelling of [name, name.toLowerCase(), mixedCase(name)]) {
|
|
27
|
+
expect(hostGitEnvironment({ Path: 'C:\\Windows', [spelling]: 'x' }, 'win32')).toEqual({ Path: 'C:\\Windows', ...ADDED });
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it.each(REMOVED)('keeps a lowercase %s on POSIX, where git does not read it', (name) => {
|
|
32
|
+
expect(hostGitEnvironment({ [name.toLowerCase()]: 'x' }, 'linux')).toEqual({ [name.toLowerCase()]: 'x', ...ADDED });
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// A denylist: whatever it does not name passes, the user's own settings among them.
|
|
36
|
+
// The docker CLI that git starts through `ext::` inherits this environment, so the Docker settings must pass.
|
|
37
|
+
it.each(['GIT_CONFIG_GLOBAL', 'GIT_CONFIG_NOSYSTEM', 'GIT_TEMPLATE_DIR', 'GIT_SSH_COMMAND', 'GIT_EXEC_PATH', 'GIT_TRACING', 'HOME',
|
|
38
|
+
'DOCKER_HOST', 'DOCKER_CONTEXT', 'DOCKER_CONFIG', 'DOCKER_TLS_VERIFY', 'DOCKER_CERT_PATH', 'SSH_AUTH_SOCK'])('passes %s through', (name) => {
|
|
39
|
+
expect(hostGitEnvironment({ [name]: 'x' }, 'linux')).toEqual({ [name]: 'x', ...ADDED });
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe('hostGitEnvironment', () => {
|
|
44
|
+
it('drops what points git at another repository and keeps the user config', () => {
|
|
45
|
+
const environment = hostGitEnvironment({
|
|
46
|
+
PATH: '/usr/bin',
|
|
47
|
+
HOME: '/home/me',
|
|
48
|
+
GIT_CONFIG_GLOBAL: '/home/me/.gitconfig',
|
|
49
|
+
GIT_DIR: '/elsewhere/.git',
|
|
50
|
+
GIT_WORK_TREE: '/elsewhere',
|
|
51
|
+
GIT_INDEX_FILE: '/elsewhere/index',
|
|
52
|
+
GIT_OBJECT_DIRECTORY: '/elsewhere/objects',
|
|
53
|
+
GIT_ALTERNATE_OBJECT_DIRECTORIES: '/elsewhere/more',
|
|
54
|
+
GIT_NAMESPACE: 'other',
|
|
55
|
+
GIT_COMMON_DIR: '/elsewhere/common',
|
|
56
|
+
GIT_CONFIG_PARAMETERS: "'core.hookspath'='/tmp/hooks'",
|
|
57
|
+
GIT_CONFIG_COUNT: '1',
|
|
58
|
+
GIT_CONFIG_KEY_0: 'core.hooksPath',
|
|
59
|
+
GIT_CONFIG_VALUE_0: '/tmp/hooks',
|
|
60
|
+
GIT_ALLOW_PROTOCOL: 'file',
|
|
61
|
+
GIT_AUTHOR_NAME: 'Someone',
|
|
62
|
+
GIT_OPTIONAL_LOCKS: '1',
|
|
63
|
+
}, 'linux');
|
|
64
|
+
expect(environment).toEqual({
|
|
65
|
+
PATH: '/usr/bin',
|
|
66
|
+
HOME: '/home/me',
|
|
67
|
+
GIT_CONFIG_GLOBAL: '/home/me/.gitconfig',
|
|
68
|
+
GIT_OPTIONAL_LOCKS: '0',
|
|
69
|
+
GIT_TERMINAL_PROMPT: '0',
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('compares names without case on Windows, where the system does too', () => {
|
|
74
|
+
expect(hostGitEnvironment({ Git_Dir: 'C:\\elsewhere', Path: 'C:\\Windows' }, 'win32')).toEqual({ Path: 'C:\\Windows', GIT_OPTIONAL_LOCKS: '0', GIT_TERMINAL_PROMPT: '0' });
|
|
75
|
+
// On POSIX `git_dir` is another variable, and git does not read it.
|
|
76
|
+
expect(hostGitEnvironment({ git_dir: '/elsewhere' }, 'linux')).toMatchObject({ git_dir: '/elsewhere' });
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe('createHostGit', () => {
|
|
81
|
+
const recordingRunner = (result) => {
|
|
82
|
+
const calls = [];
|
|
83
|
+
return { calls, runCommand: async (file, args, options) => { calls.push({ file, args, options }); return result; } };
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
it('runs git -C with an argv, the cleaned environment and the call options', async () => {
|
|
87
|
+
const { calls, runCommand } = recordingRunner({ code: 0, stdout: 'out\n', stderr: '' });
|
|
88
|
+
const git = createHostGit({ runCommand, gitPath: '/opt/git/bin/git', environment: { PATH: '/usr/bin', GIT_DIR: '/elsewhere' } });
|
|
89
|
+
expect(await git.output('/repo with space', ['write-tree'], { env: { GIT_INDEX_FILE: '/tmp/index' }, killTree: true })).toBe('out\n');
|
|
90
|
+
expect(calls).toEqual([{
|
|
91
|
+
file: '/opt/git/bin/git',
|
|
92
|
+
args: ['-C', '/repo with space', 'write-tree'],
|
|
93
|
+
options: {
|
|
94
|
+
env: { PATH: '/usr/bin', GIT_OPTIONAL_LOCKS: '0', GIT_TERMINAL_PROMPT: '0', GIT_INDEX_FILE: '/tmp/index' },
|
|
95
|
+
stdin: undefined,
|
|
96
|
+
timeoutMs: 120_000,
|
|
97
|
+
maxOutputBytes: 4 * 1024 * 1024,
|
|
98
|
+
killTree: true,
|
|
99
|
+
},
|
|
100
|
+
}]);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('names the subcommand when git fails, past its -c options', async () => {
|
|
104
|
+
const { runCommand } = recordingRunner({ code: 128, stdout: '', stderr: 'fatal: bad object\n' });
|
|
105
|
+
const git = createHostGit({ runCommand, environment: {} });
|
|
106
|
+
await expect(git.output('/repo', ['-c', 'core.splitIndex=false', 'write-tree'])).rejects.toMatchObject({
|
|
107
|
+
code: 'git_command_failed',
|
|
108
|
+
message: 'git write-tree failed: fatal: bad object',
|
|
109
|
+
details: { exitCode: 128 },
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe('parseGitVersion', () => {
|
|
115
|
+
it.each([
|
|
116
|
+
['git version 2.50.1 (Apple Git-155)\n', { major: 2, minor: 50, patch: 1 }],
|
|
117
|
+
['git version 2.54.0.windows.1\n', { major: 2, minor: 54, patch: 0 }],
|
|
118
|
+
['git version 2.39.5\n', { major: 2, minor: 39, patch: 5 }],
|
|
119
|
+
['git version 2.27.0.rc2\n', { major: 2, minor: 27, patch: 0 }],
|
|
120
|
+
['git version 3.0\n', { major: 3, minor: 0, patch: 0 }],
|
|
121
|
+
])('reads %j', (text, version) => {
|
|
122
|
+
expect(parseGitVersion(text)).toEqual(version);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it.each(['', 'version 2.50.1', 'git version two', 'hub version 2.14.2\ngit version 2.50.1'])('reads nothing from %j', (text) => {
|
|
126
|
+
expect(parseGitVersion(text)).toBeNull();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('compares versions number by number', () => {
|
|
130
|
+
const floor = { major: 2, minor: 27, patch: 0 };
|
|
131
|
+
expect(isGitVersionAtLeast({ major: 2, minor: 27, patch: 0 }, floor)).toBe(true);
|
|
132
|
+
expect(isGitVersionAtLeast({ major: 2, minor: 100, patch: 0 }, floor)).toBe(true);
|
|
133
|
+
expect(isGitVersionAtLeast({ major: 3, minor: 0, patch: 0 }, floor)).toBe(true);
|
|
134
|
+
expect(isGitVersionAtLeast({ major: 2, minor: 26, patch: 9 }, floor)).toBe(false);
|
|
135
|
+
expect(isGitVersionAtLeast({ major: 1, minor: 99, patch: 99 }, floor)).toBe(false);
|
|
136
|
+
});
|
|
137
|
+
});
|