@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.
Files changed (685) hide show
  1. package/AGENTS.md +58 -62
  2. package/CHANGELOG.md +26 -0
  3. package/CLAUDE.md +42 -62
  4. package/README.md +2 -2
  5. package/SKILL.md +0 -1
  6. package/THIRD_PARTY_NOTICES.md +5 -2
  7. package/adapters/browser/index.ts +2 -0
  8. package/adapters/chatgpt/browser-delivery-host.ts +71 -0
  9. package/adapters/chatgpt/browser-delivery-runtime.ts +783 -0
  10. package/adapters/chatgpt/controller-binding-store.ts +55 -0
  11. package/adapters/chatgpt/controller-host.ts +119 -0
  12. package/adapters/chatgpt/controller-round-host.ts +78 -0
  13. package/adapters/chatgpt/controller-round-settlement-store.ts +45 -0
  14. package/adapters/chatgpt/provider-delivery.ts +113 -0
  15. package/adapters/chatgpt/work-conversation-binding-store.ts +192 -0
  16. package/adapters/chatgpt/wsl-bridge-delivery-host.ts +63 -0
  17. package/adapters/computer/desktop-operator-contract.ts +7 -0
  18. package/adapters/computer/desktop-operator-discovery.ts +201 -0
  19. package/adapters/computer/desktop-operator-negotiation.ts +174 -0
  20. package/adapters/computer/desktop-operator-provider.ts +312 -0
  21. package/adapters/computer/index.ts +1 -0
  22. package/adapters/computer/interaction-target-authority.ts +944 -0
  23. package/adapters/controller-process/binding-store.ts +34 -0
  24. package/adapters/controller-process/controller-host.ts +26 -0
  25. package/adapters/mcp/access-mode.ts +132 -0
  26. package/adapters/mcp/audit.ts +32 -0
  27. package/adapters/mcp/auth.ts +407 -0
  28. package/adapters/mcp/controller-round-compatibility.ts +184 -0
  29. package/adapters/mcp/frozen-client-semantic-compatibility.ts +292 -0
  30. package/adapters/mcp/instructions.ts +36 -0
  31. package/adapters/mcp/legacy-context.ts +18 -0
  32. package/adapters/mcp/multi-repository.ts +437 -0
  33. package/adapters/mcp/oauth.ts +391 -0
  34. package/adapters/mcp/paths.ts +140 -0
  35. package/adapters/mcp/policy.ts +232 -0
  36. package/adapters/mcp/proxy-env.ts +62 -0
  37. package/adapters/mcp/readiness-projection.ts +24 -0
  38. package/adapters/mcp/redaction.ts +21 -0
  39. package/adapters/mcp/repo.ts +24 -0
  40. package/adapters/mcp/runtime-gateway/artifact-adapter.ts +52 -0
  41. package/adapters/mcp/runtime-gateway/context-adapter.ts +829 -0
  42. package/adapters/mcp/runtime-gateway/controller-authority-adapter.ts +191 -0
  43. package/adapters/mcp/runtime-gateway/engineering-preconditions.ts +147 -0
  44. package/adapters/mcp/runtime-gateway/execution-control-adapter.ts +28 -0
  45. package/adapters/mcp/runtime-gateway/execution-tools.ts +207 -0
  46. package/adapters/mcp/runtime-gateway/filesystem-adapter.ts +38 -0
  47. package/adapters/mcp/runtime-gateway/gateway-execution-adapter.ts +411 -0
  48. package/adapters/mcp/runtime-gateway/legacy-ios-tool-adapter.ts +105 -0
  49. package/adapters/mcp/runtime-gateway/maintenance-adapter.ts +44 -0
  50. package/adapters/mcp/runtime-gateway/model-adapter.ts +63 -0
  51. package/adapters/mcp/runtime-gateway/persisted-check-process.ts +3 -0
  52. package/adapters/mcp/runtime-gateway/plugin-adapter.ts +308 -0
  53. package/adapters/mcp/runtime-gateway/process-tools.ts +289 -0
  54. package/adapters/mcp/runtime-gateway/protected-computer-adapter.ts +76 -0
  55. package/adapters/mcp/runtime-gateway/recovery-adapter.ts +268 -0
  56. package/adapters/mcp/runtime-gateway/recovery-client-adapter.ts +70 -0
  57. package/adapters/mcp/runtime-gateway/recovery-tool-contract.ts +99 -0
  58. package/adapters/mcp/runtime-gateway/repository-compat-adapter.ts +110 -0
  59. package/adapters/mcp/runtime-gateway/result-adapter.ts +108 -0
  60. package/adapters/mcp/runtime-gateway/router.ts +728 -0
  61. package/adapters/mcp/runtime-gateway/runtime-observation-adapter.ts +1251 -0
  62. package/adapters/mcp/runtime-gateway/runtime-tool-definitions.ts +634 -0
  63. package/adapters/mcp/runtime-gateway/runtime-tool-shared.ts +27 -0
  64. package/adapters/mcp/runtime-gateway/runtime-tools.ts +68 -0
  65. package/adapters/mcp/runtime-gateway/scheduler-adapter.ts +186 -0
  66. package/adapters/mcp/runtime-gateway/shared-adapter.ts +25 -0
  67. package/adapters/mcp/runtime-gateway/status-inbox-adapter.ts +870 -0
  68. package/adapters/mcp/runtime-gateway/work-adapter.ts +3186 -0
  69. package/adapters/mcp/runtime-gateway/work-compat-adapter.ts +502 -0
  70. package/adapters/mcp/runtime-gateway/work-validation-reconciler.ts +1 -0
  71. package/adapters/mcp/server.ts +1048 -0
  72. package/adapters/mcp/setup.ts +791 -0
  73. package/adapters/mcp/tool-capability-cache.ts +87 -0
  74. package/adapters/mcp/tool-mapping/access-tools.ts +387 -0
  75. package/adapters/mcp/tool-mapping/legacy-tool-service.ts +5265 -0
  76. package/adapters/mcp/tool-mapping/post-finalize-work-attribution.ts +150 -0
  77. package/adapters/mcp/tool-mapping/repository-tools.ts +1691 -0
  78. package/adapters/mcp/tool-mapping/tools.ts +9 -0
  79. package/adapters/mcp/tool-mapping/umbrella-tools.ts +117 -0
  80. package/adapters/mcp/toolset-names.ts +74 -0
  81. package/adapters/mcp/toolset-selection.ts +41 -0
  82. package/adapters/mcp/toolset.ts +336 -0
  83. package/adapters/mcp/transports/http.ts +1456 -0
  84. package/adapters/mcp/transports/session-registry.ts +513 -0
  85. package/adapters/mcp/transports/stdio.ts +8 -0
  86. package/adapters/mcp/tunnels/openai-secure-tunnel.ts +139 -0
  87. package/adapters/mcp/types.ts +41 -0
  88. package/assets/AGENTS.md +2 -16
  89. package/assets/CLAUDE.md +2 -16
  90. package/assets/hooks/AGENTS.md +3 -14
  91. package/assets/hooks/CLAUDE.md +3 -14
  92. package/assets/hooks/hook-input.sh +9 -2
  93. package/assets/hooks/lib/workflow-state.sh +28 -10
  94. package/assets/hooks/prompt-guard.sh +63 -37
  95. package/assets/hooks/security-sentinel.sh +2 -1
  96. package/assets/hooks/session-start-context.sh +7 -5
  97. package/assets/hooks/stop-orchestrator.sh +13 -3
  98. package/assets/initializer-question-pack.v4.json +11 -7
  99. package/assets/partials/05-workflow.partial.md +18 -73
  100. package/assets/partials/07-footer.partial.md +1 -1
  101. package/assets/partials-agents/02-operating-mode.partial.md +1 -2
  102. package/assets/partials-agents/04-task-protocol.partial.md +14 -59
  103. package/assets/partials-agents/06-quality-safety.partial.md +11 -16
  104. package/assets/partials-agents/08-deep-docs.partial.md +1 -2
  105. package/assets/plan-map.json +11 -11
  106. package/assets/plugin-registry.v1.json +9 -3
  107. package/assets/plugin-trust-policy.v1.json +6 -0
  108. package/assets/recovery/ForgeRecovery.ps1 +100 -0
  109. package/assets/recovery/forge-wsl-rescue +271 -0
  110. package/assets/reference-configs/development-protocol.md +11 -15
  111. package/assets/reference-configs/document-generation.md +1 -1
  112. package/assets/reference-configs/global-working-rules.md +9 -1
  113. package/assets/reference-configs/harness-overview.md +22 -112
  114. package/assets/reference-configs/hook-operations.md +1 -1
  115. package/assets/reference-configs/sprint-contracts.md +2 -2
  116. package/assets/skill-version.json +2 -2
  117. package/assets/templates/helpers/architecture-event.ts +3 -34
  118. package/assets/templates/helpers/architecture-queue.sh +61 -10
  119. package/assets/templates/helpers/archive-workflow.sh +90 -180
  120. package/assets/templates/helpers/capability-config.ts +8 -11
  121. package/assets/templates/helpers/capability-resolver.ts +37 -19
  122. package/assets/templates/helpers/capture-plan.sh +2 -2
  123. package/assets/templates/helpers/check-architecture-sync.sh +1 -9
  124. package/assets/templates/helpers/check-task-sync.sh +82 -11
  125. package/assets/templates/helpers/check-task-workflow.sh +1 -3
  126. package/assets/templates/helpers/context-contract-sync.sh +1 -12
  127. package/assets/templates/helpers/contract-worktree.sh +26 -24
  128. package/assets/templates/helpers/ensure-task-workflow.sh +6 -8
  129. package/assets/templates/helpers/inspect-project-state.ts +1 -1
  130. package/assets/templates/helpers/migrate-project-template.sh +1 -1
  131. package/assets/templates/helpers/migrate-workflow-docs.ts +53 -239
  132. package/assets/templates/helpers/new-plan.sh +2 -2
  133. package/assets/templates/helpers/plan-to-todo.sh +3 -31
  134. package/assets/templates/helpers/sprint-backlog.sh +1 -1
  135. package/assets/templates/helpers/switch-plan.sh +1 -1
  136. package/assets/templates/helpers/workflow-contract.ts +1 -0
  137. package/assets/workflow-contract.v1.json +63 -171
  138. package/assets/workflows/xiaohongshu/xiaohongshu.collect-metrics.v1.draft.json +118 -0
  139. package/assets/workflows/xiaohongshu/xiaohongshu.publish-image-note.v1.draft.json +316 -0
  140. package/assets/workflows/xiaohongshu/xiaohongshu.publish-long-text.v1.draft.json +324 -0
  141. package/docs/architecture/CURRENT.md +148 -13
  142. package/docs/architecture/EVOLUTION.md +13 -0
  143. package/docs/architecture/decisions/20260801-controller-home-sqlite-state.md +2 -1
  144. package/docs/architecture/decisions/20260831-independent-windows-wsl-host-rescue.md +59 -0
  145. package/docs/architecture/decisions/20260904-ios-device-migration-baseline.md +120 -0
  146. package/docs/architecture/decisions/20260904-repository-runtime-data-boundary.md +42 -0
  147. package/docs/architecture/decisions/20260907-assistant-learning-loop.md +35 -0
  148. package/docs/architecture/decisions/20260907-controller-round-transition-policy.md +296 -0
  149. package/docs/architecture/decisions/20260907-maintenance-diagnosis-bounded-read.md +39 -0
  150. package/docs/architecture/domains/verification.md +7 -0
  151. package/docs/architecture/domains/workflow-engine.md +1 -1
  152. package/docs/architecture/index.md +3 -3
  153. package/docs/architecture/modules/controller-runtime/transport-lifecycle.md +19 -8
  154. package/docs/architecture/requests/2026-09-04-computer-plugin-workflow-taxonomy.md +124 -0
  155. package/docs/forge-chatgpt-mcp-setup.md +5 -3
  156. package/docs/forge-plugin-management.md +20 -4
  157. package/docs/forge-runtime-self-healing-loop.md +2 -0
  158. package/docs/operations/benchmark-cross-version-freeze.md +38 -0
  159. package/docs/operations/controller-browser-plugin.md +3 -1
  160. package/docs/operations/homebrew.md +2 -2
  161. package/docs/operations/platform-support.md +16 -8
  162. package/docs/operations/platform-support.zh-CN.md +2 -2
  163. package/docs/operations/standalone-disaster-recovery.md +26 -1
  164. package/docs/operations/uu-remote-rescue.md +41 -0
  165. package/docs/operations/windows-wsl-host-rescue.md +133 -0
  166. package/docs/public-usage-guide.md +1 -1
  167. package/docs/public-usage-guide.zh-CN.md +1 -1
  168. package/docs/tutorials/01-install-and-start.md +1 -1
  169. package/docs/tutorials/01-install-and-start.zh-CN.md +1 -1
  170. package/docs/tutorials/02-connect-chatgpt.md +4 -4
  171. package/docs/tutorials/02-connect-chatgpt.zh-CN.md +4 -4
  172. package/docs/wiki/Implementation.md +1 -1
  173. package/package.json +23 -14
  174. package/packages/kernel/controller/api/index.ts +2 -0
  175. package/packages/kernel/controller/application/controller-round-service.ts +29 -0
  176. package/packages/kernel/controller/application/controller-service.ts +30 -0
  177. package/packages/kernel/controller/domain/controller-round-transition-policy.ts +308 -0
  178. package/packages/kernel/controller/domain/controller-round.ts +107 -0
  179. package/packages/kernel/controller/domain/execution-quality.ts +133 -0
  180. package/packages/kernel/controller/domain/types.ts +56 -0
  181. package/packages/kernel/controller/index.ts +1 -0
  182. package/packages/kernel/controller/infrastructure/controller-binding-store.ts +84 -0
  183. package/packages/kernel/controller/infrastructure/controller-round-store.ts +1384 -0
  184. package/packages/kernel/controller/infrastructure/controller-session-store.ts +578 -0
  185. package/packages/kernel/controller/ports/controller-host.ts +16 -0
  186. package/packages/kernel/goal/api/index.ts +1 -0
  187. package/packages/kernel/goal/domain/types.ts +40 -0
  188. package/packages/kernel/goal/index.ts +1 -0
  189. package/packages/kernel/identity/api/index.ts +5 -0
  190. package/packages/kernel/identity/application/identity-service.ts +67 -0
  191. package/packages/kernel/identity/domain/scope.ts +63 -0
  192. package/packages/kernel/identity/domain/semantic-sync.ts +81 -0
  193. package/packages/kernel/identity/domain/types.ts +44 -0
  194. package/packages/kernel/identity/domain/workspace.ts +56 -0
  195. package/packages/kernel/identity/index.ts +1 -0
  196. package/packages/kernel/identity/infrastructure/identity-store.ts +75 -0
  197. package/packages/kernel/memory/api/index.ts +5 -0
  198. package/packages/kernel/memory/application/experience-service.ts +118 -0
  199. package/packages/kernel/memory/application/outcome-service.ts +77 -0
  200. package/packages/kernel/memory/domain/experience.ts +103 -0
  201. package/packages/kernel/memory/domain/operational-prior.ts +92 -0
  202. package/packages/kernel/memory/index.ts +1 -0
  203. package/packages/kernel/progression/api/index.ts +2 -0
  204. package/packages/kernel/progression/application/projector.ts +211 -0
  205. package/packages/kernel/progression/domain/types.ts +116 -0
  206. package/packages/kernel/progression/index.ts +1 -0
  207. package/packages/kernel/scheduler/api/index.ts +6 -0
  208. package/packages/kernel/scheduler/application/continuation-service.ts +212 -0
  209. package/packages/kernel/scheduler/application/eligibility.ts +252 -0
  210. package/packages/kernel/scheduler/application/schedule-service.ts +24 -0
  211. package/packages/kernel/scheduler/application/settlement.ts +197 -0
  212. package/packages/kernel/scheduler/domain/continuation.ts +38 -0
  213. package/packages/kernel/scheduler/domain/schedule.ts +149 -0
  214. package/packages/kernel/scheduler/index.ts +1 -0
  215. package/packages/kernel/scheduler/infrastructure/continuation-dispatch-store.ts +38 -0
  216. package/packages/kernel/scheduler/infrastructure/schedule-store.ts +785 -0
  217. package/packages/kernel/work/api/index.ts +15 -0
  218. package/packages/kernel/work/application/work-service.ts +148 -0
  219. package/packages/kernel/work/domain/admission-policy.ts +34 -0
  220. package/packages/kernel/work/domain/check-receipt.ts +43 -0
  221. package/packages/kernel/work/domain/context-closure-receipt.ts +32 -0
  222. package/packages/kernel/work/domain/engineering-contracts.ts +157 -0
  223. package/packages/kernel/work/domain/engineering-design.ts +281 -0
  224. package/packages/kernel/work/domain/engineering-profile.ts +242 -0
  225. package/packages/kernel/work/domain/execution-concurrency.ts +254 -0
  226. package/packages/kernel/work/domain/execution-snapshot.ts +33 -0
  227. package/packages/kernel/work/domain/implementation-review.ts +510 -0
  228. package/packages/kernel/work/domain/project-engineering-contract.ts +202 -0
  229. package/packages/kernel/work/domain/repository-completion-receipt.ts +73 -0
  230. package/packages/kernel/work/domain/state-machine.ts +259 -0
  231. package/packages/kernel/work/domain/types.ts +528 -0
  232. package/packages/kernel/work/index.ts +1 -0
  233. package/packages/kernel/work/infrastructure/work-contract-store.ts +1876 -0
  234. package/packages/kernel/work/ports/work-contract-store.ts +18 -0
  235. package/packages/plugin-runtime/browser/index.ts +3 -0
  236. package/packages/plugin-runtime/browser/provider-registry.ts +110 -0
  237. package/packages/plugin-runtime/browser/runtime-contract.ts +150 -0
  238. package/packages/plugin-runtime/browser/session-authority.ts +61 -0
  239. package/packages/plugin-runtime/computer/index.ts +6 -0
  240. package/packages/plugin-runtime/computer/provider-error.ts +26 -0
  241. package/packages/plugin-runtime/computer/provider-registration.ts +52 -0
  242. package/packages/plugin-runtime/computer/provider-registry.ts +63 -0
  243. package/packages/plugin-runtime/computer/provider.ts +19 -0
  244. package/packages/plugin-runtime/computer/target-authority.ts +190 -0
  245. package/packages/plugin-runtime/computer/target-persistence.ts +54 -0
  246. package/packages/plugin-runtime/external/index.ts +1 -0
  247. package/packages/plugin-runtime/external/unix-jsonl-transport.ts +457 -0
  248. package/packages/plugin-runtime/index.ts +3 -0
  249. package/packages/protocols/browser/index.ts +1 -0
  250. package/packages/protocols/browser/session.ts +98 -0
  251. package/packages/protocols/computer/contract.ts +199 -0
  252. package/packages/protocols/computer/index.ts +1 -0
  253. package/packages/protocols/failure/contract.ts +158 -0
  254. package/packages/protocols/failure/index.ts +1 -0
  255. package/packages/protocols/handoff/index.ts +1 -0
  256. package/packages/protocols/handoff/status.ts +22 -0
  257. package/packages/protocols/mcp/execution-context.ts +11 -0
  258. package/packages/protocols/mcp/index.ts +2 -0
  259. package/packages/protocols/mcp/tool-contract.ts +42 -0
  260. package/packages/workflow-runtime/api/index.ts +2 -0
  261. package/packages/workflow-runtime/application/interpreter.ts +342 -0
  262. package/packages/workflow-runtime/domain/workflow-asset.ts +223 -0
  263. package/packages/workflow-runtime/index.ts +1 -0
  264. package/scripts/assemble-template.ts +1 -1
  265. package/scripts/benchmark-computer-hotpath.ts +255 -0
  266. package/scripts/benchmark-controller-round2.ts +54 -2
  267. package/scripts/benchmark-ios-agent-device.ts +3 -6
  268. package/scripts/benchmark-mcp-transport.ts +3 -4
  269. package/scripts/benchmark-operational-memory-activation.ts +235 -0
  270. package/scripts/benchmark-operational-shadow.ts +28 -0
  271. package/scripts/bootstrap-runtime-maintenance-recovery.sh +4 -1
  272. package/scripts/canary-chatgpt-provider-delivery.ts +97 -0
  273. package/scripts/canary-controller-semantic-wait.ts +195 -0
  274. package/scripts/canary-global-hook.sh +2 -2
  275. package/scripts/check-mcp-compatibility.ts +173 -7
  276. package/scripts/check-platform-support.mjs +28 -2
  277. package/scripts/check-public-docs.sh +1 -1
  278. package/scripts/check-release-readiness.sh +1 -1
  279. package/scripts/check-repository-hygiene.mjs +92 -0
  280. package/scripts/check-runtime-architecture.mjs +1206 -95
  281. package/scripts/check-source-duplication.mjs +9 -1
  282. package/scripts/check-tarball-install-smoke.sh +98 -15
  283. package/scripts/controller-ngrok-rotation.sh +5 -4
  284. package/scripts/create-project-dirs.sh +10 -74
  285. package/scripts/forge-local-recovery-helper.mjs +204 -0
  286. package/scripts/forge-uu-remote-rescue-helper.d.mts +31 -0
  287. package/scripts/forge-uu-remote-rescue-helper.mjs +409 -0
  288. package/scripts/forge-windows-host-recovery-helper.mjs +136 -0
  289. package/scripts/hook-shim.sh +2 -2
  290. package/scripts/init-project.sh +12 -70
  291. package/scripts/install-desktop-operator-registration.ts +4 -2
  292. package/scripts/install-independent-windows-wsl-recovery.ts +125 -0
  293. package/scripts/install-standalone-recovery.ts +122 -10
  294. package/scripts/install-uu-remote-rescue-registration.ts +74 -0
  295. package/scripts/install-windows-host-recovery-registration.ts +22 -0
  296. package/scripts/lib/controller-home-env.sh +6 -3
  297. package/scripts/lib/project-init-lib.sh +39 -582
  298. package/scripts/migrate-chatgpt-automation-schedules.ts +2 -2
  299. package/scripts/migrate-windows-wsl-controller-home.ts +306 -0
  300. package/scripts/open-source-audit-allowlist.txt +12 -1
  301. package/scripts/portability-bindings.mjs +70 -0
  302. package/scripts/run-bounded-command.mjs +55 -0
  303. package/scripts/run-bun-test-file.ts +22 -6
  304. package/scripts/run-governed-gate.ts +4 -1
  305. package/scripts/smoke-mcp-http-runtime.ts +1 -1
  306. package/scripts/smoke-runtime-recovery.ts +34 -1
  307. package/scripts/smoke-schedule-engine.ts +1 -1
  308. package/scripts/stage-runtime-release.ts +6 -1
  309. package/scripts/test-governance.ts +51 -2
  310. package/src/cli/agent-jobs/job-manager.ts +10 -1
  311. package/src/cli/chatgpt-browser/bind-server.ts +2 -1
  312. package/src/cli/chatgpt-browser/binding.ts +20 -10
  313. package/src/cli/chatgpt-browser/bridge-extension.ts +75 -0
  314. package/src/cli/chatgpt-browser/bridge-provider.ts +376 -11
  315. package/src/cli/chatgpt-browser/engine.ts +10 -11
  316. package/src/cli/chatgpt-browser/native-provider.ts +24 -83
  317. package/src/cli/chatgpt-browser/oracle-provider.ts +5 -3
  318. package/src/cli/chatgpt-browser/types.ts +1 -0
  319. package/src/cli/commands/bootstrap-control.ts +334 -0
  320. package/src/cli/commands/brain-assistant.ts +38 -0
  321. package/src/cli/commands/brain.ts +193 -40
  322. package/src/cli/commands/chatgpt.ts +127 -0
  323. package/src/cli/commands/computer.ts +437 -0
  324. package/src/cli/commands/doctor.ts +18 -1
  325. package/src/cli/commands/init-hook.ts +71 -61
  326. package/src/cli/commands/init.ts +3 -1
  327. package/src/cli/commands/mcp.ts +16 -10
  328. package/src/cli/commands/plugin.ts +38 -12
  329. package/src/cli/commands/recovery.ts +493 -80
  330. package/src/cli/commands/runtime.ts +9 -4
  331. package/src/cli/commands/setup-profile.ts +89 -39
  332. package/src/cli/commands/status.ts +2 -1
  333. package/src/cli/controller/check-runner.ts +487 -98
  334. package/src/cli/controller/context/types.ts +7 -1
  335. package/src/cli/controller/context-pack.ts +23 -3
  336. package/src/cli/controller/issue-store.ts +1 -1
  337. package/src/cli/controller/systemd-user.ts +110 -0
  338. package/src/cli/controller/types.ts +10 -70
  339. package/src/cli/controller/worklog.ts +67 -13
  340. package/src/cli/editing/edit-operation-contract.ts +92 -0
  341. package/src/cli/editing/edit-session.ts +289 -13
  342. package/src/cli/github/plugin.ts +17 -12
  343. package/src/cli/hook/route-registry.ts +3 -5
  344. package/src/cli/hook/runtime.ts +49 -3
  345. package/src/cli/index.ts +3 -0
  346. package/src/cli/local-bridge/connector-freshness.ts +2 -2
  347. package/src/cli/local-bridge/console-automations.ts +2 -2
  348. package/src/cli/local-bridge/facade-api.ts +41 -14
  349. package/src/cli/local-bridge/job-store.ts +1 -1
  350. package/src/cli/local-bridge/server.ts +24 -27
  351. package/src/cli/mcp/access-mode.ts +2 -132
  352. package/src/cli/mcp/access-tools.ts +2 -387
  353. package/src/cli/mcp/audit.ts +2 -32
  354. package/src/cli/mcp/auth.ts +2 -404
  355. package/src/cli/mcp/instructions.ts +2 -34
  356. package/src/cli/mcp/legacy-context.ts +2 -16
  357. package/src/cli/mcp/legacy-tool-service.ts +2 -5308
  358. package/src/cli/mcp/multi-repository.ts +2 -436
  359. package/src/cli/mcp/oauth.ts +2 -375
  360. package/src/cli/mcp/openai-secure-tunnel.ts +2 -0
  361. package/src/cli/mcp/paths.ts +2 -140
  362. package/src/cli/mcp/policy.ts +2 -232
  363. package/src/cli/mcp/post-finalize-work-attribution.ts +2 -150
  364. package/src/cli/mcp/proxy-env.ts +2 -62
  365. package/src/cli/mcp/readiness-projection.ts +2 -24
  366. package/src/cli/mcp/redaction.ts +2 -21
  367. package/src/cli/mcp/repo.ts +2 -24
  368. package/src/cli/mcp/repository-tools.ts +2 -1613
  369. package/src/cli/mcp/server.ts +2 -951
  370. package/src/cli/mcp/setup.ts +2 -737
  371. package/src/cli/mcp/tool-capability-cache.ts +2 -87
  372. package/src/cli/mcp/tool-contract.ts +2 -14
  373. package/src/cli/mcp/tools.ts +2 -9
  374. package/src/cli/mcp/toolset-names.ts +2 -70
  375. package/src/cli/mcp/toolset-selection.ts +2 -41
  376. package/src/cli/mcp/toolset.ts +2 -336
  377. package/src/cli/mcp/transports/AGENTS.md +1 -10
  378. package/src/cli/mcp/transports/CLAUDE.md +1 -10
  379. package/src/cli/mcp/transports/http.ts +2 -1414
  380. package/src/cli/mcp/transports/session-registry.ts +2 -490
  381. package/src/cli/mcp/transports/stdio.ts +2 -8
  382. package/src/cli/mcp/types.ts +2 -41
  383. package/src/cli/mcp/umbrella-tools.ts +2 -117
  384. package/src/cli/repositories/command-executor.ts +64 -18
  385. package/src/cli/repositories/command-scope.ts +117 -1
  386. package/src/cli/repositories/controller-home.ts +119 -1
  387. package/src/cli/repositories/identity.ts +13 -0
  388. package/src/cli/repositories/registry.ts +68 -49
  389. package/src/cli/repositories/repository-snapshot.ts +53 -7
  390. package/src/cli/repositories/runtime-storage.ts +27 -2
  391. package/src/cli/repositories/safe-patch.ts +34 -38
  392. package/src/cli/repositories/selected-path-actions.ts +22 -2
  393. package/src/cli/repositories/structured-git.ts +105 -25
  394. package/src/cli/runtime/helper-runner.ts +7 -0
  395. package/src/cli/runtime-invocation.ts +8 -1
  396. package/src/cli/tools/codegraph.ts +44 -25
  397. package/src/core/adoption/manifest-templates.ts +1 -1
  398. package/src/core/adoption/plan.ts +3 -26
  399. package/src/core/adoption/workflow-contract-plan.ts +17 -9
  400. package/src/effects/process-runner.ts +8 -14
  401. package/src/runtime/context/assistant-context.ts +154 -0
  402. package/src/runtime/context/assistant-work-context.ts +147 -0
  403. package/src/runtime/context/codegraph-cache-boundary.ts +255 -0
  404. package/src/runtime/context/codegraph-read-provider.ts +55 -22
  405. package/src/runtime/context/codegraph-sidecar.cjs +8 -1
  406. package/src/runtime/context/context-closure.ts +268 -0
  407. package/src/runtime/context/context-plane.ts +211 -0
  408. package/src/runtime/context/project-engineering-contract.ts +60 -0
  409. package/src/runtime/control-plane/bootstrap/capability-provider.ts +74 -0
  410. package/src/runtime/control-plane/bootstrap/control-api.ts +57 -0
  411. package/src/runtime/control-plane/bootstrap/index.ts +5 -0
  412. package/src/runtime/control-plane/bootstrap/state-machine.ts +141 -0
  413. package/src/runtime/control-plane/bootstrap/store.ts +21 -0
  414. package/src/runtime/control-plane/bootstrap/types.ts +87 -0
  415. package/src/runtime/control-plane/cleanup-artifact-retention.ts +328 -0
  416. package/src/runtime/control-plane/codegraph-cache-retention.ts +164 -0
  417. package/src/runtime/control-plane/concurrency/work-execution-concurrency.ts +320 -0
  418. package/src/runtime/control-plane/execution/content-equivalent-commit-authority.ts +169 -0
  419. package/src/runtime/control-plane/execution/controller-authority-recovery.ts +350 -0
  420. package/src/runtime/control-plane/execution/direct-canonical-work-reconciliation.ts +85 -0
  421. package/src/runtime/control-plane/execution/direct-edit-work-completion.ts +369 -99
  422. package/src/runtime/control-plane/execution/edit-validation-coordinator.ts +103 -15
  423. package/src/runtime/control-plane/execution/implementation-review-content.ts +95 -0
  424. package/src/runtime/control-plane/execution/repository-work-attribution.ts +2 -2
  425. package/src/runtime/control-plane/execution/retained-work-resume.ts +336 -6
  426. package/src/runtime/control-plane/execution/validation.ts +28 -3
  427. package/src/runtime/control-plane/execution/work-authority-reconciler.ts +116 -0
  428. package/src/runtime/control-plane/execution/work-completion-authority.ts +44 -2
  429. package/src/runtime/control-plane/execution/work-evidence-policy.ts +34 -3
  430. package/src/runtime/control-plane/execution/work-execution-support.ts +42 -22
  431. package/src/runtime/control-plane/execution/work-finalization-service.ts +1103 -166
  432. package/src/runtime/control-plane/execution/work-handle-authority.ts +219 -22
  433. package/src/runtime/control-plane/execution/work-handle-store.ts +77 -0
  434. package/src/runtime/control-plane/execution/work-head-settlement.ts +75 -0
  435. package/src/runtime/control-plane/execution/work-lifecycle-audit.ts +145 -23
  436. package/src/runtime/control-plane/execution/work-operation-service.ts +49 -13
  437. package/src/runtime/control-plane/execution/work-preparation-service.ts +31 -15
  438. package/src/runtime/control-plane/execution/work-remote-delivery.ts +1 -1
  439. package/src/runtime/control-plane/execution/work-task-receipt.ts +20 -1
  440. package/src/runtime/control-plane/execution/work-terminal-cleanup.ts +407 -76
  441. package/src/runtime/control-plane/execution/work-validation-reconciler.ts +88 -18
  442. package/src/runtime/control-plane/execution/work-verification-context.ts +1 -1
  443. package/src/runtime/control-plane/execution/work-verification-service.ts +258 -4
  444. package/src/runtime/control-plane/execution/work-verification-snapshot.ts +200 -15
  445. package/src/runtime/control-plane/facade/capability-registry.ts +21 -10
  446. package/src/runtime/control-plane/facade/controller-round-relay.ts +3 -1118
  447. package/src/runtime/control-plane/facade/controller-session-store.ts +2 -488
  448. package/src/runtime/control-plane/facade/goal-workloop-access.ts +6 -3
  449. package/src/runtime/control-plane/facade/goal-workloop.ts +972 -137
  450. package/src/runtime/control-plane/facade/handoff-inbox-application.ts +158 -0
  451. package/src/runtime/control-plane/facade/handoff-inbox-store.ts +10 -0
  452. package/src/runtime/control-plane/facade/index.ts +4 -2
  453. package/src/runtime/control-plane/facade/operation-digest.ts +22 -58
  454. package/src/runtime/control-plane/facade/plan-contract-store.ts +1004 -38
  455. package/src/runtime/control-plane/facade/repository-work-admission.ts +19 -1
  456. package/src/runtime/control-plane/facade/requirement-authority.ts +218 -0
  457. package/src/runtime/control-plane/facade/requirement-board.ts +6 -1
  458. package/src/runtime/control-plane/facade/rh-work-operation-contract.ts +48 -0
  459. package/src/runtime/control-plane/facade/semantic-admission.ts +8 -16
  460. package/src/runtime/control-plane/facade/suggested-actions.ts +4 -1
  461. package/src/runtime/control-plane/facade/types.ts +189 -532
  462. package/src/runtime/control-plane/facade/work-admission-policy.ts +84 -35
  463. package/src/runtime/control-plane/facade/work-contract-store.ts +2 -1043
  464. package/src/runtime/control-plane/facade/work-implementation-review.ts +2 -0
  465. package/src/runtime/control-plane/facade/work-state-machine.ts +2 -250
  466. package/src/runtime/control-plane/generated-cache-retention.ts +239 -18
  467. package/src/runtime/control-plane/global-scheduler/durable-admission.ts +12 -1
  468. package/src/runtime/control-plane/global-scheduler/maintenance.ts +76 -13
  469. package/src/runtime/control-plane/global-scheduler/periodic-cleanup-entry.ts +55 -0
  470. package/src/runtime/control-plane/global-scheduler/periodic-cleanup-process.ts +98 -0
  471. package/src/runtime/control-plane/global-scheduler/reconciliation.ts +1 -1
  472. package/src/runtime/control-plane/global-scheduler/scheduler-worker-entry.ts +10 -0
  473. package/src/runtime/control-plane/global-scheduler/scheduler.ts +173 -81
  474. package/src/runtime/control-plane/global-scheduler/worker-launch.ts +18 -6
  475. package/src/runtime/control-plane/launcher/chatgpt-round-continuation.ts +397 -0
  476. package/src/runtime/control-plane/launcher/chatgpt-work-binding-store.ts +2 -177
  477. package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +301 -560
  478. package/src/runtime/control-plane/launcher/provider-mcp-bootstrap.ts +20 -4
  479. package/src/runtime/control-plane/launcher/thin-launcher.ts +50 -4
  480. package/src/runtime/control-plane/lifecycle-retention-metrics.ts +60 -0
  481. package/src/runtime/control-plane/persistence/experience-store.ts +169 -0
  482. package/src/runtime/control-plane/persistence/operational-prior-store.ts +348 -0
  483. package/src/runtime/control-plane/persistence/requirement-store.ts +30 -22
  484. package/src/runtime/control-plane/persistence/sqlite-store.ts +545 -76
  485. package/src/runtime/control-plane/persistence/workflow-content-store.ts +98 -0
  486. package/src/runtime/control-plane/persistence/workflow-registry-store.ts +186 -0
  487. package/src/runtime/control-plane/persistence/workflow-run-store.ts +123 -0
  488. package/src/runtime/control-plane/release-retention.ts +56 -16
  489. package/src/runtime/control-plane/repository-namespace-retention.ts +208 -0
  490. package/src/runtime/control-plane/routing/route-policy.ts +19 -0
  491. package/src/runtime/control-plane/runtime-cleanup.ts +574 -37
  492. package/src/runtime/control-plane/runtime-generation.ts +6 -0
  493. package/src/runtime/control-plane/workspace/semantic-sync-service.ts +465 -0
  494. package/src/runtime/control-plane/workspace/workspace-store.ts +80 -0
  495. package/src/runtime/diagnostics/incident-repair.ts +406 -0
  496. package/src/runtime/diagnostics/mcp-timing.ts +125 -6
  497. package/src/runtime/diagnostics/performance.ts +6 -14
  498. package/src/runtime/diagnostics/read-only-tool.ts +1 -1
  499. package/src/runtime/evidence/artifact-store.ts +65 -2
  500. package/src/runtime/evidence/operational-shadow.ts +38 -0
  501. package/src/runtime/evidence/process-check-receipt.ts +5 -43
  502. package/src/runtime/evidence/sensitive-output.ts +4 -4
  503. package/src/runtime/execution/jobs/receipt-recovery.ts +1 -1
  504. package/src/runtime/execution/jobs/store.ts +121 -2
  505. package/src/runtime/execution/managed-workspace.ts +55 -5
  506. package/src/runtime/execution/process-runtime/canary.ts +19 -5
  507. package/src/runtime/execution/process-runtime/check-classification.ts +48 -0
  508. package/src/runtime/execution/process-runtime/check-facade.ts +23 -9
  509. package/src/runtime/execution/process-runtime/check-receipt.ts +1 -1
  510. package/src/runtime/execution/process-runtime/check-result-retention.ts +132 -0
  511. package/src/runtime/execution/process-runtime/check-result.ts +73 -2
  512. package/src/runtime/execution/process-runtime/check-runner-sidecar.ts +42 -3
  513. package/src/runtime/execution/process-runtime/check-storage.ts +187 -0
  514. package/src/runtime/execution/process-runtime/command-facade.ts +38 -87
  515. package/src/runtime/execution/process-runtime/index.ts +1 -0
  516. package/src/runtime/execution/process-runtime/interactive-admission.ts +2 -1
  517. package/src/runtime/execution/process-runtime/lightweight-managed.ts +32 -1
  518. package/src/runtime/execution/process-runtime/persisted-check.ts +128 -21
  519. package/src/runtime/execution/process-runtime/resource-claims.ts +60 -3
  520. package/src/runtime/execution/process-runtime/runtime.ts +132 -15
  521. package/src/runtime/execution/process-runtime/types.ts +22 -0
  522. package/src/runtime/execution/workers/executor.ts +16 -13
  523. package/src/runtime/execution/workers/worker-entry.ts +1 -1
  524. package/src/runtime/gateway/mcp/execution-tools.ts +2 -207
  525. package/src/runtime/gateway/mcp/legacy-ios-tool-adapter.ts +2 -61
  526. package/src/runtime/gateway/mcp/persisted-check-process.ts +2 -3
  527. package/src/runtime/gateway/mcp/process-tools.ts +2 -289
  528. package/src/runtime/gateway/mcp/router.ts +2 -1125
  529. package/src/runtime/gateway/mcp/runtime-tool-definitions.ts +2 -565
  530. package/src/runtime/gateway/mcp/runtime-tools.ts +2 -6655
  531. package/src/runtime/gateway/mcp/work-validation-reconciler.ts +2 -1
  532. package/src/runtime/maintenance/cleanup.ts +38 -24
  533. package/src/runtime/platform/browser-product-discovery.ts +174 -0
  534. package/src/runtime/platform/computer-platform.ts +14 -0
  535. package/src/runtime/platform/executable-discovery.ts +54 -0
  536. package/src/runtime/platform/fetch-http-server.ts +90 -0
  537. package/src/runtime/platform/service-manager.ts +138 -0
  538. package/src/runtime/platform/windows-host-discovery.ts +118 -0
  539. package/src/runtime/platform/windows-recovery-deployment.ts +67 -0
  540. package/src/runtime/plugins/action-application.ts +135 -0
  541. package/src/runtime/plugins/app-store-connect-adapter.ts +9 -21
  542. package/src/runtime/plugins/browser-adapter.ts +140 -100
  543. package/src/runtime/plugins/browser-automation-service.ts +15 -36
  544. package/src/runtime/plugins/browser-macos-bridge.ts +69 -21
  545. package/src/runtime/plugins/browser-manifest-surface.ts +30 -6
  546. package/src/runtime/plugins/browser-native-session-service.ts +1 -1
  547. package/src/runtime/plugins/browser-provider-registry.ts +2 -109
  548. package/src/runtime/plugins/browser-registration.ts +16 -0
  549. package/src/runtime/plugins/browser-runtime-contract.ts +2 -154
  550. package/src/runtime/plugins/browser-runtime.ts +41 -9
  551. package/src/runtime/plugins/browser-session-authority.ts +101 -291
  552. package/src/runtime/plugins/browser-session-legacy-migration.ts +203 -0
  553. package/src/runtime/plugins/browser-session-store.ts +229 -36
  554. package/src/runtime/plugins/browser-session-types.ts +2 -98
  555. package/src/runtime/plugins/capability-authorization-grants.ts +17 -0
  556. package/src/runtime/plugins/catalog-trust.ts +82 -0
  557. package/src/runtime/plugins/computer-registration.ts +839 -0
  558. package/src/runtime/plugins/config-store.ts +119 -0
  559. package/src/runtime/plugins/desktop-operator-external-policy.ts +932 -0
  560. package/src/runtime/plugins/desktop-operator-registration.ts +111 -8
  561. package/src/runtime/plugins/errors.ts +8 -1
  562. package/src/runtime/plugins/external-adapter.ts +45 -322
  563. package/src/runtime/plugins/external-provider-policy.ts +48 -0
  564. package/src/runtime/plugins/external-registration.ts +9 -1
  565. package/src/runtime/plugins/external-unix-socket-probe.cjs +9 -3
  566. package/src/runtime/plugins/external-unix-socket.ts +64 -193
  567. package/src/runtime/plugins/first-party-external-registration.ts +163 -0
  568. package/src/runtime/plugins/first-party-registry.ts +3 -1
  569. package/src/runtime/plugins/github-adapter.ts +1 -2
  570. package/src/runtime/plugins/gmail-adapter.ts +6 -6
  571. package/src/runtime/plugins/google-calendar-adapter.ts +6 -6
  572. package/src/runtime/plugins/google-shared.ts +31 -46
  573. package/src/runtime/plugins/google-tasks-adapter.ts +6 -6
  574. package/src/runtime/plugins/ios-adapter.ts +1 -2
  575. package/src/runtime/plugins/local-system-adapter.ts +180 -0
  576. package/src/runtime/plugins/macos-capability-broker.ts +15 -105
  577. package/src/runtime/plugins/resend-adapter.ts +15 -24
  578. package/src/runtime/plugins/store.ts +493 -51
  579. package/src/runtime/plugins/types.ts +8 -1
  580. package/src/runtime/plugins/uu-remote-rescue-registration.ts +155 -0
  581. package/src/runtime/plugins/windows-host-recovery-install.ts +68 -0
  582. package/src/runtime/plugins/windows-host-recovery-registration.ts +85 -0
  583. package/src/runtime/plugins/xiaohongshu-publish.ts +86 -430
  584. package/src/runtime/projections/controller-context.ts +12 -6
  585. package/src/runtime/projections/git-status-sampler.ts +73 -22
  586. package/src/runtime/projections/materialized-view.ts +14 -8
  587. package/src/runtime/recovery/capability-action-application.ts +228 -0
  588. package/src/runtime/recovery/index.ts +3 -0
  589. package/src/runtime/recovery/lifecycle-inventory.ts +338 -0
  590. package/src/runtime/recovery/local-jobs-repair.ts +14 -14
  591. package/src/runtime/recovery/maintenance-executor.ts +358 -76
  592. package/src/runtime/recovery/quarantine-retention.ts +256 -0
  593. package/src/runtime/release/identity.ts +6 -0
  594. package/src/runtime/resources/leases/store.ts +1 -1
  595. package/src/runtime/root/active-execution-power-assertion.ts +139 -0
  596. package/src/runtime/root/assistant-learning-loop.ts +45 -0
  597. package/src/runtime/root/browser-session-composition.ts +59 -0
  598. package/src/runtime/root/computer-composition.ts +191 -0
  599. package/src/runtime/root/computer-target-composition.ts +16 -0
  600. package/src/runtime/root/computer-target-persistence.ts +39 -0
  601. package/src/runtime/root/controller-round-composition.ts +107 -0
  602. package/src/runtime/root/controller-services.ts +2 -0
  603. package/src/runtime/root/entry.ts +4 -3
  604. package/src/runtime/root/gateway-adapter.ts +3 -3
  605. package/src/runtime/root/local-bridge.ts +5 -2
  606. package/src/runtime/root/mcp-transport.ts +5 -5
  607. package/src/runtime/root/package-connector-service.ts +108 -56
  608. package/src/runtime/root/package-runtime-release.ts +85 -18
  609. package/src/runtime/root/package-runtime-service.ts +60 -97
  610. package/src/runtime/root/release-execution-canary.ts +71 -0
  611. package/src/runtime/root/release-loader.ts +68 -0
  612. package/src/runtime/root/release-manifest.ts +106 -2
  613. package/src/runtime/root/release-materialize.ts +103 -4
  614. package/src/runtime/root/release-store.ts +31 -2
  615. package/src/runtime/root/runtime.ts +240 -25
  616. package/src/runtime/root/scheduled-controller-composition.ts +51 -0
  617. package/src/runtime/root/scheduler.ts +12 -4
  618. package/src/runtime/root/service-runner.ts +1 -1
  619. package/src/runtime/root/service.ts +41 -16
  620. package/src/runtime/root/status.ts +1 -0
  621. package/src/runtime/root/types.ts +24 -1
  622. package/src/runtime/shared/local-bridge-surface.ts +1 -1
  623. package/src/runtime/shared/process-environment.ts +19 -4
  624. package/src/runtime/shared/storage-capacity.ts +68 -0
  625. package/src/runtime/standalone-recovery/controller-home-migration.ts +1116 -0
  626. package/src/runtime/standalone-recovery/core.ts +984 -230
  627. package/src/runtime/standalone-recovery/entry.ts +182 -51
  628. package/src/runtime/standalone-recovery/independent-host-rescue.ts +172 -0
  629. package/src/runtime/standalone-recovery/installer.ts +212 -104
  630. package/src/runtime/standalone-recovery/mcp-server.ts +243 -0
  631. package/src/runtime/standalone-recovery/mutation-identity-contract.ts +21 -0
  632. package/src/runtime/standalone-recovery/operation-lock.ts +160 -0
  633. package/src/runtime/standalone-recovery/performance.ts +135 -0
  634. package/src/runtime/standalone-recovery/release.ts +5 -0
  635. package/src/runtime/watchdog/workflow-watchdog.ts +1 -1
  636. package/src/runtime/workflow/schedules/chatgpt-automation-migration.ts +1 -1
  637. package/src/runtime/workflow/schedules/engine.ts +191 -343
  638. package/src/runtime/workflow/schedules/pending-activation.ts +1 -1
  639. package/src/runtime/workflow/schedules/settlement.ts +2 -191
  640. package/src/runtime/workflow/schedules/store.ts +2 -607
  641. package/src/runtime/workflow/schedules/types.ts +2 -127
  642. package/src/runtime/workflow/schedules/work-continuation.ts +180 -15
  643. package/src/runtime/workflows/first-party/xiaohongshu.ts +122 -0
  644. package/src/runtime/workflows/runtime.ts +486 -0
  645. package/src/testing/test-governance.ts +90 -29
  646. package/assets/templates/helpers/archive-architecture-request.sh +0 -244
  647. package/docs/architecture/history/README.md +0 -7
  648. package/docs/architecture/history/chatgpt-handoff-facade.md +0 -397
  649. package/docs/architecture/history/global-hook-runtime.md +0 -166
  650. package/docs/architecture/history/ios-semantic-automation-provider-v2.md +0 -170
  651. package/docs/architecture/requests/archive/2026/20260528-154740-ai-hooks-ai-hooks-hook-input-sh.md +0 -59
  652. package/docs/architecture/requests/archive/2026/20260528-154816-assets-hooks-assets-hooks-hook-input-sh.md +0 -59
  653. package/docs/architecture/requests/archive/2026/20260528-161851-ai-harness-workflow-contract-json-ai-harness-workflow-contract-json.md +0 -59
  654. package/docs/architecture/requests/archive/2026/20260528-161853-scripts-check-task-workflow-sh-scripts-check-task-workflow-sh.md +0 -59
  655. package/docs/architecture/requests/archive/2026/20260528-161911-assets-workflow-contract-v1-json-assets-workflow-contract-v1-json.md +0 -59
  656. package/docs/architecture/requests/archive/2026/20260528-222416-ai-hooks-ai-hooks-hook-input-sh.md +0 -59
  657. package/docs/architecture/requests/archive/2026/20260528-222444-ai-hooks-ai-hooks-pre-edit-guard-sh.md +0 -59
  658. package/docs/architecture/requests/archive/2026/20260528-222457-ai-hooks-ai-hooks-pre-edit-guard-sh.md +0 -59
  659. package/docs/architecture/requests/archive/2026/20260528-222648-ai-hooks-ai-hooks-pre-edit-guard-sh.md +0 -59
  660. package/docs/architecture/requests/archive/2026/20260528-222705-ai-hooks-ai-hooks-worktree-guard-sh.md +0 -59
  661. package/docs/architecture/requests/archive/2026/20260528-222726-ai-hooks-ai-hooks-prompt-guard-sh.md +0 -59
  662. package/docs/architecture/requests/archive/2026/20260528-222727-ai-hooks-ai-hooks-prompt-guard-sh.md +0 -59
  663. package/docs/architecture/requests/archive/2026/20260528-224736-ai-hooks-ai-hooks-hook-input-sh.md +0 -59
  664. package/docs/architecture/requests/archive/2026/20260528-233259-ai-hooks-ai-hooks-prompt-guard-sh.md +0 -59
  665. package/docs/architecture/requests/archive/2026/20260528-233335-assets-hooks-assets-hooks-prompt-guard-sh.md +0 -59
  666. package/docs/architecture/requests/archive/2026/20260528-233456-ai-hooks-ai-hooks-prompt-guard-sh.md +0 -59
  667. package/docs/architecture/requests/archive/2026/20260528-233457-assets-hooks-assets-hooks-prompt-guard-sh.md +0 -59
  668. package/docs/architecture/requests/archive/2026/20260528-233637-ai-hooks-ai-hooks-prompt-guard-sh.md +0 -59
  669. package/docs/architecture/requests/archive/2026/20260528-233638-assets-hooks-assets-hooks-prompt-guard-sh.md +0 -59
  670. package/docs/architecture/requests/archive/2026/20260529-000322-root-package-json.md +0 -59
  671. package/docs/architecture/requests/archive/2026/20260529-004238-ai-hooks-ai-hooks-lib-workflow-state-sh.md +0 -59
  672. package/docs/architecture/requests/archive/2026/20260529-020654-ai-harness-policy-json-ai-harness-policy-json.md +0 -63
  673. package/docs/architecture/requests/archive/2026/20260529-094236-ai-hooks-ai-hooks-prompt-guard-sh.md +0 -60
  674. package/docs/architecture/requests/archive/2026/20260529-094323-assets-hooks-assets-hooks-prompt-guard-sh.md +0 -60
  675. package/docs/architecture/requests/archive/2026/20260529-094331-ai-hooks-ai-hooks-prompt-guard-sh.md +0 -60
  676. package/docs/architecture/requests/archive/2026/20260529-094339-assets-hooks-assets-hooks-prompt-guard-sh.md +0 -60
  677. package/docs/architecture/requests/archive/2026/20260529-094430-assets-hooks-assets-hooks-session-start-context-sh.md +0 -60
  678. package/docs/architecture/requests/archive/2026/20260529-094446-ai-hooks-ai-hooks-session-start-context-sh.md +0 -60
  679. package/docs/architecture/requests/archive/2026/20260613-002040-runtime-harness-hook-adapters.md +0 -68
  680. package/docs/architecture/requests/archive/2026/root.md +0 -68
  681. package/docs/architecture/requests/archive/2026/runtime-harness-hook-adapters.md +0 -76
  682. package/docs/architecture/requests/archive/2026/verification-evals-checks.md +0 -68
  683. package/docs/architecture/requests/archive/2026/workflow-engine-contract-assets.md +0 -69
  684. package/scripts/archive-architecture-request.sh +0 -4
  685. package/scripts/run-skill-evals.ts +0 -1022
