@wardby/cli 0.7.0 → 0.9.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 (367) hide show
  1. package/.env.example +5 -5
  2. package/README.md +6 -1
  3. package/deploy/local/docker-compose.quickstart-coding.yml +11 -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 +64 -0
  9. package/dist/claude-plugin/command.js +379 -0
  10. package/dist/cli-help.d.ts +1 -1
  11. package/dist/cli-help.js +2 -1
  12. package/dist/cli.js +71 -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 +3 -9
  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 +39 -11
  46. package/dist/core/engine-native.d.ts +2 -0
  47. package/dist/core/engine-native.js +13 -0
  48. package/dist/core/error-code.js +2 -0
  49. package/dist/core/grants.d.ts +28 -3
  50. package/dist/core/grants.js +46 -4
  51. package/dist/core/host-events.js +7 -3
  52. package/dist/core/host-status.js +2 -2
  53. package/dist/core/issue-status.js +4 -3
  54. package/dist/core/listen-address.d.ts +10 -0
  55. package/dist/core/listen-address.js +14 -0
  56. package/dist/core/mark-ready.d.ts +1 -1
  57. package/dist/core/mark-ready.js +1 -1
  58. package/dist/core/mcp-client/client.d.ts +47 -0
  59. package/dist/core/mcp-client/client.js +141 -0
  60. package/dist/core/mcp-client/jsonrpc.d.ts +15 -0
  61. package/dist/core/mcp-client/jsonrpc.js +54 -0
  62. package/dist/core/mcp-client/results.d.ts +13 -0
  63. package/dist/core/mcp-client/results.js +42 -0
  64. package/dist/core/mcp-servers.d.ts +90 -0
  65. package/dist/core/mcp-servers.js +230 -0
  66. package/dist/core/mcp-tools.d.ts +108 -0
  67. package/dist/core/mcp-tools.js +287 -0
  68. package/dist/core/merge-order-check.js +38 -19
  69. package/dist/core/notification-dispatcher.js +2 -1
  70. package/dist/core/power-idle.d.ts +8 -2
  71. package/dist/core/power-idle.js +17 -3
  72. package/dist/core/power.d.ts +5 -0
  73. package/dist/core/power.js +5 -0
  74. package/dist/core/related-pull-requests.js +26 -10
  75. package/dist/core/retention-days.d.ts +5 -0
  76. package/dist/core/retention-days.js +14 -0
  77. package/dist/core/review-fix.js +2 -2
  78. package/dist/core/review-host-tools.js +3 -1
  79. package/dist/core/run-steps/config.d.ts +25 -0
  80. package/dist/core/run-steps/config.js +40 -0
  81. package/dist/core/run-steps/control-chars.d.ts +16 -0
  82. package/dist/core/run-steps/control-chars.js +26 -0
  83. package/dist/core/run-steps/detail.d.ts +32 -0
  84. package/dist/core/run-steps/detail.js +240 -0
  85. package/dist/core/run-steps/extract.d.ts +52 -0
  86. package/dist/core/run-steps/extract.js +467 -0
  87. package/dist/core/run-steps/ingress.d.ts +51 -0
  88. package/dist/core/run-steps/ingress.js +160 -0
  89. package/dist/core/run-steps/last-step.d.ts +15 -0
  90. package/dist/core/run-steps/last-step.js +35 -0
  91. package/dist/core/run-steps/native.d.ts +48 -0
  92. package/dist/core/run-steps/native.js +164 -0
  93. package/dist/core/run-steps/seen.d.ts +16 -0
  94. package/dist/core/run-steps/seen.js +35 -0
  95. package/dist/core/run-steps/summarize.d.ts +57 -0
  96. package/dist/core/run-steps/summarize.js +304 -0
  97. package/dist/core/run-steps/sweep.d.ts +21 -0
  98. package/dist/core/run-steps/sweep.js +41 -0
  99. package/dist/core/run-steps/types.d.ts +89 -0
  100. package/dist/core/run-steps/types.js +39 -0
  101. package/dist/core/run-steps/wire.d.ts +35 -0
  102. package/dist/core/run-steps/wire.js +47 -0
  103. package/dist/core/run-steps/writer.d.ts +49 -0
  104. package/dist/core/run-steps/writer.js +279 -0
  105. package/dist/core/runner.d.ts +70 -16
  106. package/dist/core/runner.js +296 -74
  107. package/dist/core/secret-broker-config.d.ts +2 -0
  108. package/dist/core/secret-broker-config.js +1 -1
  109. package/dist/core/secret-guard.d.ts +5 -2
  110. package/dist/core/secret-guard.js +22 -7
  111. package/dist/core/secrets.d.ts +11 -1
  112. package/dist/core/secrets.js +27 -1
  113. package/dist/core/serializable-retry.d.ts +18 -0
  114. package/dist/core/serializable-retry.js +43 -0
  115. package/dist/core/tool-admin.d.ts +5 -1
  116. package/dist/core/tool-admin.js +7 -2
  117. package/dist/core/tool-names.d.ts +13 -0
  118. package/dist/core/tool-names.js +20 -0
  119. package/dist/core/webhooks.d.ts +4 -0
  120. package/dist/core/webhooks.js +68 -49
  121. package/dist/core/workflow-run-events.js +2 -1
  122. package/dist/generated/prisma/browser.d.ts +39 -0
  123. package/dist/generated/prisma/client.d.ts +39 -0
  124. package/dist/generated/prisma/commonInputTypes.d.ts +80 -50
  125. package/dist/generated/prisma/enums.d.ts +6 -0
  126. package/dist/generated/prisma/enums.js +5 -0
  127. package/dist/generated/prisma/internal/class.d.ts +77 -0
  128. package/dist/generated/prisma/internal/class.js +4 -4
  129. package/dist/generated/prisma/internal/prismaNamespace.d.ts +632 -1
  130. package/dist/generated/prisma/internal/prismaNamespace.js +94 -3
  131. package/dist/generated/prisma/internal/prismaNamespaceBrowser.d.ts +98 -0
  132. package/dist/generated/prisma/internal/prismaNamespaceBrowser.js +94 -3
  133. package/dist/generated/prisma/models/Agent.d.ts +372 -1
  134. package/dist/generated/prisma/models/AgentMcpServer.d.ts +1412 -0
  135. package/dist/generated/prisma/models/AgentMcpServer.js +1 -0
  136. package/dist/generated/prisma/models/AgentRepository.d.ts +40 -1
  137. package/dist/generated/prisma/models/AuthUser.d.ts +1 -1
  138. package/dist/generated/prisma/models/CodingAgentProfile.d.ts +0 -7
  139. package/dist/generated/prisma/models/CodingProxySession.d.ts +159 -2
  140. package/dist/generated/prisma/models/CodingRun.d.ts +76 -1
  141. package/dist/generated/prisma/models/HostProvider.d.ts +2176 -0
  142. package/dist/generated/prisma/models/HostProvider.js +1 -0
  143. package/dist/generated/prisma/models/HostProviderCredential.d.ts +1285 -0
  144. package/dist/generated/prisma/models/HostProviderCredential.js +1 -0
  145. package/dist/generated/prisma/models/HostProviderOperation.d.ts +1470 -0
  146. package/dist/generated/prisma/models/HostProviderOperation.js +1 -0
  147. package/dist/generated/prisma/models/HostProviderTrustedAccount.d.ts +1145 -0
  148. package/dist/generated/prisma/models/HostProviderTrustedAccount.js +1 -0
  149. package/dist/generated/prisma/models/McpServer.d.ts +1500 -0
  150. package/dist/generated/prisma/models/McpServer.js +1 -0
  151. package/dist/generated/prisma/models/NativeGatewayCall.d.ts +39 -1
  152. package/dist/generated/prisma/models/Principal.d.ts +167 -0
  153. package/dist/generated/prisma/models/ResourceGrant.d.ts +1 -1
  154. package/dist/generated/prisma/models/Run.d.ts +503 -1
  155. package/dist/generated/prisma/models/RunStep.d.ts +1453 -0
  156. package/dist/generated/prisma/models/RunStep.js +1 -0
  157. package/dist/generated/prisma/models/Secret.d.ts +387 -0
  158. package/dist/generated/prisma/models/Tool.d.ts +223 -1
  159. package/dist/generated/prisma/models.d.ts +7 -0
  160. package/dist/help/search.js +12 -3
  161. package/dist/help-index.json +812 -52
  162. package/dist/import/create.js +16 -1
  163. package/dist/import/neutral-schema.d.ts +1 -0
  164. package/dist/import/neutral-schema.js +2 -0
  165. package/dist/mcp/auth/access.d.ts +1 -0
  166. package/dist/mcp/auth/grants-cli.d.ts +6 -4
  167. package/dist/mcp/auth/grants-cli.js +42 -3
  168. package/dist/mcp/auth/resource-server.d.ts +3 -1
  169. package/dist/mcp/auth/resource-server.js +17 -2
  170. package/dist/mcp/context.d.ts +6 -0
  171. package/dist/mcp/index.js +21 -0
  172. package/dist/mcp/tools/agents.js +135 -20
  173. package/dist/mcp/tools/grants.js +66 -28
  174. package/dist/mcp/tools/host-providers.d.ts +2 -0
  175. package/dist/mcp/tools/host-providers.js +215 -0
  176. package/dist/mcp/tools/mcp-servers.d.ts +26 -0
  177. package/dist/mcp/tools/mcp-servers.js +481 -0
  178. package/dist/mcp/tools/power.js +2 -2
  179. package/dist/mcp/tools/repositories.js +50 -10
  180. package/dist/mcp/tools/run-access.d.ts +7 -0
  181. package/dist/mcp/tools/run-access.js +14 -0
  182. package/dist/mcp/tools/run-steps.d.ts +16 -0
  183. package/dist/mcp/tools/run-steps.js +85 -0
  184. package/dist/mcp/tools/runs.js +186 -18
  185. package/dist/mcp/tools/secret-elicitation.d.ts +2 -0
  186. package/dist/mcp/tools/secret-elicitation.js +25 -2
  187. package/dist/mcp/tools/secrets.js +38 -4
  188. package/dist/mcp/tools/tools.d.ts +6 -4
  189. package/dist/mcp/tools/tools.js +40 -8
  190. package/dist/mcp/tools/trigger.js +28 -6
  191. package/dist/mcp/transport/http-limits.d.ts +2 -0
  192. package/dist/mcp/transport/http-limits.js +6 -2
  193. package/dist/mcp/webhooks/ingress.js +10 -0
  194. package/dist/native-worker/gateway-server.d.ts +18 -1
  195. package/dist/native-worker/gateway-server.js +38 -9
  196. package/dist/native-worker/gateway.d.ts +5 -0
  197. package/dist/native-worker/gateway.js +13 -1
  198. package/dist/native-worker/http-server.d.ts +6 -4
  199. package/dist/native-worker/http-server.js +53 -8
  200. package/dist/native-worker/ledger.d.ts +6 -2
  201. package/dist/native-worker/ledger.js +5 -4
  202. package/dist/native-worker/protocol.d.ts +1 -0
  203. package/dist/native-worker/protocol.js +4 -1
  204. package/dist/providers/coding-proxy/capability-hash.d.ts +2 -0
  205. package/dist/providers/coding-proxy/capability-hash.js +5 -0
  206. package/dist/providers/coding-proxy/mcp-route.d.ts +34 -0
  207. package/dist/providers/coding-proxy/mcp-route.js +166 -0
  208. package/dist/providers/coding-proxy/memory-ledger.d.ts +2 -0
  209. package/dist/providers/coding-proxy/memory-ledger.js +16 -0
  210. package/dist/providers/coding-proxy/prisma-ledger.d.ts +1 -0
  211. package/dist/providers/coding-proxy/prisma-ledger.js +36 -2
  212. package/dist/providers/coding-proxy/proxy.d.ts +33 -1
  213. package/dist/providers/coding-proxy/proxy.js +266 -75
  214. package/dist/providers/coding-proxy/run-step-forwarder.d.ts +42 -0
  215. package/dist/providers/coding-proxy/run-step-forwarder.js +206 -0
  216. package/dist/providers/coding-proxy/runtime.d.ts +13 -0
  217. package/dist/providers/coding-proxy/runtime.js +87 -4
  218. package/dist/providers/coding-proxy/server.d.ts +3 -0
  219. package/dist/providers/coding-proxy/server.js +10 -0
  220. package/dist/providers/coding-proxy/types.d.ts +18 -0
  221. package/dist/providers/engine/types.d.ts +26 -0
  222. package/dist/providers/executor/composition.js +5 -0
  223. package/dist/providers/executor/container.d.ts +47 -1
  224. package/dist/providers/executor/container.js +84 -8
  225. package/dist/providers/host-tools/invoke.d.ts +37 -0
  226. package/dist/providers/host-tools/invoke.js +117 -0
  227. package/dist/providers/host-tools/operations.d.ts +65 -0
  228. package/dist/providers/host-tools/operations.js +149 -0
  229. package/dist/providers/host-tools/registry.d.ts +79 -0
  230. package/dist/providers/host-tools/registry.js +362 -0
  231. package/dist/providers/host-tools/request-guard.d.ts +17 -0
  232. package/dist/providers/host-tools/request-guard.js +99 -0
  233. package/dist/providers/host-tools/self-test.d.ts +64 -0
  234. package/dist/providers/host-tools/self-test.js +142 -0
  235. package/dist/providers/jobs/docker-isolation.d.ts +2 -2
  236. package/dist/providers/jobs/docker-isolation.js +30 -8
  237. package/dist/providers/jobs/docker.d.ts +8 -0
  238. package/dist/providers/jobs/docker.js +33 -9
  239. package/dist/providers/jobs/kubernetes-isolation.d.ts +3 -1
  240. package/dist/providers/jobs/kubernetes-isolation.js +15 -2
  241. package/dist/providers/jobs/kubernetes.d.ts +13 -0
  242. package/dist/providers/jobs/kubernetes.js +28 -5
  243. package/dist/providers/jobs/types.d.ts +6 -0
  244. package/dist/providers/review-host/types.d.ts +6 -4
  245. package/dist/providers/review-host/types.js +1 -1
  246. package/dist/providers/vcs/git.d.ts +6 -2
  247. package/dist/providers/vcs/git.js +15 -4
  248. package/dist/providers/vcs/github.d.ts +2 -71
  249. package/dist/providers/vcs/github.js +8 -11
  250. package/dist/providers/vcs/index.d.ts +1 -1
  251. package/dist/providers/vcs/index.js +1 -1
  252. package/dist/providers/vcs/remote.d.ts +4 -0
  253. package/dist/providers/vcs/remote.js +7 -1
  254. package/dist/providers/vcs/routing.d.ts +12 -6
  255. package/dist/providers/vcs/routing.js +21 -6
  256. package/dist/providers/vcs/types.d.ts +71 -3
  257. package/dist/quickstart/index.d.ts +5 -0
  258. package/dist/quickstart/index.js +21 -0
  259. package/dist/quickstart-images.json +1 -1
  260. package/dist/sandbox/eval-core.d.ts +12 -2
  261. package/dist/sandbox/eval-core.js +20 -4
  262. package/dist/sandbox/host-functions.d.ts +4 -0
  263. package/dist/sandbox/host-functions.js +7 -2
  264. package/dist/sandbox/limits.d.ts +4 -1
  265. package/dist/sandbox/limits.js +4 -1
  266. package/dist/sandbox/run-in-sandbox.d.ts +6 -0
  267. package/dist/sandbox/run-in-sandbox.js +14 -2
  268. package/dist/sandbox/safe-fetch.d.ts +2 -0
  269. package/dist/sandbox/safe-fetch.js +1 -1
  270. package/dist/sandbox/secret-broker.d.ts +1 -0
  271. package/dist/sandbox/secret-broker.js +1 -1
  272. package/dist/sandbox/tool-timeout.d.ts +32 -0
  273. package/dist/sandbox/tool-timeout.js +52 -0
  274. package/dist/sandbox/user-tool.d.ts +8 -1
  275. package/dist/sandbox/user-tool.js +9 -3
  276. package/dist/serve.js +26 -18
  277. package/dist/viewer/api-schema.d.ts +66 -0
  278. package/dist/viewer/api-schema.js +31 -0
  279. package/dist/viewer/graph.js +2 -1
  280. package/dist/viewer/openapi.js +2 -1
  281. package/dist/viewer/run-detail.d.ts +2 -0
  282. package/dist/viewer/run-detail.js +26 -1
  283. package/dist/wardby-bin.js +5 -0
  284. package/docs/README.md +11 -1
  285. package/docs/agent-recipes.md +37 -4
  286. package/docs/architecture-runtime.md +1 -1
  287. package/docs/claude-code-plugin.md +208 -0
  288. package/docs/code-review-agents.md +6 -0
  289. package/docs/coding-agent-setup.md +94 -1
  290. package/docs/coding-worker-byo-images.md +4 -2
  291. package/docs/coding-worker-isolation.md +94 -7
  292. package/docs/debugging-agent-runs.md +354 -0
  293. package/docs/getting-started-gke.md +33 -11
  294. package/docs/getting-started-identity-provider.md +43 -37
  295. package/docs/getting-started.md +8 -0
  296. package/docs/host-providers.md +294 -0
  297. package/docs/mcp-servers.md +263 -0
  298. package/docs/native-sandbox.md +31 -9
  299. package/docs/security-deployment.md +161 -31
  300. package/docs/tool-secrets.md +3 -0
  301. package/docs/viewer-api.md +27 -12
  302. package/docs/viewer-api.openapi.yml +78 -2
  303. package/help/admin-viewer.md +5 -0
  304. package/help/agent-recipes.md +33 -2
  305. package/help/brokered-secrets.md +3 -0
  306. package/help/build-claude-code-image.md +1 -1
  307. package/help/claude-code-run-pane.md +63 -0
  308. package/help/deploy-gke.md +5 -0
  309. package/help/errors/coding-mcp-gateway-unavailable.md +46 -0
  310. package/help/errors/coding-mcp-pins-unavailable.md +35 -0
  311. package/help/errors/database-auth-invalid.md +1 -1
  312. package/help/errors/host-provider-error.md +40 -0
  313. package/help/errors/host-tool-failed.md +36 -0
  314. package/help/errors/mcp-call-limit.md +22 -0
  315. package/help/errors/mcp-call-timeout.md +22 -0
  316. package/help/errors/mcp-server-auth-failed.md +22 -0
  317. package/help/errors/mcp-server-credentials-unavailable.md +45 -0
  318. package/help/errors/mcp-server-error.md +28 -0
  319. package/help/errors/mcp-server-unreachable.md +22 -0
  320. package/help/errors/mcp-servers-off.md +21 -0
  321. package/help/errors/mcp-tool-not-allowed.md +26 -0
  322. package/help/errors/mcp-tool-removed.md +22 -0
  323. package/help/errors/mcp-unavailable.md +25 -0
  324. package/help/errors/native-sandbox-capacity.md +1 -1
  325. package/help/errors/native-sandbox-deadline-exceeded.md +1 -1
  326. package/help/errors/native-sandbox-docker-failed.md +1 -1
  327. package/help/errors/native-sandbox-gateway-error.md +1 -1
  328. package/help/errors/native-sandbox-gateway-unavailable.md +1 -1
  329. package/help/errors/native-sandbox-image-not-pinned.md +1 -1
  330. package/help/errors/native-sandbox-isolation-mismatch.md +1 -1
  331. package/help/errors/native-sandbox-network-unenforced.md +1 -1
  332. package/help/errors/native-sandbox-requires-catalog.md +1 -1
  333. package/help/errors/native-sandbox-unavailable.md +1 -1
  334. package/help/errors/native-sandbox-warm-delivery-failed.md +1 -1
  335. package/help/errors/native-sandbox-worker-exited.md +1 -1
  336. package/help/errors/native-sandbox-worker-lost.md +1 -1
  337. package/help/errors/native-sandbox-worker-unready.md +1 -1
  338. package/help/errors/repository-not-listed.md +36 -0
  339. package/help/errors/repository-required.md +36 -0
  340. package/help/errors/tool-timeout.md +38 -0
  341. package/help/host-providers.md +40 -0
  342. package/help/identity-and-access.md +4 -1
  343. package/help/low-power-mode.md +19 -8
  344. package/help/mcp-servers.md +73 -0
  345. package/help/mcp.md +9 -2
  346. package/help/native-capabilities.md +27 -1
  347. package/help/native-sandbox.md +1 -1
  348. package/help/operating-agents.md +16 -0
  349. package/help/related-pull-requests.md +7 -3
  350. package/help/repo-instructions-and-skills.md +3 -1
  351. package/help/resume-runs-locally.md +7 -0
  352. package/help/run-steps.md +140 -0
  353. package/help/stop-run.md +1 -1
  354. package/help/troubleshooting/coding-workers.md +9 -1
  355. package/help/troubleshooting/debugging-agent-runs.md +29 -0
  356. package/package.json +2 -1
  357. package/prisma/migrations/20261102000000_host_providers/migration.sql +56 -0
  358. package/prisma/migrations/20261104000000_coding_agent_repository_list/migration.sql +5 -0
  359. package/prisma/migrations/20261106000000_tool_timeout/migration.sql +4 -0
  360. package/prisma/migrations/20261107000000_mcp_servers/migration.sql +33 -0
  361. package/prisma/migrations/20261108000000_coding_mcp/migration.sql +5 -0
  362. package/prisma/migrations/20261109000000_task_run_index/migration.sql +3 -0
  363. package/prisma/migrations/20261110000000_run_steps/migration.sql +26 -0
  364. package/prisma/migrations/20261111000000_run_steps_viewer_notify/migration.sql +59 -0
  365. package/prisma/migrations/20261112000000_run_steps_detail/migration.sql +9 -0
  366. package/prisma/schema.prisma +211 -14
  367. package/scripts/git-askpass.sh +1 -1
