@moretea-labs/forge 1.8.0 → 1.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (528) hide show
  1. package/AGENTS.md +70 -7
  2. package/CHANGELOG.md +14 -0
  3. package/CLAUDE.md +17 -2
  4. package/CONTRIBUTING.md +3 -2
  5. package/THIRD_PARTY_NOTICES.md +1 -1
  6. package/adapters/chatgpt/browser-delivery-host.ts +7 -1
  7. package/adapters/chatgpt/browser-delivery-runtime.ts +529 -56
  8. package/adapters/chatgpt/controller-binding-store.ts +101 -22
  9. package/adapters/chatgpt/controller-round-host.ts +15 -25
  10. package/adapters/chatgpt/controller-round-settlement-store.ts +58 -4
  11. package/adapters/chatgpt/provider-delivery.ts +228 -1
  12. package/adapters/chatgpt/work-conversation-binding-store.ts +79 -53
  13. package/adapters/computer/chatgpt-extension-target.ts +463 -0
  14. package/adapters/computer/chatgpt-target-router.ts +103 -0
  15. package/adapters/computer/chrome-extension/background.js +385 -0
  16. package/adapters/computer/chrome-extension/content.js +378 -0
  17. package/adapters/computer/chrome-extension/core.js +24 -0
  18. package/adapters/computer/chrome-extension/manifest.json +30 -0
  19. package/adapters/computer/desktop-operator-discovery.ts +11 -31
  20. package/adapters/computer/interaction-target-authority.ts +142 -18
  21. package/adapters/mcp/auth.ts +0 -1
  22. package/adapters/mcp/instructions.ts +5 -4
  23. package/adapters/mcp/multi-repository.ts +3 -19
  24. package/adapters/mcp/oauth.ts +73 -0
  25. package/adapters/mcp/paths.ts +4 -1
  26. package/adapters/mcp/runtime-gateway/automation-receipt-adapter.ts +87 -0
  27. package/adapters/mcp/runtime-gateway/bounded-result-runtime.ts +62 -0
  28. package/adapters/mcp/runtime-gateway/cognition-settlement.ts +201 -0
  29. package/adapters/mcp/runtime-gateway/context-adapter.ts +411 -73
  30. package/adapters/mcp/runtime-gateway/context-pack-sidecar.ts +91 -0
  31. package/adapters/mcp/runtime-gateway/controller-authority-adapter.ts +20 -5
  32. package/adapters/mcp/runtime-gateway/core-capability-adapter.ts +258 -0
  33. package/adapters/mcp/runtime-gateway/engineering-preconditions.ts +10 -27
  34. package/adapters/mcp/runtime-gateway/engineering-tool-contract.ts +15 -0
  35. package/adapters/mcp/runtime-gateway/execution-tools.ts +8 -42
  36. package/adapters/mcp/runtime-gateway/gateway-execution-adapter.ts +2 -1
  37. package/adapters/mcp/runtime-gateway/plugin-adapter.ts +117 -45
  38. package/adapters/mcp/runtime-gateway/process-tools.ts +190 -22
  39. package/adapters/mcp/runtime-gateway/protected-computer-adapter.ts +16 -4
  40. package/adapters/mcp/runtime-gateway/recovery-adapter.ts +69 -52
  41. package/adapters/mcp/runtime-gateway/repository-compat-adapter.ts +4 -35
  42. package/adapters/mcp/runtime-gateway/result-adapter.ts +10 -4
  43. package/adapters/mcp/runtime-gateway/router.ts +18 -504
  44. package/adapters/mcp/runtime-gateway/routing-policy.ts +499 -0
  45. package/adapters/mcp/runtime-gateway/runtime-observation-adapter.ts +14 -65
  46. package/adapters/mcp/runtime-gateway/runtime-readiness-observation.ts +218 -0
  47. package/adapters/mcp/runtime-gateway/runtime-tool-definitions.ts +113 -173
  48. package/adapters/mcp/runtime-gateway/runtime-tools.ts +15 -5
  49. package/adapters/mcp/runtime-gateway/scheduler-adapter.ts +1 -0
  50. package/adapters/mcp/runtime-gateway/shared-adapter.ts +20 -0
  51. package/adapters/mcp/runtime-gateway/status-inbox-adapter.ts +358 -266
  52. package/adapters/mcp/runtime-gateway/typescript-navigation-sidecar.ts +136 -0
  53. package/adapters/mcp/runtime-gateway/work-adapter.ts +261 -3122
  54. package/adapters/mcp/runtime-gateway/work-controller-operations.ts +257 -0
  55. package/adapters/mcp/runtime-gateway/work-detail-projection.ts +60 -0
  56. package/adapters/mcp/runtime-gateway/work-input-wire-migration.ts +246 -0
  57. package/adapters/mcp/runtime-gateway/work-learning-operations.ts +145 -0
  58. package/adapters/mcp/runtime-gateway/work-list-projection.ts +38 -0
  59. package/adapters/mcp/runtime-gateway/work-plan-operations.ts +382 -0
  60. package/adapters/mcp/runtime-gateway/work-repair-adapter.ts +308 -0
  61. package/adapters/mcp/runtime-gateway/work-requirement-operations.ts +148 -0
  62. package/adapters/mcp/runtime-gateway/work-semantic-operations.ts +263 -0
  63. package/adapters/mcp/runtime-gateway/work-workflow-operations.ts +113 -0
  64. package/adapters/mcp/runtime-gateway/workflow-supervisor-adapter.ts +156 -0
  65. package/adapters/mcp/server.ts +67 -23
  66. package/adapters/mcp/setup.ts +1 -3
  67. package/adapters/mcp/tool-mapping/legacy-tool-service.ts +44 -170
  68. package/adapters/mcp/tool-mapping/post-finalize-work-attribution.ts +4 -9
  69. package/adapters/mcp/tool-mapping/repository-tools.ts +133 -142
  70. package/adapters/mcp/toolset-names.ts +5 -0
  71. package/adapters/mcp/toolset.ts +6 -5
  72. package/adapters/mcp/transports/http-observation.ts +365 -0
  73. package/adapters/mcp/transports/http.ts +154 -677
  74. package/adapters/mcp/transports/oauth-http.ts +322 -0
  75. package/adapters/mcp/transports/session-registry.ts +6 -0
  76. package/adapters/mcp/tunnels/openai-secure-tunnel.ts +89 -5
  77. package/assets/plugin-registry.v1.json +6 -4
  78. package/assets/reference-configs/agentic-development-flow.md +25 -0
  79. package/assets/reference-configs/global-working-rules.md +19 -0
  80. package/assets/skill-version.json +2 -2
  81. package/docs/architecture/CURRENT.md +111 -41
  82. package/docs/architecture/decisions/20260803-release-identity-binding-and-exit-policy.md +14 -5
  83. package/docs/architecture/decisions/20260823-immediate-controller-relay.md +6 -3
  84. package/docs/architecture/decisions/20260907-assistant-learning-loop.md +2 -0
  85. package/docs/architecture/decisions/20260907-controller-round-transition-policy.md +19 -4
  86. package/docs/architecture/decisions/20260916-workflow-supervisor-authority.md +249 -0
  87. package/docs/architecture/decisions/20260917-cognitive-memory-foundation.md +82 -0
  88. package/docs/architecture/decisions/20260918-stable-candidate-release-lifecycle.md +221 -0
  89. package/docs/architecture/decisions/20260919-autonomous-work-liveness.md +62 -0
  90. package/docs/architecture/decisions/20260924-thin-semantic-working-context.md +72 -0
  91. package/docs/architecture/decisions/20261001-supervisor-turn-authority.md +237 -0
  92. package/docs/architecture/decisions/20261005-forge-v3-desktop-client-boundary.md +77 -0
  93. package/docs/architecture/domains/verification.md +1 -0
  94. package/docs/architecture/domains/workflow-engine.md +6 -0
  95. package/docs/architecture/modules/controller-runtime/transport-lifecycle.md +1 -1
  96. package/docs/forge-plugin-management.md +2 -2
  97. package/docs/operations/controller-performance-and-502.md +17 -0
  98. package/docs/operations/controller-reliability-runbook.md +2 -2
  99. package/docs/operations/features.md +1 -1
  100. package/docs/operations/features.zh-CN.md +1 -1
  101. package/docs/operations/homebrew.md +2 -2
  102. package/docs/operations/mcp-tool-exposure.md +2 -0
  103. package/docs/operations/releasing.md +2 -0
  104. package/docs/operations/standalone-disaster-recovery.md +14 -6
  105. package/docs/wiki/Implementation.md +1 -1
  106. package/package.json +25 -14
  107. package/packages/kernel/cognition/api/index.ts +6 -0
  108. package/packages/kernel/cognition/application/activation-service.ts +451 -0
  109. package/packages/kernel/cognition/application/consolidation-service.ts +185 -0
  110. package/packages/kernel/cognition/application/legacy-experience-adapter.ts +38 -0
  111. package/packages/kernel/cognition/application/memory-service.ts +64 -0
  112. package/packages/kernel/cognition/domain/learning.ts +106 -0
  113. package/packages/kernel/cognition/domain/memory.ts +254 -0
  114. package/packages/kernel/controller/api/index.ts +1 -0
  115. package/packages/kernel/controller/application/continuation-service.ts +281 -0
  116. package/packages/kernel/controller/application/controller-round-service.ts +12 -0
  117. package/packages/kernel/controller/domain/controller-round-transition-policy.ts +242 -21
  118. package/packages/kernel/controller/domain/controller-round.ts +4 -1
  119. package/packages/kernel/controller/domain/execution-quality.ts +3 -1
  120. package/packages/kernel/controller/infrastructure/controller-round-store.ts +689 -103
  121. package/packages/kernel/controller/infrastructure/controller-session-store.ts +297 -71
  122. package/packages/kernel/controller/ports/controller-host.ts +2 -0
  123. package/packages/kernel/identity/api/index.ts +10 -0
  124. package/packages/kernel/identity/domain/activity-projection.ts +43 -0
  125. package/packages/kernel/identity/domain/capability-broker.ts +60 -0
  126. package/packages/kernel/identity/domain/grant.ts +72 -0
  127. package/packages/kernel/identity/domain/owned-resource.ts +116 -0
  128. package/packages/kernel/identity/domain/user-request.ts +93 -0
  129. package/packages/kernel/identity/infrastructure/activity-projection-store.ts +77 -0
  130. package/packages/kernel/identity/infrastructure/capability-broker.ts +81 -0
  131. package/packages/kernel/identity/infrastructure/grant-store.ts +284 -0
  132. package/packages/kernel/identity/infrastructure/owned-resource-store.ts +259 -0
  133. package/packages/kernel/identity/infrastructure/user-request-store.ts +182 -0
  134. package/packages/kernel/scheduler/api/index.ts +0 -2
  135. package/packages/kernel/scheduler/application/schedule-service.ts +3 -0
  136. package/packages/kernel/scheduler/application/settlement.ts +0 -15
  137. package/packages/kernel/scheduler/domain/schedule.ts +87 -0
  138. package/packages/kernel/scheduler/infrastructure/schedule-store.ts +153 -133
  139. package/packages/kernel/work/api/index.ts +3 -2
  140. package/packages/kernel/work/application/work-service.ts +17 -42
  141. package/packages/kernel/work/domain/current-task-lineage.ts +104 -0
  142. package/packages/kernel/work/domain/engineering-design.ts +10 -5
  143. package/packages/kernel/work/domain/engineering-profile.ts +8 -12
  144. package/packages/kernel/work/domain/evaluation-promotion-receipt.ts +186 -0
  145. package/packages/kernel/work/domain/execution-concurrency.ts +19 -26
  146. package/packages/kernel/work/domain/execution-snapshot.ts +0 -31
  147. package/packages/kernel/work/domain/implementation-review.ts +14 -331
  148. package/packages/kernel/work/domain/objective-graph.ts +119 -0
  149. package/packages/kernel/work/domain/project-engineering-contract.ts +23 -1
  150. package/packages/kernel/work/domain/repository-completion-receipt.ts +1 -1
  151. package/packages/kernel/work/domain/state-machine.ts +25 -63
  152. package/packages/kernel/work/domain/types.ts +57 -51
  153. package/packages/kernel/work/infrastructure/work-contract-store.ts +819 -728
  154. package/packages/kernel/work/ports/work-contract-store.ts +5 -2
  155. package/packages/plugin-runtime/browser/session-authority.ts +2 -1
  156. package/packages/plugin-runtime/computer/chatgpt-identity.ts +24 -0
  157. package/packages/plugin-runtime/computer/conversation-target.ts +143 -0
  158. package/packages/plugin-runtime/computer/index.ts +2 -0
  159. package/packages/plugin-runtime/computer/target-authority.ts +26 -1
  160. package/packages/plugin-runtime/external/unix-jsonl-transport.ts +142 -18
  161. package/packages/protocols/computer/contract.ts +27 -1
  162. package/scripts/activate-source-baseline.ts +7 -9
  163. package/scripts/canary-controller-semantic-wait.ts +12 -24
  164. package/scripts/check-mcp-compatibility.ts +48 -136
  165. package/scripts/check-runtime-architecture.mjs +622 -217
  166. package/scripts/forge-local-recovery-helper.mjs +93 -15
  167. package/scripts/lib/project-init-lib.sh +3 -0
  168. package/scripts/open-source-audit-allowlist.txt +1 -2
  169. package/scripts/run-bounded-command.mjs +5 -0
  170. package/scripts/run-governed-gate.ts +0 -1
  171. package/scripts/run-mobile-monitor-bridge.ts +0 -1
  172. package/scripts/smoke-control-plane-failure-domain.ts +94 -0
  173. package/scripts/smoke-mcp-http-runtime.ts +5 -0
  174. package/scripts/smoke-runtime-recovery.ts +12 -43
  175. package/scripts/smoke-schedule-engine.ts +23 -15
  176. package/scripts/smoke-unattended-transport-recovery.ts +223 -0
  177. package/scripts/smoke-user-request-continuation-restart.ts +232 -0
  178. package/scripts/smoke-workflow-supervisor-chrome-adapter.ts +92 -0
  179. package/scripts/smoke-workflow-supervisor-process-restart.ts +276 -0
  180. package/scripts/smoke-workflow-supervisor-restart-recovery.ts +150 -0
  181. package/scripts/smoke-workflow-supervisor.ts +103 -0
  182. package/scripts/verify-forge-runtime.sh +2 -1
  183. package/src/cli/agent-jobs/executor-health.ts +11 -34
  184. package/src/cli/agent-jobs/integration.ts +18 -49
  185. package/src/cli/agent-jobs/job-manager.ts +7 -3
  186. package/src/cli/commands/brain-assistant.ts +27 -13
  187. package/src/cli/commands/brain.ts +13 -15
  188. package/src/cli/commands/chatgpt.ts +1 -163
  189. package/src/cli/commands/recovery.ts +135 -121
  190. package/src/cli/commands/runtime.ts +3 -3
  191. package/src/cli/commands/setup-profile.ts +8 -0
  192. package/src/cli/controller/check-runner.ts +13 -2
  193. package/src/cli/controller/completion-backlog.ts +3 -3
  194. package/src/cli/controller/completion-orchestrator.ts +4 -43
  195. package/src/cli/controller/completion-target.ts +3 -72
  196. package/src/cli/controller/context/source-materializer.ts +81 -100
  197. package/src/cli/controller/context-pack.ts +10 -8
  198. package/src/cli/controller/execution-completion.ts +4 -7
  199. package/src/cli/controller/execution-policy.ts +36 -153
  200. package/src/cli/controller/governance.ts +2 -3
  201. package/src/cli/controller/issue-store.ts +4 -99
  202. package/src/cli/controller/launch-agents.ts +59 -18
  203. package/src/cli/controller/operational-plan.ts +24 -51
  204. package/src/cli/controller/runtime-config.ts +5 -1
  205. package/src/cli/controller/stuck-state-migration.ts +2 -3
  206. package/src/cli/controller/systemd-user.ts +39 -1
  207. package/src/cli/editing/edit-session.ts +1 -7
  208. package/src/cli/local-bridge/console-automations.ts +18 -13
  209. package/src/cli/local-bridge/console-view-models.ts +3 -12
  210. package/src/cli/local-bridge/facade-api.ts +64 -191
  211. package/src/cli/local-bridge/server.ts +24 -147
  212. package/src/cli/local-bridge/types.ts +0 -1
  213. package/src/cli/mcp/oauth-client-retirement.ts +74 -0
  214. package/src/cli/repositories/command-executor.ts +29 -23
  215. package/src/cli/repositories/command-scope.ts +35 -16
  216. package/src/cli/repositories/controller-home.ts +48 -0
  217. package/src/cli/repositories/ephemeral-workspace.ts +20 -30
  218. package/src/cli/repositories/git-command-executor.ts +2 -2
  219. package/src/cli/repositories/registry.ts +77 -11
  220. package/src/cli/repositories/structured-git.ts +62 -2
  221. package/src/cli/repository/session-cache.ts +92 -6
  222. package/src/runtime/context/assistant-context.ts +20 -7
  223. package/src/runtime/context/assistant-work-context.ts +89 -27
  224. package/src/runtime/context/automatic-learning.ts +565 -0
  225. package/src/runtime/context/cognitive-attention.ts +173 -0
  226. package/src/runtime/context/cognitive-skill-canary.ts +158 -0
  227. package/src/runtime/context/context-closure.ts +46 -9
  228. package/src/runtime/context/context-pack-process.ts +103 -0
  229. package/src/runtime/context/context-plane.ts +1 -9
  230. package/src/runtime/context/direct-controller-learning.ts +298 -0
  231. package/src/runtime/context/semantic-navigation-contract.ts +9 -0
  232. package/src/runtime/context/semantic-navigation.ts +6 -34
  233. package/src/runtime/context/typescript-navigation-process.ts +145 -0
  234. package/src/runtime/context/typescript-navigation.ts +89 -3
  235. package/src/runtime/control-plane/cleanup-artifact-retention.ts +212 -9
  236. package/src/runtime/control-plane/concurrency/work-execution-concurrency.ts +9 -47
  237. package/src/runtime/control-plane/execution/controller-authority-recovery.ts +34 -4
  238. package/src/runtime/control-plane/execution/edit-validation-coordinator.ts +29 -20
  239. package/src/runtime/control-plane/execution/execution-identity.ts +23 -3
  240. package/src/runtime/control-plane/execution/repository-work-attribution.ts +7 -30
  241. package/src/runtime/control-plane/execution/retained-work-resume.ts +206 -199
  242. package/src/runtime/control-plane/execution/session-store.ts +31 -1
  243. package/src/runtime/control-plane/execution/validation.ts +6 -8
  244. package/src/runtime/control-plane/execution/work-authority-reconciler.ts +13 -42
  245. package/src/runtime/control-plane/execution/work-check-evidence.ts +137 -0
  246. package/src/runtime/control-plane/execution/work-completion-authority.ts +30 -85
  247. package/src/runtime/control-plane/execution/work-delivery-service.ts +497 -0
  248. package/src/runtime/control-plane/execution/work-evidence-policy.ts +12 -50
  249. package/src/runtime/control-plane/execution/work-execution-support.ts +50 -149
  250. package/src/runtime/control-plane/execution/work-handle-authority.ts +188 -109
  251. package/src/runtime/control-plane/execution/work-handle-store.ts +19 -43
  252. package/src/runtime/control-plane/execution/work-head-settlement.ts +53 -20
  253. package/src/runtime/control-plane/execution/work-lifecycle-audit.ts +52 -29
  254. package/src/runtime/control-plane/execution/work-operation-service.ts +43 -27
  255. package/src/runtime/control-plane/execution/work-preparation-service.ts +126 -266
  256. package/src/runtime/control-plane/execution/work-remote-delivery.ts +2 -3
  257. package/src/runtime/control-plane/execution/work-revision-diff.ts +39 -0
  258. package/src/runtime/control-plane/execution/work-terminal-cleanup.ts +173 -186
  259. package/src/runtime/control-plane/execution/work-validation-reconciler.ts +28 -84
  260. package/src/runtime/control-plane/execution/work-verification-service.ts +406 -50
  261. package/src/runtime/control-plane/execution/work-verification-snapshot.ts +5 -1
  262. package/src/runtime/control-plane/facade/capability-registry.ts +362 -37
  263. package/src/runtime/control-plane/facade/check-normalization.ts +3 -9
  264. package/src/runtime/control-plane/facade/codex-delegation.ts +10 -11
  265. package/src/runtime/control-plane/facade/controller-connection.ts +135 -0
  266. package/src/runtime/control-plane/facade/goal-workloop-access.ts +1 -20
  267. package/src/runtime/control-plane/facade/goal-workloop.ts +281 -1753
  268. package/src/runtime/control-plane/facade/handoff-inbox-application.ts +12 -25
  269. package/src/runtime/control-plane/facade/handoff-inbox-store.ts +256 -52
  270. package/src/runtime/control-plane/facade/index.ts +0 -1
  271. package/src/runtime/control-plane/facade/operation-digest.ts +12 -15
  272. package/src/runtime/control-plane/facade/plan-contract-store.ts +451 -991
  273. package/src/runtime/control-plane/facade/policy-gate.ts +0 -23
  274. package/src/runtime/control-plane/facade/repository-work-admission.ts +12 -22
  275. package/src/runtime/control-plane/facade/requirement-authority.ts +102 -199
  276. package/src/runtime/control-plane/facade/requirement-board.ts +3 -3
  277. package/src/runtime/control-plane/facade/rh-work-operation-contract.ts +51 -14
  278. package/src/runtime/control-plane/facade/self-healing-loop.ts +18 -53
  279. package/src/runtime/control-plane/facade/suggested-actions.ts +15 -18
  280. package/src/runtime/control-plane/facade/types.ts +49 -108
  281. package/src/runtime/control-plane/facade/work-continuation.ts +8 -8
  282. package/src/runtime/control-plane/generated-cache-retention.ts +97 -1
  283. package/src/runtime/control-plane/global-scheduler/autonomous-continuation.ts +248 -0
  284. package/src/runtime/control-plane/global-scheduler/durable-admission.ts +1 -1
  285. package/src/runtime/control-plane/global-scheduler/maintenance.ts +239 -112
  286. package/src/runtime/control-plane/global-scheduler/reconciliation.ts +2 -1
  287. package/src/runtime/control-plane/global-scheduler/scheduler.ts +66 -40
  288. package/src/runtime/control-plane/global-scheduler/wake-signal.ts +42 -2
  289. package/src/runtime/control-plane/governance/access-policy.ts +19 -2
  290. package/src/runtime/control-plane/governance/authorization.ts +20 -3
  291. package/src/runtime/control-plane/launcher/chatgpt-round-continuation.ts +63 -300
  292. package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +28 -300
  293. package/src/runtime/control-plane/launcher/local-conversation-controller.ts +238 -0
  294. package/src/runtime/control-plane/launcher/provider-mcp-bootstrap.ts +5 -2
  295. package/src/runtime/control-plane/launcher/thin-launcher.ts +96 -240
  296. package/src/runtime/control-plane/persistence/cognition-store.ts +820 -0
  297. package/src/runtime/control-plane/persistence/experience-store.ts +162 -29
  298. package/src/runtime/control-plane/persistence/requirement-store.ts +216 -74
  299. package/src/runtime/control-plane/persistence/sqlite-store.ts +113 -6
  300. package/src/runtime/control-plane/release-retention.ts +135 -47
  301. package/src/runtime/control-plane/repository-namespace-retention.ts +2 -2
  302. package/src/runtime/control-plane/runtime-cleanup.ts +290 -21
  303. package/src/runtime/control-plane/workspace/project-onboarding.ts +80 -0
  304. package/src/runtime/control-plane/workspace/semantic-sync-service.ts +19 -5
  305. package/src/runtime/control-plane/workspace/workspace-store.ts +169 -0
  306. package/src/runtime/diagnostics/incident-repair.ts +313 -146
  307. package/src/runtime/diagnostics/mcp-timing.ts +114 -0
  308. package/src/runtime/evidence/artifact-store.ts +158 -40
  309. package/src/runtime/evidence/evidence-store.ts +112 -8
  310. package/src/runtime/evidence/output-handle-index.ts +225 -0
  311. package/src/runtime/evidence/result-store.ts +158 -44
  312. package/src/runtime/execution/jobs/store.ts +3 -4
  313. package/src/runtime/execution/managed-workspace.ts +57 -0
  314. package/src/runtime/execution/process-runtime/check-facade.ts +1 -1
  315. package/src/runtime/execution/process-runtime/check-result-retention.ts +2 -2
  316. package/src/runtime/execution/process-runtime/check-result.ts +105 -1
  317. package/src/runtime/execution/process-runtime/check-runner-sidecar.ts +18 -8
  318. package/src/runtime/execution/process-runtime/command-facade.ts +79 -27
  319. package/src/runtime/execution/process-runtime/gc.ts +15 -2
  320. package/src/runtime/execution/process-runtime/handle-index.ts +176 -0
  321. package/src/runtime/execution/process-runtime/host-command-lane.ts +139 -0
  322. package/src/runtime/execution/process-runtime/index.ts +3 -0
  323. package/src/runtime/execution/process-runtime/lightweight-managed.ts +251 -72
  324. package/src/runtime/execution/process-runtime/persisted-check.ts +3 -3
  325. package/src/runtime/execution/process-runtime/process-runner-entry.ts +47 -5
  326. package/src/runtime/execution/process-runtime/process-scope.ts +160 -0
  327. package/src/runtime/execution/process-runtime/resource-claims.ts +65 -61
  328. package/src/runtime/execution/process-runtime/runtime.ts +35 -2
  329. package/src/runtime/execution/process-runtime/store.ts +30 -2
  330. package/src/runtime/execution/process-runtime/types.ts +6 -1
  331. package/src/runtime/execution/workers/executor.ts +2 -6
  332. package/src/runtime/platform/service-inventory.ts +130 -0
  333. package/src/runtime/platform/service-manager.ts +8 -0
  334. package/src/runtime/plugins/action-application.ts +85 -28
  335. package/src/runtime/plugins/app-store-connect-adapter.ts +38 -14
  336. package/src/runtime/plugins/browser-adapter.ts +757 -34
  337. package/src/runtime/plugins/browser-automation-service.ts +23 -3
  338. package/src/runtime/plugins/browser-macos-bridge.ts +321 -29
  339. package/src/runtime/plugins/browser-manifest-surface.ts +45 -11
  340. package/src/runtime/plugins/browser-native-session-service.ts +63 -13
  341. package/src/runtime/plugins/browser-registration.ts +16 -0
  342. package/src/runtime/plugins/browser-runtime.ts +14 -1
  343. package/src/runtime/plugins/browser-session-authority.ts +2 -2
  344. package/src/runtime/plugins/browser-session-store.ts +46 -76
  345. package/src/runtime/plugins/capability-authorization-grants.ts +258 -62
  346. package/src/runtime/plugins/computer-chatgpt-browser-adapter-installer.ts +219 -0
  347. package/src/runtime/plugins/computer-chatgpt-macos-target.ts +891 -0
  348. package/src/runtime/plugins/computer-registration.ts +222 -11
  349. package/src/runtime/plugins/desktop-operator-external-policy.ts +36 -4
  350. package/src/runtime/plugins/desktop-operator-registration.ts +12 -4
  351. package/src/runtime/plugins/external-adapter.ts +42 -6
  352. package/src/runtime/plugins/external-registration.ts +21 -1
  353. package/src/runtime/plugins/gmail-adapter.ts +23 -2
  354. package/src/runtime/plugins/google-calendar-adapter.ts +2 -2
  355. package/src/runtime/plugins/google-shared.ts +14 -7
  356. package/src/runtime/plugins/google-tasks-adapter.ts +2 -2
  357. package/src/runtime/plugins/ios-adapter.ts +1 -1
  358. package/src/runtime/plugins/lightweight-action.ts +208 -2
  359. package/src/runtime/plugins/local-system-adapter.ts +12 -2
  360. package/src/runtime/plugins/managed-process-adapter.ts +12 -5
  361. package/src/runtime/plugins/plugin-action-sidecar.ts +17 -13
  362. package/src/runtime/plugins/plugin-management-adapter.ts +1 -1
  363. package/src/runtime/plugins/store.ts +602 -428
  364. package/src/runtime/plugins/types.ts +20 -0
  365. package/src/runtime/plugins/xiaohongshu-publish.ts +16 -0
  366. package/src/runtime/projections/invalidation.ts +7 -3
  367. package/src/runtime/projections/materialized-view.ts +80 -36
  368. package/src/runtime/recovery/actions.ts +3 -3
  369. package/src/runtime/recovery/capability-action-application.ts +46 -45
  370. package/src/runtime/recovery/lifecycle-inventory.ts +29 -29
  371. package/src/runtime/recovery/maintenance-executor.ts +374 -123
  372. package/src/runtime/recovery/store.ts +29 -11
  373. package/src/runtime/release/release-coordinator.ts +296 -0
  374. package/src/runtime/release/release-session.ts +716 -0
  375. package/src/runtime/release/release-state-migration.ts +25 -0
  376. package/src/runtime/resources/leases/store.ts +56 -25
  377. package/src/runtime/root/computer-composition.ts +191 -6
  378. package/src/runtime/root/controller-progression-composition.ts +192 -0
  379. package/src/runtime/root/controller-round-composition.ts +20 -1
  380. package/src/runtime/root/deployment-topology.ts +106 -0
  381. package/src/runtime/root/entry.ts +4 -3
  382. package/src/runtime/root/known-good-recovery.ts +184 -0
  383. package/src/runtime/root/local-bridge.ts +0 -1
  384. package/src/runtime/root/mcp-transport.ts +111 -18
  385. package/src/runtime/root/ownership.ts +261 -9
  386. package/src/runtime/root/package-connector-service.ts +105 -19
  387. package/src/runtime/root/package-runtime-release.ts +117 -18
  388. package/src/runtime/root/package-runtime-service.ts +17 -11
  389. package/src/runtime/root/release-execution-canary.ts +44 -12
  390. package/src/runtime/root/release-loader.ts +90 -8
  391. package/src/runtime/root/release-manifest.ts +80 -6
  392. package/src/runtime/root/release-materialize.ts +372 -28
  393. package/src/runtime/root/release-store.ts +395 -36
  394. package/src/runtime/root/runtime-lane.ts +191 -0
  395. package/src/runtime/root/runtime.ts +159 -27
  396. package/src/runtime/root/scheduled-controller-composition.ts +28 -3
  397. package/src/runtime/root/service-runner.ts +2 -2
  398. package/src/runtime/root/service.ts +36 -33
  399. package/src/runtime/root/status.ts +48 -0
  400. package/src/runtime/root/types.ts +32 -3
  401. package/src/runtime/root/workflow-supervisor-composition.ts +493 -0
  402. package/src/runtime/root/workflow-supervisor-runtime.ts +209 -0
  403. package/src/runtime/root/write-fence.ts +50 -2
  404. package/src/runtime/safe-tooling/google-credential-store.ts +76 -21
  405. package/src/runtime/safe-tooling/google-oauth-broker.ts +25 -7
  406. package/src/runtime/safe-tooling/workspace-auth.ts +1 -1
  407. package/src/runtime/shared/bounded-child-supervisor.ts +9 -1
  408. package/src/runtime/shared/process-environment.ts +20 -9
  409. package/src/runtime/shared/response-budget.ts +1 -3
  410. package/src/runtime/standalone-recovery/controller-home-migration.ts +16 -24
  411. package/src/runtime/standalone-recovery/core.ts +3702 -342
  412. package/src/runtime/standalone-recovery/entry.ts +469 -39
  413. package/src/runtime/standalone-recovery/installer.ts +262 -148
  414. package/src/runtime/standalone-recovery/mcp-server.ts +29 -193
  415. package/src/runtime/standalone-recovery/operation-lock.ts +1 -1
  416. package/src/runtime/standalone-recovery/performance.ts +54 -16
  417. package/src/runtime/standalone-recovery/release.ts +53 -5
  418. package/src/runtime/standalone-recovery/service-labels.ts +5 -2
  419. package/src/runtime/standalone-recovery/watchdog-heartbeat.ts +5 -2
  420. package/src/runtime/workflow/schedules/browser-probe.ts +17 -6
  421. package/src/runtime/workflow/schedules/chatgpt-automation-migration.ts +10 -3
  422. package/src/runtime/workflow/schedules/engine.ts +138 -172
  423. package/src/runtime/workflow/schedules/work-continuation.ts +44 -5
  424. package/src/runtime/workflows/runtime.ts +15 -19
  425. package/src/testing/test-governance.ts +5 -1
  426. package/supervisor/chatgpt-conversation.ts +23 -0
  427. package/supervisor/client.ts +201 -0
  428. package/supervisor/control-plane.ts +868 -0
  429. package/supervisor/entry.ts +15 -0
  430. package/supervisor/forge-validators.ts +80 -0
  431. package/supervisor/index.ts +5 -0
  432. package/supervisor/native-browser-adapter.ts +786 -0
  433. package/supervisor/native-messaging/host.ts +39 -0
  434. package/supervisor/native-messaging/manifest.ts +26 -0
  435. package/supervisor/native-messaging/protocol.ts +27 -0
  436. package/supervisor/paths.ts +18 -0
  437. package/supervisor/protocol.ts +202 -0
  438. package/supervisor/server.ts +404 -0
  439. package/supervisor/store.ts +1451 -0
  440. package/supervisor/tsconfig.json +8 -0
  441. package/supervisor/types.ts +255 -0
  442. package/adapters/chatgpt/controller-host.ts +0 -119
  443. package/adapters/mcp/controller-round-compatibility.ts +0 -184
  444. package/adapters/mcp/frozen-client-semantic-compatibility.ts +0 -292
  445. package/adapters/mcp/runtime-gateway/work-compat-adapter.ts +0 -502
  446. package/packages/kernel/progression/api/index.ts +0 -2
  447. package/packages/kernel/progression/application/projector.ts +0 -211
  448. package/packages/kernel/progression/domain/types.ts +0 -116
  449. package/packages/kernel/progression/index.ts +0 -1
  450. package/packages/kernel/scheduler/application/continuation-service.ts +0 -212
  451. package/packages/kernel/scheduler/domain/continuation.ts +0 -38
  452. package/packages/kernel/scheduler/infrastructure/continuation-dispatch-store.ts +0 -38
  453. package/packages/kernel/work/domain/admission-policy.ts +0 -34
  454. package/scripts/benchmark-background-check-overlap.ts +0 -228
  455. package/scripts/benchmark-computer-hotpath.ts +0 -255
  456. package/scripts/benchmark-controller-hotpath.ts +0 -212
  457. package/scripts/benchmark-controller-round2.ts +0 -191
  458. package/scripts/benchmark-ios-agent-device.ts +0 -511
  459. package/scripts/benchmark-mcp-host.sh +0 -117
  460. package/scripts/benchmark-mcp-transport.ts +0 -169
  461. package/scripts/benchmark-operational-memory-activation.ts +0 -235
  462. package/scripts/benchmark-operational-shadow.ts +0 -28
  463. package/scripts/benchmark-quality-harness.ts +0 -146
  464. package/scripts/benchmark-route-session-concurrency.ts +0 -915
  465. package/scripts/benchmark-thin-harness-gateway-ab.ts +0 -632
  466. package/scripts/benchmark-thin-harness-mcp-e2e.ts +0 -498
  467. package/scripts/benchmark-thin-harness.ts +0 -251
  468. package/scripts/benchmark-typescript-navigation.ts +0 -186
  469. package/scripts/benchmark-work-efficiency.ts +0 -334
  470. package/scripts/build-controller-ui.ts +0 -28
  471. package/scripts/check-npm-release.sh +0 -32
  472. package/scripts/check-public-release-surface.sh +0 -19
  473. package/scripts/check-release-published.sh +0 -57
  474. package/scripts/check-release-readiness.sh +0 -37
  475. package/scripts/check-release-version.mjs +0 -74
  476. package/scripts/check-tarball-install-smoke.sh +0 -150
  477. package/scripts/package-source-archive.sh +0 -147
  478. package/scripts/public-release-files.txt +0 -39
  479. package/scripts/public-release.ts +0 -174
  480. package/scripts/publish-release-tarball.sh +0 -27
  481. package/scripts/route-nl-vs-ts-eval.ts +0 -679
  482. package/scripts/stage-runtime-release.ts +0 -44
  483. package/src/cli/controller/progress.ts +0 -656
  484. package/src/cli/controller/work-mode.ts +0 -181
  485. package/src/cli/local-bridge/console-assets.ts +0 -17
  486. package/src/cli/local-bridge/dashboard.ts +0 -17
  487. package/src/cli/local-bridge/frontend/src/App.tsx +0 -10
  488. package/src/cli/local-bridge/frontend/src/components/Button.tsx +0 -1
  489. package/src/cli/local-bridge/frontend/src/components/DetailPane.tsx +0 -3
  490. package/src/cli/local-bridge/frontend/src/components/SearchField.tsx +0 -1
  491. package/src/cli/local-bridge/frontend/src/components/SectionHeader.tsx +0 -1
  492. package/src/cli/local-bridge/frontend/src/components/Segmented.tsx +0 -1
  493. package/src/cli/local-bridge/frontend/src/components/StatusText.tsx +0 -3
  494. package/src/cli/local-bridge/frontend/src/design/icons.tsx +0 -11
  495. package/src/cli/local-bridge/frontend/src/design/tokens.css +0 -8
  496. package/src/cli/local-bridge/frontend/src/features/automations/AutomationsView.tsx +0 -153
  497. package/src/cli/local-bridge/frontend/src/features/capabilities/CapabilitiesView.tsx +0 -37
  498. package/src/cli/local-bridge/frontend/src/features/overview/OverviewView.tsx +0 -183
  499. package/src/cli/local-bridge/frontend/src/features/repositories/RepositoriesView.tsx +0 -2
  500. package/src/cli/local-bridge/frontend/src/features/system/SystemView.tsx +0 -2
  501. package/src/cli/local-bridge/frontend/src/features/work/WorkView.tsx +0 -4
  502. package/src/cli/local-bridge/frontend/src/lib/api.ts +0 -30
  503. package/src/cli/local-bridge/frontend/src/lib/format.ts +0 -4
  504. package/src/cli/local-bridge/frontend/src/lib/routes.ts +0 -7
  505. package/src/cli/local-bridge/frontend/src/main.tsx +0 -1
  506. package/src/cli/local-bridge/frontend/src/shell/AppShell.tsx +0 -1
  507. package/src/cli/local-bridge/frontend/src/shell/CommandBar.tsx +0 -2
  508. package/src/cli/local-bridge/frontend/src/shell/Sidebar.tsx +0 -4
  509. package/src/cli/local-bridge/frontend/src/styles.css +0 -28
  510. package/src/cli/local-bridge/frontend/src/types.ts +0 -15
  511. package/src/cli/local-bridge/frontend/src/vite-env.d.ts +0 -1
  512. package/src/cli/local-bridge/frontend/vite.config.ts +0 -27
  513. package/src/cli/local-bridge/ui-dist/app.css +0 -1
  514. package/src/cli/local-bridge/ui-dist/app.js +0 -11228
  515. package/src/runtime/control-plane/execution/content-equivalent-commit-authority.ts +0 -169
  516. package/src/runtime/control-plane/execution/direct-canonical-work-reconciliation.ts +0 -85
  517. package/src/runtime/control-plane/execution/direct-edit-work-completion.ts +0 -655
  518. package/src/runtime/control-plane/execution/work-finalization-service.ts +0 -2850
  519. package/src/runtime/control-plane/execution/work-path-scope.ts +0 -37
  520. package/src/runtime/control-plane/execution/work-task-receipt.ts +0 -468
  521. package/src/runtime/control-plane/facade/controller-round-relay.ts +0 -3
  522. package/src/runtime/control-plane/facade/work-admission-policy.ts +0 -142
  523. package/src/runtime/control-plane/facade/work-implementation-review.ts +0 -2
  524. package/src/runtime/control-plane/facade/work-state-machine.ts +0 -2
  525. package/src/runtime/control-plane/facade/work-task-projection.ts +0 -66
  526. package/src/runtime/control-plane/routing/index.ts +0 -1
  527. package/src/runtime/control-plane/routing/route-policy.ts +0 -401
  528. package/src/runtime/control-plane/routing/workspace-admission.ts +0 -70
