@moretea-labs/forge 1.7.2 → 1.8.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/AGENTS.md +58 -62
- package/CHANGELOG.md +26 -0
- package/CLAUDE.md +42 -62
- package/README.md +2 -2
- package/SKILL.md +0 -1
- package/THIRD_PARTY_NOTICES.md +5 -2
- package/adapters/browser/index.ts +2 -0
- package/adapters/chatgpt/browser-delivery-host.ts +71 -0
- package/adapters/chatgpt/browser-delivery-runtime.ts +783 -0
- package/adapters/chatgpt/controller-binding-store.ts +55 -0
- package/adapters/chatgpt/controller-host.ts +119 -0
- package/adapters/chatgpt/controller-round-host.ts +78 -0
- package/adapters/chatgpt/controller-round-settlement-store.ts +45 -0
- package/adapters/chatgpt/provider-delivery.ts +113 -0
- package/adapters/chatgpt/work-conversation-binding-store.ts +192 -0
- package/adapters/chatgpt/wsl-bridge-delivery-host.ts +63 -0
- package/adapters/computer/desktop-operator-contract.ts +7 -0
- package/adapters/computer/desktop-operator-discovery.ts +201 -0
- package/adapters/computer/desktop-operator-negotiation.ts +174 -0
- package/adapters/computer/desktop-operator-provider.ts +312 -0
- package/adapters/computer/index.ts +1 -0
- package/adapters/computer/interaction-target-authority.ts +944 -0
- package/adapters/controller-process/binding-store.ts +34 -0
- package/adapters/controller-process/controller-host.ts +26 -0
- package/adapters/mcp/access-mode.ts +132 -0
- package/adapters/mcp/audit.ts +32 -0
- package/adapters/mcp/auth.ts +407 -0
- package/adapters/mcp/controller-round-compatibility.ts +184 -0
- package/adapters/mcp/frozen-client-semantic-compatibility.ts +292 -0
- package/adapters/mcp/instructions.ts +36 -0
- package/adapters/mcp/legacy-context.ts +18 -0
- package/adapters/mcp/multi-repository.ts +437 -0
- package/adapters/mcp/oauth.ts +391 -0
- package/adapters/mcp/paths.ts +140 -0
- package/adapters/mcp/policy.ts +232 -0
- package/adapters/mcp/proxy-env.ts +62 -0
- package/adapters/mcp/readiness-projection.ts +24 -0
- package/adapters/mcp/redaction.ts +21 -0
- package/adapters/mcp/repo.ts +24 -0
- package/adapters/mcp/runtime-gateway/artifact-adapter.ts +52 -0
- package/adapters/mcp/runtime-gateway/context-adapter.ts +829 -0
- package/adapters/mcp/runtime-gateway/controller-authority-adapter.ts +191 -0
- package/adapters/mcp/runtime-gateway/engineering-preconditions.ts +147 -0
- package/adapters/mcp/runtime-gateway/execution-control-adapter.ts +28 -0
- package/adapters/mcp/runtime-gateway/execution-tools.ts +207 -0
- package/adapters/mcp/runtime-gateway/filesystem-adapter.ts +38 -0
- package/adapters/mcp/runtime-gateway/gateway-execution-adapter.ts +411 -0
- package/adapters/mcp/runtime-gateway/legacy-ios-tool-adapter.ts +105 -0
- package/adapters/mcp/runtime-gateway/maintenance-adapter.ts +44 -0
- package/adapters/mcp/runtime-gateway/model-adapter.ts +63 -0
- package/adapters/mcp/runtime-gateway/persisted-check-process.ts +3 -0
- package/adapters/mcp/runtime-gateway/plugin-adapter.ts +308 -0
- package/adapters/mcp/runtime-gateway/process-tools.ts +289 -0
- package/adapters/mcp/runtime-gateway/protected-computer-adapter.ts +76 -0
- package/adapters/mcp/runtime-gateway/recovery-adapter.ts +268 -0
- package/adapters/mcp/runtime-gateway/recovery-client-adapter.ts +70 -0
- package/adapters/mcp/runtime-gateway/recovery-tool-contract.ts +99 -0
- package/adapters/mcp/runtime-gateway/repository-compat-adapter.ts +110 -0
- package/adapters/mcp/runtime-gateway/result-adapter.ts +108 -0
- package/adapters/mcp/runtime-gateway/router.ts +728 -0
- package/adapters/mcp/runtime-gateway/runtime-observation-adapter.ts +1251 -0
- package/adapters/mcp/runtime-gateway/runtime-tool-definitions.ts +634 -0
- package/adapters/mcp/runtime-gateway/runtime-tool-shared.ts +27 -0
- package/adapters/mcp/runtime-gateway/runtime-tools.ts +68 -0
- package/adapters/mcp/runtime-gateway/scheduler-adapter.ts +186 -0
- package/adapters/mcp/runtime-gateway/shared-adapter.ts +25 -0
- package/adapters/mcp/runtime-gateway/status-inbox-adapter.ts +870 -0
- package/adapters/mcp/runtime-gateway/work-adapter.ts +3186 -0
- package/adapters/mcp/runtime-gateway/work-compat-adapter.ts +502 -0
- package/adapters/mcp/runtime-gateway/work-validation-reconciler.ts +1 -0
- package/adapters/mcp/server.ts +1048 -0
- package/adapters/mcp/setup.ts +791 -0
- package/adapters/mcp/tool-capability-cache.ts +87 -0
- package/adapters/mcp/tool-mapping/access-tools.ts +387 -0
- package/adapters/mcp/tool-mapping/legacy-tool-service.ts +5265 -0
- package/adapters/mcp/tool-mapping/post-finalize-work-attribution.ts +150 -0
- package/adapters/mcp/tool-mapping/repository-tools.ts +1691 -0
- package/adapters/mcp/tool-mapping/tools.ts +9 -0
- package/adapters/mcp/tool-mapping/umbrella-tools.ts +117 -0
- package/adapters/mcp/toolset-names.ts +74 -0
- package/adapters/mcp/toolset-selection.ts +41 -0
- package/adapters/mcp/toolset.ts +336 -0
- package/adapters/mcp/transports/http.ts +1456 -0
- package/adapters/mcp/transports/session-registry.ts +513 -0
- package/adapters/mcp/transports/stdio.ts +8 -0
- package/adapters/mcp/tunnels/openai-secure-tunnel.ts +139 -0
- package/adapters/mcp/types.ts +41 -0
- package/assets/AGENTS.md +2 -16
- package/assets/CLAUDE.md +2 -16
- package/assets/hooks/AGENTS.md +3 -14
- package/assets/hooks/CLAUDE.md +3 -14
- package/assets/hooks/hook-input.sh +9 -2
- package/assets/hooks/lib/workflow-state.sh +28 -10
- package/assets/hooks/prompt-guard.sh +63 -37
- package/assets/hooks/security-sentinel.sh +2 -1
- package/assets/hooks/session-start-context.sh +7 -5
- package/assets/hooks/stop-orchestrator.sh +13 -3
- package/assets/initializer-question-pack.v4.json +11 -7
- package/assets/partials/05-workflow.partial.md +18 -73
- package/assets/partials/07-footer.partial.md +1 -1
- package/assets/partials-agents/02-operating-mode.partial.md +1 -2
- package/assets/partials-agents/04-task-protocol.partial.md +14 -59
- package/assets/partials-agents/06-quality-safety.partial.md +11 -16
- package/assets/partials-agents/08-deep-docs.partial.md +1 -2
- package/assets/plan-map.json +11 -11
- package/assets/plugin-registry.v1.json +9 -3
- package/assets/plugin-trust-policy.v1.json +6 -0
- package/assets/recovery/ForgeRecovery.ps1 +100 -0
- package/assets/recovery/forge-wsl-rescue +271 -0
- package/assets/reference-configs/development-protocol.md +11 -15
- package/assets/reference-configs/document-generation.md +1 -1
- package/assets/reference-configs/global-working-rules.md +9 -1
- package/assets/reference-configs/harness-overview.md +22 -112
- package/assets/reference-configs/hook-operations.md +1 -1
- package/assets/reference-configs/sprint-contracts.md +2 -2
- package/assets/skill-version.json +2 -2
- package/assets/templates/helpers/architecture-event.ts +3 -34
- package/assets/templates/helpers/architecture-queue.sh +61 -10
- package/assets/templates/helpers/archive-workflow.sh +90 -180
- package/assets/templates/helpers/capability-config.ts +8 -11
- package/assets/templates/helpers/capability-resolver.ts +37 -19
- package/assets/templates/helpers/capture-plan.sh +2 -2
- package/assets/templates/helpers/check-architecture-sync.sh +1 -9
- package/assets/templates/helpers/check-task-sync.sh +82 -11
- package/assets/templates/helpers/check-task-workflow.sh +1 -3
- package/assets/templates/helpers/context-contract-sync.sh +1 -12
- package/assets/templates/helpers/contract-worktree.sh +26 -24
- package/assets/templates/helpers/ensure-task-workflow.sh +6 -8
- package/assets/templates/helpers/inspect-project-state.ts +1 -1
- package/assets/templates/helpers/migrate-project-template.sh +1 -1
- package/assets/templates/helpers/migrate-workflow-docs.ts +53 -239
- package/assets/templates/helpers/new-plan.sh +2 -2
- package/assets/templates/helpers/plan-to-todo.sh +3 -31
- package/assets/templates/helpers/sprint-backlog.sh +1 -1
- package/assets/templates/helpers/switch-plan.sh +1 -1
- package/assets/templates/helpers/workflow-contract.ts +1 -0
- package/assets/workflow-contract.v1.json +63 -171
- package/assets/workflows/xiaohongshu/xiaohongshu.collect-metrics.v1.draft.json +118 -0
- package/assets/workflows/xiaohongshu/xiaohongshu.publish-image-note.v1.draft.json +316 -0
- package/assets/workflows/xiaohongshu/xiaohongshu.publish-long-text.v1.draft.json +324 -0
- package/docs/architecture/CURRENT.md +148 -13
- package/docs/architecture/EVOLUTION.md +13 -0
- package/docs/architecture/decisions/20260801-controller-home-sqlite-state.md +2 -1
- package/docs/architecture/decisions/20260831-independent-windows-wsl-host-rescue.md +59 -0
- package/docs/architecture/decisions/20260904-ios-device-migration-baseline.md +120 -0
- package/docs/architecture/decisions/20260904-repository-runtime-data-boundary.md +42 -0
- package/docs/architecture/decisions/20260907-assistant-learning-loop.md +35 -0
- package/docs/architecture/decisions/20260907-controller-round-transition-policy.md +296 -0
- package/docs/architecture/decisions/20260907-maintenance-diagnosis-bounded-read.md +39 -0
- package/docs/architecture/domains/verification.md +7 -0
- package/docs/architecture/domains/workflow-engine.md +1 -1
- package/docs/architecture/index.md +3 -3
- package/docs/architecture/modules/controller-runtime/transport-lifecycle.md +19 -8
- package/docs/architecture/requests/2026-09-04-computer-plugin-workflow-taxonomy.md +124 -0
- package/docs/forge-chatgpt-mcp-setup.md +5 -3
- package/docs/forge-plugin-management.md +20 -4
- package/docs/forge-runtime-self-healing-loop.md +2 -0
- package/docs/operations/benchmark-cross-version-freeze.md +38 -0
- package/docs/operations/controller-browser-plugin.md +3 -1
- package/docs/operations/homebrew.md +2 -2
- package/docs/operations/platform-support.md +16 -8
- package/docs/operations/platform-support.zh-CN.md +2 -2
- package/docs/operations/standalone-disaster-recovery.md +26 -1
- package/docs/operations/uu-remote-rescue.md +41 -0
- package/docs/operations/windows-wsl-host-rescue.md +133 -0
- package/docs/public-usage-guide.md +1 -1
- package/docs/public-usage-guide.zh-CN.md +1 -1
- package/docs/tutorials/01-install-and-start.md +1 -1
- package/docs/tutorials/01-install-and-start.zh-CN.md +1 -1
- package/docs/tutorials/02-connect-chatgpt.md +4 -4
- package/docs/tutorials/02-connect-chatgpt.zh-CN.md +4 -4
- package/docs/wiki/Implementation.md +1 -1
- package/package.json +23 -14
- package/packages/kernel/controller/api/index.ts +2 -0
- package/packages/kernel/controller/application/controller-round-service.ts +29 -0
- package/packages/kernel/controller/application/controller-service.ts +30 -0
- package/packages/kernel/controller/domain/controller-round-transition-policy.ts +308 -0
- package/packages/kernel/controller/domain/controller-round.ts +107 -0
- package/packages/kernel/controller/domain/execution-quality.ts +133 -0
- package/packages/kernel/controller/domain/types.ts +56 -0
- package/packages/kernel/controller/index.ts +1 -0
- package/packages/kernel/controller/infrastructure/controller-binding-store.ts +84 -0
- package/packages/kernel/controller/infrastructure/controller-round-store.ts +1384 -0
- package/packages/kernel/controller/infrastructure/controller-session-store.ts +578 -0
- package/packages/kernel/controller/ports/controller-host.ts +16 -0
- package/packages/kernel/goal/api/index.ts +1 -0
- package/packages/kernel/goal/domain/types.ts +40 -0
- package/packages/kernel/goal/index.ts +1 -0
- package/packages/kernel/identity/api/index.ts +5 -0
- package/packages/kernel/identity/application/identity-service.ts +67 -0
- package/packages/kernel/identity/domain/scope.ts +63 -0
- package/packages/kernel/identity/domain/semantic-sync.ts +81 -0
- package/packages/kernel/identity/domain/types.ts +44 -0
- package/packages/kernel/identity/domain/workspace.ts +56 -0
- package/packages/kernel/identity/index.ts +1 -0
- package/packages/kernel/identity/infrastructure/identity-store.ts +75 -0
- package/packages/kernel/memory/api/index.ts +5 -0
- package/packages/kernel/memory/application/experience-service.ts +118 -0
- package/packages/kernel/memory/application/outcome-service.ts +77 -0
- package/packages/kernel/memory/domain/experience.ts +103 -0
- package/packages/kernel/memory/domain/operational-prior.ts +92 -0
- package/packages/kernel/memory/index.ts +1 -0
- package/packages/kernel/progression/api/index.ts +2 -0
- package/packages/kernel/progression/application/projector.ts +211 -0
- package/packages/kernel/progression/domain/types.ts +116 -0
- package/packages/kernel/progression/index.ts +1 -0
- package/packages/kernel/scheduler/api/index.ts +6 -0
- package/packages/kernel/scheduler/application/continuation-service.ts +212 -0
- package/packages/kernel/scheduler/application/eligibility.ts +252 -0
- package/packages/kernel/scheduler/application/schedule-service.ts +24 -0
- package/packages/kernel/scheduler/application/settlement.ts +197 -0
- package/packages/kernel/scheduler/domain/continuation.ts +38 -0
- package/packages/kernel/scheduler/domain/schedule.ts +149 -0
- package/packages/kernel/scheduler/index.ts +1 -0
- package/packages/kernel/scheduler/infrastructure/continuation-dispatch-store.ts +38 -0
- package/packages/kernel/scheduler/infrastructure/schedule-store.ts +785 -0
- package/packages/kernel/work/api/index.ts +15 -0
- package/packages/kernel/work/application/work-service.ts +148 -0
- package/packages/kernel/work/domain/admission-policy.ts +34 -0
- package/packages/kernel/work/domain/check-receipt.ts +43 -0
- package/packages/kernel/work/domain/context-closure-receipt.ts +32 -0
- package/packages/kernel/work/domain/engineering-contracts.ts +157 -0
- package/packages/kernel/work/domain/engineering-design.ts +281 -0
- package/packages/kernel/work/domain/engineering-profile.ts +242 -0
- package/packages/kernel/work/domain/execution-concurrency.ts +254 -0
- package/packages/kernel/work/domain/execution-snapshot.ts +33 -0
- package/packages/kernel/work/domain/implementation-review.ts +510 -0
- package/packages/kernel/work/domain/project-engineering-contract.ts +202 -0
- package/packages/kernel/work/domain/repository-completion-receipt.ts +73 -0
- package/packages/kernel/work/domain/state-machine.ts +259 -0
- package/packages/kernel/work/domain/types.ts +528 -0
- package/packages/kernel/work/index.ts +1 -0
- package/packages/kernel/work/infrastructure/work-contract-store.ts +1876 -0
- package/packages/kernel/work/ports/work-contract-store.ts +18 -0
- package/packages/plugin-runtime/browser/index.ts +3 -0
- package/packages/plugin-runtime/browser/provider-registry.ts +110 -0
- package/packages/plugin-runtime/browser/runtime-contract.ts +150 -0
- package/packages/plugin-runtime/browser/session-authority.ts +61 -0
- package/packages/plugin-runtime/computer/index.ts +6 -0
- package/packages/plugin-runtime/computer/provider-error.ts +26 -0
- package/packages/plugin-runtime/computer/provider-registration.ts +52 -0
- package/packages/plugin-runtime/computer/provider-registry.ts +63 -0
- package/packages/plugin-runtime/computer/provider.ts +19 -0
- package/packages/plugin-runtime/computer/target-authority.ts +190 -0
- package/packages/plugin-runtime/computer/target-persistence.ts +54 -0
- package/packages/plugin-runtime/external/index.ts +1 -0
- package/packages/plugin-runtime/external/unix-jsonl-transport.ts +457 -0
- package/packages/plugin-runtime/index.ts +3 -0
- package/packages/protocols/browser/index.ts +1 -0
- package/packages/protocols/browser/session.ts +98 -0
- package/packages/protocols/computer/contract.ts +199 -0
- package/packages/protocols/computer/index.ts +1 -0
- package/packages/protocols/failure/contract.ts +158 -0
- package/packages/protocols/failure/index.ts +1 -0
- package/packages/protocols/handoff/index.ts +1 -0
- package/packages/protocols/handoff/status.ts +22 -0
- package/packages/protocols/mcp/execution-context.ts +11 -0
- package/packages/protocols/mcp/index.ts +2 -0
- package/packages/protocols/mcp/tool-contract.ts +42 -0
- package/packages/workflow-runtime/api/index.ts +2 -0
- package/packages/workflow-runtime/application/interpreter.ts +342 -0
- package/packages/workflow-runtime/domain/workflow-asset.ts +223 -0
- package/packages/workflow-runtime/index.ts +1 -0
- package/scripts/assemble-template.ts +1 -1
- package/scripts/benchmark-computer-hotpath.ts +255 -0
- package/scripts/benchmark-controller-round2.ts +54 -2
- package/scripts/benchmark-ios-agent-device.ts +3 -6
- package/scripts/benchmark-mcp-transport.ts +3 -4
- package/scripts/benchmark-operational-memory-activation.ts +235 -0
- package/scripts/benchmark-operational-shadow.ts +28 -0
- package/scripts/bootstrap-runtime-maintenance-recovery.sh +4 -1
- package/scripts/canary-chatgpt-provider-delivery.ts +97 -0
- package/scripts/canary-controller-semantic-wait.ts +195 -0
- package/scripts/canary-global-hook.sh +2 -2
- package/scripts/check-mcp-compatibility.ts +173 -7
- package/scripts/check-platform-support.mjs +28 -2
- package/scripts/check-public-docs.sh +1 -1
- package/scripts/check-release-readiness.sh +1 -1
- package/scripts/check-repository-hygiene.mjs +92 -0
- package/scripts/check-runtime-architecture.mjs +1206 -95
- package/scripts/check-source-duplication.mjs +9 -1
- package/scripts/check-tarball-install-smoke.sh +98 -15
- package/scripts/controller-ngrok-rotation.sh +5 -4
- package/scripts/create-project-dirs.sh +10 -74
- package/scripts/forge-local-recovery-helper.mjs +204 -0
- package/scripts/forge-uu-remote-rescue-helper.d.mts +31 -0
- package/scripts/forge-uu-remote-rescue-helper.mjs +409 -0
- package/scripts/forge-windows-host-recovery-helper.mjs +136 -0
- package/scripts/hook-shim.sh +2 -2
- package/scripts/init-project.sh +12 -70
- package/scripts/install-desktop-operator-registration.ts +4 -2
- package/scripts/install-independent-windows-wsl-recovery.ts +125 -0
- package/scripts/install-standalone-recovery.ts +122 -10
- package/scripts/install-uu-remote-rescue-registration.ts +74 -0
- package/scripts/install-windows-host-recovery-registration.ts +22 -0
- package/scripts/lib/controller-home-env.sh +6 -3
- package/scripts/lib/project-init-lib.sh +39 -582
- package/scripts/migrate-chatgpt-automation-schedules.ts +2 -2
- package/scripts/migrate-windows-wsl-controller-home.ts +306 -0
- package/scripts/open-source-audit-allowlist.txt +12 -1
- package/scripts/portability-bindings.mjs +70 -0
- package/scripts/run-bounded-command.mjs +55 -0
- package/scripts/run-bun-test-file.ts +22 -6
- package/scripts/run-governed-gate.ts +4 -1
- package/scripts/smoke-mcp-http-runtime.ts +1 -1
- package/scripts/smoke-runtime-recovery.ts +34 -1
- package/scripts/smoke-schedule-engine.ts +1 -1
- package/scripts/stage-runtime-release.ts +6 -1
- package/scripts/test-governance.ts +51 -2
- package/src/cli/agent-jobs/job-manager.ts +10 -1
- package/src/cli/chatgpt-browser/bind-server.ts +2 -1
- package/src/cli/chatgpt-browser/binding.ts +20 -10
- package/src/cli/chatgpt-browser/bridge-extension.ts +75 -0
- package/src/cli/chatgpt-browser/bridge-provider.ts +376 -11
- package/src/cli/chatgpt-browser/engine.ts +10 -11
- package/src/cli/chatgpt-browser/native-provider.ts +24 -83
- package/src/cli/chatgpt-browser/oracle-provider.ts +5 -3
- package/src/cli/chatgpt-browser/types.ts +1 -0
- package/src/cli/commands/bootstrap-control.ts +334 -0
- package/src/cli/commands/brain-assistant.ts +38 -0
- package/src/cli/commands/brain.ts +193 -40
- package/src/cli/commands/chatgpt.ts +127 -0
- package/src/cli/commands/computer.ts +437 -0
- package/src/cli/commands/doctor.ts +18 -1
- package/src/cli/commands/init-hook.ts +71 -61
- package/src/cli/commands/init.ts +3 -1
- package/src/cli/commands/mcp.ts +16 -10
- package/src/cli/commands/plugin.ts +38 -12
- package/src/cli/commands/recovery.ts +493 -80
- package/src/cli/commands/runtime.ts +9 -4
- package/src/cli/commands/setup-profile.ts +89 -39
- package/src/cli/commands/status.ts +2 -1
- package/src/cli/controller/check-runner.ts +487 -98
- package/src/cli/controller/context/types.ts +7 -1
- package/src/cli/controller/context-pack.ts +23 -3
- package/src/cli/controller/issue-store.ts +1 -1
- package/src/cli/controller/systemd-user.ts +110 -0
- package/src/cli/controller/types.ts +10 -70
- package/src/cli/controller/worklog.ts +67 -13
- package/src/cli/editing/edit-operation-contract.ts +92 -0
- package/src/cli/editing/edit-session.ts +289 -13
- package/src/cli/github/plugin.ts +17 -12
- package/src/cli/hook/route-registry.ts +3 -5
- package/src/cli/hook/runtime.ts +49 -3
- package/src/cli/index.ts +3 -0
- package/src/cli/local-bridge/connector-freshness.ts +2 -2
- package/src/cli/local-bridge/console-automations.ts +2 -2
- package/src/cli/local-bridge/facade-api.ts +41 -14
- package/src/cli/local-bridge/job-store.ts +1 -1
- package/src/cli/local-bridge/server.ts +24 -27
- package/src/cli/mcp/access-mode.ts +2 -132
- package/src/cli/mcp/access-tools.ts +2 -387
- package/src/cli/mcp/audit.ts +2 -32
- package/src/cli/mcp/auth.ts +2 -404
- package/src/cli/mcp/instructions.ts +2 -34
- package/src/cli/mcp/legacy-context.ts +2 -16
- package/src/cli/mcp/legacy-tool-service.ts +2 -5308
- package/src/cli/mcp/multi-repository.ts +2 -436
- package/src/cli/mcp/oauth.ts +2 -375
- package/src/cli/mcp/openai-secure-tunnel.ts +2 -0
- package/src/cli/mcp/paths.ts +2 -140
- package/src/cli/mcp/policy.ts +2 -232
- package/src/cli/mcp/post-finalize-work-attribution.ts +2 -150
- package/src/cli/mcp/proxy-env.ts +2 -62
- package/src/cli/mcp/readiness-projection.ts +2 -24
- package/src/cli/mcp/redaction.ts +2 -21
- package/src/cli/mcp/repo.ts +2 -24
- package/src/cli/mcp/repository-tools.ts +2 -1613
- package/src/cli/mcp/server.ts +2 -951
- package/src/cli/mcp/setup.ts +2 -737
- package/src/cli/mcp/tool-capability-cache.ts +2 -87
- package/src/cli/mcp/tool-contract.ts +2 -14
- package/src/cli/mcp/tools.ts +2 -9
- package/src/cli/mcp/toolset-names.ts +2 -70
- package/src/cli/mcp/toolset-selection.ts +2 -41
- package/src/cli/mcp/toolset.ts +2 -336
- package/src/cli/mcp/transports/AGENTS.md +1 -10
- package/src/cli/mcp/transports/CLAUDE.md +1 -10
- package/src/cli/mcp/transports/http.ts +2 -1414
- package/src/cli/mcp/transports/session-registry.ts +2 -490
- package/src/cli/mcp/transports/stdio.ts +2 -8
- package/src/cli/mcp/types.ts +2 -41
- package/src/cli/mcp/umbrella-tools.ts +2 -117
- package/src/cli/repositories/command-executor.ts +64 -18
- package/src/cli/repositories/command-scope.ts +117 -1
- package/src/cli/repositories/controller-home.ts +119 -1
- package/src/cli/repositories/identity.ts +13 -0
- package/src/cli/repositories/registry.ts +68 -49
- package/src/cli/repositories/repository-snapshot.ts +53 -7
- package/src/cli/repositories/runtime-storage.ts +27 -2
- package/src/cli/repositories/safe-patch.ts +34 -38
- package/src/cli/repositories/selected-path-actions.ts +22 -2
- package/src/cli/repositories/structured-git.ts +105 -25
- package/src/cli/runtime/helper-runner.ts +7 -0
- package/src/cli/runtime-invocation.ts +8 -1
- package/src/cli/tools/codegraph.ts +44 -25
- package/src/core/adoption/manifest-templates.ts +1 -1
- package/src/core/adoption/plan.ts +3 -26
- package/src/core/adoption/workflow-contract-plan.ts +17 -9
- package/src/effects/process-runner.ts +8 -14
- package/src/runtime/context/assistant-context.ts +154 -0
- package/src/runtime/context/assistant-work-context.ts +147 -0
- package/src/runtime/context/codegraph-cache-boundary.ts +255 -0
- package/src/runtime/context/codegraph-read-provider.ts +55 -22
- package/src/runtime/context/codegraph-sidecar.cjs +8 -1
- package/src/runtime/context/context-closure.ts +268 -0
- package/src/runtime/context/context-plane.ts +211 -0
- package/src/runtime/context/project-engineering-contract.ts +60 -0
- package/src/runtime/control-plane/bootstrap/capability-provider.ts +74 -0
- package/src/runtime/control-plane/bootstrap/control-api.ts +57 -0
- package/src/runtime/control-plane/bootstrap/index.ts +5 -0
- package/src/runtime/control-plane/bootstrap/state-machine.ts +141 -0
- package/src/runtime/control-plane/bootstrap/store.ts +21 -0
- package/src/runtime/control-plane/bootstrap/types.ts +87 -0
- package/src/runtime/control-plane/cleanup-artifact-retention.ts +328 -0
- package/src/runtime/control-plane/codegraph-cache-retention.ts +164 -0
- package/src/runtime/control-plane/concurrency/work-execution-concurrency.ts +320 -0
- package/src/runtime/control-plane/execution/content-equivalent-commit-authority.ts +169 -0
- package/src/runtime/control-plane/execution/controller-authority-recovery.ts +350 -0
- package/src/runtime/control-plane/execution/direct-canonical-work-reconciliation.ts +85 -0
- package/src/runtime/control-plane/execution/direct-edit-work-completion.ts +369 -99
- package/src/runtime/control-plane/execution/edit-validation-coordinator.ts +103 -15
- package/src/runtime/control-plane/execution/implementation-review-content.ts +95 -0
- package/src/runtime/control-plane/execution/repository-work-attribution.ts +2 -2
- package/src/runtime/control-plane/execution/retained-work-resume.ts +336 -6
- package/src/runtime/control-plane/execution/validation.ts +28 -3
- package/src/runtime/control-plane/execution/work-authority-reconciler.ts +116 -0
- package/src/runtime/control-plane/execution/work-completion-authority.ts +44 -2
- package/src/runtime/control-plane/execution/work-evidence-policy.ts +34 -3
- package/src/runtime/control-plane/execution/work-execution-support.ts +42 -22
- package/src/runtime/control-plane/execution/work-finalization-service.ts +1103 -166
- package/src/runtime/control-plane/execution/work-handle-authority.ts +219 -22
- package/src/runtime/control-plane/execution/work-handle-store.ts +77 -0
- package/src/runtime/control-plane/execution/work-head-settlement.ts +75 -0
- package/src/runtime/control-plane/execution/work-lifecycle-audit.ts +145 -23
- package/src/runtime/control-plane/execution/work-operation-service.ts +49 -13
- package/src/runtime/control-plane/execution/work-preparation-service.ts +31 -15
- package/src/runtime/control-plane/execution/work-remote-delivery.ts +1 -1
- package/src/runtime/control-plane/execution/work-task-receipt.ts +20 -1
- package/src/runtime/control-plane/execution/work-terminal-cleanup.ts +407 -76
- package/src/runtime/control-plane/execution/work-validation-reconciler.ts +88 -18
- package/src/runtime/control-plane/execution/work-verification-context.ts +1 -1
- package/src/runtime/control-plane/execution/work-verification-service.ts +258 -4
- package/src/runtime/control-plane/execution/work-verification-snapshot.ts +200 -15
- package/src/runtime/control-plane/facade/capability-registry.ts +21 -10
- package/src/runtime/control-plane/facade/controller-round-relay.ts +3 -1118
- package/src/runtime/control-plane/facade/controller-session-store.ts +2 -488
- package/src/runtime/control-plane/facade/goal-workloop-access.ts +6 -3
- package/src/runtime/control-plane/facade/goal-workloop.ts +972 -137
- package/src/runtime/control-plane/facade/handoff-inbox-application.ts +158 -0
- package/src/runtime/control-plane/facade/handoff-inbox-store.ts +10 -0
- package/src/runtime/control-plane/facade/index.ts +4 -2
- package/src/runtime/control-plane/facade/operation-digest.ts +22 -58
- package/src/runtime/control-plane/facade/plan-contract-store.ts +1004 -38
- package/src/runtime/control-plane/facade/repository-work-admission.ts +19 -1
- package/src/runtime/control-plane/facade/requirement-authority.ts +218 -0
- package/src/runtime/control-plane/facade/requirement-board.ts +6 -1
- package/src/runtime/control-plane/facade/rh-work-operation-contract.ts +48 -0
- package/src/runtime/control-plane/facade/semantic-admission.ts +8 -16
- package/src/runtime/control-plane/facade/suggested-actions.ts +4 -1
- package/src/runtime/control-plane/facade/types.ts +189 -532
- package/src/runtime/control-plane/facade/work-admission-policy.ts +84 -35
- package/src/runtime/control-plane/facade/work-contract-store.ts +2 -1043
- package/src/runtime/control-plane/facade/work-implementation-review.ts +2 -0
- package/src/runtime/control-plane/facade/work-state-machine.ts +2 -250
- package/src/runtime/control-plane/generated-cache-retention.ts +239 -18
- package/src/runtime/control-plane/global-scheduler/durable-admission.ts +12 -1
- package/src/runtime/control-plane/global-scheduler/maintenance.ts +76 -13
- package/src/runtime/control-plane/global-scheduler/periodic-cleanup-entry.ts +55 -0
- package/src/runtime/control-plane/global-scheduler/periodic-cleanup-process.ts +98 -0
- package/src/runtime/control-plane/global-scheduler/reconciliation.ts +1 -1
- package/src/runtime/control-plane/global-scheduler/scheduler-worker-entry.ts +10 -0
- package/src/runtime/control-plane/global-scheduler/scheduler.ts +173 -81
- package/src/runtime/control-plane/global-scheduler/worker-launch.ts +18 -6
- package/src/runtime/control-plane/launcher/chatgpt-round-continuation.ts +397 -0
- package/src/runtime/control-plane/launcher/chatgpt-work-binding-store.ts +2 -177
- package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +301 -560
- package/src/runtime/control-plane/launcher/provider-mcp-bootstrap.ts +20 -4
- package/src/runtime/control-plane/launcher/thin-launcher.ts +50 -4
- package/src/runtime/control-plane/lifecycle-retention-metrics.ts +60 -0
- package/src/runtime/control-plane/persistence/experience-store.ts +169 -0
- package/src/runtime/control-plane/persistence/operational-prior-store.ts +348 -0
- package/src/runtime/control-plane/persistence/requirement-store.ts +30 -22
- package/src/runtime/control-plane/persistence/sqlite-store.ts +545 -76
- package/src/runtime/control-plane/persistence/workflow-content-store.ts +98 -0
- package/src/runtime/control-plane/persistence/workflow-registry-store.ts +186 -0
- package/src/runtime/control-plane/persistence/workflow-run-store.ts +123 -0
- package/src/runtime/control-plane/release-retention.ts +56 -16
- package/src/runtime/control-plane/repository-namespace-retention.ts +208 -0
- package/src/runtime/control-plane/routing/route-policy.ts +19 -0
- package/src/runtime/control-plane/runtime-cleanup.ts +574 -37
- package/src/runtime/control-plane/runtime-generation.ts +6 -0
- package/src/runtime/control-plane/workspace/semantic-sync-service.ts +465 -0
- package/src/runtime/control-plane/workspace/workspace-store.ts +80 -0
- package/src/runtime/diagnostics/incident-repair.ts +406 -0
- package/src/runtime/diagnostics/mcp-timing.ts +125 -6
- package/src/runtime/diagnostics/performance.ts +6 -14
- package/src/runtime/diagnostics/read-only-tool.ts +1 -1
- package/src/runtime/evidence/artifact-store.ts +65 -2
- package/src/runtime/evidence/operational-shadow.ts +38 -0
- package/src/runtime/evidence/process-check-receipt.ts +5 -43
- package/src/runtime/evidence/sensitive-output.ts +4 -4
- package/src/runtime/execution/jobs/receipt-recovery.ts +1 -1
- package/src/runtime/execution/jobs/store.ts +121 -2
- package/src/runtime/execution/managed-workspace.ts +55 -5
- package/src/runtime/execution/process-runtime/canary.ts +19 -5
- package/src/runtime/execution/process-runtime/check-classification.ts +48 -0
- package/src/runtime/execution/process-runtime/check-facade.ts +23 -9
- package/src/runtime/execution/process-runtime/check-receipt.ts +1 -1
- package/src/runtime/execution/process-runtime/check-result-retention.ts +132 -0
- package/src/runtime/execution/process-runtime/check-result.ts +73 -2
- package/src/runtime/execution/process-runtime/check-runner-sidecar.ts +42 -3
- package/src/runtime/execution/process-runtime/check-storage.ts +187 -0
- package/src/runtime/execution/process-runtime/command-facade.ts +38 -87
- package/src/runtime/execution/process-runtime/index.ts +1 -0
- package/src/runtime/execution/process-runtime/interactive-admission.ts +2 -1
- package/src/runtime/execution/process-runtime/lightweight-managed.ts +32 -1
- package/src/runtime/execution/process-runtime/persisted-check.ts +128 -21
- package/src/runtime/execution/process-runtime/resource-claims.ts +60 -3
- package/src/runtime/execution/process-runtime/runtime.ts +132 -15
- package/src/runtime/execution/process-runtime/types.ts +22 -0
- package/src/runtime/execution/workers/executor.ts +16 -13
- package/src/runtime/execution/workers/worker-entry.ts +1 -1
- package/src/runtime/gateway/mcp/execution-tools.ts +2 -207
- package/src/runtime/gateway/mcp/legacy-ios-tool-adapter.ts +2 -61
- package/src/runtime/gateway/mcp/persisted-check-process.ts +2 -3
- package/src/runtime/gateway/mcp/process-tools.ts +2 -289
- package/src/runtime/gateway/mcp/router.ts +2 -1125
- package/src/runtime/gateway/mcp/runtime-tool-definitions.ts +2 -565
- package/src/runtime/gateway/mcp/runtime-tools.ts +2 -6655
- package/src/runtime/gateway/mcp/work-validation-reconciler.ts +2 -1
- package/src/runtime/maintenance/cleanup.ts +38 -24
- package/src/runtime/platform/browser-product-discovery.ts +174 -0
- package/src/runtime/platform/computer-platform.ts +14 -0
- package/src/runtime/platform/executable-discovery.ts +54 -0
- package/src/runtime/platform/fetch-http-server.ts +90 -0
- package/src/runtime/platform/service-manager.ts +138 -0
- package/src/runtime/platform/windows-host-discovery.ts +118 -0
- package/src/runtime/platform/windows-recovery-deployment.ts +67 -0
- package/src/runtime/plugins/action-application.ts +135 -0
- package/src/runtime/plugins/app-store-connect-adapter.ts +9 -21
- package/src/runtime/plugins/browser-adapter.ts +140 -100
- package/src/runtime/plugins/browser-automation-service.ts +15 -36
- package/src/runtime/plugins/browser-macos-bridge.ts +69 -21
- package/src/runtime/plugins/browser-manifest-surface.ts +30 -6
- package/src/runtime/plugins/browser-native-session-service.ts +1 -1
- package/src/runtime/plugins/browser-provider-registry.ts +2 -109
- package/src/runtime/plugins/browser-registration.ts +16 -0
- package/src/runtime/plugins/browser-runtime-contract.ts +2 -154
- package/src/runtime/plugins/browser-runtime.ts +41 -9
- package/src/runtime/plugins/browser-session-authority.ts +101 -291
- package/src/runtime/plugins/browser-session-legacy-migration.ts +203 -0
- package/src/runtime/plugins/browser-session-store.ts +229 -36
- package/src/runtime/plugins/browser-session-types.ts +2 -98
- package/src/runtime/plugins/capability-authorization-grants.ts +17 -0
- package/src/runtime/plugins/catalog-trust.ts +82 -0
- package/src/runtime/plugins/computer-registration.ts +839 -0
- package/src/runtime/plugins/config-store.ts +119 -0
- package/src/runtime/plugins/desktop-operator-external-policy.ts +932 -0
- package/src/runtime/plugins/desktop-operator-registration.ts +111 -8
- package/src/runtime/plugins/errors.ts +8 -1
- package/src/runtime/plugins/external-adapter.ts +45 -322
- package/src/runtime/plugins/external-provider-policy.ts +48 -0
- package/src/runtime/plugins/external-registration.ts +9 -1
- package/src/runtime/plugins/external-unix-socket-probe.cjs +9 -3
- package/src/runtime/plugins/external-unix-socket.ts +64 -193
- package/src/runtime/plugins/first-party-external-registration.ts +163 -0
- package/src/runtime/plugins/first-party-registry.ts +3 -1
- package/src/runtime/plugins/github-adapter.ts +1 -2
- package/src/runtime/plugins/gmail-adapter.ts +6 -6
- package/src/runtime/plugins/google-calendar-adapter.ts +6 -6
- package/src/runtime/plugins/google-shared.ts +31 -46
- package/src/runtime/plugins/google-tasks-adapter.ts +6 -6
- package/src/runtime/plugins/ios-adapter.ts +1 -2
- package/src/runtime/plugins/local-system-adapter.ts +180 -0
- package/src/runtime/plugins/macos-capability-broker.ts +15 -105
- package/src/runtime/plugins/resend-adapter.ts +15 -24
- package/src/runtime/plugins/store.ts +493 -51
- package/src/runtime/plugins/types.ts +8 -1
- package/src/runtime/plugins/uu-remote-rescue-registration.ts +155 -0
- package/src/runtime/plugins/windows-host-recovery-install.ts +68 -0
- package/src/runtime/plugins/windows-host-recovery-registration.ts +85 -0
- package/src/runtime/plugins/xiaohongshu-publish.ts +86 -430
- package/src/runtime/projections/controller-context.ts +12 -6
- package/src/runtime/projections/git-status-sampler.ts +73 -22
- package/src/runtime/projections/materialized-view.ts +14 -8
- package/src/runtime/recovery/capability-action-application.ts +228 -0
- package/src/runtime/recovery/index.ts +3 -0
- package/src/runtime/recovery/lifecycle-inventory.ts +338 -0
- package/src/runtime/recovery/local-jobs-repair.ts +14 -14
- package/src/runtime/recovery/maintenance-executor.ts +358 -76
- package/src/runtime/recovery/quarantine-retention.ts +256 -0
- package/src/runtime/release/identity.ts +6 -0
- package/src/runtime/resources/leases/store.ts +1 -1
- package/src/runtime/root/active-execution-power-assertion.ts +139 -0
- package/src/runtime/root/assistant-learning-loop.ts +45 -0
- package/src/runtime/root/browser-session-composition.ts +59 -0
- package/src/runtime/root/computer-composition.ts +191 -0
- package/src/runtime/root/computer-target-composition.ts +16 -0
- package/src/runtime/root/computer-target-persistence.ts +39 -0
- package/src/runtime/root/controller-round-composition.ts +107 -0
- package/src/runtime/root/controller-services.ts +2 -0
- package/src/runtime/root/entry.ts +4 -3
- package/src/runtime/root/gateway-adapter.ts +3 -3
- package/src/runtime/root/local-bridge.ts +5 -2
- package/src/runtime/root/mcp-transport.ts +5 -5
- package/src/runtime/root/package-connector-service.ts +108 -56
- package/src/runtime/root/package-runtime-release.ts +85 -18
- package/src/runtime/root/package-runtime-service.ts +60 -97
- package/src/runtime/root/release-execution-canary.ts +71 -0
- package/src/runtime/root/release-loader.ts +68 -0
- package/src/runtime/root/release-manifest.ts +106 -2
- package/src/runtime/root/release-materialize.ts +103 -4
- package/src/runtime/root/release-store.ts +31 -2
- package/src/runtime/root/runtime.ts +240 -25
- package/src/runtime/root/scheduled-controller-composition.ts +51 -0
- package/src/runtime/root/scheduler.ts +12 -4
- package/src/runtime/root/service-runner.ts +1 -1
- package/src/runtime/root/service.ts +41 -16
- package/src/runtime/root/status.ts +1 -0
- package/src/runtime/root/types.ts +24 -1
- package/src/runtime/shared/local-bridge-surface.ts +1 -1
- package/src/runtime/shared/process-environment.ts +19 -4
- package/src/runtime/shared/storage-capacity.ts +68 -0
- package/src/runtime/standalone-recovery/controller-home-migration.ts +1116 -0
- package/src/runtime/standalone-recovery/core.ts +984 -230
- package/src/runtime/standalone-recovery/entry.ts +182 -51
- package/src/runtime/standalone-recovery/independent-host-rescue.ts +172 -0
- package/src/runtime/standalone-recovery/installer.ts +212 -104
- package/src/runtime/standalone-recovery/mcp-server.ts +243 -0
- package/src/runtime/standalone-recovery/mutation-identity-contract.ts +21 -0
- package/src/runtime/standalone-recovery/operation-lock.ts +160 -0
- package/src/runtime/standalone-recovery/performance.ts +135 -0
- package/src/runtime/standalone-recovery/release.ts +5 -0
- package/src/runtime/watchdog/workflow-watchdog.ts +1 -1
- package/src/runtime/workflow/schedules/chatgpt-automation-migration.ts +1 -1
- package/src/runtime/workflow/schedules/engine.ts +191 -343
- package/src/runtime/workflow/schedules/pending-activation.ts +1 -1
- package/src/runtime/workflow/schedules/settlement.ts +2 -191
- package/src/runtime/workflow/schedules/store.ts +2 -607
- package/src/runtime/workflow/schedules/types.ts +2 -127
- package/src/runtime/workflow/schedules/work-continuation.ts +180 -15
- package/src/runtime/workflows/first-party/xiaohongshu.ts +122 -0
- package/src/runtime/workflows/runtime.ts +486 -0
- package/src/testing/test-governance.ts +90 -29
- package/assets/templates/helpers/archive-architecture-request.sh +0 -244
- package/docs/architecture/history/README.md +0 -7
- package/docs/architecture/history/chatgpt-handoff-facade.md +0 -397
- package/docs/architecture/history/global-hook-runtime.md +0 -166
- package/docs/architecture/history/ios-semantic-automation-provider-v2.md +0 -170
- package/docs/architecture/requests/archive/2026/20260528-154740-ai-hooks-ai-hooks-hook-input-sh.md +0 -59
- package/docs/architecture/requests/archive/2026/20260528-154816-assets-hooks-assets-hooks-hook-input-sh.md +0 -59
- package/docs/architecture/requests/archive/2026/20260528-161851-ai-harness-workflow-contract-json-ai-harness-workflow-contract-json.md +0 -59
- package/docs/architecture/requests/archive/2026/20260528-161853-scripts-check-task-workflow-sh-scripts-check-task-workflow-sh.md +0 -59
- package/docs/architecture/requests/archive/2026/20260528-161911-assets-workflow-contract-v1-json-assets-workflow-contract-v1-json.md +0 -59
- package/docs/architecture/requests/archive/2026/20260528-222416-ai-hooks-ai-hooks-hook-input-sh.md +0 -59
- package/docs/architecture/requests/archive/2026/20260528-222444-ai-hooks-ai-hooks-pre-edit-guard-sh.md +0 -59
- package/docs/architecture/requests/archive/2026/20260528-222457-ai-hooks-ai-hooks-pre-edit-guard-sh.md +0 -59
- package/docs/architecture/requests/archive/2026/20260528-222648-ai-hooks-ai-hooks-pre-edit-guard-sh.md +0 -59
- package/docs/architecture/requests/archive/2026/20260528-222705-ai-hooks-ai-hooks-worktree-guard-sh.md +0 -59
- package/docs/architecture/requests/archive/2026/20260528-222726-ai-hooks-ai-hooks-prompt-guard-sh.md +0 -59
- package/docs/architecture/requests/archive/2026/20260528-222727-ai-hooks-ai-hooks-prompt-guard-sh.md +0 -59
- package/docs/architecture/requests/archive/2026/20260528-224736-ai-hooks-ai-hooks-hook-input-sh.md +0 -59
- package/docs/architecture/requests/archive/2026/20260528-233259-ai-hooks-ai-hooks-prompt-guard-sh.md +0 -59
- package/docs/architecture/requests/archive/2026/20260528-233335-assets-hooks-assets-hooks-prompt-guard-sh.md +0 -59
- package/docs/architecture/requests/archive/2026/20260528-233456-ai-hooks-ai-hooks-prompt-guard-sh.md +0 -59
- package/docs/architecture/requests/archive/2026/20260528-233457-assets-hooks-assets-hooks-prompt-guard-sh.md +0 -59
- package/docs/architecture/requests/archive/2026/20260528-233637-ai-hooks-ai-hooks-prompt-guard-sh.md +0 -59
- package/docs/architecture/requests/archive/2026/20260528-233638-assets-hooks-assets-hooks-prompt-guard-sh.md +0 -59
- package/docs/architecture/requests/archive/2026/20260529-000322-root-package-json.md +0 -59
- package/docs/architecture/requests/archive/2026/20260529-004238-ai-hooks-ai-hooks-lib-workflow-state-sh.md +0 -59
- package/docs/architecture/requests/archive/2026/20260529-020654-ai-harness-policy-json-ai-harness-policy-json.md +0 -63
- package/docs/architecture/requests/archive/2026/20260529-094236-ai-hooks-ai-hooks-prompt-guard-sh.md +0 -60
- package/docs/architecture/requests/archive/2026/20260529-094323-assets-hooks-assets-hooks-prompt-guard-sh.md +0 -60
- package/docs/architecture/requests/archive/2026/20260529-094331-ai-hooks-ai-hooks-prompt-guard-sh.md +0 -60
- package/docs/architecture/requests/archive/2026/20260529-094339-assets-hooks-assets-hooks-prompt-guard-sh.md +0 -60
- package/docs/architecture/requests/archive/2026/20260529-094430-assets-hooks-assets-hooks-session-start-context-sh.md +0 -60
- package/docs/architecture/requests/archive/2026/20260529-094446-ai-hooks-ai-hooks-session-start-context-sh.md +0 -60
- package/docs/architecture/requests/archive/2026/20260613-002040-runtime-harness-hook-adapters.md +0 -68
- package/docs/architecture/requests/archive/2026/root.md +0 -68
- package/docs/architecture/requests/archive/2026/runtime-harness-hook-adapters.md +0 -76
- package/docs/architecture/requests/archive/2026/verification-evals-checks.md +0 -68
- package/docs/architecture/requests/archive/2026/workflow-engine-contract-assets.md +0 -69
- package/scripts/archive-architecture-request.sh +0 -4
- package/scripts/run-skill-evals.ts +0 -1022
|
@@ -1,1414 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import { dirname } from 'path';
|
|
4
|
-
import express, { type Request, type Response, type NextFunction } from 'express';
|
|
5
|
-
import { tokenHandler } from '@modelcontextprotocol/sdk/server/auth/handlers/token.js';
|
|
6
|
-
import { revocationHandler } from '@modelcontextprotocol/sdk/server/auth/handlers/revoke.js';
|
|
7
|
-
import { clientRegistrationHandler } from '@modelcontextprotocol/sdk/server/auth/handlers/register.js';
|
|
8
|
-
import { redirectUriMatches } from '@modelcontextprotocol/sdk/server/auth/handlers/authorize.js';
|
|
9
|
-
import { InvalidTokenError } from '@modelcontextprotocol/sdk/server/auth/errors.js';
|
|
10
|
-
import type { OAuthClientInformationFull } from '@modelcontextprotocol/sdk/shared/auth.js';
|
|
11
|
-
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
12
|
-
import {
|
|
13
|
-
buildMultiRepositoryToolDefinitions,
|
|
14
|
-
createCanonicalRuntimeProxy,
|
|
15
|
-
createMcpToolContext,
|
|
16
|
-
createForgeMcpServerFromContext,
|
|
17
|
-
readCanonicalRuntimeToolSchema,
|
|
18
|
-
type CanonicalRuntimeProxy,
|
|
19
|
-
type CanonicalRuntimeToolSchema,
|
|
20
|
-
type McpServerOptions,
|
|
21
|
-
} from '../server';
|
|
22
|
-
import {
|
|
23
|
-
loadMcpServiceLocalConfig,
|
|
24
|
-
loadMcpServiceRuntimeState,
|
|
25
|
-
mcpServiceOAuthTokenStoreFallbackPaths,
|
|
26
|
-
mcpServiceOAuthTokenStorePath,
|
|
27
|
-
parseMcpHttpAuthMode,
|
|
28
|
-
readMcpServiceBearerToken,
|
|
29
|
-
readMcpServiceOAuthPassphrase,
|
|
30
|
-
type McpLocalConfig,
|
|
31
|
-
type McpHttpAuthMode,
|
|
32
|
-
} from '../auth';
|
|
33
|
-
import { createMcpOAuthProvider, McpOAuthTokenStore } from '../oauth';
|
|
34
|
-
import { resolveMcpRepoRoot } from '../repo';
|
|
35
|
-
import { buildMcpToolDefinitions } from '../tools';
|
|
36
|
-
import { resolveControllerHome } from '../../repositories/controller-home';
|
|
37
|
-
import {
|
|
38
|
-
controllerExposureSnapshot,
|
|
39
|
-
} from '../toolset';
|
|
40
|
-
import { readForgeRuntimeStatus } from '../../../runtime/control-plane/runtime-status-client';
|
|
41
|
-
import { invalidateExecutionSession } from '../../../runtime/control-plane/execution/session-store';
|
|
42
|
-
import { runtimeIdentitySnapshot } from '../../../runtime/gateway/mcp/runtime-tools';
|
|
43
|
-
import { projectionBlocksReadiness, readRepositoryProjectionSnapshot } from '../../../runtime/projections/materialized-view';
|
|
44
|
-
import { readRuntimeGeneration } from '../../../runtime/control-plane/runtime-generation';
|
|
45
|
-
import { readRuntimeStatusSnapshot, runtimeStatusPath } from '../../../runtime/root/status';
|
|
46
|
-
import { getRepository, listRepositories } from '../../repositories/registry';
|
|
47
|
-
import { buildControllerTaskLedgerProjection } from '../../controller/task-ledger';
|
|
48
|
-
import { legacyIssueAuthorityRetired } from '../../controller/legacy-issue-cutover';
|
|
49
|
-
import { reconcileReadinessProjectionSource } from '../readiness-projection';
|
|
50
|
-
import {
|
|
51
|
-
FORGE_MCP_SCHEMA_VERSION,
|
|
52
|
-
FORGE_TOOL_SURFACE,
|
|
53
|
-
FORGE_VERSION,
|
|
54
|
-
repositoryIdentity,
|
|
55
|
-
} from '../../controller/runtime-config';
|
|
56
|
-
import { McpSessionRegistry, type McpSessionRoute } from './session-registry';
|
|
57
|
-
|
|
58
|
-
export interface McpHttpOptions extends McpServerOptions {
|
|
59
|
-
host?: string;
|
|
60
|
-
port?: number;
|
|
61
|
-
authToken?: string;
|
|
62
|
-
auth?: string;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function localControllerDiagnosticMatchesRuntime(
|
|
66
|
-
payload: Record<string, unknown> | null,
|
|
67
|
-
generation?: string,
|
|
68
|
-
): boolean {
|
|
69
|
-
return payload?.status === 'ok'
|
|
70
|
-
&& payload.toolSurface === FORGE_TOOL_SURFACE
|
|
71
|
-
&& payload.schemaVersion === FORGE_MCP_SCHEMA_VERSION
|
|
72
|
-
&& payload.version === FORGE_VERSION
|
|
73
|
-
&& (generation === undefined || payload.generation === generation);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function bearerFromRequest(req: Request): string | null {
|
|
77
|
-
const header = req.headers.authorization;
|
|
78
|
-
if (typeof header !== 'string') return null;
|
|
79
|
-
const match = /^Bearer\s+(.+)$/i.exec(header.trim());
|
|
80
|
-
return match ? match[1].trim() : null;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function principalFromRequest(req: Request): string {
|
|
84
|
-
const auth = (req as unknown as { auth?: { clientId?: string } }).auth;
|
|
85
|
-
if (auth?.clientId?.trim()) return `oauth-client:${auth.clientId.trim()}`;
|
|
86
|
-
return bearerFromRequest(req) ? 'mcp-bearer-client' : 'controller-http-client';
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
export function isAuthorizedMcpHttpRequest(req: Request, expectedToken: string | null): boolean {
|
|
90
|
-
if (!expectedToken) return false;
|
|
91
|
-
return bearerFromRequest(req) === expectedToken;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function rawBodyToJson(body: Buffer): unknown | undefined {
|
|
95
|
-
if (body.length === 0) return undefined;
|
|
96
|
-
return JSON.parse(body.toString('utf-8'));
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function isInitializeRequest(body: unknown): boolean {
|
|
100
|
-
return typeof body === 'object' && body !== null && (body as Record<string, unknown>).method === 'initialize';
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function isServerDiscoverRequest(body: unknown): boolean {
|
|
104
|
-
return typeof body === 'object' && body !== null && (body as Record<string, unknown>).method === 'server/discover';
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function sendLegacyServerDiscoverUnsupported(res: Response, body: unknown): void {
|
|
108
|
-
const record = typeof body === 'object' && body !== null ? body as Record<string, unknown> : {};
|
|
109
|
-
const id = typeof record.id === 'string' || typeof record.id === 'number' || record.id === null ? record.id : null;
|
|
110
|
-
res.setHeader('Cache-Control', 'no-store');
|
|
111
|
-
res.status(404).json({
|
|
112
|
-
jsonrpc: '2.0',
|
|
113
|
-
id,
|
|
114
|
-
error: {
|
|
115
|
-
code: -32601,
|
|
116
|
-
message: 'Method not found',
|
|
117
|
-
},
|
|
118
|
-
});
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function initializeClientIdentity(req: Request, body: unknown, route: McpSessionRoute, principalId: string): string {
|
|
122
|
-
const params = typeof body === 'object' && body !== null
|
|
123
|
-
? (body as { params?: { clientInfo?: { name?: unknown; version?: unknown } } }).params
|
|
124
|
-
: undefined;
|
|
125
|
-
const clientInfo = params?.clientInfo;
|
|
126
|
-
const clientName = typeof clientInfo?.name === 'string' && clientInfo.name.trim() ? clientInfo.name.trim() : 'unknown-client';
|
|
127
|
-
const clientVersion = typeof clientInfo?.version === 'string' && clientInfo.version.trim() ? clientInfo.version.trim() : 'unknown-version';
|
|
128
|
-
const userAgent = typeof req.headers['user-agent'] === 'string' && req.headers['user-agent'].trim()
|
|
129
|
-
? req.headers['user-agent'].trim().slice(0, 160)
|
|
130
|
-
: 'unknown-agent';
|
|
131
|
-
return `${principalId}|${route}|${clientName}/${clientVersion}|${userAgent}`;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
export interface McpSessionLookupErrorResponse {
|
|
135
|
-
status: 400 | 404;
|
|
136
|
-
body: {
|
|
137
|
-
error: 'missing_session' | 'session_not_found';
|
|
138
|
-
code: 'MCP_SESSION_REQUIRED' | 'MCP_SESSION_EXPIRED';
|
|
139
|
-
message: string;
|
|
140
|
-
recoverable: true;
|
|
141
|
-
action: 'reinitialize';
|
|
142
|
-
};
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
export function mcpSessionLookupError(sessionId: string | undefined): McpSessionLookupErrorResponse {
|
|
146
|
-
if (!sessionId?.trim()) {
|
|
147
|
-
return {
|
|
148
|
-
status: 400,
|
|
149
|
-
body: {
|
|
150
|
-
error: 'missing_session',
|
|
151
|
-
code: 'MCP_SESSION_REQUIRED',
|
|
152
|
-
message: 'Mcp-Session-Id header is required for this request.',
|
|
153
|
-
recoverable: true,
|
|
154
|
-
action: 'reinitialize',
|
|
155
|
-
},
|
|
156
|
-
};
|
|
157
|
-
}
|
|
158
|
-
return {
|
|
159
|
-
status: 404,
|
|
160
|
-
body: {
|
|
161
|
-
error: 'session_not_found',
|
|
162
|
-
code: 'MCP_SESSION_EXPIRED',
|
|
163
|
-
message: 'MCP session not found or expired; initialize a new session.',
|
|
164
|
-
recoverable: true,
|
|
165
|
-
action: 'reinitialize',
|
|
166
|
-
},
|
|
167
|
-
};
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
export function sendMcpSessionLookupError(res: Response, sessionId: string | undefined): void {
|
|
171
|
-
const response = mcpSessionLookupError(sessionId);
|
|
172
|
-
res.setHeader('Cache-Control', 'no-store');
|
|
173
|
-
res.setHeader('Mcp-Session-Reset', 'reinitialize');
|
|
174
|
-
res.setHeader('x-forge-session-reset', 'reinitialize');
|
|
175
|
-
res.status(response.status).json(response.body);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
export function mcpSessionToolSurfaceFingerprintIsCurrent(
|
|
179
|
-
sessionFingerprint: string | undefined,
|
|
180
|
-
currentFingerprint: string | undefined,
|
|
181
|
-
): boolean {
|
|
182
|
-
// Missing metadata is tolerated for compatibility and transient
|
|
183
|
-
// controller-home read failures. Once both sides are known, equality is the
|
|
184
|
-
// schema fence. Runtime generation remains diagnostic-only.
|
|
185
|
-
return !sessionFingerprint || !currentFingerprint || sessionFingerprint === currentFingerprint;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
export async function resolveMcpSessionCurrentFingerprint(
|
|
189
|
-
publishedFingerprint: string | undefined,
|
|
190
|
-
loadRuntimeFingerprint?: () => Promise<string | undefined>,
|
|
191
|
-
): Promise<string | undefined> {
|
|
192
|
-
// Runtime status publishes the exact schema fingerprint observed at session
|
|
193
|
-
// initialization/cutover. Prefer that O(1) fence on the hot path. If the
|
|
194
|
-
// publication is temporarily unavailable, retain the previous fail-safe
|
|
195
|
-
// behavior by asking the Canonical Runtime directly.
|
|
196
|
-
if (publishedFingerprint) return publishedFingerprint;
|
|
197
|
-
return await loadRuntimeFingerprint?.();
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
export function isMcpToolsListRequest(body: unknown): boolean {
|
|
201
|
-
const messages = Array.isArray(body) ? body : [body];
|
|
202
|
-
return messages.length > 0 && messages.every((message) => {
|
|
203
|
-
if (!message || typeof message !== 'object') return false;
|
|
204
|
-
return (message as { method?: unknown }).method === 'tools/list';
|
|
205
|
-
});
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
function toolCallOutsideSessionSchema(body: unknown, toolNames: readonly string[] | undefined): boolean {
|
|
209
|
-
if (!toolNames) return false;
|
|
210
|
-
const messages = Array.isArray(body) ? body : [body];
|
|
211
|
-
return messages.some((message) => {
|
|
212
|
-
if (!message || typeof message !== 'object') return false;
|
|
213
|
-
const rpc = message as { method?: unknown; params?: { name?: unknown } };
|
|
214
|
-
return rpc.method === 'tools/call'
|
|
215
|
-
&& typeof rpc.params?.name === 'string'
|
|
216
|
-
&& !toolNames.includes(rpc.params.name);
|
|
217
|
-
});
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
function sendMcpToolSurfaceReset(
|
|
221
|
-
res: Response,
|
|
222
|
-
sessionFingerprint: string | undefined,
|
|
223
|
-
currentFingerprint: string | undefined,
|
|
224
|
-
): void {
|
|
225
|
-
res.setHeader('Cache-Control', 'no-store');
|
|
226
|
-
res.setHeader('Mcp-Session-Reset', 'reinitialize');
|
|
227
|
-
res.setHeader('x-forge-session-reset', 'reinitialize');
|
|
228
|
-
res.setHeader('x-forge-session-reset-reason', 'tool_surface_changed');
|
|
229
|
-
res.status(404).json({
|
|
230
|
-
error: 'session_not_found',
|
|
231
|
-
code: 'MCP_TOOL_SURFACE_CHANGED',
|
|
232
|
-
message: 'MCP Runtime tool surface changed; initialize a new session so tools/list is refreshed.',
|
|
233
|
-
recoverable: true,
|
|
234
|
-
action: 'reinitialize',
|
|
235
|
-
previousFingerprint: sessionFingerprint,
|
|
236
|
-
currentFingerprint,
|
|
237
|
-
});
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
export function mcpRequestError(error: unknown) {
|
|
241
|
-
const rawMessage = error instanceof Error ? error.message : String(error);
|
|
242
|
-
const retryable = /(?:\b502\b|\b503\b|\b429\b|ECONNRESET|ETIMEDOUT|EAI_AGAIN|CANONICAL_RUNTIME_TIMEOUT|server_busy|session_capacity|gateway)/i.test(rawMessage);
|
|
243
|
-
return {
|
|
244
|
-
status: (retryable ? 503 : 500) as 500 | 503,
|
|
245
|
-
body: {
|
|
246
|
-
error: 'request_failed' as const,
|
|
247
|
-
code: (retryable ? 'MCP_TRANSIENT_FAILURE' : 'MCP_REQUEST_FAILED') as 'MCP_TRANSIENT_FAILURE' | 'MCP_REQUEST_FAILED',
|
|
248
|
-
message: retryable
|
|
249
|
-
? 'The MCP Runtime is temporarily unavailable; retry shortly.'
|
|
250
|
-
: 'The MCP request could not be completed.',
|
|
251
|
-
recoverable: true as const,
|
|
252
|
-
retryable,
|
|
253
|
-
sessionPreserved: true as const,
|
|
254
|
-
action: 'retry' as const,
|
|
255
|
-
},
|
|
256
|
-
};
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
export function sendMcpRequestError(res: Response, error: unknown): void {
|
|
260
|
-
const response = mcpRequestError(error);
|
|
261
|
-
res.setHeader('Cache-Control', 'no-store');
|
|
262
|
-
res.setHeader('x-forge-session-preserved', 'true');
|
|
263
|
-
res.status(response.status).json(response.body);
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
function getConfiguredPublicOrigin(config: McpLocalConfig | null): string | undefined {
|
|
267
|
-
const configured = process.env.FORGE_MCP_PUBLIC_ORIGIN?.trim();
|
|
268
|
-
if (configured) {
|
|
269
|
-
try {
|
|
270
|
-
return new URL(configured).origin;
|
|
271
|
-
} catch (_error) {
|
|
272
|
-
// Fall through to service or legacy config.
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
const endpoint = config?.chatgpt?.endpoint?.trim();
|
|
276
|
-
if (!endpoint) return undefined;
|
|
277
|
-
try {
|
|
278
|
-
return new URL(endpoint).origin;
|
|
279
|
-
} catch (_error) {
|
|
280
|
-
return undefined;
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
function getPublicOrigin(req: Request, configuredOrigin: string | undefined): string {
|
|
285
|
-
if (configuredOrigin) return configuredOrigin;
|
|
286
|
-
const proto = (req.headers['x-forwarded-proto'] as string | undefined) ?? 'https';
|
|
287
|
-
const host = (req.headers['x-forwarded-host'] as string | undefined) ?? req.headers.host ?? '127.0.0.1:8765';
|
|
288
|
-
return `${proto}://${host}`;
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
function localControllerHealthUrl(host: string, port: number): string {
|
|
292
|
-
return `http://${host === '::1' ? '[::1]' : host}:${port}/health`;
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
async function jsonHealth(url: string): Promise<Record<string, unknown> | null> {
|
|
296
|
-
const controller = new AbortController();
|
|
297
|
-
const timeout = setTimeout(() => controller.abort(), 2_000);
|
|
298
|
-
try {
|
|
299
|
-
const response = await fetch(url, {
|
|
300
|
-
signal: controller.signal,
|
|
301
|
-
headers: { accept: 'application/json' },
|
|
302
|
-
});
|
|
303
|
-
if (!response.ok) return null;
|
|
304
|
-
return await response.json() as Record<string, unknown>;
|
|
305
|
-
} catch (_error) {
|
|
306
|
-
return null;
|
|
307
|
-
} finally {
|
|
308
|
-
clearTimeout(timeout);
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
export function isAllowedMcpOAuthRedirectUri(redirectUri: string): boolean {
|
|
313
|
-
try {
|
|
314
|
-
const url = new URL(redirectUri);
|
|
315
|
-
if (url.protocol === 'http:' && ['localhost', '127.0.0.1', '[::1]'].includes(url.hostname)) {
|
|
316
|
-
return true;
|
|
317
|
-
}
|
|
318
|
-
if (
|
|
319
|
-
url.protocol === 'https:' &&
|
|
320
|
-
(url.origin === 'https://chatgpt.com' || url.origin === 'https://chat.openai.com') &&
|
|
321
|
-
url.pathname.startsWith('/connector/oauth/')
|
|
322
|
-
) {
|
|
323
|
-
return true;
|
|
324
|
-
}
|
|
325
|
-
return false;
|
|
326
|
-
} catch (_error) {
|
|
327
|
-
return false;
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
function isRegisteredRedirectUri(redirectUri: string, client: { redirect_uris?: string[] }): boolean {
|
|
332
|
-
return (client.redirect_uris ?? []).some((registered) => redirectUriMatches(redirectUri, registered));
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
function isRegisteredExternalHttpsRedirectUri(redirectUri: string, client: { redirect_uris?: string[] }): boolean {
|
|
336
|
-
try {
|
|
337
|
-
const url = new URL(redirectUri);
|
|
338
|
-
return url.protocol === 'https:' && !url.username && !url.password && isRegisteredRedirectUri(redirectUri, client);
|
|
339
|
-
} catch (_error) {
|
|
340
|
-
return false;
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
function isSafeOAuthFallbackRedirectUri(redirectUri: string): boolean {
|
|
345
|
-
try {
|
|
346
|
-
const url = new URL(redirectUri);
|
|
347
|
-
if (url.username || url.password) return false;
|
|
348
|
-
if (url.protocol === 'http:' && ['localhost', '127.0.0.1', '[::1]'].includes(url.hostname)) return true;
|
|
349
|
-
return url.protocol === 'https:';
|
|
350
|
-
} catch (_error) {
|
|
351
|
-
return false;
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
async function getOrRegisterPublicOAuthClient(
|
|
356
|
-
provider: ReturnType<typeof createMcpOAuthProvider>,
|
|
357
|
-
clientId: string,
|
|
358
|
-
redirectUri: string | undefined,
|
|
359
|
-
req: Request,
|
|
360
|
-
): Promise<OAuthClientInformationFull | undefined> {
|
|
361
|
-
const existing = await provider.clientsStore.getClient(clientId);
|
|
362
|
-
if (existing) return existing as OAuthClientInformationFull;
|
|
363
|
-
if (!redirectUri || !isSafeOAuthFallbackRedirectUri(redirectUri) || !provider.clientsStore.registerClient) return undefined;
|
|
364
|
-
oauthTrace(req, 'authorize:auto_register_public_client', {
|
|
365
|
-
redirectScheme: new URL(redirectUri).protocol,
|
|
366
|
-
redirectHost: new URL(redirectUri).hostname,
|
|
367
|
-
});
|
|
368
|
-
return provider.clientsStore.registerClient({
|
|
369
|
-
client_id: clientId,
|
|
370
|
-
client_id_issued_at: Math.floor(Date.now() / 1000),
|
|
371
|
-
client_name: 'forge OAuth fallback client',
|
|
372
|
-
redirect_uris: [redirectUri],
|
|
373
|
-
token_endpoint_auth_method: 'none',
|
|
374
|
-
grant_types: ['authorization_code', 'refresh_token'],
|
|
375
|
-
response_types: ['code'],
|
|
376
|
-
} as unknown as Omit<OAuthClientInformationFull, 'client_id' | 'client_id_issued_at'>) as OAuthClientInformationFull;
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
function escapeHtmlAttribute(value: string): string {
|
|
380
|
-
return value
|
|
381
|
-
.replace(/&/g, '&')
|
|
382
|
-
.replace(/"/g, '"')
|
|
383
|
-
.replace(/'/g, ''')
|
|
384
|
-
.replace(/</g, '<')
|
|
385
|
-
.replace(/>/g, '>');
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
function renderPassphrasePage(params: URLSearchParams): string {
|
|
389
|
-
const hiddenFields = Array.from(params.entries())
|
|
390
|
-
.filter(([key]) => key !== 'passphrase')
|
|
391
|
-
.map(([key, value]) => `<input type="hidden" name="${escapeHtmlAttribute(key)}" value="${escapeHtmlAttribute(value)}">`)
|
|
392
|
-
.join('\n');
|
|
393
|
-
|
|
394
|
-
return `<!doctype html>
|
|
395
|
-
<html lang="en">
|
|
396
|
-
<head><meta charset="utf-8"><title>Authorize forge</title>
|
|
397
|
-
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
398
|
-
<style>
|
|
399
|
-
body{font-family:-apple-system,BlinkMacSystemFont,sans-serif;display:flex;align-items:center;justify-content:center;min-height:100vh;margin:0;background:#f6f6f3;color:#1f2328}
|
|
400
|
-
.card{width:min(420px,92vw);background:#fff;border:1px solid #d8d8d0;border-radius:12px;padding:32px;box-shadow:0 12px 40px rgba(0,0,0,.08)}
|
|
401
|
-
h1{font-size:20px;margin:0 0 8px}p{margin:0 0 20px;color:#60666d;line-height:1.45}
|
|
402
|
-
input{width:100%;box-sizing:border-box;border:1px solid #bfc4c9;border-radius:8px;padding:12px;font-size:16px}
|
|
403
|
-
button{width:100%;margin-top:14px;border:0;border-radius:8px;padding:12px;background:#1f2328;color:#fff;font-size:16px;font-weight:600}
|
|
404
|
-
</style></head>
|
|
405
|
-
<body><main class="card">
|
|
406
|
-
<h1>Authorize forge</h1>
|
|
407
|
-
<p>Enter the local MCP passphrase to let this MCP client use this workflow-scoped connector.</p>
|
|
408
|
-
<form method="POST" action="/authorize">
|
|
409
|
-
${hiddenFields}
|
|
410
|
-
<input type="password" name="passphrase" placeholder="Passphrase" autofocus>
|
|
411
|
-
<button type="submit">Authorize</button>
|
|
412
|
-
</form>
|
|
413
|
-
</main></body></html>`;
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
/** Collect OAuth authorize params from query (GET) or body (POST form). */
|
|
417
|
-
function oauthAuthorizeParamSource(req: Request): Record<string, unknown> {
|
|
418
|
-
if (req.method === 'POST' && req.body && typeof req.body === 'object' && !Buffer.isBuffer(req.body)) {
|
|
419
|
-
return req.body as Record<string, unknown>;
|
|
420
|
-
}
|
|
421
|
-
return req.query as Record<string, unknown>;
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
function readOAuthAuthorizeString(source: Record<string, unknown>, key: string): string {
|
|
425
|
-
const value = source[key];
|
|
426
|
-
return typeof value === 'string' ? value.trim() : '';
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
/**
|
|
430
|
-
* True when the request lacks the OAuth parameters needed for a real authorization.
|
|
431
|
-
* Incomplete requests must not render the passphrase form (incompatible clients loop there).
|
|
432
|
-
*/
|
|
433
|
-
export function isIncompleteOAuthAuthorizeRequest(source: Record<string, unknown>): boolean {
|
|
434
|
-
const clientId = readOAuthAuthorizeString(source, 'client_id');
|
|
435
|
-
const responseType = readOAuthAuthorizeString(source, 'response_type');
|
|
436
|
-
const codeChallenge = readOAuthAuthorizeString(source, 'code_challenge');
|
|
437
|
-
const redirectUri = readOAuthAuthorizeString(source, 'redirect_uri');
|
|
438
|
-
// redirect_uri may be omitted when the client has a single registered URI; client_id is the usable redirect context.
|
|
439
|
-
const hasRedirectContext = Boolean(redirectUri) || Boolean(clientId);
|
|
440
|
-
return !clientId || !responseType || !codeChallenge || !hasRedirectContext;
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
function incompleteOAuthAuthorizeResponseBody(): {
|
|
444
|
-
error: 'invalid_request';
|
|
445
|
-
error_description: string;
|
|
446
|
-
message: string;
|
|
447
|
-
hint: string;
|
|
448
|
-
} {
|
|
449
|
-
return {
|
|
450
|
-
error: 'invalid_request',
|
|
451
|
-
error_description:
|
|
452
|
-
'OAuth authorization request is incomplete. Required: client_id, response_type, code_challenge, and a usable redirect context (redirect_uri or a registered client).',
|
|
453
|
-
message:
|
|
454
|
-
'This endpoint expects a complete OAuth authorization request (PKCE). Non-OAuth MCP clients should use /mcp-bearer with Authorization: Bearer <token> instead of /authorize.',
|
|
455
|
-
hint: 'Use POST/GET /mcp-bearer with a forge bearer token for clients that cannot complete OAuth dynamic registration and PKCE.',
|
|
456
|
-
};
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
function isOAuthDebugTraceEnabled(): boolean {
|
|
460
|
-
return process.env.FORGE_MCP_OAUTH_TRACE === '1' || process.env.FORGE_MCP_OAUTH_TRACE === 'true';
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
const SENSITIVE_OAUTH_FIELDS = new Set([
|
|
464
|
-
'passphrase',
|
|
465
|
-
'code',
|
|
466
|
-
'code_verifier',
|
|
467
|
-
'client_secret',
|
|
468
|
-
'access_token',
|
|
469
|
-
'refresh_token',
|
|
470
|
-
'token',
|
|
471
|
-
'authorization',
|
|
472
|
-
]);
|
|
473
|
-
|
|
474
|
-
function safeOAuthFieldNames(source: Record<string, unknown>): string[] {
|
|
475
|
-
return Object.keys(source)
|
|
476
|
-
.filter((key) => !SENSITIVE_OAUTH_FIELDS.has(key.toLowerCase()))
|
|
477
|
-
.sort();
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
function oauthTrace(req: Request, event: string, extra: Record<string, unknown> = {}): void {
|
|
481
|
-
if (!isOAuthDebugTraceEnabled()) return;
|
|
482
|
-
const source = oauthAuthorizeParamSource(req);
|
|
483
|
-
const userAgent = typeof req.headers['user-agent'] === 'string'
|
|
484
|
-
? req.headers['user-agent'].split(/[\s/]/)[0]
|
|
485
|
-
: undefined;
|
|
486
|
-
const safe = {
|
|
487
|
-
event,
|
|
488
|
-
method: req.method,
|
|
489
|
-
path: req.path,
|
|
490
|
-
fieldNames: safeOAuthFieldNames(source),
|
|
491
|
-
hasClientId: Boolean(readOAuthAuthorizeString(source, 'client_id')),
|
|
492
|
-
hasRedirectUri: Boolean(readOAuthAuthorizeString(source, 'redirect_uri')),
|
|
493
|
-
hasCodeChallenge: Boolean(readOAuthAuthorizeString(source, 'code_challenge')),
|
|
494
|
-
responseType: readOAuthAuthorizeString(source, 'response_type') || undefined,
|
|
495
|
-
codeChallengeMethod: readOAuthAuthorizeString(source, 'code_challenge_method') || undefined,
|
|
496
|
-
grantType: readOAuthAuthorizeString(source, 'grant_type') || undefined,
|
|
497
|
-
hasResource: Boolean(readOAuthAuthorizeString(source, 'resource')),
|
|
498
|
-
userAgent,
|
|
499
|
-
...extra,
|
|
500
|
-
};
|
|
501
|
-
console.error(`[forge:mcp-oauth] ${JSON.stringify(safe)}`);
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
function oauthTraceMiddleware(event: string): (req: Request, res: Response, next: NextFunction) => void {
|
|
505
|
-
return (req, res, next) => {
|
|
506
|
-
oauthTrace(req, `${event}:request`);
|
|
507
|
-
res.once('finish', () => oauthTrace(req, `${event}:response`, { statusCode: res.statusCode }));
|
|
508
|
-
next();
|
|
509
|
-
};
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
function rejectIncompleteOAuthAuthorize(req: Request, res: Response, next: NextFunction): void {
|
|
513
|
-
const source = oauthAuthorizeParamSource(req);
|
|
514
|
-
if (isIncompleteOAuthAuthorizeRequest(source)) {
|
|
515
|
-
oauthTrace(req, 'authorize:incomplete');
|
|
516
|
-
res.status(400).json(incompleteOAuthAuthorizeResponseBody());
|
|
517
|
-
return;
|
|
518
|
-
}
|
|
519
|
-
oauthTrace(req, 'authorize:complete');
|
|
520
|
-
next();
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
function requirePassphrase(passphrase: string): (req: Request, res: Response, next: NextFunction) => void {
|
|
524
|
-
return (req, res, next) => {
|
|
525
|
-
const provided = typeof req.body?.passphrase === 'string' ? req.body.passphrase : undefined;
|
|
526
|
-
if (provided) {
|
|
527
|
-
const a = Buffer.from(provided);
|
|
528
|
-
const b = Buffer.from(passphrase);
|
|
529
|
-
if (a.length === b.length && timingSafeEqual(a, b)) {
|
|
530
|
-
next();
|
|
531
|
-
return;
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
|
-
// Prefer body hidden fields on POST (failed passphrase re-render), else query string on GET.
|
|
535
|
-
const source = oauthAuthorizeParamSource(req);
|
|
536
|
-
const params = new URLSearchParams();
|
|
537
|
-
for (const [key, value] of Object.entries(source)) {
|
|
538
|
-
if (key === 'passphrase') continue;
|
|
539
|
-
if (typeof value === 'string') params.set(key, value);
|
|
540
|
-
}
|
|
541
|
-
if ([...params.keys()].length === 0 && req.url.includes('?')) {
|
|
542
|
-
const fromUrl = new URLSearchParams(req.url.slice(req.url.indexOf('?')));
|
|
543
|
-
for (const [key, value] of fromUrl.entries()) {
|
|
544
|
-
if (key !== 'passphrase') params.set(key, value);
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
res.type('html').send(renderPassphrasePage(params));
|
|
548
|
-
};
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
function oauthAuthorizationHandler(provider: ReturnType<typeof createMcpOAuthProvider>) {
|
|
552
|
-
return async (req: Request, res: Response) => {
|
|
553
|
-
const query = req.method === 'POST' ? req.body : req.query;
|
|
554
|
-
const clientId = typeof query.client_id === 'string' ? query.client_id : '';
|
|
555
|
-
const responseType = typeof query.response_type === 'string' ? query.response_type : '';
|
|
556
|
-
const codeChallenge = typeof query.code_challenge === 'string' ? query.code_challenge : '';
|
|
557
|
-
const codeChallengeMethod = typeof query.code_challenge_method === 'string' ? query.code_challenge_method : '';
|
|
558
|
-
const state = typeof query.state === 'string' ? query.state : undefined;
|
|
559
|
-
const scope = typeof query.scope === 'string' ? query.scope : undefined;
|
|
560
|
-
let redirectUri = typeof query.redirect_uri === 'string' ? query.redirect_uri : undefined;
|
|
561
|
-
|
|
562
|
-
if (responseType !== 'code') {
|
|
563
|
-
res.status(400).json({ error: 'unsupported_response_type', error_description: 'Only code response type is supported' });
|
|
564
|
-
return;
|
|
565
|
-
}
|
|
566
|
-
if (!codeChallenge || codeChallengeMethod !== 'S256') {
|
|
567
|
-
res.status(400).json({ error: 'invalid_request', error_description: 'PKCE S256 is required' });
|
|
568
|
-
return;
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
const client = await getOrRegisterPublicOAuthClient(provider, clientId, redirectUri, req);
|
|
572
|
-
if (!client) {
|
|
573
|
-
res.status(400).json({ error: 'invalid_client', error_description: 'Unknown client_id' });
|
|
574
|
-
return;
|
|
575
|
-
}
|
|
576
|
-
if (!redirectUri && client.redirect_uris.length === 1) {
|
|
577
|
-
redirectUri = client.redirect_uris[0];
|
|
578
|
-
}
|
|
579
|
-
if (!redirectUri || (!isAllowedMcpOAuthRedirectUri(redirectUri) && !isRegisteredExternalHttpsRedirectUri(redirectUri, client))) {
|
|
580
|
-
res.status(400).json({
|
|
581
|
-
error: 'invalid_request',
|
|
582
|
-
error_description: 'redirect_uri must be localhost, a ChatGPT connector callback URL, or a registered HTTPS client redirect_uri',
|
|
583
|
-
});
|
|
584
|
-
return;
|
|
585
|
-
}
|
|
586
|
-
if (!isRegisteredRedirectUri(redirectUri, client)) {
|
|
587
|
-
res.status(400).json({
|
|
588
|
-
error: 'invalid_request',
|
|
589
|
-
error_description: 'redirect_uri must match a registered client redirect_uri',
|
|
590
|
-
});
|
|
591
|
-
return;
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
await provider.authorize(client as OAuthClientInformationFull, {
|
|
595
|
-
state,
|
|
596
|
-
scopes: scope ? scope.split(' ') : [],
|
|
597
|
-
redirectUri,
|
|
598
|
-
codeChallenge,
|
|
599
|
-
}, res);
|
|
600
|
-
};
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
function sendBearerUnauthorized(res: Response, description: string, hasConfiguredToken: boolean): void {
|
|
604
|
-
res.setHeader('www-authenticate', 'Bearer realm="forge-mcp"');
|
|
605
|
-
res.status(hasConfiguredToken ? 401 : 503).json({
|
|
606
|
-
error: hasConfiguredToken ? 'unauthorized' : 'auth_not_configured',
|
|
607
|
-
message: description,
|
|
608
|
-
});
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
function sendOAuthUnauthorized(
|
|
612
|
-
req: Request,
|
|
613
|
-
res: Response,
|
|
614
|
-
description: string,
|
|
615
|
-
configuredOrigin: string | undefined,
|
|
616
|
-
resourcePath = '/mcp',
|
|
617
|
-
): void {
|
|
618
|
-
const resourceMetadataUrl = `${getPublicOrigin(req, configuredOrigin)}/.well-known/oauth-protected-resource${resourcePath}`;
|
|
619
|
-
res.setHeader(
|
|
620
|
-
'www-authenticate',
|
|
621
|
-
`Bearer error="invalid_token", error_description="${description}", resource_metadata="${resourceMetadataUrl}"`,
|
|
622
|
-
);
|
|
623
|
-
res.status(401).json({ error: 'invalid_token', message: description });
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
function requireMcpHttpAuth(
|
|
627
|
-
mode: McpHttpAuthMode,
|
|
628
|
-
bearerToken: string | null,
|
|
629
|
-
provider: ReturnType<typeof createMcpOAuthProvider> | null,
|
|
630
|
-
configuredOrigin: string | undefined,
|
|
631
|
-
resourcePath = '/mcp',
|
|
632
|
-
) {
|
|
633
|
-
return (req: Request, res: Response, next: NextFunction) => {
|
|
634
|
-
if (mode === 'none') {
|
|
635
|
-
next();
|
|
636
|
-
return;
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
if (mode === 'bearer') {
|
|
640
|
-
if (!isAuthorizedMcpHttpRequest(req, bearerToken)) {
|
|
641
|
-
sendBearerUnauthorized(res, bearerToken ? 'Missing or invalid Authorization header' : 'Bearer token is not configured', Boolean(bearerToken));
|
|
642
|
-
return;
|
|
643
|
-
}
|
|
644
|
-
next();
|
|
645
|
-
return;
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
if (isAuthorizedMcpHttpRequest(req, bearerToken)) {
|
|
649
|
-
next();
|
|
650
|
-
return;
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
const token = bearerFromRequest(req);
|
|
654
|
-
if (!token || !provider) {
|
|
655
|
-
sendOAuthUnauthorized(req, res, token ? 'OAuth is not configured' : 'Missing Authorization header', configuredOrigin, resourcePath);
|
|
656
|
-
return;
|
|
657
|
-
}
|
|
658
|
-
provider.verifyAccessToken(token)
|
|
659
|
-
.then((authInfo) => {
|
|
660
|
-
(req as unknown as Record<string, unknown>).auth = authInfo;
|
|
661
|
-
next();
|
|
662
|
-
})
|
|
663
|
-
.catch((error: unknown) => {
|
|
664
|
-
if (error instanceof InvalidTokenError) {
|
|
665
|
-
sendOAuthUnauthorized(req, res, error.message, configuredOrigin, resourcePath);
|
|
666
|
-
} else {
|
|
667
|
-
res.status(500).json({ error: 'server_error', message: 'Internal Server Error' });
|
|
668
|
-
}
|
|
669
|
-
});
|
|
670
|
-
};
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
interface McpRuntimeStats {
|
|
674
|
-
initializing: number;
|
|
675
|
-
activePosts: number;
|
|
676
|
-
rejectedOverload: number;
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
function positiveIntegerEnv(name: string, fallback: number): number {
|
|
680
|
-
const value = Number(process.env[name]);
|
|
681
|
-
return Number.isInteger(value) && value > 0 ? value : fallback;
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
const MAX_MCP_SESSIONS = positiveIntegerEnv('FORGE_MCP_MAX_SESSIONS', 64);
|
|
685
|
-
const MAX_MCP_SESSIONS_PER_PRINCIPAL = positiveIntegerEnv('FORGE_MCP_MAX_SESSIONS_PER_PRINCIPAL', MAX_MCP_SESSIONS);
|
|
686
|
-
const MAX_INITIALIZING_SESSIONS = positiveIntegerEnv('FORGE_MCP_MAX_INITIALIZING_SESSIONS', 8);
|
|
687
|
-
const MAX_POSTS_PER_SESSION = positiveIntegerEnv('FORGE_MCP_MAX_POSTS_PER_SESSION', 4);
|
|
688
|
-
const MAX_ACTIVE_POSTS = positiveIntegerEnv('FORGE_MCP_MAX_ACTIVE_POSTS', 32);
|
|
689
|
-
const MCP_SESSION_IDLE_TTL_MS = positiveIntegerEnv('FORGE_MCP_SESSION_IDLE_TTL_MS', 15 * 60_000);
|
|
690
|
-
const MCP_STREAM_LEASE_MS = positiveIntegerEnv('FORGE_MCP_STREAM_LEASE_MS', 30 * 60_000);
|
|
691
|
-
const MCP_SESSION_ABSOLUTE_LIFETIME_MS = positiveIntegerEnv('FORGE_MCP_SESSION_ABSOLUTE_LIFETIME_MS', 2 * 60 * 60_000);
|
|
692
|
-
const MCP_ACTIVE_POST_STALL_MS = positiveIntegerEnv('FORGE_MCP_ACTIVE_POST_STALL_MS', 10 * 60_000);
|
|
693
|
-
|
|
694
|
-
type McpToolContext = ReturnType<typeof createMcpToolContext>;
|
|
695
|
-
type HttpSessionRegistry = McpSessionRegistry<StreamableHTTPServerTransport, McpToolContext>;
|
|
696
|
-
|
|
697
|
-
async function handleMcpPost(
|
|
698
|
-
req: Request,
|
|
699
|
-
res: Response,
|
|
700
|
-
baseOptions: McpServerOptions,
|
|
701
|
-
registry: HttpSessionRegistry,
|
|
702
|
-
stats: McpRuntimeStats,
|
|
703
|
-
route: McpSessionRoute,
|
|
704
|
-
currentToolSurfaceFingerprint: () => string | undefined,
|
|
705
|
-
resolveRuntimeSchema?: (context: McpToolContext) => Promise<CanonicalRuntimeToolSchema | undefined>,
|
|
706
|
-
sharedRuntimeProxy?: CanonicalRuntimeProxy,
|
|
707
|
-
): Promise<void> {
|
|
708
|
-
let body: unknown;
|
|
709
|
-
try {
|
|
710
|
-
body = rawBodyToJson(req.body as Buffer);
|
|
711
|
-
} catch (_error) {
|
|
712
|
-
res.status(400).json({ error: 'invalid JSON request body' });
|
|
713
|
-
return;
|
|
714
|
-
}
|
|
715
|
-
const sessionId = req.headers['mcp-session-id'] as string | undefined;
|
|
716
|
-
// MCP 2026-07-28 clients probe legacy servers with server/discover before
|
|
717
|
-
// falling back to the initialize-era protocol. Forge still serves the
|
|
718
|
-
// legacy stateful transport, so reject the unsupported modern RPC with the
|
|
719
|
-
// protocol-prescribed Method not found / HTTP 404 response instead of
|
|
720
|
-
// misclassifying it as a missing-session HTTP 400.
|
|
721
|
-
if (isServerDiscoverRequest(body)) {
|
|
722
|
-
sendLegacyServerDiscoverUnsupported(res, body);
|
|
723
|
-
return;
|
|
724
|
-
}
|
|
725
|
-
if (isInitializeRequest(body)) {
|
|
726
|
-
if (sessionId) {
|
|
727
|
-
res.setHeader('Mcp-Session-Reset', 'reinitialized');
|
|
728
|
-
res.setHeader('x-forge-session-reset', 'reinitialized');
|
|
729
|
-
}
|
|
730
|
-
if (stats.initializing >= MAX_INITIALIZING_SESSIONS || stats.activePosts >= MAX_ACTIVE_POSTS) {
|
|
731
|
-
stats.rejectedOverload += 1;
|
|
732
|
-
res.setHeader('retry-after', '1');
|
|
733
|
-
res.status(503).json({
|
|
734
|
-
error: 'server_busy',
|
|
735
|
-
code: 'MCP_SERVER_BUSY',
|
|
736
|
-
message: 'Too many MCP sessions are initializing; retry shortly',
|
|
737
|
-
recoverable: true,
|
|
738
|
-
retryable: true,
|
|
739
|
-
sessionPreserved: true,
|
|
740
|
-
action: 'retry',
|
|
741
|
-
});
|
|
742
|
-
return;
|
|
743
|
-
}
|
|
744
|
-
stats.initializing += 1;
|
|
745
|
-
stats.activePosts += 1;
|
|
746
|
-
let transport: StreamableHTTPServerTransport | undefined;
|
|
747
|
-
let reservationId: string | undefined;
|
|
748
|
-
let initializedSessionId: string | undefined;
|
|
749
|
-
try {
|
|
750
|
-
const principalId = principalFromRequest(req);
|
|
751
|
-
const clientIdentity = initializeClientIdentity(req, body, route, principalId);
|
|
752
|
-
reservationId = await registry.reserveForInitialize({
|
|
753
|
-
principalId,
|
|
754
|
-
route,
|
|
755
|
-
...(principalId === 'mcp-bearer-client' ? { enforcePrincipalCapacity: false } : {}),
|
|
756
|
-
...(sessionId ? { supersedeSessionId: sessionId } : {}),
|
|
757
|
-
});
|
|
758
|
-
if (!reservationId) {
|
|
759
|
-
stats.rejectedOverload += 1;
|
|
760
|
-
res.setHeader('retry-after', '1');
|
|
761
|
-
res.status(503).json({
|
|
762
|
-
error: 'session_capacity',
|
|
763
|
-
code: 'MCP_SESSION_CAPACITY',
|
|
764
|
-
message: 'All MCP sessions are executing active requests; retry shortly',
|
|
765
|
-
recoverable: true,
|
|
766
|
-
retryable: true,
|
|
767
|
-
sessionPreserved: true,
|
|
768
|
-
action: 'retry',
|
|
769
|
-
});
|
|
770
|
-
return;
|
|
771
|
-
}
|
|
772
|
-
const sessionContext = createMcpToolContext({
|
|
773
|
-
...baseOptions,
|
|
774
|
-
sessionId: `mcp_${randomUUID().replace(/-/g, '')}`,
|
|
775
|
-
principalId,
|
|
776
|
-
});
|
|
777
|
-
let runtimeSchema = await resolveRuntimeSchema?.(sessionContext);
|
|
778
|
-
let server: ReturnType<typeof createForgeMcpServerFromContext> | undefined;
|
|
779
|
-
transport = new StreamableHTTPServerTransport({
|
|
780
|
-
sessionIdGenerator: () => randomUUID(),
|
|
781
|
-
onsessioninitialized: (newSessionId: string): void => {
|
|
782
|
-
registry.commitInitialize(reservationId!, {
|
|
783
|
-
sessionId: newSessionId,
|
|
784
|
-
transport: transport!,
|
|
785
|
-
toolContext: sessionContext,
|
|
786
|
-
route,
|
|
787
|
-
principalId,
|
|
788
|
-
clientIdentity,
|
|
789
|
-
toolSurfaceFingerprint: runtimeSchema?.fingerprint ?? currentToolSurfaceFingerprint(),
|
|
790
|
-
toolNames: runtimeSchema?.toolNames,
|
|
791
|
-
notifyToolListChanged: async () => { await server?.sendToolListChanged(); },
|
|
792
|
-
...(runtimeSchema && resolveRuntimeSchema
|
|
793
|
-
? {
|
|
794
|
-
refreshToolSurface: async () => {
|
|
795
|
-
const refreshed = await resolveRuntimeSchema(sessionContext);
|
|
796
|
-
if (!refreshed || !runtimeSchema) return undefined;
|
|
797
|
-
runtimeSchema.definitions = refreshed.definitions;
|
|
798
|
-
runtimeSchema.toolNames = refreshed.toolNames;
|
|
799
|
-
runtimeSchema.fingerprint = refreshed.fingerprint;
|
|
800
|
-
return {
|
|
801
|
-
toolSurfaceFingerprint: refreshed.fingerprint,
|
|
802
|
-
toolNames: refreshed.toolNames,
|
|
803
|
-
};
|
|
804
|
-
},
|
|
805
|
-
}
|
|
806
|
-
: {}),
|
|
807
|
-
});
|
|
808
|
-
initializedSessionId = newSessionId;
|
|
809
|
-
},
|
|
810
|
-
});
|
|
811
|
-
transport.onclose = () => {
|
|
812
|
-
if (transport?.sessionId) registry.detach(transport.sessionId);
|
|
813
|
-
};
|
|
814
|
-
server = createForgeMcpServerFromContext(sessionContext, runtimeSchema, sharedRuntimeProxy);
|
|
815
|
-
await server.connect(transport);
|
|
816
|
-
await transport.handleRequest(req, res, body);
|
|
817
|
-
} finally {
|
|
818
|
-
if (initializedSessionId) registry.endPost(initializedSessionId);
|
|
819
|
-
if (reservationId) registry.releaseInitialize(reservationId);
|
|
820
|
-
stats.initializing -= 1;
|
|
821
|
-
stats.activePosts -= 1;
|
|
822
|
-
if (!transport?.sessionId) await transport?.close().catch(() => undefined);
|
|
823
|
-
}
|
|
824
|
-
return;
|
|
825
|
-
}
|
|
826
|
-
if (sessionId) {
|
|
827
|
-
const managed = registry.get(sessionId);
|
|
828
|
-
if (managed && managed.route === route && managed.principalId === principalFromRequest(req)) {
|
|
829
|
-
if (managed.inFlightPosts >= MAX_POSTS_PER_SESSION || stats.activePosts >= MAX_ACTIVE_POSTS) {
|
|
830
|
-
stats.rejectedOverload += 1;
|
|
831
|
-
res.setHeader('retry-after', '1');
|
|
832
|
-
res.status(429).json({ error: 'session_busy', message: 'Too many MCP requests are active; retry shortly' });
|
|
833
|
-
return;
|
|
834
|
-
}
|
|
835
|
-
registry.beginPost(sessionId);
|
|
836
|
-
stats.activePosts += 1;
|
|
837
|
-
try {
|
|
838
|
-
const currentFingerprint = await resolveMcpSessionCurrentFingerprint(
|
|
839
|
-
currentToolSurfaceFingerprint(),
|
|
840
|
-
resolveRuntimeSchema
|
|
841
|
-
? async () => (await resolveRuntimeSchema(managed.toolContext))?.fingerprint
|
|
842
|
-
: undefined,
|
|
843
|
-
);
|
|
844
|
-
if (!mcpSessionToolSurfaceFingerprintIsCurrent(managed.toolSurfaceFingerprint, currentFingerprint)) {
|
|
845
|
-
// Standard MCP hot refresh: allow an explicitly requested tools/list
|
|
846
|
-
// to replace the live session's mutable Runtime schema snapshot. Hosts
|
|
847
|
-
// that ignore list_changed and call directly still hit the reset fence.
|
|
848
|
-
if (isMcpToolsListRequest(body)) {
|
|
849
|
-
const refreshed = await registry.refreshToolSurface(sessionId);
|
|
850
|
-
if (refreshed
|
|
851
|
-
&& (!currentFingerprint || refreshed.toolSurfaceFingerprint === currentFingerprint)) {
|
|
852
|
-
await managed.transport.handleRequest(req, res, body);
|
|
853
|
-
return;
|
|
854
|
-
}
|
|
855
|
-
}
|
|
856
|
-
// Keep the transport/session alive long enough for the host to observe
|
|
857
|
-
// the recoverable reset and issue a replacement initialize request.
|
|
858
|
-
// The initialize path explicitly supersedes this session afterward.
|
|
859
|
-
sendMcpToolSurfaceReset(res, managed.toolSurfaceFingerprint, currentFingerprint);
|
|
860
|
-
return;
|
|
861
|
-
}
|
|
862
|
-
if (toolCallOutsideSessionSchema(body, managed.toolNames)) {
|
|
863
|
-
// A call against a newer discovery surface is the same recoverable
|
|
864
|
-
// schema-fence condition. Closing here can make hosts unregister the
|
|
865
|
-
// entire MCP namespace before they can reinitialize it.
|
|
866
|
-
sendMcpToolSurfaceReset(res, managed.toolSurfaceFingerprint, currentFingerprint);
|
|
867
|
-
return;
|
|
868
|
-
}
|
|
869
|
-
await managed.transport.handleRequest(req, res, body);
|
|
870
|
-
} finally {
|
|
871
|
-
registry.endPost(sessionId);
|
|
872
|
-
stats.activePosts -= 1;
|
|
873
|
-
}
|
|
874
|
-
return;
|
|
875
|
-
}
|
|
876
|
-
}
|
|
877
|
-
sendMcpSessionLookupError(res, sessionId);
|
|
878
|
-
}
|
|
879
|
-
|
|
880
|
-
async function handleMcpGet(
|
|
881
|
-
req: Request,
|
|
882
|
-
res: Response,
|
|
883
|
-
registry: HttpSessionRegistry,
|
|
884
|
-
route: McpSessionRoute,
|
|
885
|
-
currentToolSurfaceFingerprint: () => string | undefined,
|
|
886
|
-
resolveRuntimeSchema?: (context: McpToolContext) => Promise<CanonicalRuntimeToolSchema | undefined>,
|
|
887
|
-
): Promise<void> {
|
|
888
|
-
const sessionId = req.headers['mcp-session-id'] as string | undefined;
|
|
889
|
-
const managed = sessionId ? registry.get(sessionId) : undefined;
|
|
890
|
-
if (!managed || managed.route !== route || managed.principalId !== principalFromRequest(req)) {
|
|
891
|
-
sendMcpSessionLookupError(res, sessionId);
|
|
892
|
-
return;
|
|
893
|
-
}
|
|
894
|
-
registry.beginStream(sessionId!);
|
|
895
|
-
let released = false;
|
|
896
|
-
const releaseStream = (): void => {
|
|
897
|
-
if (released) return;
|
|
898
|
-
released = true;
|
|
899
|
-
registry.endStream(sessionId!);
|
|
900
|
-
};
|
|
901
|
-
req.once('aborted', releaseStream);
|
|
902
|
-
res.once('close', releaseStream);
|
|
903
|
-
try {
|
|
904
|
-
const currentFingerprint = await resolveMcpSessionCurrentFingerprint(
|
|
905
|
-
currentToolSurfaceFingerprint(),
|
|
906
|
-
resolveRuntimeSchema
|
|
907
|
-
? async () => (await resolveRuntimeSchema(managed.toolContext))?.fingerprint
|
|
908
|
-
: undefined,
|
|
909
|
-
);
|
|
910
|
-
if (!mcpSessionToolSurfaceFingerprintIsCurrent(managed.toolSurfaceFingerprint, currentFingerprint)) {
|
|
911
|
-
// Preserve the existing SSE transport while asking the host to
|
|
912
|
-
// reinitialize. The replacement initialize owns supersession/cleanup.
|
|
913
|
-
sendMcpToolSurfaceReset(res, managed.toolSurfaceFingerprint, currentFingerprint);
|
|
914
|
-
return;
|
|
915
|
-
}
|
|
916
|
-
await managed.transport.handleRequest(req, res);
|
|
917
|
-
} finally {
|
|
918
|
-
req.off('aborted', releaseStream);
|
|
919
|
-
res.off('close', releaseStream);
|
|
920
|
-
releaseStream();
|
|
921
|
-
}
|
|
922
|
-
}
|
|
923
|
-
|
|
924
|
-
async function handleMcpDelete(req: Request, res: Response, registry: HttpSessionRegistry, route: McpSessionRoute): Promise<void> {
|
|
925
|
-
const sessionId = req.headers['mcp-session-id'] as string | undefined;
|
|
926
|
-
const managed = sessionId ? registry.get(sessionId) : undefined;
|
|
927
|
-
if (!managed || managed.route !== route || managed.principalId !== principalFromRequest(req)) {
|
|
928
|
-
sendMcpSessionLookupError(res, sessionId);
|
|
929
|
-
return;
|
|
930
|
-
}
|
|
931
|
-
registry.setPendingCloseReason(sessionId!, 'client_delete');
|
|
932
|
-
await managed.transport.handleRequest(req, res);
|
|
933
|
-
if (registry.get(sessionId!)) await registry.close(sessionId!, 'client_delete');
|
|
934
|
-
}
|
|
935
|
-
|
|
936
|
-
export async function startMcpHttp(opts: McpHttpOptions): Promise<void> {
|
|
937
|
-
const host = opts.host ?? '127.0.0.1';
|
|
938
|
-
const port = opts.port ?? 8765;
|
|
939
|
-
const controllerHome = resolveControllerHome(opts.controllerHome);
|
|
940
|
-
const serviceConfig = loadMcpServiceLocalConfig(controllerHome);
|
|
941
|
-
const profile = opts.profile ?? serviceConfig?.profile ?? 'controller';
|
|
942
|
-
// A controller Gateway can serve registered repositories without selecting
|
|
943
|
-
// one at startup. Do not turn its launchd working directory into an
|
|
944
|
-
// implicit repository, especially for package Runtime snapshots.
|
|
945
|
-
const repoRoot = profile === 'controller' && !opts.repo?.trim()
|
|
946
|
-
? undefined
|
|
947
|
-
: resolveMcpRepoRoot(opts.repo ?? '.');
|
|
948
|
-
const authMode = parseMcpHttpAuthMode(opts.auth ?? serviceConfig?.auth?.mode);
|
|
949
|
-
const authToken = opts.authToken ?? readMcpServiceBearerToken(controllerHome, repoRoot);
|
|
950
|
-
const oauthPassphrase = authMode === 'oauth'
|
|
951
|
-
? readMcpServiceOAuthPassphrase(controllerHome, repoRoot)
|
|
952
|
-
: null;
|
|
953
|
-
const tokenStore = authMode === 'oauth'
|
|
954
|
-
? new McpOAuthTokenStore(
|
|
955
|
-
mcpServiceOAuthTokenStorePath(controllerHome),
|
|
956
|
-
mcpServiceOAuthTokenStoreFallbackPaths(controllerHome, repoRoot),
|
|
957
|
-
)
|
|
958
|
-
: null;
|
|
959
|
-
tokenStore?.load();
|
|
960
|
-
const oauthProvider = tokenStore ? createMcpOAuthProvider(tokenStore) : null;
|
|
961
|
-
const configuredPublicOrigin = getConfiguredPublicOrigin(serviceConfig);
|
|
962
|
-
const sessionRegistry = new McpSessionRegistry<StreamableHTTPServerTransport, McpToolContext>({
|
|
963
|
-
maximumSessions: MAX_MCP_SESSIONS,
|
|
964
|
-
maximumSessionsPerPrincipal: MAX_MCP_SESSIONS_PER_PRINCIPAL,
|
|
965
|
-
idleTtlMs: MCP_SESSION_IDLE_TTL_MS,
|
|
966
|
-
streamLeaseMs: MCP_STREAM_LEASE_MS,
|
|
967
|
-
absoluteLifetimeMs: MCP_SESSION_ABSOLUTE_LIFETIME_MS,
|
|
968
|
-
activePostStallMs: MCP_ACTIVE_POST_STALL_MS,
|
|
969
|
-
onSessionClosed: (session, reason) => {
|
|
970
|
-
const context = session.toolContext;
|
|
971
|
-
if (!('controllerHome' in context) || typeof context.controllerHome !== 'string') return;
|
|
972
|
-
const executionSessionId = typeof context.sessionId === 'string' ? context.sessionId.trim() : '';
|
|
973
|
-
if (!executionSessionId) return;
|
|
974
|
-
invalidateExecutionSession(
|
|
975
|
-
context.controllerHome,
|
|
976
|
-
executionSessionId,
|
|
977
|
-
`mcp_transport_${reason}`,
|
|
978
|
-
);
|
|
979
|
-
},
|
|
980
|
-
});
|
|
981
|
-
const runtimeStats: McpRuntimeStats = { initializing: 0, activePosts: 0, rejectedOverload: 0 };
|
|
982
|
-
const toolContext = createMcpToolContext({ ...opts, repo: repoRoot, controllerHome, profile });
|
|
983
|
-
const baseOptions: McpServerOptions = {
|
|
984
|
-
repo: repoRoot,
|
|
985
|
-
controllerHome,
|
|
986
|
-
profile,
|
|
987
|
-
toolset: opts.toolset,
|
|
988
|
-
enableChatgptBrowser: opts.enableChatgptBrowser,
|
|
989
|
-
enableDevRunner: opts.enableDevRunner,
|
|
990
|
-
devRunnerAgents: opts.devRunnerAgents,
|
|
991
|
-
devRunnerTimeoutMs: opts.devRunnerTimeoutMs,
|
|
992
|
-
devRunnerMaxTimeoutMs: opts.devRunnerMaxTimeoutMs,
|
|
993
|
-
};
|
|
994
|
-
const runtimeControllerHome = 'controllerHome' in toolContext ? toolContext.controllerHome : undefined;
|
|
995
|
-
const sharedRuntimeProxy = runtimeControllerHome && 'controllerHome' in toolContext && !toolContext.runtimeSourceRoot
|
|
996
|
-
? createCanonicalRuntimeProxy(toolContext)
|
|
997
|
-
: undefined;
|
|
998
|
-
const currentRuntimeGeneration = () => runtimeControllerHome ? readRuntimeGeneration(runtimeControllerHome) : undefined;
|
|
999
|
-
const currentRuntimeToolSurfaceFingerprint = () => runtimeControllerHome
|
|
1000
|
-
? readRuntimeStatusSnapshot(runtimeControllerHome)?.toolSurfaceFingerprint
|
|
1001
|
-
: undefined;
|
|
1002
|
-
let observedRuntimeToolSurfaceFingerprint = currentRuntimeToolSurfaceFingerprint();
|
|
1003
|
-
let toolSurfaceNotificationTimer: ReturnType<typeof setTimeout> | undefined;
|
|
1004
|
-
const runtimeStatusDirectory = runtimeControllerHome ? dirname(runtimeStatusPath(runtimeControllerHome)) : undefined;
|
|
1005
|
-
const runtimeStatusWatcher = runtimeStatusDirectory && existsSync(runtimeStatusDirectory)
|
|
1006
|
-
? watch(runtimeStatusDirectory, { persistent: false }, (_event, filename) => {
|
|
1007
|
-
if (String(filename ?? '') !== 'status.json') return;
|
|
1008
|
-
if (toolSurfaceNotificationTimer) clearTimeout(toolSurfaceNotificationTimer);
|
|
1009
|
-
toolSurfaceNotificationTimer = setTimeout(() => {
|
|
1010
|
-
const currentFingerprint = currentRuntimeToolSurfaceFingerprint();
|
|
1011
|
-
if (!currentFingerprint || currentFingerprint === observedRuntimeToolSurfaceFingerprint) return;
|
|
1012
|
-
observedRuntimeToolSurfaceFingerprint = currentFingerprint;
|
|
1013
|
-
void sessionRegistry.notifyToolListChanged(currentFingerprint);
|
|
1014
|
-
}, 25);
|
|
1015
|
-
toolSurfaceNotificationTimer.unref();
|
|
1016
|
-
})
|
|
1017
|
-
: undefined;
|
|
1018
|
-
runtimeStatusWatcher?.on('error', () => undefined);
|
|
1019
|
-
const resolveRuntimeSchema = runtimeControllerHome
|
|
1020
|
-
? async (context: McpToolContext): Promise<CanonicalRuntimeToolSchema | undefined> => {
|
|
1021
|
-
if (!('controllerHome' in context)) return undefined;
|
|
1022
|
-
return await readCanonicalRuntimeToolSchema(context, sharedRuntimeProxy);
|
|
1023
|
-
}
|
|
1024
|
-
: undefined;
|
|
1025
|
-
const localControllerConfig = {
|
|
1026
|
-
enabled: serviceConfig?.localController?.enabled ?? profile === 'controller',
|
|
1027
|
-
host: serviceConfig?.localController?.host ?? '127.0.0.1',
|
|
1028
|
-
port: serviceConfig?.localController?.port ?? 8766,
|
|
1029
|
-
};
|
|
1030
|
-
const compatibilityToolDefinitions = buildMcpToolDefinitions(toolContext.policy, { enableChatgptBrowser: opts.enableChatgptBrowser === true });
|
|
1031
|
-
const toolSurface = toolContext.policy.profile === 'controller' ? FORGE_TOOL_SURFACE : `${toolContext.policy.profile}-legacy-v1`;
|
|
1032
|
-
const toolSurfaceSchemaVersion = toolContext.policy.profile === 'controller' ? FORGE_MCP_SCHEMA_VERSION : 1;
|
|
1033
|
-
const forgeVersion = FORGE_VERSION;
|
|
1034
|
-
const repoId = toolContext.policy.profile === 'controller' || !repoRoot ? undefined : repositoryIdentity(repoRoot);
|
|
1035
|
-
const startedAt = new Date().toISOString();
|
|
1036
|
-
const localOrigin = `http://${host === '::' || host === '0.0.0.0' ? '127.0.0.1' : host}:${port}`;
|
|
1037
|
-
const advertisedOrigin = configuredPublicOrigin ?? localOrigin;
|
|
1038
|
-
const app = express();
|
|
1039
|
-
app.set('trust proxy', 1);
|
|
1040
|
-
|
|
1041
|
-
const controllerHealth = () => {
|
|
1042
|
-
if (!('controllerHome' in toolContext)) return null;
|
|
1043
|
-
const runtimeGeneration = currentRuntimeGeneration();
|
|
1044
|
-
const exposure = controllerExposureSnapshot(toolContext);
|
|
1045
|
-
// Health is a bounded diagnostic endpoint, not an MCP discovery authority.
|
|
1046
|
-
// Session initialization and invocation obtain a live Runtime tools/list;
|
|
1047
|
-
// health only reports the Runtime's published identity.
|
|
1048
|
-
const runtimeFingerprint = currentRuntimeToolSurfaceFingerprint();
|
|
1049
|
-
return {
|
|
1050
|
-
configuredAccessMode: exposure.access.configuredAccessMode,
|
|
1051
|
-
effectiveAccessMode: exposure.access.effectiveAccessMode,
|
|
1052
|
-
effectiveToolset: exposure.access.effectiveToolset,
|
|
1053
|
-
exposureRevision: exposure.access.exposureRevision,
|
|
1054
|
-
accessModeSource: exposure.access.source,
|
|
1055
|
-
accessModeLastAppliedAt: exposure.access.lastAppliedAt,
|
|
1056
|
-
toolset: exposure.access.effectiveToolset,
|
|
1057
|
-
toolSurfaceFingerprint: runtimeFingerprint,
|
|
1058
|
-
runtimeToolSurfaceFingerprint: runtimeFingerprint,
|
|
1059
|
-
toolCount: undefined,
|
|
1060
|
-
generation: runtimeGeneration?.generation,
|
|
1061
|
-
source: runtimeGeneration?.source,
|
|
1062
|
-
runtimeIdentity: runtimeIdentitySnapshot(toolContext),
|
|
1063
|
-
};
|
|
1064
|
-
};
|
|
1065
|
-
|
|
1066
|
-
app.get('/health', (_req, res) => {
|
|
1067
|
-
const health = controllerHealth();
|
|
1068
|
-
res.setHeader('x-forge-tool-surface', toolSurface);
|
|
1069
|
-
res.setHeader('x-forge-version', String(forgeVersion));
|
|
1070
|
-
res.setHeader('x-forge-schema-version', String(toolSurfaceSchemaVersion));
|
|
1071
|
-
if (health?.toolset) res.setHeader('x-forge-toolset', health.toolset);
|
|
1072
|
-
if (health?.runtimeToolSurfaceFingerprint) res.setHeader('x-forge-runtime-tool-surface-fingerprint', health.runtimeToolSurfaceFingerprint);
|
|
1073
|
-
if (health?.toolSurfaceFingerprint) res.setHeader('x-forge-tool-surface-fingerprint', health.toolSurfaceFingerprint);
|
|
1074
|
-
const sessionSnapshot = sessionRegistry.snapshot();
|
|
1075
|
-
res.json({
|
|
1076
|
-
status: 'ok',
|
|
1077
|
-
server: 'forge-mcp',
|
|
1078
|
-
...(process.env.FORGE_MCP_INSTANCE_ID
|
|
1079
|
-
? { instanceId: process.env.FORGE_MCP_INSTANCE_ID }
|
|
1080
|
-
: {}),
|
|
1081
|
-
version: forgeVersion,
|
|
1082
|
-
profile: toolContext.policy.profile,
|
|
1083
|
-
toolSurface,
|
|
1084
|
-
schemaVersion: toolSurfaceSchemaVersion,
|
|
1085
|
-
toolSurfaceFingerprint: health?.toolSurfaceFingerprint,
|
|
1086
|
-
runtimeToolSurfaceFingerprint: health?.runtimeToolSurfaceFingerprint,
|
|
1087
|
-
generation: health?.generation,
|
|
1088
|
-
source: health?.source,
|
|
1089
|
-
// Deprecated diagnostic alias; not a Runtime schema claim.
|
|
1090
|
-
toolset: health?.toolset ?? 'full',
|
|
1091
|
-
gatewayToolset: health?.toolset ?? 'full',
|
|
1092
|
-
toolCount: health?.toolCount,
|
|
1093
|
-
compatibilityToolCount: compatibilityToolDefinitions.length,
|
|
1094
|
-
runtimeIdentity: health?.runtimeIdentity,
|
|
1095
|
-
configuredAccessMode: health?.configuredAccessMode,
|
|
1096
|
-
effectiveAccessMode: health?.effectiveAccessMode,
|
|
1097
|
-
effectiveToolset: health?.effectiveToolset,
|
|
1098
|
-
accessModeSource: health?.accessModeSource,
|
|
1099
|
-
accessModeLastAppliedAt: health?.accessModeLastAppliedAt,
|
|
1100
|
-
exposureRevision: health?.exposureRevision,
|
|
1101
|
-
...(repoId ? { repoId } : {}),
|
|
1102
|
-
startedAt,
|
|
1103
|
-
runner: {
|
|
1104
|
-
enabled: toolContext.policy.execution.agentRunner,
|
|
1105
|
-
defaultTimeoutMs: toolContext.policy.execution.runnerTimeoutMs,
|
|
1106
|
-
maxTimeoutMs: toolContext.policy.execution.runnerMaxTimeoutMs,
|
|
1107
|
-
},
|
|
1108
|
-
auth: authMode === 'oauth'
|
|
1109
|
-
? (oauthPassphrase ? 'oauth' : 'missing')
|
|
1110
|
-
: authMode === 'bearer'
|
|
1111
|
-
? (authToken ? 'required' : 'missing')
|
|
1112
|
-
: 'none',
|
|
1113
|
-
mcpEndpoint: `${advertisedOrigin}/mcp`,
|
|
1114
|
-
// Grok now completes standard OAuth dynamic registration + PKCE on the
|
|
1115
|
-
// canonical MCP resource. Keep /mcp-grok below only as a legacy alias.
|
|
1116
|
-
grokEndpoint: `${advertisedOrigin}/mcp`,
|
|
1117
|
-
bearerEndpoint: `${advertisedOrigin}/mcp-bearer`,
|
|
1118
|
-
sessions: {
|
|
1119
|
-
...sessionSnapshot,
|
|
1120
|
-
initializing: runtimeStats.initializing,
|
|
1121
|
-
activePosts: runtimeStats.activePosts,
|
|
1122
|
-
maximumActivePosts: MAX_ACTIVE_POSTS,
|
|
1123
|
-
rejectedOverload: runtimeStats.rejectedOverload,
|
|
1124
|
-
},
|
|
1125
|
-
});
|
|
1126
|
-
});
|
|
1127
|
-
|
|
1128
|
-
// Recovery probes this endpoint every few seconds. Keep it strictly in-memory
|
|
1129
|
-
// and transport-scoped: whole-control-plane readiness below may traverse every
|
|
1130
|
-
// repository projection and probe the local bridge, which must never become a
|
|
1131
|
-
// periodic event-loop load generator for the public MCP Connector itself.
|
|
1132
|
-
app.get('/transport-ready', (_req, res) => {
|
|
1133
|
-
const sessionSnapshot = sessionRegistry.snapshot();
|
|
1134
|
-
const sessionCapacityReady = sessionSnapshot.acceptingNewSessions
|
|
1135
|
-
&& runtimeStats.initializing < MAX_INITIALIZING_SESSIONS
|
|
1136
|
-
&& runtimeStats.activePosts < MAX_ACTIVE_POSTS;
|
|
1137
|
-
res.status(sessionCapacityReady ? 200 : 503).json({
|
|
1138
|
-
ready: sessionCapacityReady,
|
|
1139
|
-
profile: toolContext.policy.profile,
|
|
1140
|
-
gateway: sessionCapacityReady ? 'ready' : 'saturated',
|
|
1141
|
-
sessionCapacity: sessionSnapshot,
|
|
1142
|
-
runtimeCapacity: {
|
|
1143
|
-
initializing: runtimeStats.initializing,
|
|
1144
|
-
maximumInitializing: MAX_INITIALIZING_SESSIONS,
|
|
1145
|
-
activePosts: runtimeStats.activePosts,
|
|
1146
|
-
maximumActivePosts: MAX_ACTIVE_POSTS,
|
|
1147
|
-
},
|
|
1148
|
-
});
|
|
1149
|
-
});
|
|
1150
|
-
|
|
1151
|
-
app.get('/ready', async (_req, res) => {
|
|
1152
|
-
const runtimeGeneration = currentRuntimeGeneration();
|
|
1153
|
-
const sessionSnapshot = sessionRegistry.snapshot();
|
|
1154
|
-
const sessionCapacityReady = sessionSnapshot.acceptingNewSessions
|
|
1155
|
-
&& runtimeStats.initializing < MAX_INITIALIZING_SESSIONS
|
|
1156
|
-
&& runtimeStats.activePosts < MAX_ACTIVE_POSTS;
|
|
1157
|
-
if (!runtimeControllerHome) {
|
|
1158
|
-
res.status(sessionCapacityReady ? 200 : 503).json({
|
|
1159
|
-
ready: sessionCapacityReady,
|
|
1160
|
-
profile: toolContext.policy.profile,
|
|
1161
|
-
gateway: sessionCapacityReady ? 'ready' : 'saturated',
|
|
1162
|
-
controllerDaemon: 'not-required',
|
|
1163
|
-
sessionCapacity: sessionSnapshot,
|
|
1164
|
-
});
|
|
1165
|
-
return;
|
|
1166
|
-
}
|
|
1167
|
-
const daemon = readForgeRuntimeStatus(runtimeControllerHome);
|
|
1168
|
-
const runtimeState = loadMcpServiceRuntimeState(runtimeControllerHome, repoRoot);
|
|
1169
|
-
const repositories = listRepositories(runtimeControllerHome).filter((repository) => repository.enabled && !repository.removedAt);
|
|
1170
|
-
const projectionSnapshots = repositories.map((repository) => {
|
|
1171
|
-
const snapshot = readRepositoryProjectionSnapshot(runtimeControllerHome, repository.repoId);
|
|
1172
|
-
const reconciliation = reconcileReadinessProjectionSource(
|
|
1173
|
-
snapshot,
|
|
1174
|
-
legacyIssueAuthorityRetired(repository.canonicalRoot)
|
|
1175
|
-
? undefined
|
|
1176
|
-
: buildControllerTaskLedgerProjection(repository.canonicalRoot),
|
|
1177
|
-
);
|
|
1178
|
-
return { repoId: repository.repoId, snapshot, reconciliation };
|
|
1179
|
-
});
|
|
1180
|
-
const staleRepositories = projectionSnapshots
|
|
1181
|
-
.filter(({ snapshot }) => snapshot.stale)
|
|
1182
|
-
.map(({ repoId }) => repoId);
|
|
1183
|
-
const blockingStaleRepositories = projectionSnapshots
|
|
1184
|
-
.filter(({ snapshot }) => projectionBlocksReadiness(snapshot))
|
|
1185
|
-
.map(({ repoId }) => repoId);
|
|
1186
|
-
const sourceMismatches = projectionSnapshots
|
|
1187
|
-
.filter(({ reconciliation }) => reconciliation.status === 'mismatch')
|
|
1188
|
-
.map(({ repoId, reconciliation }) => ({ repoId, ...reconciliation }));
|
|
1189
|
-
const localBridgeHealth = localControllerConfig.enabled
|
|
1190
|
-
? await jsonHealth(localControllerHealthUrl(localControllerConfig.host, localControllerConfig.port))
|
|
1191
|
-
: null;
|
|
1192
|
-
const localBridgeReady = !localControllerConfig.enabled
|
|
1193
|
-
|| localControllerDiagnosticMatchesRuntime(localBridgeHealth, runtimeGeneration?.generation);
|
|
1194
|
-
const daemonReady = daemon.status === 'ready' && daemon.degraded !== true;
|
|
1195
|
-
const projectionReady = blockingStaleRepositories.length === 0;
|
|
1196
|
-
const publicConfigured = Boolean(runtimeState?.tunnel?.publicEndpoint);
|
|
1197
|
-
const publicReady = !publicConfigured || runtimeState?.tunnel?.healthy === true;
|
|
1198
|
-
const connectorReady = !publicConfigured || (
|
|
1199
|
-
publicReady
|
|
1200
|
-
&& runtimeState?.tunnel?.connectorNeedsReconnect !== true
|
|
1201
|
-
);
|
|
1202
|
-
const ready = daemonReady && projectionReady && localBridgeReady && sessionCapacityReady;
|
|
1203
|
-
res.status(ready ? 200 : 503).json({
|
|
1204
|
-
ready,
|
|
1205
|
-
generation: runtimeGeneration?.generation,
|
|
1206
|
-
source: runtimeGeneration?.source,
|
|
1207
|
-
gateway: { status: ready ? 'ready' : 'degraded', thin: true, eventLoopIsolatedFromWorkers: true },
|
|
1208
|
-
controllerDaemon: daemon,
|
|
1209
|
-
localBridge: {
|
|
1210
|
-
enabled: localControllerConfig.enabled,
|
|
1211
|
-
ready: localBridgeReady,
|
|
1212
|
-
endpoint: `http://${localControllerConfig.host === '::1' ? '[::1]' : localControllerConfig.host}:${localControllerConfig.port}/`,
|
|
1213
|
-
},
|
|
1214
|
-
projections: {
|
|
1215
|
-
ready: projectionReady,
|
|
1216
|
-
repositoryCount: repositories.length,
|
|
1217
|
-
staleRepositories,
|
|
1218
|
-
blockingStaleRepositories,
|
|
1219
|
-
sourceMismatches,
|
|
1220
|
-
},
|
|
1221
|
-
publicReadiness: {
|
|
1222
|
-
configured: publicConfigured,
|
|
1223
|
-
ready: publicReady,
|
|
1224
|
-
endpoint: runtimeState?.tunnel?.publicEndpoint,
|
|
1225
|
-
},
|
|
1226
|
-
connectorReadiness: {
|
|
1227
|
-
configured: publicConfigured,
|
|
1228
|
-
ready: connectorReady,
|
|
1229
|
-
connectorNeedsReconnect: runtimeState?.tunnel?.connectorNeedsReconnect === true,
|
|
1230
|
-
},
|
|
1231
|
-
sessionCapacity: sessionSnapshot,
|
|
1232
|
-
});
|
|
1233
|
-
});
|
|
1234
|
-
|
|
1235
|
-
app.get('/repos/:repoId/health', (req, res) => {
|
|
1236
|
-
if (!runtimeControllerHome) {
|
|
1237
|
-
res.status(404).json({ error: 'controller profile required' });
|
|
1238
|
-
return;
|
|
1239
|
-
}
|
|
1240
|
-
try {
|
|
1241
|
-
const repository = getRepository(req.params.repoId, runtimeControllerHome, { includeRemoved: true });
|
|
1242
|
-
const projection = readRepositoryProjectionSnapshot(runtimeControllerHome, repository.repoId);
|
|
1243
|
-
res.json({
|
|
1244
|
-
status: repository.enabled && !repository.removedAt ? 'ok' : 'disabled',
|
|
1245
|
-
repository: {
|
|
1246
|
-
repoId: repository.repoId,
|
|
1247
|
-
checkoutId: repository.activeCheckoutId,
|
|
1248
|
-
enabled: repository.enabled,
|
|
1249
|
-
removedAt: repository.removedAt,
|
|
1250
|
-
},
|
|
1251
|
-
projection,
|
|
1252
|
-
});
|
|
1253
|
-
} catch (error) {
|
|
1254
|
-
res.status(404).json({ error: error instanceof Error ? error.message : String(error) });
|
|
1255
|
-
}
|
|
1256
|
-
});
|
|
1257
|
-
|
|
1258
|
-
if (authMode === 'oauth' && oauthProvider) {
|
|
1259
|
-
app.use('/authorize', express.urlencoded({ extended: false, limit: '10kb' }));
|
|
1260
|
-
app.use('/authorize', oauthTraceMiddleware('authorize'));
|
|
1261
|
-
// Reject incomplete OAuth requests before rendering the passphrase form.
|
|
1262
|
-
app.use('/authorize', rejectIncompleteOAuthAuthorize);
|
|
1263
|
-
app.use('/authorize', requirePassphrase(oauthPassphrase ?? ''));
|
|
1264
|
-
app.use('/authorize', oauthAuthorizationHandler(oauthProvider));
|
|
1265
|
-
app.use('/token', oauthTraceMiddleware('token'));
|
|
1266
|
-
app.use('/token', tokenHandler({ provider: oauthProvider, rateLimit: false }));
|
|
1267
|
-
app.use('/revoke', oauthTraceMiddleware('revoke'));
|
|
1268
|
-
app.use('/revoke', revocationHandler({ provider: oauthProvider, rateLimit: false }));
|
|
1269
|
-
app.use('/register', oauthTraceMiddleware('register'));
|
|
1270
|
-
app.use('/register', clientRegistrationHandler({ clientsStore: oauthProvider.clientsStore, rateLimit: false }));
|
|
1271
|
-
app.get('/.well-known/oauth-authorization-server', (req, res) => {
|
|
1272
|
-
const origin = getPublicOrigin(req, configuredPublicOrigin);
|
|
1273
|
-
res.json({
|
|
1274
|
-
issuer: origin,
|
|
1275
|
-
authorization_endpoint: `${origin}/authorize`,
|
|
1276
|
-
token_endpoint: `${origin}/token`,
|
|
1277
|
-
revocation_endpoint: `${origin}/revoke`,
|
|
1278
|
-
registration_endpoint: `${origin}/register`,
|
|
1279
|
-
response_types_supported: ['code'],
|
|
1280
|
-
grant_types_supported: ['authorization_code', 'refresh_token'],
|
|
1281
|
-
code_challenge_methods_supported: ['S256'],
|
|
1282
|
-
token_endpoint_auth_methods_supported: ['client_secret_post', 'none'],
|
|
1283
|
-
scopes_supported: ['forge'],
|
|
1284
|
-
});
|
|
1285
|
-
});
|
|
1286
|
-
app.get('/.well-known/openid-configuration', (req, res) => {
|
|
1287
|
-
const origin = getPublicOrigin(req, configuredPublicOrigin);
|
|
1288
|
-
res.json({
|
|
1289
|
-
issuer: origin,
|
|
1290
|
-
authorization_endpoint: `${origin}/authorize`,
|
|
1291
|
-
token_endpoint: `${origin}/token`,
|
|
1292
|
-
registration_endpoint: `${origin}/register`,
|
|
1293
|
-
response_types_supported: ['code'],
|
|
1294
|
-
grant_types_supported: ['authorization_code', 'refresh_token'],
|
|
1295
|
-
code_challenge_methods_supported: ['S256'],
|
|
1296
|
-
token_endpoint_auth_methods_supported: ['client_secret_post', 'none'],
|
|
1297
|
-
scopes_supported: ['forge'],
|
|
1298
|
-
});
|
|
1299
|
-
});
|
|
1300
|
-
const protectedResourceMetadata = (resourcePath: '/mcp' | '/mcp-grok' | '/mcp-bearer') => (req: Request, res: Response): void => {
|
|
1301
|
-
const origin = getPublicOrigin(req, configuredPublicOrigin);
|
|
1302
|
-
res.json({
|
|
1303
|
-
resource: `${origin}${resourcePath}`,
|
|
1304
|
-
authorization_servers: [origin],
|
|
1305
|
-
scopes_supported: ['forge'],
|
|
1306
|
-
bearer_methods_supported: ['header'],
|
|
1307
|
-
});
|
|
1308
|
-
};
|
|
1309
|
-
app.get('/.well-known/oauth-protected-resource/mcp', protectedResourceMetadata('/mcp'));
|
|
1310
|
-
app.get('/.well-known/oauth-protected-resource/mcp-grok', protectedResourceMetadata('/mcp-grok'));
|
|
1311
|
-
app.get('/.well-known/oauth-protected-resource/mcp-bearer', protectedResourceMetadata('/mcp-bearer'));
|
|
1312
|
-
}
|
|
1313
|
-
|
|
1314
|
-
const setMcpResponseHeaders = (_req: Request, res: Response, next: NextFunction): void => {
|
|
1315
|
-
res.setHeader('x-forge-tool-surface', toolSurface);
|
|
1316
|
-
res.setHeader('x-forge-version', String(forgeVersion));
|
|
1317
|
-
res.setHeader('x-forge-schema-version', String(toolSurfaceSchemaVersion));
|
|
1318
|
-
next();
|
|
1319
|
-
};
|
|
1320
|
-
|
|
1321
|
-
// Primary MCP path: OAuth (or bearer when --auth bearer). Unchanged for ChatGPT.
|
|
1322
|
-
app.use('/mcp', setMcpResponseHeaders);
|
|
1323
|
-
app.post('/mcp', requireMcpHttpAuth(authMode, authToken, oauthProvider, configuredPublicOrigin), express.raw({ type: '*/*', limit: '1mb' }), (req, res) => {
|
|
1324
|
-
handleMcpPost(req, res, baseOptions, sessionRegistry, runtimeStats, '/mcp', currentRuntimeToolSurfaceFingerprint, resolveRuntimeSchema, sharedRuntimeProxy).catch((error: unknown) => {
|
|
1325
|
-
if (!res.headersSent) sendMcpRequestError(res, error);
|
|
1326
|
-
});
|
|
1327
|
-
});
|
|
1328
|
-
app.get('/mcp', requireMcpHttpAuth(authMode, authToken, oauthProvider, configuredPublicOrigin), (req, res) => {
|
|
1329
|
-
handleMcpGet(req, res, sessionRegistry, '/mcp', currentRuntimeToolSurfaceFingerprint, resolveRuntimeSchema).catch((error: unknown) => {
|
|
1330
|
-
if (!res.headersSent) sendMcpRequestError(res, error);
|
|
1331
|
-
});
|
|
1332
|
-
});
|
|
1333
|
-
app.delete('/mcp', requireMcpHttpAuth(authMode, authToken, oauthProvider, configuredPublicOrigin), (req, res) => {
|
|
1334
|
-
handleMcpDelete(req, res, sessionRegistry, '/mcp').catch((error: unknown) => {
|
|
1335
|
-
if (!res.headersSent) sendMcpRequestError(res, error);
|
|
1336
|
-
});
|
|
1337
|
-
});
|
|
1338
|
-
|
|
1339
|
-
// Legacy Grok OAuth resource. New Grok connectors should use canonical /mcp.
|
|
1340
|
-
app.use('/mcp-grok', setMcpResponseHeaders);
|
|
1341
|
-
app.post('/mcp-grok', requireMcpHttpAuth(authMode, authToken, oauthProvider, configuredPublicOrigin, '/mcp-grok'), express.raw({ type: '*/*', limit: '1mb' }), (req, res) => {
|
|
1342
|
-
handleMcpPost(req, res, baseOptions, sessionRegistry, runtimeStats, '/mcp-grok', currentRuntimeToolSurfaceFingerprint, resolveRuntimeSchema, sharedRuntimeProxy).catch((error: unknown) => {
|
|
1343
|
-
if (!res.headersSent) sendMcpRequestError(res, error);
|
|
1344
|
-
});
|
|
1345
|
-
});
|
|
1346
|
-
app.get('/mcp-grok', requireMcpHttpAuth(authMode, authToken, oauthProvider, configuredPublicOrigin, '/mcp-grok'), (req, res) => {
|
|
1347
|
-
handleMcpGet(req, res, sessionRegistry, '/mcp-grok', currentRuntimeToolSurfaceFingerprint, resolveRuntimeSchema).catch((error: unknown) => {
|
|
1348
|
-
if (!res.headersSent) sendMcpRequestError(res, error);
|
|
1349
|
-
});
|
|
1350
|
-
});
|
|
1351
|
-
app.delete('/mcp-grok', requireMcpHttpAuth(authMode, authToken, oauthProvider, configuredPublicOrigin, '/mcp-grok'), (req, res) => {
|
|
1352
|
-
handleMcpDelete(req, res, sessionRegistry, '/mcp-grok').catch((error: unknown) => {
|
|
1353
|
-
if (!res.headersSent) sendMcpRequestError(res, error);
|
|
1354
|
-
});
|
|
1355
|
-
});
|
|
1356
|
-
|
|
1357
|
-
// Bearer-only MCP path for clients that can send Authorization headers. Never advertises OAuth resource_metadata.
|
|
1358
|
-
app.use('/mcp-bearer', setMcpResponseHeaders);
|
|
1359
|
-
app.post('/mcp-bearer', requireMcpHttpAuth('bearer', authToken, null, configuredPublicOrigin), express.raw({ type: '*/*', limit: '1mb' }), (req, res) => {
|
|
1360
|
-
handleMcpPost(req, res, baseOptions, sessionRegistry, runtimeStats, '/mcp-bearer', currentRuntimeToolSurfaceFingerprint, resolveRuntimeSchema, sharedRuntimeProxy).catch((error: unknown) => {
|
|
1361
|
-
if (!res.headersSent) sendMcpRequestError(res, error);
|
|
1362
|
-
});
|
|
1363
|
-
});
|
|
1364
|
-
app.get('/mcp-bearer', requireMcpHttpAuth('bearer', authToken, null, configuredPublicOrigin), (req, res) => {
|
|
1365
|
-
handleMcpGet(req, res, sessionRegistry, '/mcp-bearer', currentRuntimeToolSurfaceFingerprint, resolveRuntimeSchema).catch((error: unknown) => {
|
|
1366
|
-
if (!res.headersSent) sendMcpRequestError(res, error);
|
|
1367
|
-
});
|
|
1368
|
-
});
|
|
1369
|
-
app.delete('/mcp-bearer', requireMcpHttpAuth('bearer', authToken, null, configuredPublicOrigin), (req, res) => {
|
|
1370
|
-
handleMcpDelete(req, res, sessionRegistry, '/mcp-bearer').catch((error: unknown) => {
|
|
1371
|
-
if (!res.headersSent) sendMcpRequestError(res, error);
|
|
1372
|
-
});
|
|
1373
|
-
});
|
|
1374
|
-
|
|
1375
|
-
app.use((_req, res) => res.status(404).json({ error: 'not_found' }));
|
|
1376
|
-
|
|
1377
|
-
const cleanupTimer = setInterval(() => {
|
|
1378
|
-
void sessionRegistry.prune();
|
|
1379
|
-
}, 60_000);
|
|
1380
|
-
cleanupTimer.unref();
|
|
1381
|
-
|
|
1382
|
-
const httpServer = app.listen(port, host);
|
|
1383
|
-
httpServer.keepAliveTimeout = 65_000;
|
|
1384
|
-
httpServer.headersTimeout = 70_000;
|
|
1385
|
-
httpServer.requestTimeout = 120_000;
|
|
1386
|
-
|
|
1387
|
-
httpServer.on('close', () => {
|
|
1388
|
-
clearInterval(cleanupTimer);
|
|
1389
|
-
if (toolSurfaceNotificationTimer) clearTimeout(toolSurfaceNotificationTimer);
|
|
1390
|
-
runtimeStatusWatcher?.close();
|
|
1391
|
-
void sessionRegistry.closeAll('shutdown');
|
|
1392
|
-
void sharedRuntimeProxy?.close();
|
|
1393
|
-
});
|
|
1394
|
-
|
|
1395
|
-
await new Promise<void>((resolve) => {
|
|
1396
|
-
httpServer.once('listening', resolve);
|
|
1397
|
-
});
|
|
1398
|
-
const authLabel = authMode === 'oauth'
|
|
1399
|
-
? (oauthPassphrase ? 'oauth' : 'oauth-missing')
|
|
1400
|
-
: authMode === 'bearer'
|
|
1401
|
-
? (authToken ? 'bearer' : 'missing')
|
|
1402
|
-
: 'none';
|
|
1403
|
-
console.error(
|
|
1404
|
-
`forge mcp http listening on http://${host}:${port}/mcp (auth: ${authLabel}), http://${host}:${port}/mcp-grok (auth: ${authLabel}), and http://${host}:${port}/mcp-bearer (auth: bearer)`,
|
|
1405
|
-
);
|
|
1406
|
-
|
|
1407
|
-
const shutdown = () => {
|
|
1408
|
-
void sessionRegistry.closeAll('shutdown');
|
|
1409
|
-
tokenStore?.flush();
|
|
1410
|
-
httpServer.close(() => process.exit(0));
|
|
1411
|
-
};
|
|
1412
|
-
process.once('SIGINT', shutdown);
|
|
1413
|
-
process.once('SIGTERM', shutdown);
|
|
1414
|
-
}
|
|
1
|
+
/** @deprecated Kernel V2 compatibility shim. MCP HTTP transport lives in adapters/mcp. */
|
|
2
|
+
export * from '../../../../adapters/mcp/transports/http';
|