@wardby/cli 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (304) hide show
  1. package/.env.example +5 -5
  2. package/README.md +6 -1
  3. package/deploy/local/docker-compose.quickstart-coding.yml +6 -0
  4. package/dist/claude-coding-worker/driver.d.ts +12 -0
  5. package/dist/claude-coding-worker/driver.js +29 -7
  6. package/dist/claude-coding-worker/main.js +5 -1
  7. package/dist/claude-coding-worker/sdk.js +17 -1
  8. package/dist/claude-plugin/command.d.ts +61 -0
  9. package/dist/claude-plugin/command.js +376 -0
  10. package/dist/cli-help.d.ts +1 -1
  11. package/dist/cli-help.js +2 -1
  12. package/dist/cli.js +47 -3
  13. package/dist/coding/code-host.d.ts +16 -0
  14. package/dist/coding/code-host.js +60 -0
  15. package/dist/coding/mcp-token.d.ts +12 -0
  16. package/dist/coding/mcp-token.js +26 -0
  17. package/dist/coding/protocol.js +2 -0
  18. package/dist/coding/redact.js +6 -1
  19. package/dist/coding/transcript/recorder.d.ts +5 -4
  20. package/dist/coding/transcript/recorder.js +1 -1
  21. package/dist/coding-worker/debug-trace.d.ts +3 -1
  22. package/dist/coding-worker/debug-trace.js +4 -2
  23. package/dist/coding-worker/driver.js +8 -1
  24. package/dist/coding-worker/main.js +5 -1
  25. package/dist/coding-worker/mcp-config.d.ts +27 -0
  26. package/dist/coding-worker/mcp-config.js +32 -0
  27. package/dist/coding-worker/sdk.d.ts +36 -0
  28. package/dist/coding-worker/sdk.js +28 -1
  29. package/dist/coding-worker/types.d.ts +4 -0
  30. package/dist/config/mcp-call-limit.d.ts +8 -0
  31. package/dist/config/mcp-call-limit.js +18 -0
  32. package/dist/config/mcp-servers.d.ts +8 -0
  33. package/dist/config/mcp-servers.js +12 -0
  34. package/dist/config/test-simulators.d.ts +10 -0
  35. package/dist/config/test-simulators.js +28 -0
  36. package/dist/config/tool-timeout.d.ts +1 -0
  37. package/dist/config/tool-timeout.js +18 -0
  38. package/dist/core/coding-mcp.d.ts +36 -0
  39. package/dist/core/coding-mcp.js +253 -0
  40. package/dist/core/coding-repository.d.ts +46 -0
  41. package/dist/core/coding-repository.js +92 -0
  42. package/dist/core/delegation-slots.d.ts +27 -0
  43. package/dist/core/delegation-slots.js +44 -0
  44. package/dist/core/dispatch.d.ts +6 -2
  45. package/dist/core/dispatch.js +36 -11
  46. package/dist/core/error-code.js +1 -0
  47. package/dist/core/grants.d.ts +28 -3
  48. package/dist/core/grants.js +46 -4
  49. package/dist/core/host-events.js +7 -3
  50. package/dist/core/host-status.js +2 -2
  51. package/dist/core/issue-status.js +4 -3
  52. package/dist/core/mark-ready.d.ts +1 -1
  53. package/dist/core/mark-ready.js +1 -1
  54. package/dist/core/mcp-client/client.d.ts +47 -0
  55. package/dist/core/mcp-client/client.js +141 -0
  56. package/dist/core/mcp-client/jsonrpc.d.ts +15 -0
  57. package/dist/core/mcp-client/jsonrpc.js +54 -0
  58. package/dist/core/mcp-client/results.d.ts +13 -0
  59. package/dist/core/mcp-client/results.js +42 -0
  60. package/dist/core/mcp-servers.d.ts +90 -0
  61. package/dist/core/mcp-servers.js +230 -0
  62. package/dist/core/mcp-tools.d.ts +108 -0
  63. package/dist/core/mcp-tools.js +287 -0
  64. package/dist/core/merge-order-check.js +38 -19
  65. package/dist/core/notification-dispatcher.js +2 -1
  66. package/dist/core/related-pull-requests.js +26 -10
  67. package/dist/core/review-fix.js +2 -2
  68. package/dist/core/review-host-tools.js +3 -1
  69. package/dist/core/runner.d.ts +66 -16
  70. package/dist/core/runner.js +281 -73
  71. package/dist/core/secret-broker-config.d.ts +2 -0
  72. package/dist/core/secret-broker-config.js +1 -1
  73. package/dist/core/secret-guard.d.ts +5 -2
  74. package/dist/core/secret-guard.js +22 -7
  75. package/dist/core/secrets.d.ts +11 -1
  76. package/dist/core/secrets.js +27 -1
  77. package/dist/core/serializable-retry.d.ts +18 -0
  78. package/dist/core/serializable-retry.js +43 -0
  79. package/dist/core/tool-admin.d.ts +5 -1
  80. package/dist/core/tool-admin.js +7 -2
  81. package/dist/core/tool-names.d.ts +13 -0
  82. package/dist/core/tool-names.js +20 -0
  83. package/dist/core/webhooks.d.ts +4 -0
  84. package/dist/core/webhooks.js +68 -49
  85. package/dist/core/workflow-run-events.js +2 -1
  86. package/dist/generated/prisma/browser.d.ts +31 -0
  87. package/dist/generated/prisma/client.d.ts +31 -0
  88. package/dist/generated/prisma/commonInputTypes.d.ts +50 -50
  89. package/dist/generated/prisma/internal/class.d.ts +66 -0
  90. package/dist/generated/prisma/internal/class.js +4 -4
  91. package/dist/generated/prisma/internal/prismaNamespace.d.ts +530 -1
  92. package/dist/generated/prisma/internal/prismaNamespace.js +75 -2
  93. package/dist/generated/prisma/internal/prismaNamespaceBrowser.d.ts +79 -0
  94. package/dist/generated/prisma/internal/prismaNamespaceBrowser.js +75 -2
  95. package/dist/generated/prisma/models/Agent.d.ts +271 -0
  96. package/dist/generated/prisma/models/AgentMcpServer.d.ts +1412 -0
  97. package/dist/generated/prisma/models/AgentMcpServer.js +1 -0
  98. package/dist/generated/prisma/models/AgentRepository.d.ts +40 -1
  99. package/dist/generated/prisma/models/AuthUser.d.ts +1 -1
  100. package/dist/generated/prisma/models/CodingAgentProfile.d.ts +0 -7
  101. package/dist/generated/prisma/models/CodingProxySession.d.ts +119 -2
  102. package/dist/generated/prisma/models/CodingRun.d.ts +76 -1
  103. package/dist/generated/prisma/models/HostProvider.d.ts +2176 -0
  104. package/dist/generated/prisma/models/HostProvider.js +1 -0
  105. package/dist/generated/prisma/models/HostProviderCredential.d.ts +1285 -0
  106. package/dist/generated/prisma/models/HostProviderCredential.js +1 -0
  107. package/dist/generated/prisma/models/HostProviderOperation.d.ts +1470 -0
  108. package/dist/generated/prisma/models/HostProviderOperation.js +1 -0
  109. package/dist/generated/prisma/models/HostProviderTrustedAccount.d.ts +1145 -0
  110. package/dist/generated/prisma/models/HostProviderTrustedAccount.js +1 -0
  111. package/dist/generated/prisma/models/McpServer.d.ts +1500 -0
  112. package/dist/generated/prisma/models/McpServer.js +1 -0
  113. package/dist/generated/prisma/models/NativeGatewayCall.d.ts +39 -1
  114. package/dist/generated/prisma/models/Principal.d.ts +167 -0
  115. package/dist/generated/prisma/models/ResourceGrant.d.ts +1 -1
  116. package/dist/generated/prisma/models/Secret.d.ts +387 -0
  117. package/dist/generated/prisma/models/Tool.d.ts +223 -1
  118. package/dist/generated/prisma/models.d.ts +6 -0
  119. package/dist/help/search.js +12 -3
  120. package/dist/help-index.json +719 -47
  121. package/dist/import/create.js +16 -1
  122. package/dist/import/neutral-schema.d.ts +1 -0
  123. package/dist/import/neutral-schema.js +2 -0
  124. package/dist/mcp/auth/grants-cli.d.ts +6 -4
  125. package/dist/mcp/auth/grants-cli.js +42 -3
  126. package/dist/mcp/auth/resource-server.d.ts +3 -1
  127. package/dist/mcp/auth/resource-server.js +17 -2
  128. package/dist/mcp/context.d.ts +3 -0
  129. package/dist/mcp/index.js +11 -0
  130. package/dist/mcp/tools/agents.js +100 -15
  131. package/dist/mcp/tools/grants.js +66 -28
  132. package/dist/mcp/tools/host-providers.d.ts +2 -0
  133. package/dist/mcp/tools/host-providers.js +215 -0
  134. package/dist/mcp/tools/mcp-servers.d.ts +26 -0
  135. package/dist/mcp/tools/mcp-servers.js +481 -0
  136. package/dist/mcp/tools/repositories.js +50 -10
  137. package/dist/mcp/tools/runs.js +178 -18
  138. package/dist/mcp/tools/secret-elicitation.d.ts +2 -0
  139. package/dist/mcp/tools/secret-elicitation.js +25 -2
  140. package/dist/mcp/tools/secrets.js +38 -4
  141. package/dist/mcp/tools/tools.d.ts +6 -4
  142. package/dist/mcp/tools/tools.js +40 -8
  143. package/dist/mcp/tools/trigger.js +28 -6
  144. package/dist/mcp/transport/http-limits.d.ts +2 -0
  145. package/dist/mcp/transport/http-limits.js +6 -2
  146. package/dist/mcp/webhooks/ingress.js +10 -0
  147. package/dist/native-worker/gateway-server.d.ts +14 -0
  148. package/dist/native-worker/gateway-server.js +28 -7
  149. package/dist/native-worker/gateway.d.ts +2 -0
  150. package/dist/native-worker/gateway.js +5 -1
  151. package/dist/native-worker/http-server.d.ts +4 -0
  152. package/dist/native-worker/http-server.js +51 -0
  153. package/dist/native-worker/ledger.d.ts +6 -2
  154. package/dist/native-worker/ledger.js +5 -4
  155. package/dist/native-worker/protocol.d.ts +1 -0
  156. package/dist/native-worker/protocol.js +4 -1
  157. package/dist/providers/coding-proxy/capability-hash.d.ts +2 -0
  158. package/dist/providers/coding-proxy/capability-hash.js +5 -0
  159. package/dist/providers/coding-proxy/mcp-route.d.ts +34 -0
  160. package/dist/providers/coding-proxy/mcp-route.js +166 -0
  161. package/dist/providers/coding-proxy/memory-ledger.d.ts +2 -0
  162. package/dist/providers/coding-proxy/memory-ledger.js +15 -0
  163. package/dist/providers/coding-proxy/prisma-ledger.d.ts +1 -0
  164. package/dist/providers/coding-proxy/prisma-ledger.js +30 -2
  165. package/dist/providers/coding-proxy/proxy.d.ts +7 -1
  166. package/dist/providers/coding-proxy/proxy.js +134 -34
  167. package/dist/providers/coding-proxy/runtime.d.ts +6 -0
  168. package/dist/providers/coding-proxy/runtime.js +32 -1
  169. package/dist/providers/coding-proxy/server.d.ts +3 -0
  170. package/dist/providers/coding-proxy/server.js +10 -0
  171. package/dist/providers/coding-proxy/types.d.ts +13 -0
  172. package/dist/providers/executor/composition.js +5 -0
  173. package/dist/providers/executor/container.d.ts +33 -1
  174. package/dist/providers/executor/container.js +76 -7
  175. package/dist/providers/host-tools/invoke.d.ts +37 -0
  176. package/dist/providers/host-tools/invoke.js +117 -0
  177. package/dist/providers/host-tools/operations.d.ts +65 -0
  178. package/dist/providers/host-tools/operations.js +149 -0
  179. package/dist/providers/host-tools/registry.d.ts +79 -0
  180. package/dist/providers/host-tools/registry.js +362 -0
  181. package/dist/providers/host-tools/request-guard.d.ts +17 -0
  182. package/dist/providers/host-tools/request-guard.js +99 -0
  183. package/dist/providers/host-tools/self-test.d.ts +64 -0
  184. package/dist/providers/host-tools/self-test.js +142 -0
  185. package/dist/providers/jobs/docker-isolation.d.ts +2 -2
  186. package/dist/providers/jobs/docker-isolation.js +30 -8
  187. package/dist/providers/jobs/docker.d.ts +8 -0
  188. package/dist/providers/jobs/docker.js +33 -9
  189. package/dist/providers/jobs/kubernetes-isolation.d.ts +3 -1
  190. package/dist/providers/jobs/kubernetes-isolation.js +15 -2
  191. package/dist/providers/jobs/kubernetes.d.ts +5 -0
  192. package/dist/providers/jobs/kubernetes.js +6 -1
  193. package/dist/providers/jobs/types.d.ts +6 -0
  194. package/dist/providers/review-host/types.d.ts +6 -4
  195. package/dist/providers/review-host/types.js +1 -1
  196. package/dist/providers/vcs/git.d.ts +6 -2
  197. package/dist/providers/vcs/git.js +15 -4
  198. package/dist/providers/vcs/github.d.ts +2 -71
  199. package/dist/providers/vcs/github.js +8 -11
  200. package/dist/providers/vcs/index.d.ts +1 -1
  201. package/dist/providers/vcs/index.js +1 -1
  202. package/dist/providers/vcs/remote.d.ts +4 -0
  203. package/dist/providers/vcs/remote.js +7 -1
  204. package/dist/providers/vcs/routing.d.ts +12 -6
  205. package/dist/providers/vcs/routing.js +21 -6
  206. package/dist/providers/vcs/types.d.ts +71 -3
  207. package/dist/quickstart/index.d.ts +5 -0
  208. package/dist/quickstart/index.js +21 -0
  209. package/dist/quickstart-images.json +1 -1
  210. package/dist/sandbox/eval-core.d.ts +12 -2
  211. package/dist/sandbox/eval-core.js +20 -4
  212. package/dist/sandbox/host-functions.d.ts +4 -0
  213. package/dist/sandbox/host-functions.js +7 -2
  214. package/dist/sandbox/limits.d.ts +4 -1
  215. package/dist/sandbox/limits.js +4 -1
  216. package/dist/sandbox/run-in-sandbox.d.ts +6 -0
  217. package/dist/sandbox/run-in-sandbox.js +14 -2
  218. package/dist/sandbox/safe-fetch.d.ts +2 -0
  219. package/dist/sandbox/safe-fetch.js +1 -1
  220. package/dist/sandbox/secret-broker.d.ts +1 -0
  221. package/dist/sandbox/secret-broker.js +1 -1
  222. package/dist/sandbox/tool-timeout.d.ts +32 -0
  223. package/dist/sandbox/tool-timeout.js +52 -0
  224. package/dist/sandbox/user-tool.d.ts +8 -1
  225. package/dist/sandbox/user-tool.js +9 -3
  226. package/dist/viewer/graph.js +2 -1
  227. package/dist/wardby-bin.js +5 -0
  228. package/docs/README.md +11 -1
  229. package/docs/agent-recipes.md +37 -4
  230. package/docs/architecture-runtime.md +1 -1
  231. package/docs/claude-code-plugin.md +164 -0
  232. package/docs/code-review-agents.md +6 -0
  233. package/docs/coding-agent-setup.md +77 -1
  234. package/docs/coding-worker-byo-images.md +4 -2
  235. package/docs/coding-worker-isolation.md +78 -7
  236. package/docs/debugging-agent-runs.md +242 -0
  237. package/docs/getting-started-gke.md +5 -4
  238. package/docs/getting-started-identity-provider.md +43 -37
  239. package/docs/getting-started.md +8 -0
  240. package/docs/host-providers.md +294 -0
  241. package/docs/mcp-servers.md +254 -0
  242. package/docs/native-sandbox.md +25 -9
  243. package/docs/security-deployment.md +110 -31
  244. package/docs/tool-secrets.md +3 -0
  245. package/docs/viewer-api.md +6 -0
  246. package/docs/viewer-api.openapi.yml +1 -1
  247. package/help/admin-viewer.md +4 -0
  248. package/help/agent-recipes.md +33 -2
  249. package/help/brokered-secrets.md +3 -0
  250. package/help/build-claude-code-image.md +1 -1
  251. package/help/claude-code-run-pane.md +40 -0
  252. package/help/errors/coding-mcp-pins-unavailable.md +35 -0
  253. package/help/errors/database-auth-invalid.md +1 -1
  254. package/help/errors/host-provider-error.md +40 -0
  255. package/help/errors/host-tool-failed.md +36 -0
  256. package/help/errors/mcp-call-limit.md +22 -0
  257. package/help/errors/mcp-call-timeout.md +22 -0
  258. package/help/errors/mcp-server-auth-failed.md +22 -0
  259. package/help/errors/mcp-server-credentials-unavailable.md +45 -0
  260. package/help/errors/mcp-server-error.md +28 -0
  261. package/help/errors/mcp-server-unreachable.md +22 -0
  262. package/help/errors/mcp-servers-off.md +21 -0
  263. package/help/errors/mcp-tool-not-allowed.md +26 -0
  264. package/help/errors/mcp-tool-removed.md +22 -0
  265. package/help/errors/mcp-unavailable.md +25 -0
  266. package/help/errors/native-sandbox-capacity.md +1 -1
  267. package/help/errors/native-sandbox-deadline-exceeded.md +1 -1
  268. package/help/errors/native-sandbox-docker-failed.md +1 -1
  269. package/help/errors/native-sandbox-gateway-error.md +1 -1
  270. package/help/errors/native-sandbox-gateway-unavailable.md +1 -1
  271. package/help/errors/native-sandbox-image-not-pinned.md +1 -1
  272. package/help/errors/native-sandbox-isolation-mismatch.md +1 -1
  273. package/help/errors/native-sandbox-network-unenforced.md +1 -1
  274. package/help/errors/native-sandbox-requires-catalog.md +1 -1
  275. package/help/errors/native-sandbox-unavailable.md +1 -1
  276. package/help/errors/native-sandbox-warm-delivery-failed.md +1 -1
  277. package/help/errors/native-sandbox-worker-exited.md +1 -1
  278. package/help/errors/native-sandbox-worker-lost.md +1 -1
  279. package/help/errors/native-sandbox-worker-unready.md +1 -1
  280. package/help/errors/repository-not-listed.md +36 -0
  281. package/help/errors/repository-required.md +36 -0
  282. package/help/errors/tool-timeout.md +38 -0
  283. package/help/host-providers.md +40 -0
  284. package/help/identity-and-access.md +4 -1
  285. package/help/mcp-servers.md +73 -0
  286. package/help/mcp.md +7 -2
  287. package/help/native-capabilities.md +27 -1
  288. package/help/native-sandbox.md +1 -1
  289. package/help/operating-agents.md +16 -0
  290. package/help/related-pull-requests.md +7 -3
  291. package/help/repo-instructions-and-skills.md +3 -1
  292. package/help/resume-runs-locally.md +3 -0
  293. package/help/stop-run.md +1 -1
  294. package/help/troubleshooting/coding-workers.md +9 -1
  295. package/help/troubleshooting/debugging-agent-runs.md +29 -0
  296. package/package.json +2 -1
  297. package/prisma/migrations/20261102000000_host_providers/migration.sql +56 -0
  298. package/prisma/migrations/20261104000000_coding_agent_repository_list/migration.sql +5 -0
  299. package/prisma/migrations/20261106000000_tool_timeout/migration.sql +4 -0
  300. package/prisma/migrations/20261107000000_mcp_servers/migration.sql +33 -0
  301. package/prisma/migrations/20261108000000_coding_mcp/migration.sql +5 -0
  302. package/prisma/migrations/20261109000000_task_run_index/migration.sql +3 -0
  303. package/prisma/schema.prisma +159 -14
  304. package/scripts/git-askpass.sh +1 -1