@@ -0,0 +1,287 @@
1
+ import { ownerCanUseMcpServer } from "./grants.js";
2
+ import { loadMcpServersMode, MCP_SERVERS_OFF_MESSAGE } from "../config/mcp-servers.js";
3
+ import { loadToolMaxTimeoutMs } from "../config/tool-timeout.js";
4
+ import { McpClientError, McpHttpClient } from "./mcp-client/client.js";
5
+ import { toolResultText } from "./mcp-client/results.js";
6
+ import { exposedToolName, loadServerSecrets, logCredentialsUnavailable, McpCredentialsUnavailableError, McpServerInputError, redactForServer, serverFetch, snapshotOf, } from "./mcp-servers.js";
7
+ import { parseSecretBrokerConfig } from "./secret-broker-config.js";
8
+ import { withEscapedForms } from "./secret-spans.js";
9
+ import { consoleRedactionValues, encodedForms } from "../sandbox/secret-broker.js";
10
+ export const DEFAULT_MCP_CALL_TIMEOUT_MS = 30_000;
11
+ /** What a grantee's run sees when the server owner's credentials can't be loaded. */
12
+ export const SHARED_CREDENTIALS_MESSAGE = "The MCP server's credentials can't be used; ask its owner to check it.";
13
+ /**
14
+ * A Codex coding agent runs shell commands that can read the run's MCP token, so it may use only tools
15
+ * the server marks read-only (annotations.readOnlyHint === true in the saved tool list).
16
+ */
17
+ export function isMarkedReadOnly(entry) {
18
+ return entry?.annotations?.readOnlyHint === true;
19
+ }
20
+ /**
21
+ * Whether a coding provider gets the Codex read-only rule. Fails closed: anything but exactly
22
+ * "claude-code" (Codex, an unknown value, or a coding agent with no profile) counts as Codex. A run
23
+ * is judged by the provider it was dispatched with (CodingRun.provider), never the agent's current
24
+ * profile, which can change between dispatch and launch.
25
+ */
26
+ export function isCodexCoding(provider) {
27
+ return provider !== "claude-code";
28
+ }
29
+ /**
30
+ * Builds pins for an agent's attachments: allowed tools present in the snapshot only, from servers the
31
+ * agent's owner may still use (owns, or holds use on); none when the mode is off.
32
+ */
33
+ export async function loadMcpToolPins(db, agentId, options = {}) {
34
+ if (loadMcpServersMode() === "off")
35
+ return [];
36
+ const rows = await db.agentMcpServer.findMany({
37
+ where: { agentId },
38
+ include: { server: true, agent: { select: { ownerId: true } } },
39
+ orderBy: [{ createdAt: "asc" }, { serverId: "asc" }],
40
+ });
41
+ const pins = [];
42
+ for (const row of rows) {
43
+ if (!(await ownerCanUseMcpServer(db, row.agent.ownerId, row.server)))
44
+ continue;
45
+ const byName = new Map(snapshotOf(row.server).map((t) => [t.name, t]));
46
+ for (const toolName of row.allowedTools) {
47
+ const entry = byName.get(toolName);
48
+ if (!entry)
49
+ continue;
50
+ // A refresh can drop readOnlyHint after attach: such a tool stays out of a Codex run.
51
+ if (options.readOnlyOnly === true && !isMarkedReadOnly(entry))
52
+ continue;
53
+ let exposedName;
54
+ try {
55
+ exposedName = exposedToolName(row.server.name, toolName);
56
+ }
57
+ catch {
58
+ continue;
59
+ }
60
+ pins.push({
61
+ exposedName,
62
+ toolName,
63
+ serverId: row.server.id,
64
+ serverName: row.server.name,
65
+ ...(row.callTimeoutMs !== null ? { callTimeoutMs: row.callTimeoutMs } : {}),
66
+ definition: { description: entry.description, inputSchema: entry.inputSchema },
67
+ });
68
+ }
69
+ }
70
+ return pins;
71
+ }
72
+ /**
73
+ * The pins whose exposed name no other tool of the run uses. Attach refuses clashes; this covers a
74
+ * name that became a built-in (or an attached tool) later: the MCP tool is left out, with a warning.
75
+ */
76
+ export function withoutClashingPins(pins, otherNames, log) {
77
+ const taken = new Set(otherNames);
78
+ return pins.filter((pin) => {
79
+ if (!taken.has(pin.exposedName)) {
80
+ taken.add(pin.exposedName);
81
+ return true;
82
+ }
83
+ log.warn({ serverId: pin.serverId, tool: pin.toolName, exposedName: pin.exposedName }, "MCP tool left out of the run: its name clashes with another tool");
84
+ return false;
85
+ });
86
+ }
87
+ export function mcpToolDefs(pins) {
88
+ return pins.map((pin) => ({
89
+ name: pin.exposedName,
90
+ description: `From MCP server ${pin.serverName}: ${pin.definition.description}`,
91
+ jsonSchema: pin.definition.inputSchema,
92
+ }));
93
+ }
94
+ function error(code, message) {
95
+ return JSON.stringify({ error: code, message });
96
+ }
97
+ function parseArgs(argsJson) {
98
+ if (argsJson.trim() === "")
99
+ return {};
100
+ try {
101
+ const value = JSON.parse(argsJson);
102
+ return value !== null && typeof value === "object" && !Array.isArray(value)
103
+ ? value
104
+ : undefined;
105
+ }
106
+ catch {
107
+ return undefined;
108
+ }
109
+ }
110
+ /**
111
+ * Never throws. Re-checks the live attachment, validates the arguments, runs initialize → tools/call →
112
+ * close under the time limit, and returns the redacted result text or a JSON error. One log line per call.
113
+ */
114
+ export async function handleMcpTool(pin, argsJson, ctx) {
115
+ const startedAt = Date.now();
116
+ const done = (text, outcome) => {
117
+ ctx.log.info({
118
+ runId: ctx.runId,
119
+ agentId: ctx.agentId,
120
+ serverId: pin.serverId,
121
+ tool: pin.toolName,
122
+ outcome,
123
+ durationMs: Date.now() - startedAt,
124
+ resultChars: text.length,
125
+ }, "mcp tool call");
126
+ return text;
127
+ };
128
+ const fail = (code, message) => done(error(code, message), code);
129
+ let values = [];
130
+ try {
131
+ if (loadMcpServersMode() === "off")
132
+ return fail("mcp_servers_off", MCP_SERVERS_OFF_MESSAGE);
133
+ const row = await ctx.db.agentMcpServer.findUnique({
134
+ where: { agentId_serverId: { agentId: ctx.agentId, serverId: pin.serverId } },
135
+ include: { server: true, agent: { select: { ownerId: true } } },
136
+ });
137
+ if (!row || !row.allowedTools.includes(pin.toolName)) {
138
+ return fail("mcp_tool_not_allowed", `The tool "${pin.exposedName}" is no longer allowed for this agent: its MCP server was detached or the tool was taken off the allowed list.`);
139
+ }
140
+ // A revoked or lowered grant stops the very next call, even before the attachment is pruned.
141
+ if (!(await ownerCanUseMcpServer(ctx.db, row.agent.ownerId, row.server))) {
142
+ return fail("mcp_tool_not_allowed", `The tool "${pin.exposedName}" is no longer allowed for this agent: its owner no longer has use access to MCP server "${row.server.name}".`);
143
+ }
144
+ const live = snapshotOf(row.server).find((t) => t.name === pin.toolName);
145
+ if (!live) {
146
+ return fail("mcp_tool_removed", `The tool "${pin.exposedName}" is no longer in MCP server "${row.server.name}"'s tool list.`);
147
+ }
148
+ if (ctx.readOnlyOnly === true && !isMarkedReadOnly(live)) {
149
+ return fail("mcp_tool_not_allowed", `The tool "${pin.exposedName}" is no longer allowed for this agent: Codex agents can use only tools the server marks read-only.`);
150
+ }
151
+ const args = parseArgs(argsJson);
152
+ if (!args)
153
+ return fail("invalid_arguments", "The tool's arguments must be a JSON object.");
154
+ const configured = Math.min(row.callTimeoutMs ?? pin.callTimeoutMs ?? DEFAULT_MCP_CALL_TIMEOUT_MS, loadToolMaxTimeoutMs());
155
+ const untilDeadline = ctx.deadlineAt ? ctx.deadlineAt.getTime() - Date.now() : Infinity;
156
+ const limitMs = Math.max(0, Math.min(configured, untilDeadline));
157
+ const timedOut = `The MCP server did not answer within ${limitMs} ms.`;
158
+ if (ctx.signal.aborted)
159
+ return fail("mcp_call_timeout", "The call was stopped.");
160
+ if (limitMs <= 0)
161
+ return fail("mcp_call_timeout", "The run's time limit has been reached.");
162
+ const server = {
163
+ id: row.server.id,
164
+ ownerId: row.server.ownerId,
165
+ name: row.server.name,
166
+ url: row.server.url,
167
+ secretNames: row.server.secretNames,
168
+ };
169
+ let secrets;
170
+ try {
171
+ secrets = await loadServerSecrets(ctx.db, ctx.cipher, server);
172
+ }
173
+ catch (err) {
174
+ const shared = row.agent.ownerId !== server.ownerId;
175
+ if (err instanceof McpCredentialsUnavailableError) {
176
+ logCredentialsUnavailable(ctx.log, err);
177
+ return fail(err.code, shared ? SHARED_CREDENTIALS_MESSAGE : err.message);
178
+ }
179
+ // The detail names the server owner's secrets and speaks to that owner: a grantee's run gets neither.
180
+ if (err instanceof McpServerInputError && shared) {
181
+ return fail("mcp_server_error", SHARED_CREDENTIALS_MESSAGE);
182
+ }
183
+ throw err;
184
+ }
185
+ values = secrets.values;
186
+ const controller = new AbortController();
187
+ let stopped = false;
188
+ const onStop = () => {
189
+ stopped = true;
190
+ controller.abort();
191
+ };
192
+ ctx.signal.addEventListener("abort", onStop, { once: true });
193
+ const timer = setTimeout(() => controller.abort(), limitMs);
194
+ // The race ends the call even if a transport ignores the signal.
195
+ const aborted = new Promise((_resolve, reject) => {
196
+ controller.signal.addEventListener("abort", () => reject(new McpClientError("mcp_call_timeout", "aborted")), {
197
+ once: true,
198
+ });
199
+ });
200
+ aborted.catch(() => { });
201
+ // A stop that fired while the secrets were loading came before the listener; act on it now.
202
+ if (ctx.signal.aborted)
203
+ onStop();
204
+ const client = new McpHttpClient({
205
+ fetch: serverFetch(server, secrets.accessor, { timeoutMs: limitMs, fetchImpl: ctx.fetchImpl }),
206
+ signal: controller.signal,
207
+ });
208
+ try {
209
+ const result = await Promise.race([
210
+ (async () => {
211
+ await client.initialize();
212
+ return client.callTool(pin.toolName, args);
213
+ })(),
214
+ aborted,
215
+ ]);
216
+ const { text, isError } = toolResultText(result, (raw) => redactForServer(raw, values));
217
+ return done(text, isError ? "mcp_server_error" : "ok");
218
+ }
219
+ catch (err) {
220
+ if (controller.signal.aborted) {
221
+ return fail("mcp_call_timeout", stopped ? "The call was stopped." : timedOut);
222
+ }
223
+ throw err;
224
+ }
225
+ finally {
226
+ clearTimeout(timer);
227
+ ctx.signal.removeEventListener("abort", onStop);
228
+ // Best effort; never waits on a server that already ran out of time.
229
+ if (controller.signal.aborted)
230
+ void client.close().catch(() => { });
231
+ else
232
+ await client.close().catch(() => { });
233
+ }
234
+ }
235
+ catch (err) {
236
+ const message = redactForServer(err instanceof Error ? err.message : "The MCP server call failed.", values);
237
+ if (err instanceof McpClientError)
238
+ return fail(err.code, message);
239
+ if (err instanceof McpServerInputError)
240
+ return fail("mcp_server_error", message);
241
+ return fail("mcp_server_unreachable", "The MCP server could not be reached.");
242
+ }
243
+ }
244
+ /**
245
+ * Every secret value of every server attached to the agent, named `mcp:<server>/<secret>`, in
246
+ * every form the server's own redaction looks for. A secret that no longer exists has no value to
247
+ * leak and is skipped; a read that fails rejects (callers fail closed).
248
+ */
249
+ export async function mcpNamedSecretValues(db, cipher, agentId) {
250
+ const rows = await db.agentMcpServer.findMany({
251
+ where: { agentId },
252
+ select: { server: { select: { name: true, ownerId: true, secretNames: true } } },
253
+ });
254
+ const named = [];
255
+ for (const { server } of rows) {
256
+ for (const secretName of server.secretNames) {
257
+ const stored = await db.secret.findUnique({
258
+ where: { ownerId_name: { ownerId: server.ownerId, name: secretName } },
259
+ select: { ciphertext: true, broker: true },
260
+ });
261
+ if (!stored)
262
+ continue;
263
+ const value = await cipher.decrypt(stored.ciphertext);
264
+ const base = consoleRedactionValues({ value, broker: readableBroker(stored.broker) });
265
+ named.push({
266
+ name: `mcp:${server.name}/${secretName}`,
267
+ values: withEscapedForms([...new Set([...base, ...base.flatMap(encodedForms)])]),
268
+ });
269
+ }
270
+ }
271
+ return named;
272
+ }
273
+ /** A stored broker config, or null when there is none or it cannot be read (the value alone is then redacted). */
274
+ function readableBroker(stored) {
275
+ if (stored === null || stored === undefined)
276
+ return null;
277
+ try {
278
+ return parseSecretBrokerConfig(stored);
279
+ }
280
+ catch {
281
+ return null;
282
+ }
283
+ }
284
+ /** Secret values for every attached server of this agent (for run-text masking and gateway cause checks). */
285
+ export async function mcpSecretValuesForAgent(db, cipher, agentId) {
286
+ return (await mcpNamedSecretValues(db, cipher, agentId)).flatMap((n) => n.values);
287
+ }
@@ -1,4 +1,4 @@
1
- import { CODING_CODE_PROVIDER, normalizeGitHubRepository } from "../coding/protocol.js";
1
+ import { codeProviderOf, normalizeHostedRepository, pullRequestWebUrl } from "../coding/code-host.js";
2
2
  import { logger } from "./logger.js";
