@akira-tl/forgerelay 0.8.8 → 0.8.9

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 (246) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +10 -18
  3. package/capabilities/host-integration/GUIDE.md +2 -2
  4. package/dist/activity/{audit-store.js → history/audit-store.js} +129 -21
  5. package/dist/activity/history/bash-output-store.js +250 -0
  6. package/dist/activity/{host-turn-store.js → history/host-turn-store.js} +4 -1
  7. package/dist/activity/{query-service.js → history/query-service.js} +13 -7
  8. package/dist/activity/{lifecycle.js → runtime/lifecycle.js} +1 -1
  9. package/dist/activity/{mcp-query-tools.js → runtime/mcp-query-tools.js} +3 -3
  10. package/dist/activity/storage/history.js +12 -0
  11. package/dist/activity/storage/paths.js +56 -0
  12. package/dist/activity/storage/segmented-log.js +149 -0
  13. package/dist/cli/setup-support.js +116 -0
  14. package/dist/cli.js +18 -126
  15. package/dist/lsp/code-intelligence.js +1 -1
  16. package/dist/lsp/test-support/server-fixture.js +9 -9
  17. package/dist/mcp/filesystem/filesystem-tools.js +245 -0
  18. package/dist/mcp/hooks/command-runner.js +79 -0
  19. package/dist/{hook-cli.js → mcp/hooks/hook-cli.js} +1 -1
  20. package/dist/{hooks.js → mcp/hooks/hooks.js} +3 -80
  21. package/dist/{oauth-provider.js → mcp/oauth/oauth-provider.js} +1 -1
  22. package/dist/{oauth-store.js → mcp/oauth/oauth-store.js} +5 -4
  23. package/dist/{operations → mcp/operations}/native-bulk-mutations.js +2 -2
  24. package/dist/mcp/panel/app.js +143 -0
  25. package/dist/{mcp-app-template.js → mcp/panel/mcp-app-template.js} +2 -38
  26. package/dist/mcp/process/runtime.js +257 -0
  27. package/dist/mcp/process/tools.js +346 -0
  28. package/dist/mcp/server/core/activity-support.js +70 -0
  29. package/dist/{capabilities.js → mcp/server/core/capabilities.js} +5 -3
  30. package/dist/{capability-registry.js → mcp/server/core/capability-registry.js} +2 -2
  31. package/dist/mcp/server/core/capability-support.js +90 -0
  32. package/dist/mcp/server/core/schemas.js +216 -0
  33. package/dist/mcp/server/core/tool-support.js +207 -0
  34. package/dist/mcp/server/core/version.js +9 -0
  35. package/dist/mcp/server/operations/runtime/filesystem-tools.js +400 -0
  36. package/dist/mcp/server/operations/runtime/operation-runtime.js +523 -0
  37. package/dist/mcp/server/transport/http-server.js +301 -0
  38. package/dist/mcp/server/transport/http-support.js +43 -0
  39. package/dist/mcp/server/workspace/runtime/workspace-open-presentation.js +222 -0
  40. package/dist/mcp/server/workspace/runtime/workspace-open-schema.js +187 -0
  41. package/dist/mcp/server/workspace/runtime/workspace-open.js +552 -0
  42. package/dist/mcp/server/workspace/runtime/workspace-tools.js +601 -0
  43. package/dist/{config.js → runtime/config/config.js} +3 -3
  44. package/dist/{user-config.js → runtime/config/user-config.js} +14 -54
  45. package/dist/{db → runtime/state/db}/client.js +1 -1
  46. package/dist/{db → runtime/state/db}/migrations.js +34 -3
  47. package/dist/runtime/state/lock/file-lock.js +130 -0
  48. package/dist/runtime/testing/server-fixture.js +190 -0
  49. package/dist/server.js +116 -4202
  50. package/dist/subagents/providers/adapters/claude.js +86 -52
  51. package/dist/subagents/providers/adapters/codex.js +103 -39
  52. package/dist/subagents/providers/adapters/opencode.js +114 -84
  53. package/dist/subagents/providers/adapters/pi.js +59 -8
  54. package/dist/subagents/providers/availability.js +15 -30
  55. package/dist/subagents/providers/runtime/external-command.js +56 -0
  56. package/dist/subagents/sessions/capability.js +1 -1
  57. package/dist/subagents/sessions/execution.js +1 -1
  58. package/dist/subagents/sessions/mcp/activity-audit-test-support.js +30 -0
  59. package/dist/subagents/sessions/store.js +1 -1
  60. package/dist/ui/.vite/manifest.json +342 -410
  61. package/dist/ui/activity-panel-app.html +2 -4
  62. package/dist/ui/assets/activity-panel-app-7Xe0alhm.css +1 -0
  63. package/dist/ui/assets/activity-panel-app-Sl1Jro8v.js +115 -0
  64. package/dist/ui/assets/angular-html-B_yn91wW.js +1 -0
  65. package/dist/ui/assets/{angular-ts-Bxh4-O1-.js → angular-ts-DSgtloKz.js} +1 -1
  66. package/dist/ui/assets/{apl-BmYHKQ6m.js → apl-NatWtF8X.js} +1 -1
  67. package/dist/ui/assets/{astro-C51vqDKq.js → astro-BYFEVBuN.js} +1 -1
  68. package/dist/ui/assets/{blade-CdhVsmwq.js → blade-DoO7yvMR.js} +1 -1
  69. package/dist/ui/assets/c-Rl2hMCcg.js +1 -0
  70. package/dist/ui/assets/{cobol-Cf6azoN_.js → cobol-ByC5mWiB.js} +1 -1
  71. package/dist/ui/assets/{coffee-vVdGzlmt.js → coffee-Dez77Qov.js} +1 -1
  72. package/dist/ui/assets/cpp-DtaAQT9h.js +1 -0
  73. package/dist/ui/assets/{crystal-5UZy6KIX.js → crystal-Bgmbf8ec.js} +1 -1
  74. package/dist/ui/assets/css-C1tjBtVN.js +1 -0
  75. package/dist/ui/assets/{edge-CESgo4ia.js → edge-DeH6u8Wg.js} +1 -1
  76. package/dist/ui/assets/{elixir-9FKfw4cJ.js → elixir-_H3KnmWT.js} +1 -1
  77. package/dist/ui/assets/{elm-Dv7A0aLa.js → elm-CsCe3U28.js} +1 -1
  78. package/dist/ui/assets/{erb-DHFmDvM0.js → erb-CbM-L2g8.js} +1 -1
  79. package/dist/ui/assets/{git-rebase-utbU6CzV.js → git-rebase-D0s5pDh9.js} +1 -1
  80. package/dist/ui/assets/{glimmer-js-t2kiCkyB.js → glimmer-js-BfOToq_A.js} +1 -1
  81. package/dist/ui/assets/{glimmer-ts-F0Jv5okV.js → glimmer-ts-CCUOkL2K.js} +1 -1
  82. package/dist/ui/assets/glsl-DH4ec9dE.js +1 -0
  83. package/dist/ui/assets/graphql-DilAMBoI.js +1 -0
  84. package/dist/ui/assets/{hack-8yjdbaSA.js → hack-BDkNDZlF.js} +1 -1
  85. package/dist/ui/assets/haml-D_QBp6K7.js +1 -0
  86. package/dist/ui/assets/{handlebars-BBFEg4Lo.js → handlebars-DePUrTpC.js} +1 -1
  87. package/dist/ui/assets/heavy-payload-DOwZpO6n.js +290 -0
  88. package/dist/ui/assets/html-By4dNnM4.js +1 -0
  89. package/dist/ui/assets/{html-derivative-DJAQcZyc.js → html-derivative-DDw9Ef4Q.js} +1 -1
  90. package/dist/ui/assets/{http-Dg5LSYRK.js → http-B0QxWc9M.js} +1 -1
  91. package/dist/ui/assets/{hurl-Y5uYzMZR.js → hurl-vMQ4lda1.js} +1 -1
  92. package/dist/ui/assets/java-DYRe2lWG.js +1 -0
  93. package/dist/ui/assets/javascript-CxyGqwcL.js +1 -0
  94. package/dist/ui/assets/{jinja-Bt30h8Ik.js → jinja-JUwV-fZt.js} +1 -1
  95. package/dist/ui/assets/{jison-D5uZ1Nyo.js → jison-7ZqCEvS8.js} +1 -1
  96. package/dist/ui/assets/json-dbltgiiA.js +1 -0
  97. package/dist/ui/assets/jsx-oDopT7YQ.js +1 -0
  98. package/dist/ui/assets/{julia-76hRaaGz.js → julia-CkKiNSav.js} +1 -1
  99. package/dist/ui/assets/{just-CDE7W0vW.js → just-DuqCAKJ3.js} +1 -1
  100. package/dist/ui/assets/{latex-CVO2Byab.js → latex-sudcRDSn.js} +1 -1
  101. package/dist/ui/assets/{liquid-CA-AS3EX.js → liquid-Di-08Js7.js} +1 -1
  102. package/dist/ui/assets/lua-YlqdMoV0.js +1 -0
  103. package/dist/ui/assets/{marko-_BWrPfny.js → marko-CJRNyJDb.js} +1 -1
  104. package/dist/ui/assets/{mdc-PyJv85eS.js → mdc-t12CzZHs.js} +1 -1
  105. package/dist/ui/assets/{nginx-DAWVw8yu.js → nginx-57-b4XwT.js} +1 -1
  106. package/dist/ui/assets/{nim-CKE1dzpU.js → nim-BzrswYQa.js} +1 -1
  107. package/dist/ui/assets/{perl-BBhWva19.js → perl-D9JR_iIh.js} +1 -1
  108. package/dist/ui/assets/{php-ChIeaAoF.js → php-Py2gFHil.js} +1 -1
  109. package/dist/ui/assets/{pug-CMgI5afi.js → pug-PzLLy0h1.js} +1 -1
  110. package/dist/ui/assets/{qml-CjxTkcFH.js → qml-By8fwgV4.js} +1 -1
  111. package/dist/ui/assets/r-CcezTbLq.js +1 -0
  112. package/dist/ui/assets/{razor-i4DbHr8p.js → razor-dG-vdhL_.js} +1 -1
  113. package/dist/ui/assets/regexp-BlsQ-N_8.js +1 -0
  114. package/dist/ui/assets/{rst-oy_97Kl4.js → rst-C2awDCat.js} +1 -1
  115. package/dist/ui/assets/{ruby-C5zYZIyl.js → ruby-zA2W_49d.js} +1 -1
  116. package/dist/ui/assets/{sas-BnMEh2S3.js → sas-D49bhyaN.js} +1 -1
  117. package/dist/ui/assets/scss-BQb-cp01.js +1 -0
  118. package/dist/ui/assets/shellscript-Dga4H_Ni.js +1 -0
  119. package/dist/ui/assets/{shellsession-BpE5o7_3.js → shellsession-n3-5CqcF.js} +1 -1
  120. package/dist/ui/assets/{soy-COesmhTY.js → soy-XYBOf_BA.js} +1 -1
  121. package/dist/ui/assets/sql-ET4u5Cpl.js +1 -0
  122. package/dist/ui/assets/{stata-CGQEuSZq.js → stata-CLHcJHO_.js} +1 -1
  123. package/dist/ui/assets/{surrealql-Bz4JbU-9.js → surrealql-BdjQ60T4.js} +1 -1
  124. package/dist/ui/assets/{svelte-1jM2ejXj.js → svelte-B2RPtKJe.js} +1 -1
  125. package/dist/ui/assets/{templ-C-VwELcp.js → templ-CrByPW_W.js} +1 -1
  126. package/dist/ui/assets/{tex-t1rUiQCm.js → tex-DZBkg363.js} +1 -1
  127. package/dist/ui/assets/{ts-tags-Bewoa6pS.js → ts-tags-DJR3Gphb.js} +1 -1
  128. package/dist/ui/assets/tsx-CajzROyy.js +1 -0
  129. package/dist/ui/assets/{twig-DVg-vELl.js → twig-BZjk5Zda.js} +1 -1
  130. package/dist/ui/assets/typescript-B-EWToLj.js +1 -0
  131. package/dist/ui/assets/{vue-wPQ7UlMy.js → vue-D_rHCCr8.js} +1 -1
  132. package/dist/ui/assets/{vue-html-BHtJ8geX.js → vue-html-CC4WlvVt.js} +1 -1
  133. package/dist/ui/assets/{vue-vine-CNSGAWwc.js → vue-vine-BgNMVgpk.js} +1 -1
  134. package/dist/ui/assets/xml-BI86-WbH.js +1 -0
  135. package/dist/ui/assets/{xsl-3_vJ27aE.js → xsl-DXeLZV4J.js} +1 -1
  136. package/dist/ui/assets/yaml-60_PFlWa.js +1 -0
  137. package/dist/workspaces/bootstrap.js +72 -0
  138. package/dist/workspaces/context.js +302 -0
  139. package/dist/workspaces/conversation-test-support.js +86 -0
  140. package/dist/{git-worktrees.js → workspaces/git/git-worktrees.js} +1 -1
  141. package/dist/workspaces/inventory.js +131 -0
  142. package/dist/workspaces/paths.js +55 -0
  143. package/dist/{remote-auth.js → workspaces/relay/auth/remote-auth.js} +2 -2
  144. package/dist/workspaces/relay/result-support.js +111 -0
  145. package/dist/workspaces/relay/tests/test-support.js +203 -0
  146. package/dist/{remote-mcp-connection-pool.js → workspaces/relay/transport/remote-mcp-connection-pool.js} +1 -1
  147. package/dist/workspaces/relay/types.js +1 -0
  148. package/dist/{remote-workspace-relay.js → workspaces/relay/workspace-relay.js} +23 -172
  149. package/dist/workspaces/resources/skills.js +285 -0
  150. package/dist/{review-checkpoints.js → workspaces/review/review-checkpoints.js} +2 -2
  151. package/dist/workspaces/sessions.js +457 -0
  152. package/dist/{workspace-store.js → workspaces/state/workspace-store.js} +2 -2
  153. package/dist/workspaces.js +68 -995
  154. package/docs/chatgpt-coding-workflow.md +3 -11
  155. package/docs/configuration.md +12 -25
  156. package/docs/gotchas.md +0 -30
  157. package/docs/roadmap.md +6 -10
  158. package/docs/security.md +1 -5
  159. package/docs/setup.md +0 -11
  160. package/package.json +6 -8
  161. package/scripts/ci/architecture.mjs +109 -0
  162. package/scripts/ci/verify.mjs +5 -0
  163. package/scripts/debug/accept/bootstrap.mjs +326 -0
  164. package/scripts/debug/accept/harness.mjs +76 -0
  165. package/scripts/debug/accept/support.mjs +432 -0
  166. package/scripts/debug/accept.mjs +43 -806
  167. package/scripts/debug/relay-accept/support.mjs +135 -0
  168. package/scripts/debug/relay-accept.mjs +26 -115
  169. package/scripts/debug/traffic/support.mjs +167 -0
  170. package/scripts/debug/traffic/traffic-audit.mjs +4 -161
  171. package/scripts/{lsp-interop-support.test.mjs → lsp/interop-support.test.mjs} +1 -1
  172. package/scripts/{lsp-interop.mjs → lsp/interop.mjs} +3 -3
  173. package/scripts/wiki/sync.mjs +21 -10
  174. package/dist/activity/bash-output-store.js +0 -127
  175. package/dist/pi-tools.js +0 -110
  176. package/dist/skills.js +0 -127
  177. package/dist/ui/assets/activity-panel-app-7AckaxIR.js +0 -2
  178. package/dist/ui/assets/angular-html-BkgDQl-G.js +0 -1
  179. package/dist/ui/assets/c-IBzBpxAO.js +0 -1
  180. package/dist/ui/assets/chunk-EyZ2wyi3.js +0 -1
  181. package/dist/ui/assets/cpp-CVHeO-Jk.js +0 -1
  182. package/dist/ui/assets/css-DEfus9J5.js +0 -1
  183. package/dist/ui/assets/glsl-_ooCjaeB.js +0 -1
  184. package/dist/ui/assets/graphql-B9LqP-cY.js +0 -1
  185. package/dist/ui/assets/haml-BU5EoZBn.js +0 -1
  186. package/dist/ui/assets/heavy-payload-Bol1_mcs.js +0 -4
  187. package/dist/ui/assets/html-D-C6YZk4.js +0 -1
  188. package/dist/ui/assets/java-ppHvycj5.js +0 -1
  189. package/dist/ui/assets/javascript-DDTjKLlQ.js +0 -1
  190. package/dist/ui/assets/json-Cw91r8h1.js +0 -1
  191. package/dist/ui/assets/jsx-04X2FT_1.js +0 -1
  192. package/dist/ui/assets/lua-CiyBp52Y.js +0 -1
  193. package/dist/ui/assets/r-DfFga4si.js +0 -1
  194. package/dist/ui/assets/regexp-iC9_nbSI.js +0 -1
  195. package/dist/ui/assets/review-payload-BqJp0c5e.js +0 -1
  196. package/dist/ui/assets/scrollbar-CvE-I-jG.js +0 -287
  197. package/dist/ui/assets/scss-BfZXsAWQ.js +0 -1
  198. package/dist/ui/assets/shellscript-BC2srNaB.js +0 -1
  199. package/dist/ui/assets/sql-Ccv_yeUm.js +0 -1
  200. package/dist/ui/assets/tsx-CTXzrBkN.js +0 -1
  201. package/dist/ui/assets/typescript--ElHoFkw.js +0 -1
  202. package/dist/ui/assets/workspace-app-BLW7p6IZ.js +0 -114
  203. package/dist/ui/assets/workspace-app-BjNqR0en.js +0 -1
  204. package/dist/ui/assets/workspace-app-CmaYU4DW.js +0 -3
  205. package/dist/ui/assets/workspace-app-D2bJ5fjt.css +0 -1
  206. package/dist/ui/assets/workspace-lifecycle-app-BVbI2Ilf.js +0 -1
  207. package/dist/ui/assets/xml-JnmX6vyS.js +0 -1
  208. package/dist/ui/assets/yaml-OAcZkP-w.js +0 -1
  209. package/dist/ui/workspace-app.html +0 -18
  210. package/dist/ui/workspace-lifecycle-app.html +0 -18
  211. package/docs/agent-profile-schema.md +0 -114
  212. package/docs/artifact-exchange.md +0 -65
  213. package/docs/versioning.md +0 -205
  214. /package/capabilities/{managed-worktrees → workspace/managed-worktrees}/GUIDE.md +0 -0
  215. /package/capabilities/{workspace-tasks → workspace/workspace-tasks}/GUIDE.md +0 -0
  216. /package/dist/activity/{process-output-audit.js → runtime/process-output-audit.js} +0 -0
  217. /package/dist/{artifact-error.js → mcp/artifacts/artifact-error.js} +0 -0
  218. /package/dist/{artifact-tools.js → mcp/artifacts/artifact-tools.js} +0 -0
  219. /package/dist/{incoming-artifacts.js → mcp/artifacts/incoming-artifacts.js} +0 -0
  220. /package/dist/{advertised-files.js → mcp/filesystem/advertised-files.js} +0 -0
  221. /package/dist/{apply-patch.js → mcp/filesystem/apply-patch.js} +0 -0
  222. /package/dist/{file-mutations.js → mcp/filesystem/file-mutations.js} +0 -0
  223. /package/dist/{roots.js → mcp/filesystem/roots.js} +0 -0
  224. /package/dist/{oauth → mcp/oauth}/public-url.js +0 -0
  225. /package/dist/{oauth → mcp/oauth}/router.js +0 -0
  226. /package/dist/{operations → mcp/operations}/batch/executor.js +0 -0
  227. /package/dist/{operations → mcp/operations}/batch/scheduler.js +0 -0
  228. /package/dist/{operations → mcp/operations}/batch/types.js +0 -0
  229. /package/dist/{operations → mcp/operations}/bulk-mutation.js +0 -0
  230. /package/dist/{operations → mcp/operations}/bulk-read.js +0 -0
  231. /package/dist/{operations → mcp/operations}/core-operation-executor.js +0 -0
  232. /package/dist/{process-platform.js → mcp/process/process-platform.js} +0 -0
  233. /package/dist/{process-sessions.js → mcp/process/process-sessions.js} +0 -0
  234. /package/dist/{request-meta.js → mcp/request-meta.js} +0 -0
  235. /package/dist/{mcp-sessions.js → mcp/server/transport/mcp-sessions.js} +0 -0
  236. /package/dist/{server-shutdown.js → mcp/server/transport/server-shutdown.js} +0 -0
  237. /package/dist/{logger.js → runtime/logging/logger.js} +0 -0
  238. /package/dist/{db → runtime/state/db}/schema.js +0 -0
  239. /package/dist/{composite-activity.js → workspaces/composite/composite-activity.js} +0 -0
  240. /package/dist/{composite-workspaces.js → workspaces/composite/composite-workspaces.js} +0 -0
  241. /package/dist/{git.js → workspaces/git/git.js} +0 -0
  242. /package/dist/{workspace-presentation.js → workspaces/presentation/workspace-presentation.js} +0 -0
  243. /package/dist/{remote-transport.js → workspaces/relay/transport/remote-transport.js} +0 -0
  244. /package/dist/{workspace-task-reminders.js → workspaces/tasks/workspace-task-reminders.js} +0 -0
  245. /package/dist/{workspace-tasks.js → workspaces/tasks/workspace-tasks.js} +0 -0
  246. /package/scripts/{lsp-interop-support.mjs → lsp/interop-support.mjs} +0 -0
package/dist/server.js CHANGED
@@ -1,1348 +1,47 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { readFileSync } from "node:fs";
3
- import { access, realpath } from "node:fs/promises";
4
- import { tmpdir } from "node:os";
5
- import { resolve } from "node:path";
2
+ import { realpath } from "node:fs/promises";
6
3
  import { fileURLToPath } from "node:url";
7
- import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
8
- import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
9
- import { getOAuthProtectedResourceMetadataUrl } from "@modelcontextprotocol/sdk/server/auth/router.js";
10
- import { requireBearerAuth } from "@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js";
11
- import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
12
- import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
13
- import { checkResourceAllowed, resourceUrlFromServerUrl } from "@modelcontextprotocol/sdk/shared/auth-utils.js";
14
- import { registerAppResource, registerAppTool, RESOURCE_MIME_TYPE, } from "@modelcontextprotocol/ext-apps/server";
15
- import express from "express";
16
- import * as z from "zod/v4";
17
- import { applyPatch } from "./apply-patch.js";
18
- import { ActivityAuditStore } from "./activity/audit-store.js";
19
- import { BashOutputStore } from "./activity/bash-output-store.js";
20
- import { HostTurnStore } from "./activity/host-turn-store.js";
21
- import { registerActivityQueryTools } from "./activity/mcp-query-tools.js";
22
- import { ActivityLifecycle, } from "./activity/lifecycle.js";
23
- import { ActivityQueryService } from "./activity/query-service.js";
24
- import { buildCapabilityFingerprint, loadCapabilityGuides } from "./capabilities.js";
25
- import { CapabilityError, createCapabilityRegistry, } from "./capability-registry.js";
26
- import { deletePath, renamePath } from "./file-mutations.js";
27
- import { downloadIncomingArtifact, isArtifactDownloadSupportedPlatform, } from "./artifact-tools.js";
28
- import { ArtifactError } from "./artifact-error.js";
29
- import { loadConfig } from "./config.js";
4
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
+ import { registerAppResource } from "@modelcontextprotocol/ext-apps/server";
6
+ import { registerActivityQueryTools } from "./activity/runtime/mcp-query-tools.js";
7
+ import { buildCapabilityFingerprint, loadCapabilityGuides } from "./mcp/server/core/capabilities.js";
8
+ import { CapabilityError, createCapabilityRegistry } from "./mcp/server/core/capability-registry.js";
9
+ import { downloadIncomingArtifact, isArtifactDownloadSupportedPlatform } from "./mcp/artifacts/artifact-tools.js";
10
+ import { ArtifactError } from "./mcp/artifacts/artifact-error.js";
11
+ import { loadConfig } from "./runtime/config/config.js";
30
12
  import { CodeIntelligenceError } from "./lsp/code-intelligence.js";
31
- import { CodeIntelligenceManager } from "./lsp/runtime/manager.js";
32
- import { attachHookReports, HookRunner, runToolWithHooks } from "./hooks.js";
33
- import { checkHookConfiguration } from "./hook-cli.js";
34
- import { buildServerInstructions, buildShellMutationPolicy, buildToolDescriptions, toolNames, } from "./mcp/server-instructions.js";
35
- import { createOpenAIIncomingArtifactAdapter, IncomingArtifactAdapterRegistry, } from "./incoming-artifacts.js";
36
- import { logEvent, requestIp, requestPath, commandPreview, transportSessionIdPrefix, workspaceLogLabel, } from "./logger.js";
37
- import { editFileTool, readFileTool, writeFileTool, } from "./pi-tools.js";
38
- import { SingleUserOAuthProvider } from "./oauth-provider.js";
39
- import { createForgeRelayAuthRouter } from "./oauth/router.js";
40
- import { publicEndpointUrl } from "./oauth/public-url.js";
41
- import { BatchExecutor } from "./operations/batch/executor.js";
42
- import { executeBulkRead } from "./operations/bulk-read.js";
43
- import { NativeBulkMutationExecutor } from "./operations/native-bulk-mutations.js";
44
- import { createCoreOperationExecutor, } from "./operations/core-operation-executor.js";
45
- import { McpTransportRegistry, } from "./mcp-sessions.js";
46
- import { ProcessManager, resolveProcessId, } from "./process-sessions.js";
47
- import { createReviewCheckpointManager } from "./review-checkpoints.js";
48
- import { CompositeActivityCoordinator } from "./composite-activity.js";
49
- import { CompositeWorkspaceRegistry } from "./composite-workspaces.js";
50
- import { RemoteWorkspaceRelay } from "./remote-workspace-relay.js";
51
- import { hostConversationScopeId, openAiConversationScopeId } from "./request-meta.js";
52
- import { ACTIVITY_PANEL_APP_LEGACY_URI, ACTIVITY_PANEL_APP_URI_TEMPLATE, MCP_APP_RESOURCE_TEMPLATE_REVISION, readActivityPanelAppManifestEntry, readWorkspaceAppManifestEntry, readWorkspaceLifecycleAppManifestEntry, resolveActivityPanelAppIdentity, resolveWorkspaceAppIdentity, resolveWorkspaceLifecycleAppIdentity, WORKSPACE_APP_LEGACY_URI, WORKSPACE_APP_URI_TEMPLATE, WORKSPACE_LIFECYCLE_APP_LEGACY_URI, WORKSPACE_LIFECYCLE_APP_URI_TEMPLATE, } from "./mcp-app-template.js";
53
- import { shutdownHttpServer } from "./server-shutdown.js";
54
- import { formatPathForPrompt } from "./skills.js";
55
- import { createWorkspaceStore } from "./workspace-store.js";
56
- import { WorkspaceTaskReminderTracker } from "./workspace-task-reminders.js";
57
- import { WorkspaceTaskStore } from "./workspace-tasks.js";
58
- import { compactWorkspacePresentation } from "./workspace-presentation.js";
59
- import { formatAgentsPath, WorkspaceRegistry, } from "./workspaces.js";
60
- import { formatAvailableSubagentProfile, summarizeSubagentProfile } from "./subagents/profiles.js";
61
- import { formatSubagentProviderAvailabilitySummary, formatUnavailableSubagentProvider, getSubagentProviderAvailabilitySnapshot, } from "./subagents/providers/availability.js";
62
- import { capabilityActivityAuditRequest, capabilityActivityAuditResult } from "./subagents/sessions/mcp/audit.js";
13
+ import { HookRunner } from "./mcp/hooks/hooks.js";
14
+ import { checkHookConfiguration } from "./mcp/hooks/hook-cli.js";
15
+ import { buildServerInstructions, buildToolDescriptions, toolNames } from "./mcp/server-instructions.js";
16
+ import { IncomingArtifactAdapterRegistry } from "./mcp/artifacts/incoming-artifacts.js";
17
+ import { registerProcessTools } from "./mcp/process/tools.js";
18
+ import { attachCompletedProcessNotices, recordBashCompletion } from "./mcp/process/runtime.js";
19
+ import { CompositeActivityCoordinator } from "./workspaces/composite/composite-activity.js";
20
+ import { CompositeWorkspaceRegistry } from "./workspaces/composite/composite-workspaces.js";
21
+ import { RemoteWorkspaceRelay } from "./workspaces/relay/workspace-relay.js";
22
+ import { hostConversationScopeId } from "./mcp/request-meta.js";
23
+ import { createActivityPanelApp } from "./mcp/panel/app.js";
24
+ import { shutdownHttpServer } from "./mcp/server/transport/server-shutdown.js";
25
+ import { formatPathForPrompt } from "./workspaces/resources/skills.js";
26
+ import { WorkspaceTaskReminderTracker } from "./workspaces/tasks/workspace-task-reminders.js";
27
+ import { WorkspaceTaskStore } from "./workspaces/tasks/workspace-tasks.js";
28
+ import { compactWorkspacePresentation } from "./workspaces/presentation/workspace-presentation.js";
29
+ import { formatAgentsPath } from "./workspaces.js";
30
+ import { summarizeSubagentProfile } from "./subagents/profiles.js";
31
+ import { formatSubagentProviderAvailabilitySummary } from "./subagents/providers/availability.js";
63
32
  import { createSubagentMcpRuntime } from "./subagents/sessions/mcp/runtime.js";
