@vantran-se/openchamber-web 2.1.1 → 2.2.0
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 +32 -24
- package/bin/cli.js +1 -3
- package/bin/cli.test.js +40 -16
- package/bin/lib/DOCUMENTATION.md +6 -5
- package/bin/lib/cli-args.js +6 -1
- package/bin/lib/cli-args.test.js +34 -0
- package/bin/lib/cli-lifecycle.js +0 -1
- package/bin/lib/cli-network.js +8 -2
- package/bin/lib/cli-process.js +1 -1
- package/bin/lib/cli-settings-accessors.test.js +21 -13
- package/bin/lib/cli-startup.js +17 -11
- package/bin/lib/cli-startup.test.js +92 -2
- package/bin/lib/commands-connect-url.js +8 -1
- package/bin/lib/commands-schedule.js +8 -2
- package/bin/lib/commands-serve.js +15 -21
- package/bin/lib/commands-update.js +12 -24
- package/bin/lib/commands-update.test.js +14 -46
- package/dist/assets/DiffView-Cv9MSXl1.js +4 -0
- package/dist/assets/FilePreviewCommentMenu-D4kLseHj.js +4 -0
- package/dist/assets/FilesView-BVoD8I2x.js +14 -0
- package/dist/assets/{GitView-DzuzhIPx.js → GitView-Bzb8p-ew.js} +4 -4
- package/dist/assets/GuestSurfacePane-Crdbvsxo.js +1 -0
- package/dist/assets/InProgressOperationBanner-CwMjNQgk.js +2 -0
- package/dist/assets/{JsonTreeView-B_EPRjzy.js → JsonTreeView-DRKHZfsn.js} +1 -1
- package/dist/assets/MarkdownImageGallery-DZtqT1Zn.js +1 -0
- package/dist/assets/MarkdownRendererImpl-CqG7HCNd.js +10 -0
- package/dist/assets/OnboardingScreen-DMIxwYK5.js +1 -0
- package/dist/assets/{PatchDiff-6yh1csza.js → PatchDiff-CUcCg1jk.js} +1 -1
- package/dist/assets/PierreDiffViewer-CnnR-dnD.js +228 -0
- package/dist/assets/PlanView-RCBadcY7.js +5 -0
- package/dist/assets/SettingsView-CjBjZFRO.js +30 -0
- package/dist/assets/SettingsWindow-DyM7QuhI.js +1 -0
- package/dist/assets/SubmoduleDiffSummary-BWIfgMED.js +1 -0
- package/dist/assets/ToolOutputDialog-Q0P3FuAi.js +15 -0
- package/dist/assets/ToolPartDiffPreview-p7qR9FpU.js +8 -0
- package/dist/assets/{VirtualizedFileDiff-BTJVRkyo.js → VirtualizedFileDiff-CFpjL8Ns.js} +3 -3
- package/dist/assets/{Virtualizer-xCEYz34p.js → Virtualizer-C0IsclZn.js} +1 -1
- package/dist/assets/WalkthroughView-DI31zD02.js +4 -0
- package/dist/assets/appThemeRegistry-B9gkFq9i.js +1 -0
- package/dist/assets/{computeVirtualFileMetrics-CmPe29DM.js → computeVirtualFileMetrics-C2WM6JC2.js} +2 -2
- package/dist/assets/de-YKfk5MmC.js +14 -0
- package/dist/assets/es-_gwT0kr-.js +20 -0
- package/dist/assets/fileTreeStatus-D438yi8M.js +1 -0
- package/dist/assets/fr-DqzvXmIm.js +18 -0
- package/dist/assets/index-BLaHKdtx.js +1 -0
- package/dist/assets/index-BkMXRrVQ.css +1 -0
- package/dist/assets/index-DxNa7xYB.js +65 -0
- package/dist/assets/initializeSharedPreferences-Bck5EAWP.js +1 -0
- package/dist/assets/{isDiffPlainText-CQmDCnEv.js → isDiffPlainText-Dgbw8VKk.js} +1 -1
- package/dist/assets/{isFilePlainText-CYBUzW4I.js → isFilePlainText-_ZS5UYRj.js} +1 -1
- package/dist/assets/ja-B-GFYhQE.js +20 -0
- package/dist/assets/ko-mzksfqDC.js +20 -0
- package/dist/assets/main-C5Xx8dn_.js +2 -0
- package/dist/assets/main-CfBSQM1H.js +990 -0
- package/dist/assets/markdownCore-BmWwW1ll.js +7 -0
- package/dist/assets/miniChat-mnKwy4vQ.js +2 -0
- package/dist/assets/mobile-DeEQd0wZ.js +2 -0
- package/dist/assets/nl-CWQr6NSq.js +20 -0
- package/dist/assets/{parseDiffFromFile-Cy0PNxun.js → parseDiffFromFile-BUgn-T9p.js} +2 -2
- package/dist/assets/patchFileDiff-Dt8LhgBh.js +13 -0
- package/dist/assets/pl-UmRcDUX2.js +20 -0
- package/dist/assets/pt-BR-BSLybvVF.js +20 -0
- package/dist/assets/relocateRestoredSession-B5E9xb3T.js +1 -0
- package/dist/assets/renderElectronMiniChatApp-C0a9A5_J.js +2 -0
- package/dist/assets/renderMobileApp-BNncg3Pk.js +25 -0
- package/dist/assets/runtimeEndpointReset-CZ5aafpl.js +62 -0
- package/dist/assets/{selectionMarkdown-TkYk5h9l.js → selectionMarkdown-CUN3wUld.js} +9 -9
- package/dist/assets/{sessionOwnership-Bs1xbRfB.js → sessionOwnership-BSuxCEhb.js} +1 -1
- package/dist/assets/shikiHighlight-DHDx-6eM.js +1 -0
- package/dist/assets/space-apply-DM_2pRW7.js +1 -0
- package/dist/assets/space-close-Bpomt8z0.js +1 -0
- package/dist/assets/{surface-B6n7wJtt.js → surface-C-jQaGnL.js} +1 -1
- package/dist/assets/{textMateThemeFromAppTheme-CDlKffWz.js → textMateThemeFromAppTheme-DlsTphbY.js} +1 -1
- package/dist/assets/tr-gaTrX_HK.js +11 -0
- package/dist/assets/uk-CnJF6ykl.js +20 -0
- package/dist/assets/{useAppFontEffects-DCqCVGlT.js → useAppFontEffects-DGlvOqP_.js} +194 -233
- package/dist/assets/useInlineCommentController-kMN4Wg_D.js +2 -0
- package/dist/assets/{useRootScrollLock-BowwS8kq.js → useRootScrollLock-DMPpvpNP.js} +2 -2
- package/dist/assets/{vendor-base-ui-DX96QhTP.js → vendor-base-ui-Det7U81h.js} +1 -1
- package/dist/assets/{vendor-codemirror-autocomplete-DW1FQz9_.js → vendor-codemirror-autocomplete-BSHQHbKu.js} +1 -1
- package/dist/assets/{vendor-codemirror-commands-DpmhalwN.js → vendor-codemirror-commands-C4G_dPWe.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-angular-BqExbN1M.js → vendor-codemirror-lang-angular-B6LbBGHB.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-cpp-DZA36HfS.js → vendor-codemirror-lang-cpp-BWWXBx7W.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-css-B07blnnZ.js → vendor-codemirror-lang-css-Cayfb29q.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-go-BM8K9D1-.js → vendor-codemirror-lang-go-BoKlCNKw.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-html-DaH9adfE.js → vendor-codemirror-lang-html-BQWerHsJ.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-java-CsdC0viz.js → vendor-codemirror-lang-java-DWX7Fq_K.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-javascript-CpGYXGuo.js → vendor-codemirror-lang-javascript-DbpEcZM0.js} +2 -2
- package/dist/assets/{vendor-codemirror-lang-jinja-BBeGzXw5.js → vendor-codemirror-lang-jinja-D1JIJRA7.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-json-58T70Hqs.js → vendor-codemirror-lang-json-BzgckzmB.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-less-BI-xCA59.js → vendor-codemirror-lang-less-B9EpDXFi.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-liquid-zEcMa-zy.js → vendor-codemirror-lang-liquid-9U7kRbSf.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-markdown-BLKnlLP4.js → vendor-codemirror-lang-markdown-D2TEBIvZ.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-php-9f35M83-.js → vendor-codemirror-lang-php-BT6sDVzE.js} +1 -1
- package/dist/assets/vendor-codemirror-lang-python-jAMtYiA9.js +3 -0
- package/dist/assets/{vendor-codemirror-lang-rust-MNDL_jZQ.js → vendor-codemirror-lang-rust-CVO56EiL.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-sass-p9epsSNm.js → vendor-codemirror-lang-sass-DZlOoUWu.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-sql-Bqzh0EXX.js → vendor-codemirror-lang-sql-fZ6t1DPw.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-vue-BnNUWV-p.js → vendor-codemirror-lang-vue-DxbqrLiE.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-wast-C2a-oLmk.js → vendor-codemirror-lang-wast-DyU1Gt-I.js} +1 -1
- package/dist/assets/vendor-codemirror-lang-xml-C9a_K5_p.js +1 -0
- package/dist/assets/{vendor-codemirror-lang-yaml-C0sjswB0.js → vendor-codemirror-lang-yaml-Cptq8l0e.js} +1 -1
- package/dist/assets/{vendor-codemirror-language-ByBn5zIW.js → vendor-codemirror-language-DufXoQvE.js} +2 -2
- package/dist/assets/vendor-codemirror-language-data-CtwSWKtX.js +2 -0
- package/dist/assets/{vendor-codemirror-search-ib-sHD5W.js → vendor-codemirror-search-CvXM_pZg.js} +1 -1
- package/dist/assets/{vendor-codemirror-state-G3UuR-be.js → vendor-codemirror-state-Dek9sMtw.js} +1 -1
- package/dist/assets/{vendor-codemirror-view-Bniqp7p5.js → vendor-codemirror-view-1e0FQ6Wn.js} +1 -1
- package/dist/assets/vendor-framer-motion-DxDVhOLc.js +9 -0
- package/dist/assets/vendor-katex-CuQvj9-L.js +257 -0
- package/dist/assets/vendor-katex-DdZNUVNG.css +1 -0
- package/dist/assets/{vendor-motion-dom-DcCgeG69.js → vendor-motion-dom-HXPOMxb7.js} +1 -1
- package/dist/assets/{vendor-opencode-client-DHzhmLwV.js → vendor-opencode-client-BmFcUkbQ.js} +1 -1
- package/dist/assets/{vendor-replit-codemirror-vim-DkzqxN2H.js → vendor-replit-codemirror-vim-BlJRn-cW.js} +2 -2
- package/dist/assets/{vendor-tanstack-react-virtual-H_JmXOFV.js → vendor-tanstack-react-virtual-CvuLsk7D.js} +1 -1
- package/dist/assets/{vendor-tanstack-virtual-core-Dxc8hSTB.js → vendor-tanstack-virtual-core-DPaNcJcT.js} +1 -1
- package/dist/assets/zh-CN-t2vigSB1.js +20 -0
- package/dist/assets/zh-TW-jAgqBokB.js +20 -0
- package/dist/index.html +6 -6
- package/dist/mini-chat.html +6 -6
- package/dist/mobile.html +6 -6
- package/package.json +8 -5
- package/server/index.js +80 -36
- package/server/lib/agent-memory/actions.js +1 -6
- package/server/lib/agent-memory/routes.js +1 -2
- package/server/lib/agent-memory/runtime.js +1 -8
- package/server/lib/agent-tool/DOCUMENTATION.md +7 -3
- package/server/lib/agent-tool/runtime.js +3 -6
- package/server/lib/agent-tool/runtime.test.js +0 -2
- package/server/lib/browser-control/DOCUMENTATION.md +6 -0
- package/server/lib/changelog/update-notes.js +1 -1
- package/server/lib/context-obligatory/runtime.js +1 -2
- package/server/lib/dev-tunnel/DOCUMENTATION.md +8 -0
- package/server/lib/dev-tunnel/client.js +21 -10
- package/server/lib/dev-tunnel/tunnel.test.js +34 -0
- package/server/lib/dispatch-results/runtime.js +1 -1
- package/server/lib/enterprise-mode.d.ts +1 -0
- package/server/lib/enterprise-mode.js +13 -2
- package/server/lib/enterprise-mode.test.js +21 -0
- package/server/lib/event-stream/DOCUMENTATION.md +2 -2
- package/server/lib/event-stream/index.js +1 -3
- package/server/lib/event-stream/translate-v2.js +1 -9
- package/server/lib/event-stream/translate-v2.test.js +0 -11
- package/server/lib/fs/DOCUMENTATION.md +5 -2
- package/server/lib/fs/routes.js +62 -15
- package/server/lib/fs/routes.test.js +278 -48
- package/server/lib/fs/workspace-file-names.js +104 -0
- package/server/lib/fs/workspace-file-names.test.js +62 -0
- package/server/lib/git/DOCUMENTATION.md +17 -5
- package/server/lib/git/contributor-provenance-storage.js +9 -6
- package/server/lib/git/contributor-provenance-storage.test.js +29 -0
- package/server/lib/git/credential-broker.js +1 -1
- package/server/lib/git/credential-resolver.js +1 -4
- package/server/lib/git/credentials.js +1 -0
- package/server/lib/git/discovery-endpoint.js +1 -1
- package/server/lib/git/identity-provisioning.js +2 -1
- package/server/lib/git/identity-storage.js +3 -4
- package/server/lib/git/lfs-discovery.js +14 -14
- package/server/lib/git/lfs-discovery.test.js +16 -6
- package/server/lib/git/network-operation-plan.js +50 -62
- package/server/lib/git/network-operation-plan.test.js +103 -1
- package/server/lib/git/network-operation-registry.js +4 -6
- package/server/lib/git/network-operation-storage.js +4 -6
- package/server/lib/git/network-operations.js +12 -4
- package/server/lib/git/network-operations.test.js +19 -6
- package/server/lib/git/redaction.js +2 -1
- package/server/lib/git/repository-credential-runtime.js +5 -3
- package/server/lib/git/repository-credential-runtime.test.js +22 -0
- package/server/lib/git/routes.js +31 -137
- package/server/lib/git/routes.test.js +2 -2
- package/server/lib/git/service.js +143 -27
- package/server/lib/git/service.test.js +215 -1
- package/server/lib/git/ssh-credential-storage.js +1 -4
- package/server/lib/git/submodule-discovery.js +1 -1
- package/server/lib/git/worktree-bootstrap-storage.js +3 -4
- package/server/lib/git/worktree-bootstrap-storage.test.js +15 -0
- package/server/lib/git/worktree-creation.js +194 -0
- package/server/lib/git/worktree-creation.test.js +155 -0
- package/server/lib/github/DOCUMENTATION.md +11 -6
- package/server/lib/github/auth.js +2 -4
- package/server/lib/github/device-flow.js +2 -2
- package/server/lib/github/gh-cli-credential.js +1 -0
- package/server/lib/github/octokit.js +31 -0
- package/server/lib/github/pr-status.js +35 -18
- package/server/lib/github/pr-status.test.js +271 -3
- package/server/lib/github/reference-search.js +102 -13
- package/server/lib/github/reference-search.test.js +48 -7
- package/server/lib/github/routes.auth.test.js +18 -0
- package/server/lib/github/routes.js +360 -6
- package/server/lib/github/routes.mutations.test.js +239 -0
- package/server/lib/github/routes.test.js +134 -2
- package/server/lib/gitlab/DOCUMENTATION.md +5 -1
- package/server/lib/gitlab/auth-storage.test.js +1 -0
- package/server/lib/gitlab/mappers.js +30 -1
- package/server/lib/gitlab/resources.js +230 -6
- package/server/lib/gitlab/resources.test.js +179 -0
- package/server/lib/gitlab/routes.js +200 -6
- package/server/lib/gitlab/routes.test.js +186 -2
- package/server/lib/gitlab/validation.js +1 -5
- package/server/lib/guests/DOCUMENTATION.md +1 -1
- package/server/lib/guests/oauth.js +2 -2
- package/server/lib/guests/service.js +1 -1
- package/server/lib/guests/sockets.js +1 -1
- package/server/lib/injected-env.d.ts +8 -0
- package/server/lib/injected-env.js +50 -0
- package/server/lib/injected-env.test.js +56 -0
- package/server/lib/linear/DOCUMENTATION.md +5 -6
- package/server/lib/linear/issues.js +62 -4
- package/server/lib/linear/issues.test.js +68 -0
- package/server/lib/linear/oauth.js +5 -5
- package/server/lib/linear/parse.js +2 -9
- package/server/lib/message-queue/DOCUMENTATION.md +23 -1
- package/server/lib/message-queue/runtime.js +85 -12
- package/server/lib/message-queue/runtime.test.js +172 -2
- package/server/lib/notifications/apns-runtime.js +5 -6
- package/server/lib/notifications/apns-runtime.test.js +20 -4
- package/server/lib/openchamber-control/DOCUMENTATION.md +19 -1
- package/server/lib/openchamber-control/actions.js +2 -1
- package/server/lib/openchamber-control/service.js +119 -20
- package/server/lib/openchamber-control/service.test.js +166 -0
- package/server/lib/openchamber-sessions/archive-store.js +1 -6
- package/server/lib/openchamber-sessions/routes.js +47 -12
- package/server/lib/openchamber-sessions/routes.test.js +65 -2
- package/server/lib/openchamber-sessions/session-metadata-store.js +1 -6
- package/server/lib/opencode/DOCUMENTATION.md +22 -8
- package/server/lib/opencode/agents.js +62 -0
- package/server/lib/opencode/auth-state-runtime.js +3 -1
- package/server/lib/opencode/auth.d.ts +1 -1
- package/server/lib/opencode/auth.js +8 -0
- package/server/lib/opencode/auth.test.js +31 -0
- package/server/lib/opencode/config-entity-routes.js +16 -0
- package/server/lib/opencode/config-entity-v2.test.js +16 -1
- package/server/lib/opencode/config-paths.test.js +2 -2
- package/server/lib/opencode/config-v2.js +2 -4
- package/server/lib/opencode/feature-routes-runtime.js +89 -49
- package/server/lib/opencode/lifecycle.js +41 -21
- package/server/lib/opencode/lifecycle.test.js +50 -2
- package/server/lib/opencode/model-discovery.js +1 -1
- package/server/lib/opencode/model-discovery.test.js +17 -0
- package/server/lib/opencode/models-metadata.js +0 -2
- package/server/lib/opencode/openchamber-routes.test.js +3 -3
- package/server/lib/opencode/plugins.js +6 -9
- package/server/lib/opencode/provider-source-route.test.js +101 -3
- package/server/lib/opencode/providers-disabled.test.js +59 -0
- package/server/lib/opencode/providers.js +34 -0
- package/server/lib/opencode/proxy.js +11 -3
- package/server/lib/opencode/routes.js +40 -2
- package/server/lib/opencode/settings-helpers.js +27 -1
- package/server/lib/opencode/settings-helpers.test.js +19 -7
- package/server/lib/opencode/settings-registry.json +27 -16
- package/server/lib/opencode/settings-runtime.test.js +18 -0
- package/server/lib/opencode/shared-service-runtime.js +23 -104
- package/server/lib/opencode/shared.js +77 -46
- package/server/lib/opencode/shared.test.js +12 -12
- package/server/lib/opencode/shutdown-runtime.test.js +2 -3
- package/server/lib/opencode/skill-routes.js +123 -38
- package/server/lib/opencode/skill-routes.test.js +201 -0
- package/server/lib/opencode/skills.js +81 -34
- package/server/lib/opencode/skills.test.js +176 -21
- package/server/lib/opencode/startup-pipeline-runtime.js +6 -3
- package/server/lib/opencode/startup-pipeline-runtime.test.js +2 -1
- package/server/lib/opencode/watcher.test.js +0 -54
- package/server/lib/outbound-proxy.js +52 -0
- package/server/lib/outbound-proxy.test.js +55 -0
- package/server/lib/package-manager.js +23 -17
- package/server/lib/package-manager.test.js +5 -18
- package/server/lib/permission-auto-accept/DOCUMENTATION.md +1 -1
- package/server/lib/permission-auto-accept/runtime.js +19 -47
- package/server/lib/permission-auto-accept/runtime.test.js +2 -2
- package/server/lib/project-context/DOCUMENTATION.md +7 -1
- package/server/lib/project-context/routes.js +1 -2
- package/server/lib/project-context/runtime.js +12 -14
- package/server/lib/project-context/runtime.test.js +78 -2
- package/server/lib/projects/project-config.js +30 -15
- package/server/lib/projects/project-config.test.js +30 -0
- package/server/lib/projects/project-setup.js +1 -2
- package/server/lib/projects/routes.js +1 -2
- package/server/lib/quota/DOCUMENTATION.md +35 -7
- package/server/lib/quota/index.js +1 -0
- package/server/lib/quota/providers/claude/index.js +1 -1
- package/server/lib/quota/providers/cline-pass.js +1 -1
- package/server/lib/quota/providers/exe-dev.js +1 -1
- package/server/lib/quota/providers/google/index.js +1 -1
- package/server/lib/quota/providers/hyper.js +1 -1
- package/server/lib/quota/providers/kimi.js +116 -36
- package/server/lib/quota/providers/kimi.test.js +128 -3
- package/server/lib/quota/providers/opencode-go.js +144 -21
- package/server/lib/quota/providers/opencode-go.test.js +229 -6
- package/server/lib/quota/providers/openrouter.js +1 -1
- package/server/lib/quota/providers/zenmux.js +1 -1
- package/server/lib/quota/utils/auth.js +1 -0
- package/server/lib/quota/utils/transformers.js +2 -6
- package/server/lib/relay/DOCUMENTATION.md +18 -3
- package/server/lib/relay/e2ee.js +7 -7
- package/server/lib/relay/host-client.js +84 -13
- package/server/lib/relay/host-client.test.js +201 -1
- package/server/lib/relay/identity.js +6 -29
- package/server/lib/relay/identity.test.js +18 -28
- package/server/lib/relay/key-store.js +141 -0
- package/server/lib/relay/key-store.test.js +138 -0
- package/server/lib/relay/service.js +8 -8
- package/server/lib/relay/service.test.js +10 -5
- package/server/lib/relay/signing-key.js +13 -36
- package/server/lib/relay/tunnel-codec.js +10 -10
- package/server/lib/route-module-exports.test.js +22 -0
- package/server/lib/routing/DOCUMENTATION.md +23 -3
- package/server/lib/routing/cache.js +87 -0
- package/server/lib/routing/defaults.js +11 -0
- package/server/lib/routing/history.js +12 -4
- package/server/lib/routing/runtime.js +39 -3
- package/server/lib/routing/runtime.test.js +81 -2
- package/server/lib/routing/store.js +12 -1
- package/server/lib/routing/store.test.js +13 -0
- package/server/lib/scheduled-tasks/DOCUMENTATION.md +59 -1
- package/server/lib/scheduled-tasks/existing-session.js +119 -0
- package/server/lib/scheduled-tasks/existing-session.test.js +350 -0
- package/server/lib/scheduled-tasks/loops.js +1 -8
- package/server/lib/scheduled-tasks/routes.js +8 -8
- package/server/lib/scheduled-tasks/runtime.js +103 -13
- package/server/lib/scheduled-tasks/runtime.test.js +48 -0
- package/server/lib/scheduled-tasks/service.js +33 -11
- package/server/lib/scheduled-tasks/service.test.js +128 -2
- package/server/lib/scheduled-tasks/session-defaults.js +27 -0
- package/server/lib/scheduled-tasks/session-defaults.test.js +19 -0
- package/server/lib/session-assist/context.js +1 -0
- package/server/lib/session-folders/routes.js +2 -2
- package/server/lib/session-goal/objectives.js +1 -1
- package/server/lib/session-knowledge/routes.js +2 -2
- package/server/lib/session-knowledge/runtime.js +1 -2
- package/server/lib/session-work/DOCUMENTATION.md +39 -14
- package/server/lib/session-work/questions.js +31 -2
- package/server/lib/session-work/runtime.js +22 -13
- package/server/lib/session-work/runtime.test.js +43 -26
- package/server/lib/session-work/state.js +10 -4
- package/server/lib/session-work/state.test.js +8 -1
- package/server/lib/shared/DOCUMENTATION.md +89 -0
- package/server/lib/shared/guards.js +53 -0
- package/server/lib/shared/guards.test.js +112 -0
- package/server/lib/skills-catalog/DOCUMENTATION.md +10 -8
- package/server/lib/skills-catalog/cache.js +3 -2
- package/server/lib/skills-catalog/curated-sources.js +5 -1
- package/server/lib/skills-catalog/git.js +107 -20
- package/server/lib/skills-catalog/git.test.js +78 -0
- package/server/lib/skills-catalog/install.js +21 -12
- package/server/lib/skills-catalog/scan.js +25 -15
- package/server/lib/skills-catalog/source.js +26 -1
- package/server/lib/skills-catalog/source.test.js +27 -0
- package/server/lib/source-control/DOCUMENTATION.md +2 -1
- package/server/lib/source-control/audit-storage.js +7 -6
- package/server/lib/source-control/audit-storage.test.js +11 -0
- package/server/lib/source-control/binding-service.js +2 -5
- package/server/lib/source-control/change-request-worktree.js +90 -0
- package/server/lib/source-control/change-request-worktree.test.js +121 -0
- package/server/lib/source-control/mutation-executor.js +1 -1
- package/server/lib/source-control/mutation-storage.js +11 -5
- package/server/lib/source-control/provider-instance.js +1 -2
- package/server/lib/source-control/repository-identity.js +1 -1
- package/server/lib/source-control/routes.js +14 -2
- package/server/lib/spaces/DOCUMENTATION.md +42 -17
- package/server/lib/spaces/command-stream.test.js +15 -0
- package/server/lib/spaces/dispatcher.js +7 -0
- package/server/lib/spaces/dispatcher.test.js +17 -0
- package/server/lib/spaces/hardening.js +2 -2
- package/server/lib/spaces/hardening.test.js +1 -1
- package/server/lib/spaces/host.js +2 -0
- package/server/lib/spaces/journey.js +66 -5
- package/server/lib/spaces/journey.test.js +95 -4
- package/server/lib/spaces/layout.js +1 -1
- package/server/lib/spaces/places/docker-disk.js +38 -14
- package/server/lib/spaces/places/docker-disk.test.js +80 -1
- package/server/lib/spaces/places/docker.js +35 -11
- package/server/lib/spaces/places/fake-docker.js +22 -3
- package/server/lib/spaces/places/memory-place.js +3 -0
- package/server/lib/spaces/places/packed.docker.live.test.js +1 -1
- package/server/lib/spaces/routes.js +5 -0
- package/server/lib/spaces/routes.test.js +4 -2
- package/server/lib/spaces/run-command.js +9 -2
- package/server/lib/spaces/tools.js +4 -5
- package/server/lib/spaces/tools.test.js +2 -2
- package/server/lib/terminal/runtime.js +18 -6
- package/server/lib/terminal/runtime.test.js +66 -0
- package/server/lib/tts/index.js +3 -0
- package/server/lib/tts/service.js +4 -2
- package/server/lib/walkthrough/DOCUMENTATION.md +3 -1
- package/server/lib/walkthrough/index.js +2 -3
- package/server/lib/walkthrough/pr-source.test.js +22 -0
- package/server/lib/walkthrough/sources.js +6 -1
- package/server/opencode-proxy.test.js +127 -0
- package/bin/lib/commands-startup.test.js +0 -15
- package/dist/assets/CodeMirrorCommentWidgets-CVDSjcrP.js +0 -1
- package/dist/assets/DiffView-Ct7vL3fD.js +0 -4
- package/dist/assets/FilesView-DT0uEYgT.js +0 -17
- package/dist/assets/GuestSurfacePane-DVvk1VPu.js +0 -1
- package/dist/assets/LinearIssuesView-C10MwA9h.js +0 -1
- package/dist/assets/MarkdownImageGallery-BbN_K_3d.js +0 -1
- package/dist/assets/MarkdownRendererImpl-BLt2Tkk9.js +0 -10
- package/dist/assets/OnboardingScreen-Bgp5A3WB.js +0 -1
- package/dist/assets/PierreDiffViewer-CWJLj-S-.js +0 -210
- package/dist/assets/PlanView-DsRWnsOI.js +0 -5
- package/dist/assets/PublishDialog-CCeKloRu.js +0 -2
- package/dist/assets/SettingsView-BuPPsNPp.js +0 -30
- package/dist/assets/SettingsWindow-CM5fL8Ot.js +0 -1
- package/dist/assets/SubmoduleDiffSummary-CfsMcfiQ.js +0 -1
- package/dist/assets/ToolOutputDialog-mZrfSofz.js +0 -15
- package/dist/assets/ToolPartDiffPreview-BUGCRYEL.js +0 -8
- package/dist/assets/WalkthroughView-ClBo7PPd.js +0 -4
- package/dist/assets/appThemeRegistry-CxzMxR-H.js +0 -1
- package/dist/assets/de-CBla4hjd.js +0 -14
- package/dist/assets/es-BmnZtxTD.js +0 -20
- package/dist/assets/fileTreeStatus-BSHFIGEg.js +0 -1
- package/dist/assets/fr-OVKIBUZO.js +0 -18
- package/dist/assets/index-CKCFKmjF.js +0 -1
- package/dist/assets/index-Dbx4V4GN.css +0 -1
- package/dist/assets/index-vQ1Pg1cr.js +0 -61
- package/dist/assets/ja-6v-yr-gE.js +0 -20
- package/dist/assets/ko-I_ZLvgQ9.js +0 -20
- package/dist/assets/main-CmO4dRtR.js +0 -2
- package/dist/assets/main-b9vqhlYM.js +0 -999
- package/dist/assets/markdownCore-BPEZRQfP.js +0 -7
- package/dist/assets/miniChat-B5Oi7B-6.js +0 -2
- package/dist/assets/mobile-CVtShif9.js +0 -2
- package/dist/assets/nl-DcvIJf46.js +0 -20
- package/dist/assets/patchFileDiff-CW3wCH1O.js +0 -13
- package/dist/assets/pl-JxCUl0sd.js +0 -20
- package/dist/assets/pt-BR-L89mjja2.js +0 -20
- package/dist/assets/relocateRestoredSession-DYh_9oBa.js +0 -1
- package/dist/assets/renderElectronMiniChatApp-BD-1JHSg.js +0 -2
- package/dist/assets/renderMobileApp-7s5InXt1.js +0 -25
- package/dist/assets/runtimeEndpointReset-DqLJC6L1.js +0 -53
- package/dist/assets/shikiHighlight-CXgZnvHh.js +0 -1
- package/dist/assets/space-apply-1trGVIl8.js +0 -1
- package/dist/assets/space-close-y7j-NuBv.js +0 -1
- package/dist/assets/tr-ka5J1flt.js +0 -11
- package/dist/assets/uk-PyGe6Q_u.js +0 -20
- package/dist/assets/useInlineCommentController-CrjSGCpi.js +0 -2
- package/dist/assets/vendor-codemirror-lang-python-B7ClrMIT.js +0 -3
- package/dist/assets/vendor-codemirror-lang-xml-C8tGnS64.js +0 -1
- package/dist/assets/vendor-codemirror-language-data-q2bKO1EM.js +0 -2
- package/dist/assets/vendor-framer-motion-Bq3iTZ6r.js +0 -1
- package/dist/assets/vendor-katex-CHD9BdDO.js +0 -257
- package/dist/assets/vendor-katex-Cp579UoJ.css +0 -1
- package/dist/assets/zh-CN-B0P2tybJ.js +0 -20
- package/dist/assets/zh-TW-Bs9C5cN8.js +0 -20
|
@@ -54,8 +54,9 @@ const scheduleDiskWrite = () => {
|
|
|
54
54
|
}
|
|
55
55
|
};
|
|
56
56
|
|
|
57
|
-
export function getCacheKey({ normalizedRepo, subpath, identityId }) {
|
|
58
|
-
|
|
57
|
+
export function getCacheKey({ normalizedRepo, ref, subpath, identityId }) {
|
|
58
|
+
// Branches of one repository are different catalogs.
|
|
59
|
+
const safeRepo = `${String(normalizedRepo || '').trim()}${ref ? `#${ref}` : ''}`;
|
|
59
60
|
const safeSubpath = String(subpath || '').trim();
|
|
60
61
|
const safeIdentity = String(identityId || '').trim();
|
|
61
62
|
return `${safeRepo}::${safeSubpath}::${safeIdentity}`;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { readEnterprisePolicy } from '../enterprise-mode.js';
|
|
2
|
+
|
|
1
3
|
const CURATED_SKILLS_SOURCES = [
|
|
2
4
|
{
|
|
3
5
|
id: 'anthropic',
|
|
@@ -35,6 +37,8 @@ const CURATED_SKILLS_SOURCES = [
|
|
|
35
37
|
},
|
|
36
38
|
];
|
|
37
39
|
|
|
38
|
-
|
|
40
|
+
/** The built-in catalogs, or none when the machine policy hides them. */
|
|
41
|
+
export function getCuratedSkillsSources(policyOptions) {
|
|
42
|
+
if (readEnterprisePolicy(policyOptions).hideBuiltinSkillCatalogs) return [];
|
|
39
43
|
return CURATED_SKILLS_SOURCES.slice();
|
|
40
44
|
}
|
|
@@ -1,11 +1,44 @@
|
|
|
1
1
|
import { execFile } from 'child_process';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
2
3
|
import { promisify } from 'util';
|
|
4
|
+
import { createGitCredentialBroker, GIT_CREDENTIAL_NONCE_ENV } from '../git/credential-broker.js';
|
|
5
|
+
import { normalizeGitRemoteEndpoint } from '../git/credential-resolver.js';
|
|
6
|
+
import { managedSshCommand } from '../git/network-operations.js';
|
|
3
7
|
|
|
4
8
|
const execFileAsync = promisify(execFile);
|
|
5
9
|
|
|
6
10
|
const DEFAULT_TIMEOUT_MS = 60_000;
|
|
7
11
|
const DEFAULT_MAX_BUFFER = 4 * 1024 * 1024;
|
|
8
12
|
|
|
13
|
+
const isolatedGitEnvironment = (allowedProtocol) => {
|
|
14
|
+
const env = { ...process.env };
|
|
15
|
+
for (const name of Object.keys(env)) {
|
|
16
|
+
if (/^GIT_CONFIG(?:_|$)/i.test(name)
|
|
17
|
+
|| /^GIT_TRACE(?:2)?(?:_|$)/i.test(name)
|
|
18
|
+
|| /^(?:GIT_ASKPASS|SSH_ASKPASS|SSH_ASKPASS_REQUIRE|GIT_SSH|GIT_SSH_COMMAND|GIT_SSH_VARIANT|GIT_PROXY_COMMAND|SSH_AUTH_SOCK|HTTP_PROXY|HTTPS_PROXY|ALL_PROXY|NO_PROXY|http_proxy|https_proxy|all_proxy|no_proxy|NETRC|CURL_CA_BUNDLE|SSL_CERT_FILE|SSL_CERT_DIR)$/i.test(name)) {
|
|
19
|
+
delete env[name];
|
|
20
|
+
}
|
|
21
|
+
if (/^(?:GIT_CURL_VERBOSE|GIT_REDIRECT_STDERR|GIT_SSL_CERT|GIT_SSL_KEY|GIT_SSL_CAINFO|GIT_SSL_CAPATH|GIT_SSL_CIPHER_LIST|GIT_SSL_VERSION|GIT_SSL_NO_VERIFY|GIT_SSL_CERT_PASSWORD_PROTECTED|SSLKEYLOGFILE)$/i.test(name)
|
|
22
|
+
|| /^CURL_/i.test(name)) {
|
|
23
|
+
delete env[name];
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
env.GIT_TERMINAL_PROMPT = '0';
|
|
27
|
+
env.GIT_CONFIG_NOSYSTEM = '1';
|
|
28
|
+
env.GIT_CONFIG_GLOBAL = process.platform === 'win32' ? 'NUL' : '/dev/null';
|
|
29
|
+
env.GIT_ALLOW_PROTOCOL = allowedProtocol;
|
|
30
|
+
if (allowedProtocol === 'https') {
|
|
31
|
+
// Git's Curl transport also consults netrc outside Git configuration; an
|
|
32
|
+
// ambient ~/.netrc entry must not authenticate as another account than
|
|
33
|
+
// the selected one. SSH keeps HOME for known_hosts.
|
|
34
|
+
for (const name of Object.keys(env)) {
|
|
35
|
+
if (['HOME', 'USERPROFILE', 'XDG_CONFIG_HOME', 'HOMEDRIVE', 'HOMEPATH'].includes(name.toUpperCase())) delete env[name];
|
|
36
|
+
}
|
|
37
|
+
env.HOME = env.USERPROFILE = env.XDG_CONFIG_HOME = process.platform === 'win32' ? 'NUL' : '/dev/null';
|
|
38
|
+
}
|
|
39
|
+
return env;
|
|
40
|
+
};
|
|
41
|
+
|
|
9
42
|
export function looksLikeAuthError(message) {
|
|
10
43
|
const text = String(message || '');
|
|
11
44
|
return (
|
|
@@ -13,6 +46,7 @@ export function looksLikeAuthError(message) {
|
|
|
13
46
|
/publickey/i.test(text) ||
|
|
14
47
|
/could not read from remote repository/i.test(text) ||
|
|
15
48
|
/authentication failed/i.test(text) ||
|
|
49
|
+
/selected git identity/i.test(text) ||
|
|
16
50
|
/fatal: could not/i.test(text)
|
|
17
51
|
);
|
|
18
52
|
}
|
|
@@ -25,23 +59,72 @@ export async function runGit(args, options = {}) {
|
|
|
25
59
|
const identity = options.identity || null;
|
|
26
60
|
const normalizedArgs = Array.isArray(args) ? args.slice() : [];
|
|
27
61
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
};
|
|
62
|
+
let env = { ...process.env, GIT_TERMINAL_PROMPT: '0' };
|
|
63
|
+
let credential;
|
|
64
|
+
let broker;
|
|
65
|
+
let lease;
|
|
33
66
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
67
|
+
try {
|
|
68
|
+
if (identity?.credentialId) {
|
|
69
|
+
if (!options.credentialResolver || !identity.endpoint) {
|
|
70
|
+
return { ok: false, stdout: '', stderr: '', message: 'Selected Git identity is unavailable' };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const operationId = randomUUID();
|
|
74
|
+
try {
|
|
75
|
+
credential = await options.credentialResolver.resolve({
|
|
76
|
+
mode: 'managed',
|
|
77
|
+
credentialId: identity.credentialId,
|
|
78
|
+
endpoint: normalizeGitRemoteEndpoint(identity.endpoint),
|
|
79
|
+
operationId,
|
|
80
|
+
deadline: Date.now() + timeoutMs,
|
|
81
|
+
});
|
|
82
|
+
} catch {
|
|
83
|
+
return { ok: false, stdout: '', stderr: '', message: 'Authentication failed for the selected Git identity' };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (credential.transport === 'https') {
|
|
87
|
+
broker = createGitCredentialBroker({ capacity: 1, ttlMs: timeoutMs });
|
|
88
|
+
await broker.start();
|
|
89
|
+
lease = broker.issue({ operationId, credential });
|
|
90
|
+
env = isolatedGitEnvironment('https');
|
|
91
|
+
env[GIT_CREDENTIAL_NONCE_ENV] = lease.env[GIT_CREDENTIAL_NONCE_ENV];
|
|
92
|
+
normalizedArgs.unshift(
|
|
93
|
+
'-c', 'credential.helper=',
|
|
94
|
+
'-c', 'credential.useHttpPath=true',
|
|
95
|
+
'-c', 'http.proxy=',
|
|
96
|
+
'-c', 'http.sslVerify=true',
|
|
97
|
+
'-c', 'http.followRedirects=false',
|
|
98
|
+
'-c', 'http.extraHeader=',
|
|
99
|
+
'-c', 'http.cookieFile=',
|
|
100
|
+
'-c', 'credential.interactive=false',
|
|
101
|
+
'-c', 'submodule.recurse=false',
|
|
102
|
+
'-c', 'fetch.recurseSubmodules=false',
|
|
103
|
+
...lease.gitConfigArgs,
|
|
104
|
+
);
|
|
105
|
+
} else if (credential.transport === 'ssh') {
|
|
106
|
+
env = isolatedGitEnvironment('ssh');
|
|
107
|
+
normalizedArgs.unshift('-c', `core.sshCommand=${managedSshCommand(credential.key.privateKeyPath)}`);
|
|
108
|
+
} else {
|
|
109
|
+
return { ok: false, stdout: '', stderr: '', message: 'Selected Git identity cannot authenticate this repository' };
|
|
110
|
+
}
|
|
111
|
+
} else if (identity?.anonymous) {
|
|
112
|
+
env = isolatedGitEnvironment('https');
|
|
113
|
+
normalizedArgs.unshift(
|
|
114
|
+
'-c', 'credential.helper=',
|
|
115
|
+
'-c', 'http.proxy=',
|
|
116
|
+
'-c', 'http.followRedirects=false',
|
|
117
|
+
'-c', 'http.extraHeader=',
|
|
118
|
+
'-c', 'http.cookieFile=',
|
|
119
|
+
'-c', 'credential.interactive=false',
|
|
120
|
+
'-c', 'submodule.recurse=false',
|
|
121
|
+
'-c', 'fetch.recurseSubmodules=false',
|
|
122
|
+
);
|
|
123
|
+
} else if (identity) {
|
|
124
|
+
// A selected identity that names no credential never falls back to System Git.
|
|
125
|
+
return { ok: false, stdout: '', stderr: '', message: 'Selected Git identity is unavailable' };
|
|
41
126
|
}
|
|
42
|
-
}
|
|
43
127
|
|
|
44
|
-
try {
|
|
45
128
|
const { stdout, stderr } = await execFileAsync('git', normalizedArgs, {
|
|
46
129
|
cwd,
|
|
47
130
|
env,
|
|
@@ -52,19 +135,23 @@ export async function runGit(args, options = {}) {
|
|
|
52
135
|
|
|
53
136
|
return { ok: true, stdout: stdout || '', stderr: stderr || '' };
|
|
54
137
|
} catch (error) {
|
|
55
|
-
const err = error;
|
|
56
|
-
const stdout =
|
|
57
|
-
const stderr =
|
|
58
|
-
const message = err
|
|
138
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
139
|
+
const stdout = String(err.stdout ?? '');
|
|
140
|
+
const stderr = String(err.stderr ?? '');
|
|
141
|
+
const message = err.message;
|
|
59
142
|
|
|
60
143
|
return {
|
|
61
144
|
ok: false,
|
|
62
145
|
stdout,
|
|
63
146
|
stderr,
|
|
64
147
|
message,
|
|
65
|
-
code:
|
|
66
|
-
signal:
|
|
148
|
+
code: Number.isInteger(err.code) ? err.code : null,
|
|
149
|
+
signal: err.signal == null ? null : String(err.signal),
|
|
67
150
|
};
|
|
151
|
+
} finally {
|
|
152
|
+
lease?.revoke();
|
|
153
|
+
if (credential?.transport === 'ssh') await credential.key.cleanup();
|
|
154
|
+
if (broker) await broker.close();
|
|
68
155
|
}
|
|
69
156
|
}
|
|
70
157
|
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { runGit } from './git.js';
|
|
3
|
+
|
|
4
|
+
const NULL_DEVICE = process.platform === 'win32' ? 'NUL' : '/dev/null';
|
|
5
|
+
// A shell alias makes Git print the environment it runs under.
|
|
6
|
+
const PRINT_HOME = ['-c', 'alias.print-home=!printf %s "$HOME"', 'print-home'];
|
|
7
|
+
|
|
8
|
+
const httpsResolver = (password = 'catalog-private-token') => ({
|
|
9
|
+
resolve: vi.fn(async ({ endpoint }) => ({
|
|
10
|
+
mode: 'managed',
|
|
11
|
+
transport: 'https',
|
|
12
|
+
username: 'x-access-token',
|
|
13
|
+
password,
|
|
14
|
+
allowedEndpoint: endpoint,
|
|
15
|
+
})),
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const httpsIdentity = {
|
|
19
|
+
credentialId: 'managed-account-reference',
|
|
20
|
+
endpoint: 'https://example.com/owner/private.git',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
describe('skills catalog runGit', () => {
|
|
24
|
+
it('configures managed HTTPS Git through the broker without exposing the token', async () => {
|
|
25
|
+
const password = 'catalog-private-token';
|
|
26
|
+
const credentialResolver = httpsResolver(password);
|
|
27
|
+
|
|
28
|
+
const result = await runGit(['config', '--get', 'credential.helper'], { identity: httpsIdentity, credentialResolver });
|
|
29
|
+
|
|
30
|
+
expect(result.ok).toBe(true);
|
|
31
|
+
expect(result.stdout).toContain('credential-helper.js');
|
|
32
|
+
expect(result.stdout).not.toContain(password);
|
|
33
|
+
expect(result.stderr).not.toContain(password);
|
|
34
|
+
expect(credentialResolver.resolve).toHaveBeenCalledOnce();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('runs managed SSH Git through the key wrapper and removes the key afterwards', async () => {
|
|
38
|
+
const cleanup = vi.fn(async () => {});
|
|
39
|
+
const credentialResolver = {
|
|
40
|
+
resolve: vi.fn(async () => ({
|
|
41
|
+
mode: 'managed',
|
|
42
|
+
transport: 'ssh',
|
|
43
|
+
key: { privateKeyPath: '/tmp/catalog-operation-key', cleanup },
|
|
44
|
+
})),
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const result = await runGit(['config', '--get', 'core.sshCommand'], {
|
|
48
|
+
identity: { transport: 'ssh', credentialId: 'ssh-key-reference', endpoint: 'git@example.com:owner/private.git' },
|
|
49
|
+
credentialResolver,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
expect(result.ok).toBe(true);
|
|
53
|
+
expect(result.stdout).toContain('ssh-wrapper.js');
|
|
54
|
+
expect(result.stdout).toContain('/tmp/catalog-operation-key');
|
|
55
|
+
expect(cleanup).toHaveBeenCalledOnce();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('hides the home directory from managed HTTPS Git so ~/.netrc cannot sign in', async () => {
|
|
59
|
+
const result = await runGit(PRINT_HOME, { identity: httpsIdentity, credentialResolver: httpsResolver() });
|
|
60
|
+
|
|
61
|
+
expect(result.ok).toBe(true);
|
|
62
|
+
expect(result.stdout).toBe(NULL_DEVICE);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('hides the home directory from anonymous Git', async () => {
|
|
66
|
+
const result = await runGit(PRINT_HOME, { identity: { anonymous: true, transport: 'anonymous' } });
|
|
67
|
+
|
|
68
|
+
expect(result.ok).toBe(true);
|
|
69
|
+
expect(result.stdout).toBe(NULL_DEVICE);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('refuses a selected identity without a credential instead of using System Git', async () => {
|
|
73
|
+
const result = await runGit(['--version'], { identity: { transport: 'ssh', endpoint: 'git@example.com:owner/private.git' } });
|
|
74
|
+
|
|
75
|
+
expect(result.ok).toBe(false);
|
|
76
|
+
expect(result.message).toBe('Selected Git identity is unavailable');
|
|
77
|
+
});
|
|
78
|
+
});
|
|
@@ -90,14 +90,20 @@ async function copyDirectoryNoSymlinks(srcDir, dstDir) {
|
|
|
90
90
|
await walk(srcDir, dstDir);
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
async function cloneRepo({ cloneUrl, identity, tempDir }) {
|
|
94
|
-
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
const
|
|
93
|
+
async function cloneRepo({ cloneUrl, identity, credentialResolver, tempDir, ref = null }) {
|
|
94
|
+
// A `#ref` on the source picks the branch or tag; otherwise the default branch.
|
|
95
|
+
const branch = ref ? ['--branch', ref] : [];
|
|
96
|
+
const preferred = ['clone', '--depth', '1', '--filter=blob:none', '--no-checkout', ...branch, cloneUrl, tempDir];
|
|
97
|
+
const fallback = ['clone', '--depth', '1', '--no-checkout', ...branch, cloneUrl, tempDir];
|
|
98
|
+
|
|
99
|
+
const options = { identity, credentialResolver, timeoutMs: 90_000 };
|
|
100
|
+
const result = await runGit(preferred, options);
|
|
98
101
|
if (result.ok) return { ok: true };
|
|
102
|
+
if (looksLikeAuthError(`${result.stderr || ''}\n${result.message || ''}`)) {
|
|
103
|
+
return { ok: false, error: result };
|
|
104
|
+
}
|
|
99
105
|
|
|
100
|
-
const fallbackResult = await runGit(fallback,
|
|
106
|
+
const fallbackResult = await runGit(fallback, options);
|
|
101
107
|
if (fallbackResult.ok) return { ok: true };
|
|
102
108
|
|
|
103
109
|
return {
|
|
@@ -132,6 +138,7 @@ export async function installSkillsFromRepository({
|
|
|
132
138
|
subpath,
|
|
133
139
|
defaultSubpath,
|
|
134
140
|
identity,
|
|
141
|
+
credentialResolver,
|
|
135
142
|
scope,
|
|
136
143
|
targetSource,
|
|
137
144
|
workingDirectory,
|
|
@@ -174,7 +181,7 @@ export async function installSkillsFromRepository({
|
|
|
174
181
|
const effectiveSubpath = parsed.effectiveSubpath || (typeof defaultSubpath === 'string' && defaultSubpath.trim() ? defaultSubpath.trim() : null);
|
|
175
182
|
void effectiveSubpath;
|
|
176
183
|
|
|
177
|
-
const cloneUrl = identity?.
|
|
184
|
+
const cloneUrl = identity?.transport === 'ssh' ? parsed.cloneUrlSsh : parsed.cloneUrlHttps;
|
|
178
185
|
|
|
179
186
|
const requestedDirs = Array.isArray(selections) ? selections.map((s) => String(s?.skillDir || '').trim()).filter(Boolean) : [];
|
|
180
187
|
if (requestedDirs.length === 0) {
|
|
@@ -217,23 +224,25 @@ export async function installSkillsFromRepository({
|
|
|
217
224
|
const tempBase = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'openchamber-skills-install-'));
|
|
218
225
|
|
|
219
226
|
try {
|
|
220
|
-
const cloned = await cloneRepo({ cloneUrl, identity, tempDir: tempBase });
|
|
227
|
+
const cloned = await cloneRepo({ cloneUrl, identity, credentialResolver, tempDir: tempBase, ref: parsed.ref });
|
|
221
228
|
if (!cloned.ok) {
|
|
222
229
|
const msg = `${cloned.error?.stderr || ''}\n${cloned.error?.message || ''}`.trim();
|
|
223
230
|
if (looksLikeAuthError(msg)) {
|
|
224
|
-
return { ok: false, error: { kind: 'authRequired', message: 'Authentication required to access this repository'
|
|
231
|
+
return { ok: false, error: { kind: 'authRequired', message: 'Authentication required to access this repository' } };
|
|
225
232
|
}
|
|
226
233
|
return { ok: false, error: { kind: 'networkError', message: msg || 'Failed to clone repository' } };
|
|
227
234
|
}
|
|
228
235
|
|
|
229
236
|
// Selective checkout for only requested skill dirs.
|
|
230
|
-
await runGit(['-C', tempBase, 'sparse-checkout', 'init', '--cone'], {
|
|
231
|
-
const setResult = await runGit(['-C', tempBase, 'sparse-checkout', 'set', ...requestedDirs], {
|
|
237
|
+
await runGit(['-C', tempBase, 'sparse-checkout', 'init', '--cone'], { timeoutMs: 15_000 });
|
|
238
|
+
const setResult = await runGit(['-C', tempBase, 'sparse-checkout', 'set', ...requestedDirs], { timeoutMs: 30_000 });
|
|
232
239
|
if (!setResult.ok) {
|
|
233
240
|
return { ok: false, error: { kind: 'unknown', message: setResult.stderr || setResult.message || 'Failed to configure sparse checkout' } };
|
|
234
241
|
}
|
|
235
242
|
|
|
236
|
-
|
|
243
|
+
// The checkout fetches the selected blobs from the partial clone, so it
|
|
244
|
+
// authenticates like the clone did.
|
|
245
|
+
const checkoutResult = await runGit(['-C', tempBase, 'checkout', '--force', 'HEAD'], { identity, credentialResolver, timeoutMs: 60_000 });
|
|
237
246
|
if (!checkoutResult.ok) {
|
|
238
247
|
return { ok: false, error: { kind: 'unknown', message: checkoutResult.stderr || checkoutResult.message || 'Failed to checkout repository' } };
|
|
239
248
|
}
|
|
@@ -45,14 +45,20 @@ async function safeRm(dir) {
|
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
async function cloneRepo({ cloneUrl, identity, tempDir }) {
|
|
49
|
-
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
const
|
|
48
|
+
async function cloneRepo({ cloneUrl, identity, credentialResolver, tempDir, ref = null }) {
|
|
49
|
+
// A `#ref` on the source picks the branch or tag; otherwise the default branch.
|
|
50
|
+
const branch = ref ? ['--branch', ref] : [];
|
|
51
|
+
const preferred = ['clone', '--depth', '1', '--filter=blob:none', '--no-checkout', ...branch, cloneUrl, tempDir];
|
|
52
|
+
const fallback = ['clone', '--depth', '1', '--no-checkout', ...branch, cloneUrl, tempDir];
|
|
53
|
+
|
|
54
|
+
const options = { identity, credentialResolver, timeoutMs: 60_000 };
|
|
55
|
+
const result = await runGit(preferred, options);
|
|
53
56
|
if (result.ok) return { ok: true };
|
|
57
|
+
if (looksLikeAuthError(`${result.stderr || ''}\n${result.message || ''}`)) {
|
|
58
|
+
return { ok: false, error: result };
|
|
59
|
+
}
|
|
54
60
|
|
|
55
|
-
const fallbackResult = await runGit(fallback,
|
|
61
|
+
const fallbackResult = await runGit(fallback, options);
|
|
56
62
|
if (fallbackResult.ok) return { ok: true };
|
|
57
63
|
|
|
58
64
|
return {
|
|
@@ -66,6 +72,7 @@ export async function scanSkillsRepository({
|
|
|
66
72
|
subpath,
|
|
67
73
|
defaultSubpath,
|
|
68
74
|
identity,
|
|
75
|
+
credentialResolver,
|
|
69
76
|
} = {}) {
|
|
70
77
|
const gitCheck = await assertGitAvailable();
|
|
71
78
|
if (!gitCheck.ok) {
|
|
@@ -78,16 +85,19 @@ export async function scanSkillsRepository({
|
|
|
78
85
|
}
|
|
79
86
|
|
|
80
87
|
const effectiveSubpath = parsed.effectiveSubpath || (typeof defaultSubpath === 'string' && defaultSubpath.trim() ? defaultSubpath.trim() : null);
|
|
81
|
-
const cloneUrl = identity?.
|
|
88
|
+
const cloneUrl = identity?.transport === 'ssh' ? parsed.cloneUrlSsh : parsed.cloneUrlHttps;
|
|
89
|
+
// Only commands that reach the remote get the identity: the clone, and the
|
|
90
|
+
// checkout or `show` that lazily fetch blobs from the partial clone.
|
|
91
|
+
const remoteGitOptions = { identity, credentialResolver };
|
|
82
92
|
|
|
83
93
|
const tempBase = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'openchamber-skills-scan-'));
|
|
84
94
|
|
|
85
95
|
try {
|
|
86
|
-
const cloned = await cloneRepo({ cloneUrl, identity, tempDir: tempBase });
|
|
96
|
+
const cloned = await cloneRepo({ cloneUrl, identity, credentialResolver, tempDir: tempBase, ref: parsed.ref });
|
|
87
97
|
if (!cloned.ok) {
|
|
88
98
|
const msg = `${cloned.error?.stderr || ''}\n${cloned.error?.message || ''}`.trim();
|
|
89
99
|
if (looksLikeAuthError(msg)) {
|
|
90
|
-
return { ok: false, error: { kind: 'authRequired', message: 'Authentication required to access this repository'
|
|
100
|
+
return { ok: false, error: { kind: 'authRequired', message: 'Authentication required to access this repository' } };
|
|
91
101
|
}
|
|
92
102
|
return { ok: false, error: { kind: 'networkError', message: msg || 'Failed to clone repository' } };
|
|
93
103
|
}
|
|
@@ -102,13 +112,13 @@ export async function scanSkillsRepository({
|
|
|
102
112
|
|
|
103
113
|
// Fast path: sparse checkout only SKILL.md files, then parse from disk.
|
|
104
114
|
// This avoids one `git show` per skill.
|
|
105
|
-
const sparseInit = await runGit(['-C', tempBase, 'sparse-checkout', 'init', '--no-cone'], {
|
|
115
|
+
const sparseInit = await runGit(['-C', tempBase, 'sparse-checkout', 'init', '--no-cone'], { timeoutMs: 15_000 });
|
|
106
116
|
if (sparseInit.ok) {
|
|
107
|
-
const sparseSet = await runGit(['-C', tempBase, 'sparse-checkout', 'set', ...patterns], {
|
|
117
|
+
const sparseSet = await runGit(['-C', tempBase, 'sparse-checkout', 'set', ...patterns], { timeoutMs: 30_000 });
|
|
108
118
|
if (sparseSet.ok) {
|
|
109
|
-
const checkout = await runGit(['-C', tempBase, 'checkout', '--force', 'HEAD'], {
|
|
119
|
+
const checkout = await runGit(['-C', tempBase, 'checkout', '--force', 'HEAD'], { ...remoteGitOptions, timeoutMs: 60_000 });
|
|
110
120
|
if (checkout.ok) {
|
|
111
|
-
const lsFiles = await runGit(['-C', tempBase, 'ls-files'], {
|
|
121
|
+
const lsFiles = await runGit(['-C', tempBase, 'ls-files'], { timeoutMs: 15_000 });
|
|
112
122
|
if (lsFiles.ok) {
|
|
113
123
|
skillMdPaths = lsFiles.stdout
|
|
114
124
|
.split(/\r?\n/)
|
|
@@ -127,7 +137,7 @@ export async function scanSkillsRepository({
|
|
|
127
137
|
listArgs.push('--', effectiveSubpath);
|
|
128
138
|
}
|
|
129
139
|
|
|
130
|
-
const listResult = await runGit(listArgs, {
|
|
140
|
+
const listResult = await runGit(listArgs, { timeoutMs: 30_000 });
|
|
131
141
|
if (!listResult.ok) {
|
|
132
142
|
// If subpath doesn't exist, treat as empty scan.
|
|
133
143
|
return {
|
|
@@ -172,7 +182,7 @@ export async function scanSkillsRepository({
|
|
|
172
182
|
try {
|
|
173
183
|
skillMdContent = await fs.promises.readFile(filePath, 'utf8');
|
|
174
184
|
} catch {
|
|
175
|
-
const showResult = await runGit(['-C', tempBase, 'show', `HEAD:${skillMdPath}`], {
|
|
185
|
+
const showResult = await runGit(['-C', tempBase, 'show', `HEAD:${skillMdPath}`], { ...remoteGitOptions, timeoutMs: 15_000 });
|
|
176
186
|
if (!showResult.ok) {
|
|
177
187
|
warnings.push('Failed to read SKILL.md');
|
|
178
188
|
} else {
|
|
@@ -11,8 +11,33 @@ function normalizeGitOwnerRepo(owner, repo) {
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
|
|
14
|
+
// A branch or tag after `#`: letters, digits and `._/-`, never starting with
|
|
15
|
+
// `-` (it becomes a git argument) and never with `..`.
|
|
16
|
+
const GIT_REF_PATTERN = /^(?!-)(?!.*\.\.)[A-Za-z0-9._/-]+$/;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* `owner/repo#ref`, `https://host/owner/repo.git#ref`: the source without the
|
|
20
|
+
* fragment, and the branch or tag it names (null when none).
|
|
21
|
+
*/
|
|
22
|
+
function splitSourceRef(source) {
|
|
23
|
+
const hashIndex = source.lastIndexOf('#');
|
|
24
|
+
if (hashIndex < 0) return { base: source, ref: null };
|
|
25
|
+
const ref = source.slice(hashIndex + 1).trim();
|
|
26
|
+
return { base: source.slice(0, hashIndex).trim(), ref: ref || null };
|
|
27
|
+
}
|
|
28
|
+
|
|
14
29
|
export function parseSkillRepoSource(input, options = {}) {
|
|
15
|
-
const
|
|
30
|
+
const parsed = parseSkillRepoBase(input, options);
|
|
31
|
+
if (!parsed.ok) return parsed;
|
|
32
|
+
const { ref } = splitSourceRef(typeof input === 'string' ? input.trim() : '');
|
|
33
|
+
if (ref !== null && !GIT_REF_PATTERN.test(ref)) {
|
|
34
|
+
return { ok: false, error: { kind: 'invalidSource', message: `Invalid branch or tag "${ref}"` } };
|
|
35
|
+
}
|
|
36
|
+
return { ...parsed, ref };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function parseSkillRepoBase(input, options) {
|
|
40
|
+
const raw = splitSourceRef(typeof input === 'string' ? input.trim() : '').base;
|
|
16
41
|
if (!raw) {
|
|
17
42
|
return { ok: false, error: { kind: 'invalidSource', message: 'Repository source is required' } };
|
|
18
43
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { parseSkillRepoSource } from './source.js';
|
|
4
|
+
|
|
5
|
+
describe('parseSkillRepoSource', () => {
|
|
6
|
+
it('reads a branch or tag after #, from shorthand and URLs', () => {
|
|
7
|
+
expect(parseSkillRepoSource('acme/skills#develop')).toMatchObject({ ok: true, normalizedRepo: 'acme/skills', ref: 'develop', effectiveSubpath: null });
|
|
8
|
+
expect(parseSkillRepoSource('acme/skills/pack#feature/v2')).toMatchObject({ ok: true, normalizedRepo: 'acme/skills', ref: 'feature/v2', effectiveSubpath: 'pack' });
|
|
9
|
+
expect(parseSkillRepoSource('https://github.com/acme/skills.git#v1.0.0')).toMatchObject({
|
|
10
|
+
ok: true,
|
|
11
|
+
cloneUrlHttps: 'https://github.com/acme/skills.git',
|
|
12
|
+
ref: 'v1.0.0',
|
|
13
|
+
});
|
|
14
|
+
expect(parseSkillRepoSource('git@gitlab.acme.dev:team/skills.git#beta')).toMatchObject({ ok: true, host: 'gitlab.acme.dev', ref: 'beta' });
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('has no ref without #, or with an empty one', () => {
|
|
18
|
+
expect(parseSkillRepoSource('acme/skills').ref).toBeNull();
|
|
19
|
+
expect(parseSkillRepoSource('acme/skills#').ref).toBeNull();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('refuses a ref git would read as an option or a range', () => {
|
|
23
|
+
for (const source of ['acme/skills#--upload-pack=x', 'acme/skills#a..b', 'acme/skills#x y']) {
|
|
24
|
+
expect(parseSkillRepoSource(source), source).toMatchObject({ ok: false, error: { kind: 'invalidSource' } });
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
});
|
|
@@ -17,13 +17,14 @@ Repository bindings intentionally keep provider accounts, parent Git transport c
|
|
|
17
17
|
- `binding-contract.js` owns the dependency-free strict persisted binding parser, separate binding-response parser, readiness projection, aggregate state, and public endpoint validation for the server and the web adapter. The persisted parser rejects response-only credential presentation, so `source-control-bindings.json` remains references-only. The response parser accepts only the tagged safe presentation contract and rejects unknown fields. Safe endpoint metadata accepts credential-free HTTP/HTTPS, SSH URL, and SCP display forms plus bounded base64url fingerprints; local-file, credential-bearing HTTPS, query, fragment, traversal, control-character, and unsafe fingerprint forms fail closed. Each provider stores its approved fetch endpoint and readiness. Each remote stores readiness alongside its exact fetch/push endpoints; auxiliary grants have their own readiness. Only `ready` grants authority. `account-unavailable`, `confirmation-required`, and `config-changed` retain repair metadata without granting the affected capability. Binding `state` is an aggregate UI summary, never the authority gate.
|
|
18
18
|
- `mutation-storage.js` owns strict versioned persistence for bounded source-control mutation records in `source-control-mutations.json` under the server config root. `claim` persists an idempotency key before provider work, exact key-and-digest replays return the stored record, and digest changes report a conflict. `complete` permits one transition from `running` to a terminal state. Failed records may retain only an HTTP status and stable code for replay; messages and provider payloads are rejected. Terminal records have a seven-day retention by default and may be evicted oldest-first at capacity; `running` and `outcome-unknown` records are never pruned or evicted. Snapshots use serialized atomic mode-`0600` writes and reject unknown fields, provider payloads, private text, tokens, and raw URLs.
|
|
19
19
|
- `mutation-executor.js` canonicalizes and hashes mutation input, joins exact process-local duplicates, claims persisted state before provider work, and never retries a provider mutation. Provider routes include the exact resolved credential account ID and revision in this digest and keep the credential account ID in durable replay authority. They pass the verified provider-user ID separately for receipt actor metadata and audit `providerAccountId`; clients cannot supply it. Its store must provide `withExecutionLock(key, operation)`, scoped to that store's file path and the SHA-256 digest of the key, not the input digest. The executor holds this separate lock from before claim through provider work or reconciliation and mutation/audit completion. A second daemon waits or fails busy, never reconciles live work. Completed requests replay their compact receipt after acquiring the lock. An unlocked persisted `running` record enters provider reconciliation; a crashed owner's leftover lock is reclaimed by the next contender (see below). Ambiguous provider outcomes become sticky `outcome-unknown` records when persistence remains available. A failed completion write leaves the prior snapshot available for reconciliation, never another provider write.
|
|
20
|
-
- `audit-storage.js` owns the separate version-1 `source-control-audit.json` log under `OPENCHAMBER_DATA_DIR`. It does not share records, retention, or write sequencing with mutation replay storage. The strict schema stores one logical record per provider mutation idempotency key and per server Git operation ID. It accepts only `user` as the current initiator, the `provider-api` or `openchamber-server-git` executor, an immutable runtime and repository snapshot, an authoritative provider-user account ID or `null`, authoritative transport metadata, a compact redacted target, lifecycle timestamps, and a result state, stable error code, and bounded steps. An opaque credential ID is never substituted for provider identity. Provider API records require a `null` transport reference
|
|
20
|
+
- `audit-storage.js` owns the separate version-1 `source-control-audit.json` log under `OPENCHAMBER_DATA_DIR`. It does not share records, retention, or write sequencing with mutation replay storage. The strict schema stores one logical record per provider mutation idempotency key and per server Git operation ID. It accepts only `user` as the current initiator, the `provider-api` or `openchamber-server-git` executor, an immutable runtime and repository snapshot, an authoritative provider-user account ID or `null`, authoritative transport metadata, a compact redacted target, lifecycle timestamps, and a result state, stable error code, and bounded steps. An opaque credential ID is never substituted for provider identity. Provider API records require a `null` transport reference and a change-request target, or an issue target for `issue-comment`. Single-path Git records require either an exact opaque managed `credentialId` or a system marker. Sync records keep exact fetch and push references separately, including mixed system and managed transport. Checkout hydration records a bounded list of discovered auxiliary kinds and endpoint fingerprints plus only the exact server-validated transport reference for each granted endpoint; it records `null` transport when no transfer grant is used and always records a `null` provider account. Existing hydration records with the former single parent transport remain readable but new records never write that attribution. The schema rejects unknown fields and URL-shaped transport or target values, so raw endpoints, hydration paths, directories, key paths, credential payloads, process output, provider payloads, and free-form errors cannot enter the file. GitLab account IDs retain their authoritative normalized-instance prefix because that prefix is part of the account identifier, not a transport endpoint. Writes are serialized, atomic, and mode `0600`. Resolved terminal records expire after 30 days by default and are oldest-first capacity candidates; `planned`, `running`, and `outcome-unknown` records are never pruned or evicted.
|
|
21
21
|
- `mutation-executor.js` also drives provider audit lifecycle around the existing durable claim. Exact in-process duplicates and durable replays use `provider:<idempotencyKey>` and therefore reuse the same audit record, while `transportReference` remains `null` because provider API calls do not use Git transport. The executor rejects an audited execution before claim when the route omits verified provider identity. Two credentials for the same provider user therefore share actor identity in receipts and audits, but their exact credential and binding authority still produce different mutation digests. Audit persistence does not replace or relax mutation claim, digest conflict, reconciliation, sticky unknown-outcome, or replay behavior.
|
|
22
22
|
|
|
23
23
|
Provider replay and reconciliation after restart reuse the runtime snapshot from the existing audit record. Strict audit planning still checks the other immutable fields. Terminal replay does not rewrite audit history. An unfinished audit can complete from the durable mutation receipt without repeating provider work.
|
|
24
24
|
- `binding-service.js` accepts remote names and account/credential references, normalizes provider instances before uniqueness checks and persistence, then resolves endpoint metadata and provider repository coordinates from current server-side Git state. A provider instance must match the selected remote host and applicable HTTP port; supplied repository coordinates must equal the path-derived coordinates. Client-provided URLs are never authoritative. Provider reads check the binding revision, the bound provider's own readiness and that its primary remote still exists, so a remote added or removed beside it does not interrupt change-request status. A repository with no binding (revision 0) or whose binding names no account for a host is read with the account the client is signed in to on that host, as before bindings existed: the context must name a remote that really points at the instance, and whether the account exists is the provider route's to decide. Mutation contexts pass the same checks plus the binding's saved config revision, then validate an idempotency key and an exact, copied target shape before provider code receives them. Stale revision or config/remote errors carry the current authoritative repository, revision, and redacted binding record. Account reconciliation uses the same normalization and also matches equivalent instance spellings in previously stored records.
|
|
25
25
|
- `url-redaction.js` removes URL credentials, query data, and fragments from endpoint display URLs before repository context crosses the server boundary. Endpoint fingerprints remain derived from normalized transport authority rather than the shortened display, so hidden query or fragment changes still change authority.
|
|
26
26
|
- `status-cache.js` keeps the last-known change request status per exact trusted read context (instance, account, credential revision, repository, binding revision, directory, branch, remote), bounded and TTL-fresh. A provider route answers a fresh repeat from it, answers a transient provider failure with the last-known status, drops a repository's entries after a mutation and an account's entries after invalidation. GitLab uses it; GitHub keeps its own equivalent in `github/routes.js` for now.
|
|
27
|
+
- `change-request-worktree.js` builds a change-request source from a directory and a pull or merge request number alone, for `session.create` with `pullRequest`. It chooses the read context the way the client's `getBoundSourceControlReadContexts` and `useGitHubReadContext` do: a ready bound provider first, else a remote on GitHub or a known GitLab instance read with that host's current account (each provider's `readCurrentAccountId`, the gh or glab login only while the user switched to it), GitHub before GitLab. The provider's `readChangeRequestHead` then answers the project, head branch, head revision and head owner on that context's primary remote; a GitHub pull request whose fork was deleted, or a number that is not one there, is refused with its own message. No caller-supplied project, endpoint or remote name enters the result, and the Git contributor pipeline resolves it again before any transfer.
|
|
27
28
|
- `oauth-flow-registry.js` keeps device-flow secrets in a bounded, process-local registry. Clients receive an opaque flow ID; completion acquires the matching provider and normalized instance exclusively, then releases retryable attempts or consumes terminal attempts. Restarting the server invalidates every outstanding flow.
|
|
28
29
|
|
|
29
30
|
## Cross-process locks and recovery
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isPlainObject, isString } from '../shared/guards.js';
|
|
1
2
|
import fs from 'node:fs/promises';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
import { randomUUID } from 'node:crypto';
|
|
@@ -16,7 +17,11 @@ const GIT_OPERATIONS = new Set([
|
|
|
16
17
|
]);
|
|
17
18
|
const PROVIDER_OPERATIONS = new Set([
|
|
18
19
|
'change-request-create', 'change-request-update', 'change-request-merge', 'change-request-ready',
|
|
20
|
+
'change-request-comment', 'change-request-review', 'issue-comment', 'change-request-state', 'issue-state',
|
|
21
|
+
'change-request-labels', 'issue-labels', 'change-request-reviewers',
|
|
19
22
|
]);
|
|
23
|
+
// What a provider write acts on: `issue-*` names an issue, everything else a change request.
|
|
24
|
+
const providerTargetKind = (operation) => (operation.startsWith('issue-') ? 'issue' : 'change-request');
|
|
20
25
|
const RESULT_STEPS = new Set([
|
|
21
26
|
'validated', 'authenticated', 'transferred', 'updated-local-repository', 'checked-out', 'cleaned-up',
|
|
22
27
|
'fetch:succeeded', 'fetch:skipped', 'fetch:conflicted', 'fetch:failed', 'fetch:cancelled',
|
|
@@ -26,10 +31,6 @@ const RESULT_STEPS = new Set([
|
|
|
26
31
|
]);
|
|
27
32
|
|
|
28
33
|
const emptyState = () => ({ version: VERSION, records: {} });
|
|
29
|
-
const isPlainObject = (value) => value === Object(value)
|
|
30
|
-
&& !Array.isArray(value)
|
|
31
|
-
&& Object.getPrototypeOf(value) === Object.prototype;
|
|
32
|
-
const isString = (value) => Object.prototype.toString.call(value) === '[object String]';
|
|
33
34
|
const hasExactKeys = (value, required, optional = []) => {
|
|
34
35
|
const keys = Object.keys(value);
|
|
35
36
|
return required.every((key) => keys.includes(key))
|
|
@@ -113,8 +114,8 @@ const isGitTarget = (value) => isPlainObject(value)
|
|
|
113
114
|
&& isSafeText(value.destination.displayName) && isSafeText(value.destination.fingerprint, 1024)));
|
|
114
115
|
const isProviderTarget = (value) => isPlainObject(value)
|
|
115
116
|
&& hasExactKeys(value, ['kind', 'operation', 'projectId'], ['number', 'head', 'base', 'headSha'])
|
|
116
|
-
&& value.kind === 'change-request'
|
|
117
117
|
&& PROVIDER_OPERATIONS.has(value.operation)
|
|
118
|
+
&& value.kind === providerTargetKind(value.operation)
|
|
118
119
|
&& isSafeText(value.projectId, 1024)
|
|
119
120
|
&& (value.number === undefined || isPositiveInteger(value.number))
|
|
120
121
|
&& (value.head === undefined || isSafeText(value.head, 1024))
|
|
@@ -150,7 +151,7 @@ const isRecord = (value, id) => {
|
|
|
150
151
|
|| !EXECUTORS.has(value.executorKind) || !isRuntime(value.runtime)
|
|
151
152
|
|| !isNullableText(value.repositoryId) || !isNullableAccountId(value.providerAccountId)
|
|
152
153
|
|| !isTransportReference(value.transportReference) || !isTarget(value.target)
|
|
153
|
-
|| (value.executorKind === 'provider-api' && (value.transportReference !== null || value.target
|
|
154
|
+
|| (value.executorKind === 'provider-api' && (value.transportReference !== null || !isProviderTarget(value.target)))
|
|
154
155
|
|| (value.executorKind === 'openchamber-server-git' && (value.target.kind !== 'git-network'
|
|
155
156
|
|| !hydrationTransportMatchesTarget(value.transportReference, value.target)))
|
|
156
157
|
|| (value.target.kind === 'git-network' && value.target.operation === 'checkout-hydration'
|
|
@@ -53,6 +53,17 @@ describe('source-control audit storage', () => {
|
|
|
53
53
|
expect(results.find((result) => !result.ok).error.code).toBe('SOURCE_CONTROL_AUDIT_CONFLICT');
|
|
54
54
|
});
|
|
55
55
|
|
|
56
|
+
it('names an issue comment by its issue and refuses an issue target for other writes', async () => {
|
|
57
|
+
const { store } = await setup();
|
|
58
|
+
const issueTarget = { kind: 'issue', operation: 'issue-comment', projectId: 'project_one', number: 3 };
|
|
59
|
+
await expect(store.plan(plan('issue', { target: issueTarget }))).resolves.toBeTruthy();
|
|
60
|
+
await expect(store.plan(plan('review', { target: { kind: 'change-request', operation: 'change-request-review', projectId: 'project_one', number: 7 } })))
|
|
61
|
+
.resolves.toBeTruthy();
|
|
62
|
+
await expect(store.plan(plan('issue-state', { target: { ...issueTarget, operation: 'issue-state' } }))).resolves.toBeTruthy();
|
|
63
|
+
await expect(store.plan(plan('mismatch', { target: { ...issueTarget, operation: 'change-request-comment' } }))).rejects.toThrow();
|
|
64
|
+
await expect(store.plan(plan('mismatch-two', { target: { ...issueTarget, kind: 'change-request' } }))).rejects.toThrow();
|
|
65
|
+
});
|
|
66
|
+
|
|
56
67
|
it('fails closed on busy reads and cleans failed snapshot writes', async () => {
|
|
57
68
|
const { filePath, store } = await setup();
|
|
58
69
|
await store.plan(plan('original'));
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isPlainObject, isString } from '../shared/guards.js';
|
|
1
2
|
import { normalizeSourceControlProviderInstance } from './provider-instance.js';
|
|
2
3
|
import { resolvePrivateRepositoryIdentity } from './repository-identity.js';
|
|
3
4
|
import { fingerprintRemoteUrl, redactRemoteUrl } from './url-redaction.js';
|
|
@@ -11,10 +12,6 @@ const bindingInputError = (message) => {
|
|
|
11
12
|
return error;
|
|
12
13
|
};
|
|
13
14
|
|
|
14
|
-
const isPlainObject = (value) => value === Object(value)
|
|
15
|
-
&& !Array.isArray(value)
|
|
16
|
-
&& Object.getPrototypeOf(value) === Object.prototype;
|
|
17
|
-
const isString = (value) => Object.prototype.toString.call(value) === '[object String]';
|
|
18
15
|
const safePresentationText = (value, maximum = 512) => isString(value) && value.length > 0
|
|
19
16
|
&& value.length <= maximum && value.trim() === value && !/[\0\r\n]/.test(value);
|
|
20
17
|
const safeProviderUsername = (value) => safePresentationText(value, 255) && /^[A-Za-z0-9_.-]+$/.test(value);
|
|
@@ -272,7 +269,7 @@ const normalizeProviderInstance = (provider, instance, makeError = bindingInputE
|
|
|
272
269
|
};
|
|
273
270
|
|
|
274
271
|
/** The host a remote address or a provider instance points at, lowercased; null when it names none. */
|
|
275
|
-
const hostOf = (value) => {
|
|
272
|
+
export const hostOf = (value) => {
|
|
276
273
|
const text = String(value ?? '').trim();
|
|
277
274
|
if (!text) return null;
|
|
278
275
|
try { return new URL(text.includes('://') ? text : `https://${text}`).hostname.toLowerCase() || null; }
|