@vantran-se/openchamber-web 1.24.4 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -8
- package/bin/lib/commands-connect-url.js +3 -8
- package/bin/lib/commands-tunnel.js +3 -0
- package/dist/assets/{CodeMirrorCommentWidgets-B4a0JkUz.js → CodeMirrorCommentWidgets-ntXIbhV2.js} +1 -1
- package/dist/assets/DiffView-CjxUovbM.js +4 -0
- package/dist/assets/FilesView-DFWD0RQR.js +17 -0
- package/dist/assets/{GitView-Byu2DY7s.js → GitView-BWa7e5vw.js} +4 -4
- package/dist/assets/{GuestAttachDialog-B808E0-d.js → GuestAttachDialog-DdOuyZzp.js} +1 -1
- package/dist/assets/GuestSurfacePane-DDfzid3U.js +1 -0
- package/dist/assets/{JsonTreeView-VW_7czo3.js → JsonTreeView-3sGwqexh.js} +1 -1
- package/dist/assets/LinearIssuesView-C6dpIdjw.js +1 -0
- package/dist/assets/MarkdownImageGallery-Q7YR_tl4.js +1 -0
- package/dist/assets/MarkdownRendererImpl-DxDV56bL.js +12 -0
- package/dist/assets/OnboardingScreen-rK4Stg__.js +1 -0
- package/dist/assets/{PatchDiff-DCjGhlYV.js → PatchDiff-DS-IMU1j.js} +1 -1
- package/dist/assets/{PierreDiffViewer-CEDZNkU2.js → PierreDiffViewer-DW23KWKZ.js} +1 -1
- package/dist/assets/PlanView-BCk_VobP.js +5 -0
- package/dist/assets/PluginPane-BPP1dbck.js +3 -0
- package/dist/assets/SettingsView-kMmrpn43.js +29 -0
- package/dist/assets/SettingsWindow-BpvD6IYQ.js +1 -0
- package/dist/assets/SubmoduleDiffSummary-BLttSS1X.js +1 -0
- package/dist/assets/ToolOutputDialog-CtqDqbyr.js +15 -0
- package/dist/assets/ToolPartDiffPreview-5FcKdyRy.js +8 -0
- package/dist/assets/{VirtualizedFileDiff-NU0FHFhT.js → VirtualizedFileDiff-C763hNCR.js} +4 -4
- package/dist/assets/{WalkthroughView-BZyjhqHv.js → WalkthroughView-lap5btCF.js} +2 -2
- package/dist/assets/appThemeRegistry-u4zw1l0B.js +1 -0
- package/dist/assets/{commitAndPush-BDftrjr6.js → commitAndPush-CwGJfJG-.js} +1 -1
- package/dist/assets/{computeVirtualFileMetrics-DlWlEM3x.js → computeVirtualFileMetrics-DW5jjX4n.js} +1 -1
- package/dist/assets/de-CndEg67T.js +10 -0
- package/dist/assets/es-DglvPlaC.js +16 -0
- package/dist/assets/fr-DejPCpbS.js +14 -0
- package/dist/assets/index-C2r64QfD.js +1 -0
- package/dist/assets/index-CLcM8HKx.css +1 -0
- package/dist/assets/index-CvLgnuNI.js +41 -0
- package/dist/assets/{isDiffPlainText-CO4e5J_S.js → isDiffPlainText-D47a6hMP.js} +1 -1
- package/dist/assets/{isFilePlainText-ue6kCou7.js → isFilePlainText-B-mGgeKl.js} +1 -1
- package/dist/assets/ja-CVpQawCd.js +16 -0
- package/dist/assets/ko-Bw446h9j.js +16 -0
- package/dist/assets/main-Bz4WWXbr.js +2 -0
- package/dist/assets/main-oreqDsOV.js +896 -0
- package/dist/assets/{markdownCore-CK48LG0z.js → markdownCore-D2e5sp9y.js} +4 -4
- package/dist/assets/miniChat-D1v2gsBI.js +2 -0
- package/dist/assets/mobile-DBj3lCf2.js +2 -0
- package/dist/assets/{patchFileDiff-D7QVlovD.js → patchFileDiff-BpeUOV91.js} +3 -3
- package/dist/assets/pl-BYNlPdhm.js +16 -0
- package/dist/assets/pt-BR-CL-Yfv9H.js +16 -0
- package/dist/assets/renderElectronMiniChatApp-DVtfEB2z.js +2 -0
- package/dist/assets/renderMobileApp-DFp9-xbG.js +25 -0
- package/dist/assets/runtimeEndpointReset-CsIpUep1.js +27 -0
- package/dist/assets/scheduledTasksApi-B5OHVpPM.js +1 -0
- package/dist/assets/{shikiHighlight-BPu3JiQs.js → shikiHighlight-DNm2Z_Jc.js} +1 -1
- package/dist/assets/{textMateThemeFromAppTheme-DWqRPo9A.js → textMateThemeFromAppTheme-DGF2kG7g.js} +1 -1
- package/dist/assets/tr-DzmbxLy-.js +7 -0
- package/dist/assets/uk-C7vQnwro.js +16 -0
- package/dist/assets/useAppFontEffects-Ci3FEaPf.js +7337 -0
- package/dist/assets/{useInlineCommentController-CgS0fnw_.js → useInlineCommentController-CuUuakrt.js} +2 -2
- package/dist/assets/useRootScrollLock-CQIsyERq.js +2 -0
- package/dist/assets/{vendor-codemirror-language-data-Bx2EHc1O.js → vendor-codemirror-language-data-CQa7_1cr.js} +2 -2
- package/dist/assets/vendor-cron-parser-CjvYVigk.js +2 -0
- package/dist/assets/vendor-opencode-client-_BAHQo4R.js +12 -0
- package/dist/assets/{vendor-remixicon-react-BDmGmFCA.js → vendor-remixicon-react-D6dZ0UwF.js} +1 -1
- package/dist/assets/{vendor-zod-CgAdXWZO.js → vendor-zod-Gea9uBlY.js} +1 -1
- package/dist/assets/{web-components-DxX8bZ1o.js → web-components-B6tKQyJN.js} +2 -2
- package/dist/assets/zh-CN-Cl0srS-z.js +16 -0
- package/dist/assets/zh-TW-DtII5bDb.js +16 -0
- package/dist/index.html +13 -7
- package/dist/mini-chat.html +5 -5
- package/dist/mobile.html +5 -5
- package/package.json +6 -5
- package/server/index.d.ts +1 -0
- package/server/index.js +246 -91
- package/server/lib/agent-tool/DOCUMENTATION.md +32 -15
- package/server/lib/agent-tool/runtime.js +135 -54
- package/server/lib/agent-tool/runtime.test.js +155 -87
- package/server/lib/browser-control/DOCUMENTATION.md +37 -2
- package/server/lib/browser-control/provider.js +217 -0
- package/server/lib/browser-control/provider.test.js +251 -0
- package/server/lib/context-obligatory/runtime.js +61 -50
- package/server/lib/context-obligatory/runtime.test.js +62 -162
- package/server/lib/event-stream/DOCUMENTATION.md +12 -1
- package/server/lib/event-stream/delta-coalescer.js +0 -0
- package/server/lib/event-stream/delta-coalescer.test.js +68 -57
- package/server/lib/event-stream/directory-ws-bridge.js +1 -1
- package/server/lib/event-stream/global-hub.js +10 -1
- package/server/lib/event-stream/global-hub.test.js +27 -26
- package/server/lib/event-stream/index.js +6 -0
- package/server/lib/event-stream/protocol.js +26 -19
- package/server/lib/event-stream/protocol.test.js +18 -7
- package/server/lib/event-stream/rebind.test.js +5 -5
- package/server/lib/event-stream/runtime.test.js +17 -15
- package/server/lib/event-stream/translate-v2.js +313 -0
- package/server/lib/event-stream/translate-v2.test.js +209 -0
- package/server/lib/event-stream/upstream-reader.test.js +16 -16
- package/server/lib/fs/DOCUMENTATION.md +4 -2
- package/server/lib/fs/byte-range.js +35 -0
- package/server/lib/fs/byte-range.test.js +31 -0
- package/server/lib/fs/routes.js +47 -42
- package/server/lib/fs/routes.test.js +123 -18
- package/server/lib/git/service.js +12 -0
- package/server/lib/github/DOCUMENTATION.md +1 -1
- package/server/lib/github/pr-status.js +22 -2
- package/server/lib/github/pr-status.test.js +27 -14
- package/server/lib/guests/DOCUMENTATION.md +13 -0
- package/server/lib/guests/catalog.js +12 -0
- package/server/lib/guests/catalog.test.js +33 -0
- package/server/lib/guests/routes.js +10 -1
- package/server/lib/guests/service.js +192 -14
- package/server/lib/guests/service.test.js +201 -1
- package/server/lib/guests/surface.js +630 -0
- package/server/lib/guests/surface.test.js +486 -0
- package/server/lib/markdown-image-grants/routes.js +18 -5
- package/server/lib/markdown-image-grants/routes.test.js +9 -6
- package/server/lib/message-queue/DOCUMENTATION.md +33 -22
- package/server/lib/message-queue/runtime.js +155 -100
- package/server/lib/message-queue/runtime.test.js +168 -82
- package/server/lib/notifications/DOCUMENTATION.md +28 -0
- package/server/lib/notifications/control-stream.http.test.js +68 -0
- package/server/lib/notifications/emitter-runtime.js +15 -0
- package/server/lib/notifications/emitter-runtime.test.js +32 -0
- package/server/lib/notifications/push-runtime.js +5 -1
- package/server/lib/notifications/push-runtime.test.js +2 -2
- package/server/lib/notifications/routes.js +1 -1
- package/server/lib/notifications/runtime.js +115 -26
- package/server/lib/notifications/template-runtime.js +13 -15
- package/server/lib/notifications/template-runtime.test.js +16 -9
- package/server/lib/notifications/trigger-runtime.test.js +187 -0
- package/server/lib/openchamber-control/DOCUMENTATION.md +10 -0
- package/server/lib/openchamber-control/actions.js +3 -2
- package/server/lib/openchamber-control/error.js +1 -1
- package/server/lib/openchamber-control/file-open.js +59 -0
- package/server/lib/openchamber-control/file-open.test.js +57 -0
- package/server/lib/openchamber-control/routes.test.js +12 -0
- package/server/lib/openchamber-control/service.js +102 -49
- package/server/lib/openchamber-control/service.test.js +84 -44
- package/server/lib/openchamber-sessions/archive-store.js +224 -0
- package/server/lib/openchamber-sessions/archive-store.test.js +187 -0
- package/server/lib/openchamber-sessions/opencode-client.js +22 -0
- package/server/lib/openchamber-sessions/routes.js +398 -328
- package/server/lib/openchamber-sessions/routes.test.js +777 -740
- package/server/lib/openchamber-sessions/session-metadata-store.js +285 -0
- package/server/lib/openchamber-sessions/session-metadata-store.test.js +259 -0
- package/server/lib/opencode/DOCUMENTATION.md +449 -29
- package/server/lib/opencode/agents.js +233 -327
- package/server/lib/opencode/auth.js +41 -44
- package/server/lib/opencode/auth.test.js +92 -0
- package/server/lib/opencode/cli-upgrade.d.ts +4 -0
- package/server/lib/opencode/cli-upgrade.js +18 -0
- package/server/lib/opencode/cli-upgrade.test.js +36 -0
- package/server/lib/opencode/commands.js +164 -145
- package/server/lib/opencode/compatibility.d.ts +16 -0
- package/server/lib/opencode/compatibility.js +88 -0
- package/server/lib/opencode/compatibility.test.js +62 -0
- package/server/lib/opencode/config-entity-routes.js +58 -29
- package/server/lib/opencode/config-entity-v2.test.js +290 -0
- package/server/lib/opencode/config-mutation-response.js +17 -7
- package/server/lib/opencode/config-mutation-response.test.js +4 -7
- package/server/lib/opencode/config-paths.test.js +2 -2
- package/server/lib/opencode/config-v2.d.ts +183 -0
- package/server/lib/opencode/config-v2.js +801 -0
- package/server/lib/opencode/config-v2.test.js +466 -0
- package/server/lib/opencode/credential-db.js +144 -0
- package/server/lib/opencode/credential-db.test.js +104 -0
- package/server/lib/opencode/env-runtime.js +48 -9
- package/server/lib/opencode/env-runtime.test.js +45 -0
- package/server/lib/opencode/feature-routes-runtime.js +15 -5
- package/server/lib/opencode/lifecycle-process.test.js +7 -4
- package/server/lib/opencode/lifecycle.js +85 -30
- package/server/lib/opencode/lifecycle.test.js +80 -8
- package/server/lib/opencode/login-shell-env.js +27 -0
- package/server/lib/opencode/managed-config-file.js +119 -0
- package/server/lib/opencode/managed-config-file.test.js +152 -0
- package/server/lib/opencode/managed-plugin-config.js +33 -13
- package/server/lib/opencode/mcp.js +43 -149
- package/server/lib/opencode/network-runtime.js +6 -7
- package/server/lib/opencode/openchamber-routes.test.js +2 -5
- package/server/lib/opencode/plugin-routes.js +3 -3
- package/server/lib/opencode/plugin-routes.test.js +6 -24
- package/server/lib/opencode/plugins.js +91 -40
- package/server/lib/opencode/plugins.test.js +28 -15
- package/server/lib/opencode/providers.js +63 -83
- package/server/lib/opencode/providers.test.js +128 -59
- package/server/lib/opencode/proxy-agent-wiring.test.js +4 -2
- package/server/lib/opencode/proxy.js +179 -129
- package/server/lib/opencode/pwa-manifest-routes.js +4 -2
- package/server/lib/opencode/pwa-manifest-routes.test.js +3 -2
- package/server/lib/opencode/response-envelope.js +14 -0
- package/server/lib/opencode/response-envelope.test.js +23 -0
- package/server/lib/opencode/routes-upgrade.test.js +73 -116
- package/server/lib/opencode/routes.js +121 -428
- package/server/lib/opencode/server-utils-runtime.js +10 -4
- package/server/lib/opencode/session-activity.js +78 -0
- package/server/lib/opencode/session-runtime.js +21 -10
- package/server/lib/opencode/session-runtime.test.js +9 -9
- package/server/lib/opencode/settings-helpers.js +9 -9
- package/server/lib/opencode/settings-helpers.test.js +7 -12
- package/server/lib/opencode/settings-registry.json +6 -8
- package/server/lib/opencode/settings-runtime.js +18 -1
- package/server/lib/opencode/settings-runtime.test.js +31 -2
- package/server/lib/opencode/shared.js +54 -22
- package/server/lib/opencode/shared.test.js +8 -4
- package/server/lib/opencode/shutdown-runtime.js +54 -7
- package/server/lib/opencode/shutdown-runtime.test.js +224 -30
- package/server/lib/opencode/skill-routes.js +23 -24
- package/server/lib/opencode/skills.js +7 -5
- package/server/lib/opencode/tunnel-wiring-runtime.js +2 -1
- package/server/lib/opencode/upgrade-capability.test.js +1 -1
- package/server/lib/opencode/v1-migration-topup.js +288 -0
- package/server/lib/opencode/v1-migration-topup.test.js +266 -0
- package/server/lib/opencode/v2-install.d.ts +8 -0
- package/server/lib/opencode/v2-install.js +158 -0
- package/server/lib/opencode/v2-install.test.js +124 -0
- package/server/lib/opencode/watcher.js +10 -3
- package/server/lib/opencode/watcher.test.js +17 -21
- package/server/lib/opencode/websearch-config.js +66 -0
- package/server/lib/package-manager.js +55 -68
- package/server/lib/package-manager.test.js +241 -75
- package/server/lib/permission-auto-accept/runtime.js +38 -22
- package/server/lib/permission-auto-accept/runtime.test.js +39 -17
- package/server/lib/quota/DOCUMENTATION.md +4 -0
- package/server/lib/quota/providers/xai.js +4 -4
- package/server/lib/relay/tunnel-host.js +4 -1
- package/server/lib/relay/tunnel-host.test.js +7 -0
- package/server/lib/routing/DOCUMENTATION.md +52 -21
- package/server/lib/routing/defaults.js +23 -6
- package/server/lib/routing/jev.js +17 -7
- package/server/lib/routing/routes.http.test.js +68 -33
- package/server/lib/routing/routes.js +65 -22
- package/server/lib/routing/runtime.js +145 -48
- package/server/lib/routing/runtime.test.js +79 -55
- package/server/lib/scheduled-tasks/DOCUMENTATION.md +4 -0
- package/server/lib/scheduled-tasks/issue-2710-double-execution.test.js +51 -46
- package/server/lib/scheduled-tasks/runtime.js +81 -74
- package/server/lib/scheduled-tasks/runtime.test.js +53 -1
- package/server/lib/session-assist/DOCUMENTATION.md +8 -3
- package/server/lib/session-assist/context.js +122 -46
- package/server/lib/session-assist/context.test.js +117 -49
- package/server/lib/session-assist/runtime.js +52 -22
- package/server/lib/session-assist/runtime.test.js +145 -173
- package/server/lib/session-goal/DOCUMENTATION.md +23 -15
- package/server/lib/session-goal/create.js +9 -12
- package/server/lib/session-goal/create.test.js +27 -28
- package/server/lib/session-goal/runtime.js +164 -77
- package/server/lib/session-goal/runtime.test.js +271 -519
- package/server/lib/session-knowledge/DOCUMENTATION.md +5 -0
- package/server/lib/session-knowledge/runtime.js +59 -41
- package/server/lib/session-knowledge/runtime.test.js +90 -20
- package/server/lib/small-model/DOCUMENTATION.md +154 -208
- package/server/lib/small-model/client.js +113 -0
- package/server/lib/small-model/index.js +296 -158
- package/server/lib/small-model/index.test.js +423 -256
- package/server/lib/small-model/routes.js +1 -1
- package/server/lib/spaces/DOCUMENTATION.md +807 -0
- package/server/lib/spaces/code-in-bait.js +185 -0
- package/server/lib/spaces/code-in.js +641 -0
- package/server/lib/spaces/code-in.test.js +1048 -0
- package/server/lib/spaces/errors.js +8 -0
- package/server/lib/spaces/exec-http.js +94 -0
- package/server/lib/spaces/gatekeeper-channel.js +202 -0
- package/server/lib/spaces/gatekeeper-channel.test.js +215 -0
- package/server/lib/spaces/gatekeeper-program.cjs +914 -0
- package/server/lib/spaces/gatekeeper-program.test.js +1391 -0
- package/server/lib/spaces/hardening.js +457 -0
- package/server/lib/spaces/hardening.test.js +368 -0
- package/server/lib/spaces/host-git.js +127 -0
- package/server/lib/spaces/host-git.test.js +137 -0
- package/server/lib/spaces/labels.js +234 -0
- package/server/lib/spaces/labels.test.js +219 -0
- package/server/lib/spaces/layout.js +184 -0
- package/server/lib/spaces/layout.test.js +30 -0
- package/server/lib/spaces/manager.js +82 -0
- package/server/lib/spaces/manager.test.js +114 -0
- package/server/lib/spaces/places/code-in.docker.live.test.js +216 -0
- package/server/lib/spaces/places/contract-suite.js +156 -0
- package/server/lib/spaces/places/contract.docker.live.test.js +7 -0
- package/server/lib/spaces/places/contract.test.js +6 -0
- package/server/lib/spaces/places/docker-engine.js +126 -0
- package/server/lib/spaces/places/docker-live-support.js +202 -0
- package/server/lib/spaces/places/docker-tools.js +186 -0
- package/server/lib/spaces/places/docker.js +587 -0
- package/server/lib/spaces/places/docker.test.js +1610 -0
- package/server/lib/spaces/places/escape-suite.js +1018 -0
- package/server/lib/spaces/places/escape.docker.live.test.js +7 -0
- package/server/lib/spaces/places/fake-docker.js +375 -0
- package/server/lib/spaces/places/memory-place.js +103 -0
- package/server/lib/spaces/places/packed.docker.live.test.js +77 -0
- package/server/lib/spaces/places/registry.js +43 -0
- package/server/lib/spaces/places/registry.test.js +50 -0
- package/server/lib/spaces/places/server.docker.live.test.js +225 -0
- package/server/lib/spaces/run-command.js +261 -0
- package/server/lib/spaces/run-command.test.js +396 -0
- package/server/lib/spaces/space-server.js +190 -0
- package/server/lib/spaces/space-server.test.js +335 -0
- package/server/lib/spaces/tools-filler.js +82 -0
- package/server/lib/spaces/tools-filler.test.js +148 -0
- package/server/lib/spaces/tools-pack.js +49 -0
- package/server/lib/spaces/tools-pack.test.js +57 -0
- package/server/lib/spaces/tools.js +138 -0
- package/server/lib/spaces/tools.test.js +153 -0
- package/server/lib/tts/openai-sdk.js +11 -0
- package/server/lib/tts/service.js +7 -5
- package/server/lib/tts/stt.js +3 -2
- package/server/lib/tunnels/DOCUMENTATION.md +2 -0
- package/server/lib/tunnels/routes.js +7 -0
- package/server/lib/tunnels/routes.test.js +55 -0
- package/server/lib/ui-auth/ui-auth.js +38 -2
- package/server/lib/ui-auth/ui-auth.test.js +42 -0
- package/server/lib/ui-auth/ui-passkeys.js +12 -10
- package/server/lib/walkthrough/DOCUMENTATION.md +10 -1
- package/server/lib/walkthrough/pull-request.js +61 -8
- package/server/lib/walkthrough/pull-request.test.js +54 -1
- package/server/lib/walkthrough/routes.js +20 -0
- package/server/lib/walkthrough/routes.test.js +19 -0
- package/server/opencode-proxy.test.js +190 -166
- package/server/opencode-worktree-readiness.test.js +5 -4
- package/dist/assets/DiffView-B2CD9wC8.js +0 -4
- package/dist/assets/FilesView-C4vMFclc.js +0 -13
- package/dist/assets/LinearIssuesView-EzGNCGrd.js +0 -1
- package/dist/assets/MarkdownImageGallery-B2YUDFaB.js +0 -1
- package/dist/assets/MarkdownRendererImpl-B4ikIzR3.js +0 -12
- package/dist/assets/OnboardingScreen-CQHBSTEi.js +0 -1
- package/dist/assets/PlanView-D3x-pTkZ.js +0 -5
- package/dist/assets/PluginPane-DEcgDQ9h.js +0 -3
- package/dist/assets/SettingsView-CSinuH-1.js +0 -29
- package/dist/assets/SettingsWindow-B5CHoJFZ.js +0 -1
- package/dist/assets/SubmoduleDiffSummary-oxeACCWs.js +0 -1
- package/dist/assets/ToolOutputDialog-tLNWmfcW.js +0 -15
- package/dist/assets/ToolPartDiffPreview-CflzmK-x.js +0 -8
- package/dist/assets/appThemeRegistry-DLicw4dH.js +0 -1
- package/dist/assets/de-Bro6_zyl.js +0 -10
- package/dist/assets/es-Bb9XQZvd.js +0 -16
- package/dist/assets/fr-Dgfzn-IL.js +0 -14
- package/dist/assets/index-CtQNmqUL.js +0 -1
- package/dist/assets/index-DqaMWNrC.css +0 -1
- package/dist/assets/index-Dqc6USl3.js +0 -40
- package/dist/assets/ja-BYsakGFh.js +0 -16
- package/dist/assets/ko-BSaQz9aN.js +0 -16
- package/dist/assets/main-B62_pjL5.js +0 -2
- package/dist/assets/main-BEdcOI2P.js +0 -896
- package/dist/assets/miniChat-LT8Rqofr.js +0 -2
- package/dist/assets/mobile-CqarEynh.js +0 -2
- package/dist/assets/pl-OIviPdoj.js +0 -16
- package/dist/assets/pt-BR-Cltg-E6s.js +0 -16
- package/dist/assets/renderElectronMiniChatApp-COyLTtZx.js +0 -2
- package/dist/assets/renderMobileApp-BC1MHg6g.js +0 -25
- package/dist/assets/runtimeEndpointReset-DdrazOVH.js +0 -25
- package/dist/assets/scheduledTasksApi-B2SWMGEU.js +0 -1
- package/dist/assets/tr-CQI-KPre.js +0 -7
- package/dist/assets/uk-CqW7R4DA.js +0 -16
- package/dist/assets/useAppFontEffects-ne84JBVL.js +0 -7333
- package/dist/assets/useRootScrollLock-Cpf1qoGp.js +0 -2
- package/dist/assets/vendor-cron-parser-CUcP7OfX.js +0 -2
- package/dist/assets/vendor-opencode-sdk-BJ73u-bH.js +0 -7
- package/dist/assets/zh-CN-CVZG_o_J.js +0 -16
- package/dist/assets/zh-TW-D5ZxzCZp.js +0 -16
- package/server/lib/opencode/mcp-oauth-callback.test.js +0 -111
- package/server/lib/routing/feature-flag.js +0 -11
- package/server/lib/small-model/call.js +0 -851
- package/server/lib/small-model/call.test.js +0 -1292
- package/server/lib/small-model/catalog.js +0 -13
- package/server/lib/small-model/resolve.js +0 -131
- package/server/lib/small-model/resolve.test.js +0 -197
- package/server/lib/small-model/runtime-providers.js +0 -164
- package/server/lib/small-model/runtime-providers.test.js +0 -118
- package/server/lib/system-prompt/DOCUMENTATION.md +0 -36
- package/server/lib/system-prompt/runtime.js +0 -49
- package/server/lib/system-prompt/runtime.test.js +0 -76
- package/server/lib/walkthrough/reproduce-2607.test.js +0 -148
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { SpaceError } from './errors.js';
|
|
2
|
+
import { createSpaceId, hashProjectDirectory, normalizeSpaceName } from './labels.js';
|
|
3
|
+
|
|
4
|
+
// Memory limit of a new space. Swap adds nothing on top of it.
|
|
5
|
+
const DEFAULT_SPACE_MEMORY_BYTES = 4 * 1024 * 1024 * 1024;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Thin orchestration over places. It keeps no record of spaces: every answer
|
|
9
|
+
* comes from the place at the time of the call.
|
|
10
|
+
*/
|
|
11
|
+
export function createSpaceManager({ registry, now = () => new Date() }) {
|
|
12
|
+
const requirePlace = (placeId) => {
|
|
13
|
+
const place = registry.get(placeId);
|
|
14
|
+
if (!place) {
|
|
15
|
+
throw new SpaceError('place_not_found', `There is no place named '${placeId}'. Pick a configured place.`);
|
|
16
|
+
}
|
|
17
|
+
return place;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const removeSpace = async ({ placeId, spaceId }) => {
|
|
21
|
+
const result = await requirePlace(placeId).remove(spaceId);
|
|
22
|
+
if (result.failed.length > 0) {
|
|
23
|
+
throw new SpaceError(
|
|
24
|
+
'space_remove_incomplete',
|
|
25
|
+
`Could not remove: ${result.failed.map((item) => `${item.kind} ${item.name}`).join(', ')}`,
|
|
26
|
+
result,
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
return result;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const createSpace = async ({ placeId, projectDirectory, name }) => {
|
|
33
|
+
const place = requirePlace(placeId);
|
|
34
|
+
const spec = {
|
|
35
|
+
id: createSpaceId(),
|
|
36
|
+
name: normalizeSpaceName(name),
|
|
37
|
+
project: hashProjectDirectory(projectDirectory),
|
|
38
|
+
created: now().toISOString(),
|
|
39
|
+
memoryBytes: DEFAULT_SPACE_MEMORY_BYTES,
|
|
40
|
+
};
|
|
41
|
+
await place.create(spec);
|
|
42
|
+
|
|
43
|
+
// A space that cannot be verified is never handed out. It is removed instead.
|
|
44
|
+
const discard = async () => {
|
|
45
|
+
try {
|
|
46
|
+
return await place.remove(spec.id);
|
|
47
|
+
} catch (error) {
|
|
48
|
+
return { removed: [], failed: [{ kind: 'space', name: spec.id, message: error.message }] };
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
let violations;
|
|
53
|
+
try {
|
|
54
|
+
violations = await place.verify(spec.id);
|
|
55
|
+
} catch (error) {
|
|
56
|
+
const removal = await discard();
|
|
57
|
+
const wrapped = new SpaceError(error.code ?? 'space_verification_failed', `The space was removed because it could not be verified. ${error.message}`, { removal });
|
|
58
|
+
wrapped.cause = error;
|
|
59
|
+
throw wrapped;
|
|
60
|
+
}
|
|
61
|
+
if (violations.length > 0) {
|
|
62
|
+
const removal = await discard();
|
|
63
|
+
throw new SpaceError(
|
|
64
|
+
'space_verification_failed',
|
|
65
|
+
`The space was removed because it does not match the requested restrictions: ${violations.map((violation) => violation.message).join('; ')}`,
|
|
66
|
+
{ violations, removal },
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
return { placeId: place.id, ...spec };
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const listSpaces = async ({ placeId }) => {
|
|
73
|
+
const place = requirePlace(placeId);
|
|
74
|
+
return (await place.list()).map((space) => ({ placeId: place.id, ...space }));
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const stopSpace = async ({ placeId, spaceId }) => requirePlace(placeId).stop(spaceId);
|
|
78
|
+
|
|
79
|
+
const startSpace = async ({ placeId, spaceId }) => requirePlace(placeId).start(spaceId);
|
|
80
|
+
|
|
81
|
+
return { createSpace, listSpaces, stopSpace, startSpace, removeSpace };
|
|
82
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { SpaceError } from './errors.js';
|
|
4
|
+
import { hashProjectDirectory } from './labels.js';
|
|
5
|
+
import { createSpaceManager } from './manager.js';
|
|
6
|
+
import { createMemoryPlace } from './places/memory-place.js';
|
|
7
|
+
import { createPlaceRegistry } from './places/registry.js';
|
|
8
|
+
|
|
9
|
+
const NOW = new Date('2026-09-19T10:00:00.000Z');
|
|
10
|
+
const REQUEST = { placeId: 'memory', projectDirectory: '/home/me/project', name: ' Fix login ' };
|
|
11
|
+
|
|
12
|
+
const managerFor = (place) => createSpaceManager({ registry: createPlaceRegistry([place]), now: () => NOW });
|
|
13
|
+
|
|
14
|
+
describe('createSpaceManager', () => {
|
|
15
|
+
it('creates a space with a fresh id, the project hash and the creation time', async () => {
|
|
16
|
+
const manager = managerFor(createMemoryPlace());
|
|
17
|
+
|
|
18
|
+
const space = await manager.createSpace(REQUEST);
|
|
19
|
+
expect(space).toEqual({
|
|
20
|
+
placeId: 'memory',
|
|
21
|
+
id: expect.stringMatching(/^[0-9a-f]{12}$/),
|
|
22
|
+
name: 'Fix login',
|
|
23
|
+
project: hashProjectDirectory('/home/me/project'),
|
|
24
|
+
created: '2026-09-19T10:00:00.000Z',
|
|
25
|
+
memoryBytes: 4 * 1024 * 1024 * 1024,
|
|
26
|
+
});
|
|
27
|
+
const { memoryBytes, ...listedFields } = space;
|
|
28
|
+
expect(memoryBytes).toBeGreaterThan(0);
|
|
29
|
+
expect(await manager.listSpaces({ placeId: 'memory' })).toEqual([{ ...listedFields, state: 'running', orphans: [], damaged: false, missing: [] }]);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('names the place that does not exist', async () => {
|
|
33
|
+
const manager = managerFor(createMemoryPlace());
|
|
34
|
+
|
|
35
|
+
await expect(manager.createSpace({ ...REQUEST, placeId: 'cluster' })).rejects.toMatchObject({ code: 'place_not_found' });
|
|
36
|
+
await expect(manager.listSpaces({ placeId: 'cluster' })).rejects.toMatchObject({ code: 'place_not_found' });
|
|
37
|
+
await expect(manager.stopSpace({ placeId: 'cluster', spaceId: 'a1b2c3d4e5f6' })).rejects.toMatchObject({ code: 'place_not_found' });
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('rejects a bad name or directory before it asks the place', async () => {
|
|
41
|
+
const place = createMemoryPlace();
|
|
42
|
+
const manager = managerFor(place);
|
|
43
|
+
|
|
44
|
+
await expect(manager.createSpace({ ...REQUEST, name: '' })).rejects.toMatchObject({ code: 'invalid_space_name' });
|
|
45
|
+
await expect(manager.createSpace({ ...REQUEST, projectDirectory: '' })).rejects.toMatchObject({ code: 'invalid_project_directory' });
|
|
46
|
+
expect(await place.list()).toEqual([]);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('removes and refuses a space whose verification reports violations', async () => {
|
|
50
|
+
const place = { ...createMemoryPlace(), verify: async () => [{ check: 'privileged', message: 'The container is privileged' }] };
|
|
51
|
+
const manager = managerFor(place);
|
|
52
|
+
|
|
53
|
+
const error = await manager.createSpace(REQUEST).catch((caught) => caught);
|
|
54
|
+
expect(error).toBeInstanceOf(SpaceError);
|
|
55
|
+
expect(error.code).toBe('space_verification_failed');
|
|
56
|
+
expect(error.message).toContain('The container is privileged');
|
|
57
|
+
expect(error.details.violations).toHaveLength(1);
|
|
58
|
+
expect(error.details.removal.failed).toEqual([]);
|
|
59
|
+
expect(await place.list()).toEqual([]);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('removes a space that could not be verified at all', async () => {
|
|
63
|
+
const place = { ...createMemoryPlace(), verify: async () => { throw new SpaceError('command_timeout', 'docker inspect did not finish'); } };
|
|
64
|
+
const manager = managerFor(place);
|
|
65
|
+
|
|
66
|
+
await expect(manager.createSpace(REQUEST)).rejects.toMatchObject({ code: 'command_timeout' });
|
|
67
|
+
expect(await place.list()).toEqual([]);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('reports a space it could neither verify nor remove', async () => {
|
|
71
|
+
const place = {
|
|
72
|
+
...createMemoryPlace(),
|
|
73
|
+
verify: async () => [{ check: 'user', message: 'Runs as root' }],
|
|
74
|
+
remove: async () => { throw new SpaceError('docker_command_failed', 'daemon went away'); },
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const error = await managerFor(place).createSpace(REQUEST).catch((caught) => caught);
|
|
78
|
+
expect(error.details.removal.failed).toEqual([{ kind: 'space', name: expect.any(String), message: 'daemon went away' }]);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('lets a failed create through', async () => {
|
|
82
|
+
const place = { ...createMemoryPlace(), create: async () => { throw new SpaceError('image_pull_failed', 'no internet'); } };
|
|
83
|
+
await expect(managerFor(place).createSpace(REQUEST)).rejects.toMatchObject({ code: 'image_pull_failed' });
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('lets a list failure through instead of an empty list', async () => {
|
|
87
|
+
const place = { ...createMemoryPlace(), list: async () => { throw new SpaceError('docker_command_failed', 'daemon went away'); } };
|
|
88
|
+
await expect(managerFor(place).listSpaces({ placeId: 'memory' })).rejects.toMatchObject({ code: 'docker_command_failed' });
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('reads status from the place on every call', async () => {
|
|
92
|
+
const manager = managerFor(createMemoryPlace());
|
|
93
|
+
const { id: spaceId } = await manager.createSpace(REQUEST);
|
|
94
|
+
const state = async () => (await manager.listSpaces({ placeId: 'memory' }))[0].state;
|
|
95
|
+
|
|
96
|
+
await manager.stopSpace({ placeId: 'memory', spaceId });
|
|
97
|
+
expect(await state()).toBe('exited');
|
|
98
|
+
await manager.startSpace({ placeId: 'memory', spaceId });
|
|
99
|
+
expect(await state()).toBe('running');
|
|
100
|
+
await manager.removeSpace({ placeId: 'memory', spaceId });
|
|
101
|
+
expect(await manager.listSpaces({ placeId: 'memory' })).toEqual([]);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('fails a removal that left something behind', async () => {
|
|
105
|
+
const failed = [{ kind: 'volume', name: 'openchamber-space-a1b2c3d4e5f6-volume-work', message: 'volume is in use' }];
|
|
106
|
+
const place = { ...createMemoryPlace(), remove: async () => ({ removed: [], failed }) };
|
|
107
|
+
|
|
108
|
+
await expect(managerFor(place).removeSpace({ placeId: 'memory', spaceId: 'a1b2c3d4e5f6' })).rejects.toMatchObject({
|
|
109
|
+
code: 'space_remove_incomplete',
|
|
110
|
+
message: 'Could not remove: volume openchamber-space-a1b2c3d4e5f6-volume-work',
|
|
111
|
+
details: { failed },
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
});
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
// Code in, into a real space on a real Docker daemon. Runs only with OPENCHAMBER_TEST_DOCKER=1.
|
|
2
|
+
// The bait repository arrives over `git push` through `docker exec`, and a space whose receiving
|
|
3
|
+
// side never answers is ended by the host's timeout with nothing left on the host. Every test runs
|
|
4
|
+
// alone as well as in order: the shared code in happens in `beforeAll`, and the hanging tests make
|
|
5
|
+
// spaces of their own.
|
|
6
|
+
|
|
7
|
+
import zlib from 'node:zlib';
|
|
8
|
+
|
|
9
|
+
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
|
10
|
+
|
|
11
|
+
import { OBJECTS_ONLY_TEXT, SECRET, createTestHost, hostState, makeBait, removeTestHosts, shortStatus, unexpectedChanges } from '../code-in-bait.js';
|
|
12
|
+
import { createSpaceId, hashProjectDirectory, spaceResourceName } from '../labels.js';
|
|
13
|
+
import { runCommand } from '../run-command.js';
|
|
14
|
+
import { LIVE_DOCKER_ENABLED, createLiveDockerPlace } from './docker-live-support.js';
|
|
15
|
+
|
|
16
|
+
const CREATE_TIMEOUT_MS = 25 * 60_000;
|
|
17
|
+
const WIN = process.platform === 'win32';
|
|
18
|
+
// The timing of the hanging tests. After the host gives up, and before the limit inside ends the
|
|
19
|
+
// receiver, the tests count what still runs inside and check the host's processes: that window is
|
|
20
|
+
// the inner margin. Over `DOCKER_HOST=ssh://` every docker call opens its own SSH connection, so the
|
|
21
|
+
// window is sized from a measured `docker exec` round trip, with today's local values as the floor:
|
|
22
|
+
// a 6 s host timeout and a 5 s margin. The deadline is counted from before the call, while the push
|
|
23
|
+
// starts a few docker calls later, so up to three round trips of skew are paid for as well.
|
|
24
|
+
const hangTiming = (roundTripMs) => {
|
|
25
|
+
const hostTimeoutMs = Math.max(6000, 3 * roundTripMs + 3000);
|
|
26
|
+
const innerMarginSeconds = Math.max(5, Math.ceil((4 * roundTripMs + 4000) / 1000));
|
|
27
|
+
return { roundTripMs, hostTimeoutMs, innerMarginSeconds, innerLimitMs: hostTimeoutMs + innerMarginSeconds * 1000 };
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// The docker calls each hanging test makes between `started` and the push, where the limit inside
|
|
31
|
+
// begins to count. Every `exec` of the place inspects first, so it costs two. Code in: `execArgv`,
|
|
32
|
+
// then the init script. The history: `execArgv`, the shallow check, then the side repository's init.
|
|
33
|
+
const DOCKER_CALLS_BEFORE_CODE_IN_PUSH = 3;
|
|
34
|
+
const DOCKER_CALLS_BEFORE_HISTORY_PUSH = 5;
|
|
35
|
+
|
|
36
|
+
// What the agent could have done inside by the time the history travels: a global git config in
|
|
37
|
+
// its HOME that points every git at hooks of its own, and a pre-receive hook that never ends.
|
|
38
|
+
const PLANT_HANGING_HOOK = [
|
|
39
|
+
'PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin;',
|
|
40
|
+
'mkdir -p "$HOME/planted-hooks" &&',
|
|
41
|
+
'printf "#!/bin/sh\\nexec /bin/sleep 1000\\n" > "$HOME/planted-hooks/pre-receive" &&',
|
|
42
|
+
'chmod 755 "$HOME/planted-hooks/pre-receive" &&',
|
|
43
|
+
'git config --global core.hooksPath "$HOME/planted-hooks"',
|
|
44
|
+
].join(' ');
|
|
45
|
+
const COUNT_SLEEPERS = 'PATH=/usr/bin:/bin; n=0; for p in /proc/[0-9]*; do tr "\\000" " " < "$p/cmdline" 2>/dev/null | grep -q "^/bin/sleep 1000" && n=$((n+1)); done; echo $n';
|
|
46
|
+
|
|
47
|
+
const pause = (milliseconds) => new Promise((resolve) => { setTimeout(resolve, milliseconds); });
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Waits up to two seconds for the host processes that name the container to be gone, and says
|
|
51
|
+
* whether they went. It must finish before the limit inside the space: that limit ends a leftover
|
|
52
|
+
* `docker exec` on its own, and a check that waited for it would pass without the tree kill.
|
|
53
|
+
*/
|
|
54
|
+
async function hostProcessesGone(container, innerLimitAt) {
|
|
55
|
+
let count = await hostProcessesNaming(container);
|
|
56
|
+
for (let attempt = 0; attempt < 8 && count > 0; attempt += 1) {
|
|
57
|
+
await pause(250);
|
|
58
|
+
count = await hostProcessesNaming(container);
|
|
59
|
+
}
|
|
60
|
+
expect(Date.now(), 'the host check ran into the limit inside the space and proves nothing').toBeLessThan(innerLimitAt);
|
|
61
|
+
return count === 0;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** How many host processes name this container on their command line. The docker CLI of an exec always does. */
|
|
65
|
+
async function hostProcessesNaming(container) {
|
|
66
|
+
if (WIN) {
|
|
67
|
+
const script = `@(Get-CimInstance Win32_Process | Where-Object { $_.CommandLine -like '*${container}*' -and $_.ProcessId -ne $PID }).Count`;
|
|
68
|
+
const result = await runCommand('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', script]);
|
|
69
|
+
return Number(result.stdout.trim());
|
|
70
|
+
}
|
|
71
|
+
const result = await runCommand('ps', ['-A', '-o', 'command=']);
|
|
72
|
+
return result.stdout.split('\n').filter((command) => command.includes(container)).length;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
describe.skipIf(!LIVE_DOCKER_ENABLED)('code in: docker (live)', () => {
|
|
76
|
+
const newSpec = (name) => ({
|
|
77
|
+
id: createSpaceId(),
|
|
78
|
+
name,
|
|
79
|
+
project: hashProjectDirectory('/code/in/project'),
|
|
80
|
+
created: new Date().toISOString(),
|
|
81
|
+
memoryBytes: 1024 * 1024 * 1024,
|
|
82
|
+
});
|
|
83
|
+
const spec = newSpec('Code in');
|
|
84
|
+
const spaceIds = [spec.id];
|
|
85
|
+
let place;
|
|
86
|
+
let dispose = async () => {};
|
|
87
|
+
let repo;
|
|
88
|
+
let g;
|
|
89
|
+
let codeIn;
|
|
90
|
+
let before;
|
|
91
|
+
let result;
|
|
92
|
+
let timing;
|
|
93
|
+
|
|
94
|
+
const inside = (spaceId, script, args = []) => place.exec(spaceId, ['/bin/sh', '-c', `PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin; ${script}`, 'sh', ...args], { timeoutMs: 120_000 });
|
|
95
|
+
const insideRepo = async (args) => (await inside(spec.id, 'cd "$1" && shift && git "$@"', [result.spacePath, ...args])).stdout;
|
|
96
|
+
const unchanged = () => unexpectedChanges(before, hostState(repo), { spaceIds });
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Whether nothing of the planted hook runs inside any more, asked until the limit inside has passed
|
|
100
|
+
* counted from the latest moment the push could have started: `started`, the docker calls before the
|
|
101
|
+
* push, two more round trips for the push's own `docker exec` to begin, and a few seconds. A receiver
|
|
102
|
+
* with no limit inside is still there at that bound.
|
|
103
|
+
*/
|
|
104
|
+
const insideEmptiesByItsLimit = async (spaceId, started, callsBeforePush) => {
|
|
105
|
+
const bound = started + timing.innerLimitMs + (callsBeforePush + 2) * timing.roundTripMs + 3000;
|
|
106
|
+
for (;;) {
|
|
107
|
+
if ((await inside(spaceId, COUNT_SLEEPERS)).stdout === '0\n') return true;
|
|
108
|
+
if (Date.now() > bound) return false;
|
|
109
|
+
await pause(500);
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
/** A space of its own for a test that plants something in it. */
|
|
114
|
+
const freshSpace = async (name) => {
|
|
115
|
+
const own = newSpec(name);
|
|
116
|
+
spaceIds.push(own.id);
|
|
117
|
+
await place.create(own);
|
|
118
|
+
return own;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
beforeAll(async () => {
|
|
122
|
+
({ place, dispose } = createLiveDockerPlace());
|
|
123
|
+
const host = createTestHost({ ownHome: false });
|
|
124
|
+
({ repo, g } = makeBait(host));
|
|
125
|
+
codeIn = host.codeIn(place);
|
|
126
|
+
await place.create(spec);
|
|
127
|
+
// The slowest of three round trips of `docker exec` into the space sizes the hanging tests.
|
|
128
|
+
let roundTripMs = 0;
|
|
129
|
+
for (let sample = 0; sample < 3; sample += 1) {
|
|
130
|
+
const sampleStarted = Date.now();
|
|
131
|
+
await place.exec(spec.id, ['/bin/true']);
|
|
132
|
+
roundTripMs = Math.max(roundTripMs, Date.now() - sampleStarted);
|
|
133
|
+
}
|
|
134
|
+
timing = hangTiming(roundTripMs);
|
|
135
|
+
before = hostState(repo);
|
|
136
|
+
result = await codeIn.bringCodeIn({ repository: repo, spaceId: spec.id });
|
|
137
|
+
}, CREATE_TIMEOUT_MS);
|
|
138
|
+
|
|
139
|
+
afterAll(async () => {
|
|
140
|
+
await dispose();
|
|
141
|
+
removeTestHosts();
|
|
142
|
+
}, CREATE_TIMEOUT_MS);
|
|
143
|
+
|
|
144
|
+
it('brings the bait in: the same status line for line, the exec bit, the identity, and nothing ignored', async () => {
|
|
145
|
+
expect(result.spacePath).toBe(`/spaces/${spec.id}/bait-repo-`);
|
|
146
|
+
expect(result.identityCopied).toEqual({ name: true, email: true });
|
|
147
|
+
expect(await insideRepo(['status', '--porcelain=v1', '--untracked-files=all'])).toBe(shortStatus(g));
|
|
148
|
+
expect(await insideRepo(['symbolic-ref', 'HEAD'])).toBe('refs/heads/main\n');
|
|
149
|
+
expect(await insideRepo(['rev-parse', 'HEAD'])).toBe(`${result.base}\n`);
|
|
150
|
+
expect(await insideRepo(['config', 'user.name'])).toBe('Bait Author\n');
|
|
151
|
+
expect(await insideRepo(['config', 'user.email'])).toBe('bait@example.invalid\n');
|
|
152
|
+
expect((await inside(spec.id, 'stat -c %a "$1/run.sh"', [result.spacePath])).stdout).toBe('755\n');
|
|
153
|
+
const present = await inside(spec.id, 'for f in .env node_modules secret-by-global-ignore.txt; do [ -e "$1/$f" ] && echo "$f"; done; true', [result.spacePath]);
|
|
154
|
+
expect(present.stdout).toBe('');
|
|
155
|
+
expect(unchanged()).toEqual([]);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('sends the history afterwards, unshallows the repository, and removes the side repository', async () => {
|
|
159
|
+
expect(await codeIn.sendHistory({ repository: repo, spaceId: spec.id, spacePath: result.spacePath, base: result.base })).toEqual({ status: 'sent' });
|
|
160
|
+
expect(await insideRepo(['rev-list', '--count', 'HEAD'])).toBe(g(['rev-list', '--count', 'HEAD']));
|
|
161
|
+
expect(await insideRepo(['rev-parse', '--is-shallow-repository'])).toBe('false\n');
|
|
162
|
+
expect((await inside(spec.id, '[ -e "$1" ] && echo there; true', [`/spaces/${spec.id}/.openchamber-history.git`])).stdout).toBe('');
|
|
163
|
+
// Nothing the agent sees changed: the same status as before the history came.
|
|
164
|
+
expect(await insideRepo(['status', '--porcelain=v1', '--untracked-files=all'])).toBe(shortStatus(g));
|
|
165
|
+
expect(unchanged()).toEqual([]);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// The secret is searched for on the host, in what was collected from the space. Sending it into
|
|
169
|
+
// the space as a search pattern would put it there.
|
|
170
|
+
it('puts the ignored .env secret nowhere in the space, files or git objects', async () => {
|
|
171
|
+
// Gzipped on the way out, because the space's HOME holds OpenCode's data and exec output is capped.
|
|
172
|
+
const collect = async (script, args) => {
|
|
173
|
+
const collected = await place.exec(spec.id, ['/bin/sh', '-c', `PATH=/usr/bin:/bin; ${script} | gzip -c | base64 -w 0`, 'sh', ...args], { timeoutMs: 120_000 });
|
|
174
|
+
expect(collected.code).toBe(0);
|
|
175
|
+
return zlib.gunzipSync(Buffer.from(collected.stdout, 'base64'));
|
|
176
|
+
};
|
|
177
|
+
const files = await collect('tar -C / -cf - "${1#/}" home/space', [`/spaces/${spec.id}`]);
|
|
178
|
+
const objects = await collect('git -C "$1" cat-file --batch-all-objects --batch', [result.spacePath]);
|
|
179
|
+
// Controls: an untracked file's text is in the files, and the text of a tracked file the bait
|
|
180
|
+
// deleted is in the objects and not in the compressed files, so the object dump is uncompressed.
|
|
181
|
+
expect(files.includes('untracked travels')).toBe(true);
|
|
182
|
+
expect(objects.includes(OBJECTS_ONLY_TEXT)).toBe(true);
|
|
183
|
+
expect(files.includes(OBJECTS_ONLY_TEXT)).toBe(false);
|
|
184
|
+
expect(files.includes(SECRET)).toBe(false);
|
|
185
|
+
expect(objects.includes(SECRET)).toBe(false);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('ends a history push that the space never answers at the host timeout, with no host process left and nothing inside after its limit', async () => {
|
|
189
|
+
const own = await freshSpace('Code in, hanging history');
|
|
190
|
+
const ownResult = await codeIn.bringCodeIn({ repository: repo, spaceId: own.id });
|
|
191
|
+
expect((await inside(own.id, PLANT_HANGING_HOOK)).code).toBe(0);
|
|
192
|
+
const started = Date.now();
|
|
193
|
+
|
|
194
|
+
const failure = await codeIn.sendHistory({ repository: repo, spaceId: own.id, spacePath: ownResult.spacePath, base: ownResult.base, timeoutMs: timing.hostTimeoutMs, innerMarginSeconds: timing.innerMarginSeconds }).catch((error) => error);
|
|
195
|
+
expect(failure).toMatchObject({ code: 'history_transfer_failed', details: { cause: 'command_timeout' } });
|
|
196
|
+
// The control: the receiving side really was held inside, and still is, because its limit is later.
|
|
197
|
+
expect(Number((await inside(own.id, COUNT_SLEEPERS)).stdout)).toBeGreaterThan(0);
|
|
198
|
+
expect(await hostProcessesGone(spaceResourceName(own.id, 'space'), started + timing.innerLimitMs)).toBe(true);
|
|
199
|
+
expect(await insideEmptiesByItsLimit(own.id, started, DOCKER_CALLS_BEFORE_HISTORY_PUSH), 'the receiver inside outlived the limit inside the space').toBe(true);
|
|
200
|
+
expect(unchanged()).toEqual([]);
|
|
201
|
+
}, CREATE_TIMEOUT_MS);
|
|
202
|
+
|
|
203
|
+
it('removes its host ref and leaves the host repository as it was when the space never answers the code-in push', async () => {
|
|
204
|
+
const own = await freshSpace('Code in, hanging');
|
|
205
|
+
expect((await inside(own.id, PLANT_HANGING_HOOK)).code).toBe(0);
|
|
206
|
+
const started = Date.now();
|
|
207
|
+
|
|
208
|
+
const failure = await codeIn.bringCodeIn({ repository: repo, spaceId: own.id, timeoutMs: timing.hostTimeoutMs, innerMarginSeconds: timing.innerMarginSeconds }).catch((error) => error);
|
|
209
|
+
expect(failure).toMatchObject({ code: 'code_transfer_failed', details: { step: 'the base commit', cause: 'command_timeout' } });
|
|
210
|
+
expect(g(['for-each-ref', `refs/openchamber/spaces/${own.id}/`])).toBe('');
|
|
211
|
+
expect(Number((await inside(own.id, COUNT_SLEEPERS)).stdout)).toBeGreaterThan(0);
|
|
212
|
+
expect(await hostProcessesGone(spaceResourceName(own.id, 'space'), started + timing.innerLimitMs)).toBe(true);
|
|
213
|
+
expect(await insideEmptiesByItsLimit(own.id, started, DOCKER_CALLS_BEFORE_CODE_IN_PUSH), 'the receiver inside outlived the limit inside the space').toBe(true);
|
|
214
|
+
expect(unchanged()).toEqual([]);
|
|
215
|
+
}, CREATE_TIMEOUT_MS);
|
|
216
|
+
});
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// The contract every place must pass. Stage 8 to 10 places run this same suite.
|
|
2
|
+
// `setup` resolves `{ place, dispose }`. `dispose` cleans up and may assert that nothing is left.
|
|
3
|
+
|
|
4
|
+
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
|
5
|
+
|
|
6
|
+
import { createSpaceId, hashProjectDirectory } from '../labels.js';
|
|
7
|
+
import { runCommand } from '../run-command.js';
|
|
8
|
+
import { REQUIRED_PLACE_METHODS } from './registry.js';
|
|
9
|
+
|
|
10
|
+
const CREATE_TIMEOUT_MS = 25 * 60_000;
|
|
11
|
+
|
|
12
|
+
export function runPlaceContractSuite(title, { enabled = true, setup }) {
|
|
13
|
+
describe.skipIf(!enabled)(`place contract: ${title}`, () => {
|
|
14
|
+
const spec = {
|
|
15
|
+
id: createSpaceId(),
|
|
16
|
+
name: 'Contract suite, a=b',
|
|
17
|
+
project: hashProjectDirectory('/contract/suite/project'),
|
|
18
|
+
created: new Date().toISOString(),
|
|
19
|
+
memoryBytes: 512 * 1024 * 1024,
|
|
20
|
+
};
|
|
21
|
+
let place;
|
|
22
|
+
let dispose = async () => {};
|
|
23
|
+
|
|
24
|
+
const listed = async () => (await place.list()).find((space) => space.id === spec.id);
|
|
25
|
+
|
|
26
|
+
beforeAll(async () => {
|
|
27
|
+
({ place, dispose } = await setup());
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
afterAll(async () => {
|
|
31
|
+
await dispose();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('has an id and every operation', () => {
|
|
35
|
+
expect(place.id).toMatch(/\S/);
|
|
36
|
+
for (const method of REQUIRED_PLACE_METHODS) {
|
|
37
|
+
expect(place[method]).toBeInstanceOf(Function);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('is available', async () => {
|
|
42
|
+
expect(await place.check()).toMatchObject({ available: true });
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('creates a space that verifies clean', async () => {
|
|
46
|
+
await place.create(spec);
|
|
47
|
+
expect(await place.verify(spec.id)).toEqual([]);
|
|
48
|
+
}, CREATE_TIMEOUT_MS);
|
|
49
|
+
|
|
50
|
+
it('lists the space as running, with what it was created with', async () => {
|
|
51
|
+
const { id, name, project, created } = spec;
|
|
52
|
+
expect(await listed()).toEqual({ id, name, project, created, state: 'running', orphans: [], damaged: false, missing: [] });
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('refuses to create the same space again, and leaves it alone', async () => {
|
|
56
|
+
await expect(place.create(spec)).rejects.toMatchObject({ code: 'space_name_taken' });
|
|
57
|
+
expect(await listed()).toMatchObject({ state: 'running' });
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('runs commands as uid 1000', async () => {
|
|
61
|
+
const result = await place.exec(spec.id, ['id', '-u']);
|
|
62
|
+
expect(result).toMatchObject({ code: 0, stdout: '1000\n' });
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// Added in stage 2. From here on a place cannot host a space without a gatekeeper, so this
|
|
66
|
+
// is part of the contract every later place is held to. Nothing that was here changed.
|
|
67
|
+
it('runs a command in the gatekeeper, which is a container of its own', async () => {
|
|
68
|
+
// Two containers, not one target answering for both: a hostname is per container.
|
|
69
|
+
const space = await place.exec(spec.id, ['cat', '/etc/hostname']);
|
|
70
|
+
const gatekeeper = await place.exec(spec.id, ['cat', '/etc/hostname'], { target: 'gatekeeper' });
|
|
71
|
+
expect(space).toMatchObject({ code: 0 });
|
|
72
|
+
expect(gatekeeper).toMatchObject({ code: 0 });
|
|
73
|
+
expect(space.stdout.trim()).not.toBe('');
|
|
74
|
+
expect(gatekeeper.stdout.trim()).not.toBe('');
|
|
75
|
+
expect(gatekeeper.stdout.trim()).not.toBe(space.stdout.trim());
|
|
76
|
+
|
|
77
|
+
// And it is not the space: the space's work directory is not in it.
|
|
78
|
+
const work = `/spaces/${spec.id}`;
|
|
79
|
+
expect(await place.exec(spec.id, ['test', '-d', work])).toMatchObject({ code: 0 });
|
|
80
|
+
expect((await place.exec(spec.id, ['test', '-d', work], { target: 'gatekeeper' })).code).not.toBe(0);
|
|
81
|
+
expect(await place.exec(spec.id, ['id', '-u'], { target: 'gatekeeper' })).toMatchObject({ code: 0, stdout: '1000\n' });
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// Added in stage 2. The place contract names two targets, and a place that quietly took a
|
|
85
|
+
// third would be running commands somewhere this suite never looks.
|
|
86
|
+
it('knows the two exec targets and no others', async () => {
|
|
87
|
+
for (const target of ['setup', 'host', '', 'space-old']) {
|
|
88
|
+
await expect(place.exec(spec.id, ['id', '-u'], { target })).rejects.toMatchObject({ code: 'invalid_exec_target' });
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// Added in stage 3a. Code travels into a space by `git push` over `ext::`, and git starts that
|
|
93
|
+
// process itself, so a place hands out the argv that reaches the space. It must reach the very
|
|
94
|
+
// container `exec` reaches, and never the gatekeeper. It goes into a git URL, which cannot carry
|
|
95
|
+
// an empty argument or a control character. The user is not checked here: a Docker space already
|
|
96
|
+
// runs as uid 1000 by default, so no check through the argv could fail. Each place proves its
|
|
97
|
+
// own user flag with an exact-argv test, as docker.test.js does.
|
|
98
|
+
it('hands out an argv that runs a command in the space container', async () => {
|
|
99
|
+
const argv = await place.execArgv(spec.id);
|
|
100
|
+
expect(argv.length).toBeGreaterThan(0);
|
|
101
|
+
for (const argument of argv) {
|
|
102
|
+
expect(argument).toEqual(expect.any(String));
|
|
103
|
+
expect(argument).not.toBe('');
|
|
104
|
+
expect(argument).not.toMatch(/[\x00-\x1f\x7f]/);
|
|
105
|
+
}
|
|
106
|
+
const [file, ...args] = argv;
|
|
107
|
+
const run = (command) => runCommand(file, [...args, ...command], { timeoutMs: 60_000 });
|
|
108
|
+
const hostname = await run(['cat', '/etc/hostname']);
|
|
109
|
+
expect(hostname).toMatchObject({ code: 0 });
|
|
110
|
+
expect(hostname.stdout).toBe((await place.exec(spec.id, ['cat', '/etc/hostname'])).stdout);
|
|
111
|
+
expect(hostname.stdout).not.toBe((await place.exec(spec.id, ['cat', '/etc/hostname'], { target: 'gatekeeper' })).stdout);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('stops the space and lists it as exited', async () => {
|
|
115
|
+
await place.stop(spec.id);
|
|
116
|
+
expect(await listed()).toMatchObject({ state: 'exited' });
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// Added in stage 3a. git starts the argv itself, and a stopped container answers it only with
|
|
120
|
+
// the runtime's own words, so a place refuses a stopped space here with a code of its own.
|
|
121
|
+
it('hands out no argv for a stopped space', async () => {
|
|
122
|
+
await expect(place.execArgv(spec.id)).rejects.toMatchObject({ code: 'space_not_running' });
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('starts the space again', async () => {
|
|
126
|
+
await place.start(spec.id);
|
|
127
|
+
expect(await listed()).toMatchObject({ state: 'running' });
|
|
128
|
+
expect(await place.verify(spec.id)).toEqual([]);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('removes the space and leaves no labelled resource behind', async () => {
|
|
132
|
+
const result = await place.remove(spec.id);
|
|
133
|
+
expect(result.failed).toEqual([]);
|
|
134
|
+
// `list` also reports orphaned networks and volumes, so an absent id means nothing is left.
|
|
135
|
+
expect(await listed()).toBeUndefined();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// Added in stage 2, for the same reason as the gatekeeper exec above. The test before this
|
|
139
|
+
// one ran a command in the gatekeeper, so this failure is the removal and nothing else.
|
|
140
|
+
// A place that removed the space and left its gatekeeper running would still answer here.
|
|
141
|
+
it('removes the gatekeeper with the space', async () => {
|
|
142
|
+
await expect(place.exec(spec.id, ['cat', '/etc/hostname'], { target: 'gatekeeper' })).rejects.toThrow();
|
|
143
|
+
await expect(place.exec(spec.id, ['cat', '/etc/hostname'])).rejects.toThrow();
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// Added in stage 3a. The argv comes after the same ownership check as `exec`, so a space that
|
|
147
|
+
// is gone gets none. A place that built it from the id alone would still hand one out here.
|
|
148
|
+
it('hands out no argv for a space that is gone', async () => {
|
|
149
|
+
await expect(place.execArgv(spec.id)).rejects.toThrow();
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('treats removing a missing space as done', async () => {
|
|
153
|
+
expect(await place.remove(spec.id)).toEqual({ removed: [], failed: [] });
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { runPlaceContractSuite } from './contract-suite.js';
|
|
2
|
+
import { LIVE_DOCKER_ENABLED, createLiveDockerPlace } from './docker-live-support.js';
|
|
3
|
+
|
|
4
|
+
runPlaceContractSuite('docker (live)', {
|
|
5
|
+
enabled: LIVE_DOCKER_ENABLED,
|
|
6
|
+
setup: async () => createLiveDockerPlace(),
|
|
7
|
+
});
|