@akira-tl/forgerelay 1.1.0 → 1.1.2
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/CHANGELOG.md +26 -0
- package/README.md +4 -2
- package/dist/activity/runtime/mcp-query-tools.js +16 -10
- package/dist/cli/mcp/external-mcp.js +481 -0
- package/dist/cli/mcp/status.js +295 -0
- package/dist/cli.js +14 -1
- package/dist/lsp/test-support/server-fixture.js +1 -2
- package/dist/mcp/oauth/auth-protocol.js +375 -0
- package/dist/mcp/oauth/oauth-provider.js +11 -12
- package/dist/mcp/oauth/oauth-store.js +2 -2
- package/dist/mcp/oauth/router.js +2 -6
- package/dist/mcp/operations/batch/executor.js +1 -1
- package/dist/mcp/operations/external-mcp/external-mcp-oauth.js +387 -0
- package/dist/mcp/operations/external-mcp/external-mcp-runtime.js +23 -6
- package/dist/mcp/operations/external-mcp/external-mcp.js +178 -23
- package/dist/mcp/operations/native-bulk-mutations.js +1 -1
- package/dist/mcp/process/tools.js +19 -15
- package/dist/mcp/request-context.js +18 -0
- package/dist/mcp/request-meta.js +28 -3
- package/dist/mcp/server/core/capabilities.js +1 -4
- package/dist/mcp/server/operations/runtime/filesystem-tools.js +35 -28
- package/dist/mcp/server/operations/runtime/operation-runtime.js +7 -7
- package/dist/mcp/server/transport/http-server.js +41 -20
- package/dist/mcp/server/workspace/runtime/workspace-open-presentation.js +2 -0
- package/dist/mcp/server/workspace/runtime/workspace-open.js +13 -9
- package/dist/mcp/server/workspace/runtime/workspace-tools.js +31 -27
- package/dist/runtime/config/external-mcp-auth-store.js +225 -0
- package/dist/runtime/config/external-mcp-config.js +76 -22
- package/dist/runtime/config/external-mcp-registry.js +165 -0
- package/dist/runtime/testing/server-fixture.js +4 -4
- package/dist/server.js +15 -9
- package/dist/ui/.vite/manifest.json +297 -298
- package/dist/ui/activity-panel-app.html +1 -1
- package/dist/ui/assets/activity-panel-app-Bw7ZFX2g.js +114 -0
- package/dist/ui/assets/angular-html-DeUNP12X.js +1 -0
- package/dist/ui/assets/{angular-ts-BearSZDb.js → angular-ts-B3v8FKi_.js} +1 -1
- package/dist/ui/assets/{apl-CYQtr-y6.js → apl-Bs3iQm4P.js} +1 -1
- package/dist/ui/assets/{astro-DDzqKIjK.js → astro-D6GHecBY.js} +1 -1
- package/dist/ui/assets/{blade-DkwEyHvQ.js → blade-BMNbVOHv.js} +1 -1
- package/dist/ui/assets/c-BquuA6bv.js +1 -0
- package/dist/ui/assets/{cobol-xBn5JxHE.js → cobol-BuZ6kfps.js} +1 -1
- package/dist/ui/assets/{coffee-BaXXvoJw.js → coffee-DgC5uBzL.js} +1 -1
- package/dist/ui/assets/cpp-BdvWqCaB.js +1 -0
- package/dist/ui/assets/{crystal-DjOpXVU1.js → crystal-0Q5qBvNI.js} +1 -1
- package/dist/ui/assets/css-Be_T8idh.js +1 -0
- package/dist/ui/assets/{edge-DNaRizoR.js → edge-DaGLdgh_.js} +1 -1
- package/dist/ui/assets/{elixir-CwBMwZmy.js → elixir-CMgiFIzB.js} +1 -1
- package/dist/ui/assets/{elm-gOUD2CW3.js → elm-B-ufMlMC.js} +1 -1
- package/dist/ui/assets/{erb-BcR-fjUp.js → erb-CaGiOZM-.js} +1 -1
- package/dist/ui/assets/{git-rebase-BAQPKHF6.js → git-rebase-CpwUWCzK.js} +1 -1
- package/dist/ui/assets/{glimmer-js-P_wOfSz_.js → glimmer-js-BRxccvnE.js} +1 -1
- package/dist/ui/assets/{glimmer-ts-B44aY1fZ.js → glimmer-ts-CgQbPzlT.js} +1 -1
- package/dist/ui/assets/glsl-D7JmHFr0.js +1 -0
- package/dist/ui/assets/graphql-C8n0eUBK.js +1 -0
- package/dist/ui/assets/{hack-ddU2k0nt.js → hack-lVN7KVQv.js} +1 -1
- package/dist/ui/assets/haml-h18lG0pS.js +1 -0
- package/dist/ui/assets/{handlebars-BpzaUpmz.js → handlebars-lRFjRPfj.js} +1 -1
- package/dist/ui/assets/heavy-payload-Cqx9MOkH.js +290 -0
- package/dist/ui/assets/html-DtILqTE1.js +1 -0
- package/dist/ui/assets/{html-derivative-B2beVdmA.js → html-derivative-CSTSmPup.js} +1 -1
- package/dist/ui/assets/{http-iEskZy8U.js → http-DNI8fsbY.js} +1 -1
- package/dist/ui/assets/{hurl-BuUsLSKv.js → hurl-DgRi60JG.js} +1 -1
- package/dist/ui/assets/java-CW9pZ5bh.js +1 -0
- package/dist/ui/assets/javascript-Cvg2UrWL.js +1 -0
- package/dist/ui/assets/{jinja-CRpkWwrH.js → jinja-CZw_S1FB.js} +1 -1
- package/dist/ui/assets/{jison-BYfSnuHj.js → jison-C56u_FZv.js} +1 -1
- package/dist/ui/assets/json-DW2UTv1q.js +1 -0
- package/dist/ui/assets/jsx-Zj5gobdD.js +1 -0
- package/dist/ui/assets/{julia-B8Rxy7vD.js → julia-Cvcrre-L.js} +1 -1
- package/dist/ui/assets/{just-DzhIWpFP.js → just-8CZ2u6Tj.js} +1 -1
- package/dist/ui/assets/{latex-CUNjUUL-.js → latex-D289AXWG.js} +1 -1
- package/dist/ui/assets/{liquid-hUg7nC61.js → liquid-CFEX3SkZ.js} +1 -1
- package/dist/ui/assets/lua-BKpJ0ybO.js +1 -0
- package/dist/ui/assets/{marko-CQmIkwMF.js → marko-D9maZ0a6.js} +1 -1
- package/dist/ui/assets/{mdc-CB-6dHYa.js → mdc-rM9l0qWR.js} +1 -1
- package/dist/ui/assets/{nginx-CQORo_Bt.js → nginx-C9w6p2C9.js} +1 -1
- package/dist/ui/assets/{nim-BNwBNzdn.js → nim-BRePHk0J.js} +1 -1
- package/dist/ui/assets/{perl-Cnz14fAJ.js → perl-CskTmiPQ.js} +1 -1
- package/dist/ui/assets/{php-YywDo63b.js → php-DCU_FvNI.js} +1 -1
- package/dist/ui/assets/{pug-BwbJp4cM.js → pug-XLaVxTvv.js} +1 -1
- package/dist/ui/assets/{qml-D2DY6dMH.js → qml-CaONKSl-.js} +1 -1
- package/dist/ui/assets/r-BCVR0ZE3.js +1 -0
- package/dist/ui/assets/{razor-pMTG7k41.js → razor-DDnGXxnU.js} +1 -1
- package/dist/ui/assets/regexp-DWGTYYVx.js +1 -0
- package/dist/ui/assets/{rst-CO_ndDm4.js → rst-Dz3GWvQm.js} +1 -1
- package/dist/ui/assets/{ruby-xzRn9gTh.js → ruby-DC3jGtMY.js} +1 -1
- package/dist/ui/assets/{sas-CBdtFKLA.js → sas-7mNpvsjZ.js} +1 -1
- package/dist/ui/assets/scss-uBsoCJVy.js +1 -0
- package/dist/ui/assets/shellscript-BCN9N_Jf.js +1 -0
- package/dist/ui/assets/{shellsession-BaZnm5rZ.js → shellsession-Hsw46VXV.js} +1 -1
- package/dist/ui/assets/{soy-IouG3xdB.js → soy-D6KV69HU.js} +1 -1
- package/dist/ui/assets/sql-CVzVQkJh.js +1 -0
- package/dist/ui/assets/{stata-dgbkIZHP.js → stata-BwUjXNhI.js} +1 -1
- package/dist/ui/assets/{surrealql-CBROnw_p.js → surrealql-CgloWqRa.js} +1 -1
- package/dist/ui/assets/{svelte-6r6BpX4-.js → svelte-DcC3n6IC.js} +1 -1
- package/dist/ui/assets/{templ-BKD_PsbL.js → templ-DOFfnWTJ.js} +1 -1
- package/dist/ui/assets/{tex-CKY1bwHb.js → tex-ln89VPaX.js} +1 -1
- package/dist/ui/assets/{ts-tags-BiYP5N_C.js → ts-tags-CWMzCstH.js} +1 -1
- package/dist/ui/assets/tsx-DgzfiGeK.js +1 -0
- package/dist/ui/assets/{twig-BTe1oPFW.js → twig-Bi2itZL9.js} +1 -1
- package/dist/ui/assets/typescript-CQ24f4zW.js +1 -0
- package/dist/ui/assets/{vue-CT0IiySh.js → vue-BS8jE5u6.js} +1 -1
- package/dist/ui/assets/{vue-html-B-tXcbat.js → vue-html-o38xqhyB.js} +1 -1
- package/dist/ui/assets/{vue-vine-C5ZyDCrs.js → vue-vine-D2NvKv50.js} +1 -1
- package/dist/ui/assets/xml-_44rx9_2.js +1 -0
- package/dist/ui/assets/{xsl-DvDSndNI.js → xsl-B5QWQPoi.js} +1 -1
- package/dist/ui/assets/yaml-CKL2pEWe.js +1 -0
- package/dist/workspaces/relay/auth/remote-auth.js +3 -5
- package/dist/workspaces/relay/tests/test-support.js +7 -4
- package/dist/workspaces/relay/workspace-relay.js +14 -8
- package/dist/workspaces/resources/skills.js +1 -1
- package/docs/chatgpt-coding-workflow.md +12 -6
- package/docs/configuration.md +158 -6
- package/docs/debugging.md +8 -5
- package/docs/roadmap.md +64 -3
- package/docs/security.md +18 -0
- package/package.json +11 -4
- package/scripts/debug/accept/bootstrap.mjs +2 -0
- package/scripts/debug/accept/harness.mjs +9 -2
- package/scripts/debug/accept/modern-http.mjs +169 -0
- package/scripts/debug/accept/support.mjs +10 -6
- package/scripts/debug/accept.mjs +1 -0
- package/scripts/debug/runtime.mjs +4 -1
- package/scripts/debug/runtime.test.mjs +9 -0
- package/scripts/release/parity-sandbox.mjs +35 -0
- package/scripts/release/release-gate.test.mjs +42 -1
- package/scripts/release-parity.mjs +3 -22
- package/scripts/release-proof.mjs +24 -4
- package/scripts/release-proof.test.mjs +12 -4
- package/dist/ui/assets/activity-panel-app-XlqSanXT.js +0 -115
- package/dist/ui/assets/angular-html-tB9EchbM.js +0 -1
- package/dist/ui/assets/c-Dv-N5hjn.js +0 -1
- package/dist/ui/assets/cpp-C3unXX-3.js +0 -1
- package/dist/ui/assets/css-8Xr4kNNH.js +0 -1
- package/dist/ui/assets/glsl-BvCQg9MI.js +0 -1
- package/dist/ui/assets/graphql-C9S90gn_.js +0 -1
- package/dist/ui/assets/haml-Bb1w9rMG.js +0 -1
- package/dist/ui/assets/heavy-payload-CJKqi8Ac.js +0 -290
- package/dist/ui/assets/html-dUOs5pA9.js +0 -1
- package/dist/ui/assets/java-BFZCrORY.js +0 -1
- package/dist/ui/assets/javascript-DaIQ0Zqj.js +0 -1
- package/dist/ui/assets/json-BQC_Yp48.js +0 -1
- package/dist/ui/assets/jsx-Dd-CJRf9.js +0 -1
- package/dist/ui/assets/lua-BoZTiL2I.js +0 -1
- package/dist/ui/assets/r-B5nYsy-M.js +0 -1
- package/dist/ui/assets/regexp-oP0lbks_.js +0 -1
- package/dist/ui/assets/scss-CEqgKQNf.js +0 -1
- package/dist/ui/assets/shellscript-DzJJiWQC.js +0 -1
- package/dist/ui/assets/sql-DC2rdTzV.js +0 -1
- package/dist/ui/assets/tsx-D2OMiPNF.js +0 -1
- package/dist/ui/assets/typescript-BGIABksB.js +0 -1
- package/dist/ui/assets/xml-CfeBNlMP.js +0 -1
- package/dist/ui/assets/yaml-umbj2r1g.js +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@akira-tl/forgerelay",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.2",
|
|
4
4
|
"description": "Local development control plane for MCP coding agents.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"homepage": "https://github.com/Akira-TL/forgerelay#readme",
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
"debug:serve": "node scripts/debug/serve.mjs",
|
|
41
41
|
"debug:accept": "node scripts/debug/accept.mjs",
|
|
42
42
|
"debug:accept:media": "node scripts/debug/accept/media.mjs",
|
|
43
|
+
"debug:accept:modern": "node scripts/debug/accept/modern-http.mjs",
|
|
43
44
|
"debug:accept:relay": "node scripts/debug/relay-accept.mjs",
|
|
44
45
|
"traffic:audit": "node --import tsx scripts/debug/traffic/traffic-audit.mjs",
|
|
45
46
|
"lsp:interop": "node scripts/lsp/interop.mjs",
|
|
@@ -57,7 +58,7 @@
|
|
|
57
58
|
"release:push-ready": "node scripts/release/push-ready.mjs",
|
|
58
59
|
"postinstall": "node scripts/fix-node-pty-permissions.mjs",
|
|
59
60
|
"start": "node dist/cli.js serve",
|
|
60
|
-
"test": "node --test scripts/debug/runtime.test.mjs scripts/release-proof.test.mjs scripts/release/release-gate.test.mjs scripts/release/push-ready.test.mjs scripts/release/release-version.test.mjs && tsx src/mcp/oauth/router.test.ts && tsx src/workspaces/relay/auth/remote-auth-cli.test.ts && tsx src/workspaces/relay/auth/remote-ssh-auth-cli.test.ts && tsx src/workspaces/relay/tests/lifecycle.test.ts && tsx src/workspaces/relay/tests/routing.test.ts && tsx src/workspaces/relay/tests/ssh.test.ts && tsx src/workspaces/relay/tests/process.test.ts && tsx src/workspaces/relay/tests/recovery.test.ts && tsx src/workspaces/relay/tests/checkpoint.test.ts && tsx src/runtime/config/config.test.ts && tsx src/runtime/shell/command-shell-runtime.test.ts && tsx src/runtime/instructions/shell-instructions.test.ts && tsx src/runtime/instructions/powershell-skill.test.ts && tsx src/cli/shell/setup.test.ts && tsx src/lsp/language-server-config.test.ts && tsx src/lsp/runtime/managed-language-servers.test.ts && tsx src/lsp/normalization/hover.test.ts && tsx src/lsp/references.server.test.ts && tsx src/lsp/operations/document-symbols.server.test.ts && tsx src/lsp/operations/workspace-symbols.server.test.ts && tsx src/lsp/operations/diagnostics-push.server.test.ts && tsx src/lsp/operations/diagnostics-pull.server.test.ts && tsx src/lsp/operations/request-hardening.server.test.ts && tsx src/lsp/operations/recovery.server.test.ts && tsx src/lsp/operations/lifecycle.server.test.ts && tsx src/lsp/runtime/semantic-requests.test.ts && tsx src/runtime/logging/logger.test.ts && tsx src/runtime/logging/proxy-trust.test.ts && tsx src/runtime/state/lock/file-lock.test.ts && tsx src/runtime/state/runtime-lease.test.ts && tsx src/mcp/panel/mcp-app-template.test.ts && tsx src/mcp/hooks/hooks.test.ts && tsx src/mcp/server/core/capability-registry.test.ts && tsx src/mcp/server-instructions.test.ts && tsx src/mcp/request-meta.test.ts && tsx src/mcp/artifacts/incoming-artifacts.test.ts && tsx src/mcp/artifacts/artifact-download.test.ts && tsx src/ui/core/card-types.test.ts && tsx src/ui/activity/model.test.ts && tsx src/ui/activity/detail-card.test.ts && tsx src/ui/review/patch-display.test.ts && tsx src/ui/core/tool-display.test.ts && tsx src/mcp/filesystem/apply-patch.test.ts && tsx src/mcp/process/process-platform.test.ts && tsx src/mcp/process/process-sessions.test.ts && tsx src/mcp/server/transport/mcp-sessions.test.ts && tsx src/mcp/server/transport/server-shutdown.test.ts && tsx src/mcp/server/operations/mutation-diagnostics.test.ts && tsx src/mcp/server/operations/hooks.test.ts && tsx src/subagents/providers/adapters/codex.test.ts && tsx src/subagents/providers/adapters/pi.test.ts && tsx src/subagents/providers/registry.test.ts && tsx src/subagents/providers/availability.test.ts && tsx src/subagents/profiles.test.ts && tsx src/subagents/cli-target.test.ts && tsx src/subagents/sessions/store.test.ts && tsx src/subagents/sessions/manager.test.ts && tsx src/subagents/sessions/mcp/capability.server.test.ts && tsx src/subagents/sessions/mcp/continuation.server.test.ts && tsx src/subagents/sessions/mcp/lifecycle.server.test.ts && tsx src/subagents/sessions/mcp/reconciliation.server.test.ts && tsx src/subagents/sessions/mcp/routing.server.test.ts && tsx src/mcp/filesystem/roots.test.ts && tsx src/mcp/filesystem/file-mutations.test.ts && tsx src/mcp/operations/edit-preflight.test.ts && tsx src/workspaces/resources/skills.test.ts && tsx src/runtime/state/db/migrations.test.ts && tsx src/workspaces/state/workspace-store.test.ts && tsx src/workspaces/tasks/workspace-tasks.test.ts && tsx src/workspaces/tasks/workspace-task-reminders.test.ts && tsx src/activity/history/audit-store.test.ts && tsx src/activity/history/bash-output-store.test.ts && tsx src/activity/runtime/lifecycle.test.ts && tsx src/activity/history/query-service.test.ts && tsx src/mcp/operations/core-operation-executor.test.ts && tsx src/mcp/operations/bulk-mutation.test.ts && tsx src/mcp/operations/batch/scheduler.test.ts && tsx src/mcp/operations/batch/executor-policy.test.ts && tsx src/workspaces.test.ts && tsx src/workspaces/conversation-checkout.test.ts && tsx src/workspaces/conversation-worktree.test.ts && tsx src/workspaces/git/worktree-recovery.test.ts && tsx src/mcp/server/workspace/workspace-inventory.test.ts && tsx src/mcp/server/workspace/workspace-recovery.test.ts && tsx src/mcp/server/workspace/workspace-checkpoint.test.ts && tsx src/workspaces/review/review-checkpoints.test.ts && tsx src/lsp/code-intelligence.server.test.ts && npm run build:app && tsx src/mcp/process/server.test.ts && tsx src/mcp/panel/server.test.ts && tsx src/mcp/server/server.test.ts && tsx src/mcp/oauth/oauth-store.test.ts && tsx src/cli/maintenance.test.ts && tsx src/cli/maintenance-prune.test.ts && tsx src/cli/cli.test.ts",
|
|
61
|
+
"test": "node --test scripts/debug/runtime.test.mjs scripts/release-proof.test.mjs scripts/release/release-gate.test.mjs scripts/release/push-ready.test.mjs scripts/release/release-version.test.mjs && tsx src/mcp/oauth/router.test.ts && tsx src/workspaces/relay/auth/remote-auth-cli.test.ts && tsx src/workspaces/relay/auth/remote-ssh-auth-cli.test.ts && tsx src/workspaces/relay/tests/lifecycle.test.ts && tsx src/workspaces/relay/tests/routing.test.ts && tsx src/workspaces/relay/tests/ssh.test.ts && tsx src/workspaces/relay/tests/process.test.ts && tsx src/workspaces/relay/tests/recovery.test.ts && tsx src/workspaces/relay/tests/checkpoint.test.ts && tsx src/runtime/config/config.test.ts && tsx src/runtime/config/external-mcp-registry.test.ts && tsx src/runtime/config/external-mcp-auth-store.test.ts && tsx src/mcp/operations/external-mcp/external-mcp-oauth.test.ts && tsx src/cli/mcp/status.test.ts && tsx src/cli/mcp/diagnostics.test.ts && tsx src/cli/mcp/external-mcp.test.ts && tsx src/runtime/shell/command-shell-runtime.test.ts && tsx src/runtime/instructions/shell-instructions.test.ts && tsx src/runtime/instructions/powershell-skill.test.ts && tsx src/cli/shell/setup.test.ts && tsx src/lsp/language-server-config.test.ts && tsx src/lsp/runtime/managed-language-servers.test.ts && tsx src/lsp/normalization/hover.test.ts && tsx src/lsp/references.server.test.ts && tsx src/lsp/operations/document-symbols.server.test.ts && tsx src/lsp/operations/workspace-symbols.server.test.ts && tsx src/lsp/operations/diagnostics-push.server.test.ts && tsx src/lsp/operations/diagnostics-pull.server.test.ts && tsx src/lsp/operations/request-hardening.server.test.ts && tsx src/lsp/operations/recovery.server.test.ts && tsx src/lsp/operations/lifecycle.server.test.ts && tsx src/lsp/runtime/semantic-requests.test.ts && tsx src/runtime/logging/logger.test.ts && tsx src/runtime/logging/proxy-trust.test.ts && tsx src/runtime/state/lock/file-lock.test.ts && tsx src/runtime/state/runtime-lease.test.ts && tsx src/mcp/panel/mcp-app-template.test.ts && tsx src/mcp/hooks/hooks.test.ts && tsx src/mcp/server/core/capability-registry.test.ts && tsx src/mcp/server-instructions.test.ts && tsx src/mcp/request-context.test.ts && tsx src/mcp/request-meta.test.ts && tsx src/mcp/artifacts/incoming-artifacts.test.ts && tsx src/mcp/artifacts/artifact-download.test.ts && tsx src/ui/core/card-types.test.ts && tsx src/ui/activity/model.test.ts && tsx src/ui/activity/detail-card.test.ts && tsx src/ui/review/patch-display.test.ts && tsx src/ui/core/tool-display.test.ts && tsx src/mcp/filesystem/apply-patch.test.ts && tsx src/mcp/process/process-platform.test.ts && tsx src/mcp/process/process-sessions.test.ts && tsx src/mcp/server/transport/mcp-sessions.test.ts && tsx src/mcp/server/transport/server-shutdown.test.ts && tsx src/mcp/server/operations/mutation-diagnostics.test.ts && tsx src/mcp/server/operations/hooks.test.ts && tsx src/subagents/providers/adapters/codex.test.ts && tsx src/subagents/providers/adapters/pi.test.ts && tsx src/subagents/providers/registry.test.ts && tsx src/subagents/providers/availability.test.ts && tsx src/subagents/profiles.test.ts && tsx src/subagents/cli-target.test.ts && tsx src/subagents/sessions/store.test.ts && tsx src/subagents/sessions/manager.test.ts && tsx src/subagents/sessions/mcp/capability.server.test.ts && tsx src/subagents/sessions/mcp/continuation.server.test.ts && tsx src/subagents/sessions/mcp/lifecycle.server.test.ts && tsx src/subagents/sessions/mcp/reconciliation.server.test.ts && tsx src/subagents/sessions/mcp/routing.server.test.ts && tsx src/mcp/filesystem/roots.test.ts && tsx src/mcp/filesystem/file-mutations.test.ts && tsx src/mcp/operations/edit-preflight.test.ts && tsx src/workspaces/resources/skills.test.ts && tsx src/runtime/state/db/migrations.test.ts && tsx src/workspaces/state/workspace-store.test.ts && tsx src/workspaces/tasks/workspace-tasks.test.ts && tsx src/workspaces/tasks/workspace-task-reminders.test.ts && tsx src/activity/history/audit-store.test.ts && tsx src/activity/history/bash-output-store.test.ts && tsx src/activity/runtime/lifecycle.test.ts && tsx src/activity/history/query-service.test.ts && tsx src/mcp/operations/core-operation-executor.test.ts && tsx src/mcp/operations/bulk-mutation.test.ts && tsx src/mcp/operations/batch/scheduler.test.ts && tsx src/mcp/operations/batch/executor-policy.test.ts && tsx src/workspaces.test.ts && tsx src/workspaces/conversation-checkout.test.ts && tsx src/workspaces/conversation-worktree.test.ts && tsx src/workspaces/git/worktree-recovery.test.ts && tsx src/mcp/server/workspace/workspace-inventory.test.ts && tsx src/mcp/server/workspace/workspace-recovery.test.ts && tsx src/mcp/server/workspace/workspace-checkpoint.test.ts && tsx src/workspaces/review/review-checkpoints.test.ts && tsx src/lsp/code-intelligence.server.test.ts && npm run build:app && tsx src/mcp/process/server.test.ts && tsx src/mcp/panel/server.test.ts && tsx src/mcp/server/server.test.ts && tsx src/mcp/oauth/oauth-store.test.ts && tsx src/cli/maintenance.test.ts && tsx src/cli/maintenance-prune.test.ts && tsx src/cli/cli.test.ts",
|
|
61
62
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
62
63
|
"release:check": "node scripts/release-version.mjs check",
|
|
63
64
|
"release:tag-check": "node scripts/release-version.mjs tag",
|
|
@@ -80,13 +81,18 @@
|
|
|
80
81
|
"dependencies": {
|
|
81
82
|
"@agentclientprotocol/sdk": "^1.1.0",
|
|
82
83
|
"@clack/prompts": "^1.5.1",
|
|
83
|
-
"@modelcontextprotocol/
|
|
84
|
-
"@modelcontextprotocol/
|
|
84
|
+
"@modelcontextprotocol/client": "^2.0.0",
|
|
85
|
+
"@modelcontextprotocol/core": "^2.0.0",
|
|
86
|
+
"@modelcontextprotocol/express": "^2.0.0",
|
|
87
|
+
"@modelcontextprotocol/ext-apps": "^2.0.0",
|
|
88
|
+
"@modelcontextprotocol/node": "^2.0.0",
|
|
89
|
+
"@modelcontextprotocol/server": "^2.0.0",
|
|
85
90
|
"@pierre/diffs": "^1.2.5",
|
|
86
91
|
"better-sqlite3": "^13.0.3",
|
|
87
92
|
"diff": "^8.0.3",
|
|
88
93
|
"drizzle-orm": "^0.45.2",
|
|
89
94
|
"express": "^5.2.1",
|
|
95
|
+
"express-rate-limit": "8.5.2",
|
|
90
96
|
"lucide": "^1.24.0",
|
|
91
97
|
"react": "^19.2.6",
|
|
92
98
|
"react-dom": "^19.2.6",
|
|
@@ -97,6 +103,7 @@
|
|
|
97
103
|
"zod": "^4.4.3"
|
|
98
104
|
},
|
|
99
105
|
"devDependencies": {
|
|
106
|
+
"@modelcontextprotocol/sdk": "1.29.0",
|
|
100
107
|
"@types/better-sqlite3": "^7.6.13",
|
|
101
108
|
"@types/express": "^5.0.6",
|
|
102
109
|
"@types/node": "^25.9.1",
|
|
@@ -195,6 +195,7 @@ export async function runBootstrapAcceptance({ server, packageJson, ownerToken,
|
|
|
195
195
|
"workspace.recovery",
|
|
196
196
|
"workspace.checkpoint",
|
|
197
197
|
"batch.execute",
|
|
198
|
+
"mcp.external",
|
|
198
199
|
...(process.platform === "linux" ? ["artifact.native-download"] : []),
|
|
199
200
|
"ui.mcp-app",
|
|
200
201
|
"review.changes",
|
|
@@ -207,6 +208,7 @@ export async function runBootstrapAcceptance({ server, packageJson, ownerToken,
|
|
|
207
208
|
"code.intelligence",
|
|
208
209
|
"workspace.checkpoint",
|
|
209
210
|
"workspace.tasks",
|
|
211
|
+
"mcp.external",
|
|
210
212
|
"batch.execute",
|
|
211
213
|
...(process.platform === "linux" ? ["artifact.download"] : []),
|
|
212
214
|
]);
|
|
@@ -6,6 +6,7 @@ import { tmpdir } from "node:os";
|
|
|
6
6
|
import { join, resolve } from "node:path";
|
|
7
7
|
import {
|
|
8
8
|
createDebugEnvironment,
|
|
9
|
+
debugConfigDir,
|
|
9
10
|
debugRoot,
|
|
10
11
|
repoRoot,
|
|
11
12
|
} from "../runtime.mjs";
|
|
@@ -19,6 +20,7 @@ import { assertCurlAvailable, assertDebugPortFree, pass, setupGitProject } from
|
|
|
19
20
|
export async function createAcceptanceHarness() {
|
|
20
21
|
const packageJson = JSON.parse(readFileSync(join(repoRoot, "package.json"), "utf8"));
|
|
21
22
|
const acceptanceRoot = resolve(debugRoot, "acceptance");
|
|
23
|
+
const configDir = resolve(acceptanceRoot, "config");
|
|
22
24
|
const stateDir = resolve(acceptanceRoot, "state");
|
|
23
25
|
const worktreeRoot = resolve(acceptanceRoot, "worktrees");
|
|
24
26
|
const hookLog = resolve(acceptanceRoot, "hooks.jsonl");
|
|
@@ -35,7 +37,11 @@ export async function createAcceptanceHarness() {
|
|
|
35
37
|
assertCurlAvailable();
|
|
36
38
|
await assertDebugPortFree();
|
|
37
39
|
rmSync(acceptanceRoot, { recursive: true, force: true });
|
|
38
|
-
mkdirSync(
|
|
40
|
+
mkdirSync(configDir, { recursive: true });
|
|
41
|
+
writeFileSync(
|
|
42
|
+
join(configDir, "config.json"),
|
|
43
|
+
readFileSync(join(debugConfigDir, "config.json"), "utf8"),
|
|
44
|
+
);
|
|
39
45
|
setupGitProject(checkoutWorkspace);
|
|
40
46
|
setupGitProject(lifecycleDeleteWorkspace);
|
|
41
47
|
writeFileSync(join(lifecycleDeleteWorkspace, "keep.txt"), "keep checkout files\n");
|
|
@@ -44,6 +50,7 @@ export async function createAcceptanceHarness() {
|
|
|
44
50
|
|
|
45
51
|
const { env } = createDebugEnvironment({
|
|
46
52
|
ownerToken,
|
|
53
|
+
configDir,
|
|
47
54
|
stateDir,
|
|
48
55
|
worktreeRoot,
|
|
49
56
|
hookLog,
|
|
@@ -69,7 +76,7 @@ export async function createAcceptanceHarness() {
|
|
|
69
76
|
});
|
|
70
77
|
|
|
71
78
|
return {
|
|
72
|
-
packageJson, acceptanceRoot, stateDir, worktreeRoot, hookLog, checkoutWorkspace,
|
|
79
|
+
packageJson, acceptanceRoot, configDir, stateDir, worktreeRoot, hookLog, checkoutWorkspace,
|
|
73
80
|
lifecycleDeleteWorkspace, codeIntelligenceLog, fakeLanguageServer, gitProject, releaseProject,
|
|
74
81
|
releaseRemote, ownerToken, tempAcceptanceRoot, env, server,
|
|
75
82
|
};
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { randomBytes } from "node:crypto";
|
|
4
|
+
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { join, resolve } from "node:path";
|
|
6
|
+
import { Client, StreamableHTTPClientTransport } from "@modelcontextprotocol/client";
|
|
7
|
+
import { debugRoot, repoRoot } from "../runtime.mjs";
|
|
8
|
+
import { assertPortFree, pass, stopServer, waitForHealth } from "./support.mjs";
|
|
9
|
+
|
|
10
|
+
const port = 7678;
|
|
11
|
+
const baseUrl = `http://127.0.0.1:${port}`;
|
|
12
|
+
const mcpUrl = `${baseUrl}/mcp`;
|
|
13
|
+
const acceptanceRoot = resolve(debugRoot, "modern-http-acceptance");
|
|
14
|
+
const configDir = join(acceptanceRoot, "config");
|
|
15
|
+
const stateDir = join(acceptanceRoot, "state");
|
|
16
|
+
const worktreeRoot = join(acceptanceRoot, "worktrees");
|
|
17
|
+
const workspaceRoot = join(acceptanceRoot, "workspace");
|
|
18
|
+
const ownerToken = randomBytes(32).toString("base64url");
|
|
19
|
+
|
|
20
|
+
await assertPortFree(port);
|
|
21
|
+
rmSync(acceptanceRoot, { recursive: true, force: true });
|
|
22
|
+
mkdirSync(configDir, { recursive: true });
|
|
23
|
+
mkdirSync(stateDir, { recursive: true });
|
|
24
|
+
mkdirSync(workspaceRoot, { recursive: true });
|
|
25
|
+
writeFileSync(join(workspaceRoot, "README.md"), "modern MCP acceptance workspace\n");
|
|
26
|
+
writeFileSync(join(configDir, "config.json"), `${JSON.stringify({
|
|
27
|
+
host: "127.0.0.1",
|
|
28
|
+
port,
|
|
29
|
+
allowedRoots: [workspaceRoot],
|
|
30
|
+
publicBaseUrl: baseUrl,
|
|
31
|
+
allowedHosts: ["localhost", "127.0.0.1", "::1"],
|
|
32
|
+
stateDir,
|
|
33
|
+
worktreeRoot,
|
|
34
|
+
}, null, 2)}\n`);
|
|
35
|
+
writeFileSync(join(configDir, "auth.json"), `${JSON.stringify({
|
|
36
|
+
ownerToken,
|
|
37
|
+
instanceId: "modern-http-acceptance-7678",
|
|
38
|
+
}, null, 2)}\n`, { mode: 0o600 });
|
|
39
|
+
|
|
40
|
+
const cleanEnv = Object.fromEntries(
|
|
41
|
+
Object.entries(process.env).filter(([name]) =>
|
|
42
|
+
name !== "HOST" && name !== "PORT" && !name.startsWith("FORGERELAY_")
|
|
43
|
+
),
|
|
44
|
+
);
|
|
45
|
+
const env = {
|
|
46
|
+
...cleanEnv,
|
|
47
|
+
HOST: "127.0.0.1",
|
|
48
|
+
PORT: String(port),
|
|
49
|
+
FORGERELAY_CONFIG_DIR: configDir,
|
|
50
|
+
FORGERELAY_OAUTH_OWNER_TOKEN: ownerToken,
|
|
51
|
+
FORGERELAY_PUBLIC_BASE_URL: baseUrl,
|
|
52
|
+
FORGERELAY_TOOL_MODE: "full",
|
|
53
|
+
FORGERELAY_WIDGETS: "off",
|
|
54
|
+
FORGERELAY_LOG_LEVEL: "debug",
|
|
55
|
+
FORGERELAY_LOG_FORMAT: "json",
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
let serverLogs = "";
|
|
59
|
+
const server = spawn(process.execPath, ["--import", "tsx", "src/cli.ts", "serve"], {
|
|
60
|
+
cwd: repoRoot,
|
|
61
|
+
env,
|
|
62
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
63
|
+
});
|
|
64
|
+
server.stdout?.on("data", (chunk) => { serverLogs += chunk.toString(); });
|
|
65
|
+
server.stderr?.on("data", (chunk) => { serverLogs += chunk.toString(); });
|
|
66
|
+
|
|
67
|
+
let client;
|
|
68
|
+
try {
|
|
69
|
+
await waitForHealth(server, baseUrl);
|
|
70
|
+
pass("modern HTTP health", baseUrl);
|
|
71
|
+
|
|
72
|
+
const unauthorized = await fetch(mcpUrl, {
|
|
73
|
+
method: "POST",
|
|
74
|
+
headers: {
|
|
75
|
+
"content-type": "application/json",
|
|
76
|
+
accept: "application/json, text/event-stream",
|
|
77
|
+
},
|
|
78
|
+
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "server/discover", params: {} }),
|
|
79
|
+
});
|
|
80
|
+
assert.equal(unauthorized.status, 401);
|
|
81
|
+
assert.match(unauthorized.headers.get("www-authenticate") ?? "", /Bearer .*resource_metadata=/i);
|
|
82
|
+
pass("modern HTTP bearer challenge", "unauthenticated MCP request advertises protected-resource metadata");
|
|
83
|
+
|
|
84
|
+
const tokenResponse = await fetch(`${baseUrl}/auth/cli`, {
|
|
85
|
+
method: "POST",
|
|
86
|
+
headers: { "content-type": "application/json" },
|
|
87
|
+
body: JSON.stringify({ owner_token: ownerToken }),
|
|
88
|
+
});
|
|
89
|
+
const tokenBody = await tokenResponse.text();
|
|
90
|
+
assert.equal(tokenResponse.status, 200, tokenBody);
|
|
91
|
+
const tokens = JSON.parse(tokenBody);
|
|
92
|
+
assert.equal(tokens.token_type, "bearer");
|
|
93
|
+
assert.ok(tokens.access_token);
|
|
94
|
+
pass("modern HTTP auth", "real ForgeRelay bearer token issued on 7678");
|
|
95
|
+
|
|
96
|
+
const transport = new StreamableHTTPClientTransport(new URL(mcpUrl), {
|
|
97
|
+
requestInit: {
|
|
98
|
+
headers: { authorization: `Bearer ${tokens.access_token}` },
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
client = new Client(
|
|
102
|
+
{ name: "forgerelay-modern-http-acceptance", version: "1.0.0" },
|
|
103
|
+
{ versionNegotiation: { mode: { pin: "2026-07-28" } } },
|
|
104
|
+
);
|
|
105
|
+
await client.connect(transport);
|
|
106
|
+
assert.equal(client.getProtocolEra(), "modern");
|
|
107
|
+
assert.equal(client.getNegotiatedProtocolVersion(), "2026-07-28");
|
|
108
|
+
assert.equal(transport.sessionId, undefined);
|
|
109
|
+
pass("modern protocol negotiation", "2026-07-28 connected without Mcp-Session-Id");
|
|
110
|
+
|
|
111
|
+
const listed = await client.listTools();
|
|
112
|
+
assert.ok(listed.tools.some((tool) => tool.name === "open_workspace"));
|
|
113
|
+
const opened = await client.callTool({
|
|
114
|
+
name: "open_workspace",
|
|
115
|
+
arguments: { path: workspaceRoot, context: "none" },
|
|
116
|
+
});
|
|
117
|
+
assert.notEqual(opened.isError, true);
|
|
118
|
+
const workspaceId = String(opened.structuredContent?.workspaceId ?? "");
|
|
119
|
+
assert.match(workspaceId, /^ws_/);
|
|
120
|
+
pass("modern tools call", "tools/list and open_workspace completed over stateless HTTP");
|
|
121
|
+
|
|
122
|
+
const conversationMeta = { "dev.forgerelay/conversation": "modern-http-acceptance-conversation" };
|
|
123
|
+
const panel = await client.callTool({
|
|
124
|
+
name: "activity_panel",
|
|
125
|
+
arguments: { workspaceId },
|
|
126
|
+
_meta: conversationMeta,
|
|
127
|
+
});
|
|
128
|
+
assert.notEqual(panel.isError, true);
|
|
129
|
+
const turnId = String(panel.structuredContent?.turnId ?? "");
|
|
130
|
+
assert.match(turnId, /^turn_/);
|
|
131
|
+
const restored = await client.callTool({
|
|
132
|
+
name: "activity_snapshot",
|
|
133
|
+
arguments: { workspaceId },
|
|
134
|
+
_meta: conversationMeta,
|
|
135
|
+
});
|
|
136
|
+
assert.notEqual(restored.isError, true);
|
|
137
|
+
assert.equal(restored.structuredContent?.turnId, turnId);
|
|
138
|
+
pass("modern vendor conversation continuity", "stable ForgeRelay metadata restored the Host Turn across stateless requests");
|
|
139
|
+
|
|
140
|
+
const requestScopedPanel = await client.callTool({
|
|
141
|
+
name: "activity_panel",
|
|
142
|
+
arguments: { workspaceId },
|
|
143
|
+
});
|
|
144
|
+
assert.notEqual(requestScopedPanel.isError, true);
|
|
145
|
+
const requestScopedSnapshot = await client.callTool({
|
|
146
|
+
name: "activity_snapshot",
|
|
147
|
+
arguments: { workspaceId },
|
|
148
|
+
});
|
|
149
|
+
assert.equal(requestScopedSnapshot.isError, true);
|
|
150
|
+
assert.match(
|
|
151
|
+
requestScopedSnapshot.content?.map((entry) => entry.type === "text" ? entry.text : "").join("\n") ?? "",
|
|
152
|
+
/could not resolve the current Host Turn/i,
|
|
153
|
+
);
|
|
154
|
+
pass("modern request scope isolation", "requests without stable metadata do not share Host Turn state");
|
|
155
|
+
|
|
156
|
+
assert.equal(serverLogs.includes("\"protocolEra\":\"modern\""), true);
|
|
157
|
+
pass("modern routing observability", "ForgeRelay classified the accepted request as modern");
|
|
158
|
+
|
|
159
|
+
console.log("Modern HTTP acceptance passed.");
|
|
160
|
+
} catch (error) {
|
|
161
|
+
if (serverLogs) process.stderr.write(`\n--- ForgeRelay 7678 logs ---\n${serverLogs}`);
|
|
162
|
+
throw error;
|
|
163
|
+
} finally {
|
|
164
|
+
try {
|
|
165
|
+
await client?.close();
|
|
166
|
+
} finally {
|
|
167
|
+
await stopServer(server);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
@@ -198,20 +198,20 @@ export function curlRequest({ method = "GET", url, headers = {}, body }) {
|
|
|
198
198
|
return { status, headers: responseHeaders, body: responseBody };
|
|
199
199
|
}
|
|
200
200
|
|
|
201
|
-
export async function waitForHealth(child) {
|
|
201
|
+
export async function waitForHealth(child, baseUrl = debugBaseUrl) {
|
|
202
202
|
for (let attempt = 0; attempt < 60; attempt += 1) {
|
|
203
203
|
if (child.exitCode !== null) {
|
|
204
204
|
throw new Error(`debug server exited before health check: ${child.exitCode}`);
|
|
205
205
|
}
|
|
206
206
|
try {
|
|
207
|
-
const response = jsonRequest(`${
|
|
207
|
+
const response = jsonRequest(`${baseUrl}/healthz`);
|
|
208
208
|
if (response.status === 200) return response.json;
|
|
209
209
|
} catch {
|
|
210
210
|
// Server is still starting.
|
|
211
211
|
}
|
|
212
212
|
await delay(100);
|
|
213
213
|
}
|
|
214
|
-
throw new Error(
|
|
214
|
+
throw new Error(`debug server did not become healthy at ${baseUrl}`);
|
|
215
215
|
}
|
|
216
216
|
|
|
217
217
|
export async function stopServer(child) {
|
|
@@ -397,13 +397,13 @@ export function readHookEntries(path) {
|
|
|
397
397
|
.map((line) => JSON.parse(line));
|
|
398
398
|
}
|
|
399
399
|
|
|
400
|
-
export async function
|
|
400
|
+
export async function assertPortFree(port) {
|
|
401
401
|
await new Promise((resolvePromise, rejectPromise) => {
|
|
402
|
-
const socket = connect({ host: "127.0.0.1", port
|
|
402
|
+
const socket = connect({ host: "127.0.0.1", port });
|
|
403
403
|
socket.setTimeout(500);
|
|
404
404
|
socket.once("connect", () => {
|
|
405
405
|
socket.destroy();
|
|
406
|
-
rejectPromise(new Error(
|
|
406
|
+
rejectPromise(new Error(`debug port ${port} is already in use; stop the existing debug server first`));
|
|
407
407
|
});
|
|
408
408
|
socket.once("error", (error) => {
|
|
409
409
|
socket.destroy();
|
|
@@ -420,6 +420,10 @@ export async function assertDebugPortFree() {
|
|
|
420
420
|
});
|
|
421
421
|
}
|
|
422
422
|
|
|
423
|
+
export async function assertDebugPortFree() {
|
|
424
|
+
await assertPortFree(7677);
|
|
425
|
+
}
|
|
426
|
+
|
|
423
427
|
export function assertCurlAvailable() {
|
|
424
428
|
const result = spawnSync("curl", ["--version"], { encoding: "utf8" });
|
|
425
429
|
if (result.status !== 0) {
|
package/scripts/debug/accept.mjs
CHANGED
|
@@ -90,21 +90,24 @@ export function createInteractiveDebugEnvironment({
|
|
|
90
90
|
|
|
91
91
|
export function createDebugEnvironment({
|
|
92
92
|
ownerToken,
|
|
93
|
+
configDir = debugConfigDir,
|
|
93
94
|
stateDir = resolve(debugRoot, "state"),
|
|
94
95
|
worktreeRoot = resolve(debugRoot, "worktrees"),
|
|
95
96
|
hookLog = debugHookLog,
|
|
96
97
|
widgets = process.env.FORGERELAY_DEBUG_WIDGETS ?? "off",
|
|
97
98
|
} = {}) {
|
|
98
99
|
const token = ownerToken ?? process.env.FORGERELAY_DEBUG_OWNER_TOKEN ?? createDebugOwnerToken();
|
|
100
|
+
const resolvedConfigDir = resolve(configDir);
|
|
99
101
|
mkdirSync(debugRoot, { recursive: true });
|
|
100
102
|
|
|
101
103
|
return {
|
|
102
104
|
ownerToken: token,
|
|
105
|
+
configDir: resolvedConfigDir,
|
|
103
106
|
env: {
|
|
104
107
|
...process.env,
|
|
105
108
|
HOST: "127.0.0.1",
|
|
106
109
|
PORT: "7677",
|
|
107
|
-
FORGERELAY_CONFIG_DIR:
|
|
110
|
+
FORGERELAY_CONFIG_DIR: resolvedConfigDir,
|
|
108
111
|
FORGERELAY_PUBLIC_BASE_URL: debugBaseUrl,
|
|
109
112
|
FORGERELAY_ALLOWED_ROOTS: repoRoot,
|
|
110
113
|
FORGERELAY_STATE_DIR: stateDir,
|
|
@@ -5,6 +5,7 @@ import { join } from "node:path";
|
|
|
5
5
|
import test from "node:test";
|
|
6
6
|
|
|
7
7
|
import {
|
|
8
|
+
createDebugEnvironment,
|
|
8
9
|
createInteractiveDebugEnvironment,
|
|
9
10
|
interactiveDebugConfigDir,
|
|
10
11
|
interactiveDebugUrls,
|
|
@@ -64,6 +65,14 @@ test("interactive debug uses one dedicated persisted config under ~/.forgerelay/
|
|
|
64
65
|
assert.equal(result.env.FORGERELAY_LOG_LEVEL, "debug");
|
|
65
66
|
});
|
|
66
67
|
|
|
68
|
+
test("acceptance debug environment may isolate its writable config directory", () => {
|
|
69
|
+
const isolatedConfigDir = join(tmpdir(), "forgerelay-acceptance-config");
|
|
70
|
+
const result = createDebugEnvironment({ configDir: isolatedConfigDir });
|
|
71
|
+
|
|
72
|
+
assert.equal(result.configDir, isolatedConfigDir);
|
|
73
|
+
assert.equal(result.env.FORGERELAY_CONFIG_DIR, isolatedConfigDir);
|
|
74
|
+
});
|
|
75
|
+
|
|
67
76
|
test("interactive debug config directory may be explicitly relocated without falling back to product config", async (t) => {
|
|
68
77
|
const { home } = await withDebugConfig(t);
|
|
69
78
|
const customDir = join(home, "custom-debug-config");
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { cpSync, mkdirSync, mkdtempSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
|
|
6
|
+
export function createParitySandbox(repoRoot) {
|
|
7
|
+
const sandbox = mkdtempSync(join(tmpdir(), "forgerelay-release-parity-node22-"));
|
|
8
|
+
copyTrackedTree(repoRoot, sandbox);
|
|
9
|
+
initializeGitIndex(sandbox);
|
|
10
|
+
return sandbox;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function initializeGitIndex(sandbox) {
|
|
14
|
+
execFileSync("git", ["init", "--quiet"], {
|
|
15
|
+
cwd: sandbox,
|
|
16
|
+
stdio: "ignore",
|
|
17
|
+
});
|
|
18
|
+
execFileSync("git", ["add", "--force", "--all", "--", "."], {
|
|
19
|
+
cwd: sandbox,
|
|
20
|
+
stdio: "ignore",
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function copyTrackedTree(repoRoot, destinationRoot) {
|
|
25
|
+
const files = execFileSync("git", ["ls-files", "-z"], {
|
|
26
|
+
cwd: repoRoot,
|
|
27
|
+
encoding: "utf8",
|
|
28
|
+
}).split("\0").filter(Boolean);
|
|
29
|
+
for (const relativePath of files) {
|
|
30
|
+
const source = join(repoRoot, relativePath);
|
|
31
|
+
const destination = join(destinationRoot, relativePath);
|
|
32
|
+
mkdirSync(dirname(destination), { recursive: true });
|
|
33
|
+
cpSync(source, destination, { recursive: true, force: true });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
|
-
import {
|
|
2
|
+
import { execFileSync, spawnSync } from "node:child_process";
|
|
3
|
+
import { readFile, realpath, rm } from "node:fs/promises";
|
|
3
4
|
import { dirname, resolve } from "node:path";
|
|
4
5
|
import test from "node:test";
|
|
5
6
|
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { createParitySandbox } from "./parity-sandbox.mjs";
|
|
6
8
|
|
|
7
9
|
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
|
|
8
10
|
|
|
@@ -67,6 +69,44 @@ test("architecture gate treats the append-only release-note archive as an explic
|
|
|
67
69
|
assert.match(architecture, /"docs\/releases"/);
|
|
68
70
|
});
|
|
69
71
|
|
|
72
|
+
test("release parity sandbox gives architecture an isolated non-empty Git index", async () => {
|
|
73
|
+
const sandbox = createParitySandbox(repoRoot);
|
|
74
|
+
try {
|
|
75
|
+
const architecture = spawnSync(process.execPath, ["scripts/ci/architecture.mjs"], {
|
|
76
|
+
cwd: sandbox,
|
|
77
|
+
encoding: "utf8",
|
|
78
|
+
windowsHide: true,
|
|
79
|
+
shell: false,
|
|
80
|
+
});
|
|
81
|
+
assert.equal(
|
|
82
|
+
architecture.status,
|
|
83
|
+
0,
|
|
84
|
+
architecture.stderr || architecture.stdout || "architecture check did not exit cleanly",
|
|
85
|
+
);
|
|
86
|
+
assert.doesNotMatch(architecture.stdout, /Architecture check passed: 0 tracked files;/);
|
|
87
|
+
|
|
88
|
+
const gitRoot = execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
89
|
+
cwd: sandbox,
|
|
90
|
+
encoding: "utf8",
|
|
91
|
+
}).trim();
|
|
92
|
+
const [canonicalGitRoot, canonicalSandbox, canonicalRepoRoot] = await Promise.all([
|
|
93
|
+
realpath(gitRoot),
|
|
94
|
+
realpath(sandbox),
|
|
95
|
+
realpath(repoRoot),
|
|
96
|
+
]);
|
|
97
|
+
assert.equal(canonicalGitRoot, canonicalSandbox);
|
|
98
|
+
assert.notEqual(canonicalGitRoot, canonicalRepoRoot);
|
|
99
|
+
|
|
100
|
+
const tracked = execFileSync("git", ["ls-files", "-z"], {
|
|
101
|
+
cwd: sandbox,
|
|
102
|
+
encoding: "utf8",
|
|
103
|
+
}).split("\0").filter(Boolean);
|
|
104
|
+
assert.ok(tracked.length > 0, "parity sandbox must expose copied tracked files through its own Git index");
|
|
105
|
+
} finally {
|
|
106
|
+
await rm(sandbox, { recursive: true, force: true });
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
70
110
|
test("release runtime and local parity share the checked-in Node contract", async () => {
|
|
71
111
|
const nodeVersion = (await readFile(resolve(repoRoot, ".nvmrc"), "utf8")).trim();
|
|
72
112
|
assert.equal(nodeVersion, "22.19.0");
|
|
@@ -77,6 +117,7 @@ test("release runtime and local parity share the checked-in Node contract", asyn
|
|
|
77
117
|
const source = await readFile(resolve(repoRoot, "scripts/release-parity.mjs"), "utf8");
|
|
78
118
|
assert.match(source, /readFileSync\(join\(repoRoot, "\.nvmrc"\), "utf8"\)/);
|
|
79
119
|
assert.match(source, /const NPM_VERSION = "11\.19\.1"/);
|
|
120
|
+
assert.match(source, /const sandbox = createParitySandbox\(repoRoot\)/);
|
|
80
121
|
const ciWorkflow = await readFile(resolve(repoRoot, ".github/workflows/ci.yml"), "utf8");
|
|
81
122
|
assert.match(ciWorkflow, /npm install --global npm@11\.19\.1/);
|
|
82
123
|
assert.ok(source.includes('["npm", "ci", "--no-audit", "--no-fund"]'));
|
|
@@ -1,21 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { execFileSync, spawnSync } from "node:child_process";
|
|
4
|
-
import {
|
|
4
|
+
import { readFileSync, rmSync } from "node:fs";
|
|
5
5
|
import { dirname, join, resolve } from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { createParitySandbox } from "./release/parity-sandbox.mjs";
|
|
7
8
|
|
|
8
9
|
const NPM_VERSION = "11.19.1";
|
|
9
10
|
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
10
11
|
const NODE_VERSION = readFileSync(join(repoRoot, ".nvmrc"), "utf8").trim();
|
|
11
|
-
const debugRoot = join(repoRoot, ".forgerelay-debug");
|
|
12
|
-
const sandbox = mkdtempSync(join(ensureDirectory(debugRoot), "release-parity-node22-"));
|
|
13
12
|
const npx = process.platform === "win32" ? "npx.cmd" : "npx";
|
|
14
13
|
|
|
15
14
|
assertCleanReleaseHead();
|
|
15
|
+
const sandbox = createParitySandbox(repoRoot);
|
|
16
16
|
|
|
17
17
|
try {
|
|
18
|
-
copyTrackedTree(sandbox);
|
|
19
18
|
const env = {
|
|
20
19
|
...process.env,
|
|
21
20
|
FORGERELAY_ALLOWED_ROOTS: sandbox,
|
|
@@ -61,24 +60,6 @@ function assertCleanReleaseHead() {
|
|
|
61
60
|
}
|
|
62
61
|
}
|
|
63
62
|
|
|
64
|
-
function ensureDirectory(path) {
|
|
65
|
-
mkdirSync(path, { recursive: true });
|
|
66
|
-
return `${path}/`;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function copyTrackedTree(destinationRoot) {
|
|
70
|
-
const files = execFileSync("git", ["ls-files", "-z"], {
|
|
71
|
-
cwd: repoRoot,
|
|
72
|
-
encoding: "utf8",
|
|
73
|
-
}).split("\0").filter(Boolean);
|
|
74
|
-
for (const relativePath of files) {
|
|
75
|
-
const source = join(repoRoot, relativePath);
|
|
76
|
-
const destination = join(destinationRoot, relativePath);
|
|
77
|
-
mkdirSync(dirname(destination), { recursive: true });
|
|
78
|
-
cpSync(source, destination, { recursive: true, force: true });
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
63
|
function runNodeNpm(cwd, env, command, label) {
|
|
83
64
|
run(
|
|
84
65
|
cwd,
|
|
@@ -164,6 +164,29 @@ function testEndpoint(name, fallback) {
|
|
|
164
164
|
return fallback;
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
+
function githubAuthHeaders() {
|
|
168
|
+
const token = process.env.GH_TOKEN?.trim()
|
|
169
|
+
|| process.env.GITHUB_TOKEN?.trim()
|
|
170
|
+
|| localGhToken();
|
|
171
|
+
return {
|
|
172
|
+
Accept: "application/vnd.github+json",
|
|
173
|
+
"User-Agent": "ForgeRelay-release-tag-gate",
|
|
174
|
+
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function localGhToken() {
|
|
179
|
+
if (process.env.NODE_ENV === "test") return "";
|
|
180
|
+
try {
|
|
181
|
+
return execFileSync("gh", ["auth", "token", "--hostname", "github.com"], {
|
|
182
|
+
encoding: "utf8",
|
|
183
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
184
|
+
}).trim();
|
|
185
|
+
} catch {
|
|
186
|
+
return "";
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
167
190
|
async function artifactExists(label, url, headers = {}) {
|
|
168
191
|
let response;
|
|
169
192
|
try {
|
|
@@ -195,10 +218,7 @@ async function assertReleaseTagMutable(tag, version) {
|
|
|
195
218
|
).toString();
|
|
196
219
|
const [npmPublished, githubReleased] = await Promise.all([
|
|
197
220
|
artifactExists(`npm package ${pkg.name}@${version}`, npmUrl),
|
|
198
|
-
artifactExists(`GitHub Release ${tag}`, githubUrl,
|
|
199
|
-
Accept: "application/vnd.github+json",
|
|
200
|
-
"User-Agent": "ForgeRelay-release-tag-gate",
|
|
201
|
-
}),
|
|
221
|
+
artifactExists(`GitHub Release ${tag}`, githubUrl, githubAuthHeaders()),
|
|
202
222
|
]);
|
|
203
223
|
if (npmPublished) {
|
|
204
224
|
throw new Error(`npm package ${pkg.name}@${version} already exists; release tag ${tag} is immutable`);
|
|
@@ -22,11 +22,18 @@ async function runProof(cwd, action, env = {}) {
|
|
|
22
22
|
});
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
async function artifactApiEnv(
|
|
25
|
+
async function artifactApiEnv(
|
|
26
|
+
t,
|
|
27
|
+
{ npmStatus = 404, githubStatus = 404, githubRequiredToken } = {},
|
|
28
|
+
) {
|
|
26
29
|
const server = createServer((request, response) => {
|
|
27
30
|
if (request.url?.startsWith("/npm/")) response.statusCode = npmStatus;
|
|
28
|
-
else if (request.url?.startsWith("/github/"))
|
|
29
|
-
|
|
31
|
+
else if (request.url?.startsWith("/github/")) {
|
|
32
|
+
response.statusCode = githubRequiredToken
|
|
33
|
+
&& request.headers.authorization !== `Bearer ${githubRequiredToken}`
|
|
34
|
+
? 403
|
|
35
|
+
: githubStatus;
|
|
36
|
+
} else response.statusCode = 500;
|
|
30
37
|
response.end();
|
|
31
38
|
});
|
|
32
39
|
await new Promise((resolve, reject) => {
|
|
@@ -41,6 +48,7 @@ async function artifactApiEnv(t, { npmStatus = 404, githubStatus = 404 } = {}) {
|
|
|
41
48
|
NODE_ENV: "test",
|
|
42
49
|
FORGERELAY_TEST_NPM_REGISTRY_URL: `${base}/npm/`,
|
|
43
50
|
FORGERELAY_TEST_GITHUB_API_URL: `${base}/github/`,
|
|
51
|
+
...(githubRequiredToken ? { GH_TOKEN: githubRequiredToken } : {}),
|
|
44
52
|
};
|
|
45
53
|
}
|
|
46
54
|
|
|
@@ -121,7 +129,7 @@ test("release tag hook gate uses repository facts instead of requiring a local p
|
|
|
121
129
|
await git(root, ["add", "tracked.txt"]);
|
|
122
130
|
await git(root, ["commit", "--allow-empty", "-m", "new head"]);
|
|
123
131
|
await runProof(root, "write");
|
|
124
|
-
const mutableEnv = await artifactApiEnv(t);
|
|
132
|
+
const mutableEnv = await artifactApiEnv(t, { githubRequiredToken: "release-test-token" });
|
|
125
133
|
const deletion = await runProof(root, "check-hook", {
|
|
126
134
|
...mutableEnv,
|
|
127
135
|
FORGERELAY_HOOK_PAYLOAD: JSON.stringify({ command: "git push --delete origin v1.2.3" }),
|