@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
@@ -1,11 +1,17 @@
1
- import { createHash, randomBytes, randomUUID } from "node:crypto";
1
+ import { randomBytes, randomUUID } from "node:crypto";
2
2
  import { shippedCatalog } from "../llm/catalog.js";
3
3
  import { SHIPPED_CATALOG_VERSION } from "../llm/catalog-shipped.js";
4
4
  import { deriveRegistryToken } from "../../coding/registry/token.js";
5
+ import { capabilityHash } from "./capability-hash.js";
6
+ import { CLAUDE_MCP_TOOL_PREFIX } from "../../coding/mcp-token.js";
5
7
  import { AnthropicSseUsageTracker, actualCostUsd, fingerprintRequest, parseAnthropicAuthoritativeUsage, parseAuthoritativeUsage, pricingSnapshot, safeUpstreamErrorCode, terminalUsageFromSseFrame, } from "./metering.js";
6
8
  import { conversationKey, estimateHold } from "./hold.js";
7
9
  import { estimateInterrupted, previousInput } from "./interrupted.js";
8
10
  import { createPinnedProxyFetch } from "./secure-fetch.js";
11
+ import { resultsFromRequest, stepsFromJson, streamStepExtractor, } from "../../core/run-steps/extract.js";
12
+ import { RUN_STEPS_LEVELS } from "../../core/run-steps/config.js";
13
+ import { SeenIds } from "../../core/run-steps/seen.js";
14
+ import { CLAUDE_RESULT_TOOL } from "../../core/run-steps/summarize.js";
9
15
  export const PROXY_MAX_BODY_BYTES = 8 * 1024 * 1024;
10
16
  export const PROXY_MAX_OUTPUT_TOKENS = 1_000_000;
11
17
  export const PROXY_DEFAULT_MAX_OUTPUT_TOKENS = 4_096;
@@ -30,7 +36,7 @@ const REQUEST_KEY_PATTERN = /^[\x21-\x7e]{1,200}$/;
30
36
  export const OPTIONAL_ANTHROPIC_BETAS = ["per-turn-control-2026-07-01"];
31
37
  const APPROVED_ANTHROPIC_BETAS = new Set([...CLAUDE_CODE_ANTHROPIC_BETAS, ...OPTIONAL_ANTHROPIC_BETAS]);
32
38
  const WARDBY_COMMAND_TOOL = "mcp__wardby_tools__run_command";
33
- const STRUCTURED_OUTPUT_TOOL = "StructuredOutput";
39
+ const STRUCTURED_OUTPUT_TOOL = CLAUDE_RESULT_TOOL;
34
40
  const MAX_COMMAND_BYTES = 16 * 1024;
35
41
  const MAX_TOOL_TEXT_BYTES = 256 * 1024;
36
42
  /**
@@ -48,20 +54,26 @@ export class CodingProxyError extends Error {
48
54
  this.code = code;
49
55
  }
50
56
  }
51
- export function capabilityHash(capability) {
52
- return createHash("sha256").update(capability).digest("base64url");
57
+ /** What this session's replies and requests are read for. */
58
+ function stepCapture(session, ceiling) {
59
+ const level = session.runStepsLevel ?? "summary";
60
+ if (level === "off" || ceiling === "off")
61
+ return "none";
62
+ return level === "detail" && ceiling === "detail" ? "detail" : "summary";
53
63
  }
64
+ export { capabilityHash };
54
65
  // OpenAI Responses allowlist. Everything a request may carry is listed here;
55
66
  // anything else is refused before a credential is resolved. It mirrors what the
56
67
  // pinned Codex CLI (src/coding-worker/package.json) actually sends, recorded in
57
68
  // fixtures/codex-<version>-responses-requests.json. Only client-side tools
