@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,253 @@
1
+ /**
2
+ * The native gateway's /coding-mcp route (remote MCP servers, phase 2): a stateless Streamable HTTP
3
+ * MCP server that runs a coding run's MCP tool calls. The worker reaches it only through the coding
4
+ * proxy's /mcp route, which forwards the request and the run's MCP token (`rmt_…`) unchanged; the
5
+ * proxy is not trusted, so the token is checked here again: its hash must name a proxy session that
6
+ * is active and before its deadline, whose run is still drivable (not stopped or cancelled).
7
+ *
8
+ * A Codex run (by the provider it was dispatched with, CodingRun.provider) may call only tools the
9
+ * live tool list marks read-only: its shell commands can read the token.
10
+ *
11
+ * tools/list serves the session's pins (no URLs or secrets); tools/call counts the call against the
12
+ * run's limit (WARDBY_MCP_MAX_CALLS_PER_RUN) and runs it through phase 1's handleMcpTool, which
13
+ * re-checks the live attachment and redacts the result against the server's secrets before it gets
14
+ * here. Secrets: this route's own log line carries the method, status and session id only (never the
15
+ * body, arguments, results or token), and every body it sends is either fixed text, the pins, or
16
+ * handleMcpTool's already-redacted string.
17
+ */
18
+ import { readFileSync } from "node:fs";
19
+ import { loadMcpMaxCallsPerRun } from "../config/mcp-call-limit.js";
20
+ import { MCP_TOKEN_REQUIRED_MESSAGE, mcpTokenHash, isMcpToken } from "../coding/mcp-token.js";
21
+ import { HTTP_LIMITS } from "../mcp/transport/http-limits.js";
22
+ import { handleMcpTool, isCodexCoding, mcpToolDefs } from "./mcp-tools.js";
23
+ import { runDrivability, TOOL_STOP_POLL_MS, watchRunStop } from "./runner.js";
24
+ export const CODING_MCP_PATH = "/coding-mcp";
25
+ /** What initialize answers when the client names no protocol version. */
26
+ export const CODING_MCP_DEFAULT_PROTOCOL_VERSION = "2025-06-18";
27
+ /** The package version, reported in server info (package.json sits two levels up from src/core and dist/core). */
28
+ const SERVER_VERSION = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8")).version;
29
+ const LOGGED_METHODS = new Set([
30
+ "initialize",
31
+ "ping",
32
+ "tools/list",
33
+ "tools/call",
34
+ "notifications/initialized",
35
+ "notifications/cancelled",
36
+ ]);
37
+ const JSON_TYPE = "application/json";
38
+ const reply = (status, payload) => ({
39
+ status,
40
+ body: payload === undefined ? "" : JSON.stringify(payload),
41
+ contentType: JSON_TYPE,
42
+ });
43
+ const rpcResult = (id, result) => reply(200, { jsonrpc: "2.0", id, result });
44
+ const rpcError = (status, id, code, message) => reply(status, { jsonrpc: "2.0", id, error: { code, message } });
45
+ /** An MCP tool result: the text handleMcpTool returned, flagged when it is one of its JSON errors. */
46
+ const toolResult = (text, isError) => ({
47
+ content: [{ type: "text", text }],
48
+ ...(isError ? { isError: true } : {}),
49
+ });
50
+ const toolError = (code, message) => toolResult(JSON.stringify({ error: code, message }), true);
51
+ /**
52
+ * True for handleMcpTool's error shape: exactly `{ error: <code>, message }`. A server result that
53
+ * happens to have that very shape is flagged too; the model sees the same text either way.
54
+ */
55
+ export function isMcpErrorText(text) {
56
+ if (!text.startsWith("{"))
57
+ return false;
58
+ try {
59
+ const value = JSON.parse(text);
60
+ if (value === null || typeof value !== "object" || Array.isArray(value))
61
+ return false;
62
+ const keys = Object.keys(value);
63
+ const v = value;
64
+ return (keys.length === 2 &&
65
+ typeof v.error === "string" &&
66
+ /^[a-z][a-z0-9_]*$/.test(v.error) &&
67
+ typeof v.message === "string");
68
+ }
69
+ catch {
70
+ return false;
71
+ }
72
+ }
73
+ /** One stored pin, checked field by field: a malformed entry is left out, never served. */
74
+ export function isMcpToolPin(value) {
75
+ if (value === null || typeof value !== "object" || Array.isArray(value))
76
+ return false;
77
+ const p = value;
78
+ const definition = p.definition;
79
+ return (typeof p.exposedName === "string" &&
80
+ p.exposedName !== "" &&
81
+ typeof p.toolName === "string" &&
82
+ p.toolName !== "" &&
83
+ typeof p.serverId === "string" &&
84
+ p.serverId !== "" &&
85
+ typeof p.serverName === "string" &&
86
+ (p.callTimeoutMs === undefined ||
87
+ (typeof p.callTimeoutMs === "number" && Number.isSafeInteger(p.callTimeoutMs) && p.callTimeoutMs > 0)) &&
88
+ definition !== null &&
89
+ typeof definition === "object" &&
90
+ !Array.isArray(definition) &&
91
+ typeof definition.description === "string" &&
92
+ definition.inputSchema !== null &&
93
+ typeof definition.inputSchema === "object" &&
94
+ !Array.isArray(definition.inputSchema));
95
+ }
96
+ /** The session's well-formed pins, first of each exposed name only. */
97
+ export function storedPins(stored) {
98
+ if (!Array.isArray(stored))
99
+ return [];
100
+ const seen = new Set();
101
+ const pins = [];
102
+ for (const entry of stored) {
103
+ if (!isMcpToolPin(entry) || seen.has(entry.exposedName))
104
+ continue;
105
+ seen.add(entry.exposedName);
106
+ pins.push(entry);
107
+ }
108
+ return pins;
109
+ }
110
+ /**
111
+ * Handles one JSON-RPC request body for /coding-mcp. Never throws. `signal` is the request's own: the
112
+ * listener aborts it when the proxy's connection closes before the answer, which stops a call in flight.
113
+ */
114
+ export async function handleCodingMcpRequest(authorization, body, deps, signal) {
115
+ const seen = {};
116
+ let response;
117
+ try {
118
+ response = await handle(authorization, body, deps, seen, signal);
119
+ }
120
+ catch {
121
+ // Nothing of the error leaves: its message could carry anything the call touched.
122
+ response = rpcError(500, null, -32603, "Internal error.");
123
+ }
124
+ // Method, status and session id only: never the body, the arguments, the result, or the token.
125
+ deps.log.info({ method: seen.method, status: response.status, sessionId: seen.sessionId }, "coding mcp request");
126
+ return response;
127
+ }
128
+ async function handle(authorization, body, deps, seen, signal) {
129
+ if (Buffer.byteLength(body, "utf8") > HTTP_LIMITS.json) {
130
+ return rpcError(413, null, -32600, "The request body is too large.");
131
+ }
132
+ const session = await authenticate(authorization, deps);
133
+ // No WWW-Authenticate: a client must not start OAuth discovery against this route.
134
+ if (!session)
135
+ return rpcError(401, null, -32001, MCP_TOKEN_REQUIRED_MESSAGE);
136
+ seen.sessionId = session.id;
137
+ let message;
138
+ try {
139
+ message = JSON.parse(body);
140
+ }
141
+ catch {
142
+ return rpcError(400, null, -32700, "Parse error.");
143
+ }
144
+ if (message === null || typeof message !== "object" || Array.isArray(message)) {
145
+ return rpcError(400, null, -32600, "Invalid request: one JSON-RPC message per request.");
146
+ }
147
+ const m = message;
148
+ const id = typeof m.id === "string" || typeof m.id === "number" ? m.id : null;
149
+ if (m.jsonrpc !== "2.0" || typeof m.method !== "string") {
150
+ return rpcError(400, id, -32600, "Invalid request.");
151
+ }
152
+ // The method is the client's own text: only a known one is logged, anything else as "other".
153
+ seen.method = LOGGED_METHODS.has(m.method) ? m.method : "other";
154
+ // A notification (no id) has no reply.
155
+ if (m.id === undefined)
156
+ return reply(202, undefined);
157
+ if (id === null)
158
+ return rpcError(400, null, -32600, "Invalid request: id must be a string or number.");
159
+ switch (m.method) {
160
+ case "initialize":
161
+ return rpcResult(id, {
162
+ protocolVersion: protocolVersionOf(m.params),
163
+ capabilities: { tools: {} },
164
+ serverInfo: { name: "wardby", version: SERVER_VERSION },
165
+ });
166
+ case "ping":
167
+ return rpcResult(id, {});
168
+ case "tools/list":
169
+ return rpcResult(id, {
170
+ tools: mcpToolDefs(storedPins(session.mcpTools)).map((def) => ({
171
+ name: def.name,
172
+ description: def.description,
173
+ inputSchema: def.jsonSchema,
174
+ })),
175
+ });
176
+ case "tools/call":
177
+ return rpcResult(id, await callTool(session, m.params, deps, signal));
178
+ default:
179
+ return rpcError(200, id, -32601, "Method not found.");
180
+ }
181
+ }
182
+ function protocolVersionOf(params) {
183
+ const requested = params?.protocolVersion;
184
+ return typeof requested === "string" && /^\d{4}-\d{2}-\d{2}$/.test(requested)
185
+ ? requested
186
+ : CODING_MCP_DEFAULT_PROTOCOL_VERSION;
187
+ }
188
+ async function authenticate(authorization, deps) {
189
+ const token = /^Bearer (\S+)$/.exec(authorization ?? "")?.[1];
190
+ if (!isMcpToken(token))
191
+ return undefined;
192
+ const session = await deps.db.codingProxySession.findUnique({
193
+ where: { mcpTokenHash: mcpTokenHash(token) },
194
+ select: {
195
+ id: true,
196
+ runId: true,
197
+ status: true,
198
+ deadlineAt: true,
199
+ mcpTools: true,
200
+ run: { select: { provider: true } },
201
+ },
202
+ });
203
+ if (!session || session.status !== "active")
204
+ return undefined;
205
+ if ((deps.now?.() ?? new Date()).getTime() >= session.deadlineAt.getTime())
206
+ return undefined;
207
+ // A stopped or cancelled run's session can still read active for a moment: its token ends with the
208
+ // run, as an ended session's does (401, so no call starts and no tool is listed).
209
+ if ((await runDrivability(deps.db, session.runId)) !== "drivable")
210
+ return undefined;
211
+ return session;
212
+ }
213
+ async function callTool(session, params, deps, requestSignal) {
214
+ const p = (params ?? {});
215
+ const name = typeof p.name === "string" ? p.name : "";
216
+ const pin = storedPins(session.mcpTools).find((candidate) => candidate.exposedName === name);
217
+ if (!pin) {
218
+ return toolError("mcp_tool_not_allowed", `The tool "${name.slice(0, 128)}" is not one of this run's MCP tools.`);
219
+ }
220
+ const limit = loadMcpMaxCallsPerRun();
221
+ const counted = await deps.db.$queryRaw `
222
+ UPDATE "CodingProxySession" SET "mcpCallCount" = "mcpCallCount" + 1
223
+ WHERE "id" = ${session.id} AND "mcpCallCount" < ${limit}
224
+ RETURNING "mcpCallCount"
225
+ `;
226
+ if (counted.length === 0) {
227
+ return toolError("mcp_call_limit", `This run reached its limit of ${limit} MCP tool calls.`);
228
+ }
229
+ const run = await deps.db.run.findUnique({ where: { id: session.runId }, select: { agentId: true } });
230
+ if (!run)
231
+ return toolError("mcp_tool_not_allowed", "This run no longer exists.");
232
+ // As for a native run's call: a stop_run aborts the call in flight.
233
+ // The proxy closing its request (its forward limit, or the worker gone) aborts it too.
234
+ const stop = watchRunStop(deps.db, session.runId, deps.stopPollMs ?? TOOL_STOP_POLL_MS);
235
+ const signal = requestSignal ? AbortSignal.any([stop.signal, requestSignal]) : stop.signal;
236
+ try {
237
+ const text = await handleMcpTool(pin, p.arguments === undefined ? "" : JSON.stringify(p.arguments), {
238
+ db: deps.db,
239
+ cipher: deps.cipher,
240
+ agentId: run.agentId,
241
+ runId: session.runId,
242
+ signal,
243
+ deadlineAt: session.deadlineAt,
244
+ readOnlyOnly: isCodexCoding(session.run.provider),
245
+ fetchImpl: deps.fetchImpl,
246
+ log: deps.log,
247
+ });
248
+ return toolResult(text, isMcpErrorText(text));
249
+ }
250
+ finally {
251
+ stop.done();
252
+ }
253
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Which repository a coding run works in: the agent's default
3
+ * (codingProfile.repository) or one of its write links without triggers.
4
+ * Every entry point goes through here. Errors never carry the requested value:
5
+ * it is caller- or model-written text and could hold a secret.
6
+ */
7
+ import type { Prisma } from "#prisma";
8
+ export type CodingRepositoryReader = Pick<Prisma.TransactionClient, "agentRepository" | "codingRun">;
9
+ export interface CodingRepositoryTarget {
10
+ repository: string;
11
+ baseRef: string;
12
+ viaRepositoryLink: boolean;
13
+ }
14
+ export type RepositoryRefusalCode = "repository_required" | "repository_not_listed";
15
+ export declare class RepositoryChoiceRefusedError extends Error {
16
+ readonly code: RepositoryRefusalCode;
17
+ readonly allowed: readonly string[];
18
+ constructor(code: RepositoryRefusalCode, allowed: readonly string[]);
19
+ }
20
+ /** Which of an agent's links are repositories on a coding agent's list. */
21
+ export declare const CODING_REPOSITORY_LINK_FILTER: {
22
+ readonly access: "write";
23
+ readonly triggers: {
24
+ readonly isEmpty: true;
25
+ };
26
+ };
27
+ /**
28
+ * The repositories a list adds to its default. A link can come to equal the default when
29
+ * update_agent moves the default onto it; that link adds nothing, and a run there is on the default.
30
+ */
31
+ export declare function extraCodingRepositoryLinks<Link extends {
32
+ repository: string;
33
+ }>(links: readonly Link[], defaultRepository: string): Link[];
34
+ /** Whether the agent has more than its default, for wording only: authorization is resolveCodingRepository's. */
35
+ export declare function hasCodingRepositoryList(reader: Pick<CodingRepositoryReader, "agentRepository">, agentId: string, defaultRepository: string): Promise<boolean>;
36
+ export declare function resolveCodingRepository(reader: CodingRepositoryReader, input: {
37
+ agentId: string;
38
+ profile: {
39
+ repository: string;
40
+ baseRef: string;
41
+ };
42
+ requested?: string;
43
+ continuesCodingRunId?: string;
44
+ /** A delegation must name its repository when the agent has more than its default. */
45
+ requireExplicit?: boolean;
46
+ }): Promise<CodingRepositoryTarget>;
@@ -0,0 +1,92 @@
1
+ import { codeProviderOf, normalizeHostedRepository } from "../coding/code-host.js";
2
+ import { isLocalRepository, normalizeGitHubRepository, normalizeLocalRepository } from "../coding/protocol.js";
3
+ export class RepositoryChoiceRefusedError extends Error {
4
+ code;
5
+ allowed;
6
+ constructor(code, allowed) {
7
+ super(code === "repository_required"
8
+ ? `This coding agent works in several repositories: pass repository, one of ${allowed.join(", ")}.`
9
+ : `That repository is not on this coding agent's list: ${allowed.join(", ")}.`);
10
+ this.code = code;
11
+ this.allowed = allowed;
12
+ this.name = "RepositoryChoiceRefusedError";
13
+ }
14
+ }
15
+ function canonical(value) {
16
+ try {
17
+ if (isLocalRepository(value))
18
+ return normalizeLocalRepository(value);
19
+ if (codeProviderOf(value) === "github")
20
+ return normalizeGitHubRepository(value);
21
+ return normalizeHostedRepository(value);
22
+ }
23
+ catch {
24
+ return null;
25
+ }
26
+ }
27
+ function same(a, b) {
28
+ const left = canonical(a);
29
+ return left !== null && left === canonical(b);
30
+ }
31
+ /** Which of an agent's links are repositories on a coding agent's list. */
32
+ export const CODING_REPOSITORY_LINK_FILTER = { access: "write", triggers: { isEmpty: true } };
33
+ /**
34
+ * The repositories a list adds to its default. A link can come to equal the default when
35
+ * update_agent moves the default onto it; that link adds nothing, and a run there is on the default.
36
+ */
37
+ export function extraCodingRepositoryLinks(links, defaultRepository) {
38
+ return links.filter((link) => !same(link.repository, defaultRepository));
39
+ }
40
+ async function listExtraRepositoryLinks(reader, agentId, defaultRepository) {
41
+ const links = await reader.agentRepository.findMany({
42
+ where: { agentId, ...CODING_REPOSITORY_LINK_FILTER },
43
+ select: { repository: true, baseRef: true },
44
+ orderBy: { repository: "asc" },
45
+ });
46
+ return extraCodingRepositoryLinks(links, defaultRepository);
47
+ }
48
+ /** Whether the agent has more than its default, for wording only: authorization is resolveCodingRepository's. */
49
+ export async function hasCodingRepositoryList(reader, agentId, defaultRepository) {
50
+ return (await listExtraRepositoryLinks(reader, agentId, defaultRepository)).length > 0;
51
+ }
52
+ export async function resolveCodingRepository(reader, input) {
53
+ const { profile, requested, continuesCodingRunId } = input;
54
+ const defaultTarget = { repository: profile.repository, baseRef: profile.baseRef, viaRepositoryLink: false };
55
+ if (requested !== undefined && same(requested, profile.repository))
56
+ return defaultTarget;
57
+ if (requested === undefined && continuesCodingRunId === undefined && !input.requireExplicit)
58
+ return defaultTarget;
59
+ const links = await listExtraRepositoryLinks(reader, input.agentId, profile.repository);
60
+ const onList = (name) => links.find((link) => same(link.repository, name));
61
+ const target = (link) => ({
62
+ repository: link.repository,
63
+ baseRef: link.baseRef ?? profile.baseRef,
64
+ viaRepositoryLink: true,
65
+ });
66
+ const allowed = [profile.repository, ...links.map((link) => link.repository)];
67
+ if (requested !== undefined) {
68
+ const link = onList(requested);
69
+ if (!link)
70
+ throw new RepositoryChoiceRefusedError("repository_not_listed", allowed);
71
+ return target(link);
72
+ }
73
+ if (continuesCodingRunId !== undefined) {
74
+ const repository = await continuationRepository(reader, continuesCodingRunId);
75
+ const link = repository === null ? undefined : onList(repository);
76
+ // Anything else stays on the default, where checkContinuation gives its own refusal.
77
+ return link ? target(link) : defaultTarget;
78
+ }
79
+ if (links.length > 0)
80
+ throw new RepositoryChoiceRefusedError("repository_required", allowed);
81
+ return defaultTarget;
82
+ }
83
+ async function continuationRepository(reader, runId) {
84
+ const select = { repository: true, rootCodingRunId: true };
85
+ const run = await reader.codingRun.findUnique({ where: { runId }, select });
86
+ if (!run)
87
+ return null;
88
+ if (!run.rootCodingRunId)
89
+ return run.repository;
90
+ const root = await reader.codingRun.findUnique({ where: { runId: run.rootCodingRunId }, select });
91
+ return root?.repository ?? null;
92
+ }
@@ -0,0 +1,27 @@
1
+ /** What the once-per-run rule counts: a sub-agent, and for a coding sub-agent, the repository. */
2
+ export declare function delegationSlot(childAgentId: string, repository: string | null): string;
3
+ /**
4
+ * Slots for runs already delegated. A run refused at dispatch has no CodingRun, so its
5
+ * repository is unknown; for this child that counts as its default. Retrying a refused
6
+ * non-default repository is allowed, and still counts toward the delegation limit.
7
+ */
8
+ export declare function priorDelegationSlots(rows: readonly {
9
+ agentId: string;
10
+ codingRun?: {
11
+ repository: string;
12
+ } | null;
13
+ }[], childAgentId: string, defaultRepository: string | null): string[];
14
+ /**
15
+ * At most `limit` dispatches per run (default one: a classifier deciding plan-vs-implement
16
+ * commits to exactly one child), and never the same slot twice, so a retry can't leave two
17
+ * children racing on the same task. A delegation waiting for budget holds its place.
18
+ */
19
+ export declare function delegationLimitRefusal(input: {
20
+ priorSlots: readonly string[];
21
+ waitingSlots: readonly string[];
22
+ slot: string;
23
+ boundName: string;
24
+ limit: number;
25
+ /** Whether "the same slot" means the same repository; otherwise it means the same sub-agent. */
26
+ childHasRepositoryList: boolean;
27
+ }): string | null;
@@ -0,0 +1,44 @@
1
+ /** What the once-per-run rule counts: a sub-agent, and for a coding sub-agent, the repository. */
2
+ export function delegationSlot(childAgentId, repository) {
3
+ return repository === null ? childAgentId : `${childAgentId}\n${repository.toLowerCase()}`;
4
+ }
5
+ /**
6
+ * Slots for runs already delegated. A run refused at dispatch has no CodingRun, so its
7
+ * repository is unknown; for this child that counts as its default. Retrying a refused
8
+ * non-default repository is allowed, and still counts toward the delegation limit.
9
+ */
10
+ export function priorDelegationSlots(rows, childAgentId, defaultRepository) {
11
+ return rows.map((row) => delegationSlot(row.agentId, row.codingRun?.repository ?? (row.agentId === childAgentId ? defaultRepository : null)));
12
+ }
13
+ /**
14
+ * At most `limit` dispatches per run (default one: a classifier deciding plan-vs-implement
15
+ * commits to exactly one child), and never the same slot twice, so a retry can't leave two
16
+ * children racing on the same task. A delegation waiting for budget holds its place.
17
+ */
18
+ export function delegationLimitRefusal(input) {
19
+ const { priorSlots, waitingSlots, slot, boundName, limit, childHasRepositoryList } = input;
20
+ if (priorSlots.includes(slot) || waitingSlots.includes(slot)) {
21
+ return JSON.stringify({
22
+ error: "already_dispatched",
23
+ message: childHasRepositoryList
24
+ ? `This run already delegated to "${boundName}" for this repository; a run delegates to each sub-agent and repository at most once.`
25
+ : `This run already delegated to "${boundName}"; a run delegates to each sub-agent at most once.`,
26
+ });
27
+ }
28
+ if (priorSlots.length < limit && priorSlots.length + waitingSlots.length >= limit) {
29
+ // Only a waiting delegation (parallelDelegations) holds the remaining place.
30
+ return JSON.stringify({
31
+ error: "already_dispatched",
32
+ message: `This run reached its limit of ${limit} delegations: ${priorSlots.length} made and ${waitingSlots.length} waiting for budget.`,
33
+ });
34
+ }
35
+ if (priorSlots.length >= limit) {
36
+ return JSON.stringify({
37
+ error: "already_dispatched",
38
+ message: limit === 1
39
+ ? "This run already delegated to a sub-agent; only one delegation is allowed per run."
40
+ : `This run already made ${limit} delegations, the most this agent allows per run.`,
41
+ });
42
+ }
43
+ return null;
44
+ }
@@ -7,8 +7,8 @@ import { type AttributionIntent } from "./attribution.js";
7
7
  import { type BudgetConstraint } from "./budget-groups.js";
8
8
  import { type KnowledgeNoteInput } from "../knowledge/note.js";
9
9
  import { type SelfDefectSink } from "./self-defects.js";
10
- export type DispatchDb = Pick<PrismaClient, "agent" | "run" | "runHostCheck" | "codingRun" | "task" | "webhook" | "budgetGroup" | "$transaction" | "$queryRaw">;
11
- export type DispatchTx = Pick<Prisma.TransactionClient, "agent" | "run" | "runHostCheck" | "runHostStatus" | "runIssueStatus" | "codingRun" | "codingService" | "task" | "webhook" | "budgetGroup" | "resourceGrant" | "workItem" | "localPullRequest" | "runAttribution" | "$queryRaw" | "$executeRawUnsafe">;
10
+ export type DispatchDb = Pick<PrismaClient, "agent" | "agentRepository" | "run" | "runHostCheck" | "codingRun" | "task" | "webhook" | "budgetGroup" | "$transaction" | "$queryRaw">;
11
+ export type DispatchTx = Pick<Prisma.TransactionClient, "agent" | "agentRepository" | "run" | "runHostCheck" | "runHostStatus" | "runIssueStatus" | "codingRun" | "codingService" | "task" | "webhook" | "budgetGroup" | "resourceGrant" | "workItem" | "localPullRequest" | "runAttribution" | "$queryRaw" | "$executeRawUnsafe">;
12
12
  type DispatchAgent = Prisma.AgentGetPayload<{
13
13
  include: {
14
14
  codingProfile: true;
@@ -87,6 +87,10 @@ export interface DispatchRunOptions {
87
87
  * mutually exclusive with it, since a native run has no CodingRun.
88
88
  */
89
89
  taskOverride?: string;
90
+ /** The repository for a coding run; must be on the agent's list. Default: codingProfile.repository. */
91
+ codingRepository?: string;
92
+ /** Refuse with repository_required when the agent has links and nothing names a repository. */
93
+ requireExplicitRepository?: boolean;
90
94
  /**
91
95
  * Replaces `agent.budgetUsd` as the requested ceiling for this run's coding
92
96
  * budget reservation. The reservation is still tightened by the agent's
@@ -1,4 +1,5 @@
1
- import { CODING_PROTOCOL_VERSION, CodingTaskInputSchema, normalizeGitHubRepository, normalizeGitRef, publicCodingRunResult, composeCodingTask, } from "../coding/protocol.js";
1
+ import { normalizeHostedRepository } from "../coding/code-host.js";
2
+ import { CODING_PROTOCOL_VERSION, CodingTaskInputSchema, normalizeGitRef, publicCodingRunResult, composeCodingTask, } from "../coding/protocol.js";
2
3
  import { assertCodingProvider, assertCodingProviderModel } from "../coding/provider.js";
3
4
  import { isPackageAuditActive } from "../coding/registry/audit-mode.js";
4
5
  import { ModelUnavailableError, entryOf } from "../providers/llm/catalog-types.js";
@@ -13,7 +14,9 @@ import { KNOWLEDGE_CONFIG_MAX_BYTES, KNOWLEDGE_CONFIG_PATH, resolveKnowledgeLoca
13
14
  import { KNOWLEDGE_INDEX_READ_MAX_BYTES, knowledgeSection } from "../knowledge/note.js";
14
15
  import { logger } from "./logger.js";
15
16
  import { CodingModelProviderMismatchError, resolveCodingEntry } from "./run-pricing.js";
17
+ import { resolveCodingRepository, } from "./coding-repository.js";
16
18
  import { CONTENDED_TX_MAX_WAIT_MS } from "./timing.js";
19
+ import { dispatchRunStepsLevel } from "./run-steps/config.js";
17
20
  import { fileSelfDefectForRun } from "./self-defects.js";
18
21
  const dispatchLog = logger.child({ module: "dispatch" });
19
22
  /** SQLSTATEs worth retrying the persist transaction for: serialization failure, deadlock. */
@@ -205,7 +208,7 @@ export async function checkContinuation(reader, runId, repository, dispatchingOw
205
208
  : candidate;
206
209
  if (!root)
207
210
  return { ok: false, reason: "unknown_run" };
208
- if (normalizeGitHubRepository(root.repository) !== normalizeGitHubRepository(repository)) {
211
+ if (normalizeHostedRepository(root.repository) !== normalizeHostedRepository(repository)) {
209
212
  return { ok: false, reason: "other_repository" };
210
213
  }
211
214
  const rootResult = publicCodingRunResult(root.result);
@@ -231,6 +234,15 @@ export async function checkContinuation(reader, runId, repository, dispatchingOw
231
234
  },
232
235
  };
233
236
  }
237
+ function codingTarget(reader, options, profile) {
238
+ return resolveCodingRepository(reader, {
239
+ agentId: options.agentId,
240
+ profile,
241
+ requested: options.codingRepository,
242
+ continuesCodingRunId: options.continuesCodingRunId,
243
+ requireExplicit: options.requireExplicitRepository,
244
+ });
245
+ }
234
246
  /**
235
247
  * Resolves a coding run's branch: an override, else a continuation root's
236
248
  * (see checkContinuation), else the profile's base.
@@ -430,17 +442,27 @@ export async function dispatchRun(options) {
430
442
  // The native gateway's executor has no coding configuration: the run is recorded
431
443
  // unresolved and the process that starts it reads and resolves (resolveDeferredCodingRun).
432
444
  const deferCoding = options.executor.defersCodingResolution === true;
445
+ // The repository this run works in, resolved from the agent's list before any network read. A
446
+ // refusal propagates: nothing has been written, and it is not a retryable conflict.
447
+ const previewTarget = !deferCoding && preview?.kind === "coding" && preview.codingProfile
448
+ ? await codingTarget(options.db, options, preview.codingProfile)
449
+ : undefined;
433
450
  // Only an agent allowed some service reads the repository's declaration; any
434
451
  // other coding agent never calls the host for it and gets no services.
435
452
  const declarationRead = !deferCoding &&
436
453
  preview?.kind === "coding" &&
437
454
  preview.codingProfile &&
455
+ previewTarget &&
438
456
  parseAllowedServiceNames(preview.codingProfile.services).length > 0
439
- ? await readServiceDeclaration(options, preview.codingProfile, preview.ownerId ?? null)
457
+ ? await readServiceDeclaration(options, previewTarget, preview.ownerId ?? null)
440
458
  : undefined;
441
459
  // The knowledge note's input: the repository's bundle index at the run's base (a network call).
442
- const knowledgeIndex = !deferCoding && preview?.kind === "coding" && preview.codingProfile && options.executor.readCodingRepositoryFile
443
- ? await readKnowledgeIndex(options, preview.codingProfile, declarationRead?.branch, preview.ownerId ?? null)
460
+ const knowledgeIndex = !deferCoding &&
461
+ preview?.kind === "coding" &&
462
+ preview.codingProfile &&
463
+ previewTarget &&
464
+ options.executor.readCodingRepositoryFile
465
+ ? await readKnowledgeIndex(options, previewTarget, declarationRead?.branch, preview.ownerId ?? null)
444
466
  : undefined;
445
467
  const persistOnce = () => options.db.$transaction(async (tx) => {
446
468
  if (lockGroups)
@@ -472,11 +494,14 @@ export async function dispatchRun(options) {
472
494
  let modelFailure;
473
495
  let services = [];
474
496
  let servicesRefusal;
497
+ let target;
475
498
  if (agent.kind === "coding") {
476
499
  if (!agent.codingProfile)
477
500
  throw new Error(`Coding agent "${agent.id}" has no coding profile.`);
478
501
  // An unknown coding provider is a broken profile, not a catalog state: still thrown.
479
502
  assertCodingProvider(agent.codingProfile.provider);
503
+ // Resolved again inside the transaction: an unlink since the preview refuses the run here.
504
+ target = await codingTarget(tx, options, agent.codingProfile);
480
505
  // Recorded on the run row below: the run is billed at this entry for its whole life.
481
506
  // A model the catalog cannot run (missing, disabled, or another provider's) fails
482
507
  // the run here instead of throwing: the run row commits with the reason, so the
@@ -492,8 +517,7 @@ export async function dispatchRun(options) {
492
517
  modelFailure = err.message;
493
518
  }
494
519
  if (!modelFailure && declarationRead) {
495
- if (declarationRead.repository !== agent.codingProfile.repository ||
496
- declarationRead.profileBaseRef !== agent.codingProfile.baseRef) {
520
+ if (declarationRead.repository !== target.repository || declarationRead.profileBaseRef !== target.baseRef) {
497
521
  // The declaration was read for a profile that changed before this transaction.
498
522
  throw new Error("The coding agent's repository or base branch changed during dispatch; try again.");
499
523
  }
@@ -516,6 +540,8 @@ export async function dispatchRun(options) {
516
540
  executionManaged: true,
517
541
  // Fixed for the run's life: the executor routes on this, never on the agent's current setting.
518
542
  nativeExecutionMode: agent.kind === "native" ? agent.nativeExecutionMode : null,
543
+ // Fixed for the run's life, like nativeExecutionMode: a later runSteps change reaches only later runs.
544
+ runStepsLevel: dispatchRunStepsLevel(agent),
519
545
  parentRunId: options.parentRunId,
520
546
  parentCallId: options.parentCallId,
521
547
  grantedParentMemoryKeys: options.grantedParentMemoryKeys ?? [],
@@ -551,7 +577,7 @@ export async function dispatchRun(options) {
551
577
  continuesCodingRunId: options.continuesCodingRunId,
552
578
  intent: options.attribution,
553
579
  }, now);
554
- if (agent.kind === "coding" && agent.codingProfile && codingBudget && !refusal) {
580
+ if (agent.kind === "coding" && agent.codingProfile && target && codingBudget && !refusal) {
555
581
  // Checked above too; repeated so the provider type narrows here.
556
582
  assertCodingProviderModel(agent.codingProfile.provider, agent.model);
557
583
  const request = options.codingTask ?? agent.codingProfile.defaultTask;
@@ -567,14 +593,14 @@ export async function dispatchRun(options) {
567
593
  : composeCodingTask(agent.systemPrompt, request, servicesInstructionNote(services), knowledgeIndex ? knowledgeSection(knowledgeIndex) : undefined);
568
594
  const budgetUsd = codingBudget.budgetUsd;
569
595
  // Resolved once: before the transaction when the declaration was read from it.
570
- const branch = declarationRead?.branch ?? (await resolveCodingBranch(tx, options, agent.codingProfile, agent.ownerId));
596
+ const branch = declarationRead?.branch ?? (await resolveCodingBranch(tx, options, target, agent.ownerId));
571
597
  const { baseRef, continuationOf, rootCodingRunId, mergeOrder: inheritedMergeOrder } = branch;
572
598
  const headRef = branch.headRef ?? `wardby/run-${run.id}`;
573
599
  const mergeOrder = resolveMergeOrder(options, inheritedMergeOrder);
574
600
  const input = CodingTaskInputSchema.parse({
575
601
  schemaVersion: CODING_PROTOCOL_VERSION,
576
602
  runId: run.id,
577
- repository: agent.codingProfile.repository,
603
+ repository: target.repository,
578
604
  baseRef,
579
605
  headRef,
580
606
  task,
@@ -608,6 +634,7 @@ export async function dispatchRun(options) {
608
634
  request,
609
635
  repository: input.repository,
610
636
  baseRef: input.baseRef,
637
+ viaRepositoryLink: target.viaRepositoryLink,
611
638
  headRef: input.headRef,
612
639
  provider: agent.codingProfile.provider,
613
640
  model: input.model,
@@ -645,7 +672,8 @@ export async function dispatchRun(options) {
645
672
  else if (agent.kind !== "coding" &&
646
673
  (options.codingTask !== undefined ||
647
674
  options.codingBaseRef !== undefined ||
648
- options.continuesCodingRunId !== undefined)) {
675
+ options.continuesCodingRunId !== undefined ||
676
+ options.codingRepository !== undefined)) {
649
677
  throw new Error("Coding overrides cannot be supplied for a native agent.");
650
678
  }
651
679
  const task = options.task
@@ -16,6 +16,8 @@ export declare class NativeEngine implements Engine {
16
16
  * without it — either way the terminal status is `budget_exhausted`.
17
17
  */
18
18
  private windDown;
19
+ /** Builds an event (only when ctx.onTurn is set) and hands it to ctx.onTurn. A failing hook is logged by name and never affects the run. */
20
+ private emitTurn;
19
21
  private finish;
20
22
  /**
21
23
  * One turn's tool calls, results in call order. The calls are split into