3
3
  import { maybeMarkReady } from "./mark-ready.js";
4
4
  import { collectRelatedPullRequests, runTreeRoot } from "./related-pull-requests.js";
@@ -31,18 +31,31 @@ export function mergeOrderVerdict(self, set) {
31
31
  return { status: "in_progress", waitingFor };
32
32
  return { status: "success", dependencies };
33
33
  }
34
- const prUrl = (repository, number) => `https://github.com/${repository}/pull/${number}`;
35
- /** `repository#number` as a link: never a title or any other free text from the host. */
36
- const prLink = (m) => `[${m.repository}#${m.number}](${prUrl(m.repository, m.number)})`;
34
+ /** `repository#number`, as a link when the host has one: never a title or any other free text from the host. */
35
+ const prLink = (m) => {
36
+ const label = `${m.repository}#${m.number}`;
37
+ const url = pullRequestWebUrl(codeProviderOf(m.repository), m.repository, m.number);
38
+ return url ? `[${label}](${url})` : label;
39
+ };
37
40
  const keyOf = (repository, number) => `${repository}#${number}`;
38
41
  function safeRepository(value) {
39
42
  try {
40
- return normalizeGitHubRepository(value);
43
+ return normalizeHostedRepository(value);
41
44
  }
42
45
  catch {
43
46
  return undefined;
44
47
  }
45
48
  }
