@wardby/cli 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (367) hide show
  1. package/.env.example +5 -5
  2. package/README.md +6 -1
  3. package/deploy/local/docker-compose.quickstart-coding.yml +11 -0
  4. package/dist/claude-coding-worker/driver.d.ts +12 -0
  5. package/dist/claude-coding-worker/driver.js +29 -7
  6. package/dist/claude-coding-worker/main.js +5 -1
  7. package/dist/claude-coding-worker/sdk.js +17 -1
  8. package/dist/claude-plugin/command.d.ts +64 -0
  9. package/dist/claude-plugin/command.js +379 -0
  10. package/dist/cli-help.d.ts +1 -1
  11. package/dist/cli-help.js +2 -1
  12. package/dist/cli.js +71 -3
  13. package/dist/coding/code-host.d.ts +16 -0
  14. package/dist/coding/code-host.js +60 -0
  15. package/dist/coding/mcp-token.d.ts +12 -0
  16. package/dist/coding/mcp-token.js +26 -0
  17. package/dist/coding/protocol.js +2 -0
  18. package/dist/coding/redact.js +6 -1
  19. package/dist/coding/transcript/recorder.d.ts +5 -4
  20. package/dist/coding/transcript/recorder.js +3 -9
  21. package/dist/coding-worker/debug-trace.d.ts +3 -1
  22. package/dist/coding-worker/debug-trace.js +4 -2
  23. package/dist/coding-worker/driver.js +8 -1
  24. package/dist/coding-worker/main.js +5 -1
  25. package/dist/coding-worker/mcp-config.d.ts +27 -0
  26. package/dist/coding-worker/mcp-config.js +32 -0
  27. package/dist/coding-worker/sdk.d.ts +36 -0
  28. package/dist/coding-worker/sdk.js +28 -1
  29. package/dist/coding-worker/types.d.ts +4 -0
  30. package/dist/config/mcp-call-limit.d.ts +8 -0
  31. package/dist/config/mcp-call-limit.js +18 -0
  32. package/dist/config/mcp-servers.d.ts +8 -0
  33. package/dist/config/mcp-servers.js +12 -0
  34. package/dist/config/test-simulators.d.ts +10 -0
  35. package/dist/config/test-simulators.js +28 -0
  36. package/dist/config/tool-timeout.d.ts +1 -0
  37. package/dist/config/tool-timeout.js +18 -0
  38. package/dist/core/coding-mcp.d.ts +36 -0
  39. package/dist/core/coding-mcp.js +253 -0
  40. package/dist/core/coding-repository.d.ts +46 -0
  41. package/dist/core/coding-repository.js +92 -0
  42. package/dist/core/delegation-slots.d.ts +27 -0
  43. package/dist/core/delegation-slots.js +44 -0
  44. package/dist/core/dispatch.d.ts +6 -2
  45. package/dist/core/dispatch.js +39 -11
  46. package/dist/core/engine-native.d.ts +2 -0
  47. package/dist/core/engine-native.js +13 -0
  48. package/dist/core/error-code.js +2 -0
  49. package/dist/core/grants.d.ts +28 -3
  50. package/dist/core/grants.js +46 -4
  51. package/dist/core/host-events.js +7 -3
  52. package/dist/core/host-status.js +2 -2
  53. package/dist/core/issue-status.js +4 -3
  54. package/dist/core/listen-address.d.ts +10 -0
  55. package/dist/core/listen-address.js +14 -0
  56. package/dist/core/mark-ready.d.ts +1 -1
  57. package/dist/core/mark-ready.js +1 -1
  58. package/dist/core/mcp-client/client.d.ts +47 -0
  59. package/dist/core/mcp-client/client.js +141 -0
  60. package/dist/core/mcp-client/jsonrpc.d.ts +15 -0
  61. package/dist/core/mcp-client/jsonrpc.js +54 -0
  62. package/dist/core/mcp-client/results.d.ts +13 -0
  63. package/dist/core/mcp-client/results.js +42 -0
  64. package/dist/core/mcp-servers.d.ts +90 -0
  65. package/dist/core/mcp-servers.js +230 -0
  66. package/dist/core/mcp-tools.d.ts +108 -0
  67. package/dist/core/mcp-tools.js +287 -0
  68. package/dist/core/merge-order-check.js +38 -19
  69. package/dist/core/notification-dispatcher.js +2 -1
  70. package/dist/core/power-idle.d.ts +8 -2
  71. package/dist/core/power-idle.js +17 -3
  72. package/dist/core/power.d.ts +5 -0
  73. package/dist/core/power.js +5 -0
  74. package/dist/core/related-pull-requests.js +26 -10
  75. package/dist/core/retention-days.d.ts +5 -0
  76. package/dist/core/retention-days.js +14 -0
  77. package/dist/core/review-fix.js +2 -2
  78. package/dist/core/review-host-tools.js +3 -1
  79. package/dist/core/run-steps/config.d.ts +25 -0
  80. package/dist/core/run-steps/config.js +40 -0
  81. package/dist/core/run-steps/control-chars.d.ts +16 -0
  82. package/dist/core/run-steps/control-chars.js +26 -0
  83. package/dist/core/run-steps/detail.d.ts +32 -0
  84. package/dist/core/run-steps/detail.js +240 -0
  85. package/dist/core/run-steps/extract.d.ts +52 -0
  86. package/dist/core/run-steps/extract.js +467 -0
  87. package/dist/core/run-steps/ingress.d.ts +51 -0
  88. package/dist/core/run-steps/ingress.js +160 -0
  89. package/dist/core/run-steps/last-step.d.ts +15 -0
  90. package/dist/core/run-steps/last-step.js +35 -0
  91. package/dist/core/run-steps/native.d.ts +48 -0
  92. package/dist/core/run-steps/native.js +164 -0
  93. package/dist/core/run-steps/seen.d.ts +16 -0
  94. package/dist/core/run-steps/seen.js +35 -0
  95. package/dist/core/run-steps/summarize.d.ts +57 -0
  96. package/dist/core/run-steps/summarize.js +304 -0
  97. package/dist/core/run-steps/sweep.d.ts +21 -0
  98. package/dist/core/run-steps/sweep.js +41 -0
  99. package/dist/core/run-steps/types.d.ts +89 -0
  100. package/dist/core/run-steps/types.js +39 -0
  101. package/dist/core/run-steps/wire.d.ts +35 -0
  102. package/dist/core/run-steps/wire.js +47 -0
  103. package/dist/core/run-steps/writer.d.ts +49 -0
  104. package/dist/core/run-steps/writer.js +279 -0
  105. package/dist/core/runner.d.ts +70 -16
  106. package/dist/core/runner.js +296 -74
  107. package/dist/core/secret-broker-config.d.ts +2 -0
  108. package/dist/core/secret-broker-config.js +1 -1
  109. package/dist/core/secret-guard.d.ts +5 -2
  110. package/dist/core/secret-guard.js +22 -7
  111. package/dist/core/secrets.d.ts +11 -1
  112. package/dist/core/secrets.js +27 -1
  113. package/dist/core/serializable-retry.d.ts +18 -0
  114. package/dist/core/serializable-retry.js +43 -0
  115. package/dist/core/tool-admin.d.ts +5 -1
  116. package/dist/core/tool-admin.js +7 -2
  117. package/dist/core/tool-names.d.ts +13 -0
  118. package/dist/core/tool-names.js +20 -0
  119. package/dist/core/webhooks.d.ts +4 -0
  120. package/dist/core/webhooks.js +68 -49
  121. package/dist/core/workflow-run-events.js +2 -1
  122. package/dist/generated/prisma/browser.d.ts +39 -0
  123. package/dist/generated/prisma/client.d.ts +39 -0
  124. package/dist/generated/prisma/commonInputTypes.d.ts +80 -50
  125. package/dist/generated/prisma/enums.d.ts +6 -0
  126. package/dist/generated/prisma/enums.js +5 -0
  127. package/dist/generated/prisma/internal/class.d.ts +77 -0
  128. package/dist/generated/prisma/internal/class.js +4 -4
  129. package/dist/generated/prisma/internal/prismaNamespace.d.ts +632 -1
  130. package/dist/generated/prisma/internal/prismaNamespace.js +94 -3
  131. package/dist/generated/prisma/internal/prismaNamespaceBrowser.d.ts +98 -0
  132. package/dist/generated/prisma/internal/prismaNamespaceBrowser.js +94 -3
  133. package/dist/generated/prisma/models/Agent.d.ts +372 -1
  134. package/dist/generated/prisma/models/AgentMcpServer.d.ts +1412 -0
  135. package/dist/generated/prisma/models/AgentMcpServer.js +1 -0
  136. package/dist/generated/prisma/models/AgentRepository.d.ts +40 -1
  137. package/dist/generated/prisma/models/AuthUser.d.ts +1 -1
  138. package/dist/generated/prisma/models/CodingAgentProfile.d.ts +0 -7
  139. package/dist/generated/prisma/models/CodingProxySession.d.ts +159 -2
  140. package/dist/generated/prisma/models/CodingRun.d.ts +76 -1
  141. package/dist/generated/prisma/models/HostProvider.d.ts +2176 -0
  142. package/dist/generated/prisma/models/HostProvider.js +1 -0
  143. package/dist/generated/prisma/models/HostProviderCredential.d.ts +1285 -0
  144. package/dist/generated/prisma/models/HostProviderCredential.js +1 -0
  145. package/dist/generated/prisma/models/HostProviderOperation.d.ts +1470 -0
  146. package/dist/generated/prisma/models/HostProviderOperation.js +1 -0
  147. package/dist/generated/prisma/models/HostProviderTrustedAccount.d.ts +1145 -0
  148. package/dist/generated/prisma/models/HostProviderTrustedAccount.js +1 -0
  149. package/dist/generated/prisma/models/McpServer.d.ts +1500 -0
  150. package/dist/generated/prisma/models/McpServer.js +1 -0
  151. package/dist/generated/prisma/models/NativeGatewayCall.d.ts +39 -1
  152. package/dist/generated/prisma/models/Principal.d.ts +167 -0
  153. package/dist/generated/prisma/models/ResourceGrant.d.ts +1 -1
  154. package/dist/generated/prisma/models/Run.d.ts +503 -1
  155. package/dist/generated/prisma/models/RunStep.d.ts +1453 -0
  156. package/dist/generated/prisma/models/RunStep.js +1 -0
  157. package/dist/generated/prisma/models/Secret.d.ts +387 -0
  158. package/dist/generated/prisma/models/Tool.d.ts +223 -1
  159. package/dist/generated/prisma/models.d.ts +7 -0
  160. package/dist/help/search.js +12 -3
  161. package/dist/help-index.json +812 -52
  162. package/dist/import/create.js +16 -1
  163. package/dist/import/neutral-schema.d.ts +1 -0
  164. package/dist/import/neutral-schema.js +2 -0
  165. package/dist/mcp/auth/access.d.ts +1 -0
  166. package/dist/mcp/auth/grants-cli.d.ts +6 -4
  167. package/dist/mcp/auth/grants-cli.js +42 -3
  168. package/dist/mcp/auth/resource-server.d.ts +3 -1
  169. package/dist/mcp/auth/resource-server.js +17 -2
  170. package/dist/mcp/context.d.ts +6 -0
  171. package/dist/mcp/index.js +21 -0
  172. package/dist/mcp/tools/agents.js +135 -20
  173. package/dist/mcp/tools/grants.js +66 -28
  174. package/dist/mcp/tools/host-providers.d.ts +2 -0
  175. package/dist/mcp/tools/host-providers.js +215 -0
  176. package/dist/mcp/tools/mcp-servers.d.ts +26 -0
  177. package/dist/mcp/tools/mcp-servers.js +481 -0
  178. package/dist/mcp/tools/power.js +2 -2
  179. package/dist/mcp/tools/repositories.js +50 -10
  180. package/dist/mcp/tools/run-access.d.ts +7 -0
  181. package/dist/mcp/tools/run-access.js +14 -0
  182. package/dist/mcp/tools/run-steps.d.ts +16 -0
  183. package/dist/mcp/tools/run-steps.js +85 -0
  184. package/dist/mcp/tools/runs.js +186 -18
  185. package/dist/mcp/tools/secret-elicitation.d.ts +2 -0
  186. package/dist/mcp/tools/secret-elicitation.js +25 -2
  187. package/dist/mcp/tools/secrets.js +38 -4
  188. package/dist/mcp/tools/tools.d.ts +6 -4
  189. package/dist/mcp/tools/tools.js +40 -8
  190. package/dist/mcp/tools/trigger.js +28 -6
  191. package/dist/mcp/transport/http-limits.d.ts +2 -0
  192. package/dist/mcp/transport/http-limits.js +6 -2
  193. package/dist/mcp/webhooks/ingress.js +10 -0
  194. package/dist/native-worker/gateway-server.d.ts +18 -1
  195. package/dist/native-worker/gateway-server.js +38 -9
  196. package/dist/native-worker/gateway.d.ts +5 -0
  197. package/dist/native-worker/gateway.js +13 -1
  198. package/dist/native-worker/http-server.d.ts +6 -4
  199. package/dist/native-worker/http-server.js +53 -8
  200. package/dist/native-worker/ledger.d.ts +6 -2
  201. package/dist/native-worker/ledger.js +5 -4
  202. package/dist/native-worker/protocol.d.ts +1 -0
  203. package/dist/native-worker/protocol.js +4 -1
  204. package/dist/providers/coding-proxy/capability-hash.d.ts +2 -0
  205. package/dist/providers/coding-proxy/capability-hash.js +5 -0
  206. package/dist/providers/coding-proxy/mcp-route.d.ts +34 -0
  207. package/dist/providers/coding-proxy/mcp-route.js +166 -0
  208. package/dist/providers/coding-proxy/memory-ledger.d.ts +2 -0
  209. package/dist/providers/coding-proxy/memory-ledger.js +16 -0
  210. package/dist/providers/coding-proxy/prisma-ledger.d.ts +1 -0
  211. package/dist/providers/coding-proxy/prisma-ledger.js +36 -2
  212. package/dist/providers/coding-proxy/proxy.d.ts +33 -1
  213. package/dist/providers/coding-proxy/proxy.js +266 -75
  214. package/dist/providers/coding-proxy/run-step-forwarder.d.ts +42 -0
  215. package/dist/providers/coding-proxy/run-step-forwarder.js +206 -0
  216. package/dist/providers/coding-proxy/runtime.d.ts +13 -0
  217. package/dist/providers/coding-proxy/runtime.js +87 -4
  218. package/dist/providers/coding-proxy/server.d.ts +3 -0
  219. package/dist/providers/coding-proxy/server.js +10 -0
  220. package/dist/providers/coding-proxy/types.d.ts +18 -0
  221. package/dist/providers/engine/types.d.ts +26 -0
  222. package/dist/providers/executor/composition.js +5 -0
  223. package/dist/providers/executor/container.d.ts +47 -1
  224. package/dist/providers/executor/container.js +84 -8
  225. package/dist/providers/host-tools/invoke.d.ts +37 -0
  226. package/dist/providers/host-tools/invoke.js +117 -0
  227. package/dist/providers/host-tools/operations.d.ts +65 -0
  228. package/dist/providers/host-tools/operations.js +149 -0
  229. package/dist/providers/host-tools/registry.d.ts +79 -0
  230. package/dist/providers/host-tools/registry.js +362 -0
  231. package/dist/providers/host-tools/request-guard.d.ts +17 -0
  232. package/dist/providers/host-tools/request-guard.js +99 -0
  233. package/dist/providers/host-tools/self-test.d.ts +64 -0
  234. package/dist/providers/host-tools/self-test.js +142 -0
  235. package/dist/providers/jobs/docker-isolation.d.ts +2 -2
  236. package/dist/providers/jobs/docker-isolation.js +30 -8
  237. package/dist/providers/jobs/docker.d.ts +8 -0
  238. package/dist/providers/jobs/docker.js +33 -9
  239. package/dist/providers/jobs/kubernetes-isolation.d.ts +3 -1
  240. package/dist/providers/jobs/kubernetes-isolation.js +15 -2
  241. package/dist/providers/jobs/kubernetes.d.ts +13 -0
  242. package/dist/providers/jobs/kubernetes.js +28 -5
  243. package/dist/providers/jobs/types.d.ts +6 -0
  244. package/dist/providers/review-host/types.d.ts +6 -4
  245. package/dist/providers/review-host/types.js +1 -1
  246. package/dist/providers/vcs/git.d.ts +6 -2
  247. package/dist/providers/vcs/git.js +15 -4
  248. package/dist/providers/vcs/github.d.ts +2 -71
  249. package/dist/providers/vcs/github.js +8 -11
  250. package/dist/providers/vcs/index.d.ts +1 -1
  251. package/dist/providers/vcs/index.js +1 -1
  252. package/dist/providers/vcs/remote.d.ts +4 -0
  253. package/dist/providers/vcs/remote.js +7 -1
  254. package/dist/providers/vcs/routing.d.ts +12 -6
  255. package/dist/providers/vcs/routing.js +21 -6
  256. package/dist/providers/vcs/types.d.ts +71 -3
  257. package/dist/quickstart/index.d.ts +5 -0
  258. package/dist/quickstart/index.js +21 -0
  259. package/dist/quickstart-images.json +1 -1
  260. package/dist/sandbox/eval-core.d.ts +12 -2
  261. package/dist/sandbox/eval-core.js +20 -4
  262. package/dist/sandbox/host-functions.d.ts +4 -0
  263. package/dist/sandbox/host-functions.js +7 -2
  264. package/dist/sandbox/limits.d.ts +4 -1
  265. package/dist/sandbox/limits.js +4 -1
  266. package/dist/sandbox/run-in-sandbox.d.ts +6 -0
  267. package/dist/sandbox/run-in-sandbox.js +14 -2
  268. package/dist/sandbox/safe-fetch.d.ts +2 -0
  269. package/dist/sandbox/safe-fetch.js +1 -1
  270. package/dist/sandbox/secret-broker.d.ts +1 -0
  271. package/dist/sandbox/secret-broker.js +1 -1
  272. package/dist/sandbox/tool-timeout.d.ts +32 -0
  273. package/dist/sandbox/tool-timeout.js +52 -0
  274. package/dist/sandbox/user-tool.d.ts +8 -1
  275. package/dist/sandbox/user-tool.js +9 -3
  276. package/dist/serve.js +26 -18
  277. package/dist/viewer/api-schema.d.ts +66 -0
  278. package/dist/viewer/api-schema.js +31 -0
  279. package/dist/viewer/graph.js +2 -1
  280. package/dist/viewer/openapi.js +2 -1
  281. package/dist/viewer/run-detail.d.ts +2 -0
  282. package/dist/viewer/run-detail.js +26 -1
  283. package/dist/wardby-bin.js +5 -0
  284. package/docs/README.md +11 -1
  285. package/docs/agent-recipes.md +37 -4
  286. package/docs/architecture-runtime.md +1 -1
  287. package/docs/claude-code-plugin.md +208 -0
  288. package/docs/code-review-agents.md +6 -0
  289. package/docs/coding-agent-setup.md +94 -1
  290. package/docs/coding-worker-byo-images.md +4 -2
  291. package/docs/coding-worker-isolation.md +94 -7
  292. package/docs/debugging-agent-runs.md +354 -0
  293. package/docs/getting-started-gke.md +33 -11
  294. package/docs/getting-started-identity-provider.md +43 -37
  295. package/docs/getting-started.md +8 -0
  296. package/docs/host-providers.md +294 -0
  297. package/docs/mcp-servers.md +263 -0
  298. package/docs/native-sandbox.md +31 -9
  299. package/docs/security-deployment.md +161 -31
  300. package/docs/tool-secrets.md +3 -0
  301. package/docs/viewer-api.md +27 -12
  302. package/docs/viewer-api.openapi.yml +78 -2
  303. package/help/admin-viewer.md +5 -0
  304. package/help/agent-recipes.md +33 -2
  305. package/help/brokered-secrets.md +3 -0
  306. package/help/build-claude-code-image.md +1 -1
  307. package/help/claude-code-run-pane.md +63 -0
  308. package/help/deploy-gke.md +5 -0
  309. package/help/errors/coding-mcp-gateway-unavailable.md +46 -0
  310. package/help/errors/coding-mcp-pins-unavailable.md +35 -0
  311. package/help/errors/database-auth-invalid.md +1 -1
  312. package/help/errors/host-provider-error.md +40 -0
  313. package/help/errors/host-tool-failed.md +36 -0
  314. package/help/errors/mcp-call-limit.md +22 -0
  315. package/help/errors/mcp-call-timeout.md +22 -0
  316. package/help/errors/mcp-server-auth-failed.md +22 -0
  317. package/help/errors/mcp-server-credentials-unavailable.md +45 -0
  318. package/help/errors/mcp-server-error.md +28 -0
  319. package/help/errors/mcp-server-unreachable.md +22 -0
  320. package/help/errors/mcp-servers-off.md +21 -0
  321. package/help/errors/mcp-tool-not-allowed.md +26 -0
  322. package/help/errors/mcp-tool-removed.md +22 -0
  323. package/help/errors/mcp-unavailable.md +25 -0
  324. package/help/errors/native-sandbox-capacity.md +1 -1
  325. package/help/errors/native-sandbox-deadline-exceeded.md +1 -1
  326. package/help/errors/native-sandbox-docker-failed.md +1 -1
  327. package/help/errors/native-sandbox-gateway-error.md +1 -1
  328. package/help/errors/native-sandbox-gateway-unavailable.md +1 -1
  329. package/help/errors/native-sandbox-image-not-pinned.md +1 -1
  330. package/help/errors/native-sandbox-isolation-mismatch.md +1 -1
  331. package/help/errors/native-sandbox-network-unenforced.md +1 -1
  332. package/help/errors/native-sandbox-requires-catalog.md +1 -1
  333. package/help/errors/native-sandbox-unavailable.md +1 -1
  334. package/help/errors/native-sandbox-warm-delivery-failed.md +1 -1
  335. package/help/errors/native-sandbox-worker-exited.md +1 -1
  336. package/help/errors/native-sandbox-worker-lost.md +1 -1
  337. package/help/errors/native-sandbox-worker-unready.md +1 -1
  338. package/help/errors/repository-not-listed.md +36 -0
  339. package/help/errors/repository-required.md +36 -0
  340. package/help/errors/tool-timeout.md +38 -0
  341. package/help/host-providers.md +40 -0
  342. package/help/identity-and-access.md +4 -1
  343. package/help/low-power-mode.md +19 -8
  344. package/help/mcp-servers.md +73 -0
  345. package/help/mcp.md +9 -2
  346. package/help/native-capabilities.md +27 -1
  347. package/help/native-sandbox.md +1 -1
  348. package/help/operating-agents.md +16 -0
  349. package/help/related-pull-requests.md +7 -3
  350. package/help/repo-instructions-and-skills.md +3 -1
  351. package/help/resume-runs-locally.md +7 -0
  352. package/help/run-steps.md +140 -0
  353. package/help/stop-run.md +1 -1
  354. package/help/troubleshooting/coding-workers.md +9 -1
  355. package/help/troubleshooting/debugging-agent-runs.md +29 -0
  356. package/package.json +2 -1
  357. package/prisma/migrations/20261102000000_host_providers/migration.sql +56 -0
  358. package/prisma/migrations/20261104000000_coding_agent_repository_list/migration.sql +5 -0
  359. package/prisma/migrations/20261106000000_tool_timeout/migration.sql +4 -0
  360. package/prisma/migrations/20261107000000_mcp_servers/migration.sql +33 -0
  361. package/prisma/migrations/20261108000000_coding_mcp/migration.sql +5 -0
  362. package/prisma/migrations/20261109000000_task_run_index/migration.sql +3 -0
  363. package/prisma/migrations/20261110000000_run_steps/migration.sql +26 -0
  364. package/prisma/migrations/20261111000000_run_steps_viewer_notify/migration.sql +59 -0
  365. package/prisma/migrations/20261112000000_run_steps_detail/migration.sql +9 -0
  366. package/prisma/schema.prisma +211 -14
  367. package/scripts/git-askpass.sh +1 -1
