@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
@@ -13,9 +13,11 @@ import { Prisma } from "#prisma";
13
13
  import { deriveJsonSchema, validateParams } from "../../sandbox/zod-params.js";
14
14
  import { cachedZod4Compatibility, toolSubject, withZod3ToolWarnings, zod3SampleArgsWarning, zod3ToolWarnings, zod3WindowWarning, } from "../../core/zod3-window.js";
15
15
  import { runInSandbox } from "../../sandbox/run-in-sandbox.js";
16
+ import { effectiveToolTimeoutMs, toolTimeoutProblem } from "../../sandbox/tool-timeout.js";
17
+ import { loadToolMaxTimeoutMs } from "../../config/tool-timeout.js";
16
18
  import { ToolCapabilitiesPatchSchema } from "../../sandbox/tool-capabilities.js";
17
19
  import { buildSharedDatastoreAccessor } from "../../core/datastores.js";
18
- import { findSameNamedAttachedTool, reservedToolNameReason } from "../../core/tool-names.js";
20
+ import { findSameNamedAttachedTool, findSameNamedMcpTool, reservedToolNameReason, sameNamedMcpToolMessage, } from "../../core/tool-names.js";
19
21
  import { ToolAttachedError, deleteToolGuarded, hasToolChanges, prepareToolUpdate, updateToolGuarded, } from "../../core/tool-admin.js";
20
22
  import { McpError } from "../errors.js";
21
23
  import { requireOwnedTool, requireStrictlyOwnedTool, assertStrictlyOwnedTool, visibleToPrincipal, canRead, } from "../auth/ownership.js";