@@ -1,10 +1,10 @@
1
- import { randomUUID, timingSafeEqual } from 'crypto';
1
+ import { randomUUID } from 'crypto';
2
2
  import { existsSync, watch } from 'fs';
3
3
  import { dirname } from 'path';
4
4
  import express, { type Request, type Response, type NextFunction } from 'express';
5
5
  import { NodeStreamableHTTPServerTransport, toNodeHandler, toWebRequest, type NodeMcpRequestHandler } from "@modelcontextprotocol/node";
6
- import { createMcpHandler, isLegacyRequest, type McpRequestContext, type OAuthClientInformationFull } from "@modelcontextprotocol/server";
7
- import { tokenHandler, revocationHandler, clientRegistrationHandler, redirectUriMatches, InvalidTokenError } from "@modelcontextprotocol/server-legacy/auth";
6
+ import { createMcpHandler, isLegacyRequest, type McpRequestContext } from "@modelcontextprotocol/server";
7
+ import { InvalidTokenError } from "@modelcontextprotocol/server-legacy/auth";
8
8
  import {
9
9
  buildMultiRepositoryToolDefinitions,
10
10
  createCanonicalRuntimeProxy,
@@ -17,39 +17,29 @@ import {
17
17
  } from '../server';
18
18
  import {
19
19
  loadMcpServiceLocalConfig,
20
- loadMcpServiceRuntimeState,
21
20
  mcpServiceOAuthTokenStoreFallbackPaths,
22
21
  mcpServiceOAuthTokenStorePath,
23
22
  parseMcpHttpAuthMode,
24
23
  readMcpServiceBearerToken,
25
24
  readMcpServiceOAuthPassphrase,
26
- type McpLocalConfig,
27
25
  type McpHttpAuthMode,
28
26
  } from '../auth';
29
27
  import { createMcpOAuthProvider, McpOAuthTokenStore } from '../oauth';
30
28
  import { resolveMcpRepoRoot } from '../repo';
31
- import { buildMcpToolDefinitions } from '../tool-mapping/tools';
32
29
  import { resolveControllerHome } from '../../../src/cli/repositories/controller-home';
33
- import {
34
- controllerExposureSnapshot,
35
- } from '../toolset';
36
- import { readForgeRuntimeStatus } from '../../../src/runtime/control-plane/runtime-status-client';
37
- import { invalidateExecutionSession } from '../../../src/runtime/control-plane/execution/session-store';
38
- import { runtimeIdentitySnapshot } from '../runtime-gateway/runtime-tools';
39
- import { projectionBlocksReadiness, readRepositoryProjectionSnapshot } from '../../../src/runtime/projections/materialized-view';
40
30
  import { readRuntimeGeneration } from '../../../src/runtime/control-plane/runtime-generation';
31
+ import { recordMcpTransportEvent } from '../../../src/runtime/diagnostics/mcp-timing';
41
32
  import { readRuntimeStatusSnapshot, runtimeStatusPath } from '../../../src/runtime/root/status';
42
- import { getRepository, listRepositories } from '../../../src/cli/repositories/registry';
43
- import { buildControllerTaskLedgerProjection } from '../../../src/cli/controller/task-ledger';
44
- import { legacyIssueAuthorityRetired } from '../../../src/cli/controller/legacy-issue-cutover';
45
- import { reconcileReadinessProjectionSource } from '../readiness-projection';
46
33
  import {
47
34
  FORGE_MCP_SCHEMA_VERSION,
48
35
  FORGE_TOOL_SURFACE,
49
36
  FORGE_VERSION,
50
- repositoryIdentity,
51
37
  } from '../../../src/cli/controller/runtime-config';
52
- import { McpSessionRegistry, type McpSessionRoute } from './session-registry';
38
+ import { McpSessionRegistry, type ClosableMcpTransport, type McpSessionRegistryOptions, type McpSessionRoute } from './session-registry';
39
+ import { getConfiguredPublicOrigin, getPublicOrigin, registerMcpOAuthHttpRoutes } from './oauth-http';
40
+ import { registerMcpHttpObservationRoutes } from './http-observation';
41
+ import { RUNTIME_MCP_CUTOVER_OUTCOME_UNKNOWN } from '../../../src/runtime/root/mcp-transport';
42
+ export { isAllowedMcpOAuthRedirectUri, isIncompleteOAuthAuthorizeRequest } from './oauth-http';
53
43
  import {
54
44
  connectionIdentity,
55
45
  ensureForgeInstanceIdentity,
@@ -65,17 +55,6 @@ export interface McpHttpOptions extends McpServerOptions {
65
55
  auth?: string;
66
56
  }
67
57
 
68
- function localControllerDiagnosticMatchesRuntime(
69
- payload: Record<string, unknown> | null,
70
- generation?: string,
71
- ): boolean {
72
- return payload?.status === 'ok'
73
- && payload.toolSurface === FORGE_TOOL_SURFACE
74
- && payload.schemaVersion === FORGE_MCP_SCHEMA_VERSION
75
- && payload.version === FORGE_VERSION
76
- && (generation === undefined || payload.generation === generation);
77
- }
78
-
79
58
  function bearerFromRequest(req: Request): string | null {
80
59
  const header = req.headers.authorization;
81
60
  if (typeof header !== 'string') return null;
@@ -228,8 +207,42 @@ function sendMcpToolSurfaceReset(
228
207
  });
229
208
  }
230
209
 
210
+ /**
211
+ * A schema-fenced transport cannot be reused after the reset response. Delay
212
+ * retirement until Express has flushed the reconnect instruction, then release
213
+ * its session-local state. Work identity is Controller-owned, not transport-owned.
214
+ */
215
+ function retireAfterToolSurfaceReset(
216
+ res: Response,
217
+ registry: HttpSessionRegistry,
218
+ sessionId: string,
219
+ ): void {
220
+ let retired = false;
221
+ const retire = (): void => {
222
+ if (retired) return;
223
+ retired = true;
224
+ void registry.close(sessionId, 'tool_surface_changed');
225
+ };
226
+ res.once('finish', retire);
227
+ res.once('close', retire);
228
+ }
229
+
231
230
  export function mcpRequestError(error: unknown) {
232
231
  const rawMessage = error instanceof Error ? error.message : String(error);
232
+ if (rawMessage.includes(RUNTIME_MCP_CUTOVER_OUTCOME_UNKNOWN)) {
233
+ return {
234
+ status: 503 as const,
235
+ body: {
236
+ error: 'runtime_cutover_outcome_unknown' as const,
237
+ code: RUNTIME_MCP_CUTOVER_OUTCOME_UNKNOWN,
238
+ message: 'The Canonical Runtime changed after this request was admitted. Reconcile the original request/effect identity before retrying.',
239
+ recoverable: true as const,
240
+ retryable: false as const,
241
+ sessionPreserved: true as const,
242
+ action: 'reconcile' as const,
243
+ },
244
+ };
245
+ }
233
246
  const retryable = /(?:\b502\b|\b503\b|\b429\b|ECONNRESET|ETIMEDOUT|EAI_AGAIN|CANONICAL_RUNTIME_TIMEOUT|server_busy|session_capacity|gateway)/i.test(rawMessage);
234
247
  return {
235
248
  status: (retryable ? 503 : 500) as 500 | 503,
@@ -254,343 +267,6 @@ export function sendMcpRequestError(res: Response, error: unknown): void {
254
267
  res.status(response.status).json(response.body);
255
268
  }
256
269
 
257
- function getConfiguredPublicOrigin(config: McpLocalConfig | null): string | undefined {
258
- const configured = process.env.FORGE_MCP_PUBLIC_ORIGIN?.trim();
259
- if (configured) {
260
- try {
261
- return new URL(configured).origin;
262
- } catch (_error) {
263
- // Fall through to service or legacy config.
264
- }
265
- }
266
- const endpoint = config?.chatgpt?.endpoint?.trim();
267
- if (!endpoint) return undefined;
268
- try {
269
- return new URL(endpoint).origin;
270
- } catch (_error) {
271
- return undefined;
272
- }
273
- }
274
-
275
- function getPublicOrigin(req: Request, configuredOrigin: string | undefined): string {
276
- if (configuredOrigin) return configuredOrigin;
277
- const proto = (req.headers['x-forwarded-proto'] as string | undefined) ?? 'https';
278
- const host = (req.headers['x-forwarded-host'] as string | undefined) ?? req.headers.host ?? '127.0.0.1:8765';
279
- return `${proto}://${host}`;
280
- }
281
-
282
- function localControllerHealthUrl(host: string, port: number): string {
283
- return `http://${host === '::1' ? '[::1]' : host}:${port}/health`;
284
- }
285
-
286
- async function jsonHealth(url: string): Promise<Record<string, unknown> | null> {
287
- const controller = new AbortController();
288
- const timeout = setTimeout(() => controller.abort(), 2_000);
289
- try {
290
- const response = await fetch(url, {
291
- signal: controller.signal,
292
- headers: { accept: 'application/json' },
293
- });
294
- if (!response.ok) return null;
295
- return await response.json() as Record<string, unknown>;
296
- } catch (_error) {
297
- return null;
298
- } finally {
299
- clearTimeout(timeout);
300
- }
301
- }
302
-
303
- export function isAllowedMcpOAuthRedirectUri(redirectUri: string): boolean {
304
- try {
305
- const url = new URL(redirectUri);
306
- if (url.protocol === 'http:' && ['localhost', '127.0.0.1', '[::1]'].includes(url.hostname)) {
307
- return true;
308
- }
309
- if (
310
- url.protocol === 'https:' &&
311
- (url.origin === 'https://chatgpt.com' || url.origin === 'https://chat.openai.com') &&
312
- url.pathname.startsWith('/connector/oauth/')
313
- ) {
314
- return true;
315
- }
316
- return false;
317
- } catch (_error) {
318
- return false;
319
- }
320
- }
321
-
322
- function isRegisteredRedirectUri(redirectUri: string, client: { redirect_uris?: string[] }): boolean {
323
- return (client.redirect_uris ?? []).some((registered) => redirectUriMatches(redirectUri, registered));
324
- }
325
-
326
- function isRegisteredExternalHttpsRedirectUri(redirectUri: string, client: { redirect_uris?: string[] }): boolean {
327
- try {
328
- const url = new URL(redirectUri);
329
- return url.protocol === 'https:' && !url.username && !url.password && isRegisteredRedirectUri(redirectUri, client);
330
- } catch (_error) {
331
- return false;
332
- }
333
- }
334
-
335
- function isSafeOAuthFallbackRedirectUri(redirectUri: string): boolean {
336
- try {
337
- const url = new URL(redirectUri);
338
- if (url.username || url.password) return false;
339
- if (url.protocol === 'http:' && ['localhost', '127.0.0.1', '[::1]'].includes(url.hostname)) return true;
340
- return url.protocol === 'https:';
341
- } catch (_error) {
342
- return false;
343
- }
344
- }
345
-
346
- async function getOrRegisterPublicOAuthClient(
347
- provider: ReturnType<typeof createMcpOAuthProvider>,
348
- clientId: string,
349
- redirectUri: string | undefined,
350
- req: Request,
351
- ): Promise<OAuthClientInformationFull | undefined> {
352
- const existing = await provider.clientsStore.getClient(clientId);
353
- if (existing) return existing as OAuthClientInformationFull;
354
- if (!redirectUri || !isSafeOAuthFallbackRedirectUri(redirectUri) || !provider.clientsStore.registerClient) return undefined;
355
- oauthTrace(req, 'authorize:auto_register_public_client', {
356
- redirectScheme: new URL(redirectUri).protocol,
357
- redirectHost: new URL(redirectUri).hostname,
358
- });
359
- return provider.clientsStore.registerClient({
360
- client_id: clientId,
361
- client_id_issued_at: Math.floor(Date.now() / 1000),
362
- client_name: 'forge OAuth fallback client',
363
- redirect_uris: [redirectUri],
364
- token_endpoint_auth_method: 'none',
365
- grant_types: ['authorization_code', 'refresh_token'],
366
- response_types: ['code'],
367
- } as unknown as Omit<OAuthClientInformationFull, 'client_id' | 'client_id_issued_at'>) as OAuthClientInformationFull;
368
- }
369
-
370
- function escapeHtmlAttribute(value: string): string {
371
- return value
372
- .replace(/&/g, '&amp;')
373
- .replace(/"/g, '&quot;')
374
- .replace(/'/g, '&#39;')
375
- .replace(/</g, '&lt;')
376
- .replace(/>/g, '&gt;');
377
- }
378
-
379
- function renderPassphrasePage(params: URLSearchParams): string {
380
- const hiddenFields = Array.from(params.entries())
381
- .filter(([key]) => key !== 'passphrase')
382
- .map(([key, value]) => `<input type="hidden" name="${escapeHtmlAttribute(key)}" value="${escapeHtmlAttribute(value)}">`)
383
- .join('\n');
384
-
385
- return `<!doctype html>
386
- <html lang="en">
387
- <head><meta charset="utf-8"><title>Authorize forge</title>
388
- <meta name="viewport" content="width=device-width,initial-scale=1">
389
- <style>
390
- 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}
391
- .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)}
392
- h1{font-size:20px;margin:0 0 8px}p{margin:0 0 20px;color:#60666d;line-height:1.45}
393
- input{width:100%;box-sizing:border-box;border:1px solid #bfc4c9;border-radius:8px;padding:12px;font-size:16px}
394
- button{width:100%;margin-top:14px;border:0;border-radius:8px;padding:12px;background:#1f2328;color:#fff;font-size:16px;font-weight:600}
395
- </style></head>
396
- <body><main class="card">
397
- <h1>Authorize forge</h1>
398
- <p>Enter the local MCP passphrase to let this MCP client use this workflow-scoped connector.</p>
399
- <form method="POST" action="/authorize">
400
- ${hiddenFields}
401
- <input type="password" name="passphrase" placeholder="Passphrase" autofocus>
402
- <button type="submit">Authorize</button>
403
- </form>
404
- </main></body></html>`;
405
- }
406
-
407
- /** Collect OAuth authorize params from query (GET) or body (POST form). */
408
- function oauthAuthorizeParamSource(req: Request): Record<string, unknown> {
409
- if (req.method === 'POST' && req.body && typeof req.body === 'object' && !Buffer.isBuffer(req.body)) {
410
- return req.body as Record<string, unknown>;
411
- }
412
- return req.query as Record<string, unknown>;
413
- }
414
-
415
- function readOAuthAuthorizeString(source: Record<string, unknown>, key: string): string {
416
- const value = source[key];
417
- return typeof value === 'string' ? value.trim() : '';
418
- }
419
-
420
- /**
421
- * True when the request lacks the OAuth parameters needed for a real authorization.
422
- * Incomplete requests must not render the passphrase form (incompatible clients loop there).
423
- */
424
- export function isIncompleteOAuthAuthorizeRequest(source: Record<string, unknown>): boolean {
425
- const clientId = readOAuthAuthorizeString(source, 'client_id');
426
- const responseType = readOAuthAuthorizeString(source, 'response_type');
427
- const codeChallenge = readOAuthAuthorizeString(source, 'code_challenge');
428
- const redirectUri = readOAuthAuthorizeString(source, 'redirect_uri');
429
- // redirect_uri may be omitted when the client has a single registered URI; client_id is the usable redirect context.
430
- const hasRedirectContext = Boolean(redirectUri) || Boolean(clientId);
431
- return !clientId || !responseType || !codeChallenge || !hasRedirectContext;
432
- }
433
-
434
- function incompleteOAuthAuthorizeResponseBody(): {
435
- error: 'invalid_request';
436
- error_description: string;
437
- message: string;
438
- hint: string;
439
- } {
440
- return {
441
- error: 'invalid_request',
442
- error_description:
443
- 'OAuth authorization request is incomplete. Required: client_id, response_type, code_challenge, and a usable redirect context (redirect_uri or a registered client).',
444
- message:
445
- 'This endpoint expects a complete OAuth authorization request (PKCE). Non-OAuth MCP clients should use /mcp-bearer with Authorization: Bearer <token> instead of /authorize.',
446
- hint: 'Use POST/GET /mcp-bearer with a forge bearer token for clients that cannot complete OAuth dynamic registration and PKCE.',
447
- };
448
- }
449
-
450
- function isOAuthDebugTraceEnabled(): boolean {
451
- return process.env.FORGE_MCP_OAUTH_TRACE === '1' || process.env.FORGE_MCP_OAUTH_TRACE === 'true';
452
- }
453
-
454
- const SENSITIVE_OAUTH_FIELDS = new Set([
455
- 'passphrase',
456
- 'code',
457
- 'code_verifier',
458
- 'client_secret',
459
- 'access_token',
460
- 'refresh_token',
461
- 'token',
462
- 'authorization',
463
- ]);
464
-
465
- function safeOAuthFieldNames(source: Record<string, unknown>): string[] {
466
- return Object.keys(source)
467
- .filter((key) => !SENSITIVE_OAUTH_FIELDS.has(key.toLowerCase()))
468
- .sort();
469
- }
470
-
471
- function oauthTrace(req: Request, event: string, extra: Record<string, unknown> = {}): void {
472
- if (!isOAuthDebugTraceEnabled()) return;
473
- const source = oauthAuthorizeParamSource(req);
474
- const userAgent = typeof req.headers['user-agent'] === 'string'
475
- ? req.headers['user-agent'].split(/[\s/]/)[0]
476
- : undefined;
477
- const safe = {
478
- event,
479
- method: req.method,
480
- path: req.path,
481
- fieldNames: safeOAuthFieldNames(source),
482
- hasClientId: Boolean(readOAuthAuthorizeString(source, 'client_id')),
483
- hasRedirectUri: Boolean(readOAuthAuthorizeString(source, 'redirect_uri')),
484
- hasCodeChallenge: Boolean(readOAuthAuthorizeString(source, 'code_challenge')),
485
- responseType: readOAuthAuthorizeString(source, 'response_type') || undefined,
486
- codeChallengeMethod: readOAuthAuthorizeString(source, 'code_challenge_method') || undefined,
487
- grantType: readOAuthAuthorizeString(source, 'grant_type') || undefined,
488
- hasResource: Boolean(readOAuthAuthorizeString(source, 'resource')),
489
- userAgent,
490
- ...extra,
491
- };
492
- console.error(`[forge:mcp-oauth] ${JSON.stringify(safe)}`);
493
- }
494
-
495
- function oauthTraceMiddleware(event: string): (req: Request, res: Response, next: NextFunction) => void {
496
- return (req, res, next) => {
497
- oauthTrace(req, `${event}:request`);
498
- res.once('finish', () => oauthTrace(req, `${event}:response`, { statusCode: res.statusCode }));
499
- next();
500
- };
501
- }
502
-
503
- function rejectIncompleteOAuthAuthorize(req: Request, res: Response, next: NextFunction): void {
504
- const source = oauthAuthorizeParamSource(req);
505
- if (isIncompleteOAuthAuthorizeRequest(source)) {
506
- oauthTrace(req, 'authorize:incomplete');
507
- res.status(400).json(incompleteOAuthAuthorizeResponseBody());
508
- return;
509
- }
510
- oauthTrace(req, 'authorize:complete');
511
- next();
512
- }
513
-
514
- function requirePassphrase(passphrase: string): (req: Request, res: Response, next: NextFunction) => void {
515
- return (req, res, next) => {
516
- const provided = typeof req.body?.passphrase === 'string' ? req.body.passphrase : undefined;
517
- if (provided) {
518
- const a = Buffer.from(provided);
519
- const b = Buffer.from(passphrase);
520
- if (a.length === b.length && timingSafeEqual(a, b)) {
521
- next();
522
- return;
523
- }
524
- }
525
- // Prefer body hidden fields on POST (failed passphrase re-render), else query string on GET.
526
- const source = oauthAuthorizeParamSource(req);
527
- const params = new URLSearchParams();
528
- for (const [key, value] of Object.entries(source)) {
529
- if (key === 'passphrase') continue;
530
- if (typeof value === 'string') params.set(key, value);
531
- }
532
- if ([...params.keys()].length === 0 && req.url.includes('?')) {
533
- const fromUrl = new URLSearchParams(req.url.slice(req.url.indexOf('?')));
534
- for (const [key, value] of fromUrl.entries()) {
535
- if (key !== 'passphrase') params.set(key, value);
536
- }
537
- }
538
- res.type('html').send(renderPassphrasePage(params));
539
- };
540
- }
541
-
542
- function oauthAuthorizationHandler(provider: ReturnType<typeof createMcpOAuthProvider>) {
543
- return async (req: Request, res: Response) => {
544
- const query = req.method === 'POST' ? req.body : req.query;
545
- const clientId = typeof query.client_id === 'string' ? query.client_id : '';
546
- const responseType = typeof query.response_type === 'string' ? query.response_type : '';
547
- const codeChallenge = typeof query.code_challenge === 'string' ? query.code_challenge : '';
548
- const codeChallengeMethod = typeof query.code_challenge_method === 'string' ? query.code_challenge_method : '';
549
- const state = typeof query.state === 'string' ? query.state : undefined;
550
- const scope = typeof query.scope === 'string' ? query.scope : undefined;
551
- let redirectUri = typeof query.redirect_uri === 'string' ? query.redirect_uri : undefined;
552
-
553
- if (responseType !== 'code') {
554
- res.status(400).json({ error: 'unsupported_response_type', error_description: 'Only code response type is supported' });
555
- return;
556
- }
557
- if (!codeChallenge || codeChallengeMethod !== 'S256') {
558
- res.status(400).json({ error: 'invalid_request', error_description: 'PKCE S256 is required' });
559
- return;
560
- }
561
-
562
- const client = await getOrRegisterPublicOAuthClient(provider, clientId, redirectUri, req);
563
- if (!client) {
564
- res.status(400).json({ error: 'invalid_client', error_description: 'Unknown client_id' });
565
- return;
566
- }
567
- if (!redirectUri && client.redirect_uris.length === 1) {
568
- redirectUri = client.redirect_uris[0];
569
- }
570
- if (!redirectUri || (!isAllowedMcpOAuthRedirectUri(redirectUri) && !isRegisteredExternalHttpsRedirectUri(redirectUri, client))) {
571
- res.status(400).json({
572
- error: 'invalid_request',
573
- error_description: 'redirect_uri must be localhost, a ChatGPT connector callback URL, or a registered HTTPS client redirect_uri',
574
- });
575
- return;
576
- }
577
- if (!isRegisteredRedirectUri(redirectUri, client)) {
578
- res.status(400).json({
579
- error: 'invalid_request',
580
- error_description: 'redirect_uri must match a registered client redirect_uri',
581
- });
582
- return;
583
- }
584
-
585
- await provider.authorize(client as OAuthClientInformationFull, {
586
- state,
587
- scopes: scope ? scope.split(' ') : [],
588
- redirectUri,
589
- codeChallenge,
590
- }, res);
591
- };
592
- }
593
-
594
270
  function sendBearerUnauthorized(res: Response, description: string, hasConfiguredToken: boolean): void {
595
271
  res.setHeader('www-authenticate', 'Bearer realm="forge-mcp"');
596
272
  res.status(hasConfiguredToken ? 401 : 503).json({
@@ -682,9 +358,60 @@ const MCP_STREAM_LEASE_MS = positiveIntegerEnv('FORGE_MCP_STREAM_LEASE_MS', 30 *
682
358
  const MCP_SESSION_ABSOLUTE_LIFETIME_MS = positiveIntegerEnv('FORGE_MCP_SESSION_ABSOLUTE_LIFETIME_MS', 2 * 60 * 60_000);
683
359
  const MCP_ACTIVE_POST_STALL_MS = positiveIntegerEnv('FORGE_MCP_ACTIVE_POST_STALL_MS', 10 * 60_000);
684
360
 
361
+ export function admitMcpPost(stats: McpRuntimeStats, res: Response): boolean {
362
+ if (stats.activePosts >= MAX_ACTIVE_POSTS) {
363
+ stats.rejectedOverload += 1;
364
+ res.setHeader('retry-after', '1');
365
+ res.status(503).json({
366
+ error: 'server_busy',
367
+ code: 'MCP_SERVER_BUSY',
368
+ message: 'Too many MCP requests are active; retry shortly',
369
+ recoverable: true,
370
+ retryable: true,
371
+ sessionPreserved: true,
372
+ action: 'retry',
373
+ });
374
+ return false;
375
+ }
376
+
377
+ stats.activePosts += 1;
378
+ let released = false;
379
+ const release = (): void => {
380
+ if (released) return;
381
+ released = true;
382
+ stats.activePosts = Math.max(0, stats.activePosts - 1);
383
+ res.off('finish', release);
384
+ res.off('close', release);
385
+ };
386
+ res.once('finish', release);
387
+ res.once('close', release);
388
+ return true;
389
+ }
390
+
685
391
  type McpToolContext = ReturnType<typeof createMcpToolContext>;
686
392
  type HttpSessionRegistry = McpSessionRegistry<NodeStreamableHTTPServerTransport, McpToolContext>;
687
393
 
394
+
395
+ /**
396
+ * Public HTTP transport registry. It owns transport admission/leases and
397
+ * transport-local observability only; closing a transport session must never
398
+ * invalidate durable execution, Work, or ControllerRound authority.
399
+ */
400
+ export function createMcpHttpSessionRegistry<
401
+ TTransport extends ClosableMcpTransport = NodeStreamableHTTPServerTransport,
402
+ TContext = McpToolContext,
403
+ >(options: McpSessionRegistryOptions<TTransport, TContext> = {}): McpSessionRegistry<TTransport, TContext> {
404
+ return new McpSessionRegistry<TTransport, TContext>({
405
+ maximumSessions: MAX_MCP_SESSIONS,
406
+ maximumSessionsPerPrincipal: MAX_MCP_SESSIONS_PER_PRINCIPAL,
407
+ idleTtlMs: MCP_SESSION_IDLE_TTL_MS,
408
+ streamLeaseMs: MCP_STREAM_LEASE_MS,
409
+ absoluteLifetimeMs: MCP_SESSION_ABSOLUTE_LIFETIME_MS,
410
+ activePostStallMs: MCP_ACTIVE_POST_STALL_MS,
411
+ ...options,
412
+ });
413
+ }
414
+
688
415
  function principalIdFromModernRequestContext(context: McpRequestContext): string {
689
416
  const clientId = context.authInfo?.clientId?.trim();
690
417
  if (clientId) return `oauth-client:${clientId}`;
@@ -731,6 +458,7 @@ async function handleMcpPost(
731
458
  res.status(400).json({ error: 'invalid JSON request body' });
732
459
  return;
733
460
  }
461
+ if (!admitMcpPost(stats, res)) return;
734
462
  if (modernHandler) {
735
463
  const webRequest = await toWebRequest(req, body);
736
464
  if (!(await isLegacyRequest(webRequest, body))) {
@@ -744,7 +472,7 @@ async function handleMcpPost(
744
472
  res.setHeader('Mcp-Session-Reset', 'reinitialized');
745
473
  res.setHeader('x-forge-session-reset', 'reinitialized');
746
474
  }
747
- if (stats.initializing >= MAX_INITIALIZING_SESSIONS || stats.activePosts >= MAX_ACTIVE_POSTS) {
475
+ if (stats.initializing >= MAX_INITIALIZING_SESSIONS) {
748
476
  stats.rejectedOverload += 1;
749
477
  res.setHeader('retry-after', '1');
750
478
  res.status(503).json({
@@ -759,7 +487,6 @@ async function handleMcpPost(
759
487
  return;
760
488
  }
761
489
  stats.initializing += 1;
762
- stats.activePosts += 1;
763
490
  let transport: NodeStreamableHTTPServerTransport | undefined;
764
491
  let reservationId: string | undefined;
765
492
  let initializedSessionId: string | undefined;
@@ -839,11 +566,23 @@ async function handleMcpPost(
839
566
  server = createForgeMcpServerFromContext(sessionContext, runtimeSchema, sharedRuntimeProxy);
840
567
  await server.connect(transport);
841
568
  await transport.handleRequest(req, res, body);
569
+ if (
570
+ initializedSessionId
571
+ && 'controllerHome' in sessionContext
572
+ && typeof sessionContext.controllerHome === 'string'
573
+ ) {
574
+ recordMcpTransportEvent(sessionContext.controllerHome, {
575
+ kind: 'session_initialized',
576
+ sessionId: initializedSessionId,
577
+ connectionId: connection.connectionId,
578
+ route,
579
+ principalId,
580
+ });
581
+ }
842
582
  } finally {
843
583
  if (initializedSessionId) registry.endPost(initializedSessionId);
844
584
  if (reservationId) registry.releaseInitialize(reservationId);
845
- stats.initializing -= 1;
846
- stats.activePosts -= 1;
585
+ stats.initializing = Math.max(0, stats.initializing - 1);
847
586
  if (!transport?.sessionId) await transport?.close().catch(() => undefined);
848
587
  }
849
588
  return;
@@ -851,14 +590,13 @@ async function handleMcpPost(
851
590
  if (sessionId) {
852
591
  const managed = registry.get(sessionId);
853
592
  if (managed && managed.route === route && managed.principalId === principalFromRequest(req)) {
854
- if (managed.inFlightPosts >= MAX_POSTS_PER_SESSION || stats.activePosts >= MAX_ACTIVE_POSTS) {
593
+ if (managed.inFlightPosts >= MAX_POSTS_PER_SESSION) {
855
594
  stats.rejectedOverload += 1;
856
595
  res.setHeader('retry-after', '1');
857
596
  res.status(429).json({ error: 'session_busy', message: 'Too many MCP requests are active; retry shortly' });
858
597
  return;
859
598
  }
860
599
  registry.beginPost(sessionId);
861
- stats.activePosts += 1;
862
600
  try {
863
601
  const currentFingerprint = await resolveMcpSessionCurrentFingerprint(
864
602
  currentToolSurfaceFingerprint(),
@@ -878,23 +616,21 @@ async function handleMcpPost(
878
616
  return;
879
617
  }
880
618
  }
881
- // Keep the transport/session alive long enough for the host to observe
882
- // the recoverable reset and issue a replacement initialize request.
883
- // The initialize path explicitly supersedes this session afterward.
884
619
  sendMcpToolSurfaceReset(res, managed.toolSurfaceFingerprint, currentFingerprint);
620
+ retireAfterToolSurfaceReset(res, registry, sessionId);
885
621
  return;
886
622
  }
887
623
  if (toolCallOutsideSessionSchema(body, managed.toolNames)) {
888
624
  // A call against a newer discovery surface is the same recoverable
889
- // schema-fence condition. Closing here can make hosts unregister the
890
- // entire MCP namespace before they can reinitialize it.
625
+ // schema-fence condition: this transport must reinitialize before it
626
+ // can execute anything else.
891
627
  sendMcpToolSurfaceReset(res, managed.toolSurfaceFingerprint, currentFingerprint);
628
+ retireAfterToolSurfaceReset(res, registry, sessionId);
892
629
  return;
893
630
  }
894
631
  await managed.transport.handleRequest(req, res, body);
895
632
  } finally {
896
633
  registry.endPost(sessionId);
897
- stats.activePosts -= 1;
898
634
  }
899
635
  return;
900
636
  }
@@ -933,9 +669,8 @@ async function handleMcpGet(
933
669
  : undefined,
934
670
  );
935
671
  if (!mcpSessionToolSurfaceFingerprintIsCurrent(managed.toolSurfaceFingerprint, currentFingerprint)) {
936
- // Preserve the existing SSE transport while asking the host to
937
- // reinitialize. The replacement initialize owns supersession/cleanup.
938
672
  sendMcpToolSurfaceReset(res, managed.toolSurfaceFingerprint, currentFingerprint);
673
+ retireAfterToolSurfaceReset(res, registry, sessionId!);
939
674
  return;
940
675
  }
941
676
  await managed.transport.handleRequest(req, res);
@@ -992,23 +727,17 @@ export async function startMcpHttp(opts: McpHttpOptions): Promise<void> {
992
727
  tokenStore?.load();
993
728
  const oauthProvider = tokenStore ? createMcpOAuthProvider(tokenStore) : null;
994
729
  const configuredPublicOrigin = getConfiguredPublicOrigin(serviceConfig);
995
- const sessionRegistry = new McpSessionRegistry<NodeStreamableHTTPServerTransport, McpToolContext>({
996
- maximumSessions: MAX_MCP_SESSIONS,
997
- maximumSessionsPerPrincipal: MAX_MCP_SESSIONS_PER_PRINCIPAL,
998
- idleTtlMs: MCP_SESSION_IDLE_TTL_MS,
999
- streamLeaseMs: MCP_STREAM_LEASE_MS,
1000
- absoluteLifetimeMs: MCP_SESSION_ABSOLUTE_LIFETIME_MS,
1001
- activePostStallMs: MCP_ACTIVE_POST_STALL_MS,
730
+ const sessionRegistry = createMcpHttpSessionRegistry<NodeStreamableHTTPServerTransport, McpToolContext>({
1002
731
  onSessionClosed: (session, reason) => {
1003
- const context = session.toolContext;
1004
- if (!('controllerHome' in context) || typeof context.controllerHome !== 'string') return;
1005
- const executionSessionId = typeof context.sessionId === 'string' ? context.sessionId.trim() : '';
1006
- if (!executionSessionId) return;
1007
- invalidateExecutionSession(
1008
- context.controllerHome,
1009
- executionSessionId,
1010
- `mcp_transport_${reason}`,
1011
- );
732
+ if (reason !== 'transport_close') return;
733
+ recordMcpTransportEvent(controllerHome, {
734
+ kind: 'interruption',
735
+ sessionId: session.sessionId,
736
+ connectionId: session.connectionId,
737
+ route: session.route,
738
+ principalId: session.principalId,
739
+ reason,
740
+ });
1012
741
  },
1013
742
  });
1014
743
  const runtimeStats: McpRuntimeStats = { initializing: 0, activePosts: 0, rejectedOverload: 0 };
@@ -1055,295 +784,43 @@ export async function startMcpHttp(opts: McpHttpOptions): Promise<void> {
1055
784
  return await readCanonicalRuntimeToolSchema(context, sharedRuntimeProxy);
1056
785
  }
1057
786
  : undefined;
1058
- const localControllerConfig = {
1059
- enabled: serviceConfig?.localController?.enabled ?? profile === 'controller',
1060
- host: serviceConfig?.localController?.host ?? '127.0.0.1',
1061
- port: serviceConfig?.localController?.port ?? 8766,
1062
- };
1063
- const compatibilityToolDefinitions = buildMcpToolDefinitions(toolContext.policy, { enableChatgptBrowser: opts.enableChatgptBrowser === true });
1064
787
  const toolSurface = toolContext.policy.profile === 'controller' ? FORGE_TOOL_SURFACE : `${toolContext.policy.profile}-legacy-v1`;
1065
788
  const toolSurfaceSchemaVersion = toolContext.policy.profile === 'controller' ? FORGE_MCP_SCHEMA_VERSION : 1;
1066
789
  const forgeVersion = FORGE_VERSION;
1067
- const repoId = toolContext.policy.profile === 'controller' || !repoRoot ? undefined : repositoryIdentity(repoRoot);
1068
- const startedAt = new Date().toISOString();
1069
- const localOrigin = `http://${host === '::' || host === '0.0.0.0' ? '127.0.0.1' : host}:${port}`;
1070
- const advertisedOrigin = configuredPublicOrigin ?? localOrigin;
1071
790
  const app = express();
1072
791
  app.set('trust proxy', 1);
1073
792
 
1074
- const controllerHealth = () => {
1075
- if (!('controllerHome' in toolContext)) return null;
1076
- const runtimeGeneration = currentRuntimeGeneration();
1077
- const exposure = controllerExposureSnapshot(toolContext);
1078
- // Health is a bounded diagnostic endpoint, not an MCP discovery authority.
1079
- // Session initialization and invocation obtain a live Runtime tools/list;
1080
- // health only reports the Runtime's published identity.
1081
- const runtimeFingerprint = currentRuntimeToolSurfaceFingerprint();
1082
- return {
1083
- configuredAccessMode: exposure.access.configuredAccessMode,
1084
- effectiveAccessMode: exposure.access.effectiveAccessMode,
1085
- effectiveToolset: exposure.access.effectiveToolset,
1086
- exposureRevision: exposure.access.exposureRevision,
1087
- accessModeSource: exposure.access.source,
1088
- accessModeLastAppliedAt: exposure.access.lastAppliedAt,
1089
- toolset: exposure.access.effectiveToolset,
1090
- toolSurfaceFingerprint: runtimeFingerprint,
1091
- runtimeToolSurfaceFingerprint: runtimeFingerprint,
1092
- toolCount: undefined,
1093
- generation: runtimeGeneration?.generation,
1094
- source: runtimeGeneration?.source,
1095
- runtimeIdentity: runtimeIdentitySnapshot(toolContext),
1096
- };
1097
- };
1098
-
1099
- app.get('/health', (_req, res) => {
1100
- const health = controllerHealth();
1101
- res.setHeader('x-forge-tool-surface', toolSurface);
1102
- res.setHeader('x-forge-version', String(forgeVersion));
1103
- res.setHeader('x-forge-schema-version', String(toolSurfaceSchemaVersion));
1104
- if (health?.toolset) res.setHeader('x-forge-toolset', health.toolset);
1105
- if (health?.runtimeToolSurfaceFingerprint) res.setHeader('x-forge-runtime-tool-surface-fingerprint', health.runtimeToolSurfaceFingerprint);
1106
- if (health?.toolSurfaceFingerprint) res.setHeader('x-forge-tool-surface-fingerprint', health.toolSurfaceFingerprint);
1107
- const sessionSnapshot = sessionRegistry.snapshot();
1108
- res.json({
1109
- status: 'ok',
1110
- server: 'forge-mcp',
1111
- forgeInstanceId: forgeInstance.instanceId,
1112
- ...(process.env.FORGE_MCP_INSTANCE_ID
1113
- ? { controllerInstanceId: process.env.FORGE_MCP_INSTANCE_ID }
1114
- : {}),
1115
- version: forgeVersion,
1116
- profile: toolContext.policy.profile,
1117
- toolSurface,
1118
- schemaVersion: toolSurfaceSchemaVersion,
1119
- toolSurfaceFingerprint: health?.toolSurfaceFingerprint,
1120
- runtimeToolSurfaceFingerprint: health?.runtimeToolSurfaceFingerprint,
1121
- generation: health?.generation,
1122
- source: health?.source,
1123
- // Deprecated diagnostic alias; not a Runtime schema claim.
1124
- toolset: health?.toolset ?? 'full',
1125
- gatewayToolset: health?.toolset ?? 'full',
1126
- toolCount: health?.toolCount,
1127
- compatibilityToolCount: compatibilityToolDefinitions.length,
1128
- runtimeIdentity: health?.runtimeIdentity,
1129
- configuredAccessMode: health?.configuredAccessMode,
1130
- effectiveAccessMode: health?.effectiveAccessMode,
1131
- effectiveToolset: health?.effectiveToolset,
1132
- accessModeSource: health?.accessModeSource,
1133
- accessModeLastAppliedAt: health?.accessModeLastAppliedAt,
1134
- exposureRevision: health?.exposureRevision,
1135
- ...(repoId ? { repoId } : {}),
1136
- startedAt,
1137
- runner: {
1138
- enabled: toolContext.policy.execution.agentRunner,
1139
- defaultTimeoutMs: toolContext.policy.execution.runnerTimeoutMs,
1140
- maxTimeoutMs: toolContext.policy.execution.runnerMaxTimeoutMs,
1141
- },
1142
- auth: authMode === 'oauth'
1143
- ? (oauthPassphrase ? 'oauth' : 'missing')
1144
- : authMode === 'bearer'
1145
- ? (authToken ? 'required' : 'missing')
1146
- : 'none',
1147
- ...(oauthProvider ? { oauthAuthorizationCodes: oauthProvider.authorizationCodeDiagnostics() } : {}),
1148
- mcpEndpoint: `${advertisedOrigin}/mcp`,
1149
- // Grok now completes standard OAuth dynamic registration + PKCE on the
1150
- // canonical MCP resource. Keep /mcp-grok below only as a legacy alias.
1151
- grokEndpoint: `${advertisedOrigin}/mcp`,
1152
- bearerEndpoint: `${advertisedOrigin}/mcp-bearer`,
1153
- sessions: {
1154
- ...sessionSnapshot,
1155
- initializing: runtimeStats.initializing,
1156
- activePosts: runtimeStats.activePosts,
1157
- maximumActivePosts: MAX_ACTIVE_POSTS,
1158
- rejectedOverload: runtimeStats.rejectedOverload,
1159
- },
1160
- });
1161
- });
1162
-
1163
- // Recovery probes this endpoint every few seconds. Keep it strictly in-memory
1164
- // and transport-scoped: whole-control-plane readiness below may traverse every
1165
- // repository projection and probe the local bridge, which must never become a
1166
- // periodic event-loop load generator for the public MCP Connector itself.
1167
- app.get('/transport-ready', (_req, res) => {
1168
- const sessionSnapshot = sessionRegistry.snapshot();
1169
- const sessionCapacityReady = sessionSnapshot.acceptingNewSessions
1170
- && runtimeStats.initializing < MAX_INITIALIZING_SESSIONS
1171
- && runtimeStats.activePosts < MAX_ACTIVE_POSTS;
1172
- res.status(sessionCapacityReady ? 200 : 503).json({
1173
- ready: sessionCapacityReady,
1174
- profile: toolContext.policy.profile,
1175
- gateway: sessionCapacityReady ? 'ready' : 'saturated',
1176
- sessionCapacity: sessionSnapshot,
1177
- runtimeCapacity: {
1178
- initializing: runtimeStats.initializing,
1179
- maximumInitializing: MAX_INITIALIZING_SESSIONS,
1180
- activePosts: runtimeStats.activePosts,
1181
- maximumActivePosts: MAX_ACTIVE_POSTS,
1182
- },
1183
- });
1184
- });
1185
-
1186
- app.get('/ready', async (_req, res) => {
1187
- const runtimeGeneration = currentRuntimeGeneration();
1188
- const sessionSnapshot = sessionRegistry.snapshot();
1189
- const sessionCapacityReady = sessionSnapshot.acceptingNewSessions
1190
- && runtimeStats.initializing < MAX_INITIALIZING_SESSIONS
1191
- && runtimeStats.activePosts < MAX_ACTIVE_POSTS;
1192
- if (!runtimeControllerHome) {
1193
- res.status(sessionCapacityReady ? 200 : 503).json({
1194
- ready: sessionCapacityReady,
1195
- profile: toolContext.policy.profile,
1196
- gateway: sessionCapacityReady ? 'ready' : 'saturated',
1197
- controllerDaemon: 'not-required',
1198
- sessionCapacity: sessionSnapshot,
1199
- });
1200
- return;
1201
- }
1202
- const daemon = readForgeRuntimeStatus(runtimeControllerHome);
1203
- const runtimeState = loadMcpServiceRuntimeState(runtimeControllerHome, repoRoot);
1204
- const repositories = listRepositories(runtimeControllerHome).filter((repository) => repository.enabled && !repository.removedAt);
1205
- const projectionSnapshots = repositories.map((repository) => {
1206
- const snapshot = readRepositoryProjectionSnapshot(runtimeControllerHome, repository.repoId);
1207
- const reconciliation = reconcileReadinessProjectionSource(
1208
- snapshot,
1209
- legacyIssueAuthorityRetired(repository.canonicalRoot)
1210
- ? undefined
1211
- : buildControllerTaskLedgerProjection(repository.canonicalRoot),
1212
- );
1213
- return { repoId: repository.repoId, snapshot, reconciliation };
1214
- });
1215
- const staleRepositories = projectionSnapshots
1216
- .filter(({ snapshot }) => snapshot.stale)
1217
- .map(({ repoId }) => repoId);
1218
- const blockingStaleRepositories = projectionSnapshots
1219
- .filter(({ snapshot }) => projectionBlocksReadiness(snapshot))
1220
- .map(({ repoId }) => repoId);
1221
- const sourceMismatches = projectionSnapshots
1222
- .filter(({ reconciliation }) => reconciliation.status === 'mismatch')
1223
- .map(({ repoId, reconciliation }) => ({ repoId, ...reconciliation }));
1224
- const localBridgeHealth = localControllerConfig.enabled
1225
- ? await jsonHealth(localControllerHealthUrl(localControllerConfig.host, localControllerConfig.port))
1226
- : null;
1227
- const localBridgeReady = !localControllerConfig.enabled
1228
- || localControllerDiagnosticMatchesRuntime(localBridgeHealth, runtimeGeneration?.generation);
1229
- const daemonReady = daemon.status === 'ready' && daemon.degraded !== true;
1230
- const projectionReady = blockingStaleRepositories.length === 0;
1231
- const publicConfigured = Boolean(runtimeState?.tunnel?.publicEndpoint);
1232
- const publicReady = !publicConfigured || runtimeState?.tunnel?.healthy === true;
1233
- const connectorReady = !publicConfigured || (
1234
- publicReady
1235
- && runtimeState?.tunnel?.connectorNeedsReconnect !== true
1236
- );
1237
- const ready = daemonReady && projectionReady && localBridgeReady && sessionCapacityReady;
1238
- res.status(ready ? 200 : 503).json({
1239
- ready,
1240
- generation: runtimeGeneration?.generation,
1241
- source: runtimeGeneration?.source,
1242
- gateway: { status: ready ? 'ready' : 'degraded', thin: true, eventLoopIsolatedFromWorkers: true },
1243
- controllerDaemon: daemon,
1244
- localBridge: {
1245
- enabled: localControllerConfig.enabled,
1246
- ready: localBridgeReady,
1247
- endpoint: `http://${localControllerConfig.host === '::1' ? '[::1]' : localControllerConfig.host}:${localControllerConfig.port}/`,
1248
- },
1249
- projections: {
1250
- ready: projectionReady,
1251
- repositoryCount: repositories.length,
1252
- staleRepositories,
1253
- blockingStaleRepositories,
1254
- sourceMismatches,
1255
- },
1256
- publicReadiness: {
1257
- configured: publicConfigured,
1258
- ready: publicReady,
1259
- endpoint: runtimeState?.tunnel?.publicEndpoint,
1260
- },
1261
- connectorReadiness: {
1262
- configured: publicConfigured,
1263
- ready: connectorReady,
1264
- connectorNeedsReconnect: runtimeState?.tunnel?.connectorNeedsReconnect === true,
1265
- },
1266
- sessionCapacity: sessionSnapshot,
1267
- });
1268
- });
1269
-
1270
- app.get('/repos/:repoId/health', (req, res) => {
1271
- if (!runtimeControllerHome) {
1272
- res.status(404).json({ error: 'controller profile required' });
1273
- return;
1274
- }
1275
- try {
1276
- const repository = getRepository(req.params.repoId, runtimeControllerHome, { includeRemoved: true });
1277
- const projection = readRepositoryProjectionSnapshot(runtimeControllerHome, repository.repoId);
1278
- res.json({
1279
- status: repository.enabled && !repository.removedAt ? 'ok' : 'disabled',
1280
- repository: {
1281
- repoId: repository.repoId,
1282
- checkoutId: repository.activeCheckoutId,
1283
- enabled: repository.enabled,
1284
- removedAt: repository.removedAt,
1285
- },
1286
- projection,
1287
- });
1288
- } catch (error) {
1289
- res.status(404).json({ error: error instanceof Error ? error.message : String(error) });
1290
- }
793
+ registerMcpHttpObservationRoutes({
794
+ app,
795
+ toolContext,
796
+ sessionRegistry,
797
+ runtimeStats,
798
+ runtimeControllerHome,
799
+ repoRoot,
800
+ forgeInstanceId: forgeInstance.instanceId,
801
+ currentRuntimeToolSurfaceFingerprint,
802
+ toolSurface,
803
+ toolSurfaceSchemaVersion,
804
+ forgeVersion,
805
+ authMode,
806
+ authTokenConfigured: Boolean(authToken),
807
+ oauthPassphraseConfigured: Boolean(oauthPassphrase),
808
+ oauthAuthorizationCodeDiagnostics: oauthProvider ? () => oauthProvider.authorizationCodeDiagnostics() : undefined,
809
+ configuredPublicOrigin,
810
+ host,
811
+ port,
812
+ enableChatgptBrowser: opts.enableChatgptBrowser === true,
813
+ localController: {
814
+ enabled: serviceConfig?.localController?.enabled ?? profile === 'controller',
815
+ host: serviceConfig?.localController?.host ?? '127.0.0.1',
816
+ port: serviceConfig?.localController?.port ?? 8766,
817
+ },
818
+ maxInitializingSessions: MAX_INITIALIZING_SESSIONS,
819
+ maxActivePosts: MAX_ACTIVE_POSTS,
1291
820
  });
1292
821
 
1293
822
  if (authMode === 'oauth' && oauthProvider) {
1294
- app.use('/authorize', express.urlencoded({ extended: false, limit: '10kb' }));
1295
- app.use('/authorize', oauthTraceMiddleware('authorize'));
1296
- // Reject incomplete OAuth requests before rendering the passphrase form.
1297
- app.use('/authorize', rejectIncompleteOAuthAuthorize);
1298
- app.use('/authorize', requirePassphrase(oauthPassphrase ?? ''));
1299
- app.use('/authorize', oauthAuthorizationHandler(oauthProvider));
1300
- app.use('/token', oauthTraceMiddleware('token'));
1301
- app.use('/token', tokenHandler({ provider: oauthProvider, rateLimit: false }));
1302
- app.use('/revoke', oauthTraceMiddleware('revoke'));
1303
- app.use('/revoke', revocationHandler({ provider: oauthProvider, rateLimit: false }));
1304
- app.use('/register', oauthTraceMiddleware('register'));
1305
- app.use('/register', clientRegistrationHandler({ clientsStore: oauthProvider.clientsStore, rateLimit: false }));
1306
- app.get('/.well-known/oauth-authorization-server', (req, res) => {
1307
- const origin = getPublicOrigin(req, configuredPublicOrigin);
1308
- res.json({
1309
- issuer: origin,
1310
- authorization_endpoint: `${origin}/authorize`,
1311
- token_endpoint: `${origin}/token`,
1312
- revocation_endpoint: `${origin}/revoke`,
1313
- registration_endpoint: `${origin}/register`,
1314
- response_types_supported: ['code'],
1315
- grant_types_supported: ['authorization_code', 'refresh_token'],
1316
- code_challenge_methods_supported: ['S256'],
1317
- token_endpoint_auth_methods_supported: ['client_secret_post', 'none'],
1318
- scopes_supported: ['forge'],
1319
- });
1320
- });
1321
- app.get('/.well-known/openid-configuration', (req, res) => {
1322
- const origin = getPublicOrigin(req, configuredPublicOrigin);
1323
- res.json({
1324
- issuer: origin,
1325
- authorization_endpoint: `${origin}/authorize`,
1326
- token_endpoint: `${origin}/token`,
1327
- registration_endpoint: `${origin}/register`,
1328
- response_types_supported: ['code'],
1329
- grant_types_supported: ['authorization_code', 'refresh_token'],
1330
- code_challenge_methods_supported: ['S256'],
1331
- token_endpoint_auth_methods_supported: ['client_secret_post', 'none'],
1332
- scopes_supported: ['forge'],
1333
- });
1334
- });
1335
- const protectedResourceMetadata = (resourcePath: '/mcp' | '/mcp-grok' | '/mcp-bearer') => (req: Request, res: Response): void => {
1336
- const origin = getPublicOrigin(req, configuredPublicOrigin);
1337
- res.json({
1338
- resource: `${origin}${resourcePath}`,
1339
- authorization_servers: [origin],
1340
- scopes_supported: ['forge'],
1341
- bearer_methods_supported: ['header'],
1342
- });
1343
- };
1344
- app.get('/.well-known/oauth-protected-resource/mcp', protectedResourceMetadata('/mcp'));
1345
- app.get('/.well-known/oauth-protected-resource/mcp-grok', protectedResourceMetadata('/mcp-grok'));
1346
- app.get('/.well-known/oauth-protected-resource/mcp-bearer', protectedResourceMetadata('/mcp-bearer'));
823
+ registerMcpOAuthHttpRoutes(app, oauthProvider, oauthPassphrase ?? '', configuredPublicOrigin);
1347
824
  }
1348
825
 
1349
826
  const setMcpResponseHeaders = (_req: Request, res: Response, next: NextFunction): void => {