@@ -0,0 +1,467 @@
1
+ /**
2
+ * Run steps from the model traffic the coding proxy relays: one extractor per wire format, one
3
+ * output shape (ExtractedStep / ExtractedResult). Nothing here throws for any input; a frame or body
4
+ * it can't read gives no step. A tool result's content is never kept below the detail level; at
5
+ * detail only its end (MAX_RESULT_OUTPUT_CHARS) is. Its status comes from its first lines. Each request resends the whole history, so results are read from the newest turn
6
+ * only.
7
+ */
8
+ import { bounded, CLAUDE_RESULT_TOOL, codingResultStepText, displayToolName, FINISHED_STEP_TEXT, toolStepText, } from "./summarize.js";
9
+ import { MAX_RESULT_OUTPUT_CHARS, MAX_STEP_TEXT_CHARS, tailOf } from "./types.js";
10
+ /** Anthropic stop reasons that cut a reply off mid-output. */
11
+ const CUT_STOP_REASONS = new Set(["max_tokens", "model_context_window_exceeded"]);
12
+ /** Marks the reply's last step truncated when it was cut and that step is text being written. */
13
+ function markCut(steps, cut) {
14
+ const last = steps.at(-1);
15
+ if (cut && last?.kind === "said" && !last.truncated)
16
+ steps[steps.length - 1] = { ...last, truncated: true };
17
+ return steps;
18
+ }
19
+ export const MAX_RESULTS_PER_REQUEST = 64;
20
+ const STATUS_HEAD_CHARS = 512;
21
+ const EXIT_LINE = /^(?:exit_code=|Exit code: |Process exited with code )(-?\d{1,6})\s*$/m;
22
+ const SCRIPT_FAILED = /^\s*Script (?:failed|terminated|error)\b/;
23
+ const OUTPUT_ITEMS = new Set(["function_call_output", "custom_tool_call_output"]);
24
+ /** Items of a turn's run of tool calls (their outputs are OUTPUT_ITEMS). */
25
+ const CALL_ITEMS = new Set(["function_call", "custom_tool_call", "tool_search_call"]);
26
+ const obj = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : undefined;
27
+ const str = (value) => (typeof value === "string" ? value : undefined);
28
+ function sseData(frame) {
29
+ const data = frame
30
+ .split("\n")
31
+ .filter((line) => line.startsWith("data:"))
32
+ .map((line) => line.slice(5).trimStart())
33
+ .join("\n");
34
+ if (!data || data === "[DONE]")
35
+ return undefined;
36
+ try {
37
+ return obj(JSON.parse(data));
38
+ }
39
+ catch {
40
+ return undefined;
41
+ }
42
+ }
43
+ function append(block, piece) {
44
+ if (block.truncated)
45
+ return;
46
+ const room = MAX_STEP_TEXT_CHARS - block.buffer.length;
47
+ if (piece.length > room) {
48
+ block.buffer += piece.slice(0, room);
49
+ block.truncated = true;
50
+ }
51
+ else
52
+ block.buffer += piece;
53
+ }
54
+ /** A coding run's final answer (Claude Code's StructuredOutput call) as one said step, never its arguments. */
55
+ function resultStep(callId, argsJson, cut) {
56
+ const answer = codingResultStepText(argsJson) ?? { text: FINISHED_STEP_TEXT, truncated: false };
57
+ return { kind: "said", callId, text: answer.text, truncated: answer.truncated || cut };
58
+ }
59
+ function closeBlock(block, options) {
60
+ if (block.kind === "said") {
61
+ return block.buffer.trim()
62
+ ? { kind: "said", callId: block.id, text: block.buffer, truncated: block.truncated }
63
+ : undefined;
64
+ }
65
+ if (block.name === CLAUDE_RESULT_TOOL)
66
+ return resultStep(block.id, block.buffer || "{}", block.truncated);
67
+ const tool = displayToolName(block.name);
68
+ const text = toolStepText(tool, block.buffer || "{}");
69
+ return {
70
+ kind: "tool",
71
+ callId: block.id,
72
+ tool,
73
+ text: text.text,
74
+ truncated: text.truncated || block.truncated,
75
+ ...(options?.detail ? { input: block.buffer || "{}", inputTruncated: block.truncated } : {}),
76
+ };
77
+ }
78
+ let streamCount = 0;
79
+ const MAX_OPEN_BLOCKS = 64;
80
+ /** Anthropic Messages streams: input_json_delta assembled per block index, emitted on content_block_stop. */
81
+ export class AnthropicStreamSteps {
82
+ options;
83
+ messageId = `msg${++streamCount}`;
84
+ blocks = new Map();
85
+ done = [];
86
+ stopped = false;
87
+ cutStop = false;
88
+ constructor(options) {
89
+ this.options = options;
90
+ }
91
+ consume(frame) {
92
+ try {
93
+ this.read(frame);
94
+ }
95
+ catch {
96
+ // A step is dropped, never the relay.
97
+ }
98
+ }
99
+ take() {
100
+ const out = this.done;
101
+ this.done = [];
102
+ return out;
103
+ }
104
+ finish() {
105
+ try {
106
+ const cut = !this.stopped || this.cutStop;
107
+ for (const [index, open] of [...this.blocks].sort(([a], [b]) => a - b)) {
108
+ this.blocks.delete(index);
109
+ if (open.kind !== "said")
110
+ continue;
111
+ const step = closeBlock({ ...open, truncated: true }, this.options);
112
+ if (step)
113
+ this.done.push(step);
114
+ }
115
+ return markCut(this.take(), cut);
116
+ }
117
+ catch {
118
+ return [];
119
+ }
120
+ }
121
+ read(frame) {
122
+ if (!frame.includes("message_") && !frame.includes("content_block_"))
123
+ return;
124
+ const event = sseData(frame);
125
+ if (!event)
126
+ return;
127
+ const index = typeof event.index === "number" ? event.index : undefined;
128
+ switch (event.type) {
129
+ case "message_stop":
130
+ this.stopped = true;
131
+ return;
132
+ case "message_delta": {
133
+ const reason = str(obj(event.delta)?.stop_reason);
134
+ if (reason && CUT_STOP_REASONS.has(reason))
135
+ this.cutStop = true;
136
+ return;
137
+ }
138
+ case "message_start":
139
+ this.messageId = str(obj(event.message)?.id) ?? this.messageId;
140
+ return;
141
+ case "content_block_start": {
142
+ const block = obj(event.content_block);
143
+ if (index === undefined || !block)
144
+ return;
145
+ if (!this.blocks.has(index) && this.blocks.size >= MAX_OPEN_BLOCKS)
146
+ return;
147
+ const id = str(block.id);
148
+ const name = str(block.name);
149
+ if (block.type === "tool_use" && id && name) {
150
+ this.blocks.set(index, { kind: "tool", id, name, buffer: "", truncated: false });
151
+ }
152
+ else if (block.type === "text") {
153
+ const open = {
154
+ kind: "said",
155
+ id: `said:${this.messageId}:${index}`,
156
+ name: "",
157
+ buffer: "",
158
+ truncated: false,
159
+ };
160
+ append(open, str(block.text) ?? "");
161
+ this.blocks.set(index, open);
162
+ }
163
+ return;
164
+ }
165
+ case "content_block_delta": {
166
+ const open = index === undefined ? undefined : this.blocks.get(index);
167
+ const delta = obj(event.delta);
168
+ if (!open || !delta)
169
+ return;
170
+ const piece = open.kind === "tool" ? str(delta.partial_json) : str(delta.text);
171
+ if (piece)
172
+ append(open, piece);
173
+ return;
174
+ }
175
+ case "content_block_stop": {
176
+ const open = index === undefined ? undefined : this.blocks.get(index);
177
+ if (!open || index === undefined)
178
+ return;
179
+ this.blocks.delete(index);
180
+ const step = closeBlock(open, this.options);
181
+ if (step)
182
+ this.done.push(step);
183
+ return;
184
+ }
185
+ default:
186
+ return;
187
+ }
188
+ }
189
+ }
190
+ /** One Responses output item as a step: function_call, custom_tool_call, or an assistant message. */
191
+ export function openAiItemStep(value, options) {
192
+ const item = obj(value);
193
+ if (!item)
194
+ return undefined;
195
+ const callId = str(item.call_id);
196
+ const name = str(item.name);
197
+ switch (item.type) {
198
+ case "function_call": {
199
+ if (!callId || !name)
200
+ return undefined;
201
+ const tool = displayToolName(name, str(item.namespace));
202
+ const args = str(item.arguments) ?? "{}";
203
+ return {
204
+ kind: "tool",
205
+ callId,
206
+ tool,
207
+ ...toolStepText(tool, args),
208
+ ...(options?.detail
209
+ ? { input: args.slice(0, MAX_STEP_TEXT_CHARS), inputTruncated: args.length > MAX_STEP_TEXT_CHARS }
210
+ : {}),
211
+ };
212
+ }
213
+ case "custom_tool_call": {
214
+ if (!callId || !name)
215
+ return undefined;
216
+ const tool = displayToolName(name);
217
+ const input = str(item.input) ?? "";
218
+ // The input is free-form text: as a JSON string it takes toolStepText's free-form path, which
219
+ // marks the step cut when later lines are dropped.
220
+ const text = toolStepText(tool, JSON.stringify(input.slice(0, MAX_STEP_TEXT_CHARS)));
221
+ return {
222
+ kind: "tool",
223
+ callId,
224
+ tool,
225
+ text: text.text,
226
+ truncated: text.truncated || input.length > MAX_STEP_TEXT_CHARS,
227
+ ...(options?.detail
228
+ ? { input: input.slice(0, MAX_STEP_TEXT_CHARS), inputTruncated: input.length > MAX_STEP_TEXT_CHARS }
229
+ : {}),
230
+ };
231
+ }
232
+ case "message": {
233
+ const id = str(item.id);
234
+ if (!id || (item.role !== undefined && item.role !== "assistant"))
235
+ return undefined;
236
+ const parts = Array.isArray(item.content) ? item.content : [];
237
+ const text = parts
238
+ .map((part) => obj(part))
239
+ .filter((part) => part?.type === "output_text")
240
+ .map((part) => str(part?.text) ?? "")
241
+ .join("\n");
242
+ if (!text.trim())
243
+ return undefined;
244
+ // Codex's last message is its final answer as JSON: said as the answer's summary.
245
+ const out = codingResultStepText(text) ?? bounded(text);
246
+ return {
247
+ kind: "said",
248
+ callId: `said:${id}`,
249
+ text: out.text,
250
+ truncated: out.truncated || item.status === "incomplete",
251
+ };
252
+ }
253
+ default:
254
+ return undefined;
255
+ }
256
+ }
257
+ /** OpenAI Responses streams: one step per response.output_item.done. */
258
+ export class OpenAiStreamSteps {
259
+ options;
260
+ done = [];
261
+ completed = false;
262
+ constructor(options) {
263
+ this.options = options;
264
+ }
265
+ consume(frame) {
266
+ try {
267
+ if (!frame.includes("response.output_item.done") && !frame.includes("response.completed"))
268
+ return;
269
+ const event = sseData(frame);
270
+ if (event?.type === "response.completed") {
271
+ if (obj(event.response)?.status !== "incomplete")
272
+ this.completed = true;
273
+ return;
274
+ }
275
+ if (event?.type !== "response.output_item.done")
276
+ return;
277
+ const step = openAiItemStep(event.item, this.options);
278
+ if (step)
279
+ this.done.push(step);
280
+ }
281
+ catch {
282
+ // A step is dropped, never the relay.
283
+ }
284
+ }
285
+ take() {
286
+ const out = this.done;
287
+ this.done = [];
288
+ return out;
289
+ }
290
+ finish() {
291
+ try {
292
+ // Anything but response.completed (incomplete, failed, an error or a cut stream) ended it early.
293
+ return markCut(this.take(), !this.completed);
294
+ }
295
+ catch {
296
+ return [];
297
+ }
298
+ }
299
+ }
300
+ export function streamStepExtractor(protocol, options) {
301
+ return protocol === "anthropic-messages" ? new AnthropicStreamSteps(options) : new OpenAiStreamSteps(options);
302
+ }
303
+ /** A whole (non-stream) reply's steps. */
304
+ export function stepsFromJson(protocol, value, options) {
305
+ try {
306
+ const body = obj(value);
307
+ if (!body)
308
+ return [];
309
+ if (protocol === "openai-responses") {
310
+ const steps = (Array.isArray(body.output) ? body.output : []).flatMap((item) => openAiItemStep(item, options) ?? []);
311
+ return markCut(steps, body.status === "incomplete" || obj(body.incomplete_details) !== undefined);
312
+ }
313
+ const messageId = str(body.id) ?? "msg";
314
+ const steps = [];
315
+ (Array.isArray(body.content) ? body.content : []).forEach((value, index) => {
316
+ const block = obj(value);
317
+ const id = str(block?.id);
318
+ const name = str(block?.name);
319
+ const text = str(block?.text);
320
+ if (block?.type === "tool_use" && id && name === CLAUDE_RESULT_TOOL) {
321
+ steps.push(resultStep(id, JSON.stringify(block.input ?? {}), false));
322
+ }
323
+ else if (block?.type === "tool_use" && id && name) {
324
+ const tool = displayToolName(name);
325
+ const inputJson = JSON.stringify(block.input ?? {});
326
+ steps.push({
327
+ kind: "tool",
328
+ callId: id,
329
+ tool,
330
+ ...toolStepText(tool, inputJson),
331
+ ...(options?.detail
332
+ ? { input: inputJson.slice(0, MAX_STEP_TEXT_CHARS), inputTruncated: inputJson.length > MAX_STEP_TEXT_CHARS }
333
+ : {}),
334
+ });
335
+ }
336
+ else if (block?.type === "text" && text?.trim()) {
337
+ steps.push({ kind: "said", callId: `said:${messageId}:${index}`, ...bounded(text) });
338
+ }
339
+ });
340
+ const reason = str(body.stop_reason);
341
+ return markCut(steps, reason !== undefined && CUT_STOP_REASONS.has(reason));
342
+ }
343
+ catch {
344
+ return [];
345
+ }
346
+ }
347
+ /** A result's status from its first lines: an exit line wins, else the error flag. */
348
+ export function statusFromOutput(head, isError) {
349
+ const lines = head.slice(0, STATUS_HEAD_CHARS).split("\n").slice(0, 8).join("\n");
350
+ const match = EXIT_LINE.exec(lines);
351
+ if (!match)
352
+ return isError || SCRIPT_FAILED.test(lines) ? "error" : "ok";
353
+ const code = Number(match[1]);
354
+ if (code === 0)
355
+ return "ok";
356
+ return code > 0 && code < 1000 ? `exit ${code}` : "error";
357
+ }
358
+ /** The first STATUS_HEAD_CHARS of a result's text parts; images and other parts are skipped. */
359
+ function contentHead(content) {
360
+ if (typeof content === "string")
361
+ return content.slice(0, STATUS_HEAD_CHARS);
362
+ if (!Array.isArray(content))
363
+ return "";
364
+ let head = "";
365
+ for (const part of content) {
366
+ const text = str(obj(part)?.text);
367
+ if (text === undefined)
368
+ continue;
369
+ head += head ? `\n${text}` : text;
370
+ if (head.length >= STATUS_HEAD_CHARS)
371
+ break;
372
+ }
373
+ return head.slice(0, STATUS_HEAD_CHARS);
374
+ }
375
+ /** The end of a result's text parts (images and other parts skipped), for the detail level. */
376
+ function contentTail(content) {
377
+ if (typeof content === "string") {
378
+ const tail = tailOf(content);
379
+ return { output: tail.text, outputCut: tail.cut };
380
+ }
381
+ if (!Array.isArray(content))
382
+ return undefined;
383
+ const parts = [];
384
+ let size = 0;
385
+ let cut = false;
386
+ for (let index = content.length - 1; index >= 0; index--) {
387
+ const text = str(obj(content[index])?.text);
388
+ if (text === undefined)
389
+ continue;
390
+ if (size >= MAX_RESULT_OUTPUT_CHARS) {
391
+ cut = true;
392
+ break;
393
+ }
394
+ parts.unshift(text);
395
+ size += text.length + 1;
396
+ }
397
+ if (parts.length === 0)
398
+ return undefined;
399
+ const tail = tailOf(parts.join("\n"));
400
+ return { output: tail.text, outputCut: cut || tail.cut };
401
+ }
402
+ /** The results a request carries for the turn before it: the newest part of the history only. */
403
+ export function resultsFromRequest(protocol, value, options) {
404
+ try {
405
+ const body = obj(value);
406
+ if (!body)
407
+ return [];
408
+ if (protocol === "anthropic-messages") {
409
+ const messages = Array.isArray(body.messages) ? body.messages : [];
410
+ const results = [];
411
+ // Every user message after the last assistant message: results may be split from a following text.
412
+ for (let index = messages.length - 1; index >= 0; index--) {
413
+ const message = obj(messages[index]);
414
+ if (message?.role === "system")
415
+ continue;
416
+ if (message?.role !== "user")
417
+ break;
418
+ if (!Array.isArray(message.content))
419
+ continue;
420
+ for (let at = message.content.length - 1; at >= 0; at--) {
421
+ const block = obj(message.content[at]);
422
+ const callId = str(block?.tool_use_id);
423
+ if (block?.type !== "tool_result" || !callId)
424
+ continue;
425
+ results.push({
426
+ callId,
427
+ status: statusFromOutput(contentHead(block.content), block.is_error === true),
428
+ ...(options?.detail ? (contentTail(block.content) ?? {}) : {}),
429
+ });
430
+ }
431
+ }
432
+ return results.slice(0, MAX_RESULTS_PER_REQUEST).reverse();
433
+ }
434
+ const input = Array.isArray(body.input) ? body.input : [];
435
+ // The newest turn: past trailing user/developer messages, the run of calls and their outputs
436
+ // (they interleave), back to a reasoning item, an assistant message or anything else.
437
+ let end = input.length;
438
+ while (end > 0) {
439
+ const item = obj(input[end - 1]);
440
+ if (item?.type === "message" && (item.role === "user" || item.role === "developer"))
441
+ end--;
442
+ else
443
+ break;
444
+ }
445
+ const results = [];
446
+ for (let index = end - 1; index >= 0 && results.length < MAX_RESULTS_PER_REQUEST; index--) {
447
+ const item = obj(input[index]);
448
+ const type = str(item?.type);
449
+ if (type && OUTPUT_ITEMS.has(type)) {
450
+ const callId = str(item?.call_id);
451
+ if (callId) {
452
+ results.push({
453
+ callId,
454
+ status: statusFromOutput(contentHead(item?.output), false),
455
+ ...(options?.detail ? (contentTail(item?.output) ?? {}) : {}),
456
+ });
457
+ }
458
+ }
459
+ else if (!type || !CALL_ITEMS.has(type))
460
+ break;
461
+ }
462
+ return results.reverse();
463
+ }
464
+ catch {
465
+ return [];
466
+ }
467
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * The control plane's run-steps listener (WARDBY_RUN_STEPS_LISTEN, `wardby serve` only): the coding
3
+ * proxy posts each coding run's steps here with the step text unmasked, and this process redacts
4
+ * them by exact value (RunStepWriter, the same stepSummarizer path as native runs) before anything
5
+ * is stored. A listener of its own, so the MCP endpoint's canonical-Host check and its public
6
+ * exposure are untouched.
7
+ *
8
+ * Each request is authenticated by the run capability (`rrp_…`), found by its hash in the run's
9
+ * proxy session, from the headers alone: the body is read only for a live session. The run's worker
10
+ * and the proxy both hold this capability (it is also the worker's model API key), so it does not
11
+ * tell the proxy from the worker: it limits a caller to its own run (a batch naming any other run
12
+ * is refused, run_mismatch), and only the deployment's network policy keeps workers off this port.
13
+ * The capability is handed to the writer as one more exact value to redact,
14
+ * so a copy of it the model wrote, glued to other text or cut off at the end of a step, is masked
15
+ * like the agent's own secrets. The run's MCP token (`rmt_…`) can't be passed the same way: only
16
+ * its hash is stored, and the value leaves the control plane's memory once the job is launched; it
17
+ * is masked by its token shape instead.
18
+ *
19
+ * Secrets: the request body, the step text and the Authorization value are never logged, echoed or
20
+ * stored by this module. Log lines carry a fixed event, a status and an error's name only (never an
21
+ * error's message: a database or writer error may quote the lookup or the text); every response
22
+ * body is a fixed code.
23
+ */
24
+ import { type Server } from "node:http";
25
+ import type { PrismaClient } from "#prisma";
26
+ import { type ListenAddress } from "../listen-address.js";
27
+ import { type RunStepSink } from "./types.js";
28
+ /** Open connections at once; Node accepts and then closes any beyond this (they do not wait in the listen backlog). */
29
+ export declare const MAX_INGRESS_CONNECTIONS = 64;
30
+ export interface RunStepIngressDeps {
31
+ db: Pick<PrismaClient, "codingProxySession">;
32
+ sink: RunStepSink;
33
+ now?: () => Date;
34
+ log?: {
35
+ warn(fields: Record<string, unknown>, message: string): void;
36
+ };
37
+ /** Tests only; HTTP_LIMITS.requestMs (15 s, under the forwarder's 20 s) otherwise. */
38
+ requestTimeoutMs?: number;
39
+ }
40
+ export interface RunStepIngressResponse {
41
+ status: number;
42
+ body: {
43
+ ok: true;
44
+ } | {
45
+ error: string;
46
+ };
47
+ }
48
+ export declare function handleRunStepPost(deps: RunStepIngressDeps, authorization: string | undefined, body: string): Promise<RunStepIngressResponse>;
49
+ /** WARDBY_RUN_STEPS_LISTEN: `host:port` or `:port`; unset or blank = no listener. */
50
+ export declare function parseRunStepsListen(value: string | undefined): ListenAddress | undefined;
51
+ export declare function startRunStepIngress(listen: ListenAddress, deps: RunStepIngressDeps): Promise<Server>;
@@ -0,0 +1,160 @@
1
+ /**
2
+ * The control plane's run-steps listener (WARDBY_RUN_STEPS_LISTEN, `wardby serve` only): the coding
3
+ * proxy posts each coding run's steps here with the step text unmasked, and this process redacts
4
+ * them by exact value (RunStepWriter, the same stepSummarizer path as native runs) before anything
5
+ * is stored. A listener of its own, so the MCP endpoint's canonical-Host check and its public
6
+ * exposure are untouched.
7
+ *
8
+ * Each request is authenticated by the run capability (`rrp_…`), found by its hash in the run's
9
+ * proxy session, from the headers alone: the body is read only for a live session. The run's worker
10
+ * and the proxy both hold this capability (it is also the worker's model API key), so it does not
11
+ * tell the proxy from the worker: it limits a caller to its own run (a batch naming any other run
12
+ * is refused, run_mismatch), and only the deployment's network policy keeps workers off this port.
13
+ * The capability is handed to the writer as one more exact value to redact,
14
+ * so a copy of it the model wrote, glued to other text or cut off at the end of a step, is masked
15
+ * like the agent's own secrets. The run's MCP token (`rmt_…`) can't be passed the same way: only
16
+ * its hash is stored, and the value leaves the control plane's memory once the job is launched; it
17
+ * is masked by its token shape instead.
18
+ *
19
+ * Secrets: the request body, the step text and the Authorization value are never logged, echoed or
20
+ * stored by this module. Log lines carry a fixed event, a status and an error's name only (never an
21
+ * error's message: a database or writer error may quote the lookup or the text); every response
22
+ * body is a fixed code.
23
+ */
24
+ import { createServer } from "node:http";
25
+ import { deriveRegistryToken } from "../../coding/registry/token.js";
26
+ import { capabilityHash } from "../../providers/coding-proxy/capability-hash.js";
27
+ import { HTTP_LIMITS, HttpBoundaryError, readBody } from "../../mcp/transport/http-limits.js";
28
+ import { parseListenAddress } from "../listen-address.js";
29
+ import { logger } from "../logger.js";
30
+ import { MAX_RUN_STEPS_POST_BYTES, RUN_STEPS_INGRESS_PATH } from "./types.js";
31
+ import { parseRunStepPost } from "./wire.js";
32
+ const ingressLog = logger.child({ module: "run-steps" });
33
+ /** `rrp_` + 32 random bytes, base64url (CodingProxy.createSession). */
34
+ const CAPABILITY = /^rrp_[A-Za-z0-9_-]{43}$/;
35
+ /** A reply that finished just before the session's deadline may still be in the forwarder's queue. */
36
+ const SESSION_GRACE_MS = 10 * 60_000;
37
+ /** Open connections at once; Node accepts and then closes any beyond this (they do not wait in the listen backlog). */
38
+ export const MAX_INGRESS_CONNECTIONS = 64;
39
+ /** How often Node checks requestTimeout and headersTimeout (its default, 30 s, would let a slow client stay). */
40
+ const CONNECTIONS_CHECK_MS = 2_000;
41
+ /** Fixed codes for the HTTP boundary's refusals; any other status is "bad_request". */
42
+ const BOUNDARY_CODES = { 413: "payload_too_large", 415: "unsupported_encoding" };
43
+ class RequestTimeoutError extends Error {
44
+ name = "TimeoutError";
45
+ }
46
+ /** From the Authorization header alone: the caller's live session, or the refusal. */
47
+ async function authenticate(deps, authorization) {
48
+ const token = /^Bearer (\S+)$/.exec(authorization ?? "")?.[1];
49
+ if (!token || !CAPABILITY.test(token))
50
+ return { status: 401, body: { error: "unauthorized" } };
51
+ const session = await deps.db.codingProxySession.findUnique({
52
+ where: { capabilityHash: capabilityHash(token) },
53
+ select: { runId: true, deadlineAt: true },
54
+ });
55
+ if (!session)
56
+ return { status: 401, body: { error: "unauthorized" } };
57
+ // Only the deadline counts (a cancelled session's last steps still land): bounded by the grace.
58
+ const now = (deps.now ?? (() => new Date()))();
59
+ if (session.deadlineAt.getTime() + SESSION_GRACE_MS < now.getTime()) {
60
+ return { status: 403, body: { error: "session_ended" } };
61
+ }
62
+ return { runId: session.runId, capability: token };
63
+ }
64
+ /** An authenticated caller's body: parsed, bound to its run, then written. */
65
+ async function accept(deps, caller, body) {
66
+ let value;
67
+ try {
68
+ value = JSON.parse(body);
69
+ }
70
+ catch {
71
+ return { status: 400, body: { error: "invalid_json" } };
72
+ }
73
+ const post = parseRunStepPost(value);
74
+ if (!post)
75
+ return { status: 400, body: { error: "invalid_batch" } };
76
+ // A capability names one run: a batch for any other run is refused, never written there.
77
+ if (post.runId !== caller.runId)
78
+ return { status: 403, body: { error: "run_mismatch" } };
79
+ await deps.sink.record(caller.runId, post.batch, [caller.capability, deriveRegistryToken(caller.capability)]);
80
+ return { status: 202, body: { ok: true } };
81
+ }
82
+ export async function handleRunStepPost(deps, authorization, body) {
83
+ const caller = await authenticate(deps, authorization);
84
+ return "status" in caller ? caller : accept(deps, caller, body);
85
+ }
86
+ /** WARDBY_RUN_STEPS_LISTEN: `host:port` or `:port`; unset or blank = no listener. */
87
+ export function parseRunStepsListen(value) {
88
+ return parseListenAddress("WARDBY_RUN_STEPS_LISTEN", value);
89
+ }
90
+ /**
91
+ * Writes a fixed answer. A refusal also closes the connection once the answer is out: its body
92
+ * may be unread, and is never drained.
93
+ */
94
+ function send(res, status, body) {
95
+ const refusal = status >= 300;
96
+ res.writeHead(status, {
97
+ "content-type": "application/json",
98
+ "cache-control": "no-store",
99
+ ...(refusal ? { connection: "close" } : {}),
100
+ });
101
+ if (refusal)
102
+ res.once("finish", () => res.socket?.destroy());
103
+ res.end(JSON.stringify(body));
104
+ }
105
+ const errorName = (err) => (err instanceof Error ? err.name : typeof err);
106
+ export function startRunStepIngress(listen, deps) {
107
+ const log = deps.log ?? ingressLog;
108
+ const limitMs = deps.requestTimeoutMs ?? HTTP_LIMITS.requestMs;
109
+ const serve = async (req, res) => {
110
+ if (req.method !== "POST" || req.url !== RUN_STEPS_INGRESS_PATH)
111
+ return send(res, 404, { error: "not_found" });
112
+ // One limit for the whole request (lookup, body, write), so the forwarder always gets an answer.
113
+ const controller = new AbortController();
114
+ const timer = setTimeout(() => controller.abort(), limitMs);
115
+ const timedOut = new Promise((_, reject) => controller.signal.addEventListener("abort", () => reject(new RequestTimeoutError()), { once: true }));
116
+ const work = (async () => {
117
+ const caller = await authenticate(deps, req.headers.authorization);
118
+ if ("status" in caller)
119
+ return caller;
120
+ const body = await readBody(req, MAX_RUN_STEPS_POST_BYTES, controller.signal);
121
+ return accept(deps, caller, body);
122
+ })();
123
+ // After a timeout answer, a failure that lands later is logged by name only (the body read's
124
+ // own abort is not a failure).
125
+ work.catch((err) => {
126
+ if (controller.signal.aborted && !(err instanceof HttpBoundaryError)) {
127
+ log.warn({ event: "run_steps_ingress_late_failure", errorName: errorName(err) }, "run steps request failed late");
128
+ }
129
+ });
130
+ try {
131
+ const result = await Promise.race([work, timedOut]);
132
+ send(res, result.status, result.body);
133
+ }
134
+ catch (err) {
135
+ const timeout = err instanceof RequestTimeoutError;
136
+ const status = timeout ? 503 : err instanceof HttpBoundaryError ? err.status : 500;
137
+ const code = timeout ? "timeout" : status === 500 ? "internal" : (BOUNDARY_CODES[status] ?? "bad_request");
138
+ log.warn({ event: "run_steps_ingress_failed", status, errorName: errorName(err) }, "run steps request failed");
139
+ if (!res.headersSent)
140
+ send(res, status, { error: code });
141
+ }
142
+ finally {
143
+ clearTimeout(timer);
144
+ }
145
+ };
146
+ const server = createServer({
147
+ maxHeaderSize: 16 * 1024,
148
+ requestTimeout: limitMs,
149
+ headersTimeout: Math.min(HTTP_LIMITS.headersMs, limitMs),
150
+ connectionsCheckingInterval: Math.min(CONNECTIONS_CHECK_MS, limitMs),
151
+ }, (req, res) => void serve(req, res));
152
+ server.maxConnections = MAX_INGRESS_CONNECTIONS;
153
+ return new Promise((resolve, reject) => {
154
+ server.once("error", reject);
155
+ server.listen(listen.port, listen.host, () => {
156
+ server.off("error", reject);
157
+ resolve(server);
158
+ });
159
+ });
160
+ }