64
- // Legacy MCP Streamable HTTP clients can reconnect without closing the previous
65
- // transport. Bound stale transport-session retention so abandoned transports do
66
- // not accumulate for the life of the process.
67
- const MCP_TRANSPORT_IDLE_TIMEOUT_MS = 24 * 60 * 60 * 1_000;
68
- const MAX_MCP_TRANSPORT_SESSIONS = 64;
69
- const FORGERELAY_VERSION = readForgeRelayVersion();
70
- const MCP_TRANSPORT_CLEANUP_INTERVAL_MS = 5 * 60 * 1_000;
71
- const WRITE_TOOL_ANNOTATIONS = {
72
- readOnlyHint: false,
73
- destructiveHint: true,
74
- idempotentHint: false,
75
- openWorldHint: false,
76
- };
77
- const EDIT_TOOL_ANNOTATIONS = {
78
- readOnlyHint: false,
79
- destructiveHint: true,
80
- idempotentHint: false,
81
- openWorldHint: false,
82
- };
83
- const SHELL_TOOL_ANNOTATIONS = {
84
- readOnlyHint: false,
85
- destructiveHint: true,
86
- idempotentHint: false,
87
- openWorldHint: true,
88
- };
89
- function shouldAttachWidget(mode, kind) {
90
- if (mode === "off")
91
- return false;
92
- return kind === "activity";
93
- }
94
- function toolWidgetDescriptorMeta(config, kind) {
95
- if (!shouldAttachWidget(config.widgets, kind))
96
- return { _meta: {} };
97
- const resourceUri = kind === "activity"
98
- ? currentActivityPanelAppIdentity(config).uri
99
- : currentWorkspaceLifecycleAppIdentity(config).uri;
100
- return {
101
- _meta: {
102
- ui: {
103
- resourceUri,
104
- visibility: ["model", "app"],
105
- },
106
- },
107
- };
108
- }
109
- function workspaceLogContext(workspace, _transportSessionId) {
110
- return {
111
- workspaceId: workspace.id,
112
- workspace: workspaceLogLabel(workspace.root, workspace.id),
113
- };
114
- }
115
- function formatDiscoveredWorkspaceInstructions(files, workspaceRoot) {
116
- return [
117
- "Workspace instructions discovered for this path. Apply them to follow-up work under their directories:",
118
- ...files.flatMap((file) => [
119
- `--- ${formatAgentsPath(file.path, workspaceRoot)} ---`,
120
- file.content.trimEnd(),
121
- ]),
122
- ].join("\n");
123
- }
124
- async function assertWorkspaceInstructionsLoadedBeforeSideEffect(workspaces, workspace, paths) {
125
- const discovered = new Map();
126
- for (const path of paths) {
127
- const absolutePath = resolve(workspace.root, path);
128
- for (const file of await workspaces.discoverPathInstructions(workspace, absolutePath)) {
129
- discovered.set(file.path, file);
130
- }
131
- }
132
- if (discovered.size === 0)
133
- return;
134
- throw new Error([
135
- formatDiscoveredWorkspaceInstructions([...discovered.values()], workspace.root),
136
- "Apply these instructions, then retry this tool call. No mutation or command was executed.",
137
- ].join("\n"));
138
- }
139
- function resultOutputSchema(extra = {}) {
140
- return {
141
- result: z
142
- .string()
143
- .describe("Model-readable result text for follow-up reasoning and plain MCP hosts."),
144
- ...extra,
145
- };
146
- }
147
- const workspaceSkillOutputSchema = z.object({
148
- name: z.string(),
149
- description: z.string(),
150
- });
151
- const workspaceSkillDiagnosticOutputSchema = z.object({
152
- type: z.enum(["warning", "error", "collision"]),
153
- message: z.string(),
154
- collision: z.object({
155
- resourceType: z.enum(["extension", "skill", "prompt", "theme"]),
156
- name: z.string(),
157
- }).optional(),
158
- });
159
- function redactSkillDiagnosticPaths(diagnostics) {
160
- return diagnostics.map((diagnostic) => {
161
- let message = diagnostic.message;
162
- const hiddenPaths = [
163
- diagnostic.path,
164
- diagnostic.collision?.winnerPath,
165
- diagnostic.collision?.loserPath,
166
- ].filter((path) => Boolean(path));
167
- for (const path of hiddenPaths) {
168
- message = message.split(path).join("<skill-path>");
169
- }
170
- return {
171
- type: diagnostic.type,
172
- message,
173
- ...(diagnostic.collision
174
- ? {
175
- collision: {
176
- resourceType: diagnostic.collision.resourceType,
177
- name: diagnostic.collision.name,
178
- },
179
- }
180
- : {}),
181
- };
182
- });
183
- }
184
- const capabilityFingerprintOutputSchema = z.object({
185
- version: z.string(),
186
- toolMode: z.enum(["minimal", "full", "codex"]),
187
- capabilities: z.array(z.string()),
188
- });
189
- const capabilityGuideOutputSchema = z.object({
190
- name: z.string(),
191
- description: z.string(),
192
- whenToRead: z.string(),
193
- path: z.string(),
194
- });
195
- const capabilityCatalogGuideOutputSchema = z.object({
196
- name: z.string(),
197
- path: z.string(),
198
- readBeforeFirstUse: z.boolean(),
199
- });
200
- const capabilityCatalogOutputSchema = z.object({
201
- name: z.string(),
202
- description: z.string(),
203
- available: z.boolean(),
204
- unavailableReason: z.string().optional(),
205
- batchPolicy: z.enum(["parallel", "serial", "unsupported"]),
206
- guide: capabilityCatalogGuideOutputSchema,
207
- });
208
- const capabilityErrorOutputSchema = z.object({
209
- code: z.string(),
210
- message: z.string(),
211
- });
212
- const workspaceAgentsFileOutputSchema = z.object({
213
- path: z.string(),
214
- content: z.string(),
215
- });
216
- const workspaceLocalAgentOutputSchema = z.object({
217
- name: z.string(),
218
- description: z.string(),
219
- provider: z.string(),
220
- model: z.string().optional(),
221
- thinking: z.string().optional(),
222
- providerAvailable: z.boolean().optional(),
223
- providerUnavailableReason: z.string().optional(),
224
- });
225
- const workspaceSubagentProviderOutputSchema = z.object({
226
- name: z.string(),
227
- available: z.boolean(),
228
- continuationSupported: z.boolean(),
229
- reason: z.string().optional(),
230
- });
231
- const workspaceAvailableAgentsFileOutputSchema = z.object({
232
- path: z.string(),
233
- });
234
- const workspaceInventoryEntryOutputSchema = z.object({
235
- label: z.string(),
236
- workspaceId: z.string(),
237
- root: z.string(),
238
- status: z.string(),
239
- state: z.enum(["active", "stale", "invalid", "closed"]),
240
- mode: z.enum(["checkout", "worktree"]),
241
- sourceRoot: z.string().optional(),
242
- branch: z.string().optional(),
243
- targetBranch: z.string().optional(),
244
- managed: z.boolean(),
245
- createdAt: z.string(),
246
- lastUsedAt: z.string(),
247
- idleMs: z.number().nonnegative(),
248
- rootValid: z.boolean(),
249
- current: z.boolean(),
250
- });
251
- const workspaceInventorySummaryOutputSchema = z.object({
252
- total: z.number().int().nonnegative(),
253
- matching: z.number().int().nonnegative(),
254
- active: z.number().int().nonnegative(),
255
- stale: z.number().int().nonnegative(),
256
- invalid: z.number().int().nonnegative(),
257
- closed: z.number().int().nonnegative(),
258
- });
259
- const workspaceInventoryPageOutputSchema = z.object({
260
- offset: z.number().int().nonnegative(),
261
- limit: z.number().int().positive(),
262
- hasMore: z.boolean(),
263
- });
264
- const workspaceTaskInspectionSummaryOutputSchema = z.object({
265
- level: z.literal("summary"),
266
- version: z.literal(1),
267
- revision: z.number().int().nonnegative(),
268
- lists: z.array(z.object({
269
- id: z.string(),
270
- name: z.string(),
271
- state: z.enum(["active", "archived"]),
272
- revision: z.number().int().positive(),
273
- taskCount: z.number().int().nonnegative(),
274
- unfinishedTaskCount: z.number().int().nonnegative(),
275
- })),
276
- });
277
- const workspaceInspectionMemberOutputSchema = z.object({
278
- name: z.string(),
279
- purpose: z.string(),
280
- workspaceId: z.string(),
281
- known: z.boolean(),
282
- location: z.enum(["local", "relay"]).optional(),
283
- state: z.enum(["active", "stale", "invalid", "closed"]).optional(),
284
- status: z.string().optional(),
285
- routeState: z.literal("known").optional(),
286
- mode: z.enum(["checkout", "worktree"]).optional(),
287
- rootValid: z.boolean().optional(),
288
- });
289
- const workspaceInspectionOutputSchema = z.union([
290
- z.object({
291
- workspaceId: z.string(),
292
- kind: z.literal("workspace"),
293
- location: z.literal("local"),
294
- label: z.string(),
295
- root: z.string(),
296
- status: z.string(),
297
- state: z.enum(["active", "stale", "invalid", "closed"]),
298
- mode: z.enum(["checkout", "worktree"]),
299
- sourceRoot: z.string().optional(),
300
- branch: z.string().optional(),
301
- targetBranch: z.string().optional(),
302
- managed: z.boolean(),
303
- createdAt: z.string(),
304
- lastUsedAt: z.string(),
305
- idleMs: z.number().nonnegative(),
306
- rootValid: z.boolean(),
307
- taskSummary: workspaceTaskInspectionSummaryOutputSchema.optional(),
308
- }),
309
- z.object({
310
- workspaceId: z.string(),
311
- kind: z.literal("workspace"),
312
- location: z.literal("relay"),
313
- root: z.string(),
314
- routeState: z.literal("known"),
315
- status: z.string().optional(),
316
- state: z.enum(["active", "stale", "invalid", "closed"]).optional(),
317
- mode: z.enum(["checkout", "worktree"]),
318
- sourceRoot: z.string().optional(),
319
- branch: z.string().optional(),
320
- targetBranch: z.string().optional(),
321
- managed: z.boolean().optional(),
322
- createdAt: z.string().optional(),
323
- lastUsedAt: z.string().optional(),
324
- idleMs: z.number().nonnegative().optional(),
325
- rootValid: z.boolean().optional(),
326
- taskSummary: workspaceTaskInspectionSummaryOutputSchema.optional(),
327
- relay: z.string(),
328
- executionLocation: z.string(),
329
- }),
330
- z.object({
331
- workspaceId: z.string(),
332
- kind: z.literal("composite"),
333
- name: z.string(),
334
- status: z.enum(["active", "closed"]),
335
- state: z.enum(["active", "closed"]),
336
- createdAt: z.string(),
337
- lastUsedAt: z.string(),
338
- members: z.array(workspaceInspectionMemberOutputSchema),
339
- taskSummary: workspaceTaskInspectionSummaryOutputSchema.optional(),
340
- }),
341
- ]);
342
- const reviewFileOutputSchema = z.object({
343
- path: z.string(),
344
- previousPath: z.string().optional(),
345
- type: z.enum(["change", "rename-pure", "rename-changed", "new", "deleted"]),
346
- additions: z.number(),
347
- removals: z.number(),
348
- });
349
- const reviewSummaryOutputSchema = z.object({
350
- files: z.number(),
351
- additions: z.number(),
352
- removals: z.number(),
353
- });
354
- function sendJsonRpcError(res, status, code, message) {
355
- res.status(status).json({
356
- jsonrpc: "2.0",
357
- error: { code, message },
358
- id: null,
359
- });
360
- }
361
- function requestLogFields(req, config) {
362
- return {
363
- ip: requestIp(req),
364
- host: req.header("host"),
365
- userAgent: req.header("user-agent"),
366
- origin: req.header("origin"),
367
- referer: req.header("referer"),
368
- contentLength: req.header("content-length"),
369
- };
370
- }
371
- function mcpRequestDebugFields(body) {
372
- if (!body || typeof body !== "object" || Array.isArray(body))
373
- return {};
374
- const request = body;
375
- const rpcMethod = typeof request.method === "string" ? request.method : undefined;
376
- const params = request.params && typeof request.params === "object" && !Array.isArray(request.params)
377
- ? request.params
378
- : undefined;
379
- const rpcMeta = params?._meta && typeof params._meta === "object" && !Array.isArray(params._meta)
380
- ? params._meta
381
- : undefined;
382
- const rpcMetaKeys = rpcMeta ? Object.keys(rpcMeta).sort() : [];
383
- let rpcTarget;
384
- if (rpcMethod === "resources/read" && typeof params?.uri === "string") {
385
- rpcTarget = params.uri;
386
- }
387
- else if (rpcMethod === "tools/call" && typeof params?.name === "string") {
388
- rpcTarget = params.name;
389
- }
390
- return {
391
- rpcMethod,
392
- rpcTarget,
393
- ...(rpcMetaKeys.length > 0 ? { rpcMetaKeys } : {}),
394
- };
395
- }
396
- function logToolCall(config, fields) {
397
- if (!config.logging.toolCalls)
398
- return;
399
- const { command, ...safeFields } = fields;
400
- logEvent(config.logging, fields.success ? "info" : "warn", "tool_call", {
401
- ...safeFields,
402
- commandPreview: config.logging.shellCommands && command ? commandPreview(command) : undefined,
403
- });
404
- }
405
- function contentText(content) {
406
- return content
407
- .filter((item) => item.type === "text")
408
- .map((item) => item.text)
409
- .join("\n");
410
- }
411
- function toolErrorPreview(content) {
412
- const text = contentText(content).replace(/\s+/g, " ").trim();
413
- if (!text)
414
- return undefined;
415
- return text.length > 240 ? `${text.slice(0, 237)}...` : text;
416
- }
417
- function logFailedToolResponse(config, fields, content, startedAt) {
418
- logToolCall(config, {
419
- ...fields,
420
- success: false,
421
- durationMs: Math.round(performance.now() - startedAt),
422
- error: toolErrorPreview(content),
423
- });
424
- }
425
- function textBlock(text) {
426
- return { type: "text", text };
427
- }
428
- function attachWorkspaceTaskReminder(result, reminder) {
429
- if (!reminder || toolResultIsError(result) || typeof result !== "object" || result === null)
430
- return result;
431
- const content = result.content;
432
- if (!Array.isArray(content))
433
- return result;
434
- return {
435
- ...result,
436
- content: [...content, textBlock(reminder)],
437
- };
438
- }
439
- function textSummary(content) {
440
- const text = contentText(content);
441
- return {
442
- lines: text.length === 0 ? 0 : text.split("\n").length,
443
- characters: text.length,
444
- };
445
- }
446
- function contentLineCount(content) {
447
- if (content.length === 0)
448
- return 0;
449
- return content.endsWith("\n")
450
- ? content.slice(0, -1).split("\n").length
451
- : content.split("\n").length;
452
- }
453
- function countDiffStats(diff) {
454
- if (!diff)
455
- return { additions: 0, removals: 0 };
456
- let additions = 0;
457
- let removals = 0;
458
- for (const line of diff.split("\n")) {
459
- if (line.startsWith("+") && !line.startsWith("+++"))
460
- additions++;
461
- if (line.startsWith("-") && !line.startsWith("---"))
462
- removals++;
463
- }
464
- return { additions, removals };
465
- }
466
- function newFilePatch(path, content) {
467
- const lines = content.length === 0
468
- ? []
469
- : content.endsWith("\n")
470
- ? content.slice(0, -1).split("\n")
471
- : content.split("\n");
472
- const hunkLength = lines.length;
473
- const hunkRange = hunkLength === 0 ? "+0,0" : `+1,${hunkLength}`;
474
- const body = lines.map((line) => `+${line}`).join("\n");
475
- return [
476
- `diff --git a/${path} b/${path}`,
477
- "new file mode 100644",
478
- "index 0000000..0000000",
479
- "--- /dev/null",
480
- `+++ b/${path}`,
481
- `@@ -0,0 ${hunkRange} @@`,
482
- body,
483
- ]
484
- .filter((line) => line.length > 0)
485
- .join("\n");
486
- }
487
- function assetBaseUrl(config) {
488
- return `${config.publicBaseUrl.replace(/\/+$/, "")}/mcp-app-assets`;
489
- }
490
- function uiManifestUrl() {
491
- return new URL("../dist/ui/.vite/manifest.json", import.meta.url);
492
- }
493
- function uiBuildDirectoryUrl() {
494
- return new URL("../dist/ui/", import.meta.url);
495
- }
496
- const cachedWorkspaceAppIdentities = new Map();
497
- const cachedWorkspaceLifecycleAppIdentities = new Map();
498
- const cachedActivityPanelAppIdentities = new Map();
499
- function appResourceContractRevision(config) {
500
- return [
501
- MCP_APP_RESOURCE_TEMPLATE_REVISION,
502
- `publicBaseUrls=${JSON.stringify(config.publicBaseUrls)}`,
503
- ].join("\0");
504
- }
505
- function appIdentityOptions(config) {
506
- return {
507
- manifestUrl: uiManifestUrl(),
508
- buildDirectoryUrl: uiBuildDirectoryUrl(),
509
- fallbackRevision: FORGERELAY_VERSION,
510
- resourceTemplateRevision: appResourceContractRevision(config),
511
- };
512
- }
513
- function currentWorkspaceAppIdentity(config) {
514
- const key = appResourceContractRevision(config);
515
- let identity = cachedWorkspaceAppIdentities.get(key);
516
- if (!identity) {
517
- identity = resolveWorkspaceAppIdentity(appIdentityOptions(config));
518
- cachedWorkspaceAppIdentities.set(key, identity);
519
- }
520
- return identity;
521
- }
522
- function currentWorkspaceLifecycleAppIdentity(config) {
523
- const key = appResourceContractRevision(config);
524
- let identity = cachedWorkspaceLifecycleAppIdentities.get(key);
525
- if (!identity) {
526
- identity = resolveWorkspaceLifecycleAppIdentity(appIdentityOptions(config));
527
- cachedWorkspaceLifecycleAppIdentities.set(key, identity);
528
- }
529
- return identity;
530
- }
531
- function currentActivityPanelAppIdentity(config) {
532
- const key = appResourceContractRevision(config);
533
- let identity = cachedActivityPanelAppIdentities.get(key);
534
- if (!identity) {
535
- identity = resolveActivityPanelAppIdentity(appIdentityOptions(config));
536
- cachedActivityPanelAppIdentities.set(key, identity);
537
- }
538
- return identity;
539
- }
540
- function getWorkspaceAppManifestEntry() {
541
- return readWorkspaceAppManifestEntry(uiManifestUrl());
542
- }
543
- function getWorkspaceLifecycleAppManifestEntry() {
544
- return readWorkspaceLifecycleAppManifestEntry(uiManifestUrl());
545
- }
546
- function getActivityPanelAppManifestEntry() {
547
- return readActivityPanelAppManifestEntry(uiManifestUrl());
548
- }
549
- function assetUrl(baseUrl, assetPath) {
550
- return `${baseUrl}/${assetPath.replace(/^\/+/, "")}`;
551
- }
552
- function mcpAppHtml(config, entry, title, waitingMessage) {
553
- const baseUrl = assetBaseUrl(config);
554
- const stylesheets = (entry.css ?? [])
555
- .map((stylesheet) => ` <link rel="stylesheet" crossorigin href="${assetUrl(baseUrl, stylesheet)}" />`)
556
- .join("\n");
557
- return `<!doctype html>
558
- <html lang="en">
559
- <head>
560
- <meta charset="UTF-8" />
561
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
562
- <title>${title}</title>
563
- <script type="module" crossorigin src="${assetUrl(baseUrl, entry.file)}"></script>
564
- ${stylesheets}
565
- </head>
566
- <body>
567
- <main id="app" class="shell">
568
- <section class="empty">${waitingMessage}</section>
569
- </main>
570
- </body>
571
- </html>`;
572
- }
573
- function workspaceAppHtml(config) {
574
- return mcpAppHtml(config, getWorkspaceAppManifestEntry(), "ForgeRelay Workspace", "Waiting for a workspace result.");
575
- }
576
- function workspaceLifecycleAppHtml(config) {
577
- return mcpAppHtml(config, getWorkspaceLifecycleAppManifestEntry(), "ForgeRelay Workspace Lifecycle", "Waiting for a workspace result.");
578
- }
579
- function activityPanelAppHtml(config) {
580
- return mcpAppHtml(config, getActivityPanelAppManifestEntry(), "ForgeRelay Activity Panel", "Waiting for Activity Panel state.");
581
- }
582
- function appDomain(config) {
583
- return new URL(config.publicBaseUrl).origin;
584
- }
585
- function appCsp(config) {
586
- return {
587
- resourceDomains: [...config.publicBaseUrls],
588
- connectDomains: [...config.publicBaseUrls],
589
- };
590
- }
591
- function uiBuildDirectory() {
592
- return fileURLToPath(new URL("../dist/ui", import.meta.url));
593
- }
594
- function setAssetHeaders(res) {
595
- res.setHeader("Access-Control-Allow-Origin", "*");
596
- res.setHeader("Access-Control-Allow-Methods", "GET, HEAD, OPTIONS");
597
- res.setHeader("Access-Control-Allow-Headers", "Content-Type, Range");
598
- res.setHeader("Cross-Origin-Resource-Policy", "cross-origin");
599
- }
600
- async function assertMcpAppAssets(entry) {
601
- const candidates = [entry.file, ...(entry.css ?? [])].map((assetPath) => new URL(`../dist/ui/${assetPath}`, import.meta.url));
602
- for (const candidate of candidates)
603
- await access(candidate);
604
- }
605
- function appResourceVariant(requestedUri, currentUri, legacyUri) {
606
- if (requestedUri === currentUri)
607
- return "current";
608
- if (requestedUri === legacyUri)
609
- return "legacy";
610
- return "historical";
611
- }
612
- async function readMcpAppResource(config, options) {
613
- const compatibility = appResourceVariant(options.requestedUri, options.currentUri, options.legacyUri);
614
- try {
615
- await assertMcpAppAssets(options.entry);
616
- const result = {
617
- contents: [{
618
- uri: options.requestedUri,
619
- mimeType: RESOURCE_MIME_TYPE,
620
- text: options.html,
621
- _meta: {
622
- ui: {
623
- domain: appDomain(config),
624
- csp: appCsp(config),
625
- },
626
- // MCP Apps defines resource metadata under `_meta.ui`. Inspector 2.3.0
627
- // reads the content-item CSP/domain from `_meta` directly, so mirror
628
- // these values until that installed-host compatibility gap is gone.
629
- domain: appDomain(config),
630
- csp: appCsp(config),
631
- },
632
- }],
633
- };
634
- logEvent(config.logging, "debug", "mcp_app_template_read", {
635
- requestedUri: options.requestedUri,
636
- currentUri: options.currentUri,
637
- compatibility,
638
- transportSessionIdPrefix: transportSessionIdPrefix(options.transportSessionId),
639
- });
640
- return result;
641
- }
642
- catch (error) {
643
- logEvent(config.logging, "warn", "mcp_app_template_read_failed", {
644
- requestedUri: options.requestedUri,
645
- currentUri: options.currentUri,
646
- compatibility,
647
- error: error instanceof Error ? error.message : String(error),
648
- transportSessionIdPrefix: transportSessionIdPrefix(options.transportSessionId),
649
- });
650
- throw error;
651
- }
652
- }
653
- function readWorkspaceAppResource(config, requestedUri, transportSessionId, currentUri = currentWorkspaceAppIdentity(config).uri, legacyUri = WORKSPACE_APP_LEGACY_URI) {
654
- return readMcpAppResource(config, {
655
- requestedUri,
656
- currentUri,
657
- legacyUri,
658
- entry: getWorkspaceAppManifestEntry(),
659
- html: workspaceAppHtml(config),
660
- transportSessionId,
661
- });
662
- }
663
- function readWorkspaceLifecycleAppResource(config, requestedUri, transportSessionId) {
664
- return readMcpAppResource(config, {
665
- requestedUri,
666
- currentUri: currentWorkspaceLifecycleAppIdentity(config).uri,
667
- legacyUri: WORKSPACE_LIFECYCLE_APP_LEGACY_URI,
668
- entry: getWorkspaceLifecycleAppManifestEntry(),
669
- html: workspaceLifecycleAppHtml(config),
670
- transportSessionId,
671
- });
672
- }
673
- function readActivityPanelAppResource(config, requestedUri, transportSessionId) {
674
- return readMcpAppResource(config, {
675
- requestedUri,
676
- currentUri: currentActivityPanelAppIdentity(config).uri,
677
- legacyUri: ACTIVITY_PANEL_APP_LEGACY_URI,
678
- entry: getActivityPanelAppManifestEntry(),
679
- html: activityPanelAppHtml(config),
680
- transportSessionId,
681
- });
682
- }
683
- const PROCESS_RESPONSE_OUTPUT_LINES = 10;
684
- function compactProcessOutput(output) {
685
- if (!output)
686
- return { output: "", truncated: false };
687
- const trailingNewline = output.endsWith("\n");
688
- const body = trailingNewline ? output.slice(0, -1) : output;
689
- const lines = body.split("\n");
690
- if (lines.length <= PROCESS_RESPONSE_OUTPUT_LINES)
691
- return { output, truncated: false };
692
- const compact = lines.slice(-PROCESS_RESPONSE_OUTPUT_LINES).join("\n");
693
- return {
694
- output: trailingNewline ? `${compact}\n` : compact,
695
- truncated: true,
696
- };
697
- }
698
- function outputIdNotice(outputId) {
699
- return outputId ? `Full output ID: ${outputId}.` : "";
700
- }
701
- function processResult(snapshot) {
702
- const status = snapshot.running
703
- ? `Process running with process ID ${snapshot.processId}.`
704
- : snapshot.timedOut
705
- ? "Process timed out and was terminated."
706
- : snapshot.signal
707
- ? `Process exited after signal ${snapshot.signal}.`
708
- : `Process exited with code ${snapshot.exitCode ?? "unknown"}.`;
709
- const compact = compactProcessOutput(snapshot.output).output.replace(/\n$/, "");
710
- return [compact, status, outputIdNotice(snapshot.outputId)].filter(Boolean).join("\n");
711
- }
712
- function completedProcessResult(snapshot) {
713
- const status = snapshot.timedOut
714
- ? `Background process ${snapshot.processId} timed out and was terminated.`
715
- : snapshot.signal
716
- ? `Background process ${snapshot.processId} exited after signal ${snapshot.signal}.`
717
- : `Background process ${snapshot.processId} exited with code ${snapshot.exitCode ?? "unknown"}.`;
718
- const command = `Command: ${snapshot.command}`;
719
- const output = compactProcessOutput(snapshot.output).output.replace(/\n$/, "");
720
- return [status, command, output, outputIdNotice(snapshot.outputId)].filter(Boolean).join("\n");
721
- }
722
- function attachCompletedProcessNotices(processSessions, workspaceId, result, onCompleted) {
723
- if (result instanceof Error) {
724
- const completed = processSessions.takeCompleted(workspaceId);
725
- for (const snapshot of completed)
726
- onCompleted?.(snapshot);
727
- if (completed.length > 0) {
728
- result.message = [
729
- result.message,
730
- ...completed.map((snapshot) => completedProcessResult(snapshot)),
731
- ].join("\n\n");
732
- }
733
- return result;
734
- }
735
- if (typeof result !== "object" || result === null)
736
- return result;
737
- const content = result.content;
738
- if (!Array.isArray(content))
739
- return result;
740
- const structured = result.structuredContent;
741
- const currentProcessId = structured?.running === true
742
- ? typeof structured.processId === "number"
743
- ? structured.processId
744
- : typeof structured.sessionId === "number"
745
- ? structured.sessionId
746
- : undefined
747
- : undefined;
748
- const completed = processSessions.takeCompleted(workspaceId, undefined, currentProcessId);
749
- for (const snapshot of completed)
750
- onCompleted?.(snapshot);
751
- if (completed.length === 0)
752
- return result;
753
- return {
754
- ...result,
755
- content: [
756
- ...content,
757
- ...completed.map((snapshot) => textBlock(completedProcessResult(snapshot))),
758
- ],
759
- };
760
- }
761
- function processOutputSchema() {
762
- return resultOutputSchema({
763
- processId: z.number().int().positive().optional().describe("Canonical process handle for bash(action=\"process\") or the active command adapter."),
764
- sessionId: z.number().int().positive().optional().describe("Deprecated alias of processId for compatibility."),
765
- outputId: z.string().optional().describe("Stable local audit identifier for retrieving the complete original process output."),
766
- running: z.boolean(),
767
- exitCode: z.number().int().optional(),
768
- signal: z.string().optional(),
769
- timedOut: z.boolean(),
770
- wallTimeMs: z.number().nonnegative(),
771
- outputTruncated: z.boolean(),
772
- });
773
- }
774
- function readForgeRelayVersion() {
775
- const packageJson = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
776
- if (typeof packageJson.version !== "string" || packageJson.version.length === 0) {
777
- throw new Error("Unable to read ForgeRelay package version.");
778
- }
779
- return packageJson.version;
780
- }
781
- function processToolResponse(tool, workspaceId, snapshot, summary) {
782
- const compact = compactProcessOutput(snapshot.output);
783
- const result = processResult(snapshot);
784
- const content = [textBlock(result)];
785
- const outputSummary = textSummary(compact.output ? [textBlock(compact.output)] : []);
786
- return {
787
- content,
788
- _meta: {
789
- tool,
790
- card: {
791
- workspaceId,
792
- summary: { ...summary, ...outputSummary },
793
- payload: { content },
794
- },
795
- },
796
- structuredContent: {
797
- result,
798
- processId: snapshot.processId,
799
- sessionId: snapshot.sessionId,
800
- outputId: snapshot.outputId,
801
- running: snapshot.running,
802
- exitCode: snapshot.exitCode,
803
- signal: snapshot.signal,
804
- timedOut: snapshot.timedOut,
805
- wallTimeMs: snapshot.wallTimeMs,
806
- outputTruncated: snapshot.outputTruncated || compact.truncated,
807
- },
808
- };
809
- }
810
- function durableOutputResult(record) {
811
- const status = record.status === "running"
812
- ? `Process ${record.processId} is still running.`
813
- : record.timedOut
814
- ? `Process ${record.processId} timed out and was terminated.`
815
- : record.signal
816
- ? `Process ${record.processId} exited after signal ${record.signal}.`
817
- : `Process ${record.processId} exited with code ${record.exitCode ?? "unknown"}.`;
818
- return [record.output.replace(/\n$/, ""), status, `Full output ID: ${record.outputId}.`]
819
- .filter(Boolean)
820
- .join("\n");
821
- }
822
- function durableOutputResponse(tool, workspaceId, record) {
823
- const result = durableOutputResult(record);
824
- const content = [textBlock(result)];
825
- const finishedAt = record.finishedAt ? Date.parse(record.finishedAt) : Date.now();
826
- const startedAt = Date.parse(record.startedAt);
827
- return {
828
- content,
829
- _meta: {
830
- tool,
831
- card: {
832
- workspaceId,
833
- summary: textSummary(record.output ? [textBlock(record.output)] : []),
834
- payload: { content },
835
- },
836
- },
837
- structuredContent: {
838
- result,
839
- processId: record.processId,
840
- sessionId: record.processId,
841
- outputId: record.outputId,
842
- running: record.status === "running",
843
- exitCode: record.exitCode,
844
- signal: record.signal,
845
- timedOut: record.timedOut,
846
- wallTimeMs: Math.max(0, Number.isFinite(finishedAt - startedAt) ? finishedAt - startedAt : 0),
847
- outputTruncated: false,
848
- },
849
- };
850
- }
851
- function markReturnedOutput(store, result) {
852
- if (typeof result !== "object" || result === null)
853
- return;
854
- const structured = result.structuredContent;
855
- if (typeof structured !== "object" || structured === null)
856
- return;
857
- const record = structured;
858
- if (record.running === true && typeof record.outputId === "string") {
859
- store.markReturned(record.outputId);
860
- }
861
- }
862
- function readWorkspaceBashOutput(store, workspaceId, outputId) {
863
- const record = store.read(outputId);
864
- if (!record)
865
- throw new Error(`Unknown Bash output: ${outputId}`);
866
- if (record.workspaceId !== workspaceId) {
867
- throw new Error(`Bash output ${outputId} does not belong to workspace ${workspaceId}.`);
868
- }
869
- return record;
870
- }
871
- function bashCompletionError(record) {
872
- if (record.error)
873
- return record.error;
874
- if (record.timedOut)
875
- return `Background process ${record.processId} timed out.`;
876
- if (record.signal)
877
- return `Background process ${record.processId} exited after signal ${record.signal}.`;
878
- return `Background process ${record.processId} exited with code ${record.exitCode ?? "unknown"}.`;
879
- }
880
- function recordBashCompletion(lifecycle, store, outputId) {
881
- if (!outputId)
882
- return;
883
- const completion = store.claimCompletion(outputId);
884
- if (!completion)
885
- return;
886
- lifecycle.recordLinked({
887
- sourceActivityId: completion.activityId,
888
- tool: "bash_result",
889
- request: {
890
- processId: completion.processId,
891
- outputId: completion.outputId,
892
- },
893
- result: {
894
- processId: completion.processId,
895
- outputId: completion.outputId,
896
- exitCode: completion.exitCode,
897
- signal: completion.signal,
898
- timedOut: completion.timedOut,
899
- },
900
- outcome: completion.status === "failed"
901
- ? { type: "failed", error: bashCompletionError(completion) }
902
- : { type: "succeeded" },
903
- });
904
- }
905
- function workspaceHookInvocation(workspace) {
906
- return {
907
- workspaceId: workspace.id,
908
- workspaceRoot: workspace.root,
909
- workspaceMode: workspace.mode,
910
- sourceRoot: workspace.sourceRoot,
911
- };
912
- }
913
- function capabilityContextFor(workspace) {
914
- return {
915
- workspaceId: workspace.id,
916
- workspaceKind: "workspace",
917
- workspaceRoot: workspace.root,
918
- guides: workspace.capabilityGuides.map((guide) => ({
919
- name: guide.name,
920
- description: guide.description,
921
- whenToRead: guide.whenToRead,
922
- path: formatPathForPrompt(guide.filePath),
923
- })),
924
- };
925
- }
926
- function compositeCapabilityContext(workspaceId, guides) {
927
- return {
928
- workspaceId,
929
- workspaceKind: "composite",
930
- guides: guides.map((guide) => ({
931
- name: guide.name,
932
- description: guide.description,
933
- whenToRead: guide.whenToRead,
934
- path: formatPathForPrompt(guide.filePath),
935
- })),
936
- };
937
- }
938
- function requireCapabilityWorkspaceRoot(context) {
939
- if (!context.workspaceRoot) {
940
- throw new CapabilityError("capability_unavailable", `Capability execution requires a filesystem-backed Workspace; ${context.workspaceId} is ${context.workspaceKind}.`);
941
- }
942
- return context.workspaceRoot;
943
- }
944
- function runWorkspaceTasksCapability(store, workspaceId, input) {
945
- switch (input.operation) {
946
- case "get":
947
- if (input.level === "headers")
948
- return store.readHeaders(workspaceId, input.listId);
949
- if (input.level === "detail")
950
- return store.readTaskDetail(workspaceId, input.listId, input.taskId);
951
- return store.readSummary(workspaceId);
952
- case "list.create":
953
- store.createList(workspaceId, { name: input.name, position: input.position });
954
- return store.readSummary(workspaceId);
955
- case "list.update":
956
- store.updateList(workspaceId, input.listId, {
957
- name: input.name,
958
- state: input.state,
959
- position: input.position,
960
- });
961
- return store.readSummary(workspaceId);
962
- case "list.delete":
963
- store.deleteList(workspaceId, input.listId);
964
- return store.readSummary(workspaceId);
965
- case "task.create":
966
- store.createTask(workspaceId, input.listId, {
967
- subject: input.subject,
968
- content: input.content,
969
- status: input.status,
970
- position: input.position,
971
- });
972
- return store.readHeaders(workspaceId, input.listId);
973
- case "task.update":
974
- store.updateTask(workspaceId, input.listId, input.taskId, {
975
- subject: input.subject,
976
- content: input.content,
977
- status: input.status,
978
- position: input.position,
979
- });
980
- return store.readHeaders(workspaceId, input.listId);
981
- case "task.delete":
982
- store.deleteTask(workspaceId, input.listId, input.taskId);
983
- return store.readHeaders(workspaceId, input.listId);
984
- }
985
- }
986
- async function reviewWorkspaceChanges(reviewCheckpoints, workspace) {
987
- return reviewCheckpoints.reviewChanges({
988
- workspaceId: workspace.id,
989
- root: workspace.root,
990
- markReviewed: true,
991
- });
992
- }
993
- function toolResultIsError(result) {
994
- return typeof result === "object" && result !== null && result.isError === true;
995
- }
996
- function toolResultText(result) {
997
- if (typeof result !== "object" || result === null)
998
- return String(result ?? "");
999
- const record = result;
1000
- if (Array.isArray(record.content)) {
1001
- const text = record.content
1002
- .map((entry) => {
1003
- if (typeof entry !== "object" || entry === null)
1004
- return "";
1005
- const value = entry.text;
1006
- return typeof value === "string" ? value : "";
1007
- })
1008
- .filter(Boolean)
1009
- .join("\n");
1010
- if (text)
1011
- return text;
1012
- }
1013
- if (typeof record.structuredContent === "object" && record.structuredContent !== null) {
1014
- const value = record.structuredContent.result;
1015
- if (typeof value === "string")
1016
- return value;
1017
- }
1018
- return "";
1019
- }
1020
- function toolResultContent(result) {
1021
- if (typeof result !== "object" || result === null)
1022
- return [];
1023
- const content = result.content;
1024
- return Array.isArray(content) ? content : [];
1025
- }
1026
- function remapCompositeToolResult(result, executionWorkspaceId, compositeWorkspaceId, member) {
1027
- if (typeof result !== "object" || result === null)
1028
- return result;
1029
- const record = result;
1030
- const remapped = replaceWorkspaceIdentity(record, executionWorkspaceId, compositeWorkspaceId);
1031
- const meta = typeof remapped._meta === "object" && remapped._meta !== null
1032
- ? { ...remapped._meta }
1033
- : undefined;
1034
- if (meta) {
1035
- const card = typeof meta.card === "object" && meta.card !== null
1036
- ? { ...meta.card, workspaceId: compositeWorkspaceId, member }
1037
- : undefined;
1038
- if (card)
1039
- meta.card = card;
1040
- remapped._meta = meta;
1041
- }
1042
- const structured = typeof remapped.structuredContent === "object" && remapped.structuredContent !== null
1043
- ? { ...remapped.structuredContent, member }
1044
- : undefined;
1045
- if (structured)
1046
- remapped.structuredContent = structured;
1047
- return remapped;
1048
- }
1049
- function replaceWorkspaceIdentity(value, from, to) {
1050
- if (typeof value === "string")
1051
- return value.split(from).join(to);
1052
- if (Array.isArray(value))
1053
- return value.map((entry) => replaceWorkspaceIdentity(entry, from, to));
1054
- if (!value || typeof value !== "object")
1055
- return value;
1056
- return Object.fromEntries(Object.entries(value)
1057
- .map(([key, entry]) => [key, replaceWorkspaceIdentity(entry, from, to)]));
1058
- }
1059
- function toolResultAgentsFiles(result) {
1060
- if (typeof result !== "object" || result === null)
1061
- return [];
1062
- const structured = result.structuredContent;
1063
- if (typeof structured !== "object" || structured === null)
1064
- return [];
1065
- const agentsFiles = structured.agentsFiles;
1066
- if (!Array.isArray(agentsFiles))
1067
- return [];
1068
- return agentsFiles.flatMap((entry) => {
1069
- if (typeof entry !== "object" || entry === null)
1070
- return [];
1071
- const path = entry.path;
1072
- const content = entry.content;
1073
- return typeof path === "string" && typeof content === "string" ? [{ path, content }] : [];
1074
- });
1075
- }
1076
- function workspaceActivitySnapshot(workspace) {
1077
- return {
1078
- id: workspace.id,
1079
- root: workspace.root,
1080
- mode: workspace.mode,
1081
- ...(workspace.sourceRoot ? { sourceRoot: workspace.sourceRoot } : {}),
1082
- ...(workspace.worktree?.branch ? { branch: workspace.worktree.branch } : {}),
1083
- ...(workspace.worktree?.targetBranch ? { targetBranch: workspace.worktree.targetBranch } : {}),
1084
- };
1085
- }
1086
- function activityFailureMessage(result) {
1087
- if (typeof result !== "object" || result === null)
1088
- return "Tool returned a failed result.";
1089
- const record = result;
1090
- if (Array.isArray(record.content)) {
1091
- const text = record.content
1092
- .map((entry) => {
1093
- if (typeof entry !== "object" || entry === null)
1094
- return "";
1095
- const value = entry.text;
1096
- return typeof value === "string" ? value : "";
1097
- })
1098
- .filter(Boolean)
1099
- .join("\n");
1100
- if (text)
1101
- return text;
1102
- }
1103
- if (typeof record.structuredContent === "object" && record.structuredContent !== null) {
1104
- const value = record.structuredContent.result;
1105
- if (typeof value === "string" && value)
1106
- return value;
1107
- }
1108
- return "Tool returned a failed result.";
1109
- }
1110
- function standardActivityOutcome(result) {
1111
- return toolResultIsError(result)
1112
- ? { type: "failed", error: activityFailureMessage(result) }
1113
- : { type: "succeeded" };
1114
- }
1115
- function processActivityOutcome(result) {
1116
- if (toolResultIsError(result))
1117
- return { type: "failed", error: activityFailureMessage(result) };
1118
- if (typeof result !== "object" || result === null)
1119
- return { type: "succeeded" };
1120
- const structured = result.structuredContent;
1121
- if (typeof structured !== "object" || structured === null)
1122
- return { type: "succeeded" };
1123
- const process = structured;
1124
- if (process.running === true)
1125
- return { type: "returned" };
1126
- if (process.timedOut === true ||
1127
- typeof process.signal === "string" ||
1128
- (typeof process.exitCode === "number" && process.exitCode !== 0)) {
1129
- return { type: "failed", error: activityFailureMessage(result) };
1130
- }
1131
- return { type: "succeeded" };
1132
- }
1133
- function activityRelationFor(context) {
1134
- return {
1135
- ...(context.parentActivityId ? { parentActivityId: context.parentActivityId } : {}),
1136
- ...(context.turnId ? { turnId: context.turnId } : {}),
1137
- };
1138
- }
1139
- function activityRequestFor(input, context) {
1140
- if (!context.activityMember || !input || typeof input !== "object" || Array.isArray(input))
1141
- return input;
1142
- return {
1143
- ...input,
1144
- member: context.activityMember,
1145
- };
1146
- }
1147
- function runActivityTool(lifecycle, workspace, conversationScopeId, tool, request, operation, outcome = standardActivityOutcome, relation = {}, auditResult) {
1148
- return lifecycle.run({
1149
- tool,
1150
- workspace: workspaceActivitySnapshot(workspace),
1151
- conversationScopeId,
1152
- request,
1153
- operation,
1154
- outcome,
1155
- ...(auditResult ? { auditResult } : {}),
1156
- ...relation,
1157
- });
1158
- }
1159
- function runActivityToolWithHooks(lifecycle, hooks, workspace, conversationScopeId, request, hookOptions, relation = {}) {
1160
- return runActivityTool(lifecycle, workspace, conversationScopeId, hookOptions.tool, request, () => runToolWithHooks(hooks, hookOptions), standardActivityOutcome, relation);
1161
- }
1162
- function registerProcessTools(server, config, workspaces, processSessions, hooks, activityLifecycle, bashOutputStore, shellRun, routing) {
1163
- if (config.toolMode === "codex") {
1164
- registerAppTool(server, "exec_command", {
1165
- title: "Execute command",
1166
- description: `Run a command inside an open workspace. Returns its result when it exits during the yield window, otherwise returns a processId for write_stdin. Use this for file inspection, tests, builds, package scripts, generators, formatters, and long-running processes. ${buildShellMutationPolicy()} Call open_workspace first and pass workspaceId.`,
1167
- inputSchema: {
1168
- workspaceId: z.string().describe("Workspace identifier returned by open_workspace."),
1169
- member: z.string().optional().describe("Required for a Composite Workspace; explicit member name that owns this process."),
1170
- cmd: z.string().min(1).describe("Shell command to execute."),
1171
- tty: z
1172
- .boolean()
1173
- .optional()
1174
- .describe("Allocate a pseudo-terminal for interactive commands. Defaults to false."),
1175
- columns: z.number().int().min(1).max(1_000).optional().describe("Initial PTY width. Defaults to 80."),
1176
- rows: z.number().int().min(1).max(1_000).optional().describe("Initial PTY height. Defaults to 24."),
1177
- workingDirectory: z
1178
- .string()
1179
- .optional()
1180
- .describe("Working directory relative to the workspace root. Defaults to the workspace root."),
1181
- yieldTimeMs: z
1182
- .number()
1183
- .int()
1184
- .min(0)
1185
- .max(300_000)
1186
- .optional()
1187
- .describe("Feedback window before returning a processId. Use 0 for immediate background handoff. Defaults to 10000ms."),
1188
- timeoutMs: z
1189
- .number()
1190
- .int()
1191
- .min(1)
1192
- .max(86_400_000)
1193
- .optional()
1194
- .describe("Total execution timeout from process start. On expiry ForgeRelay terminates the process. Omit for no ForgeRelay execution deadline."),
1195
- maxOutputTokens: z
1196
- .number()
1197
- .int()
1198
- .positive()
1199
- .max(100_000)
1200
- .optional()
1201
- .describe("Approximate output token budget. Defaults to 10000."),
1202
- },
1203
- outputSchema: processOutputSchema(),
1204
- ...toolWidgetDescriptorMeta(config, "shell"),
1205
- annotations: SHELL_TOOL_ANNOTATIONS,
1206
- }, async ({ workspaceId, member, cmd, tty, columns, rows, workingDirectory, yieldTimeMs, timeoutMs, maxOutputTokens }, extra) => {
1207
- const target = routing.resolve(workspaceId, member);
1208
- const context = await routing.prepare(target, extra._meta, extra.signal, extra.sessionId);
1209
- if (routing.isRemote(target.executionWorkspaceId)) {
1210
- return routing.presentSemantic(await routing.execCommandRemote(target.executionWorkspaceId, {
1211
- cmd,
1212
- ...(tty !== undefined ? { tty } : {}),
1213
- ...(columns !== undefined ? { columns } : {}),
1214
- ...(rows !== undefined ? { rows } : {}),
1215
- ...(workingDirectory !== undefined ? { workingDirectory } : {}),
1216
- ...(yieldTimeMs !== undefined ? { yieldTimeMs } : {}),
1217
- ...(timeoutMs !== undefined ? { timeoutMs } : {}),
1218
- ...(maxOutputTokens !== undefined ? { maxOutputTokens } : {}),
1219
- }, routing.hostScopeIdFor(extra._meta, extra.sessionId)), target);
1220
- }
1221
- return routing.presentSemantic(await shellRun({
1222
- workspaceId: target.executionWorkspaceId,
1223
- command: cmd,
1224
- surface: "exec_command",
1225
- tty,
1226
- columns,
1227
- rows,
1228
- workingDirectory,
1229
- yieldTimeMs,
1230
- timeoutMs,
1231
- maxOutputTokens,
1232
- }, context), target);
1233
- });
1234
- }
1235
- if (config.toolMode !== "codex")
1236
- return;
1237
- registerAppTool(server, "write_stdin", {
1238
- title: "Write to process",
1239
- description: "Poll or write characters to a running process returned by exec_command, or retrieve complete durable process output by outputId. Omit chars or pass an empty string to poll. Waiting never kills the process; pass \\u0003 to explicitly send Ctrl-C.",
1240
- inputSchema: {
1241
- workspaceId: z.string().describe("Workspace identifier used to start the process."),
1242
- member: z.string().optional().describe("Required for a Composite Workspace; explicit member name that owns the process."),
1243
- processId: z.number().int().positive().optional().describe("Canonical process identifier returned by bash or exec_command."),
1244
- sessionId: z.number().int().positive().optional().describe("Deprecated alias for processId. Retained for compatibility."),
1245
- outputId: z.string().optional().describe("Stable output identifier returned by exec_command. When supplied, retrieve the complete durable output instead of controlling a process."),
1246
- chars: z.string().optional().describe("Characters to write. Omit or pass an empty string to poll."),
1247
- columns: z.number().int().min(1).max(1_000).optional().describe("Resize a PTY to this width."),
1248
- rows: z.number().int().min(1).max(1_000).optional().describe("Resize a PTY to this height."),
1249
- yieldTimeMs: z
1250
- .number()
1251
- .int()
1252
- .min(0)
1253
- .max(300_000)
1254
- .optional()
1255
- .describe("Milliseconds to keep waiting before returning again, max 300000. Polling defaults to 5000; interaction defaults to 250."),
1256
- maxOutputTokens: z
1257
- .number()
1258
- .int()
1259
- .positive()
1260
- .max(100_000)
1261
- .optional()
1262
- .describe("Approximate output token budget. Defaults to 10000."),
1263
- },
1264
- outputSchema: processOutputSchema(),
1265
- ...toolWidgetDescriptorMeta(config, "shell"),
1266
- annotations: SHELL_TOOL_ANNOTATIONS,
1267
- }, async ({ workspaceId, member, processId, sessionId, outputId, chars, columns, rows, yieldTimeMs, maxOutputTokens }, extra) => {
1268
- const target = routing.resolve(workspaceId, member);
1269
- await routing.prepare(target, extra._meta, extra.signal, extra.sessionId);
1270
- if (routing.isRemote(target.executionWorkspaceId)) {
1271
- return routing.present(await routing.writeStdinRemote(target.executionWorkspaceId, {
1272
- ...(processId !== undefined ? { processId } : {}),
1273
- ...(sessionId !== undefined ? { sessionId } : {}),
1274
- ...(outputId !== undefined ? { outputId } : {}),
1275
- ...(chars !== undefined ? { chars } : {}),
1276
- ...(columns !== undefined ? { columns } : {}),
1277
- ...(rows !== undefined ? { rows } : {}),
1278
- ...(yieldTimeMs !== undefined ? { yieldTimeMs } : {}),
1279
- ...(maxOutputTokens !== undefined ? { maxOutputTokens } : {}),
1280
- }, routing.hostScopeIdFor(extra._meta, extra.sessionId)), target);
1281
- }
1282
- const executionWorkspaceId = target.executionWorkspaceId;
1283
- const workspace = workspaces.getWorkspace(executionWorkspaceId);
1284
- if (outputId !== undefined) {
1285
- if (processId !== undefined || sessionId !== undefined || chars !== undefined || columns !== undefined ||
1286
- rows !== undefined || yieldTimeMs !== undefined || maxOutputTokens !== undefined) {
1287
- throw new Error("write_stdin outputId lookup cannot be combined with process control fields.");
1288
- }
1289
- return runToolWithHooks(hooks, {
1290
- signal: extra.signal,
1291
- tool: "write_stdin",
1292
- invocation: workspaceHookInvocation(workspace),
1293
- payload: { outputId },
1294
- operation: async () => durableOutputResponse("write_stdin", executionWorkspaceId, readWorkspaceBashOutput(bashOutputStore, executionWorkspaceId, outputId)),
1295
- });
1296
- }
1297
- const resolvedProcessId = resolveProcessId(processId, sessionId);
1298
- return runToolWithHooks(hooks, {
1299
- signal: extra.signal,
1300
- tool: "write_stdin",
1301
- invocation: workspaceHookInvocation(workspace),
1302
- payload: {
1303
- processId: resolvedProcessId,
1304
- charactersWritten: chars?.length ?? 0,
1305
- columns,
1306
- rows,
1307
- },
1308
- operation: async () => {
1309
- const startedAt = performance.now();
1310
- const snapshot = await processSessions.write({
1311
- workspaceId: executionWorkspaceId,
1312
- processId: resolvedProcessId,
1313
- chars,
1314
- columns,
1315
- rows,
1316
- yieldTimeMs,
1317
- maxOutputTokens,
1318
- signal: extra.signal,
1319
- });
1320
- logToolCall(config, {
1321
- tool: "write_stdin",
1322
- ...workspaceLogContext(workspace, extra.sessionId),
1323
- exitCode: snapshot.exitCode,
1324
- running: snapshot.running,
1325
- processId: snapshot.processId,
1326
- success: snapshot.running || snapshot.exitCode === 0,
1327
- durationMs: Math.round(performance.now() - startedAt),
1328
- });
1329
- const response = processToolResponse("write_stdin", executionWorkspaceId, snapshot, {
1330
- processId: resolvedProcessId,
1331
- charactersWritten: chars?.length ?? 0,
1332
- running: snapshot.running,
1333
- exitCode: snapshot.exitCode,
1334
- wallTimeMs: snapshot.wallTimeMs,
1335
- });
1336
- if (!snapshot.running) {
1337
- recordBashCompletion(activityLifecycle, bashOutputStore, snapshot.outputId);
1338
- }
1339
- return response;
1340
- },
1341
- }).then((result) => routing.present(result, target));
1342
- });
1343
- }
33
+ import { FORGERELAY_VERSION } from "./mcp/server/core/version.js";
34
+ import { capabilityContextFor, requireCapabilityWorkspaceRoot, reviewWorkspaceChanges, runWorkspaceTasksCapability } from "./mcp/server/core/capability-support.js";
35
+ import { createHttpServer } from "./mcp/server/transport/http-server.js";
36
+ import { registerFilesystemTools } from "./mcp/server/operations/runtime/filesystem-tools.js";
37
+ import { createOperationRuntime } from "./mcp/server/operations/runtime/operation-runtime.js";
38
+ import { registerWorkspaceAuxiliaryTools } from "./mcp/server/workspace/runtime/workspace-tools.js";
39
+ import { registerOpenWorkspaceTool } from "./mcp/server/workspace/runtime/workspace-open.js";
40
+ import { redactSkillDiagnosticPaths } from "./mcp/server/core/schemas.js";
41
+ import { attachWorkspaceTaskReminder, remapCompositeToolResult, toolResultIsError } from "./mcp/server/core/tool-support.js";
1344
42
  export function createMcpServer(config, workspaces, reviewCheckpoints, processSessions, subagentProviders, incomingArtifactAdapters, codeIntelligence, activityLifecycle, bashOutputStore, activityQueries, options = {}) {
1345
43
  const connectionScopeId = `mcp-connection:${randomUUID()}`;
44
+ const activityPanelApp = createActivityPanelApp(config, FORGERELAY_VERSION);
1346
45
  const ownsRemoteWorkspaces = options.remoteWorkspaces === undefined;
1347
46
  const remoteWorkspaces = options.remoteWorkspaces
1348
47
  ?? new RemoteWorkspaceRelay(config.configDir, config.stateDir);
@@ -1591,511 +290,11 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1591
290
  : `Composite member ${memberName} context was already delivered for this Host context; keep using Composite workspaceId ${compositeWorkspaceId} with member=${memberName}.`,
1592
291
  };