@@ -29,6 +29,7 @@
29
29
  * gets 404, not another principal's final text/cost.
30
30
  */
31
31
  import { dispatchRun } from "../../core/dispatch.js";
32
+ import { RepositoryChoiceRefusedError, resolveCodingRepository } from "../../core/coding-repository.js";
32
33
  import { AttributionError, explicitAttribution, RESPONSE_PATH_SNAPSHOT_BUDGET, } from "../../core/attribution.js";
33
34
  import { CodingBaseRefSchema, CodingTaskOverrideSchema, isLocalRepository } from "../../coding/protocol.js";
34
35
  import { loadLocalRepoRoots, resolveLocalRepository, LocalRepoError } from "../../coding/local-repo.js";
@@ -46,6 +47,7 @@ const TriggerAgentSchema = z
46
47
  agentId: z.string().trim().min(1).max(128),
47
48
  task: CodingTaskOverrideSchema.optional(),
48
49
  baseRef: CodingBaseRefSchema.optional(),
50
+ repository: z.string().min(1).max(300).optional(),
49
51
  issue: z.unknown().optional(),
50
52
  review: z
51
53
  .object({
@@ -127,10 +129,10 @@ function assertOverrideAllowed(principalId, agent, args) {
127
129
  // Strictly the owner: the stdio operator's owner-level access doesn't make
128
130
  // it the owner whose repository authority the task steers (review M8).
129
131
  const isOwner = agent.ownerId !== null && agent.ownerId === principalId;
130
- if (isOwner || (args.task === undefined && args.baseRef === undefined))
132
+ if (isOwner || (args.task === undefined && args.baseRef === undefined && args.repository === undefined))
131
133
  return;
132
134
  if (!agent.codingProfile?.allowWebhookTaskOverride) {
133
- throw new McpError(403, `Agent "${agent.id}": only its owner can pass a task or baseRef, unless the owner allows it (codingProfile.allowWebhookTaskOverride).`);
135
+ throw new McpError(403, `Agent "${agent.id}": only its owner can pass a task, baseRef or repository, unless the owner allows it (codingProfile.allowWebhookTaskOverride).`);
134
136
  }
135
137
  }
136
138
  /** execute on the agent, plus the override rule; `db` is the transaction when re-checking. */
@@ -150,6 +152,10 @@ export function registerTriggerTool(mcp) {
150
152
  agentId: { type: "string" },
151
153
  task: { type: "string" },
152
154
  baseRef: { type: "string" },
155
+ repository: {
156
+ type: "string",
157
+ description: "Coding agents only: a repository on the agent's list (see get_agent). Omit to use codingProfile.repository.",
158
+ },
153
159
  review: {
154
160
  type: "object",
155
161
  additionalProperties: false,
@@ -189,13 +195,28 @@ export function registerTriggerTool(mcp) {
189
195
  }
190
196
  review = await resolveLocalReview(ctx, agent.id, args.review);
191
197
  }
192
- if (agent.kind !== "coding" && (args.task !== undefined || args.baseRef !== undefined)) {
193
- throw new McpError(400, "Task and baseRef overrides are only valid for coding agents.");
198
+ if (agent.kind !== "coding" &&
199
+ (args.task !== undefined || args.baseRef !== undefined || args.repository !== undefined)) {
200
+ throw new McpError(400, "Task, baseRef and repository overrides are only valid for coding agents.");
194
201
  }
195
202
  // A local repository is re-checked against the roots as they are now
196
203
  // (they can narrow after the agent was saved), before any run exists.
197
204
  let warnings = [];
198
- const repository = agent.kind === "coding" ? agent.codingProfile?.repository : undefined;
205
+ // The resolver's refusal lists the agent's repositories, so it runs only
206
+ // after requireTriggerable (execute on the agent) and never echoes the
207
+ // requested value.
208
+ const refused = (err) => {
209
+ if (err instanceof RepositoryChoiceRefusedError)
210
+ throw new McpError(400, `${err.code}: ${err.message}`);
211
+ throw err;
212
+ };
213
+ const repository = agent.kind === "coding" && agent.codingProfile
214
+ ? (await resolveCodingRepository(ctx.db, {
215
+ agentId: agent.id,
216
+ profile: agent.codingProfile,
217
+ requested: args.repository,
218
+ }).catch(refused)).repository
219
+ : undefined;
199
220
  if (repository !== undefined && isLocalRepository(repository)) {
200
221
  try {
201
222
  const resolved = await resolveLocalRepository(repository, loadLocalRepoRoots(process.env).roots);
@@ -238,6 +259,7 @@ export function registerTriggerTool(mcp) {
238
259
  trigger: "manual",
239
260
  codingTask: args.task,
240
261
  codingBaseRef: args.baseRef,
262
+ codingRepository: args.repository,
241
263
  attribution,
242
264
  task: ctx.clientSupportsTasks ? { principalId: ctx.principal.id, ttlMs: DEFAULT_TASK_TTL_MS } : undefined,
243
265
  triggeredById: ctx.principal.id,
@@ -262,7 +284,7 @@ export function registerTriggerTool(mcp) {
262
284
  catch (err) {
263
285
  if (pullRequest)
264
286
  await ctx.db.localPullRequest.deleteMany({ where: { id: pullRequest.id } }).catch(() => { });
265
- throw err;
287
+ return refused(err);
266
288
  }
267
289
  if (ctx.clientSupportsTasks) {
268
290
  if (!dispatched.task)
@@ -14,4 +14,6 @@ export declare class PayloadTooLargeError extends HttpBoundaryError {
14
14
  }
15
15
  export declare function canonicalUrl(value: string): URL;
16
16
  export declare function readBody(req: IncomingMessage, maxBytes: number, signal: AbortSignal): Promise<string>;
17
+ /** readBody's bytes, undecoded (for a route that forwards the body unchanged). */
18
+ export declare function readBodyBytes(req: IncomingMessage, maxBytes: number, signal: AbortSignal): Promise<Buffer>;
17
19
  export declare function parseBody(raw: string, contentType: string | undefined): unknown;
@@ -23,7 +23,11 @@ export function canonicalUrl(value) {
23
23
  }
24
24
  return url;
25
25
  }
26
- export function readBody(req, maxBytes, signal) {
26
+ export async function readBody(req, maxBytes, signal) {
27
+ return (await readBodyBytes(req, maxBytes, signal)).toString("utf8");
28
+ }
29
+ /** readBody's bytes, undecoded (for a route that forwards the body unchanged). */
30
+ export function readBodyBytes(req, maxBytes, signal) {
27
31
  const length = req.headers["content-length"];
28
32
  if (length && (!/^\d+$/.test(length) || Number(length) > maxBytes))
29
33
  return Promise.reject(new PayloadTooLargeError());
@@ -55,7 +59,7 @@ export function readBody(req, maxBytes, signal) {
55
59
  };
56
60
  const end = () => {
57
61
  clean();
58
- resolve(Buffer.concat(chunks, size).toString("utf8"));
62
+ resolve(Buffer.concat(chunks, size));
59
63
  };
60
64
  req.on("data", data);
61
65
  req.once("end", end);
@@ -18,11 +18,19 @@ export async function handleWebhookIngress(webhookId, req, db, executor, issueTr
18
18
  if (req.body.task !== undefined && codingTask === undefined) {
19
19
  return { status: 400, body: { error: "invalid_task" } };
20
20
  }
21
+ // `wardbyRepository`, not `repository`, for the same reason: a forwarded GitHub
22
+ // payload carries its own top-level `repository` object.
23
+ const repository = req.body.wardbyRepository;
24
+ if (repository !== undefined &&
25
+ (typeof repository !== "string" || repository.length === 0 || repository.length > 300)) {
26
+ return { status: 400, body: { error: "invalid_repository" } };
27
+ }
21
28
  // `wardbyIssue`, not `issue`: forwarded third-party payloads (GitHub, Jira)
22
29
  // carry their own top-level `issue` object, which is not ours to read.
23
30
  const result = await resolveWebhookRun(webhookId, secret, db, executor, codingTask, {
24
31
  issue: req.body.wardbyIssue,
25
32
  issueTrackers,
33
+ repository,
26
34
  });
27
35
  if (result.ok) {
28
36
  return { status: 202, body: { runId: result.runId } };
@@ -30,6 +38,8 @@ export async function handleWebhookIngress(webhookId, req, db, executor, issueTr
30
38
  switch (result.reason) {
31
39
  case "invalid_issue":
32
40
  return { status: 400, body: { error: "invalid_issue", error_description: result.message } };
41
+ case "invalid_repository":
42
+ return { status: 400, body: { error: "invalid_repository" } };
33
43
  case "not_found":
34
44
  return { status: 404, body: { error: "not_found" } };
35
45
  case "invalid_secret":
@@ -11,6 +11,7 @@
11
11
  * reported usage; the worker's own usage numbers are never read.
12
12
  */
13
13
  import { createNativeRunTools, durableDelegate, type NativeRunFinishContext, type NativeRunProviders, type RunnerDb, type SandboxSessionSnapshot } from "../core/runner.js";
14
+ import type { CodingMcpDeps } from "../core/coding-mcp.js";
14
15
  import { ReviewReadLedger } from "../core/review-reads.js";
15
16
  import type { LlmProvider, LlmStreamEvent } from "../providers/llm/types.js";
16
17
  import { PrismaGatewayLedger, type GatewaySessionRecord } from "./ledger.js";
@@ -26,6 +27,8 @@ export interface GatewayServerDeps {
26
27
  providers: NativeRunProviders;
27
28
  /** Delegation long-poll window; tests shorten it. */
28
29
  pollWindowMs?: number;
30
+ /** Serves POST /coding-mcp on the same listener (remote MCP servers for coding runs); unset → 404. */
31
+ codingMcp?: CodingMcpDeps;
29
32
  }
30
33
  /** What a request gets back: one JSON value, or a stream of model events. */
31
34
  export type GatewayResponse = {
@@ -72,6 +75,17 @@ export declare function streamModel(ctx: RebuiltRun, ledger: PrismaGatewayLedger
72
75
  export declare const CONSOLE_WITHHELD_MESSAGE = "sandbox tool console line withheld: the tool's secret values could not be read";
73
76
  /** Serves one privileged bridge call for a worker (exported for tests). */
74
77
  export declare function hostCall(ctx: RebuiltRun, ledger: PrismaGatewayLedger, callId: string, params: GatewayParams<"host.call">): Promise<unknown>;
78
+ /** Added to a tool's limit for one host call: the worker's own wall time ends the call first. */
79
+ export declare const GATEWAY_HOST_CALL_MARGIN_MS = 20000;
80
+ /** How long one host call of `tool` may take: its limit plus the margin, cut to the run's deadline. */
81
+ export declare function hostCallTimeoutMs(ctx: {
82
+ session: {
83
+ deadlineAt: Date;
84
+ };
85
+ tools: {
86
+ toolTimeoutMs: (name: string) => number | undefined;
87
+ };
88
+ }, tool: string, now?: number): number;
75
89
  /** The worker's terminal call: the run's one terminal write (masked by finishNativeRun), then the session ends. */
76
90
  export declare function finish(ctx: RebuiltRun, ledger: PrismaGatewayLedger, callId: string, params: GatewayParams<"finish">): Promise<null>;
77
91
  export {};
@@ -16,6 +16,7 @@ import { DELEGATE_TOOL_PREFIX } from "../core/tool-names.js";
16
16
  import { relayedOutputTokens } from "../providers/coding-proxy/interrupted.js";
17
17
  import { RoutingLlmProvider } from "../providers/llm/routing.js";
18
18
  import { isPrivilegedBridgeName } from "../sandbox/host-functions.js";
19
+ import { DEFAULT_TOOL_TIMEOUT_MS } from "../sandbox/tool-timeout.js";
19
20
  import { forWorker, gatewayLog } from "./gateway.js";
20
21
  import { PrismaGatewayLedger } from "./ledger.js";
21
22
  import { GatewayError, GatewayRequestSchema, parseParams, } from "./protocol.js";
@@ -65,6 +66,7 @@ export function runSecretValues(ctx, capability) {
65
66
  return async () => [
66
67
  capability,
67
68
  ...(await userToolSecretValues(ctx.tools, Object.keys(ctx.snapshot.input.userTools))),
69
+ ...(await ctx.tools.mcpSecretValues()),
68
70
  ];
69
71
  }
70
72
  /**
@@ -178,6 +180,7 @@ async function rebuild(deps, session) {
178
180
  repoAccess,
179
181
  issueTrackers,
180
182
  reviewReads,
183
+ deadlineAt: session.deadlineAt,
181
184
  }),
182
185
  finishContext: { runId: session.runId, db, providers, reviewHosts, repoAccess, issueTrackers },
183
186
  delegation: {
@@ -335,9 +338,15 @@ export async function hostCall(ctx, ledger, callId, params) {
335
338
  }
336
339
  redact = values;
337
340
  }
338
- const host = ctx.tools.privilegedHostFor(params.tool, AbortSignal.timeout(30_000), redact);
339
- if (!host)
341
+ // The tool's own limit plus a margin (30 s for a default tool), never past the run's deadline,
342
+ // and aborted if the run is stopped meanwhile.
343
+ const stop = ctx.tools.watchStop();
344
+ const signal = AbortSignal.any([AbortSignal.timeout(hostCallTimeoutMs(ctx, params.tool)), stop.signal]);
345
+ const host = ctx.tools.privilegedHostFor(params.tool, signal, redact);
346
+ if (!host) {
347
+ stop.done();
340
348
  throw new GatewayError("not_allowed", `"${params.tool}" is not a user tool of this run.`);
349
+ }
341
350
  const bridge = host[params.bridge];
342
351
  const run = async () => {
343
352
  try {
@@ -348,11 +357,23 @@ export async function hostCall(ctx, ledger, callId, params) {
348
357
  throw new GatewayError("bridge_error", err instanceof Error ? err.message.slice(0, 1024) : "Host function failed.");
349
358
  }
350
359
  };
351
- // Console lines are not worth a ledger row, and a secret value must never be stored in one
352
- // (a secrets.get is a side-effect-free read, so a retried callId simply reads it again).
353
- if (params.bridge === "__bridge_console" || params.bridge === "__bridge_secretsGet")
354
- return run();
355
- return replayable(ledger, ctx.session.id, callId, "host.call", run);
360
+ try {
361
+ // Console lines are not worth a ledger row, and a secret value must never be stored in one
362
+ // (a secrets.get is a side-effect-free read, so a retried callId simply reads it again).
363
+ if (params.bridge === "__bridge_console" || params.bridge === "__bridge_secretsGet")
364
+ return await run();
365
+ return await replayable(ledger, ctx.session.id, callId, "host.call", run);
366
+ }
367
+ finally {
368
+ stop.done();
369
+ }
370
+ }
371
+ /** Added to a tool's limit for one host call: the worker's own wall time ends the call first. */
372
+ export const GATEWAY_HOST_CALL_MARGIN_MS = 20_000;
373
+ /** How long one host call of `tool` may take: its limit plus the margin, cut to the run's deadline. */
374
+ export function hostCallTimeoutMs(ctx, tool, now = Date.now()) {
375
+ const limitMs = (ctx.tools.toolTimeoutMs(tool) ?? DEFAULT_TOOL_TIMEOUT_MS) + GATEWAY_HOST_CALL_MARGIN_MS;
376
+ return Math.max(0, Math.min(limitMs, ctx.session.deadlineAt.getTime() - now));
356
377
  }
357
378
  /** Runs a one-shot call once per callId; a retry replays its recorded result. */
358
379
  async function replayable(ledger, sessionId, callId, method, run) {
@@ -63,6 +63,8 @@ export interface SandboxedEngineOptions {
63
63
  * the run's values is not sent to the worker; without this, no cause code is sent.
64
64
  */
65
65
  readableSecretValues?: (tool: string) => Promise<string[]>;
66
+ /** Every secret value of the run's attached MCP servers, checked the same way. */
67
+ mcpSecretValues?: () => Promise<string[]>;
66
68
  drivability: () => Promise<RunDrivability>;
67
69
  launcher: WorkerLauncher;
68
70
  }
@@ -116,8 +116,12 @@ export async function runSandboxedEngine(options) {
116
116
  };
117
117
  /** The run's readable secret values, for checking a failure's cause code before the worker sees it. */
118
118
  const readable = options.readableSecretValues;
119
+ const mcpValues = options.mcpSecretValues;
119
120
  const runSecretValues = readable
120
- ? async () => (await Promise.all(Object.keys(input.userTools).map((tool) => readable(tool)))).flat()
121
+ ? async () => [
122
+ ...(await Promise.all(Object.keys(input.userTools).map((tool) => readable(tool)))).flat(),
123
+ ...(mcpValues ? await mcpValues() : []),
124
+ ]
121
125
  : undefined;
122
126
  const served = {
123
127
  async *stream(raw) {
@@ -7,6 +7,10 @@
7
7
  * POST /native-gateway/v1/call body: GatewayRequest
8
8
  * → application/json { ok: true, result } | { ok: false, error: { code, message } }
9
9
  * → application/x-ndjson (llm.stream) { event } … then { end: true } or { error: { code, message, causeCode? } }
10
+ *
11
+ * POST /coding-mcp a coding run's MCP calls, forwarded by the coding proxy's /mcp route
12
+ * (src/core/coding-mcp.ts); its own auth: the run's MCP token, not a
13
+ * gateway capability. 404 when the listener was started without it.
10
14
  */
11
15
  import { type Server } from "node:http";
12
16
  import { type GatewayServerDeps } from "./gateway-server.js";
@@ -7,9 +7,15 @@
7
7
  * POST /native-gateway/v1/call body: GatewayRequest
8
8
  * → application/json { ok: true, result } | { ok: false, error: { code, message } }
9
9
  * → application/x-ndjson (llm.stream) { event } … then { end: true } or { error: { code, message, causeCode? } }
10
+ *
11
+ * POST /coding-mcp a coding run's MCP calls, forwarded by the coding proxy's /mcp route
12
+ * (src/core/coding-mcp.ts); its own auth: the run's MCP token, not a
13
+ * gateway capability. 404 when the listener was started without it.
10
14
  */
11
15
  import { createServer } from "node:http";
16
+ import { CODING_MCP_PATH, handleCodingMcpRequest } from "../core/coding-mcp.js";
12
17
  import { logger } from "../core/logger.js";
18
+ import { HTTP_LIMITS, HttpBoundaryError, readBody as readLimitedBody } from "../mcp/transport/http-limits.js";
13
19
  import { logUnexpectedError } from "./gateway.js";
14
20
  import { serveGatewayRequest } from "./gateway-server.js";
15
21
  import { GatewayError, MAX_MESSAGE_BYTES, toGatewayError } from "./protocol.js";
@@ -58,6 +64,10 @@ export function createGatewayServer(deps) {
58
64
  send(healthy ? 200 : 503, { ok: healthy });
59
65
  return;
60
66
  }
67
+ if (new URL(req.url ?? "/", "http://gateway").pathname === CODING_MCP_PATH && deps.codingMcp) {
68
+ await serveCodingMcp(req, res, deps.codingMcp);
69
+ return;
70
+ }
61
71
  if (req.method !== "POST" || req.url !== NATIVE_GATEWAY_PATH) {
62
72
  send(404, { ok: false, error: { code: "not_found", message: "not found" } });
63
73
  return;
@@ -101,6 +111,47 @@ export function createGatewayServer(deps) {
101
111
  });
102
112
  });
103
113
  }
114
+ /** /coding-mcp: one JSON-RPC message per POST, answered as one JSON body (or 202 with none). */
115
+ async function serveCodingMcp(req, res, deps) {
116
+ if (req.method !== "POST") {
117
+ res.setHeader("allow", "POST");
118
+ res.setHeader("content-type", "application/json");
119
+ res.writeHead(405);
120
+ res.end(JSON.stringify({ jsonrpc: "2.0", id: null, error: { code: -32600, message: "Use POST." } }));
121
+ return;
122
+ }
123
+ let body;
124
+ try {
125
+ body = await readLimitedBody(req, HTTP_LIMITS.json, AbortSignal.timeout(HTTP_LIMITS.requestMs));
126
+ }
127
+ catch (err) {
128
+ const status = err instanceof HttpBoundaryError ? err.status : 400;
129
+ res.setHeader("content-type", "application/json");
130
+ res.writeHead(status);
131
+ res.end(JSON.stringify({
132
+ jsonrpc: "2.0",
133
+ id: null,
134
+ error: { code: -32600, message: "The request body could not be read." },
135
+ }));
136
+ // A body left unread would otherwise keep the socket busy.
137
+ if (status === 413)
138
+ req.resume();
139
+ return;
140
+ }
141
+ // The proxy gives up on a call at its forward limit (or its worker goes away): the closed response
142
+ // aborts the call in flight rather than leave it running against the MCP server.
143
+ const closed = new AbortController();
144
+ res.on("close", () => {
145
+ if (!res.writableFinished)
146
+ closed.abort();
147
+ });
148
+ const response = await handleCodingMcpRequest(req.headers.authorization, body, deps, closed.signal);
149
+ if (closed.signal.aborted)
150
+ return;
151
+ res.setHeader("content-type", response.contentType);
152
+ res.writeHead(response.status);
153
+ res.end(response.body);
154
+ }
104
155
  /** Starts the listener; resolves once it is accepting connections. */
105
156
  export async function startGatewayServer(listen, deps) {
106
157
  const server = createGatewayServer(deps);
@@ -115,11 +115,15 @@ export declare class PrismaGatewayLedger {
115
115
  setDelegation(sessionId: string, callId: string, state: {
116
116
  status: "pending" | "waiting_budget";
117
117
  childAgentId: string;
118
+ childRepository?: string;
118
119
  childRunId?: string;
119
120
  }): Promise<void>;
120
121
  delegation(sessionId: string, callId: string): Promise<DelegationState | null>;
121
- /** The sub-agents other delegations of this run are waiting for budget to start. */
122
- waitingChildAgentIds(sessionId: string, exceptCallId: string): Promise<string[]>;
122
+ /** The sub-agents (and repositories, null = the child's default) other delegations of this run wait on budget to start. */
123
+ waitingDelegations(sessionId: string, exceptCallId: string): Promise<{
124
+ childAgentId: string;
125
+ childRepository: string | null;
126
+ }[]>;
123
127
  /** Delegations of this session whose child run is started and not yet reported. */
124
128
  openDelegationChildren(sessionId: string): Promise<string[]>;
125
129
  }
@@ -202,6 +202,7 @@ export class PrismaGatewayLedger {
202
202
  data: {
203
203
  status: state.status,
204
204
  childAgentId: state.childAgentId,
205
+ ...(state.childRepository ? { childRepository: state.childRepository } : {}),
205
206
  ...(state.childRunId ? { childRunId: state.childRunId } : {}),
206
207
  },
207
208
  });
@@ -213,13 +214,13 @@ export class PrismaGatewayLedger {
213
214
  const status = row.status === "waiting_budget" ? "waiting_budget" : row.status === "completed" ? "completed" : "pending";
214
215
  return { status, childRunId: row.childRunId, childAgentId: row.childAgentId, createdAt: row.createdAt };
215
216
  }
216
- /** The sub-agents other delegations of this run are waiting for budget to start. */
217
- async waitingChildAgentIds(sessionId, exceptCallId) {
217
+ /** The sub-agents (and repositories, null = the child's default) other delegations of this run wait on budget to start. */
218
+ async waitingDelegations(sessionId, exceptCallId) {
218
219
  const rows = await this.db.nativeGatewayCall.findMany({
219
220
  where: { sessionId, status: "waiting_budget", callId: { not: exceptCallId }, childAgentId: { not: null } },
220
- select: { childAgentId: true },
221
+ select: { childAgentId: true, childRepository: true },
221
222
  });
222
- return rows.map((row) => row.childAgentId);
223
+ return rows.map((row) => ({ childAgentId: row.childAgentId, childRepository: row.childRepository }));
223
224
  }
224
225
  /** Delegations of this session whose child run is started and not yet reported. */
225
226
  async openDelegationChildren(sessionId) {
@@ -145,6 +145,7 @@ export declare const WorkerInputSchema: z.ZodObject<{
145
145
  userTools: z.ZodRecord<z.ZodString, z.ZodObject<{
146
146
  code: z.ZodString;
147
147
  paramsZod: z.ZodString;
148
+ timeoutMs: z.ZodOptional<z.ZodNumber>;
148
149
  }, z.core.$strict>>;
149
150
  runsConcurrently: z.ZodArray<z.ZodString>;
150
151
  pricing: z.ZodObject<{
@@ -125,7 +125,10 @@ const LoadedToolSchema = z
125
125
  .object({ name: z.string(), description: z.string(), jsonSchema: z.record(z.string(), z.unknown()) })
126
126
  .strict();
127
127
  /** A user tool as the worker runs it: code and schema only. Capabilities stay on the gateway. */
128
- const WorkerUserToolSchema = z.object({ code: z.string(), paramsZod: z.string() }).strict();
128
+ /** `timeoutMs`: the tool's time limit for this run, already capped by the server; absent = the default. */
129
+ const WorkerUserToolSchema = z
130
+ .object({ code: z.string(), paramsZod: z.string(), timeoutMs: z.number().int().positive().optional() })
131
+ .strict();
129
132
  const PricingSchema = z
130
133
  .object({
131
134
  provider: z.string(),
@@ -0,0 +1,2 @@
1
+ /** SHA-256 (base64url) of a run credential: the proxy stores only this, never the value. */
2
+ export declare function capabilityHash(capability: string): string;
@@ -0,0 +1,5 @@
1
+ import { createHash } from "node:crypto";
2
+ /** SHA-256 (base64url) of a run credential: the proxy stores only this, never the value. */
3
+ export function capabilityHash(capability) {
4
+ return createHash("sha256").update(capability).digest("base64url");
5
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * The coding proxy's /mcp route (remote MCP servers, phase 2): the worker's MCP server URL. It
3
+ * checks the run's MCP token (`rmt_…`, WARDBY_MCP_TOKEN in the worker) against the proxy session,
4
+ * then forwards the JSON-RPC body unchanged, with the same Authorization, to the native gateway's
5
+ * /coding-mcp route (WARDBY_MCP_GATEWAY_URL), which checks the token again and runs the call. The
6
+ * proxy never sees an MCP server URL or secret; the gateway redacts results before they get here.
7
+ *
8
+ * Secrets: the token is only hashed and forwarded, never logged or echoed. The route's log line
9
+ * carries the session id, a sanitized JSON-RPC method, the status and a fixed reason code only:
10
+ * never the body, the token or an error's text. Every body this route writes is fixed text or the
11
+ * gateway's answer, unchanged.
12
+ */
13
+ import type { IncomingMessage, ServerResponse } from "node:http";
14
+ import type { ProxyLedger } from "./types.js";
15
+ export declare const MCP_ROUTE_PATH = "/mcp";
16
+ /** The longest a forwarded call may take, when the session's deadline is later. */
17
+ export declare const MCP_FORWARD_MAX_MS: number;
18
+ export declare const MCP_UNAVAILABLE_MESSAGE = "MCP tools aren't available on this server.";
19
+ /**
20
+ * A JSON-RPC method as the log may carry it: its first 64 characters when they are letters, `/`
21
+ * and `_` only, else "other". Anything that starts like an MCP token is "other" too.
22
+ */
23
+ export declare function mcpLogMethod(value: unknown): string;
24
+ export interface McpRouteOptions {
25
+ ledger: Pick<ProxyLedger, "findSessionByMcpTokenHash">;
26
+ /** The native gateway's base URL; without one, /mcp answers 503 mcp_unavailable. */
27
+ gatewayUrl: string | undefined;
28
+ fetchImpl?: typeof fetch;
29
+ now?: () => Date;
30
+ log?: {
31
+ info(fields: Record<string, unknown>, message: string): void;
32
+ };
33
+ }
34
+ export declare function createMcpRoute(options: McpRouteOptions): (req: IncomingMessage, res: ServerResponse) => Promise<void>;
@@ -0,0 +1,166 @@
1
+ import { isMcpToken, mcpTokenHash, MCP_TOKEN_REQUIRED_MESSAGE } from "../../coding/mcp-token.js";
2
+ import { logger } from "../../core/logger.js";
3
+ import { HTTP_LIMITS, HttpBoundaryError, readBodyBytes } from "../../mcp/transport/http-limits.js";
4
+ export const MCP_ROUTE_PATH = "/mcp";
5
+ /** The gateway's route this one forwards to. */
6
+ const GATEWAY_PATH = "/coding-mcp";
7
+ /** The longest a forwarded call may take, when the session's deadline is later. */
8
+ export const MCP_FORWARD_MAX_MS = 5 * 60_000;
9
+ /** The largest gateway answer relayed (a tool result is text the model reads; same as a model request). */
10
+ const MAX_GATEWAY_RESPONSE_BYTES = 8 * 1024 * 1024;
11
+ export const MCP_UNAVAILABLE_MESSAGE = "MCP tools aren't available on this server.";
12
+ const MCP_UNREACHABLE_MESSAGE = "MCP tools can't be reached right now.";
13
+ const LOG_METHOD = /^[A-Za-z/_]+$/;
14
+ /**
15
+ * A JSON-RPC method as the log may carry it: its first 64 characters when they are letters, `/`
16
+ * and `_` only, else "other". Anything that starts like an MCP token is "other" too.
17
+ */
18
+ export function mcpLogMethod(value) {
19
+ if (typeof value !== "string")
20
+ return "other";
21
+ const method = value.slice(0, 64);
22
+ return LOG_METHOD.test(method) && !method.includes("rmt_") ? method : "other";
23
+ }
24
+ function methodOf(body) {
25
+ try {
26
+ const message = JSON.parse(body.toString("utf8"));
27
+ return mcpLogMethod(message?.method);
28
+ }
29
+ catch {
30
+ return "other";
31
+ }
32
+ }
33
+ class GatewayError extends Error {
34
+ reason;
35
+ constructor(reason) {
36
+ super(reason);
37
+ this.reason = reason;
38
+ }
39
+ }
40
+ const JSON_HEADERS = { "content-type": "application/json", "cache-control": "no-store" };
41
+ const rpcError = (code, message) => JSON.stringify({ jsonrpc: "2.0", id: null, error: { code, message } });
42
+ async function readGatewayBody(response) {
43
+ const chunks = [];
44
+ let size = 0;
45
+ if (!response.body)
46
+ return Buffer.alloc(0);
47
+ for await (const chunk of response.body) {
48
+ size += chunk.byteLength;
49
+ if (size > MAX_GATEWAY_RESPONSE_BYTES) {
50
+ void response.body.cancel().catch(() => undefined);
51
+ throw new GatewayError("gateway_response_too_large");
52
+ }
53
+ chunks.push(Buffer.from(chunk));
54
+ }
55
+ return Buffer.concat(chunks, size);
56
+ }
57
+ export function createMcpRoute(options) {
58
+ const fetchImpl = options.fetchImpl ?? fetch;
59
+ const now = options.now ?? (() => new Date());
60
+ const log = options.log ?? logger.child({ module: "coding-proxy" });
61
+ const gatewayUrl = options.gatewayUrl?.replace(/\/+$/, "");
62
+ return async (req, res) => {
63
+ const startedAt = performance.now();
64
+ const seen = {};
65
+ /** Refused before the body was read: close the connection rather than read what is still coming. */
66
+ const closeIfBodyUnread = () => {
67
+ if (!req.complete && !res.headersSent)
68
+ res.setHeader("connection", "close");
69
+ };
70
+ const send = (status, body, headers = JSON_HEADERS) => {
71
+ res.writeHead(status, headers).end(body);
72
+ };
73
+ try {
74
+ if (req.method !== "POST") {
75
+ closeIfBodyUnread();
76
+ res.setHeader("allow", "POST");
77
+ if (req.method === "HEAD")
78
+ res.writeHead(405, JSON_HEADERS).end();
79
+ else
80
+ send(405, rpcError(-32600, "Use POST."));
81
+ return;
82
+ }
83
+ if (!gatewayUrl) {
84
+ closeIfBodyUnread();
85
+ send(503, JSON.stringify({ error: "mcp_unavailable", message: MCP_UNAVAILABLE_MESSAGE }));
86
+ return;
87
+ }
88
+ const token = /^Bearer (\S+)$/.exec(req.headers.authorization ?? "")?.[1];
89
+ const session = isMcpToken(token)
90
+ ? await options.ledger.findSessionByMcpTokenHash(mcpTokenHash(token))
91
+ : undefined;
92
+ const at = now().getTime();
93
+ if (!session || session.status !== "active" || at >= session.deadlineAt.getTime()) {
94
+ // No WWW-Authenticate: a client must not start OAuth discovery against this route.
95
+ closeIfBodyUnread();
96
+ send(401, rpcError(-32001, MCP_TOKEN_REQUIRED_MESSAGE));
97
+ return;
98
+ }
99
+ seen.sessionId = session.id;
100
+ if (req.headers["content-type"]?.split(";", 1)[0].trim().toLowerCase() !== "application/json") {
101
+ closeIfBodyUnread();
102
+ send(415, rpcError(-32600, "Send one JSON-RPC message as application/json."));
103
+ return;
104
+ }
105
+ let body;
106
+ try {
107
+ body = await readBodyBytes(req, HTTP_LIMITS.json, AbortSignal.timeout(HTTP_LIMITS.requestMs));
108
+ }
109
+ catch (error) {
110
+ closeIfBodyUnread();
111
+ send(error instanceof HttpBoundaryError ? error.status : 400, rpcError(-32600, "The request body could not be read."));
112
+ return;
113
+ }
114
+ seen.method = methodOf(body);
115
+ // The call may take as long as it needs, up to the run's deadline or MCP_FORWARD_MAX_MS. The
116
+ // server's requestTimeout covers receiving the request only, which is over by now.
117
+ const controller = new AbortController();
118
+ const timer = setTimeout(() => controller.abort(new GatewayError("gateway_timeout")), Math.min(MCP_FORWARD_MAX_MS, session.deadlineAt.getTime() - at));
119
+ const clientGone = () => {
120
+ if (!res.writableFinished)
121
+ controller.abort(new GatewayError("client_disconnected"));
122
+ };
123
+ res.once("close", clientGone);
124
+ try {
125
+ const upstream = await fetchImpl(`${gatewayUrl}${GATEWAY_PATH}`, {
126
+ method: "POST",
127
+ headers: { authorization: req.headers.authorization, "content-type": "application/json" },
128
+ body: new Uint8Array(body),
129
+ redirect: "error",
130
+ signal: controller.signal,
131
+ });
132
+ const answer = await readGatewayBody(upstream);
133
+ send(upstream.status, answer, {
134
+ "content-type": upstream.headers.get("content-type") ?? "application/json",
135
+ "cache-control": "no-store",
136
+ });
137
+ }
138
+ catch (error) {
139
+ const reason = controller.signal.reason instanceof GatewayError ? controller.signal.reason.reason : undefined;
140
+ throw error instanceof GatewayError ? error : new GatewayError(reason ?? "gateway_unreachable");
141
+ }
142
+ finally {
143
+ clearTimeout(timer);
144
+ res.off("close", clientGone);
145
+ }
146
+ }
147
+ catch (error) {
148
+ // Nothing of the error's text leaves or is logged: only a fixed reason code.
149
+ seen.reason = error instanceof GatewayError ? error.reason : "gateway_unreachable";
150
+ if (!res.headersSent)
151
+ send(502, JSON.stringify({ error: "mcp_unavailable", message: MCP_UNREACHABLE_MESSAGE }));
152
+ else
153
+ res.destroy();
154
+ }
155
+ finally {
156
+ log.info({
157
+ event: "mcp.request",
158
+ ...(seen.sessionId ? { sessionId: seen.sessionId } : {}),
159
+ ...(seen.method ? { method: seen.method } : {}),
160
+ status: res.statusCode,
161
+ ...(seen.reason ? { reason: seen.reason } : {}),
162
+ durationMs: Math.round(performance.now() - startedAt),
163
+ }, "coding proxy mcp request");
164
+ }
165
+ };
166
+ }