49
+ const canPostCheck = (host) => host?.upsertNamedCheck !== undefined && host.pullRequestOrigin !== undefined;
50
+ /** The host that can post this repository's merge order check, if any. */
51
+ function checkHost(hosts, repository) {
52
+ const host = hosts?.[codeProviderOf(repository)];
53
+ return canPostCheck(host) ? host : undefined;
54
+ }
55
+ /** Whether any configured host can post the check; none means nothing to read or sync. */
56
+ function anyCheckHost(hosts) {
57
+ return Object.values(hosts ?? {}).some(canPostCheck);
58
+ }
46
59
  function originState(origin) {
47
60
  if (origin.merged)
48
61
  return "merged";
@@ -109,8 +122,7 @@ export function mergeOrderCheckInput(self, verdict, steps, headSha, leadRunning
109
122
  */
110
123
  export async function syncMergeOrderChecks(deps, seedRunId, opts = {}) {
111
124
  const log = deps.log ?? moduleLog;
112
- const host = deps.hosts?.[CODING_CODE_PROVIDER];
113
- if (!host?.upsertNamedCheck || !host.pullRequestOrigin)
125
+ if (!anyCheckHost(deps.hosts))
114
126
  return;
115
127
  try {
116
128
  const group = await collectRelatedPullRequests(deps.db, seedRunId);
@@ -154,9 +166,15 @@ export async function syncMergeOrderChecks(deps, seedRunId, opts = {}) {
154
166
  continue;
155
167
  }
156
168
  try {
157
- const origin = reused && key === keyOf(reused, opts.origin.number)
158
- ? await opts.origin.read()
159
- : await host.pullRequestOrigin(pr.repository, pr.number);
169
+ let origin;
170
+ // Reading only needs the read: a host that can't post checks still reports its pull requests.
171
+ const memberHost = deps.hosts?.[codeProviderOf(pr.repository)];
172
+ if (reused && key === keyOf(reused, opts.origin.number))
173
+ origin = await opts.origin.read();
174
+ else if (memberHost?.pullRequestOrigin)
175
+ origin = await memberHost.pullRequestOrigin(pr.repository, pr.number);
176
+ else
177
+ throw new Error("no_host_for_repository");
160
178
  let headSha;
161
179
  if (origin.markerRunId) {
162
180
  const opener = await deps.db.codingRun.findUnique({
@@ -186,9 +204,12 @@ export async function syncMergeOrderChecks(deps, seedRunId, opts = {}) {
186
204
  const verdict = mergeOrderVerdict(self, set);
187
205
  if (verdict.status === "skip")
188
206
  continue;
207
+ const selfHost = checkHost(deps.hosts, self.repository);
208
+ if (!selfHost)
209
+ continue;
189
210
  const input = mergeOrderCheckInput(self, verdict, steps, target.headSha, leadRunning);
190
211
  try {
191
- await host.upsertNamedCheck(self.repository, input);
212
+ await selfHost.upsertNamedCheck(self.repository, input);
192
213
  log.info({ seedRunId, repository: self.repository, number: self.number, status: verdict.status }, "merge order check posted");
193
214
  }
194
215
  catch (err) {
@@ -197,7 +218,7 @@ export async function syncMergeOrderChecks(deps, seedRunId, opts = {}) {
197
218
  }
198
219
  if (input.status === "completed" && input.conclusion === "success") {
199
220
  // The last earlier step merged: the pull request may now be ready for review (#287).
200
- await maybeMarkReady({ db: deps.db, hosts: deps.hosts, log: deps.log }, self.repository, self.number);
221
+ await maybeMarkReady({ db: deps.db, hosts: deps.hosts, log: deps.log }, self.repository, self.number, codeProviderOf(self.repository));
201
222
  }
202
223
  }
203
224
  }
@@ -212,8 +233,7 @@ export async function syncMergeOrderChecks(deps, seedRunId, opts = {}) {
212
233
  export async function syncMergeOrderChecksAfterRun(deps, run) {
213
234
  if (run.parentRunId !== null)
214
235
  return;
215
- const host = deps.hosts?.[CODING_CODE_PROVIDER];
216
- if (!host?.upsertNamedCheck || !host.pullRequestOrigin)
236
+ if (!anyCheckHost(deps.hosts))
217
237
  return;
218
238
  try {
219
239
  if (!(await deps.db.run.findFirst({ where: { parentRunId: run.id }, select: { id: true } })))
@@ -250,12 +270,12 @@ async function orderedSeeds(db, repository, number) {
250
270
  */
251
271
  export async function syncMergeOrderChecksForPullRequest(deps, pr, opts = {}) {
252
272
  const log = deps.log ?? moduleLog;
253
- const host = deps.hosts?.[CODING_CODE_PROVIDER];
254
- if (!host?.upsertNamedCheck || !host.pullRequestOrigin)
255
- return;
256
273
  const repository = safeRepository(pr.repository);
257
274
  if (!repository || !Number.isSafeInteger(pr.number) || pr.number <= 0)
258
275
  return;
276
+ // This pull request's own host need not post checks: its siblings' checks may depend on it.
277
+ if (!anyCheckHost(deps.hosts))
278
+ return;
259
279
  let roots;
260
280
  try {
261
281
  roots = [];
@@ -297,8 +317,7 @@ export function resetMergeOrderSweepForTests() {
297
317
  */
298
318
  export async function sweepMergeOrderChecks(deps, now = new Date()) {
299
319
  const log = deps.log ?? moduleLog;
300
- const host = deps.hosts?.[CODING_CODE_PROVIDER];
301
- if (!host?.upsertNamedCheck || !host.pullRequestOrigin)
320
+ if (!anyCheckHost(deps.hosts))
302
321
  return 0;
303
322
  if (now.getTime() - lastSweepAt < MERGE_ORDER_SWEEP_INTERVAL_MS)
304
323
  return 0;
@@ -34,6 +34,7 @@
34
34
  */
35
35
  import { Prisma } from "#prisma";
36
36
  import { ChatError, } from "../providers/chat/types.js";
37
+ import { pullRequestWebUrl } from "../coding/code-host.js";
37
38
  import { tryAcquireLease } from "./lease.js";
38
39
  import { logger } from "./logger.js";
39
40
  import { broadcasts, nextStatus, renderEvent, renderParent, senderName, } from "./notification-templates.js";
@@ -82,7 +83,7 @@ async function subjectOf(db, d) {
82
83
  return { label: ev.workItemKey, title: item?.title ?? null, url: item?.url ?? null };
83
84
  }
84
85
  if (ev.repository && ev.prNumber != null) {
85
- const url = ev.codeProvider === "github" ? `https://github.com/${ev.repository}/pull/${ev.prNumber}` : null;
86
+ const url = pullRequestWebUrl(ev.codeProvider ?? "", ev.repository, ev.prNumber);
86
87
  return { label: `${ev.repository}#${ev.prNumber}`, title: null, url };
87
88
  }
88
89
  if (ev.runId)
@@ -2,7 +2,9 @@
2
2
  * Low-power mode's idle rules: whether the coding proxy or native gateway has
3
3
  * had work since `since`. A target is busy while any of its runs is pending
4
4
  * (queued included) or running, or finished at or after `since`; the gateway
5
- * is also busy while any gateway session is active. powerBlockers lists the
5
+ * is also busy while any gateway session is active. The gateway's runs are
6
+ * sandboxed native runs plus coding runs with MCP tools: a coding run's MCP
7
+ * calls go coding proxy -> gateway, so it needs both. powerBlockers lists the
6
8
  * run ids that keep a target busy right now (recent finishes do not count).
7
9
  */
8
10
  import type { PrismaClient } from "#prisma";
@@ -10,5 +12,9 @@ import type { PowerTarget } from "./power.js";
10
12
  export declare function proxyBusy(db: PrismaClient, since: Date): Promise<boolean>;
11
13
  export declare function gatewayBusy(db: PrismaClient, since: Date): Promise<boolean>;
12
14
  export declare function powerBusy(db: PrismaClient): (target: PowerTarget, since: Date) => Promise<boolean>;
13
- /** Run ids keeping `target` busy right now: active or queued runs (plus active gateway sessions for the gateway). */
15
+ /**
16
+ * Run ids keeping `target` busy right now: active or queued runs (plus, for the gateway, active
17
+ * gateway sessions and active or queued coding runs with MCP tools). "Active" for a coding run is
18
+ * the proxy's rule: its run is pending or running.
19
+ */
14
20
  export declare function powerBlockers(db: PrismaClient, target: PowerTarget): Promise<string[]>;
@@ -1,4 +1,6 @@
1
1
  const ACTIVE = { in: ["pending", "running"] };
2
+ /** A coding run whose proxy session holds an MCP token: it has MCP tools, served through the gateway. */
3
+ const WITH_MCP_TOOLS = { proxySession: { is: { mcpTokenHash: { not: null } } } };
2
4
  export async function proxyBusy(db, since) {
3
5
  const n = await db.codingRun.count({
4
6
  where: { run: { OR: [{ status: ACTIVE }, { finishedAt: { gte: since } }] } },
@@ -11,20 +13,32 @@ export async function gatewayBusy(db, since) {
11
13
  });
12
14
  if (runs > 0)
13
15
  return true;
16
+ const mcpCodingRuns = await db.codingRun.count({
17
+ where: { ...WITH_MCP_TOOLS, run: { OR: [{ status: ACTIVE }, { finishedAt: { gte: since } }] } },
18
+ });
19
+ if (mcpCodingRuns > 0)
20
+ return true;
14
21
  return (await db.nativeGatewaySession.count({ where: { status: "active" } })) > 0;
15
22
  }
16
23
  export function powerBusy(db) {
17
24
  return (target, since) => (target === "proxy" ? proxyBusy(db, since) : gatewayBusy(db, since));
18
25
  }
19
- /** Run ids keeping `target` busy right now: active or queued runs (plus active gateway sessions for the gateway). */
26
+ /**
27
+ * Run ids keeping `target` busy right now: active or queued runs (plus, for the gateway, active
28
+ * gateway sessions and active or queued coding runs with MCP tools). "Active" for a coding run is
29
+ * the proxy's rule: its run is pending or running.
30
+ */
20
31
  export async function powerBlockers(db, target) {
21
32
  if (target === "proxy") {
22
33
  const rows = await db.codingRun.findMany({ where: { run: { status: ACTIVE } }, select: { runId: true } });
23
34
  return [...new Set(rows.map((r) => r.runId))].sort();
24
35
  }
25
- const [runs, sessions] = await Promise.all([
36
+ const [runs, sessions, mcpCodingRuns] = await Promise.all([
26
37
  db.run.findMany({ where: { nativeExecutionMode: "sandbox", status: ACTIVE }, select: { id: true } }),
27
38
  db.nativeGatewaySession.findMany({ where: { status: "active" }, select: { runId: true } }),
39
+ db.codingRun.findMany({ where: { ...WITH_MCP_TOOLS, run: { status: ACTIVE } }, select: { runId: true } }),
28
40
  ]);
29
- return [...new Set([...runs.map((r) => r.id), ...sessions.map((s) => s.runId)])].sort();
41
+ return [
42
+ ...new Set([...runs.map((r) => r.id), ...sessions.map((s) => s.runId), ...mcpCodingRuns.map((r) => r.runId)]),
43
+ ].sort();
30
44
  }
@@ -4,6 +4,11 @@ export type PowerTarget = "proxy" | "gateway";
4
4
  export declare const LOW_POWER_WAKE_TIMEOUT = "low_power_wake_timeout";
5
5
  export declare const LOW_POWER_SCALE_FORBIDDEN = "low_power_scale_forbidden";
6
6
  export declare const LOW_POWER_DISABLED = "low_power_disabled";
7
+ /**
8
+ * A coding run with MCP tools could not wake the native gateway that serves them: the run fails
9
+ * before its worker launches (category mcp_gateway_unavailable).
10
+ */
11
+ export declare const CODING_MCP_GATEWAY_UNAVAILABLE = "coding_mcp_gateway_unavailable";
7
12
  /** ISO-8601 time the control plane last scaled the Deployment up from zero. */
8
13
  export declare const POWER_WOKE_AT = "wardby.io/power-woke-at";
9
14
  /**
@@ -4,6 +4,11 @@ const powerLog = logger.child({ module: "power" });
4
4
  export const LOW_POWER_WAKE_TIMEOUT = "low_power_wake_timeout";
5
5
  export const LOW_POWER_SCALE_FORBIDDEN = "low_power_scale_forbidden";
6
6
  export const LOW_POWER_DISABLED = "low_power_disabled";
7
+ /**
8
+ * A coding run with MCP tools could not wake the native gateway that serves them: the run fails
9
+ * before its worker launches (category mcp_gateway_unavailable).
10
+ */
11
+ export const CODING_MCP_GATEWAY_UNAVAILABLE = "coding_mcp_gateway_unavailable";
7
12
  /** ISO-8601 time the control plane last scaled the Deployment up from zero. */
8
13
  export const POWER_WOKE_AT = "wardby.io/power-woke-at";
9
14
  /**
@@ -23,7 +23,7 @@
23
23
  * brings every later follow-up tree that continued one of its pull requests.
24
24
  */
25
25
  import { Prisma } from "#prisma";
26
- import { CODING_CODE_PROVIDER, normalizeGitHubRepository } from "../coding/protocol.js";
26
+ import { codeProviderOf, normalizeHostedRepository, pullRequestWebUrl } from "../coding/code-host.js";
27
27
  import { issueReference, } from "../providers/issue-tracker/types.js";
28
28
  import { renderRelatedSection } from "../providers/vcs/github.js";
29
29
  import { pullRequestOutcome } from "./host-status.js";
@@ -91,7 +91,7 @@ export async function runTreeRoot(db, markerRunId, repository) {
91
91
  }
92
92
  function safeRepository(value) {
93
93
  try {
94
- return normalizeGitHubRepository(value);
94
+ return normalizeHostedRepository(value);
95
95
  }
96
96
  catch {
97
97
  return undefined;
@@ -222,7 +222,8 @@ onlyIssueKey) {
222
222
  if (issueFilter.size > 0) {
223
223
  const recorded = await db.issuePullRequest.findMany({
224
224
  where: {
225
- codeProvider: CODING_CODE_PROVIDER,
225
+ // A local repository has no pull requests to record: everything here is on a code host.
226
+ codeProvider: { not: "local" },
226
227
  OR: [...issueFilter.values()].map((i) => ({ issueProvider: i.provider, issueKey: i.key })),
227
228
  },
228
229
  select: { repository: true, number: true, createdAt: true, openedByRunId: true, state: true },
@@ -269,6 +270,7 @@ onlyIssueKey) {
269
270
  ...(roots.size > 0 ? { roots: [...roots].map(([id, root]) => ({ id, ...root })) } : {}),
270
271
  };
271
272
  }
273
+ const canEditRelatedSection = (host) => host?.replaceRelatedSection !== undefined && host.pullRequestOrigin !== undefined;
272
274
  function issueFor(group, trackers) {
273
275
  if (!group.issue)
274
276
  return undefined;
@@ -283,8 +285,12 @@ function issueFor(group, trackers) {
283
285
  * deployment recorded in that repository, are edited.
284
286
  */
285
287
  export async function updateRelatedPullRequests(db, run, hosts, trackers) {
286
- const host = hosts?.[CODING_CODE_PROVIDER];
287
- if (!host?.replaceRelatedSection || !host.pullRequestOrigin)
288
+ // A request's pull requests may span hosts: each is read and written through its own.
289
+ const editorFor = (repository) => {
290
+ const host = hosts?.[codeProviderOf(repository)];
291
+ return canEditRelatedSection(host) ? host : undefined;
292
+ };
293
+ if (!Object.values(hosts ?? {}).some(canEditRelatedSection))
288
294
  return;
289
295
  try {
290
296
  // A run that delegated nothing has no request set of its own.
@@ -306,6 +312,10 @@ export async function updateRelatedPullRequests(db, run, hosts, trackers) {
306
312
  continue;
307
313
  }
308
314
  try {
315
+ // Reading only needs the read: a host that can't edit the section still reports its pull requests.
316
+ const host = hosts?.[codeProviderOf(pr.repository)];
317
+ if (!host?.pullRequestOrigin)
318
+ throw new Error("no_host_for_repository");
309
319
  const origin = await host.pullRequestOrigin(pr.repository, pr.number);
310
320
  const state = origin.merged
311
321
  ? "merged"
@@ -358,6 +368,9 @@ export async function updateRelatedPullRequests(db, run, hosts, trackers) {
358
368
  });
359
369
  if (!block)
360
370
  continue;
371
+ const host = editorFor(target.repository);
372
+ if (!host)
373
+ continue;
361
374
  try {
362
375
  const outcome = await host.replaceRelatedSection(target.repository, target.number, {
363
376
  expectedMarkerRunId: target.markerRunId,
@@ -381,7 +394,7 @@ const SAFE_RUN_ID = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
381
394
  export const SIBLING_GUIDANCE = "Continue the pull request you were asked about, and a listed open one only when the change requires it. " +
382
395
  "Never open a new pull request in a repository that already has an open pull request listed here. Merged or " +
383
396
  "closed pull requests are not listed: a change there needs a new pull request.";
384
- const HINT_REPOSITORY = /^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/;
397
+ const HINT_REPOSITORY = /^(?:[a-z][a-z0-9-]{1,30}:)?[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/;
385
398
  /**
386
399
  * The continuation hint for a ticket (a GitHub or Jira issue) that already has
387
400
  * open wardby pull requests: one sentence per pull request (up to
@@ -396,9 +409,12 @@ export function openPullRequestHints(prs) {
396
409
  Number.isSafeInteger(p.number) &&
397
410
  p.number > 0)
398
411
  .slice(0, MAX_SIBLING_HINTS)
399
- .map((p) => `This issue already has an open pull request wardby opened: ${p.repository}#${p.number} ` +
400
- `(https://github.com/${p.repository}/pull/${p.number}). ` +
401
- `To revise it, delegate with continuePriorRun set to exactly "${p.openedByRunId}".`);
412
+ .map((p) => {
413
+ const url = pullRequestWebUrl(codeProviderOf(p.repository), p.repository, p.number);
414
+ return (`This issue already has an open pull request wardby opened: ${p.repository}#${p.number}` +
415
+ `${url ? ` (${url})` : ""}. ` +
416
+ `To revise it, delegate with continuePriorRun set to exactly "${p.openedByRunId}".`);
417
+ });
402
418
  return lines.length > 0 ? [...lines, SIBLING_GUIDANCE].join("\n\n") : "";
403
419
  }
404
420
  /**
@@ -436,7 +452,7 @@ export async function openSiblings(db, seedRunId, exclude) {
436
452
  export async function openSiblingsForIssue(db, issue) {
437
453
  try {
438
454
  const newest = await db.issuePullRequest.findMany({
439
- where: { issueProvider: issue.provider, issueKey: issue.key, codeProvider: CODING_CODE_PROVIDER },
455
+ where: { issueProvider: issue.provider, issueKey: issue.key, codeProvider: { not: "local" } },
440
456
  orderBy: { createdAt: "desc" },
441
457
  take: 1,
442
458
  select: { openedByRunId: true },
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Reads a retention setting in whole days (1 to 3650) from the environment. An unset or blank value
3
+ * gives `defaultDays`; anything else out of range stops start-up, naming the variable.
4
+ */
5
+ export declare function loadRetentionDays(env: NodeJS.ProcessEnv, name: string, defaultDays: number): number;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Reads a retention setting in whole days (1 to 3650) from the environment. An unset or blank value
3
+ * gives `defaultDays`; anything else out of range stops start-up, naming the variable.
4
+ */
5
+ export function loadRetentionDays(env, name, defaultDays) {
6
+ const raw = env[name]?.trim();
7
+ if (!raw)
8
+ return defaultDays;
9
+ const days = Number(raw);
10
+ if (!Number.isInteger(days) || days < 1 || days > 3650) {
11
+ throw new Error(`${name} must be a whole number of days from 1 to 3650.`);
12
+ }
13
+ return days;
14
+ }