@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,457 @@
|
|
|
1
|
+
import { SpaceError } from './errors.js';
|
|
2
|
+
import {
|
|
3
|
+
ROLE_NETWORK,
|
|
4
|
+
ROLE_OUTER_NETWORK,
|
|
5
|
+
ROLE_TOOLS,
|
|
6
|
+
parseSpaceLabels,
|
|
7
|
+
parseToolsLabels,
|
|
8
|
+
spaceResourceName,
|
|
9
|
+
spaceResourcePrefix,
|
|
10
|
+
toolsKeyFromVolumeName,
|
|
11
|
+
} from './labels.js';
|
|
12
|
+
import {
|
|
13
|
+
GATEKEEPER_ALIAS,
|
|
14
|
+
GATEKEEPER_COMMAND,
|
|
15
|
+
GATEKEEPER_ENVIRONMENT,
|
|
16
|
+
IMAGE_CAT,
|
|
17
|
+
IMAGE_CHOWN,
|
|
18
|
+
IMAGE_NODE,
|
|
19
|
+
IMAGE_SH,
|
|
20
|
+
SPACE_ENVIRONMENT,
|
|
21
|
+
SPACE_HOME,
|
|
22
|
+
SPACE_SERVER_COMMAND,
|
|
23
|
+
SPACE_USER,
|
|
24
|
+
TOOLS_MARKER_PATH,
|
|
25
|
+
TOOLS_MOUNT_PATH,
|
|
26
|
+
spaceWorkPath,
|
|
27
|
+
} from './layout.js';
|
|
28
|
+
import { FILLER_PROGRAM } from './tools-filler.js';
|
|
29
|
+
import { TOOLS_STAGING_PATH } from './tools.js';
|
|
30
|
+
|
|
31
|
+
const SPACE_PIDS_LIMIT = 512;
|
|
32
|
+
const SPACE_SHM_BYTES = 64 * 1024 * 1024;
|
|
33
|
+
const TMPFS_OPTIONS = 'rw,exec,nosuid,size=256m';
|
|
34
|
+
// The gatekeeper's tmpfs holds one file, its own program, and nothing runs from it: Node reads
|
|
35
|
+
// the file. So it is small and without `exec`, where a space needs both.
|
|
36
|
+
const GATEKEEPER_TMPFS_OPTIONS = 'rw,noexec,nosuid,size=16m';
|
|
37
|
+
// Measured: an idle gatekeeper with its listeners holds about 24 MiB. This is ten times that.
|
|
38
|
+
// The tunnel cap is 64, and two sockets with their buffers cost well under a megabyte each, so
|
|
39
|
+
// a gatekeeper at its busiest stays far below the limit and an unexpected leak is still capped.
|
|
40
|
+
const GATEKEEPER_MEMORY_BYTES = 256 * 1024 * 1024;
|
|
41
|
+
const MIN_MEMORY_BYTES = 64 * 1024 * 1024;
|
|
42
|
+
const SETUP_MEMORY_BYTES = 128 * 1024 * 1024;
|
|
43
|
+
// The filler keeps the npm cache and the tarballs in its tmpfs, and a tmpfs counts as memory.
|
|
44
|
+
// Measured: the cache of one fill is 444 MB.
|
|
45
|
+
const FILLER_MEMORY_BYTES = 2 * 1024 * 1024 * 1024;
|
|
46
|
+
const FILLER_TMPFS_OPTIONS = 'rw,exec,nosuid,size=1g';
|
|
47
|
+
// Variables that would put a secret where `docker inspect` shows it.
|
|
48
|
+
const FORBIDDEN_ENVIRONMENT = ['OPENCHAMBER_UI_PASSWORD', 'OPENCODE_AUTH_CONTENT'];
|
|
49
|
+
|
|
50
|
+
// Container output lands in a file on the Docker host. One 10 MB file, no rotation copies.
|
|
51
|
+
// The `local` driver refuses max-file=1 unless compression is off.
|
|
52
|
+
const LOG_DRIVER = 'local';
|
|
53
|
+
const LOG_MAX_SIZE = '10m';
|
|
54
|
+
const LOG_MAX_FILE = '1';
|
|
55
|
+
const LOG_ARGS = ['--log-driver', LOG_DRIVER, '--log-opt', `max-size=${LOG_MAX_SIZE}`, '--log-opt', `max-file=${LOG_MAX_FILE}`, '--log-opt', 'compress=false'];
|
|
56
|
+
|
|
57
|
+
// With this option the bridge gets no address on the Docker host, so the space
|
|
58
|
+
// cannot reach services that listen on the host. Docker Engine 28 and newer.
|
|
59
|
+
const GATEWAY_MODE_OPTION = 'com.docker.network.bridge.gateway_mode_ipv4';
|
|
60
|
+
const GATEWAY_MODE = 'isolated';
|
|
61
|
+
|
|
62
|
+
const NO_NEW_PRIVILEGES = ['no-new-privileges', 'no-new-privileges:true'];
|
|
63
|
+
// What a correct container reports: '' for pid, uts and userns, 'private' for ipc and cgroupns.
|
|
64
|
+
const PRIVATE_NAMESPACE_MODES = ['', 'private'];
|
|
65
|
+
const NAMESPACE_FIELDS = ['PidMode', 'IpcMode', 'UTSMode', 'UsernsMode', 'CgroupnsMode'];
|
|
66
|
+
|
|
67
|
+
const volumeMount = (volume, destination) => ['--mount', `type=volume,src=${volume},dst=${destination}`];
|
|
68
|
+
const readOnlyVolumeMount = (volume, destination) => ['--mount', `type=volume,src=${volume},dst=${destination},readonly`];
|
|
69
|
+
|
|
70
|
+
// Equal values mean swap adds nothing on top of the limit.
|
|
71
|
+
const memoryArgs = (bytes) => ['--memory', String(bytes), '--memory-swap', String(bytes)];
|
|
72
|
+
|
|
73
|
+
export function requireMemoryBytes(value) {
|
|
74
|
+
if (!Number.isSafeInteger(value) || value < MIN_MEMORY_BYTES) {
|
|
75
|
+
throw new SpaceError('invalid_memory_limit', `A space needs a memory limit of at least ${MIN_MEMORY_BYTES} bytes`);
|
|
76
|
+
}
|
|
77
|
+
return value;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** The `docker network create` argv for the inner network of a space. Only the gatekeeper joins it too. */
|
|
81
|
+
export function buildSpaceNetworkArgs({ network, labelArguments }) {
|
|
82
|
+
return [
|
|
83
|
+
'network', 'create',
|
|
84
|
+
'--driver', 'bridge',
|
|
85
|
+
'--internal',
|
|
86
|
+
'--ipv6=false',
|
|
87
|
+
'--opt', `${GATEWAY_MODE_OPTION}=${GATEWAY_MODE}`,
|
|
88
|
+
...labelArguments,
|
|
89
|
+
network,
|
|
90
|
+
];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* The `docker network create` argv for the outer network of a space. An ordinary bridge: this is
|
|
95
|
+
* the gatekeeper's way out, and the space is never attached to it. One per space, so two spaces
|
|
96
|
+
* never share a network and a gatekeeper never sees another space's traffic.
|
|
97
|
+
*/
|
|
98
|
+
export function buildGatekeeperNetworkArgs({ network, labelArguments }) {
|
|
99
|
+
return [
|
|
100
|
+
'network', 'create',
|
|
101
|
+
'--driver', 'bridge',
|
|
102
|
+
'--ipv6=false',
|
|
103
|
+
...labelArguments,
|
|
104
|
+
network,
|
|
105
|
+
];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* The full `docker create` argv for a gatekeeper container. It is hardened like a space, with
|
|
110
|
+
* three differences, each of them checked in findGatekeeperHardeningViolations: no mount at all,
|
|
111
|
+
* a memory limit of its own, and a tmpfs that holds only its program.
|
|
112
|
+
*
|
|
113
|
+
* It is created on the inner network under a fixed alias. The place attaches the outer network
|
|
114
|
+
* afterwards, because Docker refuses `--network-alias` together with a second `--network`.
|
|
115
|
+
*/
|
|
116
|
+
export function buildGatekeeperCreateArgs({ containerName, labelArguments, network, image }) {
|
|
117
|
+
return [
|
|
118
|
+
'create',
|
|
119
|
+
'--name', containerName,
|
|
120
|
+
...labelArguments,
|
|
121
|
+
'--init',
|
|
122
|
+
'--user', SPACE_USER,
|
|
123
|
+
'--read-only',
|
|
124
|
+
'--tmpfs', `/tmp:${GATEKEEPER_TMPFS_OPTIONS}`,
|
|
125
|
+
'--security-opt', 'no-new-privileges',
|
|
126
|
+
'--cap-drop', 'ALL',
|
|
127
|
+
'--pids-limit', String(SPACE_PIDS_LIMIT),
|
|
128
|
+
...memoryArgs(GATEKEEPER_MEMORY_BYTES),
|
|
129
|
+
'--shm-size', '64m',
|
|
130
|
+
'--ipc', 'private',
|
|
131
|
+
'--cgroupns', 'private',
|
|
132
|
+
...LOG_ARGS,
|
|
133
|
+
'--network', network,
|
|
134
|
+
'--network-alias', GATEKEEPER_ALIAS,
|
|
135
|
+
...Object.entries(GATEKEEPER_ENVIRONMENT).flatMap(([name, value]) => ['--env', `${name}=${value}`]),
|
|
136
|
+
image,
|
|
137
|
+
...GATEKEEPER_COMMAND,
|
|
138
|
+
];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* The full `docker create` argv for a space container. Every restriction here has
|
|
143
|
+
* a matching check in findHardeningViolations.
|
|
144
|
+
*/
|
|
145
|
+
export function buildSpaceCreateArgs({ spaceId, containerName, labelArguments, network, workVolume, homeVolume, toolsVolume, memoryBytes, image }) {
|
|
146
|
+
return [
|
|
147
|
+
'create',
|
|
148
|
+
'--name', containerName,
|
|
149
|
+
...labelArguments,
|
|
150
|
+
'--init',
|
|
151
|
+
'--user', SPACE_USER,
|
|
152
|
+
'--read-only',
|
|
153
|
+
'--tmpfs', `/tmp:${TMPFS_OPTIONS}`,
|
|
154
|
+
'--security-opt', 'no-new-privileges',
|
|
155
|
+
'--cap-drop', 'ALL',
|
|
156
|
+
'--pids-limit', String(SPACE_PIDS_LIMIT),
|
|
157
|
+
...memoryArgs(requireMemoryBytes(memoryBytes)),
|
|
158
|
+
'--shm-size', '64m',
|
|
159
|
+
// Asked for by name, so a daemon whose default is `host` or `shareable` still passes the checker.
|
|
160
|
+
'--ipc', 'private',
|
|
161
|
+
'--cgroupns', 'private',
|
|
162
|
+
...LOG_ARGS,
|
|
163
|
+
'--network', network,
|
|
164
|
+
...volumeMount(workVolume, spaceWorkPath(spaceId)),
|
|
165
|
+
...volumeMount(homeVolume, SPACE_HOME),
|
|
166
|
+
...readOnlyVolumeMount(toolsVolume, TOOLS_MOUNT_PATH),
|
|
167
|
+
...Object.entries(SPACE_ENVIRONMENT).flatMap(([name, value]) => ['--env', `${name}=${value}`]),
|
|
168
|
+
image,
|
|
169
|
+
...SPACE_SERVER_COMMAND,
|
|
170
|
+
];
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* The argv for the one-shot root container that hands the two fresh volumes to
|
|
175
|
+
* the space user. It has no network and one capability, and runs a fixed command.
|
|
176
|
+
*/
|
|
177
|
+
export function buildVolumeOwnershipRunArgs({ spaceId, containerName, labelArguments, workVolume, homeVolume, image }) {
|
|
178
|
+
return [
|
|
179
|
+
'run', '--rm',
|
|
180
|
+
'--name', containerName,
|
|
181
|
+
...labelArguments,
|
|
182
|
+
'--user', '0:0',
|
|
183
|
+
'--network', 'none',
|
|
184
|
+
'--cap-drop', 'ALL',
|
|
185
|
+
'--cap-add', 'CHOWN',
|
|
186
|
+
'--security-opt', 'no-new-privileges',
|
|
187
|
+
'--read-only',
|
|
188
|
+
'--ipc', 'private',
|
|
189
|
+
'--cgroupns', 'private',
|
|
190
|
+
...memoryArgs(SETUP_MEMORY_BYTES),
|
|
191
|
+
...LOG_ARGS,
|
|
192
|
+
...volumeMount(workVolume, spaceWorkPath(spaceId)),
|
|
193
|
+
...volumeMount(homeVolume, SPACE_HOME),
|
|
194
|
+
image,
|
|
195
|
+
IMAGE_CHOWN, SPACE_USER, spaceWorkPath(spaceId), SPACE_HOME,
|
|
196
|
+
];
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* The argv for the one-shot container that fills a fresh tools volume. It is the one container
|
|
201
|
+
* here with a way out: the default bridge, to reach the npm registry. Otherwise it is
|
|
202
|
+
* hardened like a space. It runs as root with every capability dropped: root owns the
|
|
203
|
+
* fresh volume, so npm needs none, and what it writes is readable by the space user.
|
|
204
|
+
* The program is fixed, and its input arrives on stdin.
|
|
205
|
+
*/
|
|
206
|
+
export function buildToolsFillRunArgs({ containerName, labelArguments, toolsVolume, image }) {
|
|
207
|
+
return [
|
|
208
|
+
'run', '--rm', '--interactive',
|
|
209
|
+
'--name', containerName,
|
|
210
|
+
...labelArguments,
|
|
211
|
+
'--init',
|
|
212
|
+
'--user', '0:0',
|
|
213
|
+
'--network', 'bridge',
|
|
214
|
+
'--cap-drop', 'ALL',
|
|
215
|
+
'--security-opt', 'no-new-privileges',
|
|
216
|
+
'--read-only',
|
|
217
|
+
'--tmpfs', `/tmp:${FILLER_TMPFS_OPTIONS}`,
|
|
218
|
+
'--pids-limit', String(SPACE_PIDS_LIMIT),
|
|
219
|
+
'--ipc', 'private',
|
|
220
|
+
'--cgroupns', 'private',
|
|
221
|
+
...memoryArgs(FILLER_MEMORY_BYTES),
|
|
222
|
+
...LOG_ARGS,
|
|
223
|
+
...volumeMount(toolsVolume, TOOLS_MOUNT_PATH),
|
|
224
|
+
// npm keeps its logs under HOME, and the root filesystem is read-only.
|
|
225
|
+
'--env', 'HOME=/tmp',
|
|
226
|
+
image,
|
|
227
|
+
IMAGE_NODE, '-e', FILLER_PROGRAM, TOOLS_MOUNT_PATH, TOOLS_STAGING_PATH,
|
|
228
|
+
];
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Exit code of the marker check for "there is no marker". It is a code of its own, because
|
|
232
|
+
// the docker CLI exits with 1 when it cannot reach the daemon, and `cat` exits with 1 for a missing file.
|
|
233
|
+
// Mixing the two up would remove a filled volume.
|
|
234
|
+
export const TOOLS_MARKER_MISSING_EXIT_CODE = 42;
|
|
235
|
+
const MARKER_CHECK_SCRIPT = `[ -f "$1" ] || exit ${TOOLS_MARKER_MISSING_EXIT_CODE}; ${IMAGE_CAT} "$1"`;
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* The argv for the one-shot container that prints the fill marker of a tools volume.
|
|
239
|
+
* It runs as the space user, so a marker it can read is a volume a space can read.
|
|
240
|
+
*/
|
|
241
|
+
export function buildToolsCheckRunArgs({ containerName, labelArguments, toolsVolume, image }) {
|
|
242
|
+
return [
|
|
243
|
+
'run', '--rm',
|
|
244
|
+
'--name', containerName,
|
|
245
|
+
...labelArguments,
|
|
246
|
+
'--user', SPACE_USER,
|
|
247
|
+
'--network', 'none',
|
|
248
|
+
'--cap-drop', 'ALL',
|
|
249
|
+
'--security-opt', 'no-new-privileges',
|
|
250
|
+
'--read-only',
|
|
251
|
+
'--ipc', 'private',
|
|
252
|
+
'--cgroupns', 'private',
|
|
253
|
+
...memoryArgs(SETUP_MEMORY_BYTES),
|
|
254
|
+
...LOG_ARGS,
|
|
255
|
+
...readOnlyVolumeMount(toolsVolume, TOOLS_MOUNT_PATH),
|
|
256
|
+
image,
|
|
257
|
+
IMAGE_SH, '-c', MARKER_CHECK_SCRIPT, 'sh', TOOLS_MARKER_PATH,
|
|
258
|
+
];
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const mentionsRuntimeSocket = (text) => String(text ?? '').includes('docker.sock');
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* The checks a space and a gatekeeper share, in the order they are reported. What differs is
|
|
265
|
+
* the tmpfs, the network the container must run on, and which mounts it may have, which
|
|
266
|
+
* `mountViolations(mounts)` answers.
|
|
267
|
+
*/
|
|
268
|
+
function findCommonViolations({ container, tmpfsOptions, networkMode, mountViolations }) {
|
|
269
|
+
const violations = [];
|
|
270
|
+
const violate = (check, message) => violations.push({ check, message });
|
|
271
|
+
|
|
272
|
+
const config = container?.Config ?? {};
|
|
273
|
+
const host = container?.HostConfig ?? {};
|
|
274
|
+
const mounts = container?.Mounts ?? [];
|
|
275
|
+
|
|
276
|
+
const secretVariables = (config.Env ?? []).map((entry) => String(entry).split('=')[0]).filter((name) => FORBIDDEN_ENVIRONMENT.includes(name));
|
|
277
|
+
if (secretVariables.length > 0) violate('environment', `The container environment holds a secret: ${secretVariables.join(', ')}`);
|
|
278
|
+
if (config.User !== SPACE_USER) violate('user', `Runs as '${config.User ?? ''}', expected ${SPACE_USER}`);
|
|
279
|
+
if (host.ReadonlyRootfs !== true) violate('read_only', 'The root filesystem is writable');
|
|
280
|
+
if (host.Privileged !== false) violate('privileged', 'The container is privileged');
|
|
281
|
+
if (!(host.CapDrop ?? []).includes('ALL')) violate('cap_drop', 'Capabilities are not all dropped');
|
|
282
|
+
if ((host.CapAdd ?? []).length > 0) violate('cap_add', `Capabilities were added: ${host.CapAdd.join(', ')}`);
|
|
283
|
+
|
|
284
|
+
const securityOptions = host.SecurityOpt ?? [];
|
|
285
|
+
if (!securityOptions.some((option) => NO_NEW_PRIVILEGES.includes(option))) {
|
|
286
|
+
violate('no_new_privileges', 'Privilege escalation is not blocked');
|
|
287
|
+
}
|
|
288
|
+
const extraSecurityOptions = securityOptions.filter((option) => !NO_NEW_PRIVILEGES.includes(option));
|
|
289
|
+
if (extraSecurityOptions.length > 0) violate('security_options', `Unexpected security options: ${extraSecurityOptions.join(', ')}`);
|
|
290
|
+
|
|
291
|
+
if (host.Init !== true) violate('init', 'There is no init process');
|
|
292
|
+
if (!(host.PidsLimit > 0)) violate('pids_limit', 'There is no process limit');
|
|
293
|
+
if (!(host.Memory > 0)) violate('memory', 'There is no memory limit');
|
|
294
|
+
if (host.MemorySwap !== host.Memory) violate('memory_swap', 'Swap is allowed on top of the memory limit');
|
|
295
|
+
if (host.ShmSize !== SPACE_SHM_BYTES) violate('shm_size', `Shared memory is ${host.ShmSize ?? 'unset'} bytes, expected ${SPACE_SHM_BYTES}`);
|
|
296
|
+
|
|
297
|
+
const log = host.LogConfig ?? {};
|
|
298
|
+
if (log.Type !== LOG_DRIVER || log.Config?.['max-size'] !== LOG_MAX_SIZE || log.Config?.['max-file'] !== LOG_MAX_FILE) {
|
|
299
|
+
violate('log_limit', 'Container output is not capped on the Docker host');
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const tmpfs = host.Tmpfs ?? {};
|
|
303
|
+
const tmpOptions = String(tmpfs['/tmp'] ?? '').split(',');
|
|
304
|
+
if (Object.keys(tmpfs).length !== 1 || !tmpfsOptions.split(',').every((option) => tmpOptions.includes(option))) {
|
|
305
|
+
violate('tmpfs', 'The only tmpfs must be /tmp with nosuid and a size limit');
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if ((host.Binds ?? []).length > 0) violate('binds', `Host paths are mounted: ${host.Binds.join(', ')}`);
|
|
309
|
+
if ((host.VolumesFrom ?? []).length > 0) violate('volumes_from', 'Volumes of another container are attached');
|
|
310
|
+
violations.push(...mountViolations(mounts));
|
|
311
|
+
const socketMounted = mounts.some((mount) => mentionsRuntimeSocket(mount.Source) || mentionsRuntimeSocket(mount.Destination))
|
|
312
|
+
|| (host.Binds ?? []).some(mentionsRuntimeSocket);
|
|
313
|
+
if (socketMounted) violate('runtime_socket', 'The container runtime socket is mounted');
|
|
314
|
+
|
|
315
|
+
for (const field of NAMESPACE_FIELDS) {
|
|
316
|
+
if (!PRIVATE_NAMESPACE_MODES.includes(host[field] ?? '')) violate('namespace', `${field} is '${host[field]}', which is not private`);
|
|
317
|
+
}
|
|
318
|
+
if (host.NetworkMode !== networkMode) violate('network_mode', `NetworkMode is '${host.NetworkMode ?? ''}', expected the network of this space`);
|
|
319
|
+
if (Object.keys(host.Sysctls ?? {}).length > 0) violate('sysctls', 'Kernel parameters were changed');
|
|
320
|
+
if (!['', 'runc'].includes(host.Runtime ?? '')) violate('runtime', `Unexpected container runtime '${host.Runtime}'`);
|
|
321
|
+
if ((host.Devices ?? []).length > 0) violate('devices', 'Host devices are attached');
|
|
322
|
+
if (Object.keys(host.PortBindings ?? {}).length > 0) violate('port_bindings', 'Ports are published on the host');
|
|
323
|
+
|
|
324
|
+
return violations;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Which mounts a space may have. The one exception to "only volumes of this space" is the mount
|
|
329
|
+
* at the tools path, and it has rules of its own. A tools volume mounted anywhere else gets no exception.
|
|
330
|
+
*/
|
|
331
|
+
function findSpaceMountViolations({ mounts, prefix, owner, toolsVolume }) {
|
|
332
|
+
const violations = [];
|
|
333
|
+
const violate = (check, message) => violations.push({ check, message });
|
|
334
|
+
|
|
335
|
+
for (const mount of mounts.filter((candidate) => candidate.Destination !== TOOLS_MOUNT_PATH)) {
|
|
336
|
+
if (mount.Type !== 'volume' || !String(mount.Name ?? '').startsWith(prefix)) {
|
|
337
|
+
violate('mounts', `Mount at ${mount.Destination} is not a volume of this space`);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
const toolsMounts = mounts.filter((candidate) => candidate.Destination === TOOLS_MOUNT_PATH);
|
|
341
|
+
if (toolsMounts.length !== 1) {
|
|
342
|
+
violate('tools_mount', `Expected one tools volume at ${TOOLS_MOUNT_PATH}, found ${toolsMounts.length}`);
|
|
343
|
+
return violations;
|
|
344
|
+
}
|
|
345
|
+
const [mount] = toolsMounts;
|
|
346
|
+
const key = toolsKeyFromVolumeName(mount.Name, owner);
|
|
347
|
+
if (mount.Type !== 'volume' || key === null) {
|
|
348
|
+
violate('tools_mount', `Mount at ${TOOLS_MOUNT_PATH} is not a tools volume of this installation`);
|
|
349
|
+
}
|
|
350
|
+
// The agent must never change the programs that the next space will run.
|
|
351
|
+
if (mount.RW !== false) violate('tools_read_only', 'The tools volume is writable from inside the space');
|
|
352
|
+
const toolsLabels = parseToolsLabels(toolsVolume?.Labels);
|
|
353
|
+
if (toolsVolume?.Name !== mount.Name || toolsLabels?.role !== ROLE_TOOLS || toolsLabels?.owner !== owner || toolsLabels?.key !== key) {
|
|
354
|
+
violate('tools_labels', 'The tools volume does not carry the tools labels of this installation');
|
|
355
|
+
}
|
|
356
|
+
return violations;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Compares the parsed `docker inspect` entry of a space container and of its
|
|
361
|
+
* network with the requested hardening. Returns one `{ check, message }` per
|
|
362
|
+
* difference. An empty list means the space is verified.
|
|
363
|
+
*
|
|
364
|
+
* `toolsVolume` is the inspect entry of the volume mounted at the tools path, or null.
|
|
365
|
+
*
|
|
366
|
+
* Docker fills every field read here at `docker create`, so this runs before the
|
|
367
|
+
* container starts.
|
|
368
|
+
*/
|
|
369
|
+
export function findHardeningViolations({ spaceId, owner, container, network, toolsVolume }) {
|
|
370
|
+
const prefix = spaceResourcePrefix(spaceId);
|
|
371
|
+
const networkName = spaceResourceName(spaceId, ROLE_NETWORK);
|
|
372
|
+
const violations = findCommonViolations({
|
|
373
|
+
container,
|
|
374
|
+
tmpfsOptions: TMPFS_OPTIONS,
|
|
375
|
+
networkMode: networkName,
|
|
376
|
+
mountViolations: (mounts) => findSpaceMountViolations({ mounts, prefix, owner, toolsVolume }),
|
|
377
|
+
});
|
|
378
|
+
const violate = (check, message) => violations.push({ check, message });
|
|
379
|
+
|
|
380
|
+
// The space stays on one network, its inner one. The gatekeeper is the other member of it.
|
|
381
|
+
const attached = Object.keys(container?.NetworkSettings?.Networks ?? {});
|
|
382
|
+
if (attached.length !== 1 || attached[0] !== networkName) {
|
|
383
|
+
violate('networks', `Attached to [${attached.join(', ')}], expected only the network of this space`);
|
|
384
|
+
}
|
|
385
|
+
violations.push(...findInnerNetworkViolations({ spaceId, owner, network }));
|
|
386
|
+
return violations;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/** The demands on the inner network of a space: no way out, and no address on the Docker host. */
|
|
390
|
+
function findInnerNetworkViolations({ spaceId, owner, network }) {
|
|
391
|
+
const violations = [];
|
|
392
|
+
const violate = (check, message) => violations.push({ check, message });
|
|
393
|
+
|
|
394
|
+
if (network?.Internal !== true) violate('network_internal', 'The network of this space can reach outside');
|
|
395
|
+
if (network?.EnableIPv6 === true) violate('network_ipv6', 'The network of this space has IPv6');
|
|
396
|
+
// An engine that ignores the option still gives the bridge a gateway address, so look at the effect too.
|
|
397
|
+
const hasGateway = (network?.IPAM?.Config ?? []).some((range) => Boolean(range.Gateway));
|
|
398
|
+
if (network?.Options?.[GATEWAY_MODE_OPTION] !== GATEWAY_MODE || hasGateway) {
|
|
399
|
+
violate('network_host_isolation', 'The network of this space has an address on the Docker host. Docker Engine 28 or newer is needed.');
|
|
400
|
+
}
|
|
401
|
+
const networkLabels = parseSpaceLabels(network?.Labels);
|
|
402
|
+
if (networkLabels?.id !== spaceId || networkLabels?.owner !== owner || networkLabels?.role !== ROLE_NETWORK) {
|
|
403
|
+
violate('network_labels', 'The network does not carry the labels of this space');
|
|
404
|
+
}
|
|
405
|
+
return violations;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const aliasesOf = (entry) => [...(entry?.Aliases ?? []), ...(entry?.DNSNames ?? [])];
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* The same comparison for the gatekeeper of a space. Every check name starts with `gatekeeper_`,
|
|
412
|
+
* so a caller that verifies both containers can tell them apart.
|
|
413
|
+
*
|
|
414
|
+
* It is hardened like a space, with three deliberate differences:
|
|
415
|
+
* - two networks, this space's inner one and this space's outer one, and no others;
|
|
416
|
+
* - no mount at all, so its only writable place is the tmpfs that vanishes with the container;
|
|
417
|
+
* - a memory limit of its own, because it runs two small listeners and not an agent.
|
|
418
|
+
*/
|
|
419
|
+
export function findGatekeeperHardeningViolations({ spaceId, owner, container, outerNetwork }) {
|
|
420
|
+
const innerName = spaceResourceName(spaceId, ROLE_NETWORK);
|
|
421
|
+
const outerName = spaceResourceName(spaceId, ROLE_OUTER_NETWORK);
|
|
422
|
+
const violations = findCommonViolations({
|
|
423
|
+
container,
|
|
424
|
+
tmpfsOptions: GATEKEEPER_TMPFS_OPTIONS,
|
|
425
|
+
networkMode: innerName,
|
|
426
|
+
mountViolations: (mounts) => (mounts.length === 0 ? [] : [{
|
|
427
|
+
check: 'mounts',
|
|
428
|
+
message: `The gatekeeper has ${mounts.length} mount(s) and must have none: ${mounts.map((mount) => mount.Destination).join(', ')}`,
|
|
429
|
+
}]),
|
|
430
|
+
});
|
|
431
|
+
const violate = (check, message) => violations.push({ check, message });
|
|
432
|
+
|
|
433
|
+
const host = container?.HostConfig ?? {};
|
|
434
|
+
// The limit is ours and fixed, unlike a space's, so an exact value is the honest check.
|
|
435
|
+
if (host.Memory > 0 && host.Memory !== GATEKEEPER_MEMORY_BYTES) {
|
|
436
|
+
violate('memory_limit', `The memory limit is ${host.Memory} bytes, expected ${GATEKEEPER_MEMORY_BYTES}`);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const networks = container?.NetworkSettings?.Networks ?? {};
|
|
440
|
+
// Exactly these two, by name. The inner network itself is the space checker's business, and
|
|
441
|
+
// every caller of this function runs that one beside it.
|
|
442
|
+
if (Object.keys(networks).sort().join(' ') !== [innerName, outerName].sort().join(' ')) {
|
|
443
|
+
violate('networks', `Attached to [${Object.keys(networks).join(', ')}], expected the inner and the outer network of this space`);
|
|
444
|
+
}
|
|
445
|
+
// The space reaches the corridor under this name and under no other.
|
|
446
|
+
if (networks[innerName] && !aliasesOf(networks[innerName]).includes(GATEKEEPER_ALIAS)) {
|
|
447
|
+
violate('alias', `The gatekeeper is not reachable as '${GATEKEEPER_ALIAS}' on the inner network of this space`);
|
|
448
|
+
}
|
|
449
|
+
// The outer network is the way out, so `Internal` would leave the corridor with nowhere to go.
|
|
450
|
+
if (outerNetwork?.Internal === true) violate('outer_network', 'The outer network of this space cannot reach outside, so the corridor would have no way out');
|
|
451
|
+
const outerLabels = parseSpaceLabels(outerNetwork?.Labels);
|
|
452
|
+
if (outerLabels?.id !== spaceId || outerLabels?.owner !== owner || outerLabels?.role !== ROLE_OUTER_NETWORK) {
|
|
453
|
+
violate('outer_network_labels', 'The outer network does not carry the labels of this space');
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
return violations.map(({ check, message }) => ({ check: `gatekeeper_${check}`, message: `Gatekeeper: ${message}` }));
|
|
457
|
+
}
|