@@ -46,9 +48,16 @@ async function describeAgents(agents, ctx) {
46
48
  export function projectTool(tool, principalId) {
47
49
  if (tool.ownerId !== null && tool.ownerId === principalId)
48
50
  return tool;
49
- return tool.ownerId === null
50
- ? { id: tool.id, name: tool.name, description: tool.description, public: true }
51
- : { id: tool.id, name: tool.name, description: tool.description, public: false, ownerIsCaller: false };
51
+ // timeoutMs is not code: anyone who sees the tool sees how long its calls may take.
52
+ const shown = { id: tool.id, name: tool.name, description: tool.description, timeoutMs: tool.timeoutMs };
53
+ return tool.ownerId === null ? { ...shown, public: true } : { ...shown, public: false, ownerIsCaller: false };
54
+ }
55
+ const TIMEOUT_MS_DESCRIPTION = "The tool's time limit per call, in milliseconds (at least 1000). Omitted: the default of 10000. Capped by the server's WARDBY_TOOL_MAX_TIMEOUT_MS (default 300000); a larger value is refused. Each fetch times out just before it, and sleep() is capped at it.";
56
+ /** Refuses (400) a timeoutMs this server can't store, naming its ceiling. */
57
+ function assertToolTimeout(timeoutMs) {
58
+ const problem = toolTimeoutProblem(timeoutMs, loadToolMaxTimeoutMs());
59
+ if (problem)
60
+ throw new McpError(400, problem);
52
61
  }
53
62
  /**
54
63
  * Non-blocking: which of the secrets just granted to this attachment are
@@ -83,11 +92,14 @@ export function registerToolAuthoringTools(mcp) {
83
92
  description: { type: "string" },
84
93
  paramsZod: { type: "string" },
85
94
  code: { type: "string" },
95
+ timeoutMs: { type: "integer", description: TIMEOUT_MS_DESCRIPTION },
86
96
  },
87
97
  required: ["name", "description", "paramsZod", "code"],
88
98
  },
89
99
  handler: async (args, ctx) => {
90
100
  assertToolNameAllowed(args.name);
101
+ if (args.timeoutMs !== undefined)
102
+ assertToolTimeout(args.timeoutMs);
91
103
  const schemaResult = await deriveJsonSchema(args.paramsZod);
92
104
  if (!schemaResult.ok) {
93
105
  return textResult({ ok: false, errorKind: schemaResult.errorKind, errorMessage: schemaResult.errorMessage });
@@ -101,6 +113,7 @@ export function registerToolAuthoringTools(mcp) {
101
113
  code: args.code,
102
114
  jsonSchema: schemaResult.value,
103
115
  ownerId: ctx.principal.id,
116
+ ...(args.timeoutMs !== undefined ? { timeoutMs: args.timeoutMs } : {}),
104
117
  },
105
118
  });
106
119
  const warnings = await zod3ToolWarnings(toolSubject(tool.name), args.paramsZod);
@@ -117,7 +130,7 @@ export function registerToolAuthoringTools(mcp) {
117
130
  mcp.registerTool({
118
131
  name: "update_tool",
119
132
  scope: "tools:write",
120
- description: "Replaces a tool's description, paramsZod and/or code in place (its name can't change: agents' prompts call it by name). Owner-only: public tools can't be changed over MCP. Refused while the tool is attached to any agent you don't own, including owner-less ones and agents shared with you, since the change would reach another owner's agent; detach it there first or create a new tool. Runs already started keep the version they loaded for their whole lifetime; the next run picks up the new one.",
133
+ description: "Replaces a tool's description, paramsZod, code and/or timeoutMs (null: back to the default limit) in place (its name can't change: agents' prompts call it by name). Owner-only: public tools can't be changed over MCP. Refused while the tool is attached to any agent you don't own, including owner-less ones and agents shared with you, since the change would reach another owner's agent; detach it there first or create a new tool. Runs already started keep the version they loaded for their whole lifetime; the next run picks up the new one.",
121
134
  inputSchema: {
122
135
  type: "object",
123
136
  additionalProperties: false,
@@ -126,14 +139,25 @@ export function registerToolAuthoringTools(mcp) {
126
139
  description: { type: "string" },
127
140
  paramsZod: { type: "string" },
128
141
  code: { type: "string" },
142
+ timeoutMs: {
143
+ type: ["integer", "null"],
144
+ description: `${TIMEOUT_MS_DESCRIPTION} Pass null to go back to the default.`,
145
+ },
129
146
  },
130
147
  required: ["toolId"],
131
148
  },
132
149
  handler: async (args, ctx) => {
133
- const changes = { description: args.description, paramsZod: args.paramsZod, code: args.code };
150
+ const changes = {
151
+ description: args.description,
152
+ paramsZod: args.paramsZod,
153
+ code: args.code,
154
+ timeoutMs: args.timeoutMs,
155
+ };
134
156
  if (!hasToolChanges(changes)) {
135
- throw new McpError(400, "update_tool needs at least one of description, paramsZod or code.");
157
+ throw new McpError(400, "update_tool needs at least one of description, paramsZod, code or timeoutMs.");
136
158
  }
159
+ if (args.timeoutMs !== undefined && args.timeoutMs !== null)
160
+ assertToolTimeout(args.timeoutMs);
137
161
  // Checked once up front so a non-owner never gets as far as a sandbox
138
162
  // compile, and again (with the attachments) inside the transaction.
139
163
  await requireStrictlyOwnedTool(ctx.db, args.toolId, ctx.principal.id);
@@ -196,7 +220,7 @@ export function registerToolAuthoringTools(mcp) {
196
220
  mcp.registerTool({
197
221
  name: "dry_run_tool",
198
222
  scope: "tools:write",
199
- description: "Test-runs tool code against sample args without persisting it. Fetch is denied by default, same as a freshly attached tool — pass allowedHosts to test code that calls fetch(). For this release a paramsZod is still compiled with Zod 3; a `warnings` list in the result means the next release, which compiles it with Zod 4 only, will reject it or treat the sample args differently.",
223
+ description: "Test-runs tool code against sample args without persisting it, with the same time limit a run would give it (timeoutMs, as on create_tool). Fetch is denied by default, same as a freshly attached tool — pass allowedHosts to test code that calls fetch(). For this release a paramsZod is still compiled with Zod 3; a `warnings` list in the result means the next release, which compiles it with Zod 4 only, will reject it or treat the sample args differently.",
200
224
  inputSchema: {
201
225
  type: "object",
202
226
  properties: {
@@ -204,10 +228,13 @@ export function registerToolAuthoringTools(mcp) {
204
228
  code: { type: "string" },
205
229
  sampleArgs: { type: "object" },
206
230
  allowedHosts: { type: "array", items: { type: "string" } },
231
+ timeoutMs: { type: "integer", description: TIMEOUT_MS_DESCRIPTION },
207
232
  },
208
233
  required: ["paramsZod", "code", "sampleArgs"],
209
234
  },
210
235
  handler: async (args, ctx) => {
236
+ if (args.timeoutMs !== undefined)
237
+ assertToolTimeout(args.timeoutMs);
211
238
  const hosts = ToolCapabilitiesPatchSchema.pick({ allowedHosts: true }).safeParse({
212
239
  allowedHosts: args.allowedHosts,
213
240
  });
@@ -254,6 +281,7 @@ export function registerToolAuthoringTools(mcp) {
254
281
  sharedDatastore: buildSharedDatastoreAccessor(ctx.principal.id, ctx.providers.datastore, ctx.db),
255
282
  toolName: "dry_run_tool",
256
283
  allowedFetchHosts: hosts.data.allowedHosts ?? [],
284
+ timeoutMs: effectiveToolTimeoutMs(args.timeoutMs, loadToolMaxTimeoutMs()),
257
285
  });
258
286
  return textResult({ jsonSchema, result: sandboxResult, ...warnings });
259
287
  },
@@ -334,6 +362,10 @@ export function registerToolAuthoringTools(mcp) {
334
362
  : " (a tool owned by another principal)";
335
363
  throw new McpError(409, `Agent "${args.agentId}" already has a different tool named "${tool.name}" attached${which}; detach it first.`);
336
364
  }
365
+ // Nor a name one of the agent's MCP servers already gives the model.
366
+ const mcpClash = await findSameNamedMcpTool(tx, args.agentId, tool.name);
367
+ if (mcpClash)
368
+ throw new McpError(409, sameNamedMcpToolMessage(args.agentId, tool.name, mcpClash.serverName));
337
369
  // Re-granting an attachment this owner never vouched for: what is
338
370
  // not restated is reset, never silently adopted.
339
371
  const keepUnstated = existing?.capabilitiesGrantedById === agent.ownerId;
@@ -29,6 +29,7 @@
29
29
  * gets 404, not another principal's final text/cost.
30
30
  */
31
31
  import { dispatchRun } from "../../core/dispatch.js";
32
+ import { RepositoryChoiceRefusedError, resolveCodingRepository } from "../../core/coding-repository.js";
32
33
  import { AttributionError, explicitAttribution, RESPONSE_PATH_SNAPSHOT_BUDGET, } from "../../core/attribution.js";
33
34
  import { CodingBaseRefSchema, CodingTaskOverrideSchema, isLocalRepository } from "../../coding/protocol.js";
34
35
  import { loadLocalRepoRoots, resolveLocalRepository, LocalRepoError } from "../../coding/local-repo.js";
@@ -46,6 +47,7 @@ const TriggerAgentSchema = z
46
47
  agentId: z.string().trim().min(1).max(128),
47
48
  task: CodingTaskOverrideSchema.optional(),
48
49
  baseRef: CodingBaseRefSchema.optional(),
50
+ repository: z.string().min(1).max(300).optional(),
49
51
  issue: z.unknown().optional(),
50
52
  review: z
51
53
  .object({
@@ -127,10 +129,10 @@ function assertOverrideAllowed(principalId, agent, args) {
127
129
  // Strictly the owner: the stdio operator's owner-level access doesn't make
128
130
  // it the owner whose repository authority the task steers (review M8).
129
131
  const isOwner = agent.ownerId !== null && agent.ownerId === principalId;
130
- if (isOwner || (args.task === undefined && args.baseRef === undefined))
132
+ if (isOwner || (args.task === undefined && args.baseRef === undefined && args.repository === undefined))
131
133
  return;
132
134
  if (!agent.codingProfile?.allowWebhookTaskOverride) {
133
- throw new McpError(403, `Agent "${agent.id}": only its owner can pass a task or baseRef, unless the owner allows it (codingProfile.allowWebhookTaskOverride).`);
135
+ throw new McpError(403, `Agent "${agent.id}": only its owner can pass a task, baseRef or repository, unless the owner allows it (codingProfile.allowWebhookTaskOverride).`);
134
136
  }
135
137
  }
136
138
  /** execute on the agent, plus the override rule; `db` is the transaction when re-checking. */
@@ -150,6 +152,10 @@ export function registerTriggerTool(mcp) {
150
152
  agentId: { type: "string" },
151
153
  task: { type: "string" },
152
154
  baseRef: { type: "string" },
155
+ repository: {
156
+ type: "string",
157
+ description: "Coding agents only: a repository on the agent's list (see get_agent). Omit to use codingProfile.repository.",
158
+ },
153
159
  review: {
154
160
  type: "object",
155
161
  additionalProperties: false,
@@ -189,13 +195,28 @@ export function registerTriggerTool(mcp) {
189
195
  }
190
196
  review = await resolveLocalReview(ctx, agent.id, args.review);
191
197
  }
192
- if (agent.kind !== "coding" && (args.task !== undefined || args.baseRef !== undefined)) {
193
- throw new McpError(400, "Task and baseRef overrides are only valid for coding agents.");
198
+ if (agent.kind !== "coding" &&
199
+ (args.task !== undefined || args.baseRef !== undefined || args.repository !== undefined)) {
200
+ throw new McpError(400, "Task, baseRef and repository overrides are only valid for coding agents.");
194
201
  }
195
202
  // A local repository is re-checked against the roots as they are now
196
203
  // (they can narrow after the agent was saved), before any run exists.
197
204
  let warnings = [];
198
- const repository = agent.kind === "coding" ? agent.codingProfile?.repository : undefined;
205
+ // The resolver's refusal lists the agent's repositories, so it runs only
206
+ // after requireTriggerable (execute on the agent) and never echoes the
207
+ // requested value.
208
+ const refused = (err) => {
209
+ if (err instanceof RepositoryChoiceRefusedError)
210
+ throw new McpError(400, `${err.code}: ${err.message}`);
211
+ throw err;
212
+ };
213
+ const repository = agent.kind === "coding" && agent.codingProfile
214
+ ? (await resolveCodingRepository(ctx.db, {
215
+ agentId: agent.id,
216
+ profile: agent.codingProfile,
217
+ requested: args.repository,
218
+ }).catch(refused)).repository
219
+ : undefined;
199
220
  if (repository !== undefined && isLocalRepository(repository)) {
200
221
  try {
201
222
  const resolved = await resolveLocalRepository(repository, loadLocalRepoRoots(process.env).roots);
@@ -238,6 +259,7 @@ export function registerTriggerTool(mcp) {
238
259
  trigger: "manual",
239
260
  codingTask: args.task,
240
261
  codingBaseRef: args.baseRef,
262
+ codingRepository: args.repository,
241
263
  attribution,
242
264
  task: ctx.clientSupportsTasks ? { principalId: ctx.principal.id, ttlMs: DEFAULT_TASK_TTL_MS } : undefined,
243
265
  triggeredById: ctx.principal.id,
@@ -262,7 +284,7 @@ export function registerTriggerTool(mcp) {
262
284
  catch (err) {
263
285
  if (pullRequest)
264
286
  await ctx.db.localPullRequest.deleteMany({ where: { id: pullRequest.id } }).catch(() => { });
265
- throw err;
287
+ return refused(err);
266
288
  }
267
289
  if (ctx.clientSupportsTasks) {
268
290
  if (!dispatched.task)
@@ -14,4 +14,6 @@ export declare class PayloadTooLargeError extends HttpBoundaryError {
14
14
  }
15
15
  export declare function canonicalUrl(value: string): URL;
16
16
  export declare function readBody(req: IncomingMessage, maxBytes: number, signal: AbortSignal): Promise<string>;
17
+ /** readBody's bytes, undecoded (for a route that forwards the body unchanged). */
18
+ export declare function readBodyBytes(req: IncomingMessage, maxBytes: number, signal: AbortSignal): Promise<Buffer>;
17
19
  export declare function parseBody(raw: string, contentType: string | undefined): unknown;
@@ -23,7 +23,11 @@ export function canonicalUrl(value) {
23
23
  }
24
24
  return url;
25
25
  }
26
- export function readBody(req, maxBytes, signal) {
26
+ export async function readBody(req, maxBytes, signal) {
27
+ return (await readBodyBytes(req, maxBytes, signal)).toString("utf8");
28
+ }
29
+ /** readBody's bytes, undecoded (for a route that forwards the body unchanged). */
30
+ export function readBodyBytes(req, maxBytes, signal) {
27
31
  const length = req.headers["content-length"];
28
32
  if (length && (!/^\d+$/.test(length) || Number(length) > maxBytes))
29
33
  return Promise.reject(new PayloadTooLargeError());
@@ -55,7 +59,7 @@ export function readBody(req, maxBytes, signal) {
55
59
  };
56
60
  const end = () => {
57
61
  clean();
58
- resolve(Buffer.concat(chunks, size).toString("utf8"));
62
+ resolve(Buffer.concat(chunks, size));
59
63
  };
60
64
  req.on("data", data);
61
65
  req.once("end", end);
@@ -18,11 +18,19 @@ export async function handleWebhookIngress(webhookId, req, db, executor, issueTr
18
18
  if (req.body.task !== undefined && codingTask === undefined) {
19
19
  return { status: 400, body: { error: "invalid_task" } };
20
20
  }
21
+ // `wardbyRepository`, not `repository`, for the same reason: a forwarded GitHub
22
+ // payload carries its own top-level `repository` object.
23
+ const repository = req.body.wardbyRepository;
24
+ if (repository !== undefined &&
25
+ (typeof repository !== "string" || repository.length === 0 || repository.length > 300)) {
26
+ return { status: 400, body: { error: "invalid_repository" } };
27
+ }
21
28
  // `wardbyIssue`, not `issue`: forwarded third-party payloads (GitHub, Jira)
22
29
  // carry their own top-level `issue` object, which is not ours to read.
23
30
  const result = await resolveWebhookRun(webhookId, secret, db, executor, codingTask, {
24
31
  issue: req.body.wardbyIssue,
25
32
  issueTrackers,
33
+ repository,
26
34
  });
27
35
  if (result.ok) {
28
36
  return { status: 202, body: { runId: result.runId } };
@@ -30,6 +38,8 @@ export async function handleWebhookIngress(webhookId, req, db, executor, issueTr
30
38
  switch (result.reason) {
31
39
  case "invalid_issue":
32
40
  return { status: 400, body: { error: "invalid_issue", error_description: result.message } };
41
+ case "invalid_repository":
42
+ return { status: 400, body: { error: "invalid_repository" } };
33
43
  case "not_found":
34
44
  return { status: 404, body: { error: "not_found" } };
35
45
  case "invalid_secret":
@@ -10,7 +10,9 @@
10
10
  * session budget before the provider is called and settles at the provider's
11
11
  * reported usage; the worker's own usage numbers are never read.
12
12
  */
13
+ import type { RunStepSink } from "../core/run-steps/types.js";
13
14
  import { createNativeRunTools, durableDelegate, type NativeRunFinishContext, type NativeRunProviders, type RunnerDb, type SandboxSessionSnapshot } from "../core/runner.js";
15
+ import type { CodingMcpDeps } from "../core/coding-mcp.js";
14
16
  import { ReviewReadLedger } from "../core/review-reads.js";
15
17
  import type { LlmProvider, LlmStreamEvent } from "../providers/llm/types.js";
16
18
  import { PrismaGatewayLedger, type GatewaySessionRecord } from "./ledger.js";
@@ -26,6 +28,8 @@ export interface GatewayServerDeps {
26
28
  providers: NativeRunProviders;
27
29
  /** Delegation long-poll window; tests shorten it. */
28
30
  pollWindowMs?: number;
31
+ /** Serves POST /coding-mcp on the same listener (remote MCP servers for coding runs); unset → 404. */
32
+ codingMcp?: CodingMcpDeps;
29
33
  }
30
34
  /** What a request gets back: one JSON value, or a stream of model events. */
31
35
  export type GatewayResponse = {
@@ -63,7 +67,9 @@ interface RebuiltRun {
63
67
  delegation: Omit<Parameters<typeof durableDelegate>[0], "ledger" | "sessionId">;
64
68
  }
65
69
  /** Exported for its unit test; the HTTP handler is its only production caller. */
66
- export declare function streamModel(ctx: RebuiltRun, ledger: PrismaGatewayLedger, callId: string, params: GatewayParams<"llm.stream">): AsyncIterable<LlmStreamEvent>;
70
+ export declare function streamModel(ctx: RebuiltRun, ledger: PrismaGatewayLedger, callId: string, params: GatewayParams<"llm.stream">, steps?: RunStepSink,
71
+ /** The request's bearer capability, redacted from the recorded steps like a secret value. */
72
+ capability?: string): AsyncIterable<LlmStreamEvent>;
67
73
  /**
68
74
  * Logged (at warn) in place of a user tool's console line when that tool's redaction list cannot
69
75
  * be read: without the complete list the line cannot be shown to be free of its secret values, so
@@ -72,6 +78,17 @@ export declare function streamModel(ctx: RebuiltRun, ledger: PrismaGatewayLedger
72
78
  export declare const CONSOLE_WITHHELD_MESSAGE = "sandbox tool console line withheld: the tool's secret values could not be read";
73
79
  /** Serves one privileged bridge call for a worker (exported for tests). */
74
80
  export declare function hostCall(ctx: RebuiltRun, ledger: PrismaGatewayLedger, callId: string, params: GatewayParams<"host.call">): Promise<unknown>;
81
+ /** Added to a tool's limit for one host call: the worker's own wall time ends the call first. */
82
+ export declare const GATEWAY_HOST_CALL_MARGIN_MS = 20000;
83
+ /** How long one host call of `tool` may take: its limit plus the margin, cut to the run's deadline. */
84
+ export declare function hostCallTimeoutMs(ctx: {
85
+ session: {
86
+ deadlineAt: Date;
87
+ };
88
+ tools: {
89
+ toolTimeoutMs: (name: string) => number | undefined;
90
+ };
91
+ }, tool: string, now?: number): number;
75
92
  /** The worker's terminal call: the run's one terminal write (masked by finishNativeRun), then the session ends. */
76
93
  export declare function finish(ctx: RebuiltRun, ledger: PrismaGatewayLedger, callId: string, params: GatewayParams<"finish">): Promise<null>;
77
94
  export {};
@@ -10,12 +10,14 @@
10
10
  * session budget before the provider is called and settles at the provider's
11
11
  * reported usage; the worker's own usage numbers are never read.
12
12
  */
13
+ import { NativeStreamSteps, recordStreamSteps } from "../core/run-steps/native.js";
13
14
  import { createNativeRunTools, durableDelegate, failNativeRun, finishNativeRun, nativeRunIntegrations, recordNativeRunProgress, runDrivability, sandboxCapabilityHash, userToolSecretValues, } from "../core/runner.js";
14
15
  import { ReviewReadLedger } from "../core/review-reads.js";
15
16
  import { DELEGATE_TOOL_PREFIX } from "../core/tool-names.js";
16
17
  import { relayedOutputTokens } from "../providers/coding-proxy/interrupted.js";
17
18
  import { RoutingLlmProvider } from "../providers/llm/routing.js";
18
19
  import { isPrivilegedBridgeName } from "../sandbox/host-functions.js";
20
+ import { DEFAULT_TOOL_TIMEOUT_MS } from "../sandbox/tool-timeout.js";
19
21
  import { forWorker, gatewayLog } from "./gateway.js";
20
22
  import { PrismaGatewayLedger } from "./ledger.js";
21
23
  import { GatewayError, GatewayRequestSchema, parseParams, } from "./protocol.js";
@@ -65,6 +67,7 @@ export function runSecretValues(ctx, capability) {
65
67
  return async () => [
66
68
  capability,
67
69
  ...(await userToolSecretValues(ctx.tools, Object.keys(ctx.snapshot.input.userTools))),
70
+ ...(await ctx.tools.mcpSecretValues()),
68
71
  ];
69
72
  }
70
73
  /**
@@ -127,7 +130,7 @@ async function handle(deps, authorization, body, scope) {
127
130
  return {
128
131
  kind: "stream",
129
132
  status: 200,
130
- events: checkedEvents(streamModel(ctx, ledger, request.callId, params), scope.secretValues),
133
+ events: checkedEvents(streamModel(ctx, ledger, request.callId, params, deps.providers.runSteps, token), scope.secretValues),
131
134
  };
132
135
  case "llm.countTokens": {
133
136
  const p = params;
@@ -178,6 +181,7 @@ async function rebuild(deps, session) {
178
181
  repoAccess,
179
182
  issueTrackers,
180
183
  reviewReads,
184
+ deadlineAt: session.deadlineAt,
181
185
  }),
182
186
  finishContext: { runId: session.runId, db, providers, reviewHosts, repoAccess, issueTrackers },
183
187
  delegation: {
@@ -192,7 +196,9 @@ async function rebuild(deps, session) {
192
196
  };
193
197
  }
194
198
  /** Exported for its unit test; the HTTP handler is its only production caller. */
195
- export async function* streamModel(ctx, ledger, callId, params) {
199
+ export async function* streamModel(ctx, ledger, callId, params, steps,
200
+ /** The request's bearer capability, redacted from the recorded steps like a secret value. */
201
+ capability) {
196
202
  const req = params.request;
197
203
  if (req.model !== ctx.snapshot.input.agent.model)
198
204
  throw new GatewayError("not_allowed", "A run may only call its own model.");
@@ -207,6 +213,8 @@ export async function* streamModel(ctx, ledger, callId, params) {
207
213
  if (reserved.outcome === "budget_exhausted") {
208
214
  throw new GatewayError("budget_exhausted", "This run's budget cannot cover another model call.");
209
215
  }
216
+ // Run steps: what this call streams, and the results its request carries for the last turn.
217
+ const tap = steps ? new NativeStreamSteps(callId) : undefined;
210
218
  let settled = false;
211
219
  let sawEvent = false;
212
220
  // Set once the provider's response starts: it may bill hidden thinking or reasoning before any event.
@@ -235,6 +243,7 @@ export async function* streamModel(ctx, ledger, callId, params) {
235
243
  costUsd: event.usage.costUsd,
236
244
  });
237
245
  }
246
+ tap?.observe(event);
238
247
  yield event;
239
248
  }
240
249
  }
@@ -281,6 +290,8 @@ export async function* streamModel(ctx, ledger, callId, params) {
281
290
  costUsd: ctx.llm.priceUsd(req.model, { inputTokens, outputTokens: output }),
282
291
  });
283
292
  }
293
+ // Recorded before the stream ends (bounded), so the next call's results find these steps.
294
+ await recordStreamSteps(steps, ctx.session.runId, tap, req.messages, capability ? [capability] : []);
284
295
  }
285
296
  }
286
297
  /** Output tokens of a cut stream's output: counted, else the deliberately high bytes / 3 estimate. */
@@ -335,9 +346,15 @@ export async function hostCall(ctx, ledger, callId, params) {
335
346
  }
336
347
  redact = values;
337
348
  }
338
- const host = ctx.tools.privilegedHostFor(params.tool, AbortSignal.timeout(30_000), redact);
339
- if (!host)
349
+ // The tool's own limit plus a margin (30 s for a default tool), never past the run's deadline,
350
+ // and aborted if the run is stopped meanwhile.
351
+ const stop = ctx.tools.watchStop();
352
+ const signal = AbortSignal.any([AbortSignal.timeout(hostCallTimeoutMs(ctx, params.tool)), stop.signal]);
353
+ const host = ctx.tools.privilegedHostFor(params.tool, signal, redact);
354
+ if (!host) {
355
+ stop.done();
340
356
  throw new GatewayError("not_allowed", `"${params.tool}" is not a user tool of this run.`);
357
+ }
341
358
  const bridge = host[params.bridge];
342
359
  const run = async () => {
343
360
  try {
@@ -348,11 +365,23 @@ export async function hostCall(ctx, ledger, callId, params) {
348
365
  throw new GatewayError("bridge_error", err instanceof Error ? err.message.slice(0, 1024) : "Host function failed.");
349
366
  }
350
367
  };
351
- // Console lines are not worth a ledger row, and a secret value must never be stored in one
352
- // (a secrets.get is a side-effect-free read, so a retried callId simply reads it again).
353
- if (params.bridge === "__bridge_console" || params.bridge === "__bridge_secretsGet")
354
- return run();
355
- return replayable(ledger, ctx.session.id, callId, "host.call", run);
368
+ try {
369
+ // Console lines are not worth a ledger row, and a secret value must never be stored in one
370
+ // (a secrets.get is a side-effect-free read, so a retried callId simply reads it again).
371
+ if (params.bridge === "__bridge_console" || params.bridge === "__bridge_secretsGet")
372
+ return await run();
373
+ return await replayable(ledger, ctx.session.id, callId, "host.call", run);
374
+ }
375
+ finally {
376
+ stop.done();
377
+ }
378
+ }
379
+ /** Added to a tool's limit for one host call: the worker's own wall time ends the call first. */
380
+ export const GATEWAY_HOST_CALL_MARGIN_MS = 20_000;
381
+ /** How long one host call of `tool` may take: its limit plus the margin, cut to the run's deadline. */
382
+ export function hostCallTimeoutMs(ctx, tool, now = Date.now()) {
383
+ const limitMs = (ctx.tools.toolTimeoutMs(tool) ?? DEFAULT_TOOL_TIMEOUT_MS) + GATEWAY_HOST_CALL_MARGIN_MS;
384
+ return Math.max(0, Math.min(limitMs, ctx.session.deadlineAt.getTime() - now));
356
385
  }
357
386
  /** Runs a one-shot call once per callId; a retry replays its recorded result. */
358
387
  async function replayable(ledger, sessionId, callId, method, run) {
@@ -13,6 +13,7 @@
13
13
  import type { EngineResult, EngineRunContext } from "../providers/engine/types.js";
14
14
  import type { LlmStreamEvent } from "../providers/llm/types.js";
15
15
  import pino from "pino";
16
+ import type { RunStepSink } from "../core/run-steps/types.js";
16
17
  import { type PrivilegedHost } from "../sandbox/host-functions.js";
17
18
  import { logger } from "../core/logger.js";
18
19
  import { GatewayError, type WorkerInput } from "./protocol.js";
@@ -63,8 +64,12 @@ export interface SandboxedEngineOptions {
63
64
  * the run's values is not sent to the worker; without this, no cause code is sent.
64
65
  */
65
66
  readableSecretValues?: (tool: string) => Promise<string[]>;
67
+ /** Every secret value of the run's attached MCP servers, checked the same way. */
68
+ mcpSecretValues?: () => Promise<string[]>;
66
69
  drivability: () => Promise<RunDrivability>;
67
70
  launcher: WorkerLauncher;
71
+ /** Records the run's steps (src/core/run-steps); absent = none. */
72
+ runSteps?: RunStepSink;
68
73
  }
69
74
  /** Runs one native run's engine in a worker and returns its result with gateway-metered usage. */
70
75
  export declare function runSandboxedEngine(options: SandboxedEngineOptions): Promise<EngineResult>;
@@ -11,6 +11,7 @@
11
11
  * sandbox bridges are served only by name from the run's own surface.
12
12
  */
13
13
  import pino from "pino";
14
+ import { NativeStreamSteps, recordStreamSteps } from "../core/run-steps/native.js";
14
15
  import { isPrivilegedBridgeName, redactExactSecretValues, withEscapedForms, } from "../sandbox/host-functions.js";
15
16
  import { logger } from "../core/logger.js";
16
17
  import { declaredCodeOf } from "../core/error-code.js";
@@ -116,8 +117,12 @@ export async function runSandboxedEngine(options) {
116
117
  };
117
118
  /** The run's readable secret values, for checking a failure's cause code before the worker sees it. */
118
119
  const readable = options.readableSecretValues;
120
+ const mcpValues = options.mcpSecretValues;
119
121
  const runSecretValues = readable
120
- ? async () => (await Promise.all(Object.keys(input.userTools).map((tool) => readable(tool)))).flat()
122
+ ? async () => [
123
+ ...(await Promise.all(Object.keys(input.userTools).map((tool) => readable(tool)))).flat(),
124
+ ...(mcpValues ? await mcpValues() : []),
125
+ ]
121
126
  : undefined;
122
127
  const served = {
123
128
  async *stream(raw) {
@@ -130,6 +135,9 @@ export async function runSandboxedEngine(options) {
130
135
  const req = params.request;
131
136
  let sawDone = false;
132
137
  let streamedText = "";
138
+ // Run steps: what this call streams, and the results its request carries for the last turn.
139
+ const sink = options.runSteps;
140
+ const steps = sink ? new NativeStreamSteps(request.callId) : undefined;
133
141
  try {
134
142
  for await (const event of llm.stream(req, controller.signal)) {
135
143
  if (event.type === "done") {
@@ -139,6 +147,7 @@ export async function runSandboxedEngine(options) {
139
147
  else if (event.type === "text") {
140
148
  streamedText += event.delta;
141
149
  }
150
+ steps?.observe(event);
142
151
  yield event;
143
152
  }
144
153
  }
@@ -153,6 +162,9 @@ export async function runSandboxedEngine(options) {
153
162
  : 0;
154
163
  meter({ inputTokens, outputTokens, costUsd: llm.priceUsd(req.model, { inputTokens, outputTokens }) });
155
164
  }
165
+ // Recorded before the stream ends (bounded), so the next call's results find these steps.
166
+ // The run's capability (when the worker has one) is redacted like a secret value.
167
+ await recordStreamSteps(sink, runId, steps, req.messages, input.gateway ? [input.gateway.capability] : []);
156
168
  }
157
169
  },
158
170
  async call(raw) {
@@ -7,16 +7,18 @@
7
7
  * POST /native-gateway/v1/call body: GatewayRequest
8
8
  * → application/json { ok: true, result } | { ok: false, error: { code, message } }
9
9
  * → application/x-ndjson (llm.stream) { event } … then { end: true } or { error: { code, message, causeCode? } }
10
+ *
11
+ * POST /coding-mcp a coding run's MCP calls, forwarded by the coding proxy's /mcp route
12
+ * (src/core/coding-mcp.ts); its own auth: the run's MCP token, not a
13
+ * gateway capability. 404 when the listener was started without it.
10
14
  */
11
15
  import { type Server } from "node:http";
16
+ import { type ListenAddress } from "../core/listen-address.js";
12
17
  import { type GatewayServerDeps } from "./gateway-server.js";
13
18
  /** The HTTP listener's own log. Exported so tests can watch it. */
14
19
  export declare const httpLog: import("pino").Logger<never, boolean>;
15
20
  export declare const NATIVE_GATEWAY_PATH = "/native-gateway/v1/call";
16
- export interface NativeGatewayListen {
17
- host: string;
18
- port: number;
19
- }
21
+ export type NativeGatewayListen = ListenAddress;
20
22
  /** Parses NATIVE_GATEWAY_LISTEN ("host:port" or ":port"); undefined when unset. */
21
23
  export declare function parseGatewayListen(value: string | undefined): NativeGatewayListen | undefined;
22
24
  export declare function createGatewayServer(deps: GatewayServerDeps): Server;
@@ -7,9 +7,16 @@
7
7
  * POST /native-gateway/v1/call body: GatewayRequest
8
8
  * → application/json { ok: true, result } | { ok: false, error: { code, message } }
9
9
  * → application/x-ndjson (llm.stream) { event } … then { end: true } or { error: { code, message, causeCode? } }
10
+ *
11
+ * POST /coding-mcp a coding run's MCP calls, forwarded by the coding proxy's /mcp route
12
+ * (src/core/coding-mcp.ts); its own auth: the run's MCP token, not a
13
+ * gateway capability. 404 when the listener was started without it.
10
14
  */
11
15
  import { createServer } from "node:http";
16
+ import { CODING_MCP_PATH, handleCodingMcpRequest } from "../core/coding-mcp.js";
17
+ import { parseListenAddress } from "../core/listen-address.js";
12
18
  import { logger } from "../core/logger.js";
19
+ import { HTTP_LIMITS, HttpBoundaryError, readBody as readLimitedBody } from "../mcp/transport/http-limits.js";
13
20
  import { logUnexpectedError } from "./gateway.js";
14
21
  import { serveGatewayRequest } from "./gateway-server.js";
15
22
  import { GatewayError, MAX_MESSAGE_BYTES, toGatewayError } from "./protocol.js";
@@ -18,14 +25,7 @@ export const httpLog = logger.child({ module: "native-gateway-http" });
18
25
  export const NATIVE_GATEWAY_PATH = "/native-gateway/v1/call";
19
26
  /** Parses NATIVE_GATEWAY_LISTEN ("host:port" or ":port"); undefined when unset. */
20
27
  export function parseGatewayListen(value) {
21
- if (!value)
22
- return undefined;
23
- const match = /^(.*):(\d{1,5})$/.exec(value.trim());
24
- const port = match ? Number(match[2]) : NaN;
25
- if (!match || !Number.isInteger(port) || port < 1 || port > 65_535) {
26
- throw new Error(`NATIVE_GATEWAY_LISTEN must be host:port or :port (got "${value}").`);
27
- }
28
- return { host: match[1] || "0.0.0.0", port };
28
+ return parseListenAddress("NATIVE_GATEWAY_LISTEN", value);
29
29
  }
30
30
  function readBody(req) {
31
31
  return new Promise((resolve, reject) => {
@@ -58,6 +58,10 @@ export function createGatewayServer(deps) {
58
58
  send(healthy ? 200 : 503, { ok: healthy });
59
59
  return;
60
60
  }
61
+ if (new URL(req.url ?? "/", "http://gateway").pathname === CODING_MCP_PATH && deps.codingMcp) {
62
+ await serveCodingMcp(req, res, deps.codingMcp);
63
+ return;
64
+ }
61
65
  if (req.method !== "POST" || req.url !== NATIVE_GATEWAY_PATH) {
62
66
  send(404, { ok: false, error: { code: "not_found", message: "not found" } });
63
67
  return;
@@ -101,6 +105,47 @@ export function createGatewayServer(deps) {
101
105
  });
102
106
  });
103
107
  }
108
+ /** /coding-mcp: one JSON-RPC message per POST, answered as one JSON body (or 202 with none). */
109
+ async function serveCodingMcp(req, res, deps) {
110
+ if (req.method !== "POST") {
111
+ res.setHeader("allow", "POST");
112
+ res.setHeader("content-type", "application/json");
113
+ res.writeHead(405);
114
+ res.end(JSON.stringify({ jsonrpc: "2.0", id: null, error: { code: -32600, message: "Use POST." } }));
115
+ return;
116
+ }
117
+ let body;
118
+ try {
119
+ body = await readLimitedBody(req, HTTP_LIMITS.json, AbortSignal.timeout(HTTP_LIMITS.requestMs));
120
+ }
121
+ catch (err) {
122
+ const status = err instanceof HttpBoundaryError ? err.status : 400;
123
+ res.setHeader("content-type", "application/json");
124
+ res.writeHead(status);
125
+ res.end(JSON.stringify({
126
+ jsonrpc: "2.0",
127
+ id: null,
128
+ error: { code: -32600, message: "The request body could not be read." },
129
+ }));
130
+ // A body left unread would otherwise keep the socket busy.
131
+ if (status === 413)
132
+ req.resume();
133
+ return;
134
+ }
135
+ // The proxy gives up on a call at its forward limit (or its worker goes away): the closed response
136
+ // aborts the call in flight rather than leave it running against the MCP server.
137
+ const closed = new AbortController();
138
+ res.on("close", () => {
139
+ if (!res.writableFinished)
140
+ closed.abort();
141
+ });
142
+ const response = await handleCodingMcpRequest(req.headers.authorization, body, deps, closed.signal);
143
+ if (closed.signal.aborted)
144
+ return;
145
+ res.setHeader("content-type", response.contentType);
146
+ res.writeHead(response.status);
147
+ res.end(response.body);
148
+ }
104
149
  /** Starts the listener; resolves once it is accepting connections. */
105
150
  export async function startGatewayServer(listen, deps) {
106
151
  const server = createGatewayServer(deps);