58
- // (function, custom, and namespaces of them) are allowed: a hosted tool (web
59
- // search, remote MCP, code interpreter, image generation, file search, ...), a
60
- // remote image or file, a stored prompt, or a previous response would make
61
- // OpenAI fetch or run something outside the worker's egress policy and outside
62
- // the token-metered budget. Tool names vary by model and Codex version, so they
63
- // are checked for shape, not against a fixed list: a client-side tool the model
64
- // calls is executed by the worker itself, which the container boundary governs.
69
+ // (function, custom, namespaces of them, and the client-executed tool_search)
70
+ // are allowed: a hosted tool (web search, remote MCP, code interpreter, image
71
+ // generation, file search, ...), a remote image or file, a stored prompt, or a
72
+ // previous response would make OpenAI fetch or run something outside the
73
+ // worker's egress policy and outside the token-metered budget. Tool names vary
74
+ // by model and Codex version, so they are checked for shape, not against a
75
+ // fixed list: a client-side tool the model calls is executed by the worker
76
+ // itself, which the container boundary governs.
65
77
  const OPENAI_REQUEST_KEYS = new Set([
66
78
  "model",
67
79
  "instructions",
@@ -151,13 +163,32 @@ function openAiToolName(value, code = "invalid_openai_request") {
151
163
  if (typeof value !== "string" || !OPENAI_TOOL_NAME.test(value))
152
164
  throw new CodingProxyError(400, code);
153
165
  }
154
- function validateOpenAiFunctionTool(tool) {
155
- openAiOnlyKeys(tool, ["type", "name", "description", "strict", "parameters"], "invalid_openai_tool");
166
+ function validateOpenAiFunctionTool(tool, inSearchOutput) {
167
+ openAiOnlyKeys(tool, inSearchOutput
168
+ ? ["type", "name", "description", "strict", "defer_loading", "parameters"]
169
+ : ["type", "name", "description", "strict", "parameters"], "invalid_openai_tool");
156
170
  openAiToolName(tool.name, "invalid_openai_tool");
157
171
  openAiOptional(tool.description, (value) => openAiString(value, MAX_TOOL_TEXT_BYTES, "invalid_openai_tool"));
158
172
  if (tool.strict !== undefined && typeof tool.strict !== "boolean") {
159
173
  throw new CodingProxyError(400, "invalid_openai_tool");
160
174
  }
175
+ // Codex marks the tools a tool_search_output loads with defer_loading: true,
176
+ // and sends it nowhere else. It only delays when the model sees the
177
+ // definition, which is checked in full.
178
+ if (tool.defer_loading !== undefined && tool.defer_loading !== true) {
179
+ throw new CodingProxyError(400, "invalid_openai_tool");
180
+ }
181
+ openAiOptional(tool.parameters, (value) => openAiRecord(value, "invalid_openai_tool"));
182
+ }
183
+ // The tool_search tool Codex sends for models whose metadata asks for it. Only
184
+ // client execution: Codex runs the search over its own deferred tools and
185
+ // returns them in a tool_search_output item. A server-executed search would
186
+ // have OpenAI run it.
187
+ function validateOpenAiToolSearchTool(tool) {
188
+ openAiOnlyKeys(tool, ["type", "execution", "description", "parameters"], "invalid_openai_tool");
189
+ if (tool.execution !== "client")
190
+ throw new CodingProxyError(400, "openai_tool_not_allowed:tool_search");
191
+ openAiOptional(tool.description, (value) => openAiString(value, MAX_TOOL_TEXT_BYTES, "invalid_openai_tool"));
161
192
  openAiOptional(tool.parameters, (value) => openAiRecord(value, "invalid_openai_tool"));
162
193
  }
163
194
  function validateOpenAiCustomTool(tool) {
@@ -175,30 +206,57 @@ function validateOpenAiCustomTool(tool) {
175
206
  }
176
207
  openAiString(format.definition, MAX_TOOL_TEXT_BYTES, "invalid_openai_tool");
177
208
  }
178
- function validateOpenAiTools(value, count, nested = false) {
209
+ // Every tool list (the request's tools, each additional_tools item, each
210
+ // tool_search_output item) is capped at MAX_OPENAI_TOOLS entries on its own.
211
+ // Across the request, `distinct` holds the `namespace/name` of each callable
212
+ // tool and is capped too. Codex replays every earlier tool_search_output on
213
+ // each turn, so a long run repeats the same definitions many times: each copy
214
+ // is still validated, but only a tool not seen before adds to the shared cap.
215
+ function validateOpenAiTools(value, distinct, { toolSearch = false, inSearchOutput = false, namespace, listed = { value: 0 }, } = {}) {
179
216
  if (!Array.isArray(value))
180
217
  throw new CodingProxyError(400, "invalid_openai_tool");
218
+ const callable = (name) => {
219
+ distinct.add(`${namespace ?? ""}/${name}`);
220
+ if (distinct.size > MAX_OPENAI_TOOLS)
221
+ throw new CodingProxyError(400, "invalid_openai_tool");
222
+ };
181
223
  for (const candidate of value) {
182
- if (++count.value > MAX_OPENAI_TOOLS)
224
+ if (++listed.value > MAX_OPENAI_TOOLS)
183
225
  throw new CodingProxyError(400, "invalid_openai_tool");
184
226
  const tool = openAiRecord(candidate, "invalid_openai_tool");
185
227
  if (tool.type === "function") {
186
- validateOpenAiFunctionTool(tool);
228
+ validateOpenAiFunctionTool(tool, inSearchOutput);
229
+ callable(tool.name);
187
230
  }
188
231
  else if (tool.type === "custom") {
189
232
  validateOpenAiCustomTool(tool);
233
+ callable(tool.name);
190
234
  }
191
- else if (tool.type === "namespace" && !nested) {
235
+ else if (tool.type === "namespace" && namespace === undefined) {
192
236
  openAiOnlyKeys(tool, ["type", "name", "description", "tools"], "invalid_openai_tool");
193
237
  openAiToolName(tool.name, "invalid_openai_tool");
194
238
  openAiOptional(tool.description, (value) => openAiString(value, MAX_TOOL_TEXT_BYTES, "invalid_openai_tool"));
195
- validateOpenAiTools(tool.tools, count, true);
239
+ validateOpenAiTools(tool.tools, distinct, { inSearchOutput, namespace: tool.name, listed });
240
+ }
241
+ else if (tool.type === "tool_search" && toolSearch) {
242
+ validateOpenAiToolSearchTool(tool);
243
+ callable("tool_search");
196
244
  }
197
245
  else {
198
246
  throw new CodingProxyError(400, openAiCode("openai_tool_not_allowed", tool.type));
199
247
  }
200
248
  }
201
249
  }
250
+ // The arguments the model gave the client-side tool_search, as its parameters
251
+ // in the captures declare them: a query and an optional result limit.
252
+ function validateOpenAiToolSearchArguments(value) {
253
+ const args = openAiRecord(value);
254
+ openAiOnlyKeys(args, ["query", "limit"]);
255
+ openAiString(args.query, MAX_TOOL_TEXT_BYTES);
256
+ if (args.limit !== undefined && !(Number.isSafeInteger(args.limit) && args.limit > 0)) {
257
+ throw new CodingProxyError(400, "invalid_openai_request");
258
+ }
259
+ }
202
260
  function validateOpenAiContentPart(value, allowed) {
203
261
  const part = openAiRecord(value);
204
262
  if (typeof part.type !== "string" || !allowed.has(part.type)) {
@@ -241,7 +299,7 @@ function validateOpenAiTextParts(value, type) {
241
299
  openAiString(part.text, PROXY_MAX_BODY_BYTES);
242
300
  }
243
301
  }
244
- function validateOpenAiInputItem(value, toolCount) {
302
+ function validateOpenAiInputItem(value, distinctTools) {
245
303
  const item = openAiRecord(value);
246
304
  switch (item.type) {
247
305
  case undefined:
@@ -300,12 +358,32 @@ function validateOpenAiInputItem(value, toolCount) {
300
358
  openAiOptional(item.recipient, (value) => openAiString(value, MAX_OPENAI_ID_LENGTH));
301
359
  validateOpenAiContent(item.content, OPENAI_AGENT_MESSAGE_PARTS, false);
302
360
  return;
361
+ case "tool_search_call":
362
+ openAiOnlyKeys(item, ["type", "id", "call_id", "status", "execution", "arguments"]);
363
+ openAiOptional(item.id, openAiId);
364
+ openAiId(item.call_id);
365
+ openAiOptional(item.status, (value) => openAiString(value, 64));
366
+ if (item.execution !== "client")
367
+ throw new CodingProxyError(400, openAiCode("openai_input_not_allowed", item.type));
368
+ validateOpenAiToolSearchArguments(item.arguments);
369
+ return;
370
+ case "tool_search_output":
371
+ // The tools the client-side search loaded: each one passes the same checks
372
+ // as a top-level tool, so this cannot bring in a shape the proxy refuses.
373
+ openAiOnlyKeys(item, ["type", "id", "call_id", "status", "execution", "tools"]);
374
+ openAiOptional(item.id, openAiId);
375
+ openAiId(item.call_id);
376
+ openAiOptional(item.status, (value) => openAiString(value, 64));
377
+ if (item.execution !== "client")
378
+ throw new CodingProxyError(400, openAiCode("openai_input_not_allowed", item.type));
379
+ validateOpenAiTools(item.tools, distinctTools, { inSearchOutput: true });
380
+ return;
303
381
  case "additional_tools":
304
382
  openAiOnlyKeys(item, ["type", "id", "role", "tools"]);
305
383
  openAiOptional(item.id, openAiId);
306
384
  if (item.role !== undefined && item.role !== "developer")
307
385
  throw new CodingProxyError(400, "invalid_openai_request");
308
- validateOpenAiTools(item.tools, toolCount);
386
+ validateOpenAiTools(item.tools, distinctTools);
309
387
  return;
310
388
  default:
311
389
  throw new CodingProxyError(400, openAiCode("openai_input_not_allowed", item.type));
@@ -372,9 +450,9 @@ function validateOpenAiBody(body) {
372
450
  throw new CodingProxyError(400, "invalid_openai_request");
373
451
  }
374
452
  openAiOptional(body.instructions, (value) => openAiString(value, PROXY_MAX_BODY_BYTES));
375
- const toolCount = { value: 0 };
453
+ const distinctTools = new Set();
376
454
  if (body.tools !== undefined)
377
- validateOpenAiTools(body.tools, toolCount);
455
+ validateOpenAiTools(body.tools, distinctTools, { toolSearch: true });
378
456
  if (body.tool_choice !== undefined) {
379
457
  if (typeof body.tool_choice !== "string" || !OPENAI_TOOL_CHOICES.has(body.tool_choice)) {
380
458
  throw new CodingProxyError(400, "openai_tool_choice_not_allowed");
@@ -401,7 +479,7 @@ function validateOpenAiBody(body) {
401
479
  throw new CodingProxyError(400, "invalid_openai_request");
402
480
  }
403
481
  for (const item of body.input)
404
- validateOpenAiInputItem(item, toolCount);
482
+ validateOpenAiInputItem(item, distinctTools);
405
483
  }
406
484
  function parseOpenAiRequest(rawBody) {
407
485
  if (Buffer.byteLength(rawBody) > PROXY_MAX_BODY_BYTES)
@@ -516,15 +594,33 @@ function validateStructuredOutputTool(value) {
516
594
  if (tool.cache_control !== undefined)
517
595
  validateCacheControl(tool.cache_control);
518
596
  }
519
- function validateApprovedTool(value) {
597
+ /**
598
+ * One of the run's pinned MCP tools, as Claude Code advertises it: a plain client tool named
599
+ * `mcp__wardby_mcp__<exposed name>` (fixtures/claude-code-2.1.286-mcp-requests.json). Only the name
600
+ * is checked against the pins; the gateway checks the call itself again.
601
+ */
602
+ function validateMcpTool(tool) {
603
+ onlyKeys(tool, ["name", "description", "input_schema", "cache_control", "type"]);
604
+ if ((tool.description !== undefined &&
605
+ (typeof tool.description !== "string" || tool.description.length > MAX_TOOL_TEXT_BYTES)) ||
606
+ (tool.type !== undefined && tool.type !== "custom")) {
607
+ throw new CodingProxyError(400, "tools_not_allowed");
608
+ }
609
+ record(tool.input_schema, "tools_not_allowed");
610
+ if (tool.cache_control !== undefined)
611
+ validateCacheControl(tool.cache_control);
612
+ }
613
+ function validateApprovedTool(value, mcpTools) {
520
614
  const tool = record(value, "tools_not_allowed");
521
615
  if (tool.name === WARDBY_COMMAND_TOOL)
522
616
  return validateCommandTool(tool);
523
617
  if (tool.name === STRUCTURED_OUTPUT_TOOL)
524
618
  return validateStructuredOutputTool(tool);
619
+ if (typeof tool.name === "string" && mcpTools.has(tool.name))
620
+ return validateMcpTool(tool);
525
621
  throw new CodingProxyError(400, "tools_not_allowed");
526
622
  }
527
- function validateToolUseBlock(value) {
623
+ function validateToolUseBlock(value, mcpTools) {
528
624
  onlyKeys(value, ["type", "id", "name", "input"]);
529
625
  if (typeof value.id !== "string" || value.id.length < 1 || value.id.length > 512) {
530
626
  throw new CodingProxyError(400, "unsupported_anthropic_feature");
@@ -533,6 +629,9 @@ function validateToolUseBlock(value) {
533
629
  return validateCommandInput(value.input);
534
630
  if (value.name === STRUCTURED_OUTPUT_TOOL)
535
631
  return void record(value.input, "unsupported_anthropic_feature");
632
+ if (typeof value.name === "string" && mcpTools.has(value.name)) {
633
+ return void record(value.input, "unsupported_anthropic_feature");
634
+ }
536
635
  throw new CodingProxyError(400, "unsupported_anthropic_feature");
537
636
  }
538
637
  function validateToolResultBlock(value) {
@@ -559,7 +658,7 @@ function validateThinkingBlock(value) {
559
658
  throw new CodingProxyError(400, "unsupported_anthropic_feature");
560
659
  }
561
660
  }
562
- function validateMessageBlocks(value, role) {
661
+ function validateMessageBlocks(value, role, mcpTools) {
563
662
  if (!Array.isArray(value) || value.length > 10_000)
564
663
  throw new CodingProxyError(400, "invalid_anthropic_request");
565
664
  for (const candidate of value) {
@@ -568,7 +667,7 @@ function validateMessageBlocks(value, role) {
568
667
  validateTextBlock(block);
569
668
  }
570
669
  else if (block.type === "tool_use" && role === "assistant") {
571
- validateToolUseBlock(block);
670
+ validateToolUseBlock(block, mcpTools);
572
671
  }
573
672
  else if (block.type === "tool_result" && role === "user") {
574
673
  validateToolResultBlock(block);
@@ -624,7 +723,7 @@ function validateEffortConfig(value, { thinkingMode, efforts }, betas) {
624
723
  throw new CodingProxyError(400, "unsupported_anthropic_feature");
625
724
  }
626
725
  }
627
- function parseAnthropicRequest(rawBody, betaHeader, shape) {
726
+ function parseAnthropicRequest(rawBody, betaHeader, shape, mcpTools) {
628
727
  const beta = parseAnthropicBeta(betaHeader);
629
728
  if (Buffer.byteLength(rawBody) > PROXY_MAX_BODY_BYTES)
630
729
  throw new CodingProxyError(413, "payload_too_large");
@@ -688,18 +787,20 @@ function parseAnthropicRequest(rawBody, betaHeader, shape) {
688
787
  continue;
689
788
  }
690
789
  }
691
- validateMessageBlocks(message.content, message.role);
790
+ validateMessageBlocks(message.content, message.role, mcpTools);
692
791
  }
693
792
  if (body.tools !== undefined) {
694
- if (!Array.isArray(body.tools) || body.tools.length > 2)
793
+ // run_command and StructuredOutput, plus one per pinned MCP tool.
794
+ if (!Array.isArray(body.tools) || body.tools.length > 2 + mcpTools.size) {
695
795
  throw new CodingProxyError(400, "tools_not_allowed");
796
+ }
696
797
  const names = new Set();
697
798
  for (const tool of body.tools) {
698
799
  const definition = record(tool, "tools_not_allowed");
699
800
  if (typeof definition.name !== "string" || names.has(definition.name))
700
801
  throw new CodingProxyError(400, "tools_not_allowed");
701
802
  names.add(definition.name);
702
- validateApprovedTool(definition);
803
+ validateApprovedTool(definition, mcpTools);
703
804
  }
704
805
  }
705
806
  if (body.metadata !== undefined) {
@@ -760,13 +861,17 @@ function parseAnthropicRequest(rawBody, betaHeader, shape) {
760
861
  anthropicBeta: beta.header,
761
862
  };
762
863
  }
763
- function parseRequest(protocol, rawBody, anthropicBeta, shape) {
864
+ /** The Claude tool names of a session's pinned MCP tools; empty for a session without pins. */
865
+ function claudeMcpToolNames(pins) {
866
+ return new Set((pins ?? []).map((pin) => `${CLAUDE_MCP_TOOL_PREFIX}${pin.exposedName}`));
867
+ }
868
+ function parseRequest(protocol, rawBody, anthropicBeta, shape, mcpTools) {
764
869
  if (protocol !== "anthropic-messages" && anthropicBeta !== undefined) {
765
870
  throw new CodingProxyError(400, "invalid_anthropic_beta");
766
871
  }
767
872
  try {
768
873
  return protocol === "anthropic-messages"
769
- ? parseAnthropicRequest(rawBody, anthropicBeta, shape)
874
+ ? parseAnthropicRequest(rawBody, anthropicBeta, shape, mcpTools)
770
875
  : parseOpenAiRequest(rawBody);
771
876
  }
772
877
  catch (error) {
@@ -821,9 +926,17 @@ export class CodingProxy {
821
926
  priceVersion;
822
927
  activeRequests = new Map();
823
928
  onModelRequest;
929
+ steps;
930
+ createStepExtractor;
931
+ runStepsCeiling;
932
+ /** Results already handed over, per session: each request resends the history. */
933
+ seenResults = new SeenIds();
824
934
  constructor(options) {
825
935
  this.ledger = options.ledger;
826
936
  this.onModelRequest = options.onModelRequest;
937
+ this.steps = options.steps;
938
+ this.createStepExtractor = options.createStepExtractor;
939
+ this.runStepsCeiling = options.runStepsCeiling ?? "summary";
827
940
  this.credentials = options.credentials;
828
941
  this.upstreamUrls = {
829
942
  "openai-responses": options.upstreamUrl ?? "https://api.openai.com/v1/responses",
@@ -874,6 +987,14 @@ export class CodingProxy {
874
987
  throw new Error("invalid_proxy_deadline");
875
988
  if (!input.credentialRef || input.credentialRef.length > 200)
876
989
  throw new Error("invalid_proxy_credential_reference");
990
+ // Both or neither: pins without a token could never be called, a token without pins lists nothing.
991
+ const mcp = input.mcpTools !== undefined || input.mcpTokenHash !== undefined;
992
+ if (mcp && (!input.mcpTools || input.mcpTools.length === 0 || !input.mcpTokenHash)) {
993
+ throw new Error("invalid_proxy_mcp_tools");
994
+ }
995
+ if (input.runStepsLevel !== undefined && !RUN_STEPS_LEVELS.includes(input.runStepsLevel)) {
996
+ throw new Error("invalid_proxy_run_steps_level");
997
+ }
877
998
  const capability = `rrp_${randomBytes(32).toString("base64url")}`;
878
999
  const id = randomUUID();
879
1000
  const registryTokenHash = capabilityHash(deriveRegistryToken(capability));
@@ -888,6 +1009,8 @@ export class CodingProxy {
888
1009
  budgetUsd: input.budgetUsd,
889
1010
  registryTokenHash,
890
1011
  terms: input.terms,
1012
+ ...(mcp ? { mcpTools: input.mcpTools, mcpTokenHash: input.mcpTokenHash } : {}),
1013
+ ...(input.runStepsLevel ? { runStepsLevel: input.runStepsLevel } : {}),
891
1014
  });
892
1015
  this.audit({ type: "session.created", runId: input.runId });
893
1016
  return {
@@ -901,6 +1024,7 @@ export class CodingProxy {
901
1024
  };
902
1025
  }
903
1026
  async cancelSession(sessionId) {
1027
+ this.seenResults.forget(sessionId);
904
1028
  await this.ledger.cancelSession(sessionId);
905
1029
  for (const controller of this.activeRequests.get(sessionId) ?? [])
906
1030
  controller.abort();
@@ -934,7 +1058,7 @@ export class CodingProxy {
934
1058
  };
935
1059
  let parsed;
936
1060
  try {
937
- parsed = parseRequest(input.protocol, input.rawBody, input.anthropicBeta, shape);
1061
+ parsed = parseRequest(input.protocol, input.rawBody, input.anthropicBeta, shape, claudeMcpToolNames(session.mcpTools));
938
1062
  }
939
1063
  catch (error) {
940
1064
  // The session is already authenticated and no credential has been
@@ -1000,6 +1124,8 @@ export class CodingProxy {
1000
1124
  now: this.now(),
1001
1125
  });
1002
1126
  if (reservation.outcome === "inactive") {
1127
+ // The session has ended (cancelled, expired or closed): another replica may have ended it.
1128
+ this.seenResults.forget(session.id);
1003
1129
  this.audit({ type: "request.rejected", runId: session.runId, reason: reservation.reason });
1004
1130
  throw new CodingProxyError(403, `session_${reservation.reason}`);
1005
1131
  }
@@ -1029,6 +1155,14 @@ export class CodingProxy {
1029
1155
  model: request.model,
1030
1156
  ...holdAudit,
1031
1157
  });
1158
+ // Run steps: the results this (validated) request carries for the previous turn, once per call id.
1159
+ const capture = stepCapture(session, this.runStepsCeiling);
1160
+ if (capture !== "none") {
1161
+ this.submitSteps(input.bearer, session.runId, () => ({
1162
+ steps: [],
1163
+ results: this.seenResults.fresh(session.id, resultsFromRequest(session.protocol, parsed.body, { detail: capture === "detail" })),
1164
+ }));
1165
+ }
1032
1166
  let key;
1033
1167
  try {
1034
1168
  key = await this.credentials.resolve(session.credentialRef);
@@ -1048,6 +1182,7 @@ export class CodingProxy {
1048
1182
  }
1049
1183
  const remainingMs = session.deadlineAt.getTime() - this.now().getTime();
1050
1184
  if (remainingMs <= 0) {
1185
+ this.seenResults.forget(session.id);
1051
1186
  await this.ledger.release(request.id, 403);
1052
1187
  throw new CodingProxyError(403, "session_expired");
1053
1188
  }
@@ -1121,9 +1256,9 @@ export class CodingProxy {
1121
1256
  }
1122
1257
  try {
1123
1258
  if (parsed.stream)
1124
- await this.forwardStream(session, request, upstream, sink, progress);
1259
+ await this.forwardStream(session, request, upstream, sink, progress, input.bearer);
1125
1260
  else
1126
- await this.forwardJson(session, request, upstream, sink);
1261
+ await this.forwardJson(session, request, upstream, sink, input.bearer);
1127
1262
  }
1128
1263
  catch (error) {
1129
1264
  await this.markUncertain(session, request, upstream.status, failureReason(error), responseHeadersOf(upstream), progress);
@@ -1193,7 +1328,7 @@ export class CodingProxy {
1193
1328
  throw new CodingProxyError(502, "reservation_invariant_violated");
1194
1329
  }
1195
1330
  }
1196
- async forwardJson(session, request, upstream, sink) {
1331
+ async forwardJson(session, request, upstream, sink, capability) {
1197
1332
  const declaredLength = Number(upstream.headers.get("content-length"));
1198
1333
  if (Number.isFinite(declaredLength) && declaredLength > MAX_UPSTREAM_JSON_BYTES) {
1199
1334
  throw new Error("upstream_response_too_large");
@@ -1213,8 +1348,15 @@ export class CodingProxy {
1213
1348
  sink.start(upstream.status, { "content-type": "application/json", "cache-control": "no-store" });
1214
1349
  await safeWrite(sink, bytes, { value: true });
1215
1350
  sink.end();
1351
+ const capture = stepCapture(session, this.runStepsCeiling);
1352
+ if (capture !== "none") {
1353
+ this.submitSteps(capability, session.runId, () => ({
1354
+ steps: stepsFromJson(session.protocol, value, { detail: capture === "detail" }),
1355
+ results: [],
1356
+ }));
1357
+ }
1216
1358
  }
1217
- async forwardStream(session, request, upstream, sink, progress) {
1359
+ async forwardStream(session, request, upstream, sink, progress, capability) {
1218
1360
  if (!upstream.body)
1219
1361
  throw new Error("missing_upstream_body");
1220
1362
  sink.start(upstream.status, { "content-type": "text/event-stream", "cache-control": "no-store" });
@@ -1225,49 +1367,98 @@ export class CodingProxy {
1225
1367
  let completed = false;
1226
1368
  let failedWithoutUsage = false;
1227
1369
  const anthropicUsage = session.protocol === "anthropic-messages" ? (progress.anthropic = new AnthropicSseUsageTracker()) : undefined;
1370
+ // Run steps: each frame is read after it was relayed. An extractor that throws is dropped, never the relay.
1371
+ let steps = this.newStepExtractor(session);
1372
+ const observe = (frame) => {
1373
+ if (!steps)
1374
+ return;
1375
+ try {
1376
+ steps.consume(frame);
1377
+ }
1378
+ catch {
1379
+ steps = undefined;
1380
+ }
1381
+ };
1228
1382
  const body = upstream.body;
1229
- for await (const chunk of body) {
1230
- receivedBytes += chunk.byteLength;
1231
- if (receivedBytes > MAX_UPSTREAM_JSON_BYTES)
1232
- throw new Error("upstream_response_too_large");
1233
- buffer = (buffer + decoder.decode(chunk, { stream: true })).replaceAll("\r\n", "\n");
1234
- for (;;) {
1235
- const boundary = buffer.indexOf("\n\n");
1236
- if (boundary < 0)
1237
- break;
1238
- const frame = buffer.slice(0, boundary);
1239
- buffer = buffer.slice(boundary + 2);
1240
- const terminal = anthropicUsage ? anthropicUsage.consume(frame) : terminalUsageFromSseFrame(frame);
1241
- if (!terminal.terminal) {
1242
- progress.relayedOutputBytes += relayedTextBytes(frame, session.protocol);
1243
- await safeWrite(sink, Buffer.from(`${frame}\n\n`), connected);
1244
- continue;
1245
- }
1246
- if (!terminal.usage) {
1247
- if (!terminal.failure)
1248
- throw new Error("terminal_usage_missing");
1249
- // The upstream failed the response and reported no usage. Pass its failure on so the
1250
- // client sees the real error instead of a cut connection; the reservation stays held.
1251
- await this.recordUpstreamFailure(session, terminal.failure);
1252
- if (!completed) {
1253
- await this.markUncertain(session, request, upstream.status, `upstream_failed:${terminal.failure}`, responseHeadersOf(upstream), progress);
1383
+ try {
1384
+ for await (const chunk of body) {
1385
+ receivedBytes += chunk.byteLength;
1386
+ if (receivedBytes > MAX_UPSTREAM_JSON_BYTES)
1387
+ throw new Error("upstream_response_too_large");
1388
+ buffer = (buffer + decoder.decode(chunk, { stream: true })).replaceAll("\r\n", "\n");
1389
+ for (;;) {
1390
+ const boundary = buffer.indexOf("\n\n");
1391
+ if (boundary < 0)
1392
+ break;
1393
+ const frame = buffer.slice(0, boundary);
1394
+ buffer = buffer.slice(boundary + 2);
1395
+ const terminal = anthropicUsage ? anthropicUsage.consume(frame) : terminalUsageFromSseFrame(frame);
1396
+ if (!terminal.terminal) {
1397
+ progress.relayedOutputBytes += relayedTextBytes(frame, session.protocol);
1398
+ await safeWrite(sink, Buffer.from(`${frame}\n\n`), connected);
1399
+ observe(frame);
1400
+ continue;
1401
+ }
1402
+ if (!terminal.usage) {
1403
+ if (!terminal.failure)
1404
+ throw new Error("terminal_usage_missing");
1405
+ // The upstream failed the response and reported no usage. Pass its failure on so the
1406
+ // client sees the real error instead of a cut connection; the reservation stays held.
1407
+ await this.recordUpstreamFailure(session, terminal.failure);
1408
+ if (!completed) {
1409
+ await this.markUncertain(session, request, upstream.status, `upstream_failed:${terminal.failure}`, responseHeadersOf(upstream), progress);
1410
+ }
1411
+ failedWithoutUsage = true;
1412
+ await safeWrite(sink, Buffer.from(`${frame}\n\n`), connected);
1413
+ observe(frame);
1414
+ continue;
1254
1415
  }
1255
- failedWithoutUsage = true;
1416
+ await this.complete(session, request, terminal.usage, upstream.status);
1417
+ completed = true;
1256
1418
  await safeWrite(sink, Buffer.from(`${frame}\n\n`), connected);
1257
- continue;
1419
+ observe(frame);
1258
1420
  }
1259
- await this.complete(session, request, terminal.usage, upstream.status);
1260
- completed = true;
1261
- await safeWrite(sink, Buffer.from(`${frame}\n\n`), connected);
1262
1421
  }
1422
+ buffer = (buffer + decoder.decode()).replaceAll("\r\n", "\n");
1423
+ if (!completed && !failedWithoutUsage)
1424
+ throw new Error("terminal_usage_missing");
1425
+ if (buffer)
1426
+ await safeWrite(sink, Buffer.from(buffer), connected);
1427
+ if (connected.value)
1428
+ sink.end();
1429
+ }
1430
+ finally {
1431
+ // The reply's steps, also when the stream broke (then its open text is marked cut).
1432
+ const extractor = steps;
1433
+ if (extractor)
1434
+ this.submitSteps(capability, session.runId, () => ({ steps: extractor.finish(), results: [] }));
1435
+ }
1436
+ }
1437
+ /** Queues a step batch for the control plane. Best effort: any failure drops the steps, never the request. */
1438
+ submitSteps(capability, runId, build) {
1439
+ if (!this.steps)
1440
+ return;
1441
+ try {
1442
+ const batch = build();
1443
+ if (batch.steps.length > 0 || batch.results.length > 0)
1444
+ this.steps.submit(capability, runId, batch);
1445
+ }
1446
+ catch {
1447
+ // Dropped: steps are never a reason to fail a model request.
1448
+ }
1449
+ }
1450
+ newStepExtractor(session) {
1451
+ if (!this.steps)
1452
+ return undefined;
1453
+ const capture = stepCapture(session, this.runStepsCeiling);
1454
+ if (capture === "none")
1455
+ return undefined;
1456
+ try {
1457
+ return (this.createStepExtractor ?? streamStepExtractor)(session.protocol, { detail: capture === "detail" });
1458
+ }
1459
+ catch {
1460
+ return undefined;
1263
1461
  }
1264
- buffer = (buffer + decoder.decode()).replaceAll("\r\n", "\n");
1265
- if (!completed && !failedWithoutUsage)
1266
- throw new Error("terminal_usage_missing");
1267
- if (buffer)
1268
- await safeWrite(sink, Buffer.from(buffer), connected);
1269
- if (connected.value)
1270
- sink.end();
1271
1462
  }
1272
1463
  /** Best effort: the executor reads it to name the failure; it must never break the relay. */
1273
1464
  async recordUpstreamFailure(session, code) {
@@ -0,0 +1,42 @@
1
+ import { type RunStepBatch, type RunStepSubmitter } from "../../core/run-steps/types.js";
2
+ export declare const MAX_FORWARD_QUEUE = 256;
3
+ /** A queue holds at most this many bytes of (estimated) serialized submissions. */
4
+ export declare const MAX_FORWARD_QUEUE_BYTES: number;
5
+ export interface RunStepForwarderOptions {
6
+ /** WARDBY_RUN_STEPS_URL, without a trailing slash. */
7
+ url: string;
8
+ fetchImpl?: typeof fetch;
9
+ maxQueue?: number;
10
+ maxQueueBytes?: number;
11
+ timeoutMs?: number;
12
+ /** How long flush() and close() wait for the queue to settle before giving up. */
13
+ flushTimeoutMs?: number;
14
+ log?: {
15
+ info(fields: Record<string, unknown>, message: string): void;
16
+ };
17
+ }
18
+ export declare class RunStepForwarder implements RunStepSubmitter {
19
+ private readonly options;
20
+ private queue;
21
+ private queueBytes;
22
+ private draining;
23
+ private scheduled;
24
+ private closed;
25
+ private inflight;
26
+ private dropped;
27
+ private readonly loggedReasons;
28
+ constructor(options: RunStepForwarderOptions);
29
+ submit(capability: string, runId: string, batch: RunStepBatch): void;
30
+ /** Resolves once everything submitted so far was posted or dropped, or after a bound. Never rejects. */
31
+ flush(): Promise<void>;
32
+ /** Stops accepting, waits at most the flush bound, then drops what is left and aborts the in-flight POST. */
33
+ close(): Promise<void>;
34
+ private kick;
35
+ private drain;
36
+ /**
37
+ * The capability goes in the Authorization header only. The response body is never read, and a
38
+ * failure is logged by a fixed reason: an error's or a response's text never reaches a log line.
39
+ */
40
+ private post;
41
+ private drop;
42
+ }