@@ -0,0 +1,1456 @@
1
+ import { randomUUID, timingSafeEqual } from 'crypto';
2
+ import { existsSync, watch } from 'fs';
3
+ import { dirname } from 'path';
4
+ import express, { type Request, type Response, type NextFunction } from 'express';
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";
8
+ import {
9
+ buildMultiRepositoryToolDefinitions,
10
+ createCanonicalRuntimeProxy,
11
+ createMcpToolContext,
12
+ createForgeMcpServerFromContext,
13
+ readCanonicalRuntimeToolSchema,
14
+ type CanonicalRuntimeProxy,
15
+ type CanonicalRuntimeToolSchema,
16
+ type McpServerOptions,
17
+ } from '../server';
18
+ import {
19
+ loadMcpServiceLocalConfig,
20
+ loadMcpServiceRuntimeState,
21
+ mcpServiceOAuthTokenStoreFallbackPaths,
22
+ mcpServiceOAuthTokenStorePath,
23
+ parseMcpHttpAuthMode,
24
+ readMcpServiceBearerToken,
25
+ readMcpServiceOAuthPassphrase,
26
+ type McpLocalConfig,
27
+ type McpHttpAuthMode,
28
+ } from '../auth';
29
+ import { createMcpOAuthProvider, McpOAuthTokenStore } from '../oauth';
30
+ import { resolveMcpRepoRoot } from '../repo';
31
+ import { buildMcpToolDefinitions } from '../tool-mapping/tools';
32
+ 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
+ import { readRuntimeGeneration } from '../../../src/runtime/control-plane/runtime-generation';
41
+ 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
+ import {
47
+ FORGE_MCP_SCHEMA_VERSION,
48
+ FORGE_TOOL_SURFACE,
49
+ FORGE_VERSION,
50
+ repositoryIdentity,
51
+ } from '../../../src/cli/controller/runtime-config';
52
+ import { McpSessionRegistry, type McpSessionRoute } from './session-registry';
53
+ import {
54
+ connectionIdentity,
55
+ ensureForgeInstanceIdentity,
56
+ principal,
57
+ type ForgeInstanceIdentity,
58
+ type Principal,
59
+ } from '../../../packages/kernel/identity/api/index';
60
+
61
+ export interface McpHttpOptions extends McpServerOptions {
62
+ host?: string;
63
+ port?: number;
64
+ authToken?: string;
65
+ auth?: string;
66
+ }
67
+
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
+ function bearerFromRequest(req: Request): string | null {
80
+ const header = req.headers.authorization;
81
+ if (typeof header !== 'string') return null;
82
+ const match = /^Bearer\s+(.+)$/i.exec(header.trim());
83
+ return match ? match[1].trim() : null;
84
+ }
85
+
86
+ function principalIdentityFromRequest(req: Request): Principal {
87
+ const auth = (req as unknown as { auth?: { clientId?: string } }).auth;
88
+ if (auth?.clientId?.trim()) return principal(`oauth-client:${auth.clientId.trim()}`, 'oauth_client', 'mcp-oauth');
89
+ return bearerFromRequest(req)
90
+ ? principal('mcp-bearer-client', 'bearer_client', 'mcp-bearer')
91
+ : principal('controller-http-client', 'controller', 'mcp-http');
92
+ }
93
+
94
+ function principalFromRequest(req: Request): string {
95
+ return principalIdentityFromRequest(req).principalId;
96
+ }
97
+
98
+ export function isAuthorizedMcpHttpRequest(req: Request, expectedToken: string | null): boolean {
99
+ if (!expectedToken) return false;
100
+ return bearerFromRequest(req) === expectedToken;
101
+ }
102
+
103
+ function rawBodyToJson(body: Buffer): unknown | undefined {
104
+ if (body.length === 0) return undefined;
105
+ return JSON.parse(body.toString('utf-8'));
106
+ }
107
+
108
+ function isInitializeRequest(body: unknown): boolean {
109
+ return typeof body === 'object' && body !== null && (body as Record<string, unknown>).method === 'initialize';
110
+ }
111
+
112
+ function initializeClientIdentity(req: Request, body: unknown, route: McpSessionRoute, principalId: string): string {
113
+ const params = typeof body === 'object' && body !== null
114
+ ? (body as { params?: { clientInfo?: { name?: unknown; version?: unknown } } }).params
115
+ : undefined;
116
+ const clientInfo = params?.clientInfo;
117
+ const clientName = typeof clientInfo?.name === 'string' && clientInfo.name.trim() ? clientInfo.name.trim() : 'unknown-client';
118
+ const clientVersion = typeof clientInfo?.version === 'string' && clientInfo.version.trim() ? clientInfo.version.trim() : 'unknown-version';
119
+ const userAgent = typeof req.headers['user-agent'] === 'string' && req.headers['user-agent'].trim()
120
+ ? req.headers['user-agent'].trim().slice(0, 160)
121
+ : 'unknown-agent';
122
+ return `${principalId}|${route}|${clientName}/${clientVersion}|${userAgent}`;
123
+ }
124
+
125
+ export interface McpSessionLookupErrorResponse {
126
+ status: 400 | 404;
127
+ body: {
128
+ error: 'missing_session' | 'session_not_found';
129
+ code: 'MCP_SESSION_REQUIRED' | 'MCP_SESSION_EXPIRED';
130
+ message: string;
131
+ recoverable: true;
132
+ action: 'reinitialize';
133
+ };
134
+ }
135
+
136
+ export function mcpSessionLookupError(sessionId: string | undefined): McpSessionLookupErrorResponse {
137
+ if (!sessionId?.trim()) {
138
+ return {
139
+ status: 400,
140
+ body: {
141
+ error: 'missing_session',
142
+ code: 'MCP_SESSION_REQUIRED',
143
+ message: 'Mcp-Session-Id header is required for this request.',
144
+ recoverable: true,
145
+ action: 'reinitialize',
146
+ },
147
+ };
148
+ }
149
+ return {
150
+ status: 404,
151
+ body: {
152
+ error: 'session_not_found',
153
+ code: 'MCP_SESSION_EXPIRED',
154
+ message: 'MCP session not found or expired; initialize a new session.',
155
+ recoverable: true,
156
+ action: 'reinitialize',
157
+ },
158
+ };
159
+ }
160
+
161
+ export function sendMcpSessionLookupError(res: Response, sessionId: string | undefined): void {
162
+ const response = mcpSessionLookupError(sessionId);
163
+ res.setHeader('Cache-Control', 'no-store');
164
+ res.setHeader('Mcp-Session-Reset', 'reinitialize');
165
+ res.setHeader('x-forge-session-reset', 'reinitialize');
166
+ res.status(response.status).json(response.body);
167
+ }
168
+
169
+ export function mcpSessionToolSurfaceFingerprintIsCurrent(
170
+ sessionFingerprint: string | undefined,
171
+ currentFingerprint: string | undefined,
172
+ ): boolean {
173
+ // Missing metadata is tolerated for compatibility and transient
174
+ // controller-home read failures. Once both sides are known, equality is the
175
+ // schema fence. Runtime generation remains diagnostic-only.
176
+ return !sessionFingerprint || !currentFingerprint || sessionFingerprint === currentFingerprint;
177
+ }
178
+
179
+ export async function resolveMcpSessionCurrentFingerprint(
180
+ publishedFingerprint: string | undefined,
181
+ loadRuntimeFingerprint?: () => Promise<string | undefined>,
182
+ ): Promise<string | undefined> {
183
+ // Runtime status publishes the exact schema fingerprint observed at session
184
+ // initialization/cutover. Prefer that O(1) fence on the hot path. If the
185
+ // publication is temporarily unavailable, retain the previous fail-safe
186
+ // behavior by asking the Canonical Runtime directly.
187
+ if (publishedFingerprint) return publishedFingerprint;
188
+ return await loadRuntimeFingerprint?.();
189
+ }
190
+
191
+ export function isMcpToolsListRequest(body: unknown): boolean {
192
+ const messages = Array.isArray(body) ? body : [body];
193
+ return messages.length > 0 && messages.every((message) => {
194
+ if (!message || typeof message !== 'object') return false;
195
+ return (message as { method?: unknown }).method === 'tools/list';
196
+ });
197
+ }
198
+
199
+ function toolCallOutsideSessionSchema(body: unknown, toolNames: readonly string[] | undefined): boolean {
200
+ if (!toolNames) return false;
201
+ const messages = Array.isArray(body) ? body : [body];
202
+ return messages.some((message) => {
203
+ if (!message || typeof message !== 'object') return false;
204
+ const rpc = message as { method?: unknown; params?: { name?: unknown } };
205
+ return rpc.method === 'tools/call'
206
+ && typeof rpc.params?.name === 'string'
207
+ && !toolNames.includes(rpc.params.name);
208
+ });
209
+ }
210
+
211
+ function sendMcpToolSurfaceReset(
212
+ res: Response,
213
+ sessionFingerprint: string | undefined,
214
+ currentFingerprint: string | undefined,
215
+ ): void {
216
+ res.setHeader('Cache-Control', 'no-store');
217
+ res.setHeader('Mcp-Session-Reset', 'reinitialize');
218
+ res.setHeader('x-forge-session-reset', 'reinitialize');
219
+ res.setHeader('x-forge-session-reset-reason', 'tool_surface_changed');
220
+ res.status(404).json({
221
+ error: 'session_not_found',
222
+ code: 'MCP_TOOL_SURFACE_CHANGED',
223
+ message: 'MCP Runtime tool surface changed; initialize a new session so tools/list is refreshed.',
224
+ recoverable: true,
225
+ action: 'reinitialize',
226
+ previousFingerprint: sessionFingerprint,
227
+ currentFingerprint,
228
+ });
229
+ }
230
+
231
+ export function mcpRequestError(error: unknown) {
232
+ const rawMessage = error instanceof Error ? error.message : String(error);
233
+ const retryable = /(?:\b502\b|\b503\b|\b429\b|ECONNRESET|ETIMEDOUT|EAI_AGAIN|CANONICAL_RUNTIME_TIMEOUT|server_busy|session_capacity|gateway)/i.test(rawMessage);
234
+ return {
235
+ status: (retryable ? 503 : 500) as 500 | 503,
236
+ body: {
237
+ error: 'request_failed' as const,
238
+ code: (retryable ? 'MCP_TRANSIENT_FAILURE' : 'MCP_REQUEST_FAILED') as 'MCP_TRANSIENT_FAILURE' | 'MCP_REQUEST_FAILED',
239
+ message: retryable
240
+ ? 'The MCP Runtime is temporarily unavailable; retry shortly.'
241
+ : 'The MCP request could not be completed.',
242
+ recoverable: true as const,
243
+ retryable,
244
+ sessionPreserved: true as const,
245
+ action: 'retry' as const,
246
+ },
247
+ };
248
+ }
249
+
250
+ export function sendMcpRequestError(res: Response, error: unknown): void {
251
+ const response = mcpRequestError(error);
252
+ res.setHeader('Cache-Control', 'no-store');
253
+ res.setHeader('x-forge-session-preserved', 'true');
254
+ res.status(response.status).json(response.body);
255
+ }
256
+
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
+ function sendBearerUnauthorized(res: Response, description: string, hasConfiguredToken: boolean): void {
595
+ res.setHeader('www-authenticate', 'Bearer realm="forge-mcp"');
596
+ res.status(hasConfiguredToken ? 401 : 503).json({
597
+ error: hasConfiguredToken ? 'unauthorized' : 'auth_not_configured',
598
+ message: description,
599
+ });
600
+ }
601
+
602
+ function sendOAuthUnauthorized(
603
+ req: Request,
604
+ res: Response,
605
+ description: string,
606
+ configuredOrigin: string | undefined,
607
+ resourcePath = '/mcp',
608
+ ): void {
609
+ const resourceMetadataUrl = `${getPublicOrigin(req, configuredOrigin)}/.well-known/oauth-protected-resource${resourcePath}`;
610
+ res.setHeader(
611
+ 'www-authenticate',
612
+ `Bearer error="invalid_token", error_description="${description}", resource_metadata="${resourceMetadataUrl}"`,
613
+ );
614
+ res.status(401).json({ error: 'invalid_token', message: description });
615
+ }
616
+
617
+ function requireMcpHttpAuth(
618
+ mode: McpHttpAuthMode,
619
+ bearerToken: string | null,
620
+ provider: ReturnType<typeof createMcpOAuthProvider> | null,
621
+ configuredOrigin: string | undefined,
622
+ resourcePath = '/mcp',
623
+ ) {
624
+ return (req: Request, res: Response, next: NextFunction) => {
625
+ if (mode === 'none') {
626
+ next();
627
+ return;
628
+ }
629
+
630
+ if (mode === 'bearer') {
631
+ if (!isAuthorizedMcpHttpRequest(req, bearerToken)) {
632
+ sendBearerUnauthorized(res, bearerToken ? 'Missing or invalid Authorization header' : 'Bearer token is not configured', Boolean(bearerToken));
633
+ return;
634
+ }
635
+ next();
636
+ return;
637
+ }
638
+
639
+ if (isAuthorizedMcpHttpRequest(req, bearerToken)) {
640
+ next();
641
+ return;
642
+ }
643
+
644
+ const token = bearerFromRequest(req);
645
+ if (!token || !provider) {
646
+ sendOAuthUnauthorized(req, res, token ? 'OAuth is not configured' : 'Missing Authorization header', configuredOrigin, resourcePath);
647
+ return;
648
+ }
649
+ provider.verifyAccessToken(token)
650
+ .then((authInfo) => {
651
+ (req as unknown as Record<string, unknown>).auth = authInfo;
652
+ next();
653
+ })
654
+ .catch((error: unknown) => {
655
+ if (error instanceof InvalidTokenError) {
656
+ sendOAuthUnauthorized(req, res, error.message, configuredOrigin, resourcePath);
657
+ } else {
658
+ res.status(500).json({ error: 'server_error', message: 'Internal Server Error' });
659
+ }
660
+ });
661
+ };
662
+ }
663
+
664
+ interface McpRuntimeStats {
665
+ initializing: number;
666
+ activePosts: number;
667
+ rejectedOverload: number;
668
+ }
669
+
670
+ function positiveIntegerEnv(name: string, fallback: number): number {
671
+ const value = Number(process.env[name]);
672
+ return Number.isInteger(value) && value > 0 ? value : fallback;
673
+ }
674
+
675
+ const MAX_MCP_SESSIONS = positiveIntegerEnv('FORGE_MCP_MAX_SESSIONS', 64);
676
+ const MAX_MCP_SESSIONS_PER_PRINCIPAL = positiveIntegerEnv('FORGE_MCP_MAX_SESSIONS_PER_PRINCIPAL', MAX_MCP_SESSIONS);
677
+ const MAX_INITIALIZING_SESSIONS = positiveIntegerEnv('FORGE_MCP_MAX_INITIALIZING_SESSIONS', 8);
678
+ const MAX_POSTS_PER_SESSION = positiveIntegerEnv('FORGE_MCP_MAX_POSTS_PER_SESSION', 4);
679
+ const MAX_ACTIVE_POSTS = positiveIntegerEnv('FORGE_MCP_MAX_ACTIVE_POSTS', 32);
680
+ const MCP_SESSION_IDLE_TTL_MS = positiveIntegerEnv('FORGE_MCP_SESSION_IDLE_TTL_MS', 15 * 60_000);
681
+ const MCP_STREAM_LEASE_MS = positiveIntegerEnv('FORGE_MCP_STREAM_LEASE_MS', 30 * 60_000);
682
+ const MCP_SESSION_ABSOLUTE_LIFETIME_MS = positiveIntegerEnv('FORGE_MCP_SESSION_ABSOLUTE_LIFETIME_MS', 2 * 60 * 60_000);
683
+ const MCP_ACTIVE_POST_STALL_MS = positiveIntegerEnv('FORGE_MCP_ACTIVE_POST_STALL_MS', 10 * 60_000);
684
+
685
+ type McpToolContext = ReturnType<typeof createMcpToolContext>;
686
+ type HttpSessionRegistry = McpSessionRegistry<NodeStreamableHTTPServerTransport, McpToolContext>;
687
+
688
+ function principalIdFromModernRequestContext(context: McpRequestContext): string {
689
+ const clientId = context.authInfo?.clientId?.trim();
690
+ if (clientId) return `oauth-client:${clientId}`;
691
+ const authorization = context.requestInfo?.headers.get('authorization')?.trim() ?? '';
692
+ return /^Bearer\s+/i.test(authorization) ? 'mcp-bearer-client' : 'controller-http-client';
693
+ }
694
+
695
+ function createModernMcpHttpHandler(
696
+ baseOptions: McpServerOptions,
697
+ resolveRuntimeSchema?: (context: McpToolContext) => Promise<CanonicalRuntimeToolSchema | undefined>,
698
+ sharedRuntimeProxy?: CanonicalRuntimeProxy,
699
+ ): { handler: ReturnType<typeof createMcpHandler>; nodeHandler: NodeMcpRequestHandler } {
700
+ const handler = createMcpHandler(async (requestContext) => {
701
+ const toolContext = createMcpToolContext({
702
+ ...baseOptions,
703
+ principalId: principalIdFromModernRequestContext(requestContext),
704
+ });
705
+ const runtimeSchema = await resolveRuntimeSchema?.(toolContext);
706
+ return createForgeMcpServerFromContext(toolContext, runtimeSchema, sharedRuntimeProxy);
707
+ }, {
708
+ legacy: 'reject',
709
+ responseMode: 'auto',
710
+ });
711
+ return { handler, nodeHandler: toNodeHandler(handler) };
712
+ }
713
+
714
+ async function handleMcpPost(
715
+ req: Request,
716
+ res: Response,
717
+ baseOptions: McpServerOptions,
718
+ registry: HttpSessionRegistry,
719
+ stats: McpRuntimeStats,
720
+ route: McpSessionRoute,
721
+ forgeInstance: ForgeInstanceIdentity,
722
+ currentToolSurfaceFingerprint: () => string | undefined,
723
+ resolveRuntimeSchema?: (context: McpToolContext) => Promise<CanonicalRuntimeToolSchema | undefined>,
724
+ sharedRuntimeProxy?: CanonicalRuntimeProxy,
725
+ modernHandler?: NodeMcpRequestHandler,
726
+ ): Promise<void> {
727
+ let body: unknown;
728
+ try {
729
+ body = rawBodyToJson(req.body as Buffer);
730
+ } catch (_error) {
731
+ res.status(400).json({ error: 'invalid JSON request body' });
732
+ return;
733
+ }
734
+ if (modernHandler) {
735
+ const webRequest = await toWebRequest(req, body);
736
+ if (!(await isLegacyRequest(webRequest, body))) {
737
+ await modernHandler(req, res, body);
738
+ return;
739
+ }
740
+ }
741
+ const sessionId = req.headers['mcp-session-id'] as string | undefined;
742
+ if (isInitializeRequest(body)) {
743
+ if (sessionId) {
744
+ res.setHeader('Mcp-Session-Reset', 'reinitialized');
745
+ res.setHeader('x-forge-session-reset', 'reinitialized');
746
+ }
747
+ if (stats.initializing >= MAX_INITIALIZING_SESSIONS || stats.activePosts >= MAX_ACTIVE_POSTS) {
748
+ stats.rejectedOverload += 1;
749
+ res.setHeader('retry-after', '1');
750
+ res.status(503).json({
751
+ error: 'server_busy',
752
+ code: 'MCP_SERVER_BUSY',
753
+ message: 'Too many MCP sessions are initializing; retry shortly',
754
+ recoverable: true,
755
+ retryable: true,
756
+ sessionPreserved: true,
757
+ action: 'retry',
758
+ });
759
+ return;
760
+ }
761
+ stats.initializing += 1;
762
+ stats.activePosts += 1;
763
+ let transport: NodeStreamableHTTPServerTransport | undefined;
764
+ let reservationId: string | undefined;
765
+ let initializedSessionId: string | undefined;
766
+ try {
767
+ const principalIdentity = principalIdentityFromRequest(req);
768
+ const principalId = principalIdentity.principalId;
769
+ const connection = connectionIdentity({
770
+ instance: forgeInstance,
771
+ adapterId: 'mcp-http',
772
+ principal: principalIdentity,
773
+ });
774
+ const clientIdentity = initializeClientIdentity(req, body, route, principalId);
775
+ reservationId = await registry.reserveForInitialize({
776
+ principalId,
777
+ connectionId: connection.connectionId,
778
+ route,
779
+ ...(principalId === 'mcp-bearer-client' ? { enforcePrincipalCapacity: false } : {}),
780
+ ...(sessionId ? { supersedeSessionId: sessionId } : {}),
781
+ });
782
+ if (!reservationId) {
783
+ stats.rejectedOverload += 1;
784
+ res.setHeader('retry-after', '1');
785
+ res.status(503).json({
786
+ error: 'session_capacity',
787
+ code: 'MCP_SESSION_CAPACITY',
788
+ message: 'All MCP sessions are executing active requests; retry shortly',
789
+ recoverable: true,
790
+ retryable: true,
791
+ sessionPreserved: true,
792
+ action: 'retry',
793
+ });
794
+ return;
795
+ }
796
+ const sessionContext = createMcpToolContext({
797
+ ...baseOptions,
798
+ sessionId: `mcp_${randomUUID().replace(/-/g, '')}`,
799
+ principalId,
800
+ });
801
+ let runtimeSchema = await resolveRuntimeSchema?.(sessionContext);
802
+ let server: ReturnType<typeof createForgeMcpServerFromContext> | undefined;
803
+ transport = new NodeStreamableHTTPServerTransport({
804
+ sessionIdGenerator: () => randomUUID(),
805
+ onsessioninitialized: (newSessionId: string): void => {
806
+ registry.commitInitialize(reservationId!, {
807
+ sessionId: newSessionId,
808
+ transport: transport!,
809
+ toolContext: sessionContext,
810
+ route,
811
+ principalId,
812
+ connectionId: connection.connectionId,
813
+ clientIdentity,
814
+ toolSurfaceFingerprint: runtimeSchema?.fingerprint ?? currentToolSurfaceFingerprint(),
815
+ toolNames: runtimeSchema?.toolNames,
816
+ notifyToolListChanged: async () => { await server?.sendToolListChanged(); },
817
+ ...(runtimeSchema && resolveRuntimeSchema
818
+ ? {
819
+ refreshToolSurface: async () => {
820
+ const refreshed = await resolveRuntimeSchema(sessionContext);
821
+ if (!refreshed || !runtimeSchema) return undefined;
822
+ runtimeSchema.definitions = refreshed.definitions;
823
+ runtimeSchema.toolNames = refreshed.toolNames;
824
+ runtimeSchema.fingerprint = refreshed.fingerprint;
825
+ return {
826
+ toolSurfaceFingerprint: refreshed.fingerprint,
827
+ toolNames: refreshed.toolNames,
828
+ };
829
+ },
830
+ }
831
+ : {}),
832
+ });
833
+ initializedSessionId = newSessionId;
834
+ },
835
+ });
836
+ transport.onclose = () => {
837
+ if (transport?.sessionId) registry.detach(transport.sessionId);
838
+ };
839
+ server = createForgeMcpServerFromContext(sessionContext, runtimeSchema, sharedRuntimeProxy);
840
+ await server.connect(transport);
841
+ await transport.handleRequest(req, res, body);
842
+ } finally {
843
+ if (initializedSessionId) registry.endPost(initializedSessionId);
844
+ if (reservationId) registry.releaseInitialize(reservationId);
845
+ stats.initializing -= 1;
846
+ stats.activePosts -= 1;
847
+ if (!transport?.sessionId) await transport?.close().catch(() => undefined);
848
+ }
849
+ return;
850
+ }
851
+ if (sessionId) {
852
+ const managed = registry.get(sessionId);
853
+ if (managed && managed.route === route && managed.principalId === principalFromRequest(req)) {
854
+ if (managed.inFlightPosts >= MAX_POSTS_PER_SESSION || stats.activePosts >= MAX_ACTIVE_POSTS) {
855
+ stats.rejectedOverload += 1;
856
+ res.setHeader('retry-after', '1');
857
+ res.status(429).json({ error: 'session_busy', message: 'Too many MCP requests are active; retry shortly' });
858
+ return;
859
+ }
860
+ registry.beginPost(sessionId);
861
+ stats.activePosts += 1;
862
+ try {
863
+ const currentFingerprint = await resolveMcpSessionCurrentFingerprint(
864
+ currentToolSurfaceFingerprint(),
865
+ resolveRuntimeSchema
866
+ ? async () => (await resolveRuntimeSchema(managed.toolContext))?.fingerprint
867
+ : undefined,
868
+ );
869
+ if (!mcpSessionToolSurfaceFingerprintIsCurrent(managed.toolSurfaceFingerprint, currentFingerprint)) {
870
+ // Standard MCP hot refresh: allow an explicitly requested tools/list
871
+ // to replace the live session's mutable Runtime schema snapshot. Hosts
872
+ // that ignore list_changed and call directly still hit the reset fence.
873
+ if (isMcpToolsListRequest(body)) {
874
+ const refreshed = await registry.refreshToolSurface(sessionId);
875
+ if (refreshed
876
+ && (!currentFingerprint || refreshed.toolSurfaceFingerprint === currentFingerprint)) {
877
+ await managed.transport.handleRequest(req, res, body);
878
+ return;
879
+ }
880
+ }
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
+ sendMcpToolSurfaceReset(res, managed.toolSurfaceFingerprint, currentFingerprint);
885
+ return;
886
+ }
887
+ if (toolCallOutsideSessionSchema(body, managed.toolNames)) {
888
+ // 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.
891
+ sendMcpToolSurfaceReset(res, managed.toolSurfaceFingerprint, currentFingerprint);
892
+ return;
893
+ }
894
+ await managed.transport.handleRequest(req, res, body);
895
+ } finally {
896
+ registry.endPost(sessionId);
897
+ stats.activePosts -= 1;
898
+ }
899
+ return;
900
+ }
901
+ }
902
+ sendMcpSessionLookupError(res, sessionId);
903
+ }
904
+
905
+ async function handleMcpGet(
906
+ req: Request,
907
+ res: Response,
908
+ registry: HttpSessionRegistry,
909
+ route: McpSessionRoute,
910
+ currentToolSurfaceFingerprint: () => string | undefined,
911
+ resolveRuntimeSchema?: (context: McpToolContext) => Promise<CanonicalRuntimeToolSchema | undefined>,
912
+ ): Promise<void> {
913
+ const sessionId = req.headers['mcp-session-id'] as string | undefined;
914
+ const managed = sessionId ? registry.get(sessionId) : undefined;
915
+ if (!managed || managed.route !== route || managed.principalId !== principalFromRequest(req)) {
916
+ sendMcpSessionLookupError(res, sessionId);
917
+ return;
918
+ }
919
+ registry.beginStream(sessionId!);
920
+ let released = false;
921
+ const releaseStream = (): void => {
922
+ if (released) return;
923
+ released = true;
924
+ registry.endStream(sessionId!);
925
+ };
926
+ req.once('aborted', releaseStream);
927
+ res.once('close', releaseStream);
928
+ try {
929
+ const currentFingerprint = await resolveMcpSessionCurrentFingerprint(
930
+ currentToolSurfaceFingerprint(),
931
+ resolveRuntimeSchema
932
+ ? async () => (await resolveRuntimeSchema(managed.toolContext))?.fingerprint
933
+ : undefined,
934
+ );
935
+ 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
+ sendMcpToolSurfaceReset(res, managed.toolSurfaceFingerprint, currentFingerprint);
939
+ return;
940
+ }
941
+ await managed.transport.handleRequest(req, res);
942
+ } finally {
943
+ req.off('aborted', releaseStream);
944
+ res.off('close', releaseStream);
945
+ releaseStream();
946
+ }
947
+ }
948
+
949
+ async function handleMcpDelete(req: Request, res: Response, registry: HttpSessionRegistry, route: McpSessionRoute): Promise<void> {
950
+ const sessionId = req.headers['mcp-session-id'] as string | undefined;
951
+ const managed = sessionId ? registry.get(sessionId) : undefined;
952
+ if (!managed || managed.route !== route || managed.principalId !== principalFromRequest(req)) {
953
+ sendMcpSessionLookupError(res, sessionId);
954
+ return;
955
+ }
956
+ registry.setPendingCloseReason(sessionId!, 'client_delete');
957
+ await managed.transport.handleRequest(req, res);
958
+ if (registry.get(sessionId!)) await registry.close(sessionId!, 'client_delete');
959
+ }
960
+
961
+ export async function startMcpHttp(opts: McpHttpOptions): Promise<void> {
962
+ const host = opts.host ?? '127.0.0.1';
963
+ const port = opts.port ?? 8765;
964
+ const controllerHome = resolveControllerHome(opts.controllerHome);
965
+ const forgeInstance = ensureForgeInstanceIdentity({
966
+ controllerHome,
967
+ preferredInstanceId: process.env.FORGE_INSTANCE_ID?.trim(),
968
+ });
969
+ const serviceConfig = loadMcpServiceLocalConfig(controllerHome);
970
+ const configuredForgeInstanceId = serviceConfig?.identity?.forgeInstanceId?.trim();
971
+ if (configuredForgeInstanceId && configuredForgeInstanceId !== forgeInstance.instanceId) {
972
+ throw new Error(`MCP_FORGE_INSTANCE_ID_MISMATCH: config=${configuredForgeInstanceId} kernel=${forgeInstance.instanceId}`);
973
+ }
974
+ const profile = opts.profile ?? serviceConfig?.profile ?? 'controller';
975
+ // A controller Gateway can serve registered repositories without selecting
976
+ // one at startup. Do not turn its launchd working directory into an
977
+ // implicit repository, especially for package Runtime snapshots.
978
+ const repoRoot = profile === 'controller' && !opts.repo?.trim()
979
+ ? undefined
980
+ : resolveMcpRepoRoot(opts.repo ?? '.');
981
+ const authMode = parseMcpHttpAuthMode(opts.auth ?? serviceConfig?.auth?.mode);
982
+ const authToken = opts.authToken ?? readMcpServiceBearerToken(controllerHome, repoRoot);
983
+ const oauthPassphrase = authMode === 'oauth'
984
+ ? readMcpServiceOAuthPassphrase(controllerHome, repoRoot)
985
+ : null;
986
+ const tokenStore = authMode === 'oauth'
987
+ ? new McpOAuthTokenStore(
988
+ mcpServiceOAuthTokenStorePath(controllerHome),
989
+ mcpServiceOAuthTokenStoreFallbackPaths(controllerHome, repoRoot),
990
+ )
991
+ : null;
992
+ tokenStore?.load();
993
+ const oauthProvider = tokenStore ? createMcpOAuthProvider(tokenStore) : null;
994
+ 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,
1002
+ 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
+ );
1012
+ },
1013
+ });
1014
+ const runtimeStats: McpRuntimeStats = { initializing: 0, activePosts: 0, rejectedOverload: 0 };
1015
+ const toolContext = createMcpToolContext({ ...opts, repo: repoRoot, controllerHome, profile });
1016
+ const baseOptions: McpServerOptions = {
1017
+ repo: repoRoot,
1018
+ controllerHome,
1019
+ profile,
1020
+ toolset: opts.toolset,
1021
+ enableChatgptBrowser: opts.enableChatgptBrowser,
1022
+ enableDevRunner: opts.enableDevRunner,
1023
+ devRunnerAgents: opts.devRunnerAgents,
1024
+ devRunnerTimeoutMs: opts.devRunnerTimeoutMs,
1025
+ devRunnerMaxTimeoutMs: opts.devRunnerMaxTimeoutMs,
1026
+ };
1027
+ const runtimeControllerHome = 'controllerHome' in toolContext ? toolContext.controllerHome : undefined;
1028
+ const sharedRuntimeProxy = runtimeControllerHome && 'controllerHome' in toolContext && !toolContext.runtimeSourceRoot
1029
+ ? createCanonicalRuntimeProxy(toolContext)
1030
+ : undefined;
1031
+ const currentRuntimeGeneration = () => runtimeControllerHome ? readRuntimeGeneration(runtimeControllerHome) : undefined;
1032
+ const currentRuntimeToolSurfaceFingerprint = () => runtimeControllerHome
1033
+ ? readRuntimeStatusSnapshot(runtimeControllerHome)?.toolSurfaceFingerprint
1034
+ : undefined;
1035
+ let observedRuntimeToolSurfaceFingerprint = currentRuntimeToolSurfaceFingerprint();
1036
+ let toolSurfaceNotificationTimer: ReturnType<typeof setTimeout> | undefined;
1037
+ const runtimeStatusDirectory = runtimeControllerHome ? dirname(runtimeStatusPath(runtimeControllerHome)) : undefined;
1038
+ const runtimeStatusWatcher = runtimeStatusDirectory && existsSync(runtimeStatusDirectory)
1039
+ ? watch(runtimeStatusDirectory, { persistent: false }, (_event, filename) => {
1040
+ if (String(filename ?? '') !== 'status.json') return;
1041
+ if (toolSurfaceNotificationTimer) clearTimeout(toolSurfaceNotificationTimer);
1042
+ toolSurfaceNotificationTimer = setTimeout(() => {
1043
+ const currentFingerprint = currentRuntimeToolSurfaceFingerprint();
1044
+ if (!currentFingerprint || currentFingerprint === observedRuntimeToolSurfaceFingerprint) return;
1045
+ observedRuntimeToolSurfaceFingerprint = currentFingerprint;
1046
+ void sessionRegistry.notifyToolListChanged(currentFingerprint);
1047
+ }, 25);
1048
+ toolSurfaceNotificationTimer.unref();
1049
+ })
1050
+ : undefined;
1051
+ runtimeStatusWatcher?.on('error', () => undefined);
1052
+ const resolveRuntimeSchema = runtimeControllerHome
1053
+ ? async (context: McpToolContext): Promise<CanonicalRuntimeToolSchema | undefined> => {
1054
+ if (!('controllerHome' in context)) return undefined;
1055
+ return await readCanonicalRuntimeToolSchema(context, sharedRuntimeProxy);
1056
+ }
1057
+ : 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
+ const toolSurface = toolContext.policy.profile === 'controller' ? FORGE_TOOL_SURFACE : `${toolContext.policy.profile}-legacy-v1`;
1065
+ const toolSurfaceSchemaVersion = toolContext.policy.profile === 'controller' ? FORGE_MCP_SCHEMA_VERSION : 1;
1066
+ 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
+ const app = express();
1072
+ app.set('trust proxy', 1);
1073
+
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
+ }
1291
+ });
1292
+
1293
+ 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'));
1347
+ }
1348
+
1349
+ const setMcpResponseHeaders = (_req: Request, res: Response, next: NextFunction): void => {
1350
+ res.setHeader('x-forge-tool-surface', toolSurface);
1351
+ res.setHeader('x-forge-version', String(forgeVersion));
1352
+ res.setHeader('x-forge-schema-version', String(toolSurfaceSchemaVersion));
1353
+ next();
1354
+ };
1355
+
1356
+ // Modern MCP 2026-07-28 is the canonical public serving boundary. The SDK
1357
+ // owns protocol-era classification and serves modern requests without
1358
+ // Mcp-Session-Id. Existing 2025-era traffic is routed explicitly to the
1359
+ // bounded stateful compatibility path below.
1360
+ const modernMcp = createModernMcpHttpHandler(baseOptions, resolveRuntimeSchema, sharedRuntimeProxy);
1361
+
1362
+ // Primary MCP path: OAuth (or bearer when --auth bearer).
1363
+ app.use('/mcp', setMcpResponseHeaders);
1364
+ app.post('/mcp', requireMcpHttpAuth(authMode, authToken, oauthProvider, configuredPublicOrigin), express.raw({ type: '*/*', limit: '1mb' }), (req, res) => {
1365
+ handleMcpPost(req, res, baseOptions, sessionRegistry, runtimeStats, '/mcp', forgeInstance, currentRuntimeToolSurfaceFingerprint, resolveRuntimeSchema, sharedRuntimeProxy, modernMcp.nodeHandler).catch((error: unknown) => {
1366
+ if (!res.headersSent) sendMcpRequestError(res, error);
1367
+ });
1368
+ });
1369
+ app.get('/mcp', requireMcpHttpAuth(authMode, authToken, oauthProvider, configuredPublicOrigin), (req, res) => {
1370
+ handleMcpGet(req, res, sessionRegistry, '/mcp', currentRuntimeToolSurfaceFingerprint, resolveRuntimeSchema).catch((error: unknown) => {
1371
+ if (!res.headersSent) sendMcpRequestError(res, error);
1372
+ });
1373
+ });
1374
+ app.delete('/mcp', requireMcpHttpAuth(authMode, authToken, oauthProvider, configuredPublicOrigin), (req, res) => {
1375
+ handleMcpDelete(req, res, sessionRegistry, '/mcp').catch((error: unknown) => {
1376
+ if (!res.headersSent) sendMcpRequestError(res, error);
1377
+ });
1378
+ });
1379
+
1380
+ // Legacy Grok OAuth resource. New Grok connectors should use canonical /mcp.
1381
+ app.use('/mcp-grok', setMcpResponseHeaders);
1382
+ app.post('/mcp-grok', requireMcpHttpAuth(authMode, authToken, oauthProvider, configuredPublicOrigin, '/mcp-grok'), express.raw({ type: '*/*', limit: '1mb' }), (req, res) => {
1383
+ handleMcpPost(req, res, baseOptions, sessionRegistry, runtimeStats, '/mcp-grok', forgeInstance, currentRuntimeToolSurfaceFingerprint, resolveRuntimeSchema, sharedRuntimeProxy, modernMcp.nodeHandler).catch((error: unknown) => {
1384
+ if (!res.headersSent) sendMcpRequestError(res, error);
1385
+ });
1386
+ });
1387
+ app.get('/mcp-grok', requireMcpHttpAuth(authMode, authToken, oauthProvider, configuredPublicOrigin, '/mcp-grok'), (req, res) => {
1388
+ handleMcpGet(req, res, sessionRegistry, '/mcp-grok', currentRuntimeToolSurfaceFingerprint, resolveRuntimeSchema).catch((error: unknown) => {
1389
+ if (!res.headersSent) sendMcpRequestError(res, error);
1390
+ });
1391
+ });
1392
+ app.delete('/mcp-grok', requireMcpHttpAuth(authMode, authToken, oauthProvider, configuredPublicOrigin, '/mcp-grok'), (req, res) => {
1393
+ handleMcpDelete(req, res, sessionRegistry, '/mcp-grok').catch((error: unknown) => {
1394
+ if (!res.headersSent) sendMcpRequestError(res, error);
1395
+ });
1396
+ });
1397
+
1398
+ // Bearer-only MCP path for clients that can send Authorization headers. Never advertises OAuth resource_metadata.
1399
+ app.use('/mcp-bearer', setMcpResponseHeaders);
1400
+ app.post('/mcp-bearer', requireMcpHttpAuth('bearer', authToken, null, configuredPublicOrigin), express.raw({ type: '*/*', limit: '1mb' }), (req, res) => {
1401
+ handleMcpPost(req, res, baseOptions, sessionRegistry, runtimeStats, '/mcp-bearer', forgeInstance, currentRuntimeToolSurfaceFingerprint, resolveRuntimeSchema, sharedRuntimeProxy, modernMcp.nodeHandler).catch((error: unknown) => {
1402
+ if (!res.headersSent) sendMcpRequestError(res, error);
1403
+ });
1404
+ });
1405
+ app.get('/mcp-bearer', requireMcpHttpAuth('bearer', authToken, null, configuredPublicOrigin), (req, res) => {
1406
+ handleMcpGet(req, res, sessionRegistry, '/mcp-bearer', currentRuntimeToolSurfaceFingerprint, resolveRuntimeSchema).catch((error: unknown) => {
1407
+ if (!res.headersSent) sendMcpRequestError(res, error);
1408
+ });
1409
+ });
1410
+ app.delete('/mcp-bearer', requireMcpHttpAuth('bearer', authToken, null, configuredPublicOrigin), (req, res) => {
1411
+ handleMcpDelete(req, res, sessionRegistry, '/mcp-bearer').catch((error: unknown) => {
1412
+ if (!res.headersSent) sendMcpRequestError(res, error);
1413
+ });
1414
+ });
1415
+
1416
+ app.use((_req, res) => res.status(404).json({ error: 'not_found' }));
1417
+
1418
+ const cleanupTimer = setInterval(() => {
1419
+ void sessionRegistry.prune();
1420
+ }, 60_000);
1421
+ cleanupTimer.unref();
1422
+
1423
+ const httpServer = app.listen(port, host);
1424
+ httpServer.keepAliveTimeout = 65_000;
1425
+ httpServer.headersTimeout = 70_000;
1426
+ httpServer.requestTimeout = 120_000;
1427
+
1428
+ httpServer.on('close', () => {
1429
+ clearInterval(cleanupTimer);
1430
+ if (toolSurfaceNotificationTimer) clearTimeout(toolSurfaceNotificationTimer);
1431
+ runtimeStatusWatcher?.close();
1432
+ void sessionRegistry.closeAll('shutdown');
1433
+ void modernMcp.handler.close();
1434
+ void sharedRuntimeProxy?.close();
1435
+ });
1436
+
1437
+ await new Promise<void>((resolve) => {
1438
+ httpServer.once('listening', resolve);
1439
+ });
1440
+ const authLabel = authMode === 'oauth'
1441
+ ? (oauthPassphrase ? 'oauth' : 'oauth-missing')
1442
+ : authMode === 'bearer'
1443
+ ? (authToken ? 'bearer' : 'missing')
1444
+ : 'none';
1445
+ console.error(
1446
+ `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)`,
1447
+ );
1448
+
1449
+ const shutdown = () => {
1450
+ void sessionRegistry.closeAll('shutdown');
1451
+ tokenStore?.flush();
1452
+ httpServer.close(() => process.exit(0));
1453
+ };
1454
+ process.once('SIGINT', shutdown);
1455
+ process.once('SIGTERM', shutdown);
1456
+ }