1593
292
  };
1594
- const coreOperations = createCoreOperationExecutor({
1595
- read: async (input, context) => {
1596
- const { workspaceId, ...readInput } = input;
1597
- const workspace = workspaces.getWorkspace(workspaceId);
1598
- return runActivityToolWithHooks(activityLifecycle, hooks, workspace, hostScopeIdFor(context.requestMeta, context.sessionId), activityRequestFor(input, context), {
1599
- signal: context.signal,
1600
- tool: toolNames.read,
1601
- invocation: workspaceHookInvocation(workspace),
1602
- payload: { path: readInput.path, offset: readInput.offset, limit: readInput.limit },
1603
- isFailure: toolResultIsError,
1604
- operation: async () => {
1605
- const startedAt = performance.now();
1606
- const readPath = workspaces.resolveReadPath(workspace, readInput.path);
1607
- const discoveredInstructions = (await workspaces.discoverPathInstructions(workspace, readPath.absolutePath)).filter((file) => file.path !== readPath.absolutePath);
1608
- const response = await readFileTool({ ...readInput, path: readPath.absolutePath }, {
1609
- cwd: workspace.root,
1610
- root: workspace.root,
1611
- readRoots: readPath.readRoots,
1612
- });
1613
- if (response.isError) {
1614
- logFailedToolResponse(config, {
1615
- tool: toolNames.read,
1616
- ...workspaceLogContext(workspace, context.sessionId),
1617
- path: readInput.path,
1618
- }, response.content, startedAt);
1619
- return response;
1620
- }
1621
- workspaces.markReadPathLoaded(workspace, readPath);
1622
- const discoveredInstructionContent = discoveredInstructions.length > 0
1623
- ? textBlock(formatDiscoveredWorkspaceInstructions(discoveredInstructions, workspace.root))
1624
- : undefined;
1625
- const content = discoveredInstructionContent
1626
- ? [discoveredInstructionContent, ...response.content]
1627
- : response.content;
1628
- const summary = {
1629
- ...textSummary(response.content),
1630
- offset: readInput.offset ?? 1,
1631
- limited: readInput.limit !== undefined,
1632
- };
1633
- logToolCall(config, {
1634
- tool: toolNames.read,
1635
- ...workspaceLogContext(workspace, context.sessionId),
1636
- path: readInput.path,
1637
- success: true,
1638
- durationMs: Math.round(performance.now() - startedAt),
1639
- });
1640
- return {
1641
- ...response,
1642
- content,
1643
- _meta: {
1644
- tool: toolNames.read,
1645
- card: {
1646
- workspaceId,
1647
- path: readInput.path,
1648
- summary,
1649
- payload: { content: response.content },
1650
- },
1651
- },
1652
- structuredContent: {
1653
- result: contentText(content),
1654
- ...(discoveredInstructions.length > 0
1655
- ? {
1656
- agentsFiles: discoveredInstructions.map((file) => ({
1657
- path: formatAgentsPath(file.path, workspace.root),
1658
- content: file.content,
1659
- })),
1660
- }
1661
- : {}),
1662
- },
1663
- };
1664
- },
1665
- }, activityRelationFor(context));
1666
- },
1667
- write: async (input, context) => {
1668
- const { workspaceId, ...writeInput } = input;
1669
- const workspace = workspaces.getWorkspace(workspaceId);
1670
- return runActivityToolWithHooks(activityLifecycle, hooks, workspace, hostScopeIdFor(context.requestMeta, context.sessionId), activityRequestFor(input, context), {
1671
- signal: context.signal,
1672
- tool: toolNames.write,
1673
- invocation: workspaceHookInvocation(workspace),
1674
- payload: { path: writeInput.path },
1675
- isFailure: toolResultIsError,
1676
- changedPaths: (result) => toolResultIsError(result) ? [] : [writeInput.path],
1677
- operation: async () => {
1678
- const startedAt = performance.now();
1679
- await assertWorkspaceInstructionsLoadedBeforeSideEffect(workspaces, workspace, [writeInput.path]);
1680
- const response = await writeFileTool(writeInput, {
1681
- cwd: workspace.root,
1682
- root: workspace.root,
1683
- fileRoots: workspaces.fileToolRoots(workspace),
1684
- });
1685
- if (response.isError) {
1686
- logFailedToolResponse(config, {
1687
- tool: toolNames.write,
1688
- ...workspaceLogContext(workspace, context.sessionId),
1689
- path: writeInput.path,
1690
- }, response.content, startedAt);
1691
- return response;
1692
- }
1693
- const patch = newFilePatch(writeInput.path, writeInput.content);
1694
- const stats = countDiffStats(patch);
1695
- const summary = {
1696
- ...stats,
1697
- lines: contentLineCount(writeInput.content),
1698
- characters: writeInput.content.length,
1699
- };
1700
- logToolCall(config, {
1701
- tool: toolNames.write,
1702
- ...workspaceLogContext(workspace, context.sessionId),
1703
- path: writeInput.path,
1704
- success: true,
1705
- durationMs: Math.round(performance.now() - startedAt),
1706
- });
1707
- return {
1708
- ...response,
1709
- _meta: {
1710
- tool: toolNames.write,
1711
- card: {
1712
- workspaceId,
1713
- path: writeInput.path,
1714
- summary,
1715
- payload: {
1716
- content: response.content,
1717
- patch,
1718
- },
1719
- },
1720
- },
1721
- structuredContent: {
1722
- result: contentText(response.content),
1723
- },
1724
- };
1725
- },
1726
- }, activityRelationFor(context));
1727
- },
1728
- edit: async (input, context) => {
1729
- const { workspaceId, ...editInput } = input;
1730
- const workspace = workspaces.getWorkspace(workspaceId);
1731
- return runActivityToolWithHooks(activityLifecycle, hooks, workspace, hostScopeIdFor(context.requestMeta, context.sessionId), activityRequestFor(input, context), {
1732
- signal: context.signal,
1733
- tool: toolNames.edit,
1734
- invocation: workspaceHookInvocation(workspace),
1735
- payload: { path: editInput.path, editCount: editInput.edits.length },
1736
- isFailure: toolResultIsError,
1737
- changedPaths: (result) => toolResultIsError(result) ? [] : [editInput.path],
1738
- operation: async () => {
1739
- const startedAt = performance.now();
1740
- await assertWorkspaceInstructionsLoadedBeforeSideEffect(workspaces, workspace, [editInput.path]);
1741
- const response = await editFileTool(editInput, {
1742
- cwd: workspace.root,
1743
- root: workspace.root,
1744
- fileRoots: workspaces.fileToolRoots(workspace),
1745
- });
1746
- if (response.isError) {
1747
- logFailedToolResponse(config, {
1748
- tool: toolNames.edit,
1749
- ...workspaceLogContext(workspace, context.sessionId),
1750
- path: editInput.path,
1751
- }, response.content, startedAt);
1752
- return response;
1753
- }
1754
- const stats = countDiffStats(response.details?.patch ?? response.details?.diff);
1755
- const summary = {
1756
- ...stats,
1757
- editCount: editInput.edits.length,
1758
- };
1759
- const editResultText = `Edited ${editInput.path} (+${stats.additions} -${stats.removals}).`;
1760
- const editContent = [textBlock(editResultText)];
1761
- logToolCall(config, {
1762
- tool: toolNames.edit,
1763
- ...workspaceLogContext(workspace, context.sessionId),
1764
- path: editInput.path,
1765
- success: true,
1766
- durationMs: Math.round(performance.now() - startedAt),
1767
- });
1768
- return {
1769
- content: editContent,
1770
- _meta: {
1771
- tool: toolNames.edit,
1772
- card: {
1773
- workspaceId,
1774
- path: editInput.path,
1775
- summary,
1776
- payload: {
1777
- diff: response.details?.diff,
1778
- patch: response.details?.patch,
1779
- },
1780
- },
1781
- },
1782
- structuredContent: {
1783
- status: "applied",
1784
- result: contentText(editContent),
1785
- },
1786
- };
1787
- },
1788
- }, activityRelationFor(context));
1789
- },
1790
- rename: async (input, context) => {
1791
- const { workspaceId, path, newPath } = input;
1792
- const workspace = workspaces.getWorkspace(workspaceId);
1793
- return runActivityToolWithHooks(activityLifecycle, hooks, workspace, hostScopeIdFor(context.requestMeta, context.sessionId), activityRequestFor(input, context), {
1794
- signal: context.signal,
1795
- tool: toolNames.rename,
1796
- invocation: workspaceHookInvocation(workspace),
1797
- payload: { path, newPath, paths: [path, newPath] },
1798
- changedPaths: () => [path, newPath],
1799
- operation: async () => {
1800
- const startedAt = performance.now();
1801
- try {
1802
- await assertWorkspaceInstructionsLoadedBeforeSideEffect(workspaces, workspace, [path, newPath]);
1803
- await renamePath({ path, newPath }, {
1804
- cwd: workspace.root,
1805
- allowedRoots: workspaces.fileToolRoots(workspace),
1806
- });
1807
- const result = `Renamed ${path} to ${newPath}.`;
1808
- const content = [textBlock(result)];
1809
- logToolCall(config, {
1810
- tool: toolNames.rename,
1811
- ...workspaceLogContext(workspace, context.sessionId),
1812
- path: `${path} -> ${newPath}`,
1813
- success: true,
1814
- durationMs: Math.round(performance.now() - startedAt),
1815
- });
1816
- return {
1817
- content,
1818
- _meta: {
1819
- tool: toolNames.rename,
1820
- card: {
1821
- workspaceId,
1822
- path: newPath,
1823
- summary: { previousPath: path },
1824
- payload: { content },
1825
- },
1826
- },
1827
- structuredContent: {
1828
- result,
1829
- status: "renamed",
1830
- path,
1831
- newPath,
1832
- },
1833
- };
1834
- }
1835
- catch (error) {
1836
- logToolCall(config, {
1837
- tool: toolNames.rename,
1838
- ...workspaceLogContext(workspace, context.sessionId),
1839
- path: `${path} -> ${newPath}`,
1840
- success: false,
1841
- durationMs: Math.round(performance.now() - startedAt),
1842
- error: error instanceof Error ? error.message : String(error),
1843
- });
1844
- throw error;
1845
- }
1846
- },
1847
- }, activityRelationFor(context));
1848
- },
1849
- delete: async (input, context) => {
1850
- const { workspaceId, path, recursive } = input;
1851
- const workspace = workspaces.getWorkspace(workspaceId);
1852
- return runActivityToolWithHooks(activityLifecycle, hooks, workspace, hostScopeIdFor(context.requestMeta, context.sessionId), activityRequestFor(input, context), {
1853
- signal: context.signal,
1854
- tool: toolNames.delete,
1855
- invocation: workspaceHookInvocation(workspace),
1856
- payload: { path, recursive: recursive ?? false },
1857
- changedPaths: () => [path],
1858
- operation: async () => {
1859
- const startedAt = performance.now();
1860
- try {
1861
- await assertWorkspaceInstructionsLoadedBeforeSideEffect(workspaces, workspace, [path]);
1862
- const deleted = await deletePath({ path, recursive }, {
1863
- cwd: workspace.root,
1864
- allowedRoots: workspaces.fileToolRoots(workspace),
1865
- });
1866
- const result = `Deleted ${path}${deleted.recursive ? " recursively" : ""}.`;
1867
- const content = [textBlock(result)];
1868
- logToolCall(config, {
1869
- tool: toolNames.delete,
1870
- ...workspaceLogContext(workspace, context.sessionId),
1871
- path,
1872
- success: true,
1873
- durationMs: Math.round(performance.now() - startedAt),
1874
- });
1875
- return {
1876
- content,
1877
- _meta: {
1878
- tool: toolNames.delete,
1879
- card: {
1880
- workspaceId,
1881
- path,
1882
- summary: { recursive: deleted.recursive },
1883
- payload: { content },
1884
- },
1885
- },
1886
- structuredContent: {
1887
- result,
1888
- status: "deleted",
1889
- path,
1890
- recursive: deleted.recursive,
1891
- },
1892
- };
1893
- }
1894
- catch (error) {
1895
- logToolCall(config, {
1896
- tool: toolNames.delete,
1897
- ...workspaceLogContext(workspace, context.sessionId),
1898
- path,
1899
- success: false,
1900
- durationMs: Math.round(performance.now() - startedAt),
1901
- error: error instanceof Error ? error.message : String(error),
1902
- });
1903
- throw error;
1904
- }
1905
- },
1906
- }, activityRelationFor(context));
1907
- },
1908
- shellRun: async (input, context) => {
1909
- const { workspaceId, command, surface, tty, columns, rows, workingDirectory, yieldTimeMs, timeoutMs, maxOutputTokens, } = input;
1910
- const workspace = workspaces.getWorkspace(workspaceId);
1911
- const activityRequest = surface === "exec_command"
1912
- ? {
1913
- workspaceId,
1914
- cmd: command,
1915
- tty,
1916
- columns,
1917
- rows,
1918
- workingDirectory,
1919
- yieldTimeMs,
1920
- timeoutMs,
1921
- maxOutputTokens,
1922
- }
1923
- : {
1924
- workspaceId,
1925
- action: "run",
1926
- command,
1927
- tty,
1928
- columns,
1929
- rows,
1930
- workingDirectory,
1931
- yieldTimeMs,
1932
- timeoutMs,
1933
- maxOutputTokens,
1934
- };
1935
- let undeliveredProcessId;
1936
- const activityResult = await runActivityTool(activityLifecycle, workspace, hostScopeIdFor(context.requestMeta, context.sessionId), surface, activityRequestFor(activityRequest, context), async (activityContext) => {
1937
- try {
1938
- const result = await runToolWithHooks(hooks, {
1939
- signal: context.signal,
1940
- tool: surface,
1941
- invocation: workspaceHookInvocation(workspace),
1942
- payload: surface === "exec_command"
1943
- ? { command, workingDirectory: workingDirectory ?? "." }
1944
- : { action: "run", command, workingDirectory: workingDirectory ?? "." },
1945
- ...(surface === "bash" ? { isFailure: toolResultIsError } : {}),
1946
- operation: async () => {
1947
- const startedAt = performance.now();
1948
- const cwd = workspaces.resolveWorkingDirectory(workspace, workingDirectory);
1949
- await assertWorkspaceInstructionsLoadedBeforeSideEffect(workspaces, workspace, [cwd]);
1950
- const snapshot = await processSessions.start({
1951
- workspaceId,
1952
- command,
1953
- cwd,
1954
- workspaceRoot: workspace.root,
1955
- tty,
1956
- columns,
1957
- rows,
1958
- yieldTimeMs,
1959
- timeoutMs,
1960
- maxOutputTokens,
1961
- ...(surface === "exec_command" ? { codexCi: true } : {}),
1962
- signal: context.signal,
1963
- audit: activityContext,
1964
- });
1965
- undeliveredProcessId = snapshot.running ? snapshot.processId : undefined;
1966
- logToolCall(config, {
1967
- tool: surface,
1968
- ...workspaceLogContext(workspace, context.sessionId),
1969
- workingDirectory: workingDirectory ?? ".",
1970
- command,
1971
- commandLength: command.length,
1972
- exitCode: snapshot.exitCode,
1973
- running: snapshot.running,
1974
- processId: snapshot.processId,
1975
- success: surface === "exec_command"
1976
- ? snapshot.running || snapshot.exitCode === 0
1977
- : snapshot.running || (snapshot.exitCode === 0 && !snapshot.signal),
1978
- durationMs: Math.round(performance.now() - startedAt),
1979
- });
1980
- const response = processToolResponse(surface, workspaceId, snapshot, {
1981
- ...(surface === "bash" ? { action: "run" } : {}),
1982
- command,
1983
- workingDirectory: workingDirectory ?? ".",
1984
- running: snapshot.running,
1985
- exitCode: snapshot.exitCode,
1986
- wallTimeMs: snapshot.wallTimeMs,
1987
- });
1988
- return surface === "bash" && !snapshot.running && (snapshot.signal || snapshot.exitCode !== 0)
1989
- ? { ...response, isError: true }
1990
- : response;
1991
- },
1992
- });
1993
- context.signal?.throwIfAborted();
1994
- return result;
1995
- }
1996
- catch (error) {
1997
- if (undeliveredProcessId !== undefined) {
1998
- processSessions.discardUndelivered(workspaceId, undeliveredProcessId);
1999
- }
2000
- throw error;
2001
- }
2002
- }, processActivityOutcome, activityRelationFor(context));
2003
- markReturnedOutput(bashOutputStore, activityResult);
2004
- return activityResult;
2005
- },
2006
- capabilityRun: async (input, context) => {
2007
- const { workspaceId, name, arguments: capabilityArguments, file } = input;
2008
- const workspace = workspaces.getWorkspace(workspaceId);
2009
- let changedPaths = [];
2010
- return runActivityTool(activityLifecycle, workspace, hostScopeIdFor(context.requestMeta, context.sessionId), toolNames.capability, activityRequestFor(capabilityActivityAuditRequest(input), context), (activityContext) => runToolWithHooks(hooks, {
2011
- signal: context.signal,
2012
- tool: toolNames.capability,
2013
- invocation: workspaceHookInvocation(workspace),
2014
- payload: { name, action: "run" },
2015
- isFailure: toolResultIsError,
2016
- changedPaths: () => changedPaths,
2017
- operation: async () => {
2018
- const startedAt = performance.now();
2019
- try {
2020
- const execution = await capabilityRegistry.run(name, capabilityArguments ?? {}, capabilityContextFor(workspace), {
2021
- nativeFile: file,
2022
- signal: context.signal,
2023
- requestMeta: context.requestMeta,
2024
- sessionId: context.sessionId,
2025
- batch: context.batch,
2026
- activityId: activityContext.activityId,
2027
- });
2028
- changedPaths = execution.changedPaths ?? [];
2029
- const result = {
2030
- content: [textBlock(`Capability ${name} completed.\n${JSON.stringify(execution.value, null, 2)}`)],
2031
- ...(execution.card
2032
- ? {
2033
- _meta: {
2034
- tool: toolNames.capability,
2035
- card: {
2036
- workspaceId,
2037
- capabilityName: name,
2038
- summary: execution.card.summary ?? {},
2039
- files: execution.card.files,
2040
- payload: execution.card.payload ?? {},
2041
- },
2042
- },
2043
- }
2044
- : {}),
2045
- structuredContent: { name, action: "run", result: execution.value },
2046
- };
2047
- logToolCall(config, {
2048
- tool: toolNames.capability,
2049
- ...workspaceLogContext(workspace, context.sessionId),
2050
- capability: name,
2051
- action: "run",
2052
- success: true,
2053
- durationMs: Math.round(performance.now() - startedAt),
2054
- });
2055
- return result;
2056
- }
2057
- catch (error) {
2058
- const capabilityError = error instanceof CapabilityError
2059
- ? error
2060
- : new CapabilityError("execution_failed", error instanceof Error ? error.message : String(error));
2061
- const result = {
2062
- content: [textBlock(`${capabilityError.code}: ${capabilityError.message}`)],
2063
- structuredContent: {
2064
- name,
2065
- action: "run",
2066
- error: { code: capabilityError.code, message: capabilityError.message },
2067
- },
2068
- isError: true,
2069
- };
2070
- logFailedToolResponse(config, {
2071
- tool: toolNames.capability,
2072
- ...workspaceLogContext(workspace, context.sessionId),
2073
- capability: name,
2074
- action: "run",
2075
- }, result.content, startedAt);
2076
- return result;
2077
- }
2078
- },
2079
- }), standardActivityOutcome, activityRelationFor(context), (result) => capabilityActivityAuditResult(name, result));
2080
- },
2081
- });
2082
- batchExecutor = new BatchExecutor({
2083
- lifecycle: activityLifecycle,
2084
- workspaces,
2085
- coreOperations,
2086
- resultIsError: toolResultIsError,
2087
- capabilityBatchPolicy: (name) => capabilityRegistry.batchPolicy(name),
2088
- shellSurface: "bash",
2089
- });
2090
- const nativeBulkMutations = new NativeBulkMutationExecutor({
2091
- lifecycle: activityLifecycle,
2092
- workspaces,
2093
- coreOperations,
2094
- preflightInstructions: (workspace, paths) => assertWorkspaceInstructionsLoadedBeforeSideEffect(workspaces, workspace, paths),
2095
- resultIsError: toolResultIsError,
2096
- resultText: toolResultText,
2097
- resultContent: toolResultContent,
293
+ const operationRuntime = createOperationRuntime({
294
+ config, workspaces, activityLifecycle, hooks, processSessions, bashOutputStore, capabilityRegistry, hostScopeIdFor,
2098
295
  });
296
+ batchExecutor = operationRuntime.batchExecutor;
297
+ const { coreOperations, nativeBulkMutations } = operationRuntime;
2099
298
  const server = new McpServer({
2100
299
  name: "forgerelay",
2101
300
  title: "ForgeRelay",
@@ -2173,982 +372,38 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
2173
372
  ...compact,
2174
373
  });
2175
374
  };
2176
- const workspaceAppResourceMetadata = {
2177
- description: "Historical ForgeRelay tool card UI.",
2178
- _meta: {
2179
- ui: {
2180
- domain: appDomain(config),
2181
- csp: appCsp(config),
2182
- },
375
+ registerAppResource(server, "ForgeRelay Activity Panel", activityPanelApp.uri, activityPanelApp.resourceMetadata, async (uri, extra) => activityPanelApp.readResource(uri.toString(), extra.sessionId));
376
+ registerOpenWorkspaceTool({
377
+ server, config, workspaces, remoteWorkspaces, compositeWorkspaces, workspaceTasks, processSessions,
378
+ capabilityRegistry, compositeTaskGuides, loadCompositeMemberContext, rememberWorkspacePanelState, hostScopeIdFor,
379
+ presentation: {
380
+ config, forgerelayVersion: FORGERELAY_VERSION, workspaces, workspaceTasks, reviewCheckpoints,
381
+ capabilityRegistry, subagentProviders, hooks, rememberWorkspacePanelState,
2183
382
  },
2184
- };
2185
- const workspaceLifecycleResourceMetadata = {
2186
- description: "Historical ForgeRelay Workspace lifecycle UI compatibility resource.",
2187
- _meta: workspaceAppResourceMetadata._meta,
2188
- };
2189
- const activityPanelResourceMetadata = {
2190
- description: "ForgeRelay unified Workspace and Activity UI for one Host Turn.",
2191
- _meta: workspaceAppResourceMetadata._meta,
2192
- };
2193
- const currentWorkspaceAppUri = currentWorkspaceAppIdentity(config).uri;
2194
- registerAppResource(server, "ForgeRelay historical tool card", currentWorkspaceAppUri, workspaceAppResourceMetadata, async (uri, extra) => readWorkspaceAppResource(config, uri.toString(), extra.sessionId));
2195
- registerAppResource(server, "ForgeRelay historical tool card legacy", WORKSPACE_APP_LEGACY_URI, workspaceAppResourceMetadata, async (uri, extra) => readWorkspaceAppResource(config, uri.toString(), extra.sessionId));
2196
- server.registerResource("ForgeRelay historical tool card compatibility", new ResourceTemplate(WORKSPACE_APP_URI_TEMPLATE, { list: undefined }), { ...workspaceAppResourceMetadata, mimeType: RESOURCE_MIME_TYPE }, async (uri, _variables, extra) => readWorkspaceAppResource(config, uri.toString(), extra.sessionId));
2197
- server.registerResource("ForgeRelay historical Workspace Lifecycle compatibility", new ResourceTemplate(WORKSPACE_LIFECYCLE_APP_URI_TEMPLATE, { list: undefined }), { ...workspaceLifecycleResourceMetadata, mimeType: RESOURCE_MIME_TYPE }, async (uri, _variables, extra) => readWorkspaceLifecycleAppResource(config, uri.toString(), extra.sessionId));
2198
- const currentActivityPanelAppUri = currentActivityPanelAppIdentity(config).uri;
2199
- registerAppResource(server, "ForgeRelay Activity Panel", currentActivityPanelAppUri, activityPanelResourceMetadata, async (uri, extra) => readActivityPanelAppResource(config, uri.toString(), extra.sessionId));
2200
- registerAppResource(server, "ForgeRelay Activity Panel legacy", ACTIVITY_PANEL_APP_LEGACY_URI, activityPanelResourceMetadata, async (uri, extra) => readActivityPanelAppResource(config, uri.toString(), extra.sessionId));
2201
- server.registerResource("ForgeRelay Activity Panel compatibility", new ResourceTemplate(ACTIVITY_PANEL_APP_URI_TEMPLATE, { list: undefined }), { ...activityPanelResourceMetadata, mimeType: RESOURCE_MIME_TYPE }, async (uri, _variables, extra) => readActivityPanelAppResource(config, uri.toString(), extra.sessionId));
2202
- registerAppTool(server, "open_workspace", {
2203
- title: "Open workspace",
2204
- description: "Open or resume a ForgeRelay Workspace. Ordinary workspaces default to local execution; relay may name a registered remote ForgeRelay. Composite Workspaces use the same open lifecycle but have kind=\"composite\" and a name instead of a mounted root. Reuse the returned workspaceId for later calls. Bootstrap context is delivered automatically only when needed and can be suppressed or refreshed.",
2205
- inputSchema: {
2206
- action: z
2207
- .enum(["open", "list", "inspect", "member"])
2208
- .optional()
2209
- .describe("Defaults to open. Use list for lightweight inventory, inspect for bounded read-only metadata about one known Workspace without opening/resuming it, or member to change Composite membership."),
2210
- memberAction: z
2211
- .enum(["add", "update", "remove"])
2212
- .optional()
2213
- .describe("Required with action=member."),
2214
- member: z.object({
2215
- name: z.string().describe("Stable member name such as code or compute. For update/remove this identifies the existing member."),
2216
- newName: z.string().optional().describe("Optional replacement member name for memberAction=update."),
2217
- purpose: z.string().optional().describe("Agent-facing purpose. Required when adding a member; optional replacement when updating."),
2218
- workspaceId: z.string().optional().describe("Existing ordinary or relayed Workspace to mount. Mutually exclusive with path."),
2219
- path: z.string().optional().describe("Workspace path to open internally and mount. Mutually exclusive with workspaceId."),
2220
- relay: z.string().optional().describe("Optional registered remote ForgeRelay alias for a path-backed member."),
2221
- mode: z.enum(["checkout", "worktree"]).optional(),
2222
- baseRef: z.string().optional(),
2223
- newWorktree: z.boolean().optional(),
2224
- newWorkspace: z.boolean().optional(),
2225
- }).optional().describe("Composite member definition used by action=member."),
2226
- kind: z
2227
- .enum(["workspace", "composite"])
2228
- .optional()
2229
- .describe("Workspace kind for action=open. Defaults to workspace. Use composite with name and no path to create or reopen a named Composite Workspace."),
2230
- name: z
2231
- .string()
2232
- .optional()
2233
- .describe("Composite Workspace name. Used only with kind=\"composite\" when creating/opening by name."),
2234
- memberName: z
2235
- .string()
2236
- .optional()
2237
- .describe("For action=open on a Composite Workspace, load bootstrap context for this named member without making it an implicit current member."),
2238
- path: z
2239
- .string()
2240
- .optional()
2241
- .describe("Project path to open for an ordinary Workspace. Required for action=open unless workspaceId is supplied or kind=\"composite\" with name is used. With mode=\"worktree\", this may also be a managed worktree path previously returned by ForgeRelay."),
2242
- relay: z
2243
- .string()
2244
- .optional()
2245
- .describe("Optional registered remote ForgeRelay alias. When supplied for action=open, the workspace is opened and executed on that remote instance while this Gateway returns its own workspaceId."),
2246
- workspaceId: z
2247
- .string()
2248
- .optional()
2249
- .describe("For action=open, an existing Workspace ID to resume or reuse. Historical duplicate IDs from earlier ForgeRelay versions may resolve to the canonical Workspace ID. For action=list, filters inventory. For action=inspect, identifies the single Workspace to inspect without opening or binding it."),
2250
- mode: z
2251
- .enum(["checkout", "worktree"])
2252
- .optional()
2253
- .describe("For action=open, defaults to checkout and uses the actual directory unless worktree isolation is explicitly requested. For action=list, filters by workspace mode."),
2254
- baseRef: z
2255
- .string()
2256
- .optional()
2257
- .describe("Local branch to base a managed worktree on and eventually merge back into. Only used with mode=\"worktree\". Defaults to the source checkout's current branch."),
2258
- newWorktree: z
2259
- .boolean()
2260
- .optional()
2261
- .describe("When true, create another isolated managed Git worktree instead of reusing the existing physical worktree. Use only when the user explicitly requests separate Git isolation."),
2262
- newWorkspace: z
2263
- .boolean()
2264
- .optional()
2265
- .describe("Deprecated compatibility flag. It no longer creates another Workspace identity for the same physical checkout or managed worktree; ForgeRelay reuses that target's canonical Workspace. Use newWorktree=true when the user explicitly needs separate Git isolation."),
2266
- context: z
2267
- .enum(["auto", "full", "none"])
2268
- .optional()
2269
- .describe("Bootstrap context policy for action=open. auto (default) sends full project context only when this conversation has not received the current context fingerprint; full forces a refresh; none opens/resumes without returning the full bootstrap context."),
2270
- root: z
2271
- .string()
2272
- .optional()
2273
- .describe("For action=list, filter by canonical workspace root or source root."),
2274
- status: z
2275
- .string()
2276
- .optional()
2277
- .describe("For action=list, filter by persisted workspace status such as active or closed."),
2278
- state: z
2279
- .enum(["active", "stale", "invalid", "closed"])
2280
- .optional()
2281
- .describe("For action=list, filter by derived lifecycle state."),
2282
- staleOnly: z
2283
- .boolean()
2284
- .optional()
2285
- .describe("For action=list, return only active Workspaces idle for more than two days."),
2286
- offset: z
2287
- .number()
2288
- .int()
2289
- .nonnegative()
2290
- .optional()
2291
- .describe("For action=list, zero-based inventory offset. Defaults to 0."),
2292
- limit: z
2293
- .number()
2294
- .int()
2295
- .min(1)
2296
- .max(100)
2297
- .optional()
2298
- .describe("For action=list, maximum records to return. Defaults to 50; maximum 100."),
383
+ });
384
+ registerActivityQueryTools(server, activityQueries, connectionScopeId, activityPanelApp.toolMeta._meta, config.activityPanelExpanded, config.logging, workspacePanelState, {
385
+ panel: async (workspaceId, conversationScopeId) => {
386
+ if (compositeWorkspaces.has(workspaceId)) {
387
+ return compositeActivity.beginPanel(workspaceId, conversationScopeId);
388
+ }
389
+ return remoteWorkspaces.has(workspaceId)
390
+ ? remoteWorkspaces.activityPanel(workspaceId, conversationScopeId)
391
+ : undefined;
2299
392
  },
2300
- outputSchema: {
2301
- action: z.enum(["open", "list", "inspect", "member"]),
2302
- workspaceId: z.string().optional(),
2303
- memberAction: z.enum(["add", "update", "remove"]).optional(),
2304
- kind: z.enum(["workspace", "composite"]).optional(),
2305
- name: z.string().optional(),
2306
- status: z.string().optional(),
2307
- state: z.enum(["active", "stale", "invalid", "closed"]).optional(),
2308
- members: z.array(z.object({
2309
- name: z.string(),
2310
- purpose: z.string(),
2311
- workspaceId: z.string(),
2312
- })).optional(),
2313
- memberContext: z.unknown().optional(),
2314
- root: z.string().optional(),
2315
- mode: z.enum(["checkout", "worktree"]).optional(),
2316
- sourceRoot: z.string().optional(),
2317
- worktree: z
2318
- .object({
2319
- path: z.string(),
2320
- baseRef: z.string(),
2321
- baseSha: z.string(),
2322
- branch: z.string().optional(),
2323
- targetBranch: z.string().optional(),
2324
- dirtySource: z.boolean(),
2325
- detached: z.boolean(),
2326
- managed: z.boolean(),
2327
- })
2328
- .optional(),
2329
- worktrees: z.array(z.object({
2330
- workspaceId: z.string(),
2331
- path: z.string(),
2332
- baseRef: z.string(),
2333
- baseSha: z.string(),
2334
- branch: z.string().optional(),
2335
- targetBranch: z.string().optional(),
2336
- managed: z.boolean(),
2337
- current: z.boolean(),
2338
- })).optional(),
2339
- staleWorkspaces: z.array(z.object({
2340
- workspaceId: z.string(),
2341
- root: z.string(),
2342
- mode: z.enum(["checkout", "worktree"]),
2343
- lastUsedAt: z.string(),
2344
- idleMs: z.number().nonnegative(),
2345
- branch: z.string().optional(),
2346
- targetBranch: z.string().optional(),
2347
- managed: z.boolean(),
2348
- })).optional(),
2349
- capabilityFingerprint: capabilityFingerprintOutputSchema.optional(),
2350
- contextFingerprint: z.string().optional(),
2351
- capabilityCatalog: z.array(capabilityCatalogOutputSchema).optional(),
2352
- capabilityGuides: z.array(capabilityGuideOutputSchema).optional(),
2353
- agentsFiles: z.array(workspaceAgentsFileOutputSchema).optional(),
2354
- availableAgentsFiles: z.array(workspaceAvailableAgentsFileOutputSchema).optional(),
2355
- skills: z.array(workspaceSkillOutputSchema).optional(),
2356
- agentProviders: z.array(workspaceSubagentProviderOutputSchema).optional(),
2357
- agents: z.array(workspaceLocalAgentOutputSchema).optional(),
2358
- skillDiagnostics: z.array(workspaceSkillDiagnosticOutputSchema).optional(),
2359
- workspaces: z.array(workspaceInventoryEntryOutputSchema).optional(),
2360
- compositeWorkspaces: z.array(z.object({
2361
- workspaceId: z.string(),
2362
- kind: z.literal("composite"),
2363
- name: z.string(),
2364
- status: z.enum(["active", "closed"]),
2365
- state: z.enum(["active", "closed"]),
2366
- members: z.array(z.object({
2367
- name: z.string(),
2368
- purpose: z.string(),
2369
- workspaceId: z.string(),
2370
- })),
2371
- createdAt: z.string(),
2372
- lastUsedAt: z.string(),
2373
- })).optional(),
2374
- summary: workspaceInventorySummaryOutputSchema.optional(),
2375
- page: workspaceInventoryPageOutputSchema.optional(),
2376
- inspection: workspaceInspectionOutputSchema.optional(),
2377
- instruction: z.string(),
393
+ snapshot: async (input, conversationScopeId) => {
394
+ const compositeTurnId = input.turnId ?? (input.workspaceId && compositeWorkspaces.has(input.workspaceId)
395
+ ? compositeActivity.currentTurnId(conversationScopeId, input.workspaceId)
396
+ : undefined);
397
+ if (compositeTurnId) {
398
+ const composite = await compositeActivity.snapshot(compositeTurnId, input.knownRevision);
399
+ if (composite)
400
+ return composite;
401
+ }
402
+ return remoteWorkspaces.activitySnapshot(input, conversationScopeId);
2378
403
  },
2379
- _meta: {},
2380
- annotations: {
2381
- readOnlyHint: false,
2382
- destructiveHint: false,
2383
- idempotentHint: false,
2384
- openWorldHint: false,
2385
- },
2386
- }, async ({ action = "open", memberAction, member, kind, name, memberName, path, relay, workspaceId, mode, baseRef, newWorktree, newWorkspace, context, root, status, state, staleOnly, offset, limit, }, { _meta, sessionId }) => {
2387
- const startedAt = performance.now();
2388
- const conversationScopeId = openAiConversationScopeId(_meta);
2389
- const protectedWorkspaceIds = processSessions.activeWorkspaceIds();
2390
- const inspectTaskSummary = (targetWorkspaceId) => {
2391
- try {
2392
- const summary = workspaceTasks.inspectSummary(targetWorkspaceId);
2393
- if (!summary)
2394
- return undefined;
2395
- const { fingerprint: _fingerprint, ...inspectionSummary } = summary;
2396
- return inspectionSummary;
2397
- }
2398
- catch {
2399
- return undefined;
2400
- }
2401
- };
2402
- const inspectCompositeMember = async (entry) => {
2403
- if (remoteWorkspaces.has(entry.workspaceId)) {
2404
- try {
2405
- const inspected = await remoteWorkspaces.inspectWorkspace(entry.workspaceId);
2406
- return {
2407
- name: entry.name,
2408
- purpose: entry.purpose,
2409
- workspaceId: entry.workspaceId,
2410
- known: true,
2411
- location: inspected.location,
2412
- routeState: inspected.routeState,
2413
- state: inspected.state,
2414
- status: inspected.status,
2415
- mode: inspected.mode,
2416
- rootValid: inspected.rootValid,
2417
- };
2418
- }
2419
- catch {
2420
- return {
2421
- name: entry.name,
2422
- purpose: entry.purpose,
2423
- workspaceId: entry.workspaceId,
2424
- known: false,
2425
- };
2426
- }
2427
- }
2428
- try {
2429
- const inspected = await workspaces.inspectWorkspace(entry.workspaceId);
2430
- return {
2431
- name: entry.name,
2432
- purpose: entry.purpose,
2433
- workspaceId: entry.workspaceId,
2434
- known: true,
2435
- location: inspected.location,
2436
- state: inspected.state,
2437
- status: inspected.status,
2438
- mode: inspected.mode,
2439
- rootValid: inspected.rootValid,
2440
- };
2441
- }
2442
- catch {
2443
- return {
2444
- name: entry.name,
2445
- purpose: entry.purpose,
2446
- workspaceId: entry.workspaceId,
2447
- known: false,
2448
- };
2449
- }
2450
- };
2451
- if (action === "inspect") {
2452
- if (!workspaceId) {
2453
- throw new Error("open_workspace action=inspect requires workspaceId.");
2454
- }
2455
- if (memberAction !== undefined || member !== undefined || kind !== undefined || name !== undefined ||
2456
- memberName !== undefined || path !== undefined || relay !== undefined || mode !== undefined ||
2457
- baseRef !== undefined || newWorktree !== undefined || newWorkspace !== undefined || context !== undefined ||
2458
- root !== undefined || status !== undefined || state !== undefined || staleOnly !== undefined ||
2459
- offset !== undefined || limit !== undefined) {
2460
- throw new Error("open_workspace action=inspect accepts only workspaceId. It never opens, resumes, binds, or mutates the inspected Workspace.");
2461
- }
2462
- let inspection;
2463
- if (compositeWorkspaces.has(workspaceId)) {
2464
- const composite = compositeWorkspaces.get(workspaceId);
2465
- const members = await Promise.all(composite.members.map(inspectCompositeMember));
2466
- const taskSummary = inspectTaskSummary(composite.id);
2467
- inspection = {
2468
- workspaceId: composite.id,
2469
- kind: "composite",
2470
- name: composite.name,
2471
- status: composite.status,
2472
- state: composite.status,
2473
- createdAt: composite.createdAt,
2474
- lastUsedAt: composite.lastUsedAt,
2475
- members,
2476
- ...(taskSummary ? { taskSummary } : {}),
2477
- };
2478
- }
2479
- else if (remoteWorkspaces.has(workspaceId)) {
2480
- inspection = await remoteWorkspaces.inspectWorkspace(workspaceId);
2481
- }
2482
- else {
2483
- const inspected = await workspaces.inspectWorkspace(workspaceId);
2484
- const taskSummary = inspectTaskSummary(inspected.workspaceId);
2485
- inspection = {
2486
- ...inspected,
2487
- ...(taskSummary ? { taskSummary } : {}),
2488
- };
2489
- }
2490
- const instruction = "This is a bounded read-only Workspace inspection. It does not open/resume the target, deliver bootstrap context, bind this conversation, or grant file/process/Git/Capability authority. Explicitly open the Workspace before modifying or executing against it.";
2491
- const result = [
2492
- `Inspected Workspace ${inspection.workspaceId} (${inspection.kind}).`,
2493
- inspection.kind === "composite"
2494
- ? `State: ${inspection.state}; members=${inspection.members.length}.`
2495
- : inspection.location === "relay"
2496
- ? inspection.state
2497
- ? `State: ${inspection.state}; route=${inspection.routeState}; mode=${inspection.mode}; location=${inspection.location}. Lifecycle and Task facts come from the Execution ForgeRelay.`
2498
- : `Route: ${inspection.routeState}; mode=${inspection.mode}; location=${inspection.location}.`
2499
- : `State: ${inspection.state}; mode=${inspection.mode}; location=${inspection.location}.`,
2500
- "Task summary is included only when durable Task state already exists on the owning Workspace; Task bodies are never returned.",
2501
- instruction,
2502
- ].join("\n");
2503
- logToolCall(config, {
2504
- tool: "open_workspace",
2505
- action: "inspect",
2506
- success: true,
2507
- durationMs: Math.round(performance.now() - startedAt),
2508
- });
2509
- return {
2510
- content: [textBlock(result)],
2511
- structuredContent: {
2512
- action: "inspect",
2513
- workspaceId: inspection.workspaceId,
2514
- kind: inspection.kind,
2515
- inspection,
2516
- instruction,
2517
- },
2518
- };
2519
- }
2520
- if (action === "member") {
2521
- if (!workspaceId || !compositeWorkspaces.has(workspaceId)) {
2522
- throw new Error("open_workspace action=member requires an existing Composite Workspace workspaceId.");
2523
- }
2524
- if (!compositeWorkspaces.isActive(workspaceId)) {
2525
- throw new Error(`Composite Workspace ${workspaceId} is closed. Reopen it with open_workspace before changing members.`);
2526
- }
2527
- if (!memberAction || !member) {
2528
- throw new Error("open_workspace action=member requires memberAction and member.");
2529
- }
2530
- if (kind !== undefined || name !== undefined || memberName !== undefined || path !== undefined || relay !== undefined || mode !== undefined ||
2531
- baseRef !== undefined || newWorktree !== undefined || newWorkspace !== undefined || context !== undefined ||
2532
- root !== undefined || status !== undefined || state !== undefined || staleOnly !== undefined ||
2533
- offset !== undefined || limit !== undefined) {
2534
- throw new Error("open_workspace action=member accepts workspaceId, memberAction, and member only. Put any Workspace open definition inside member.");
2535
- }
2536
- const resolveMemberTargetWorkspaceId = async () => {
2537
- const byWorkspaceId = typeof member.workspaceId === "string" && member.workspaceId.length > 0;
2538
- const byPath = typeof member.path === "string" && member.path.length > 0;
2539
- if (byWorkspaceId === byPath) {
2540
- throw new Error("A Composite Workspace member target requires exactly one of member.workspaceId or member.path.");
2541
- }
2542
- if (byWorkspaceId) {
2543
- const targetWorkspaceId = member.workspaceId;
2544
- if (compositeWorkspaces.has(targetWorkspaceId)) {
2545
- throw new Error("A Composite Workspace cannot be mounted as a Composite Workspace member.");
2546
- }
2547
- if (member.relay !== undefined || member.mode !== undefined || member.baseRef !== undefined ||
2548
- member.newWorktree !== undefined || member.newWorkspace !== undefined) {
2549
- throw new Error("member.workspaceId cannot be combined with relay/mode/baseRef/newWorktree/newWorkspace.");
2550
- }
2551
- if (!remoteWorkspaces.has(targetWorkspaceId))
2552
- workspaces.getWorkspace(targetWorkspaceId);
2553
- return targetWorkspaceId;
2554
- }
2555
- if (member.relay !== undefined) {
2556
- const opened = await remoteWorkspaces.openWorkspace(member.relay, {
2557
- path: member.path,
2558
- ...(member.mode ? { mode: member.mode } : {}),
2559
- ...(member.baseRef ? { baseRef: member.baseRef } : {}),
2560
- ...(member.newWorktree !== undefined ? { newWorktree: member.newWorktree } : {}),
2561
- ...(member.newWorkspace !== undefined ? { newWorkspace: member.newWorkspace } : {}),
2562
- context: "none",
2563
- });
2564
- return opened.workspaceId;
2565
- }
2566
- const opened = await workspaces.openWorkspace({
2567
- path: member.path,
2568
- ...(member.mode ? { mode: member.mode } : {}),
2569
- ...(member.baseRef ? { baseRef: member.baseRef } : {}),
2570
- ...(member.newWorktree !== undefined ? { newWorktree: member.newWorktree } : {}),
2571
- ...(member.newWorkspace !== undefined ? { newWorkspace: member.newWorkspace } : {}),
2572
- context: "none",
2573
- }, { protectedWorkspaceIds });
2574
- return opened.workspace.id;
2575
- };
2576
- let composite;
2577
- if (memberAction === "add") {
2578
- if (member.newName !== undefined) {
2579
- throw new Error("Adding a Composite Workspace member does not accept member.newName.");
2580
- }
2581
- const purpose = member.purpose?.trim();
2582
- if (!purpose)
2583
- throw new Error("Adding a Composite Workspace member requires member.purpose.");
2584
- const targetWorkspaceId = await resolveMemberTargetWorkspaceId();
2585
- composite = compositeWorkspaces.addMember(workspaceId, {
2586
- name: member.name,
2587
- purpose,
2588
- workspaceId: targetWorkspaceId,
2589
- });
2590
- }
2591
- else if (memberAction === "update") {
2592
- const targetFieldsPresent = member.workspaceId !== undefined || member.path !== undefined || member.relay !== undefined ||
2593
- member.mode !== undefined || member.baseRef !== undefined || member.newWorktree !== undefined ||
2594
- member.newWorkspace !== undefined;
2595
- if (member.newName === undefined && member.purpose === undefined && !targetFieldsPresent) {
2596
- throw new Error("Updating a Composite Workspace member requires newName, purpose, or a replacement Workspace target.");
2597
- }
2598
- const targetWorkspaceId = targetFieldsPresent
2599
- ? await resolveMemberTargetWorkspaceId()
2600
- : undefined;
2601
- composite = compositeWorkspaces.updateMember(workspaceId, member.name, {
2602
- ...(member.newName !== undefined ? { name: member.newName } : {}),
2603
- ...(member.purpose !== undefined ? { purpose: member.purpose } : {}),
2604
- ...(targetWorkspaceId !== undefined ? { workspaceId: targetWorkspaceId } : {}),
2605
- });
2606
- }
2607
- else {
2608
- if (member.newName !== undefined || member.purpose !== undefined || member.workspaceId !== undefined || member.path !== undefined ||
2609
- member.relay !== undefined || member.mode !== undefined || member.baseRef !== undefined ||
2610
- member.newWorktree !== undefined || member.newWorkspace !== undefined) {
2611
- throw new Error("Removing a Composite Workspace member accepts only member.name.");
2612
- }
2613
- composite = compositeWorkspaces.removeMember(workspaceId, member.name);
2614
- }
2615
- const memberActionVerb = memberAction === "add"
2616
- ? "Added"
2617
- : memberAction === "update"
2618
- ? "Updated"
2619
- : "Removed";
2620
- const memberActionPreposition = memberAction === "remove" ? "from" : "in";
2621
- const instruction = [
2622
- `${memberActionVerb} member ${member.name} ${memberActionPreposition} Composite Workspace ${composite.name} (${composite.id}).`,
2623
- composite.members.length > 0
2624
- ? `Members: ${composite.members.map((entry) => `${entry.name} — ${entry.purpose}`).join("; ")}.`
2625
- : "This Composite Workspace currently has no members.",
2626
- "Use the Composite workspaceId as the top-level handle. Work operations on it require an explicit member name; ForgeRelay never infers a member from tool type or purpose.",
2627
- ].join("\n");
2628
- const response = {
2629
- content: [textBlock(instruction)],
2630
- _meta: {
2631
- tool: "open_workspace",
2632
- card: {
2633
- workspaceId: composite.id,
2634
- kind: "composite",
2635
- name: composite.name,
2636
- path: composite.name,
2637
- members: composite.members,
2638
- instruction,
2639
- summary: { members: composite.members.length },
2640
- },
2641
- },
2642
- structuredContent: {
2643
- action: "member",
2644
- workspaceId: composite.id,
2645
- memberAction,
2646
- kind: "composite",
2647
- name: composite.name,
2648
- members: composite.members,
2649
- instruction,
2650
- },
2651
- };
2652
- rememberWorkspacePanelState(composite.id, response);
2653
- return response;
2654
- }
2655
- if (action === "list") {
2656
- if (path !== undefined || relay !== undefined || name !== undefined || memberName !== undefined || baseRef !== undefined || newWorktree !== undefined ||
2657
- newWorkspace !== undefined || context !== undefined) {
2658
- throw new Error("open_workspace action=list does not accept path, relay, name, memberName, baseRef, newWorktree, newWorkspace, or context. Use kind/root/workspaceId/mode/status/state/staleOnly for inventory filters.");
2659
- }
2660
- const compositeInventory = () => compositeWorkspaces.list()
2661
- .filter((entry) => workspaceId === undefined || entry.id === workspaceId)
2662
- .filter((entry) => status === undefined || entry.status === status)
2663
- .filter((entry) => state === undefined || entry.status === state)
2664
- .map((entry) => ({
2665
- workspaceId: entry.id,
2666
- kind: entry.kind,
2667
- name: entry.name,
2668
- status: entry.status,
2669
- state: entry.status,
2670
- members: entry.members,
2671
- createdAt: entry.createdAt,
2672
- lastUsedAt: entry.lastUsedAt,
2673
- }));
2674
- if (kind === "composite") {
2675
- if (root !== undefined || mode !== undefined || staleOnly !== undefined ||
2676
- offset !== undefined || limit !== undefined) {
2677
- throw new Error("Composite Workspace inventory does not accept root/mode/staleOnly/offset/limit filters; use workspaceId/status/state when selecting Composite Workspaces.");
2678
- }
2679
- const composites = compositeInventory();
2680
- const instruction = "Open a Composite Workspace by workspaceId to resume or reopen it. close_workspace preserves its identity; action=delete permanently dissolves only Composite-owned state.";
2681
- const result = [
2682
- `Composite Workspace inventory: ${composites.length} matching record${composites.length === 1 ? "" : "s"}.`,
2683
- ...composites.map((entry) => `${entry.name} [${entry.workspaceId}] state=${entry.state} members=${entry.members.length} last-used=${entry.lastUsedAt}`),
2684
- instruction,
2685
- ].join("\n");
2686
- return {
2687
- content: [textBlock(result)],
2688
- structuredContent: {
2689
- action: "list",
2690
- compositeWorkspaces: composites,
2691
- instruction,
2692
- },
2693
- };
2694
- }
2695
- const inventory = await workspaces.listWorkspaces({ workspaceId, mode, root, status, state, staleOnly, offset, limit }, { conversationScopeId, protectedWorkspaceIds });
2696
- const composites = kind === "workspace" || root !== undefined || mode !== undefined || staleOnly
2697
- ? []
2698
- : compositeInventory();
2699
- const nextOffset = inventory.page.offset + inventory.page.limit;
2700
- const instruction = [
2701
- "Resume a selected workspaceId with open_workspace(action=\"open\", workspaceId=...).",
2702
- "Use close_workspace only after the user chooses cleanup; Composite close preserves identity, while action=delete dissolves only Composite-owned state. Never close inventory entries automatically.",
2703
- inventory.page.hasMore
2704
- ? `More matching workspaces are available; continue with offset=${nextOffset}.`
2705
- : undefined,
2706
- ].filter(Boolean).join(" ");
2707
- const result = [
2708
- `Logical workspace inventory: ${inventory.summary.matching} matching ordinary records; ${composites.length} Composite Workspace record${composites.length === 1 ? "" : "s"}.`,
2709
- `States: active=${inventory.summary.active}, stale=${inventory.summary.stale}, invalid=${inventory.summary.invalid}, closed=${inventory.summary.closed}.`,
2710
- ...inventory.workspaces.map((entry) => [
2711
- entry.label,
2712
- `state=${entry.state}`,
2713
- `status=${entry.status}`,
2714
- `mode=${entry.mode}`,
2715
- entry.managed ? "managed" : undefined,
2716
- entry.current ? "current" : undefined,
2717
- `root=${entry.root}`,
2718
- `last-used=${entry.lastUsedAt}`,
2719
- ].filter(Boolean).join(" ")),
2720
- ...composites.map((entry) => `${entry.name} [${entry.workspaceId}] kind=composite state=${entry.state} members=${entry.members.length}`),
2721
- instruction,
2722
- ].join("\n");
2723
- logToolCall(config, {
2724
- tool: "open_workspace",
2725
- action: "list",
2726
- path: root,
2727
- success: true,
2728
- durationMs: Math.round(performance.now() - startedAt),
2729
- });
2730
- return {
2731
- content: [textBlock(result)],
2732
- structuredContent: {
2733
- action: "list",
2734
- ...inventory,
2735
- ...(composites.length > 0 ? { compositeWorkspaces: composites } : {}),
2736
- instruction,
2737
- },
2738
- };
2739
- }
2740
- if (root !== undefined || status !== undefined || state !== undefined ||
2741
- staleOnly !== undefined || offset !== undefined || limit !== undefined) {
2742
- throw new Error("open_workspace inventory filters root, status, state, staleOnly, offset, and limit are only valid with action=list.");
2743
- }
2744
- const openingComposite = kind === "composite" ||
2745
- (workspaceId !== undefined && compositeWorkspaces.has(workspaceId));
2746
- if (openingComposite) {
2747
- if (relay !== undefined || path !== undefined || mode !== undefined || baseRef !== undefined ||
2748
- newWorktree !== undefined || newWorkspace !== undefined) {
2749
- throw new Error("Composite Workspace open accepts name/workspaceId/context only; members are attached separately and keep their own Workspace definitions.");
2750
- }
2751
- if (workspaceId !== undefined && kind === "workspace") {
2752
- throw new Error(`${workspaceId} is a Composite Workspace, not an ordinary Workspace.`);
2753
- }
2754
- const composite = workspaceId !== undefined
2755
- ? compositeWorkspaces.open(workspaceId)
2756
- : compositeWorkspaces.create(name ?? "");
2757
- workspaceTasks.initializeWorkspace(composite.id);
2758
- const compositeTaskContext = compositeCapabilityContext(composite.id, compositeTaskGuides);
2759
- const compositeCapabilityCatalog = capabilityRegistry.catalog(compositeTaskContext);
2760
- const compositeCapabilityGuides = compositeTaskGuides.map((guide) => ({
2761
- name: guide.name,
2762
- description: guide.description,
2763
- whenToRead: guide.whenToRead,
2764
- path: formatPathForPrompt(guide.filePath),
2765
- }));
2766
- const memberContext = memberName
2767
- ? await loadCompositeMemberContext(composite.id, memberName, context ?? "auto", conversationScopeId, protectedWorkspaceIds)
2768
- : undefined;
2769
- const instruction = [
2770
- `This is Composite Workspace ${composite.name} (${composite.id}).`,
2771
- "It has no mounted working directory of its own. Use the Composite workspaceId as the top-level Workspace handle and explicitly select one named member for member-scoped work operations.",
2772
- composite.members.length > 0
2773
- ? `Members: ${composite.members.map((member) => `${member.name} — ${member.purpose}`).join("; ")}.`
2774
- : "This Composite Workspace currently has no members.",
2775
- "Member names and purposes are structural context and are always returned when this Composite Workspace is opened. context=auto/full/none controls only heavy member bootstrap context, not this Composite identity.",
2776
- compositeCapabilityCatalog.length > 0
2777
- ? `Composite-owned capabilities: ${compositeCapabilityCatalog.map((entry) => entry.name).join(", ")}. Use these without member because their state belongs to the Composite Workspace itself.`
2778
- : undefined,
2779
- composite.members.length > 0
2780
- ? "Before first work on a member, reopen this Composite Workspace with memberName=<member> and context=auto to receive that member's project bootstrap without creating an implicit current member."
2781
- : undefined,
2782
- "close_workspace preserves this Composite identity for later reopen. Use action=delete only when the user explicitly wants to dissolve the Composite relationship; neither operation closes or cleans up member Workspaces.",
2783
- ].join("\n\n");
2784
- const response = {
2785
- content: [textBlock(instruction)],
2786
- _meta: {
2787
- tool: "open_workspace",
2788
- card: {
2789
- workspaceId: composite.id,
2790
- kind: "composite",
2791
- name: composite.name,
2792
- path: composite.name,
2793
- members: composite.members,
2794
- instruction,
2795
- summary: { members: composite.members.length, status: composite.status },
2796
- },
2797
- },
2798
- structuredContent: {
2799
- action: "open",
2800
- workspaceId: composite.id,
2801
- kind: "composite",
2802
- name: composite.name,
2803
- status: composite.status,
2804
- state: composite.status,
2805
- members: composite.members,
2806
- capabilityCatalog: compositeCapabilityCatalog,
2807
- capabilityGuides: compositeCapabilityGuides,
2808
- ...(memberContext ? { memberContext } : {}),
2809
- instruction,
2810
- },
2811
- };
2812
- logToolCall(config, {
2813
- tool: "open_workspace",
2814
- action: "composite",
2815
- success: true,
2816
- durationMs: Math.round(performance.now() - startedAt),
2817
- });
2818
- rememberWorkspacePanelState(composite.id, response);
2819
- return response;
2820
- }
2821
- if (name !== undefined || memberName !== undefined) {
2822
- throw new Error("open_workspace name/memberName are only valid for a Composite Workspace.");
2823
- }
2824
- if (workspaceId !== undefined && remoteWorkspaces.has(workspaceId)) {
2825
- if (path !== undefined || relay !== undefined || mode !== undefined || baseRef !== undefined ||
2826
- newWorktree !== undefined || newWorkspace !== undefined) {
2827
- throw new Error("Resuming a relayed Workspace by workspaceId accepts context only.");
2828
- }
2829
- const resumed = await remoteWorkspaces.resumeWorkspace(workspaceId, context ?? "auto", hostScopeIdFor(_meta, sessionId));
2830
- rememberWorkspacePanelState(workspaceId, resumed);
2831
- return resumed;
2832
- }
2833
- if (relay !== undefined) {
2834
- if (!path)
2835
- throw new Error("Relayed open_workspace requires path.");
2836
- const opened = await remoteWorkspaces.openWorkspace(relay, {
2837
- path,
2838
- mode,
2839
- baseRef,
2840
- newWorktree,
2841
- newWorkspace,
2842
- context,
2843
- }, hostScopeIdFor(_meta, sessionId));
2844
- const relayedSkills = Array.isArray(opened.skills)
2845
- ? opened.skills
2846
- : [];
2847
- const relayedCapabilities = Array.isArray(opened.capabilityCatalog)
2848
- ? opened.capabilityCatalog
2849
- : [];
2850
- const result = [
2851
- `Opened relayed workspace ${opened.workspaceId}.`,
2852
- `Execution remote: ${relay}`,
2853
- `Root: ${opened.root}`,
2854
- `Mode: ${opened.mode}`,
2855
- relayedSkills.length > 0
2856
- ? `Available skills: ${relayedSkills.map((skill) => String(skill.name ?? "")).filter(Boolean).join(", ")}`
2857
- : undefined,
2858
- relayedCapabilities.length > 0
2859
- ? `Optional capabilities: ${relayedCapabilities.map((entry) => String(entry.name ?? "")).filter(Boolean).join(", ")}`
2860
- : undefined,
2861
- opened.instruction,
2862
- ].filter(Boolean).join("\n");
2863
- const response = {
2864
- content: [textBlock(result)],
2865
- _meta: {
2866
- tool: "open_workspace",
2867
- card: {
2868
- workspaceId: opened.workspaceId,
2869
- kind: "workspace",
2870
- root: opened.root,
2871
- path: opened.root,
2872
- mode: opened.mode,
2873
- relay,
2874
- instruction: opened.instruction,
2875
- summary: { mode: opened.mode, relay },
2876
- },
2877
- },
2878
- structuredContent: {
2879
- action: "open",
2880
- workspaceId: opened.workspaceId,
2881
- kind: "workspace",
2882
- root: opened.root,
2883
- mode: opened.mode,
2884
- ...(opened.sourceRoot ? { sourceRoot: opened.sourceRoot } : {}),
2885
- ...(opened.contextFingerprint !== undefined
2886
- ? { contextFingerprint: opened.contextFingerprint }
2887
- : {}),
2888
- ...(opened.capabilityFingerprint !== undefined
2889
- ? { capabilityFingerprint: opened.capabilityFingerprint }
2890
- : {}),
2891
- ...(opened.capabilityCatalog !== undefined
2892
- ? { capabilityCatalog: opened.capabilityCatalog }
2893
- : {}),
2894
- ...(opened.capabilityGuides !== undefined
2895
- ? { capabilityGuides: opened.capabilityGuides }
2896
- : {}),
2897
- ...(opened.agentsFiles !== undefined ? { agentsFiles: opened.agentsFiles } : {}),
2898
- ...(opened.availableAgentsFiles !== undefined
2899
- ? { availableAgentsFiles: opened.availableAgentsFiles }
2900
- : {}),
2901
- ...(opened.skills !== undefined ? { skills: opened.skills } : {}),
2902
- ...(opened.agentProviders !== undefined
2903
- ? { agentProviders: opened.agentProviders }
2904
- : {}),
2905
- ...(opened.agents !== undefined ? { agents: opened.agents } : {}),
2906
- ...(opened.skillDiagnostics !== undefined
2907
- ? { skillDiagnostics: opened.skillDiagnostics }
2908
- : {}),
2909
- instruction: opened.instruction,
2910
- },
2911
- };
2912
- logToolCall(config, {
2913
- tool: "open_workspace",
2914
- action: "relay",
2915
- path: opened.root,
2916
- success: true,
2917
- durationMs: Math.round(performance.now() - startedAt),
2918
- });
2919
- rememberWorkspacePanelState(opened.workspaceId, response);
2920
- return response;
2921
- }
2922
- const { workspace, agentsFiles, availableAgentsFiles, hookReports, workspaceReused, includeBootstrapContext, bootstrapContextComponents, contextFingerprint, } = await workspaces.openWorkspace({ path, workspaceId, mode, baseRef, newWorktree, newWorkspace, context }, {
2923
- conversationScopeId,
2924
- protectedWorkspaceIds,
2925
- });
2926
- workspaceTasks.initializeWorkspace(workspace.id);
2927
- const knownWorktrees = await workspaces.listKnownWorktrees(workspace);
2928
- const staleWorkspaces = await workspaces.listStaleWorkspaces(workspace);
2929
- const capabilityFingerprint = buildCapabilityFingerprint(config, FORGERELAY_VERSION, {
2930
- artifactDownloadSupported: isArtifactDownloadSupportedPlatform(),
2931
- });
2932
- if (config.widgets === "changes") {
2933
- await reviewCheckpoints.initializeWorkspace({
2934
- workspaceId: workspace.id,
2935
- root: workspace.root,
2936
- });
2937
- }
2938
- const cardSkills = workspace.skills
2939
- .filter((skill) => !skill.disableModelInvocation)
2940
- .map((skill) => ({
2941
- name: skill.name,
2942
- description: skill.description,
2943
- }));
2944
- const capabilityGuides = workspace.capabilityGuides.map((guide) => ({
2945
- name: guide.name,
2946
- description: guide.description,
2947
- whenToRead: guide.whenToRead,
2948
- path: formatPathForPrompt(guide.filePath),
2949
- }));
2950
- const capabilityCatalog = capabilityRegistry.catalog(capabilityContextFor(workspace));
2951
- const cardAgentProviders = config.subagents ? subagentProviders : [];
2952
- const cardAgents = workspace.agentProfiles.map((profile) => {
2953
- const summary = summarizeSubagentProfile(profile);
2954
- const availability = cardAgentProviders.find((provider) => provider.name === summary.provider);
2955
- return {
2956
- ...summary,
2957
- providerAvailable: availability?.available,
2958
- providerUnavailableReason: availability?.reason,
2959
- };
2960
- });
2961
- const cardAgentsFiles = agentsFiles.map((file) => ({
2962
- path: formatAgentsPath(file.path, workspace.root),
2963
- content: file.content,
2964
- }));
2965
- const cardAvailableAgentsFiles = availableAgentsFiles.map((file) => ({
2966
- path: formatAgentsPath(file.path, workspace.root),
2967
- }));
2968
- const bootstrapComponents = new Set(bootstrapContextComponents);
2969
- const visibleSkills = bootstrapComponents.has("skills") ? cardSkills : [];
2970
- const visibleSkillDiagnostics = bootstrapComponents.has("skillDiagnostics")
2971
- ? redactSkillDiagnosticPaths(workspace.skillDiagnostics)
2972
- : [];
2973
- const visibleCapabilityGuides = bootstrapComponents.has("capabilityGuides") ? capabilityGuides : [];
2974
- const visibleAgentProviders = bootstrapComponents.has("agentProfiles") ? cardAgentProviders : [];
2975
- const visibleAgents = bootstrapComponents.has("agentProfiles") ? cardAgents : [];
2976
- const loadedAgentsFiles = bootstrapComponents.has("agentsFiles") ? cardAgentsFiles : [];
2977
- const availableAgentsFileOutputs = bootstrapComponents.has("availableAgentsFiles")
2978
- ? cardAvailableAgentsFiles
2979
- : [];
2980
- const workspaceContextInstruction = "For later open_workspace calls, context=\"auto\" avoids repeating unchanged bootstrap context; use context=\"none\" when only the workspace handle/metadata is needed, or context=\"full\" to force a refresh.";
2981
- const workspaceManagementInstruction = "Use open_workspace(action=\"list\") for lightweight Workspace inventory. Use action=\"inspect\" with one known workspaceId for bounded read-only metadata without opening/resuming it. Explicitly open a Workspace before executing or mutating against it, and ask the user before close_workspace cleanup.";
2982
- const cardInstruction = config.skillsEnabled
2983
- ? `Use this workspaceId in all subsequent tool calls for this project. Follow loaded agentsFiles instructions. Read an availableAgentsFiles path before working under it. When a task matches an available skill, load it with read(path=\"skills://<name>\") before proceeding. When a task matches a capability guide, read its advertised path before proceeding. ${workspaceContextInstruction} ${workspaceManagementInstruction}`
2984
- : `Use this workspaceId in all subsequent tool calls for this project. Follow loaded agentsFiles instructions. Read an availableAgentsFiles path before working under it. When a task matches a capability guide, read its advertised path before proceeding. ${workspaceContextInstruction} ${workspaceManagementInstruction}`;
2985
- const instruction = workspaceReused
2986
- ? includeBootstrapContext
2987
- ? [
2988
- `Workspace already exists as ${workspace.id} for this directory.`,
2989
- "Reuse this workspaceId for subsequent tool calls.",
2990
- `Project bootstrap context components included in this response: ${bootstrapContextComponents.join(", ")}. Components not listed are unchanged and are not repeated.`,
2991
- workspaceContextInstruction,
2992
- workspaceManagementInstruction,
2993
- ].join("\n\n")
2994
- : [
2995
- `Workspace already open as ${workspace.id}.`,
2996
- "Reuse this workspaceId for subsequent tool calls. This is the same directory previously opened in this conversation.",
2997
- "Continue following the project instructions, nested instruction files, skills, capability guides, agent profiles, and diagnostics previously provided for this workspace. They remain active and are not repeated here.",
2998
- workspaceContextInstruction,
2999
- workspaceManagementInstruction,
3000
- ].join("\n\n")
3001
- : workspace.mode === "worktree"
3002
- ? "Use this workspaceId for subsequent tool calls. Follow the project instructions, nested instruction files, skills, agent profiles, and diagnostics returned for this isolated worktree."
3003
- : cardInstruction;
3004
- const resultContent = [
3005
- {
3006
- type: "text",
3007
- text: [
3008
- workspaceReused
3009
- ? `Workspace already open as ${workspace.id}.`
3010
- : workspace.mode === "worktree"
3011
- ? `Opened isolated worktree workspace ${workspace.id}.`
3012
- : `Opened workspace ${workspace.id}.`,
3013
- `Root: ${workspace.root}`,
3014
- `Mode: ${workspace.mode}`,
3015
- loadedAgentsFiles.length > 0
3016
- ? `Loaded project instructions: ${loadedAgentsFiles.map((file) => file.path).join(", ")}`
3017
- : undefined,
3018
- availableAgentsFileOutputs.length > 0
3019
- ? `Available nested instructions: ${availableAgentsFileOutputs.map((file) => file.path).join(", ")}`
3020
- : undefined,
3021
- visibleSkills.length > 0
3022
- ? `Available skills: ${visibleSkills.map((skill) => skill.name).join(", ")}`
3023
- : undefined,
3024
- capabilityCatalog.length > 0
3025
- ? `Optional capabilities: ${capabilityCatalog.map((entry) => entry.name).join(", ")}`
3026
- : undefined,
3027
- visibleCapabilityGuides.length > 0
3028
- ? `Capability guides: ${visibleCapabilityGuides.map((guide) => guide.name).join(", ")}`
3029
- : undefined,
3030
- visibleAgentProviders.some((provider) => provider.available)
3031
- ? `Available subagent providers: ${visibleAgentProviders.filter((provider) => provider.available).map((provider) => provider.name).join(", ")}`
3032
- : undefined,
3033
- visibleAgentProviders.some((provider) => !provider.available)
3034
- ? `Unavailable subagent providers: ${visibleAgentProviders.filter((provider) => !provider.available).map(formatUnavailableSubagentProvider).join(", ")}`
3035
- : undefined,
3036
- visibleAgents.length > 0
3037
- ? `Available subagent profiles: ${visibleAgents.map(formatAvailableSubagentProfile).join(", ")}`
3038
- : undefined,
3039
- knownWorktrees.length > 0
3040
- ? `Known worktrees: ${knownWorktrees.map((worktree) => `${worktree.path} [${worktree.workspaceId}]${worktree.branch ? ` branch=${worktree.branch}` : ""}${worktree.targetBranch ? ` target=${worktree.targetBranch}` : ""}${worktree.current ? " (current)" : ""}`).join(", ")}`
3041
- : undefined,
3042
- staleWorkspaces.length > 0
3043
- ? `This Workspace has been idle for more than 2 days: ${staleWorkspaces.map((stale) => `${stale.workspaceId} last-used=${stale.lastUsedAt}`).join(", ")}. It remains available to resume or explicitly close; do not clean it up automatically.`
3044
- : undefined,
3045
- `ForgeRelay ${capabilityFingerprint.version} capabilities: ${capabilityFingerprint.capabilities.join(", ")}`,
3046
- instruction,
3047
- ].filter(Boolean).join("\n"),
3048
- },
3049
- ];
3050
- logToolCall(config, {
3051
- tool: "open_workspace",
3052
- ...workspaceLogContext(workspace, sessionId),
3053
- path: workspace.root,
3054
- success: true,
3055
- durationMs: Math.round(performance.now() - startedAt),
3056
- });
3057
- const workspaceCard = {
3058
- workspaceId: workspace.id,
3059
- kind: "workspace",
3060
- root: workspace.root,
3061
- path: workspace.root,
3062
- mode: workspace.mode,
3063
- workspaceReused,
3064
- includeBootstrapContext,
3065
- sourceRoot: workspace.sourceRoot,
3066
- worktree: workspace.worktree,
3067
- worktrees: knownWorktrees,
3068
- staleWorkspaces,
3069
- capabilityFingerprint,
3070
- contextFingerprint,
3071
- capabilityCatalog,
3072
- agentsFiles: cardAgentsFiles,
3073
- availableAgentsFiles: cardAvailableAgentsFiles,
3074
- skills: cardSkills,
3075
- agentProviders: cardAgentProviders,
3076
- agents: cardAgents,
3077
- instruction: cardInstruction,
3078
- summary: {
3079
- mode: workspace.mode,
3080
- agentsFiles: cardAgentsFiles.length,
3081
- availableAgentsFiles: cardAvailableAgentsFiles.length,
3082
- skills: cardSkills.length,
3083
- capabilities: capabilityCatalog.length,
3084
- agentProviders: cardAgentProviders.length,
3085
- agents: cardAgents.length,
3086
- },
3087
- };
3088
- const response = hooks.decorateResult(workspace.id, attachHookReports({
3089
- content: resultContent,
3090
- _meta: {
3091
- tool: "open_workspace",
3092
- card: includeBootstrapContext
3093
- ? workspaceCard
3094
- : compactWorkspacePresentation(workspaceCard),
3095
- },
3096
- structuredContent: {
3097
- action: "open",
3098
- workspaceId: workspace.id,
3099
- kind: "workspace",
3100
- root: workspace.root,
3101
- mode: workspace.mode,
3102
- sourceRoot: workspace.sourceRoot,
3103
- worktree: workspace.worktree,
3104
- worktrees: knownWorktrees,
3105
- staleWorkspaces,
3106
- capabilityFingerprint,
3107
- contextFingerprint,
3108
- capabilityCatalog,
3109
- ...(bootstrapComponents.has("capabilityGuides")
3110
- ? { capabilityGuides: visibleCapabilityGuides }
3111
- : {}),
3112
- ...(bootstrapComponents.has("agentsFiles") ? { agentsFiles: loadedAgentsFiles } : {}),
3113
- ...(bootstrapComponents.has("availableAgentsFiles")
3114
- ? { availableAgentsFiles: availableAgentsFileOutputs }
3115
- : {}),
3116
- ...(bootstrapComponents.has("skills") ? { skills: visibleSkills } : {}),
3117
- ...(bootstrapComponents.has("agentProfiles")
3118
- ? { agentProviders: visibleAgentProviders, agents: visibleAgents }
3119
- : {}),
3120
- ...(bootstrapComponents.has("skillDiagnostics")
3121
- ? { skillDiagnostics: visibleSkillDiagnostics }
3122
- : {}),
3123
- instruction,
3124
- },
3125
- }, hookReports));
3126
- rememberWorkspacePanelState(workspace.id, response);
3127
- return response;
3128
- });
3129
- registerActivityQueryTools(server, activityQueries, connectionScopeId, toolWidgetDescriptorMeta(config, "activity")._meta, config.activityPanelExpanded, config.logging, workspacePanelState, {
3130
- panel: async (workspaceId, conversationScopeId) => {
3131
- if (compositeWorkspaces.has(workspaceId)) {
3132
- return compositeActivity.beginPanel(workspaceId, conversationScopeId);
3133
- }
3134
- return remoteWorkspaces.has(workspaceId)
3135
- ? remoteWorkspaces.activityPanel(workspaceId, conversationScopeId)
3136
- : undefined;
3137
- },
3138
- snapshot: async (input, conversationScopeId) => {
3139
- const compositeTurnId = input.turnId ?? (input.workspaceId && compositeWorkspaces.has(input.workspaceId)
3140
- ? compositeActivity.currentTurnId(conversationScopeId, input.workspaceId)
3141
- : undefined);
3142
- if (compositeTurnId) {
3143
- const composite = await compositeActivity.snapshot(compositeTurnId, input.knownRevision);
3144
- if (composite)
3145
- return composite;
3146
- }
3147
- return remoteWorkspaces.activitySnapshot(input, conversationScopeId);
3148
- },
3149
- index: async (turnId, knownRevision, conversationScopeId) => {
3150
- const composite = await compositeActivity.index(turnId, knownRevision);
3151
- return composite ?? remoteWorkspaces.activityIndex(turnId, knownRevision, conversationScopeId);
404
+ index: async (turnId, knownRevision, conversationScopeId) => {
405
+ const composite = await compositeActivity.index(turnId, knownRevision);
406
+ return composite ?? remoteWorkspaces.activityIndex(turnId, knownRevision, conversationScopeId);
3152
407
  },
3153
408
  detail: async (turnId, activityId, conversationScopeId) => {
3154
409
  const composite = await compositeActivity.detail(turnId, activityId);
@@ -3159,1130 +414,53 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
3159
414
  return composite ?? remoteWorkspaces.activityOutput(turnId, outputId, conversationScopeId, cursor);
3160
415
  },
3161
416
  });
3162
- registerAppTool(server, toolNames.capability, {
3163
- title: "Use optional capability",
3164
- description: "Describe or run one optional ForgeRelay capability advertised by open_workspace. Use describe when the capability contract is unfamiliar, then read its advertised guide if needed. Run dispatches only explicitly registered capabilities; it cannot invoke arbitrary shell commands, URLs, or methods.",
3165
- inputSchema: {
3166
- workspaceId: z.string().describe("Workspace identifier returned by open_workspace."),
3167
- member: z.string().optional().describe("Required for a Composite Workspace; explicit member name whose capability surface is used."),
3168
- name: z
3169
- .string()
3170
- .regex(/^[a-z][a-z0-9-]*(?:\.[a-z][a-z0-9-]*)+$/)
3171
- .describe("Stable dotted capability name advertised by open_workspace."),
3172
- action: z.enum(["describe", "run"]),
3173
- arguments: z
3174
- .record(z.string(), z.unknown())
3175
- .optional()
3176
- .describe("Capability-specific JSON arguments. Omit for describe and for capabilities with no arguments."),
3177
- file: z
3178
- .unknown()
3179
- .optional()
3180
- .describe("Host-native file value. Only capabilities whose describe result advertises native-file transport may consume it."),
3181
- },
3182
- outputSchema: {
3183
- name: z.string(),
3184
- action: z.enum(["describe", "run"]),
3185
- member: z.string().optional(),
3186
- capability: z.unknown().optional(),
3187
- result: z.unknown().optional(),
3188
- error: capabilityErrorOutputSchema.optional(),
3189
- },
3190
- _meta: {
3191
- ...toolWidgetDescriptorMeta(config, "capability")._meta,
3192
- "openai/fileParams": ["file"],
3193
- },
3194
- annotations: {
3195
- readOnlyHint: false,
3196
- destructiveHint: false,
3197
- idempotentHint: false,
3198
- openWorldHint: true,
3199
- },
3200
- }, async ({ workspaceId, member, name, action, arguments: capabilityArguments, file }, extra) => {
3201
- if (name === "workspace.tasks" && compositeWorkspaces.has(workspaceId)) {
3202
- if (member !== undefined) {
3203
- throw new Error(`workspace.tasks belongs to Composite Workspace ${workspaceId} itself and does not accept member.`);
3204
- }
3205
- if (!compositeWorkspaces.isActive(workspaceId)) {
3206
- throw new Error(`Composite Workspace ${workspaceId} is closed. Reopen it with open_workspace before use.`);
3207
- }
3208
- const startedAt = performance.now();
3209
- const context = compositeCapabilityContext(workspaceId, compositeTaskGuides);
3210
- try {
3211
- if (action === "run") {
3212
- const execution = await capabilityRegistry.run(name, capabilityArguments ?? {}, context, {
3213
- nativeFile: file,
3214
- signal: extra.signal,
3215
- requestMeta: extra._meta,
3216
- sessionId: extra.sessionId,
3217
- });
3218
- const result = {
3219
- content: [textBlock(`Capability ${name} completed.\n${JSON.stringify(execution.value, null, 2)}`)],
3220
- structuredContent: { name, action, result: execution.value },
3221
- };
3222
- logToolCall(config, {
3223
- tool: toolNames.capability,
3224
- capability: name,
3225
- action,
3226
- success: true,
3227
- durationMs: Math.round(performance.now() - startedAt),
3228
- });
3229
- return result;
3230
- }
3231
- const capability = capabilityRegistry.describe(name, context);
3232
- const result = {
3233
- content: [textBlock([
3234
- `${capability.name}: ${capability.description}`,
3235
- `Available: ${capability.available}`,
3236
- `Guide: ${capability.guide.path}`,
3237
- capability.guide.readBeforeFirstUse
3238
- ? "Read the guide before first use when this contract is unfamiliar."
3239
- : undefined,
3240
- ].filter(Boolean).join("\n"))],
3241
- structuredContent: { name, action, capability },
3242
- };
3243
- logToolCall(config, {
3244
- tool: toolNames.capability,
3245
- capability: name,
3246
- action,
3247
- success: true,
3248
- durationMs: Math.round(performance.now() - startedAt),
3249
- });
3250
- return result;
3251
- }
3252
- catch (error) {
3253
- if (extra.signal.aborted)
3254
- throw error;
3255
- const capabilityError = error instanceof CapabilityError
3256
- ? error
3257
- : new CapabilityError("execution_failed", error instanceof Error ? error.message : String(error));
3258
- return {
3259
- content: [textBlock(`${capabilityError.code}: ${capabilityError.message}`)],
3260
- structuredContent: {
3261
- name,
3262
- action,
3263
- error: { code: capabilityError.code, message: capabilityError.message },
3264
- },
3265
- isError: true,
3266
- };
3267
- }
3268
- }
3269
- const target = resolveExecutionTarget(workspaceId, member);
3270
- const executionWorkspaceId = target.executionWorkspaceId;
3271
- const executionContext = await prepareExecutionContext(target, extra._meta, extra.signal, extra.sessionId);
3272
- if (remoteWorkspaces.has(executionWorkspaceId)) {
3273
- const response = await remoteWorkspaces.capability(executionWorkspaceId, {
3274
- name,
3275
- action,
3276
- ...(capabilityArguments !== undefined ? { arguments: capabilityArguments } : {}),
3277
- ...(file !== undefined ? { file } : {}),
3278
- }, hostScopeIdFor(extra._meta, extra.sessionId));
3279
- return action === "run" && name !== "workspace.tasks"
3280
- ? presentSemanticWorkResult(response, target)
3281
- : presentExecutionResult(response, target);
3282
- }
3283
- if (action === "run" && name === "batch.execute") {
3284
- const workspace = workspaces.getWorkspace(executionWorkspaceId);
3285
- const startedAt = performance.now();
3286
- try {
3287
- const execution = await capabilityRegistry.run(name, capabilityArguments ?? {}, capabilityContextFor(workspace), {
3288
- nativeFile: file,
3289
- signal: extra.signal,
3290
- requestMeta: extra._meta,
3291
- sessionId: extra.sessionId,
3292
- });
3293
- const result = {
3294
- content: [textBlock(`Capability ${name} completed.\n${JSON.stringify(execution.value, null, 2)}`)],
3295
- structuredContent: { name, action, result: execution.value },
3296
- };
3297
- logToolCall(config, {
3298
- tool: toolNames.capability,
3299
- ...workspaceLogContext(workspace, extra.sessionId),
3300
- capability: name,
3301
- action,
3302
- success: true,
3303
- durationMs: Math.round(performance.now() - startedAt),
3304
- });
3305
- return presentSemanticWorkResult(result, target);
3306
- }
3307
- catch (error) {
3308
- if (extra.signal.aborted)
3309
- throw error;
3310
- const capabilityError = error instanceof CapabilityError
3311
- ? error
3312
- : new CapabilityError("execution_failed", error instanceof Error ? error.message : String(error));
3313
- const result = {
3314
- content: [textBlock(`${capabilityError.code}: ${capabilityError.message}`)],
3315
- structuredContent: {
3316
- name,
3317
- action,
3318
- error: { code: capabilityError.code, message: capabilityError.message },
3319
- },
3320
- isError: true,
3321
- };
3322
- logFailedToolResponse(config, {
3323
- tool: toolNames.capability,
3324
- ...workspaceLogContext(workspace, extra.sessionId),
3325
- capability: name,
3326
- action,
3327
- }, result.content, startedAt);
3328
- return presentExecutionResult(result, target);
3329
- }
3330
- }
3331
- if (action === "run") {
3332
- const response = await coreOperations.capabilityRun({ workspaceId: executionWorkspaceId, name, arguments: capabilityArguments, file }, executionContext);
3333
- return name === "workspace.tasks"
3334
- ? presentExecutionResult(response, target)
3335
- : presentSemanticWorkResult(response, target);
3336
- }
3337
- const workspace = workspaces.getWorkspace(executionWorkspaceId);
3338
- return runActivityToolWithHooks(activityLifecycle, hooks, workspace, hostScopeIdFor(extra._meta, extra.sessionId), activityRequestFor({ workspaceId: executionWorkspaceId, name, action, arguments: capabilityArguments, file }, executionContext), {
3339
- signal: extra.signal,
3340
- tool: toolNames.capability,
3341
- invocation: workspaceHookInvocation(workspace),
3342
- payload: { name, action },
3343
- isFailure: toolResultIsError,
3344
- operation: async () => {
3345
- const startedAt = performance.now();
3346
- try {
3347
- const capability = capabilityRegistry.describe(name, capabilityContextFor(workspace));
3348
- const result = {
3349
- content: [textBlock([
3350
- `${capability.name}: ${capability.description}`,
3351
- `Available: ${capability.available}`,
3352
- `Guide: ${capability.guide.path}`,
3353
- capability.guide.readBeforeFirstUse
3354
- ? "Read the guide before first use when this contract is unfamiliar."
3355
- : undefined,
3356
- ].filter(Boolean).join("\n"))],
3357
- structuredContent: { name, action, capability },
3358
- };
3359
- logToolCall(config, {
3360
- tool: toolNames.capability,
3361
- ...workspaceLogContext(workspace),
3362
- capability: name,
3363
- action,
3364
- success: true,
3365
- durationMs: Math.round(performance.now() - startedAt),
3366
- });
3367
- return result;
3368
- }
3369
- catch (error) {
3370
- const capabilityError = error instanceof CapabilityError
3371
- ? error
3372
- : new CapabilityError("execution_failed", error instanceof Error ? error.message : String(error));
3373
- const result = {
3374
- content: [textBlock(`${capabilityError.code}: ${capabilityError.message}`)],
3375
- structuredContent: {
3376
- name,
3377
- action,
3378
- error: { code: capabilityError.code, message: capabilityError.message },
3379
- },
3380
- isError: true,
3381
- };
3382
- logFailedToolResponse(config, {
3383
- tool: toolNames.capability,
3384
- ...workspaceLogContext(workspace),
3385
- capability: name,
3386
- action,
3387
- }, result.content, startedAt);
3388
- return result;
3389
- }
3390
- },
3391
- }, activityRelationFor(executionContext)).then((result) => presentExecutionResult(result, target));
417
+ registerWorkspaceAuxiliaryTools({
418
+ server, config, workspaces, remoteWorkspaces, compositeWorkspaces, compositeTaskGuides, capabilityRegistry,
419
+ coreOperations, activityLifecycle, hooks, workspaceTasks, taskReminders, activityQueries, compositeActivity,
420
+ workspacePanelStates, processSessions, reviewCheckpoints, codeIntelligence, resolveExecutionTarget,
421
+ prepareExecutionContext, hostScopeIdFor, presentExecutionResult, presentSemanticWorkResult,
3392
422
  });
3393
- registerAppTool(server, toolNames.closeWorkspace, {
3394
- title: "Close workspace",
3395
- description: "Close or explicitly delete one Workspace after the user chooses cleanup. action=close (default) preserves checkout, managed-worktree, Composite, and relayed identity for later reopen. action=delete permanently removes ForgeRelay-owned state. Managed-worktree-backed Workspaces still finalize safely when active and require commitMessage. Composite delete dissolves only Composite-owned state and never closes member Workspaces. Checkout project files are never deleted.",
3396
- inputSchema: {
3397
- workspaceId: z.string().describe("Workspace identifier to close or delete."),
3398
- action: z
3399
- .enum(["close", "delete"])
3400
- .optional()
3401
- .describe("Defaults to close. close preserves checkout identity, managed-worktree identity, and Composite identity for later reopen; delete removes ForgeRelay-owned state. Composite delete dissolves only the Composite relationship. Active managed worktrees still require safe finalization and commitMessage; checkout project files are never deleted."),
3402
- commitMessage: z
3403
- .string()
3404
- .min(1)
3405
- .optional()
3406
- .describe("Required only for a managed-worktree-backed workspace; concise Git commit message for remaining worktree changes."),
3407
- },
3408
- outputSchema: resultOutputSchema({
3409
- workspaceId: z.string(),
3410
- action: z.enum(["close", "delete"]).optional(),
3411
- kind: z.enum(["workspace", "composite"]).optional(),
3412
- mode: z.enum(["checkout", "worktree"]).optional(),
3413
- name: z.string().optional(),
3414
- members: z.array(z.object({
3415
- name: z.string(),
3416
- purpose: z.string(),
3417
- workspaceId: z.string(),
3418
- })).optional(),
3419
- status: z.enum(["active", "closed"]).optional(),
3420
- dissolved: z.boolean().optional(),
3421
- sourceRoot: z.string().optional(),
3422
- branch: z.string().optional(),
3423
- targetBranch: z.string().optional(),
3424
- commitSha: z.string().optional(),
3425
- mergedSha: z.string().optional(),
3426
- committed: z.boolean().optional(),
3427
- cleanupWarning: z.string().optional(),
3428
- }),
3429
- _meta: {},
3430
- annotations: WRITE_TOOL_ANNOTATIONS,
3431
- }, async ({ workspaceId, action = "close", commitMessage }, extra) => {
3432
- if (compositeWorkspaces.has(workspaceId)) {
3433
- if (commitMessage !== undefined) {
3434
- throw new Error("close_workspace commitMessage is not valid for a Composite Workspace.");
3435
- }
3436
- const composite = action === "delete"
3437
- ? compositeWorkspaces.dissolve(workspaceId)
3438
- : compositeWorkspaces.close(workspaceId);
3439
- if (action === "delete") {
3440
- workspaceTasks.deleteWorkspace(workspaceId);
3441
- taskReminders.forget(workspaceId);
3442
- }
3443
- compositeActivity.forgetComposite(workspaceId);
3444
- workspacePanelStates.delete(workspaceId);
3445
- const result = [
3446
- action === "delete"
3447
- ? `Deleted Composite Workspace ${composite.name} (${workspaceId}); its Composite relationship and ForgeRelay-owned Composite state were dissolved.`
3448
- : `Closed Composite Workspace ${composite.name} (${workspaceId}); its identity and member topology were preserved for later reopen.`,
3449
- composite.members.length > 0
3450
- ? `Preserved member Workspaces: ${composite.members.map((member) => `${member.name} [${member.workspaceId}]`).join(", ")}.`
3451
- : "The Composite Workspace had no members.",
3452
- "Member Workspace handles, managed worktrees, processes, files, and Workspace Relay routes were not closed, finalized, deleted, or otherwise mutated.",
3453
- ].join("\n");
3454
- return {
3455
- content: [textBlock(result)],
3456
- _meta: {
3457
- tool: toolNames.closeWorkspace,
3458
- card: {
3459
- workspaceId,
3460
- action,
3461
- kind: "composite",
3462
- name: composite.name,
3463
- members: composite.members,
3464
- ...(action === "close" ? { status: "closed" } : {}),
3465
- dissolved: action === "delete",
3466
- payload: { content: [textBlock(result)] },
3467
- },
3468
- },
3469
- structuredContent: {
3470
- result,
3471
- workspaceId,
3472
- action,
3473
- kind: "composite",
3474
- name: composite.name,
3475
- members: composite.members,
3476
- ...(action === "close" ? { status: "closed" } : {}),
3477
- dissolved: action === "delete",
3478
- },
3479
- };
3480
- }
3481
- if (remoteWorkspaces.has(workspaceId)) {
3482
- const response = await remoteWorkspaces.closeWorkspace(workspaceId, { action, ...(commitMessage !== undefined ? { commitMessage } : {}) }, hostScopeIdFor(extra._meta, extra.sessionId));
3483
- workspacePanelStates.delete(workspaceId);
3484
- return response;
3485
- }
3486
- const session = workspaces.getWorkspaceSession(workspaceId);
3487
- if (action === "delete" && session.mode === "checkout") {
3488
- if (commitMessage !== undefined) {
3489
- throw new Error("close_workspace commitMessage is not valid with action=delete for a checkout Workspace.");
3490
- }
3491
- if (processSessions.activeWorkspaceIds().has(session.id)) {
3492
- throw new Error(`Workspace ${session.id} still owns a running process. Poll, interrupt, or wait for it before deleting this Workspace.`);
3493
- }
3494
- const response = await runToolWithHooks(hooks, {
3495
- signal: extra.signal,
3496
- tool: toolNames.closeWorkspace,
3497
- invocation: {
3498
- workspaceId: session.id,
3499
- workspaceRoot: session.root,
3500
- workspaceMode: session.mode,
3501
- sourceRoot: session.sourceRoot,
3502
- },
3503
- payload: { workspaceId: session.id, action: "delete", mode: session.mode },
3504
- operation: async () => {
3505
- workspaces.deleteWorkspace(session.id);
3506
- workspaceTasks.deleteWorkspace(session.id);
3507
- taskReminders.forget(session.id);
3508
- await reviewCheckpoints.releaseWorkspace(session.id);
3509
- const result = `Deleted ForgeRelay Workspace ${session.id}. Physical project files were not removed.`;
3510
- return {
3511
- content: [textBlock(result)],
3512
- _meta: {
3513
- tool: toolNames.closeWorkspace,
3514
- card: {
3515
- workspaceId: session.id,
3516
- action: "delete",
3517
- mode: "checkout",
3518
- payload: { content: [textBlock(result)] },
3519
- },
3520
- },
3521
- structuredContent: {
3522
- result,
3523
- workspaceId: session.id,
3524
- action: "delete",
3525
- mode: "checkout",
3526
- },
3527
- };
3528
- },
3529
- });
3530
- workspacePanelStates.delete(session.id);
3531
- return response;
3532
- }
3533
- if (action === "delete" && session.mode === "worktree" && session.status === "closed") {
3534
- if (commitMessage !== undefined) {
3535
- throw new Error("close_workspace commitMessage is not needed when deleting an already-closed managed-worktree Workspace.");
3536
- }
3537
- const hookRoot = session.sourceRoot ?? session.root;
3538
- const response = await runToolWithHooks(hooks, {
3539
- signal: extra.signal,
3540
- tool: toolNames.closeWorkspace,
3541
- invocation: {
3542
- workspaceId: session.id,
3543
- workspaceRoot: hookRoot,
3544
- workspaceMode: session.mode,
3545
- sourceRoot: session.sourceRoot,
3546
- },
3547
- payload: { workspaceId: session.id, action: "delete", mode: session.mode },
3548
- operation: async () => {
3549
- workspaces.deleteWorkspace(session.id);
3550
- workspaceTasks.deleteWorkspace(session.id);
3551
- taskReminders.forget(session.id);
3552
- await reviewCheckpoints.releaseWorkspace(session.id);
3553
- const result = `Deleted closed managed-worktree Workspace ${session.id}. Its already-removed worktree backing was not recreated.`;
3554
- return {
3555
- content: [textBlock(result)],
3556
- _meta: {
3557
- tool: toolNames.closeWorkspace,
3558
- card: {
3559
- workspaceId: session.id,
3560
- action: "delete",
3561
- mode: "worktree",
3562
- sourceRoot: session.sourceRoot,
3563
- targetBranch: session.targetBranch,
3564
- payload: { content: [textBlock(result)] },
3565
- },
3566
- },
3567
- structuredContent: {
3568
- result,
3569
- workspaceId: session.id,
3570
- action: "delete",
3571
- mode: "worktree",
3572
- sourceRoot: session.sourceRoot,
3573
- targetBranch: session.targetBranch,
3574
- },
3575
- };
3576
- },
3577
- });
3578
- workspacePanelStates.delete(session.id);
3579
- return response;
3580
- }
3581
- const workspace = workspaces.getWorkspace(session.id);
3582
- const response = await runToolWithHooks(hooks, {
3583
- signal: extra.signal,
3584
- tool: toolNames.closeWorkspace,
3585
- invocation: workspaceHookInvocation(workspace),
3586
- payload: { workspaceId: workspace.id, action, commitMessage, mode: workspace.mode },
3587
- afterCwd: (response) => "sourceRoot" in response.structuredContent &&
3588
- typeof response.structuredContent.sourceRoot === "string"
3589
- ? response.structuredContent.sourceRoot
3590
- : undefined,
3591
- operation: async () => {
3592
- if (workspace.mode === "worktree") {
3593
- if (!commitMessage) {
3594
- throw new Error(`Managed-worktree-backed Workspace ${workspace.id} requires commitMessage when ${action === "delete" ? "deleting active work" : "closing"}.`);
3595
- }
3596
- const physicalWorkspaceIds = workspaces.workspaceIdsForPhysicalWorkspace(workspace);
3597
- const busyWorkspaceIds = physicalWorkspaceIds
3598
- .filter((id) => processSessions.activeWorkspaceIds().has(id));
3599
- if (busyWorkspaceIds.length > 0) {
3600
- throw new Error(`Cannot close this worktree-backed Workspace while Workspace processes are still running: ${busyWorkspaceIds.join(", ")}.`);
3601
- }
3602
- const startedAt = performance.now();
3603
- const retirement = await codeIntelligence.retireWorkspaceRoot(workspace.root);
3604
- let closed;
3605
- try {
3606
- closed = await workspaces.closeWorktree(workspace.id, commitMessage);
3607
- }
3608
- finally {
3609
- codeIntelligence.restoreWorkspaceRoot(retirement.root);
3610
- }
3611
- await Promise.all(physicalWorkspaceIds.map((id) => reviewCheckpoints.releaseWorkspace(id)));
3612
- if (action === "delete") {
3613
- workspaces.deleteWorkspace(workspace.id);
3614
- workspaceTasks.deleteWorkspace(workspace.id);
3615
- taskReminders.forget(workspace.id);
3616
- }
3617
- const result = [
3618
- action === "delete"
3619
- ? `Safely finalized and deleted managed-worktree Workspace ${workspace.id}.`
3620
- : `Closed managed-worktree-backed Workspace ${workspace.id}; its identity was preserved for later reopen.`,
3621
- `Merged ${closed.branch} into ${closed.targetBranch} by fast-forward.`,
3622
- `Source checkout: ${closed.sourceRoot}`,
3623
- `Commit: ${closed.commitSha}`,
3624
- closed.cleanupWarning
3625
- ? `Cleanup warning: ${closed.cleanupWarning}`
3626
- : "The managed worktree directory and branch were removed.",
3627
- ].join("\n");
3628
- logToolCall(config, {
3629
- tool: toolNames.closeWorkspace,
3630
- ...workspaceLogContext(workspace, extra.sessionId),
3631
- path: closed.sourceRoot,
3632
- success: true,
3633
- durationMs: Math.round(performance.now() - startedAt),
3634
- });
3635
- return attachHookReports({
3636
- content: [textBlock(result)],
3637
- _meta: {
3638
- tool: toolNames.closeWorkspace,
3639
- card: {
3640
- workspaceId: workspace.id,
3641
- action,
3642
- mode: "worktree",
3643
- sourceRoot: closed.sourceRoot,
3644
- branch: closed.branch,
3645
- targetBranch: closed.targetBranch,
3646
- commitSha: closed.commitSha,
3647
- mergedSha: closed.mergedSha,
3648
- committed: closed.committed,
3649
- cleanupWarning: closed.cleanupWarning,
3650
- payload: { content: [textBlock(result)] },
3651
- },
3652
- },
3653
- structuredContent: {
3654
- result,
3655
- workspaceId: workspace.id,
3656
- action,
3657
- mode: "worktree",
3658
- sourceRoot: closed.sourceRoot,
3659
- branch: closed.branch,
3660
- targetBranch: closed.targetBranch,
3661
- commitSha: closed.commitSha,
3662
- mergedSha: closed.mergedSha,
3663
- committed: closed.committed,
3664
- cleanupWarning: closed.cleanupWarning,
3665
- },
3666
- }, closed.hookReports);
3667
- }
3668
- if (commitMessage !== undefined) {
3669
- throw new Error("close_workspace commitMessage is only valid for managed-worktree-backed workspaces.");
3670
- }
3671
- const checkoutWorkspaceId = workspace.id;
3672
- if (processSessions.activeWorkspaceIds().has(checkoutWorkspaceId)) {
3673
- throw new Error(`Workspace ${checkoutWorkspaceId} still owns a running process. Poll, interrupt, or wait for it before closing this workspace.`);
3674
- }
3675
- workspaces.closeWorkspace(checkoutWorkspaceId);
3676
- await reviewCheckpoints.releaseWorkspace(checkoutWorkspaceId);
3677
- const result = `Closed checkout-backed Workspace ${checkoutWorkspaceId}; its ForgeRelay identity was preserved for later reopen. Physical project files were not removed.`;
3678
- return {
3679
- content: [textBlock(result)],
3680
- _meta: {
3681
- tool: toolNames.closeWorkspace,
3682
- card: {
3683
- workspaceId: checkoutWorkspaceId,
3684
- action: "close",
3685
- mode: "checkout",
3686
- payload: { content: [textBlock(result)] },
3687
- },
3688
- },
3689
- structuredContent: {
3690
- result,
3691
- workspaceId: checkoutWorkspaceId,
3692
- action: "close",
3693
- mode: "checkout",
3694
- },
3695
- };
3696
- },
3697
- });
3698
- workspacePanelStates.delete(workspace.id);
3699
- return response;
3700
- });
3701
- registerAppTool(server, toolNames.read, {
3702
- title: "Read file",
3703
- description: toolDescriptions.read,
3704
- inputSchema: {
3705
- workspaceId: z
3706
- .string()
3707
- .describe("Workspace identifier returned by open_workspace."),
3708
- member: z
3709
- .string()
3710
- .optional()
3711
- .describe("Required for Composite member-scoped file reads. Omit only when reading an advertised Composite-owned capability guide."),
3712
- path: z
3713
- .string()
3714
- .optional()
3715
- .describe(config.skillsEnabled
3716
- ? "One file path to read, relative to the workspace root or absolute inside the OS temp directory. Load an available skill with skills://<name>; after loading it, read files in that skill with skills://<name>/<relative-path>. Advertised capability-guide paths from open_workspace are also readable. Use exactly one of path or paths."
3717
- : "One file path to read, relative to the workspace root or absolute inside the OS temp directory. May also be an advertised capability-guide path from open_workspace. Use exactly one of path or paths."),
3718
- paths: z
3719
- .array(z.string())
3720
- .min(1)
3721
- .max(100)
3722
- .optional()
3723
- .describe("Multiple file paths to read in one call. Uses the same offset/limit for every file. Use exactly one of path or paths."),
3724
- offset: z
3725
- .number()
3726
- .int()
3727
- .positive()
3728
- .optional()
3729
- .describe("1-indexed line number to start reading from."),
3730
- limit: z
3731
- .number()
3732
- .int()
3733
- .positive()
3734
- .optional()
3735
- .describe("Maximum number of lines to read."),
3736
- },
3737
- outputSchema: resultOutputSchema({
3738
- agentsFiles: z.array(workspaceAgentsFileOutputSchema).optional(),
3739
- results: z.array(z.object({
3740
- path: z.string(),
3741
- status: z.enum(["done", "error"]),
3742
- result: z.string(),
3743
- })).optional(),
3744
- files: z.number().int().nonnegative().optional(),
3745
- failed: z.number().int().nonnegative().optional(),
3746
- }),
3747
- ...toolWidgetDescriptorMeta(config, "read"),
3748
- annotations: { readOnlyHint: true },
3749
- }, async ({ workspaceId, member, path, paths, offset, limit }, extra) => {
3750
- if ((path === undefined) === (paths === undefined)) {
3751
- throw new Error("read requires exactly one of path or paths.");
3752
- }
3753
- if (compositeWorkspaces.has(workspaceId) && member === undefined && path !== undefined) {
3754
- const guide = compositeTaskGuides.find((candidate) => formatPathForPrompt(candidate.filePath) === path || candidate.filePath === path);
3755
- if (guide) {
3756
- if (!compositeWorkspaces.isActive(workspaceId)) {
3757
- throw new Error(`Composite Workspace ${workspaceId} is closed. Reopen it with open_workspace before use.`);
3758
- }
3759
- const startedAt = performance.now();
3760
- const raw = readFileSync(guide.filePath, "utf8");
3761
- const start = (offset ?? 1) - 1;
3762
- const end = limit === undefined ? undefined : start + limit;
3763
- const result = raw.split("\n").slice(start, end).join("\n");
3764
- logToolCall(config, {
3765
- tool: toolNames.read,
3766
- path: formatPathForPrompt(guide.filePath),
3767
- success: true,
3768
- durationMs: Math.round(performance.now() - startedAt),
3769
- });
3770
- return {
3771
- content: [textBlock(result)],
3772
- structuredContent: { result },
3773
- };
3774
- }
3775
- }
3776
- const target = resolveExecutionTarget(workspaceId, member);
3777
- const executionWorkspaceId = target.executionWorkspaceId;
3778
- const executionContext = await prepareExecutionContext(target, extra._meta, extra.signal, extra.sessionId);
3779
- if (remoteWorkspaces.has(executionWorkspaceId)) {
3780
- return presentSemanticWorkResult(await remoteWorkspaces.read(executionWorkspaceId, { path, paths, offset, limit }, hostScopeIdFor(extra._meta, extra.sessionId)), target);
3781
- }
3782
- if (path !== undefined) {
3783
- return presentSemanticWorkResult(await coreOperations.read({ workspaceId: executionWorkspaceId, path, offset, limit }, executionContext), target);
3784
- }
3785
- const workspace = workspaces.getWorkspace(executionWorkspaceId);
3786
- let response;
3787
- await runActivityTool(activityLifecycle, workspace, hostScopeIdFor(extra._meta, extra.sessionId), toolNames.read, activityRequestFor({ workspaceId: executionWorkspaceId, paths, offset, limit }, executionContext), async (parentContext) => {
3788
- const execution = await executeBulkRead({
3789
- paths: paths,
3790
- signal: extra.signal,
3791
- run: (childPath) => coreOperations.read({ workspaceId: executionWorkspaceId, path: childPath, offset, limit }, {
3792
- ...executionContext,
3793
- parentActivityId: parentContext.activityId,
3794
- turnId: parentContext.turnId,
3795
- }),
3796
- isError: toolResultIsError,
3797
- resultText: toolResultText,
3798
- });
3799
- const content = execution.children.flatMap((child) => [
3800
- textBlock(`--- ${child.path} · ${child.status} ---`),
3801
- ...(child.response
3802
- ? toolResultContent(child.response)
3803
- : [textBlock(child.result)]),
3804
- ]);
3805
- const seenAgentPaths = new Set();
3806
- const agentsFiles = execution.children.flatMap((child) => child.response ? toolResultAgentsFiles(child.response) : []).filter((file) => {
3807
- if (seenAgentPaths.has(file.path))
3808
- return false;
3809
- seenAgentPaths.add(file.path);
3810
- return true;
3811
- });
3812
- response = {
3813
- content,
3814
- structuredContent: {
3815
- result: contentText(content),
3816
- results: execution.children.map(({ path: childPath, status, result }) => ({
3817
- path: childPath,
3818
- status,
3819
- result,
3820
- })),
3821
- files: execution.children.length,
3822
- failed: execution.failed,
3823
- ...(agentsFiles.length > 0 ? { agentsFiles } : {}),
3824
- },
3825
- };
3826
- return {
3827
- childCount: execution.children.length,
3828
- succeeded: execution.succeeded,
3829
- failed: execution.failed,
3830
- };
3831
- }, (summary) => summary.failed > 0
3832
- ? { type: "failed", error: `${summary.failed} of ${summary.childCount} child Reads failed.` }
3833
- : { type: "succeeded" }, activityRelationFor(executionContext));
3834
- if (!response)
3835
- throw new Error("Bulk Read completed without a response.");
3836
- return presentSemanticWorkResult(response, target);
3837
- });
3838
- if (config.toolMode !== "codex") {
3839
- registerAppTool(server, toolNames.write, {
3840
- title: "Write file",
3841
- description: toolDescriptions.write,
3842
- inputSchema: {
3843
- workspaceId: z
3844
- .string()
3845
- .describe("Workspace identifier returned by open_workspace."),
3846
- member: z.string().optional().describe("Required for a Composite Workspace; explicit member name that owns this operation."),
3847
- path: z
3848
- .string()
3849
- .describe("File path to write, relative to the workspace root or absolute inside the OS temp directory."),
3850
- content: z.string().describe("Complete new file content."),
3851
- },
3852
- outputSchema: resultOutputSchema(),
3853
- ...toolWidgetDescriptorMeta(config, "write"),
3854
- annotations: WRITE_TOOL_ANNOTATIONS,
3855
- }, async ({ workspaceId, member, ...input }, extra) => {
3856
- const target = resolveExecutionTarget(workspaceId, member);
3857
- const executionWorkspaceId = target.executionWorkspaceId;
3858
- const executionContext = await prepareExecutionContext(target, extra._meta, extra.signal, extra.sessionId);
3859
- if (remoteWorkspaces.has(executionWorkspaceId)) {
3860
- return presentSemanticWorkResult(await remoteWorkspaces.write(executionWorkspaceId, input, hostScopeIdFor(extra._meta, extra.sessionId)), target);
3861
- }
3862
- return presentSemanticWorkResult(await coreOperations.write({ workspaceId: executionWorkspaceId, ...input }, executionContext), target);
3863
- });
3864
- registerAppTool(server, toolNames.edit, {
3865
- title: "Edit file",
3866
- description: toolDescriptions.edit,
3867
- inputSchema: {
3868
- workspaceId: z
3869
- .string()
3870
- .describe("Workspace identifier returned by open_workspace."),
3871
- member: z.string().optional().describe("Required for a Composite Workspace; explicit member name that owns this operation."),
3872
- path: z
3873
- .string()
3874
- .optional()
3875
- .describe("One file path to edit. Use exactly one of path or paths."),
3876
- paths: z
3877
- .array(z.string())
3878
- .min(1)
3879
- .max(100)
3880
- .optional()
3881
- .describe("Multiple file paths to edit with the same edits. Use exactly one of path or paths."),
3882
- edits: z
3883
- .array(z.object({
3884
- oldText: z
3885
- .string()
3886
- .describe("Exact text to replace. Must match uniquely in the original file."),
3887
- newText: z.string().describe("Replacement text."),
3888
- }))
3889
- .min(1),
3890
- },
3891
- outputSchema: resultOutputSchema({
3892
- status: z.enum(["applied", "partial"]),
3893
- results: z.array(z.object({
3894
- path: z.string(),
3895
- status: z.enum(["done", "error", "unexecuted"]),
3896
- result: z.string().optional(),
3897
- })).optional(),
3898
- files: z.number().int().nonnegative().optional(),
3899
- completed: z.number().int().nonnegative().optional(),
3900
- failed: z.number().int().nonnegative().optional(),
3901
- unexecuted: z.number().int().nonnegative().optional(),
3902
- }),
3903
- ...toolWidgetDescriptorMeta(config, "edit"),
3904
- annotations: EDIT_TOOL_ANNOTATIONS,
3905
- }, async ({ workspaceId, member, path, paths, edits }, extra) => {
3906
- if ((path === undefined) === (paths === undefined)) {
3907
- throw new Error("edit requires exactly one of path or paths.");
3908
- }
3909
- const target = resolveExecutionTarget(workspaceId, member);
3910
- const executionWorkspaceId = target.executionWorkspaceId;
3911
- const executionContext = await prepareExecutionContext(target, extra._meta, extra.signal, extra.sessionId);
3912
- if (remoteWorkspaces.has(executionWorkspaceId)) {
3913
- return presentSemanticWorkResult(await remoteWorkspaces.edit(executionWorkspaceId, { path, paths, edits }, hostScopeIdFor(extra._meta, extra.sessionId)), target);
3914
- }
3915
- if (path !== undefined) {
3916
- return presentSemanticWorkResult(await coreOperations.edit({ workspaceId: executionWorkspaceId, path, edits }, executionContext), target);
3917
- }
3918
- return presentSemanticWorkResult(await nativeBulkMutations.edit({ workspaceId: executionWorkspaceId, paths: paths, edits }, executionContext), target);
3919
- });
3920
- }
3921
- registerAppTool(server, toolNames.rename, {
3922
- title: "Rename path",
3923
- description: toolDescriptions.rename,
3924
- inputSchema: {
3925
- workspaceId: z.string().describe("Workspace identifier returned by open_workspace."),
3926
- member: z.string().optional().describe("Required for a Composite Workspace; explicit member name that owns this operation."),
3927
- path: z.string().describe("Source file or directory path relative to the workspace root, or absolute inside the OS temp directory."),
3928
- newPath: z.string().describe("Destination path relative to the workspace root, or absolute inside the OS temp directory. The destination must not already exist."),
3929
- },
3930
- outputSchema: resultOutputSchema({
3931
- status: z.literal("renamed"),
3932
- path: z.string(),
3933
- newPath: z.string(),
3934
- }),
3935
- ...toolWidgetDescriptorMeta(config, "edit"),
3936
- annotations: EDIT_TOOL_ANNOTATIONS,
3937
- }, async ({ workspaceId, member, path, newPath }, extra) => {
3938
- const target = resolveExecutionTarget(workspaceId, member);
3939
- const executionWorkspaceId = target.executionWorkspaceId;
3940
- const executionContext = await prepareExecutionContext(target, extra._meta, extra.signal, extra.sessionId);
3941
- if (remoteWorkspaces.has(executionWorkspaceId)) {
3942
- return presentSemanticWorkResult(await remoteWorkspaces.rename(executionWorkspaceId, { path, newPath }, hostScopeIdFor(extra._meta, extra.sessionId)), target);
3943
- }
3944
- return presentSemanticWorkResult(await coreOperations.rename({ workspaceId: executionWorkspaceId, path, newPath }, executionContext), target);
423
+ registerFilesystemTools({
424
+ server,
425
+ config,
426
+ workspaces,
427
+ compositeWorkspaces,
428
+ compositeTaskGuides,
429
+ remoteWorkspaces,
430
+ coreOperations,
431
+ nativeBulkMutations,
432
+ activityLifecycle,
433
+ hooks,
434
+ toolDescriptions,
435
+ resolveExecutionTarget,
436
+ prepareExecutionContext,
437
+ presentSemanticWorkResult,
438
+ hostScopeIdFor,
3945
439
  });
3946
- registerAppTool(server, toolNames.delete, {
3947
- title: "Delete path",
3948
- description: toolDescriptions.delete,
3949
- inputSchema: {
3950
- workspaceId: z.string().describe("Workspace identifier returned by open_workspace."),
3951
- member: z.string().optional().describe("Required for a Composite Workspace; explicit member name that owns this operation."),
3952
- path: z.string().optional().describe("One file or directory path to delete. Use exactly one of path or paths."),
3953
- paths: z
3954
- .array(z.string())
3955
- .min(1)
3956
- .max(100)
3957
- .optional()
3958
- .describe("Multiple paths to delete in one call. Use exactly one of path or paths."),
3959
- recursive: z.boolean().optional().describe("Delete non-empty directory trees. Defaults to false and applies to every bulk target."),
440
+ registerProcessTools({
441
+ server,
442
+ config,
443
+ workspaces,
444
+ processSessions,
445
+ hooks,
446
+ activityLifecycle,
447
+ bashOutputStore,
448
+ shellRun: (input, context) => coreOperations.shellRun(input, context),
449
+ routing: {
450
+ resolve: resolveExecutionTarget,
451
+ prepare: prepareExecutionContext,
452
+ present: presentExecutionResult,
453
+ presentSemantic: presentSemanticWorkResult,
454
+ isRemote: (workspaceId) => remoteWorkspaces.has(workspaceId),
455
+ bashRemote: (workspaceId, input, conversationScopeId) => remoteWorkspaces.bash(workspaceId, input, conversationScopeId),
456
+ execCommandRemote: (workspaceId, input, conversationScopeId) => remoteWorkspaces.execCommand(workspaceId, input, conversationScopeId),
457
+ writeStdinRemote: (workspaceId, input, conversationScopeId) => remoteWorkspaces.writeStdin(workspaceId, input, conversationScopeId),
458
+ hostScopeIdFor,
459
+ },
460
+ descriptions: {
461
+ shell: toolDescriptions.shell,
462
+ shellCommand: toolDescriptions.shellCommand,
3960
463
  },
3961
- outputSchema: resultOutputSchema({
3962
- status: z.enum(["deleted", "partial"]),
3963
- path: z.string().optional(),
3964
- recursive: z.boolean().optional(),
3965
- results: z.array(z.object({
3966
- path: z.string(),
3967
- status: z.enum(["done", "error", "unexecuted"]),
3968
- result: z.string().optional(),
3969
- })).optional(),
3970
- paths: z.number().int().nonnegative().optional(),
3971
- completed: z.number().int().nonnegative().optional(),
3972
- failed: z.number().int().nonnegative().optional(),
3973
- unexecuted: z.number().int().nonnegative().optional(),
3974
- }),
3975
- ...toolWidgetDescriptorMeta(config, "edit"),
3976
- annotations: EDIT_TOOL_ANNOTATIONS,
3977
- }, async ({ workspaceId, member, path, paths, recursive }, extra) => {
3978
- if ((path === undefined) === (paths === undefined)) {
3979
- throw new Error("delete requires exactly one of path or paths.");
3980
- }
3981
- const target = resolveExecutionTarget(workspaceId, member);
3982
- const executionWorkspaceId = target.executionWorkspaceId;
3983
- const executionContext = await prepareExecutionContext(target, extra._meta, extra.signal, extra.sessionId);
3984
- if (remoteWorkspaces.has(executionWorkspaceId)) {
3985
- return presentSemanticWorkResult(await remoteWorkspaces.delete(executionWorkspaceId, { path, paths, recursive }, hostScopeIdFor(extra._meta, extra.sessionId)), target);
3986
- }
3987
- if (path !== undefined) {
3988
- return presentSemanticWorkResult(await coreOperations.delete({ workspaceId: executionWorkspaceId, path, recursive }, executionContext), target);
3989
- }
3990
- return presentSemanticWorkResult(await nativeBulkMutations.delete({ workspaceId: executionWorkspaceId, paths: paths, recursive }, executionContext), target);
3991
- });
3992
- if (config.toolMode === "codex") {
3993
- registerAppTool(server, "apply_patch", {
3994
- title: "Apply patch",
3995
- description: toolDescriptions.applyPatch,
3996
- inputSchema: {
3997
- workspaceId: z
3998
- .string()
3999
- .describe("Workspace identifier returned by open_workspace."),
4000
- member: z.string().optional().describe("Required for a Composite Workspace; explicit member name that owns this patch."),
4001
- patch: z
4002
- .string()
4003
- .describe("Patch text enclosed by *** Begin Patch and *** End Patch markers."),
4004
- },
4005
- outputSchema: resultOutputSchema({
4006
- additions: z.number(),
4007
- removals: z.number(),
4008
- files: z.array(z.object({
4009
- path: z.string(),
4010
- previousPath: z.string().optional(),
4011
- operation: z.enum(["add", "update", "delete", "move"]),
4012
- })),
4013
- }),
4014
- ...toolWidgetDescriptorMeta(config, "edit"),
4015
- annotations: EDIT_TOOL_ANNOTATIONS,
4016
- }, async ({ workspaceId, member, patch }, extra) => {
4017
- const target = resolveExecutionTarget(workspaceId, member);
4018
- const executionWorkspaceId = target.executionWorkspaceId;
4019
- const executionContext = await prepareExecutionContext(target, extra._meta, extra.signal, extra.sessionId);
4020
- if (remoteWorkspaces.has(executionWorkspaceId)) {
4021
- return presentSemanticWorkResult(await remoteWorkspaces.applyPatch(executionWorkspaceId, { patch }, hostScopeIdFor(extra._meta, extra.sessionId)), target);
4022
- }
4023
- const workspace = workspaces.getWorkspace(executionWorkspaceId);
4024
- return runActivityToolWithHooks(activityLifecycle, hooks, workspace, hostScopeIdFor(extra._meta, extra.sessionId), activityRequestFor({ workspaceId: executionWorkspaceId, patch }, executionContext), {
4025
- signal: extra.signal,
4026
- tool: "apply_patch",
4027
- invocation: workspaceHookInvocation(workspace),
4028
- payload: { patchBytes: Buffer.byteLength(patch) },
4029
- changedPaths: (response) => Array.from(new Set(response.structuredContent.files.flatMap((file) => [file.previousPath, file.path])
4030
- .filter((path) => Boolean(path)))),
4031
- operation: async () => {
4032
- const startedAt = performance.now();
4033
- const applied = await applyPatch(workspace.root, patch, [tmpdir()]);
4034
- const paths = applied.files.map((file) => file.path).join(", ");
4035
- const result = `Applied patch to ${applied.files.length} file(s): ${paths}`;
4036
- const content = [textBlock(result)];
4037
- const displayPath = applied.files.length === 1
4038
- ? applied.files[0]?.path
4039
- : `${applied.files.length} files`;
4040
- logToolCall(config, {
4041
- tool: "apply_patch",
4042
- ...workspaceLogContext(workspace, extra.sessionId),
4043
- path: displayPath,
4044
- success: true,
4045
- durationMs: Math.round(performance.now() - startedAt),
4046
- });
4047
- return {
4048
- content,
4049
- _meta: {
4050
- tool: "apply_patch",
4051
- card: {
4052
- workspaceId: executionWorkspaceId,
4053
- path: displayPath,
4054
- summary: {
4055
- files: applied.files.length,
4056
- additions: applied.additions,
4057
- removals: applied.removals,
4058
- },
4059
- files: applied.files,
4060
- payload: { patch: applied.patch },
4061
- },
4062
- },
4063
- structuredContent: {
4064
- result,
4065
- additions: applied.additions,
4066
- removals: applied.removals,
4067
- files: applied.files,
4068
- },
4069
- };
4070
- },
4071
- }, activityRelationFor(executionContext)).then((result) => presentSemanticWorkResult(result, target));
4072
- });
4073
- }
4074
- if (config.toolMode !== "codex") {
4075
- registerAppTool(server, toolNames.shell, {
4076
- title: "Bash",
4077
- description: toolDescriptions.shell,
4078
- inputSchema: {
4079
- workspaceId: z
4080
- .string()
4081
- .describe("Workspace identifier returned by open_workspace."),
4082
- member: z.string().optional().describe("Required for a Composite Workspace; explicit member name that owns this process operation."),
4083
- action: z
4084
- .enum(["run", "process", "output"])
4085
- .optional()
4086
- .describe("Defaults to run. Use process with a returned processId to poll/interact, or output with outputId to retrieve complete durable output."),
4087
- command: z
4088
- .string()
4089
- .optional()
4090
- .describe(`${toolDescriptions.shellCommand} Required for action=run.`),
4091
- processId: z
4092
- .number()
4093
- .int()
4094
- .positive()
4095
- .optional()
4096
- .describe("Process identifier returned by a previous bash action=run call. Required for action=process."),
4097
- outputId: z
4098
- .string()
4099
- .optional()
4100
- .describe("Stable output identifier returned by a Bash run. Required for action=output."),
4101
- input: z
4102
- .string()
4103
- .optional()
4104
- .describe("Characters to write for action=process. Omit to poll/wait without input."),
4105
- interrupt: z
4106
- .boolean()
4107
- .optional()
4108
- .describe("For action=process, send SIGINT to the process. Cannot be combined with input."),
4109
- tty: z
4110
- .boolean()
4111
- .optional()
4112
- .describe("For action=run, allocate a pseudo-terminal for interactive commands. Defaults to false."),
4113
- columns: z
4114
- .number()
4115
- .int()
4116
- .min(1)
4117
- .max(1_000)
4118
- .optional()
4119
- .describe("Initial PTY width for action=run, or resize width for action=process."),
4120
- rows: z
4121
- .number()
4122
- .int()
4123
- .min(1)
4124
- .max(1_000)
4125
- .optional()
4126
- .describe("Initial PTY height for action=run, or resize height for action=process."),
4127
- workingDirectory: z
4128
- .string()
4129
- .optional()
4130
- .describe("For action=run, working directory relative to the workspace root. Defaults to the workspace root."),
4131
- yieldTimeMs: z
4132
- .number()
4133
- .int()
4134
- .min(0)
4135
- .max(300_000)
4136
- .optional()
4137
- .describe("Maximum feedback wait, not a minimum delay: if the process finishes sooner, the call returns immediately. For long-running commands or wait-only action=process calls, set a long window near the Host request deadline (60000ms when supported) instead of repeated short polling. For action=run, use 0 for immediate background handoff; otherwise defaults to 10000ms. For action=process, wait-only calls default to 5000ms and interaction to 250ms."),
4138
- timeoutMs: z
4139
- .number()
4140
- .int()
4141
- .min(1)
4142
- .max(86_400_000)
4143
- .optional()
4144
- .describe("For action=run, total execution timeout from process start. On expiry ForgeRelay terminates the process. Omit for no ForgeRelay execution deadline."),
4145
- maxOutputTokens: z
4146
- .number()
4147
- .int()
4148
- .positive()
4149
- .max(100_000)
4150
- .optional()
4151
- .describe("Approximate output token budget. Defaults to 10000."),
4152
- },
4153
- outputSchema: processOutputSchema(),
4154
- ...toolWidgetDescriptorMeta(config, "shell"),
4155
- annotations: SHELL_TOOL_ANNOTATIONS,
4156
- }, async ({ workspaceId, member, action = "run", command, processId, outputId, input, interrupt, tty, columns, rows, workingDirectory, yieldTimeMs, timeoutMs, maxOutputTokens, }, extra) => {
4157
- const target = resolveExecutionTarget(workspaceId, member);
4158
- const executionWorkspaceId = target.executionWorkspaceId;
4159
- const executionContext = await prepareExecutionContext(target, extra._meta, extra.signal, extra.sessionId);
4160
- if (remoteWorkspaces.has(executionWorkspaceId)) {
4161
- const response = await remoteWorkspaces.bash(executionWorkspaceId, {
4162
- action,
4163
- ...(command !== undefined ? { command } : {}),
4164
- ...(processId !== undefined ? { processId } : {}),
4165
- ...(outputId !== undefined ? { outputId } : {}),
4166
- ...(input !== undefined ? { input } : {}),
4167
- ...(interrupt !== undefined ? { interrupt } : {}),
4168
- ...(tty !== undefined ? { tty } : {}),
4169
- ...(columns !== undefined ? { columns } : {}),
4170
- ...(rows !== undefined ? { rows } : {}),
4171
- ...(workingDirectory !== undefined ? { workingDirectory } : {}),
4172
- ...(yieldTimeMs !== undefined ? { yieldTimeMs } : {}),
4173
- ...(timeoutMs !== undefined ? { timeoutMs } : {}),
4174
- ...(maxOutputTokens !== undefined ? { maxOutputTokens } : {}),
4175
- }, hostScopeIdFor(extra._meta, extra.sessionId));
4176
- return action === "run"
4177
- ? presentSemanticWorkResult(response, target)
4178
- : presentExecutionResult(response, target);
4179
- }
4180
- const workspace = workspaces.getWorkspace(executionWorkspaceId);
4181
- if (action === "run") {
4182
- if (!command)
4183
- throw new Error("bash action=run requires command.");
4184
- if (processId !== undefined || outputId !== undefined || input !== undefined || interrupt !== undefined) {
4185
- throw new Error("bash action=run does not accept processId, outputId, input, or interrupt.");
4186
- }
4187
- return presentSemanticWorkResult(await coreOperations.shellRun({
4188
- workspaceId: executionWorkspaceId,
4189
- command,
4190
- surface: "bash",
4191
- tty,
4192
- columns,
4193
- rows,
4194
- workingDirectory,
4195
- yieldTimeMs,
4196
- timeoutMs,
4197
- maxOutputTokens,
4198
- }, executionContext), target);
4199
- }
4200
- if (action === "output") {
4201
- if (!outputId)
4202
- throw new Error("bash action=output requires outputId.");
4203
- if (command !== undefined || processId !== undefined || input !== undefined || interrupt !== undefined ||
4204
- tty !== undefined || columns !== undefined || rows !== undefined || workingDirectory !== undefined ||
4205
- yieldTimeMs !== undefined || timeoutMs !== undefined || maxOutputTokens !== undefined) {
4206
- throw new Error("bash action=output accepts only workspaceId and outputId.");
4207
- }
4208
- return runToolWithHooks(hooks, {
4209
- signal: extra.signal,
4210
- tool: toolNames.shell,
4211
- invocation: workspaceHookInvocation(workspace),
4212
- payload: { action, outputId },
4213
- operation: async () => durableOutputResponse(toolNames.shell, executionWorkspaceId, readWorkspaceBashOutput(bashOutputStore, executionWorkspaceId, outputId)),
4214
- }).then((result) => presentExecutionResult(result, target));
4215
- }
4216
- if (outputId !== undefined)
4217
- throw new Error("bash action=process does not accept outputId.");
4218
- if (command !== undefined || workingDirectory !== undefined || tty !== undefined || timeoutMs !== undefined) {
4219
- throw new Error("bash action=process does not accept command, workingDirectory, tty, or timeoutMs.");
4220
- }
4221
- if (processId === undefined)
4222
- throw new Error("bash action=process requires processId.");
4223
- if (interrupt && input !== undefined) {
4224
- throw new Error("bash action=process cannot combine interrupt with input.");
4225
- }
4226
- return runToolWithHooks(hooks, {
4227
- signal: extra.signal,
4228
- tool: toolNames.shell,
4229
- invocation: workspaceHookInvocation(workspace),
4230
- payload: {
4231
- action,
4232
- processId,
4233
- inputLength: input?.length ?? 0,
4234
- interrupt: interrupt ?? false,
4235
- columns,
4236
- rows,
4237
- },
4238
- isFailure: toolResultIsError,
4239
- operation: async () => {
4240
- const startedAt = performance.now();
4241
- const snapshot = await processSessions.write({
4242
- workspaceId: executionWorkspaceId,
4243
- processId,
4244
- chars: interrupt ? "\u0003" : input,
4245
- columns,
4246
- rows,
4247
- yieldTimeMs,
4248
- maxOutputTokens,
4249
- signal: extra.signal,
4250
- });
4251
- logToolCall(config, {
4252
- tool: toolNames.shell,
4253
- ...workspaceLogContext(workspace, extra.sessionId),
4254
- exitCode: snapshot.exitCode,
4255
- running: snapshot.running,
4256
- processId: snapshot.processId,
4257
- success: snapshot.running || snapshot.exitCode === 0,
4258
- durationMs: Math.round(performance.now() - startedAt),
4259
- });
4260
- const response = processToolResponse(toolNames.shell, executionWorkspaceId, snapshot, {
4261
- action,
4262
- processId,
4263
- inputLength: input?.length ?? 0,
4264
- interrupt: interrupt ?? false,
4265
- running: snapshot.running,
4266
- exitCode: snapshot.exitCode,
4267
- wallTimeMs: snapshot.wallTimeMs,
4268
- });
4269
- if (!snapshot.running) {
4270
- recordBashCompletion(activityLifecycle, bashOutputStore, snapshot.outputId);
4271
- }
4272
- return presentExecutionResult(response, target);
4273
- },
4274
- });
4275
- });
4276
- }
4277
- registerProcessTools(server, config, workspaces, processSessions, hooks, activityLifecycle, bashOutputStore, (input, context) => coreOperations.shellRun(input, context), {
4278
- resolve: resolveExecutionTarget,
4279
- prepare: prepareExecutionContext,
4280
- present: presentExecutionResult,
4281
- presentSemantic: presentSemanticWorkResult,
4282
- isRemote: (workspaceId) => remoteWorkspaces.has(workspaceId),
4283
- execCommandRemote: (workspaceId, input, conversationScopeId) => remoteWorkspaces.execCommand(workspaceId, input, conversationScopeId),
4284
- writeStdinRemote: (workspaceId, input, conversationScopeId) => remoteWorkspaces.writeStdin(workspaceId, input, conversationScopeId),
4285
- hostScopeIdFor,
4286
464
  });
4287
465
  if (ownsRemoteWorkspaces) {
4288
466
  const closeServer = server.close.bind(server);
@@ -4302,271 +480,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
4302
480
  return server;
4303
481
  }
4304
482
  export function createServer(config = loadConfig(), options = {}) {
4305
- const incomingArtifactAdapters = options.incomingArtifactAdapters
4306
- ?? [createOpenAIIncomingArtifactAdapter()];
4307
- const allowedHosts = config.allowedHosts.includes("*")
4308
- ? undefined
4309
- : Array.from(new Set([config.host, ...config.allowedHosts]));
4310
- const app = createMcpExpressApp({
4311
- host: config.host,
4312
- ...(allowedHosts ? { allowedHosts } : {}),
4313
- });
4314
- const transports = new McpTransportRegistry({
4315
- maxTransports: MAX_MCP_TRANSPORT_SESSIONS,
4316
- });
4317
- const mcpUrl = publicEndpointUrl(config.publicBaseUrl, "mcp");
4318
- const resourceServerUrl = resourceUrlFromServerUrl(mcpUrl);
4319
- const oauthProvider = new SingleUserOAuthProvider(config.oauth, mcpUrl, config.stateDir);
4320
- const bearerAuth = requireBearerAuth({
4321
- verifier: oauthProvider,
4322
- requiredScopes: [config.oauth.scopes[0] ?? "devspace"],
4323
- resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(resourceServerUrl),
4324
- });
4325
- const workspaceStore = createWorkspaceStore(config.stateDir);
4326
- const workspaces = new WorkspaceRegistry(config, workspaceStore);
4327
- const sharedRemoteWorkspaces = new RemoteWorkspaceRelay(config.configDir, config.stateDir);
4328
- const sharedCompositeWorkspaces = new CompositeWorkspaceRegistry(config.stateDir);
4329
- const sharedWorkspaceTasks = new WorkspaceTaskStore(config.stateDir);
4330
- const sharedTaskReminders = new WorkspaceTaskReminderTracker(config.taskReminderInterval, sharedWorkspaceTasks);
4331
- const activityAuditStore = new ActivityAuditStore(config.stateDir);
4332
- const bashOutputStore = new BashOutputStore(config.stateDir);
4333
- const hostTurnStore = new HostTurnStore(config.stateDir);
4334
- const activityQueries = new ActivityQueryService(hostTurnStore, activityAuditStore, bashOutputStore);
4335
- const activityLifecycle = new ActivityLifecycle(activityAuditStore, {
4336
- turnIdForConversation: (conversationScopeId, workspaceId) => activityQueries.currentTurnId(conversationScopeId, workspaceId),
4337
- });
4338
- const reviewCheckpoints = createReviewCheckpointManager();
4339
- const processSessions = new ProcessManager({ outputAudit: bashOutputStore });
4340
- const codeIntelligence = new CodeIntelligenceManager(config);
4341
- const subagentProviders = config.subagents
4342
- ? getSubagentProviderAvailabilitySnapshot()
4343
- : [];
4344
- const logTransportCloseResults = (reason, results) => {
4345
- let closedCount = 0;
4346
- for (const result of results) {
4347
- if (result.error) {
4348
- logEvent(config.logging, "warn", "mcp_transport_session_close_failed", {
4349
- reason,
4350
- transportSessionIdPrefix: transportSessionIdPrefix(result.transportSessionId),
4351
- error: result.error instanceof Error
4352
- ? result.error.message
4353
- : String(result.error),
4354
- });
4355
- continue;
4356
- }
4357
- closedCount += 1;
4358
- if (reason !== "server_shutdown") {
4359
- logEvent(config.logging, "debug", "mcp_transport_session_closed", {
4360
- reason,
4361
- transportSessionIdPrefix: transportSessionIdPrefix(result.transportSessionId),
4362
- });
4363
- }
4364
- }
4365
- if (reason === "server_shutdown" && closedCount > 0) {
4366
- logEvent(config.logging, "debug", "mcp_transport_sessions_closed", {
4367
- reason,
4368
- count: closedCount,
4369
- });
4370
- }
4371
- };
4372
- const logRuntimeResources = () => {
4373
- const memory = process.memoryUsage();
4374
- const processStats = processSessions.stats();
4375
- const codeStats = codeIntelligence.stats();
4376
- logEvent(config.logging, "debug", "runtime_resources", {
4377
- rssBytes: memory.rss,
4378
- heapUsedBytes: memory.heapUsed,
4379
- heapTotalBytes: memory.heapTotal,
4380
- externalBytes: memory.external,
4381
- arrayBuffersBytes: memory.arrayBuffers,
4382
- mcpTransports: transports.size,
4383
- processesTotal: processStats.total,
4384
- processesRunning: processStats.running,
4385
- processesCompleted: processStats.completed,
4386
- cachedWorkspaces: workspaces.cachedWorkspaceCount,
4387
- reviewStates: reviewCheckpoints.stateCount,
4388
- languageServices: codeStats.servicesTotal,
4389
- languageServicesActive: codeStats.servicesActive,
4390
- languageProcessesRunning: codeStats.processesRunning,
4391
- languageOperationsInFlight: codeStats.operationsInFlight,
4392
- languageRequestsActive: codeStats.semanticRequestsActive,
4393
- languageRequestsQueued: codeStats.semanticRequestsQueued,
4394
- languageOpenDocuments: codeStats.openDocuments,
4395
- languageDiagnosticSnapshots: codeStats.diagnosticSnapshots,
4396
- languageDiagnosticsRetained: codeStats.diagnosticsRetained,
4397
- languageStderrBytes: codeStats.stderrBytes,
4398
- languagePendingCreations: codeStats.pendingCreations,
4399
- languageCrashCooldowns: codeStats.crashCooldowns,
4400
- languageInvalidatedServices: codeStats.invalidatedServices,
4401
- languageRetiredWorkspaceRoots: codeStats.retiredWorkspaceRoots,
4402
- });
4403
- };
4404
- const transportCleanupTimer = setInterval(() => {
4405
- void transports
4406
- .closeIdle(MCP_TRANSPORT_IDLE_TIMEOUT_MS)
4407
- .then((results) => logTransportCloseResults("idle_timeout", results))
4408
- .finally(logRuntimeResources);
4409
- }, MCP_TRANSPORT_CLEANUP_INTERVAL_MS);
4410
- transportCleanupTimer.unref();
4411
- logRuntimeResources();
4412
- if (config.logging.trustProxy) {
4413
- app.set("trust proxy", 1);
4414
- }
4415
- app.use((req, res, next) => {
4416
- const requestId = randomUUID();
4417
- const startedAt = performance.now();
4418
- res.locals.requestId = requestId;
4419
- res.on("finish", () => {
4420
- const path = requestPath(req);
4421
- if (!config.logging.requests)
4422
- return;
4423
- if (!config.logging.assets && path.startsWith("/mcp-app-assets"))
4424
- return;
4425
- logEvent(config.logging, "info", "http_request", {
4426
- requestId,
4427
- method: req.method,
4428
- path,
4429
- status: res.statusCode,
4430
- durationMs: Math.round(performance.now() - startedAt),
4431
- ...requestLogFields(req, config),
4432
- });
4433
- });
4434
- next();
4435
- });
4436
- app.use(createForgeRelayAuthRouter({
4437
- provider: oauthProvider,
4438
- cliAuthenticationProvider: oauthProvider,
4439
- instanceId: config.instanceId,
4440
- issuerUrl: new URL(config.publicBaseUrl),
4441
- resourceServerUrl,
4442
- scopesSupported: config.oauth.scopes,
4443
- resourceName: "ForgeRelay",
4444
- }));
4445
- app.options("/mcp-app-assets/{*asset}", (_req, res) => {
4446
- setAssetHeaders(res);
4447
- res.sendStatus(204);
4448
- });
4449
- app.use("/mcp-app-assets", express.static(uiBuildDirectory(), {
4450
- immutable: true,
4451
- maxAge: "1y",
4452
- fallthrough: false,
4453
- setHeaders: setAssetHeaders,
4454
- }));
4455
- app.get("/healthz", (_req, res) => {
4456
- res.json({ ok: true, name: "forgerelay" });
4457
- });
4458
- app.all("/mcp", async (req, res) => {
4459
- const requestId = res.locals.requestId;
4460
- const transportSessionId = req.header("mcp-session-id");
4461
- const initializeRequest = req.method === "POST" && isInitializeRequest(req.body);
4462
- await new Promise((resolve, reject) => {
4463
- bearerAuth(req, res, (error) => {
4464
- if (error)
4465
- reject(error);
4466
- else
4467
- resolve();
4468
- });
4469
- });
4470
- if (res.headersSent)
4471
- return;
4472
- if (!req.auth?.resource || !checkResourceAllowed({ requestedResource: req.auth.resource, configuredResource: resourceServerUrl })) {
4473
- logEvent(config.logging, "warn", "auth_denied", {
4474
- requestId,
4475
- method: req.method,
4476
- path: requestPath(req),
4477
- reason: "invalid_oauth_resource",
4478
- ...requestLogFields(req, config),
4479
- });
4480
- sendJsonRpcError(res, 401, -32001, "Unauthorized");
4481
- return;
4482
- }
4483
- logEvent(config.logging, "debug", "mcp_request", {
4484
- requestId,
4485
- httpMethod: req.method,
4486
- transportSessionIdPresent: Boolean(transportSessionId),
4487
- transportSessionIdPrefix: transportSessionIdPrefix(transportSessionId),
4488
- isInitialize: initializeRequest,
4489
- ...mcpRequestDebugFields(req.body),
4490
- });
4491
- try {
4492
- let transport;
4493
- if (transportSessionId) {
4494
- transport = transports.get(transportSessionId);
4495
- if (!transport) {
4496
- sendJsonRpcError(res, 404, -32000, "Unknown MCP transport session");
4497
- return;
4498
- }
4499
- }
4500
- else if (initializeRequest) {
4501
- transport = new StreamableHTTPServerTransport({
4502
- sessionIdGenerator: () => randomUUID(),
4503
- onsessioninitialized: (newTransportSessionId) => {
4504
- if (transport) {
4505
- void transports
4506
- .register(newTransportSessionId, transport)
4507
- .then((results) => logTransportCloseResults("capacity_limit", results));
4508
- }
4509
- logEvent(config.logging, "debug", "mcp_transport_session_created", {
4510
- requestId,
4511
- transportSessionIdPrefix: transportSessionIdPrefix(newTransportSessionId),
4512
- ...requestLogFields(req, config),
4513
- });
4514
- },
4515
- });
4516
- transport.onclose = () => {
4517
- const closedTransportSessionId = transport?.sessionId;
4518
- if (closedTransportSessionId && transports.remove(closedTransportSessionId)) {
4519
- logEvent(config.logging, "debug", "mcp_transport_session_closed", {
4520
- reason: "transport_close",
4521
- transportSessionIdPrefix: transportSessionIdPrefix(closedTransportSessionId),
4522
- });
4523
- }
4524
- };
4525
- const server = createMcpServer(config, workspaces, reviewCheckpoints, processSessions, subagentProviders, incomingArtifactAdapters, codeIntelligence, activityLifecycle, bashOutputStore, activityQueries, {
4526
- taskReminders: sharedTaskReminders,
4527
- remoteWorkspaces: sharedRemoteWorkspaces,
4528
- compositeWorkspaces: sharedCompositeWorkspaces,
4529
- });
4530
- await server.connect(transport);
4531
- }
4532
- else {
4533
- sendJsonRpcError(res, 400, -32000, "No valid MCP transport session");
4534
- return;
4535
- }
4536
- await transport.handleRequest(req, res, req.body);
4537
- }
4538
- catch (error) {
4539
- logEvent(config.logging, "error", "mcp_request_error", {
4540
- requestId,
4541
- error: error instanceof Error ? error.message : String(error),
4542
- });
4543
- if (!res.headersSent) {
4544
- sendJsonRpcError(res, 500, -32603, "Internal server error");
4545
- }
4546
- }
4547
- });
4548
- let closePromise;
4549
- return {
4550
- app,
4551
- config,
4552
- subagentProviders,
4553
- close: () => {
4554
- closePromise ??= (async () => {
4555
- clearInterval(transportCleanupTimer);
4556
- const results = await transports.closeAll();
4557
- logTransportCloseResults("server_shutdown", results);
4558
- await sharedRemoteWorkspaces.shutdown();
4559
- processSessions.shutdown();
4560
- await codeIntelligence.shutdown();
4561
- oauthProvider.close();
4562
- hostTurnStore.close();
4563
- bashOutputStore.close();
4564
- activityAuditStore.close();
4565
- workspaceStore.close?.();
4566
- })();
4567
- return closePromise;
4568
- },
4569
- };
483
+ return createHttpServer(config, options, createMcpServer);
4570
484
  }
4571
485
  async function isMainModule() {
4572
486
  if (!process.argv[1])