@wardby/cli 0.1.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 (412) hide show
  1. package/.env.example +131 -0
  2. package/LICENSE +202 -0
  3. package/NOTICE +2 -0
  4. package/README.md +282 -0
  5. package/bin/wardby.js +3 -0
  6. package/deploy/local/docker-compose.yml +19 -0
  7. package/dist/claude-coding-worker/driver.d.ts +70 -0
  8. package/dist/claude-coding-worker/driver.js +128 -0
  9. package/dist/claude-coding-worker/main.d.ts +2 -0
  10. package/dist/claude-coding-worker/main.js +41 -0
  11. package/dist/claude-coding-worker/sdk.d.ts +4 -0
  12. package/dist/claude-coding-worker/sdk.js +43 -0
  13. package/dist/claude-coding-worker/tool-relay.d.ts +1 -0
  14. package/dist/claude-coding-worker/tool-relay.js +9 -0
  15. package/dist/cli-help.d.ts +1 -0
  16. package/dist/cli-help.js +20 -0
  17. package/dist/cli.d.ts +18 -0
  18. package/dist/cli.js +629 -0
  19. package/dist/coding/observability.d.ts +59 -0
  20. package/dist/coding/observability.js +77 -0
  21. package/dist/coding/profile.d.ts +88 -0
  22. package/dist/coding/profile.js +137 -0
  23. package/dist/coding/protocol.d.ts +345 -0
  24. package/dist/coding/protocol.js +445 -0
  25. package/dist/coding/provider.d.ts +4 -0
  26. package/dist/coding/provider.js +18 -0
  27. package/dist/coding-proxy/main.d.ts +1 -0
  28. package/dist/coding-proxy/main.js +51 -0
  29. package/dist/coding-worker/artifact.d.ts +16 -0
  30. package/dist/coding-worker/artifact.js +37 -0
  31. package/dist/coding-worker/driver.d.ts +46 -0
  32. package/dist/coding-worker/driver.js +158 -0
  33. package/dist/coding-worker/errors.d.ts +1 -0
  34. package/dist/coding-worker/errors.js +23 -0
  35. package/dist/coding-worker/isolation-probe.d.ts +1 -0
  36. package/dist/coding-worker/isolation-probe.js +146 -0
  37. package/dist/coding-worker/keeper.d.ts +4 -0
  38. package/dist/coding-worker/keeper.js +29 -0
  39. package/dist/coding-worker/main.d.ts +2 -0
  40. package/dist/coding-worker/main.js +43 -0
  41. package/dist/coding-worker/sdk.d.ts +26 -0
  42. package/dist/coding-worker/sdk.js +28 -0
  43. package/dist/coding-worker/types.d.ts +64 -0
  44. package/dist/coding-worker/types.js +1 -0
  45. package/dist/config/providers.d.ts +158 -0
  46. package/dist/config/providers.js +162 -0
  47. package/dist/core/budget-groups.d.ts +72 -0
  48. package/dist/core/budget-groups.js +149 -0
  49. package/dist/core/budget.d.ts +63 -0
  50. package/dist/core/budget.js +69 -0
  51. package/dist/core/coding-queue.d.ts +32 -0
  52. package/dist/core/coding-queue.js +54 -0
  53. package/dist/core/cron.d.ts +31 -0
  54. package/dist/core/cron.js +43 -0
  55. package/dist/core/datastores.d.ts +41 -0
  56. package/dist/core/datastores.js +105 -0
  57. package/dist/core/db.d.ts +2 -0
  58. package/dist/core/db.js +2 -0
  59. package/dist/core/dispatch.d.ts +81 -0
  60. package/dist/core/dispatch.js +184 -0
  61. package/dist/core/engine-native.d.ts +29 -0
  62. package/dist/core/engine-native.js +259 -0
  63. package/dist/core/http-runtime.d.ts +7 -0
  64. package/dist/core/http-runtime.js +101 -0
  65. package/dist/core/lease.d.ts +13 -0
  66. package/dist/core/lease.js +22 -0
  67. package/dist/core/logger.d.ts +13 -0
  68. package/dist/core/logger.js +45 -0
  69. package/dist/core/memory-tools.d.ts +11 -0
  70. package/dist/core/memory-tools.js +122 -0
  71. package/dist/core/reconciler.d.ts +45 -0
  72. package/dist/core/reconciler.js +152 -0
  73. package/dist/core/runner.d.ts +87 -0
  74. package/dist/core/runner.js +529 -0
  75. package/dist/core/scheduler.d.ts +47 -0
  76. package/dist/core/scheduler.js +121 -0
  77. package/dist/core/secrets.d.ts +46 -0
  78. package/dist/core/secrets.js +100 -0
  79. package/dist/core/subagent-memory-tools.d.ts +18 -0
  80. package/dist/core/subagent-memory-tools.js +121 -0
  81. package/dist/core/timing.d.ts +7 -0
  82. package/dist/core/timing.js +7 -0
  83. package/dist/core/webhooks.d.ts +23 -0
  84. package/dist/core/webhooks.js +113 -0
  85. package/dist/env.d.ts +11 -0
  86. package/dist/env.js +13 -0
  87. package/dist/import/budgets.d.ts +16 -0
  88. package/dist/import/budgets.js +21 -0
  89. package/dist/import/bundle.d.ts +16 -0
  90. package/dist/import/bundle.js +61 -0
  91. package/dist/import/cli-args.d.ts +2 -0
  92. package/dist/import/cli-args.js +44 -0
  93. package/dist/import/create.d.ts +34 -0
  94. package/dist/import/create.js +363 -0
  95. package/dist/import/index.d.ts +25 -0
  96. package/dist/import/index.js +130 -0
  97. package/dist/import/model-gate.d.ts +1 -0
  98. package/dist/import/model-gate.js +3 -0
  99. package/dist/import/neutral-schema.d.ts +369 -0
  100. package/dist/import/neutral-schema.js +109 -0
  101. package/dist/import/preflight.d.ts +41 -0
  102. package/dist/import/preflight.js +71 -0
  103. package/dist/import/report.d.ts +5 -0
  104. package/dist/import/report.js +98 -0
  105. package/dist/import/tool-scan.d.ts +8 -0
  106. package/dist/import/tool-scan.js +20 -0
  107. package/dist/mcp/auth/ownership.d.ts +113 -0
  108. package/dist/mcp/auth/ownership.js +87 -0
  109. package/dist/mcp/auth/principal.d.ts +9 -0
  110. package/dist/mcp/auth/principal.js +9 -0
  111. package/dist/mcp/auth/resource-server.d.ts +47 -0
  112. package/dist/mcp/auth/resource-server.js +82 -0
  113. package/dist/mcp/auth/self-hosted/browser.d.ts +3 -0
  114. package/dist/mcp/auth/self-hosted/browser.js +273 -0
  115. package/dist/mcp/auth/self-hosted/cli.d.ts +2 -0
  116. package/dist/mcp/auth/self-hosted/cli.js +39 -0
  117. package/dist/mcp/auth/self-hosted/credentials.d.ts +58 -0
  118. package/dist/mcp/auth/self-hosted/credentials.js +106 -0
  119. package/dist/mcp/auth/self-hosted/rate-limit.d.ts +13 -0
  120. package/dist/mcp/auth/self-hosted/rate-limit.js +20 -0
  121. package/dist/mcp/auth/self-hosted/session.d.ts +36 -0
  122. package/dist/mcp/auth/self-hosted/session.js +110 -0
  123. package/dist/mcp/capabilities.d.ts +11 -0
  124. package/dist/mcp/capabilities.js +10 -0
  125. package/dist/mcp/context.d.ts +46 -0
  126. package/dist/mcp/context.js +1 -0
  127. package/dist/mcp/errors.d.ts +13 -0
  128. package/dist/mcp/errors.js +22 -0
  129. package/dist/mcp/index.d.ts +32 -0
  130. package/dist/mcp/index.js +187 -0
  131. package/dist/mcp/jsonrpc.d.ts +8 -0
  132. package/dist/mcp/jsonrpc.js +1 -0
  133. package/dist/mcp/server.d.ts +107 -0
  134. package/dist/mcp/server.js +203 -0
  135. package/dist/mcp/tasks/manager.d.ts +63 -0
  136. package/dist/mcp/tasks/manager.js +103 -0
  137. package/dist/mcp/tools/agents.d.ts +2 -0
  138. package/dist/mcp/tools/agents.js +375 -0
  139. package/dist/mcp/tools/budget-groups.d.ts +2 -0
  140. package/dist/mcp/tools/budget-groups.js +136 -0
  141. package/dist/mcp/tools/datastore.d.ts +2 -0
  142. package/dist/mcp/tools/datastore.js +217 -0
  143. package/dist/mcp/tools/memory.d.ts +3 -0
  144. package/dist/mcp/tools/memory.js +60 -0
  145. package/dist/mcp/tools/models.d.ts +8 -0
  146. package/dist/mcp/tools/models.js +15 -0
  147. package/dist/mcp/tools/runs.d.ts +2 -0
  148. package/dist/mcp/tools/runs.js +61 -0
  149. package/dist/mcp/tools/scheduling.d.ts +2 -0
  150. package/dist/mcp/tools/scheduling.js +48 -0
  151. package/dist/mcp/tools/secret-elicitation-form.d.ts +22 -0
  152. package/dist/mcp/tools/secret-elicitation-form.js +93 -0
  153. package/dist/mcp/tools/secret-elicitation-server.d.ts +5 -0
  154. package/dist/mcp/tools/secret-elicitation-server.js +57 -0
  155. package/dist/mcp/tools/secret-elicitation.d.ts +38 -0
  156. package/dist/mcp/tools/secret-elicitation.js +52 -0
  157. package/dist/mcp/tools/secrets.d.ts +13 -0
  158. package/dist/mcp/tools/secrets.js +124 -0
  159. package/dist/mcp/tools/subagents.d.ts +2 -0
  160. package/dist/mcp/tools/subagents.js +133 -0
  161. package/dist/mcp/tools/text-result.d.ts +6 -0
  162. package/dist/mcp/tools/text-result.js +3 -0
  163. package/dist/mcp/tools/tools.d.ts +2 -0
  164. package/dist/mcp/tools/tools.js +211 -0
  165. package/dist/mcp/tools/trigger.d.ts +2 -0
  166. package/dist/mcp/tools/trigger.js +120 -0
  167. package/dist/mcp/tools/webhooks.d.ts +2 -0
  168. package/dist/mcp/tools/webhooks.js +34 -0
  169. package/dist/mcp/transport/http-limits.d.ts +17 -0
  170. package/dist/mcp/transport/http-limits.js +92 -0
  171. package/dist/mcp/transport/stdio.d.ts +23 -0
  172. package/dist/mcp/transport/stdio.js +6 -0
  173. package/dist/mcp/transport/streamable-http.d.ts +34 -0
  174. package/dist/mcp/transport/streamable-http.js +158 -0
  175. package/dist/mcp/unattended-schedules.d.ts +15 -0
  176. package/dist/mcp/unattended-schedules.js +21 -0
  177. package/dist/mcp/webhooks/ingress.d.ts +17 -0
  178. package/dist/mcp/webhooks/ingress.js +33 -0
  179. package/dist/observability/config.d.ts +6 -0
  180. package/dist/observability/config.js +20 -0
  181. package/dist/observability/metrics-server.d.ts +11 -0
  182. package/dist/observability/metrics-server.js +47 -0
  183. package/dist/observability/metrics.d.ts +36 -0
  184. package/dist/observability/metrics.js +158 -0
  185. package/dist/providers/auth/authorization-server.d.ts +41 -0
  186. package/dist/providers/auth/authorization-server.js +1 -0
  187. package/dist/providers/auth/delegating.d.ts +44 -0
  188. package/dist/providers/auth/delegating.js +105 -0
  189. package/dist/providers/auth/index.d.ts +11 -0
  190. package/dist/providers/auth/index.js +20 -0
  191. package/dist/providers/auth/release-gate.d.ts +1 -0
  192. package/dist/providers/auth/release-gate.js +4 -0
  193. package/dist/providers/auth/self-hosted.d.ts +78 -0
  194. package/dist/providers/auth/self-hosted.js +377 -0
  195. package/dist/providers/auth/subject.d.ts +1 -0
  196. package/dist/providers/auth/subject.js +6 -0
  197. package/dist/providers/auth/types.d.ts +63 -0
  198. package/dist/providers/auth/types.js +22 -0
  199. package/dist/providers/coding-proxy/deny-port.d.ts +5 -0
  200. package/dist/providers/coding-proxy/deny-port.js +45 -0
  201. package/dist/providers/coding-proxy/environment-credentials.d.ts +7 -0
  202. package/dist/providers/coding-proxy/environment-credentials.js +16 -0
  203. package/dist/providers/coding-proxy/index.d.ts +7 -0
  204. package/dist/providers/coding-proxy/index.js +7 -0
  205. package/dist/providers/coding-proxy/memory-ledger.d.ts +17 -0
  206. package/dist/providers/coding-proxy/memory-ledger.js +103 -0
  207. package/dist/providers/coding-proxy/metering.d.ts +20 -0
  208. package/dist/providers/coding-proxy/metering.js +161 -0
  209. package/dist/providers/coding-proxy/prisma-ledger.d.ts +15 -0
  210. package/dist/providers/coding-proxy/prisma-ledger.js +213 -0
  211. package/dist/providers/coding-proxy/proxy.d.ts +75 -0
  212. package/dist/providers/coding-proxy/proxy.js +721 -0
  213. package/dist/providers/coding-proxy/runtime.d.ts +18 -0
  214. package/dist/providers/coding-proxy/runtime.js +39 -0
  215. package/dist/providers/coding-proxy/secure-fetch.d.ts +9 -0
  216. package/dist/providers/coding-proxy/secure-fetch.js +72 -0
  217. package/dist/providers/coding-proxy/server.d.ts +17 -0
  218. package/dist/providers/coding-proxy/server.js +139 -0
  219. package/dist/providers/coding-proxy/types.d.ts +99 -0
  220. package/dist/providers/coding-proxy/types.js +1 -0
  221. package/dist/providers/datastore/index.d.ts +2 -0
  222. package/dist/providers/datastore/index.js +2 -0
  223. package/dist/providers/datastore/postgres.d.ts +20 -0
  224. package/dist/providers/datastore/postgres.js +130 -0
  225. package/dist/providers/datastore/scoped.d.ts +10 -0
  226. package/dist/providers/datastore/scoped.js +46 -0
  227. package/dist/providers/datastore/types.d.ts +31 -0
  228. package/dist/providers/datastore/types.js +7 -0
  229. package/dist/providers/email/types.d.ts +46 -0
  230. package/dist/providers/email/types.js +8 -0
  231. package/dist/providers/engine/index.d.ts +1 -0
  232. package/dist/providers/engine/index.js +1 -0
  233. package/dist/providers/engine/types.d.ts +67 -0
  234. package/dist/providers/engine/types.js +16 -0
  235. package/dist/providers/executor/build.d.ts +6 -0
  236. package/dist/providers/executor/build.js +15 -0
  237. package/dist/providers/executor/composition.d.ts +14 -0
  238. package/dist/providers/executor/composition.js +123 -0
  239. package/dist/providers/executor/container.d.ts +184 -0
  240. package/dist/providers/executor/container.js +936 -0
  241. package/dist/providers/executor/dbos-status.d.ts +50 -0
  242. package/dist/providers/executor/dbos-status.js +64 -0
  243. package/dist/providers/executor/dbos.d.ts +53 -0
  244. package/dist/providers/executor/dbos.js +269 -0
  245. package/dist/providers/executor/in-process.d.ts +18 -0
  246. package/dist/providers/executor/in-process.js +38 -0
  247. package/dist/providers/executor/index.d.ts +8 -0
  248. package/dist/providers/executor/index.js +8 -0
  249. package/dist/providers/executor/routing.d.ts +48 -0
  250. package/dist/providers/executor/routing.js +70 -0
  251. package/dist/providers/executor/types.d.ts +55 -0
  252. package/dist/providers/executor/types.js +1 -0
  253. package/dist/providers/index.d.ts +43 -0
  254. package/dist/providers/index.js +18 -0
  255. package/dist/providers/jobs/docker-isolation.d.ts +166 -0
  256. package/dist/providers/jobs/docker-isolation.js +724 -0
  257. package/dist/providers/jobs/docker.d.ts +127 -0
  258. package/dist/providers/jobs/docker.js +1027 -0
  259. package/dist/providers/jobs/fake-kubernetes-api.d.ts +98 -0
  260. package/dist/providers/jobs/fake-kubernetes-api.js +142 -0
  261. package/dist/providers/jobs/fake.d.ts +23 -0
  262. package/dist/providers/jobs/fake.js +132 -0
  263. package/dist/providers/jobs/kubernetes-api.d.ts +60 -0
  264. package/dist/providers/jobs/kubernetes-api.js +20 -0
  265. package/dist/providers/jobs/kubernetes-client.d.ts +36 -0
  266. package/dist/providers/jobs/kubernetes-client.js +171 -0
  267. package/dist/providers/jobs/kubernetes-dry-run-fixture.d.ts +49 -0
  268. package/dist/providers/jobs/kubernetes-dry-run-fixture.js +141 -0
  269. package/dist/providers/jobs/kubernetes-isolation.d.ts +147 -0
  270. package/dist/providers/jobs/kubernetes-isolation.js +607 -0
  271. package/dist/providers/jobs/kubernetes-platform.d.ts +177 -0
  272. package/dist/providers/jobs/kubernetes-platform.js +280 -0
  273. package/dist/providers/jobs/kubernetes-preflight.d.ts +59 -0
  274. package/dist/providers/jobs/kubernetes-preflight.js +339 -0
  275. package/dist/providers/jobs/kubernetes-witness.d.ts +10 -0
  276. package/dist/providers/jobs/kubernetes-witness.js +193 -0
  277. package/dist/providers/jobs/kubernetes.d.ts +127 -0
  278. package/dist/providers/jobs/kubernetes.js +874 -0
  279. package/dist/providers/jobs/safe-extract.d.ts +9 -0
  280. package/dist/providers/jobs/safe-extract.js +325 -0
  281. package/dist/providers/jobs/types.d.ts +78 -0
  282. package/dist/providers/jobs/types.js +6 -0
  283. package/dist/providers/jobs/workspace-swap.d.ts +2 -0
  284. package/dist/providers/jobs/workspace-swap.js +47 -0
  285. package/dist/providers/llm/anthropic.d.ts +13 -0
  286. package/dist/providers/llm/anthropic.js +21 -0
  287. package/dist/providers/llm/bedrock.d.ts +19 -0
  288. package/dist/providers/llm/bedrock.js +33 -0
  289. package/dist/providers/llm/claude-messages.d.ts +106 -0
  290. package/dist/providers/llm/claude-messages.js +157 -0
  291. package/dist/providers/llm/claude-provider.d.ts +40 -0
  292. package/dist/providers/llm/claude-provider.js +31 -0
  293. package/dist/providers/llm/index.d.ts +10 -0
  294. package/dist/providers/llm/index.js +10 -0
  295. package/dist/providers/llm/openai.d.ts +23 -0
  296. package/dist/providers/llm/openai.js +156 -0
  297. package/dist/providers/llm/pricing-anthropic.d.ts +4 -0
  298. package/dist/providers/llm/pricing-anthropic.js +30 -0
  299. package/dist/providers/llm/pricing-bedrock-claude.d.ts +12 -0
  300. package/dist/providers/llm/pricing-bedrock-claude.js +37 -0
  301. package/dist/providers/llm/pricing-core.d.ts +26 -0
  302. package/dist/providers/llm/pricing-core.js +17 -0
  303. package/dist/providers/llm/pricing.d.ts +30 -0
  304. package/dist/providers/llm/pricing.js +74 -0
  305. package/dist/providers/llm/registration.d.ts +8 -0
  306. package/dist/providers/llm/registration.js +39 -0
  307. package/dist/providers/llm/routing.d.ts +25 -0
  308. package/dist/providers/llm/routing.js +32 -0
  309. package/dist/providers/llm/types.d.ts +85 -0
  310. package/dist/providers/llm/types.js +12 -0
  311. package/dist/providers/memory/index.d.ts +2 -0
  312. package/dist/providers/memory/index.js +2 -0
  313. package/dist/providers/memory/postgres.d.ts +20 -0
  314. package/dist/providers/memory/postgres.js +72 -0
  315. package/dist/providers/memory/types.d.ts +32 -0
  316. package/dist/providers/memory/types.js +17 -0
  317. package/dist/providers/secrets/app-key.d.ts +8 -0
  318. package/dist/providers/secrets/app-key.js +50 -0
  319. package/dist/providers/secrets/index.d.ts +10 -0
  320. package/dist/providers/secrets/index.js +13 -0
  321. package/dist/providers/secrets/transfer-envelope.d.ts +12 -0
  322. package/dist/providers/secrets/transfer-envelope.js +35 -0
  323. package/dist/providers/secrets/types.d.ts +18 -0
  324. package/dist/providers/secrets/types.js +12 -0
  325. package/dist/providers/storage/types.d.ts +18 -0
  326. package/dist/providers/storage/types.js +7 -0
  327. package/dist/providers/vcs/git.d.ts +105 -0
  328. package/dist/providers/vcs/git.js +710 -0
  329. package/dist/providers/vcs/github.d.ts +105 -0
  330. package/dist/providers/vcs/github.js +394 -0
  331. package/dist/providers/vcs/index.d.ts +6 -0
  332. package/dist/providers/vcs/index.js +26 -0
  333. package/dist/providers/vcs/types.d.ts +118 -0
  334. package/dist/providers/vcs/types.js +1 -0
  335. package/dist/sandbox/bounded-json.d.ts +2 -0
  336. package/dist/sandbox/bounded-json.js +36 -0
  337. package/dist/sandbox/bridge.d.ts +15 -0
  338. package/dist/sandbox/bridge.js +78 -0
  339. package/dist/sandbox/eval-core.d.ts +37 -0
  340. package/dist/sandbox/eval-core.js +159 -0
  341. package/dist/sandbox/fetch-policy.d.ts +24 -0
  342. package/dist/sandbox/fetch-policy.js +96 -0
  343. package/dist/sandbox/generated/zod-to-json-schema.bundle.js +5 -0
  344. package/dist/sandbox/generated/zod.bundle.js +1 -0
  345. package/dist/sandbox/host-functions.d.ts +29 -0
  346. package/dist/sandbox/host-functions.js +168 -0
  347. package/dist/sandbox/limits.d.ts +39 -0
  348. package/dist/sandbox/limits.js +39 -0
  349. package/dist/sandbox/parser-worker/__fixtures__/crash.d.ts +1 -0
  350. package/dist/sandbox/parser-worker/__fixtures__/crash.js +6 -0
  351. package/dist/sandbox/parser-worker/__fixtures__/echo.d.ts +1 -0
  352. package/dist/sandbox/parser-worker/__fixtures__/echo.js +5 -0
  353. package/dist/sandbox/parser-worker/__fixtures__/reject.d.ts +1 -0
  354. package/dist/sandbox/parser-worker/__fixtures__/reject.js +5 -0
  355. package/dist/sandbox/parser-worker/__fixtures__/spin-forever.d.ts +1 -0
  356. package/dist/sandbox/parser-worker/__fixtures__/spin-forever.js +11 -0
  357. package/dist/sandbox/parser-worker/pool.d.ts +13 -0
  358. package/dist/sandbox/parser-worker/pool.js +111 -0
  359. package/dist/sandbox/parser-worker/worker.d.ts +14 -0
  360. package/dist/sandbox/parser-worker/worker.js +63 -0
  361. package/dist/sandbox/pii-redaction.d.ts +1 -0
  362. package/dist/sandbox/pii-redaction.js +29 -0
  363. package/dist/sandbox/prelude.d.ts +26 -0
  364. package/dist/sandbox/prelude.js +408 -0
  365. package/dist/sandbox/quickjs-module.d.ts +13 -0
  366. package/dist/sandbox/quickjs-module.js +19 -0
  367. package/dist/sandbox/run-in-sandbox.d.ts +34 -0
  368. package/dist/sandbox/run-in-sandbox.js +53 -0
  369. package/dist/sandbox/safe-fetch.d.ts +26 -0
  370. package/dist/sandbox/safe-fetch.js +140 -0
  371. package/dist/sandbox/tool-capabilities.d.ts +66 -0
  372. package/dist/sandbox/tool-capabilities.js +103 -0
  373. package/dist/sandbox/zod-params.d.ts +22 -0
  374. package/dist/sandbox/zod-params.js +60 -0
  375. package/dist/serve.d.ts +14 -0
  376. package/dist/serve.js +54 -0
  377. package/dist/wardby-bin.d.ts +2 -0
  378. package/dist/wardby-bin.js +20 -0
  379. package/package.json +123 -0
  380. package/prisma/migrations/20260905000000_init/migration.sql +34 -0
  381. package/prisma/migrations/20260905010000_scheduler_durability/migration.sql +31 -0
  382. package/prisma/migrations/20260905020000_tools_multiturn/migration.sql +47 -0
  383. package/prisma/migrations/20260905030000_cache_tool_json_schema/migration.sql +13 -0
  384. package/prisma/migrations/20260906000000_mcp_phase4/migration.sql +115 -0
  385. package/prisma/migrations/20260906010000_run_final_text/migration.sql +7 -0
  386. package/prisma/migrations/20260906020000_run_turns/migration.sql +6 -0
  387. package/prisma/migrations/20260906030000_task_principal_id/migration.sql +5 -0
  388. package/prisma/migrations/20260906040000_secure_self_hosted_oauth/migration.sql +67 -0
  389. package/prisma/migrations/20260906050000_phase5_coding_agents/migration.sql +52 -0
  390. package/prisma/migrations/20260906060000_tool_capability_scoping/migration.sql +22 -0
  391. package/prisma/migrations/20260906070000_datastore_pii_flag/migration.sql +6 -0
  392. package/prisma/migrations/20260906080000_secret_elicitation_outcomes/migration.sql +13 -0
  393. package/prisma/migrations/20260907010000_coding_proxy_ledger/migration.sql +57 -0
  394. package/prisma/migrations/20260907020000_budget_groups/migration.sql +24 -0
  395. package/prisma/migrations/20260907030000_coding_webhook_task_override/migration.sql +4 -0
  396. package/prisma/migrations/20260908010000_run_execution_backend/migration.sql +2 -0
  397. package/prisma/migrations/20260908020000_coding_run_audit_metadata/migration.sql +7 -0
  398. package/prisma/migrations/20260910010000_agent_secret_bound_name/migration.sql +16 -0
  399. package/prisma/migrations/20260911010000_coding_worker_toolchains/migration.sql +5 -0
  400. package/prisma/migrations/20260912010000_coding_provider_contract/migration.sql +24 -0
  401. package/prisma/migrations/20260912020000_coding_proxy_protocol/migration.sql +10 -0
  402. package/prisma/migrations/20260912030000_agent_memory/migration.sql +18 -0
  403. package/prisma/migrations/20260912040000_named_shared_datastores/migration.sql +68 -0
  404. package/prisma/migrations/20260913000000_run_trigger_webhook/migration.sql +2 -0
  405. package/prisma/migrations/20260913010000_agent_sub_agent/migration.sql +32 -0
  406. package/prisma/migrations/20260913020000_run_task_override/migration.sql +5 -0
  407. package/prisma/migrations/20260913030000_coding_run_root/migration.sql +11 -0
  408. package/prisma/migrations/20260922010000_coding_run_queued_at/migration.sql +8 -0
  409. package/prisma/migrations/20260922020000_coding_workspace_disk/migration.sql +8 -0
  410. package/prisma/migrations/migration_lock.toml +3 -0
  411. package/prisma/schema.prisma +723 -0
  412. package/scripts/git-askpass.sh +11 -0
@@ -0,0 +1,936 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { mkdir, realpath, rename, rm, writeFile } from "node:fs/promises";
3
+ import { join, resolve, sep } from "node:path";
4
+ import { CodingProfileSchema } from "../../coding/profile.js";
5
+ import { logger } from "../../core/logger.js";
6
+ import { codingRunObserver } from "../../coding/observability.js";
7
+ import { CODING_PROTOCOL_VERSION, CodingRunResultSchema, CodingTaskInputSchema, parseCodingAgentOutputJson, redactAndTruncate, } from "../../coding/protocol.js";
8
+ import { getModelPricing } from "../llm/pricing.js";
9
+ import { getAnthropicPricing } from "../llm/pricing-anthropic.js";
10
+ import { isImmutableDockerImage } from "../jobs/docker-isolation.js";
11
+ import { HEARTBEAT_TIMEOUT_MS } from "../../core/timing.js";
12
+ const containerLog = logger.child({ module: "container-executor" });
13
+ const TERMINAL_STATUSES = new Set(["succeeded", "failed", "refused", "lost", "budget_exhausted", "cancelled"]);
14
+ const PROVISIONING_BACKEND = "provisioning";
15
+ /** Serializes concurrency-slot claims across replicas. Distinct from the OAuth client lock (7412901). */
16
+ const CODING_SLOT_LOCK_SQL = "SELECT 1 AS locked FROM pg_advisory_xact_lock(7412902)";
17
+ /**
18
+ * Private sentinel thrown inside claimProvisioning's $transaction callback
19
+ * when the Run has already left pending/running (e.g. drainCodingQueue
20
+ * committed a coding_queue_timeout failure between the CodingRun claim
21
+ * update and the Run status flip). Throwing aborts the whole transaction --
22
+ * including the CodingRun claim -- so the claim is never left behind on a
23
+ * run that just got revived as "running"; the catch outside the
24
+ * transaction turns it into a plain "unavailable" result.
25
+ */
26
+ class RunNoLongerActiveError extends Error {
27
+ }
28
+ function proxyProtocol(provider) {
29
+ if (provider === "codex")
30
+ return "openai-responses";
31
+ if (provider === "claude-code")
32
+ return "anthropic-messages";
33
+ throw new Error("coding_provider_unsupported");
34
+ }
35
+ export class PrismaContainerExecutionStore {
36
+ db;
37
+ options;
38
+ constructor(db, options = {}) {
39
+ this.db = db;
40
+ this.options = options;
41
+ }
42
+ async load(runId) {
43
+ const row = await this.db.run.findUnique({
44
+ where: { id: runId },
45
+ include: { agent: true, codingRun: { include: { proxySession: true } } },
46
+ });
47
+ if (!row?.codingRun)
48
+ return null;
49
+ return {
50
+ runId: row.id,
51
+ status: row.status,
52
+ agentKind: row.agent.kind,
53
+ agentName: row.agent.name,
54
+ ownerId: row.agent.ownerId,
55
+ task: row.codingRun.task,
56
+ repository: row.codingRun.repository,
57
+ baseRef: row.codingRun.baseRef,
58
+ headRef: row.codingRun.headRef,
59
+ provider: row.codingRun.provider,
60
+ model: row.codingRun.model,
61
+ timeoutSec: row.codingRun.timeoutSec,
62
+ allowedEgress: row.codingRun.allowedEgress,
63
+ protectedPaths: row.codingRun.protectedPaths,
64
+ rootCodingRunId: row.codingRun.rootCodingRunId,
65
+ budgetUsd: Number(row.codingRun.budgetReservedUsd),
66
+ tokensIn: row.tokensIn,
67
+ tokensOut: row.tokensOut,
68
+ costUsd: Number(row.costUsd),
69
+ jobHandle: row.codingRun.jobBackend && row.codingRun.jobBackend !== PROVISIONING_BACKEND && row.codingRun.jobHandle
70
+ ? { backend: row.codingRun.jobBackend, id: row.codingRun.jobHandle }
71
+ : null,
72
+ provisioningClaim: row.codingRun.jobBackend === PROVISIONING_BACKEND ? (row.codingRun.jobHandle ?? null) : null,
73
+ proxySessionId: row.codingRun.proxySession?.id ?? null,
74
+ result: row.codingRun.result,
75
+ workerImage: row.codingRun.workerImage,
76
+ workspaceDiskMb: row.codingRun.workspaceDiskMb,
77
+ };
78
+ }
79
+ async claimProvisioning(runId, claimId) {
80
+ try {
81
+ return await this.db.$transaction(async (tx) => {
82
+ const { maxConcurrent } = this.options;
83
+ if (maxConcurrent !== undefined) {
84
+ // Slot usage is derived from run state, never a separate counter: a
85
+ // run that finishes, fails, is stopped, or is reconciled to lost stops
86
+ // counting, so a crashed replica cannot leak a slot.
87
+ await tx.$queryRawUnsafe(CODING_SLOT_LOCK_SQL);
88
+ const active = await tx.codingRun.count({
89
+ where: { jobBackend: { not: null }, run: { status: { in: ["pending", "running"] } } },
90
+ });
91
+ // Oldest first (spec §6): a free slot belongs to the queued runs
92
+ // ahead of this one, not to whichever claim reaches the lock first.
93
+ // Without this a fresh dispatch could take a slot freed with no
94
+ // immediate drain (stopped from another replica, say) and starve
95
+ // older queued runs into coding_queue_timeout. A run not yet queued
96
+ // is behind every queued run; queued runs order by (queuedAt,
97
+ // runId), the same order drainCodingQueue starts them in.
98
+ const self = await tx.codingRun.findUnique({ where: { runId }, select: { queuedAt: true } });
99
+ const selfQueuedAt = self?.queuedAt ?? null;
100
+ const queuedAhead = await tx.codingRun.count({
101
+ where: {
102
+ runId: { not: runId },
103
+ queuedAt: { not: null },
104
+ jobBackend: null,
105
+ run: { status: "pending" },
106
+ ...(selfQueuedAt
107
+ ? { OR: [{ queuedAt: { lt: selfQueuedAt } }, { queuedAt: selfQueuedAt, runId: { lt: runId } }] }
108
+ : {}),
109
+ },
110
+ });
111
+ if (active + queuedAhead >= maxConcurrent) {
112
+ const queued = await tx.codingRun.updateMany({
113
+ where: { runId, jobBackend: null, queuedAt: null, run: { status: "pending" } },
114
+ data: { queuedAt: new Date() },
115
+ });
116
+ if (queued.count === 1)
117
+ return "queued";
118
+ const alreadyQueued = await tx.codingRun.count({
119
+ where: { runId, jobBackend: null, queuedAt: { not: null }, run: { status: "pending" } },
120
+ });
121
+ return alreadyQueued === 1 ? "queued" : "unavailable";
122
+ }
123
+ }
124
+ const claimed = await tx.codingRun.updateMany({
125
+ where: {
126
+ runId,
127
+ jobBackend: null,
128
+ jobHandle: null,
129
+ run: { status: { in: ["pending", "running"] } },
130
+ },
131
+ data: { jobBackend: PROVISIONING_BACKEND, jobHandle: claimId, queuedAt: null },
132
+ });
133
+ if (claimed.count === 0)
134
+ return "unavailable";
135
+ const started = await tx.run.updateMany({
136
+ where: { id: runId, status: { in: ["pending", "running"] } },
137
+ data: { status: "running", heartbeatAt: new Date() },
138
+ });
139
+ if (started.count === 0) {
140
+ // The Run left pending/running between the CodingRun claim above and
141
+ // here (e.g. drainCodingQueue's coding_queue_timeout failure landed
142
+ // mid-claim). Abort the whole transaction so the CodingRun claim
143
+ // rolls back too, rather than reviving a run that just failed.
144
+ throw new RunNoLongerActiveError();
145
+ }
146
+ return "claimed";
147
+ });
148
+ }
149
+ catch (err) {
150
+ if (err instanceof RunNoLongerActiveError)
151
+ return "unavailable";
152
+ throw err;
153
+ }
154
+ }
155
+ async persistHandle(runId, claimId, handle) {
156
+ await this.db.$transaction(async (tx) => {
157
+ const coding = await tx.codingRun.findUnique({ where: { runId }, include: { run: true } });
158
+ if (!coding)
159
+ throw new Error("coding_run_not_found");
160
+ if (coding.run.status !== "pending" && coding.run.status !== "running") {
161
+ throw new Error("coding_run_not_active");
162
+ }
163
+ if (coding.jobBackend || coding.jobHandle) {
164
+ if (coding.jobBackend === handle.backend && coding.jobHandle === handle.id)
165
+ return;
166
+ if (coding.jobBackend !== PROVISIONING_BACKEND || coding.jobHandle !== claimId) {
167
+ throw new Error("coding_job_handle_conflict");
168
+ }
169
+ }
170
+ const persisted = await tx.codingRun.updateMany({
171
+ where: { runId, jobBackend: PROVISIONING_BACKEND, jobHandle: claimId },
172
+ data: { jobBackend: handle.backend, jobHandle: handle.id },
173
+ });
174
+ if (persisted.count !== 1)
175
+ throw new Error("coding_job_handle_conflict");
176
+ await tx.run.update({
177
+ where: { id: runId },
178
+ data: { status: "running", heartbeatAt: new Date() },
179
+ });
180
+ });
181
+ }
182
+ async heartbeat(runId) {
183
+ await this.db.run.updateMany({
184
+ where: { id: runId, status: "running" },
185
+ data: { heartbeatAt: new Date() },
186
+ });
187
+ }
188
+ async complete(runId, status, result) {
189
+ await this.db.$transaction(async (tx) => {
190
+ const run = await tx.run.findUnique({ where: { id: runId }, include: { codingRun: true } });
191
+ if (!run?.codingRun)
192
+ throw new Error("coding_run_not_found");
193
+ if (TERMINAL_STATUSES.has(run.status)) {
194
+ if (run.codingRun.result && stableJson(run.codingRun.result) === stableJson(result))
195
+ return;
196
+ throw new Error("coding_run_terminal_conflict");
197
+ }
198
+ await tx.codingRun.update({
199
+ where: { runId },
200
+ data: { result, resultSchema: CODING_PROTOCOL_VERSION },
201
+ });
202
+ await tx.run.update({
203
+ where: { id: runId },
204
+ data: {
205
+ status,
206
+ finalText: result.summary,
207
+ finishedAt: new Date(),
208
+ heartbeatAt: new Date(),
209
+ error: null,
210
+ },
211
+ });
212
+ });
213
+ }
214
+ async terminate(runId, status, error, audit) {
215
+ await this.db.$transaction(async (tx) => {
216
+ const updated = await tx.run.updateMany({
217
+ where: { id: runId, status: { in: ["pending", "running"] } },
218
+ data: { status, error, finishedAt: new Date(), heartbeatAt: new Date() },
219
+ });
220
+ if (updated.count && audit) {
221
+ await tx.codingRun.update({
222
+ where: { runId },
223
+ data: { failureCategory: audit.failureCategory, diagnosticId: audit.diagnosticId },
224
+ });
225
+ }
226
+ });
227
+ }
228
+ }
229
+ /** Plain capabilities live only for the short provisioning window. */
230
+ export class RunCapabilityVault {
231
+ values = new Map();
232
+ set(runId, capability) {
233
+ if (this.values.has(runId))
234
+ throw new Error("coding_capability_exists");
235
+ this.values.set(runId, capability);
236
+ }
237
+ async get(runId) {
238
+ const capability = this.values.get(runId);
239
+ if (!capability)
240
+ throw new Error("coding_capability_unavailable");
241
+ return capability;
242
+ }
243
+ delete(runId) {
244
+ this.values.delete(runId);
245
+ }
246
+ }
247
+ class PreflightError extends Error {
248
+ }
249
+ export class ContainerExecutor {
250
+ options;
251
+ artifactRoot;
252
+ active = new Map();
253
+ sleep;
254
+ now;
255
+ observer;
256
+ startedAt = new Map();
257
+ constructor(options) {
258
+ this.options = options;
259
+ this.artifactRoot = resolve(options.artifactRoot);
260
+ if (this.artifactRoot === resolve("/"))
261
+ throw new Error("coding_artifact_root_invalid");
262
+ if (!isImmutableDockerImage(options.workerImage)) {
263
+ throw new Error("coding_worker_image_invalid");
264
+ }
265
+ for (const versions of Object.values(options.additionalWorkerImages ?? {})) {
266
+ for (const image of Object.values(versions)) {
267
+ if (!isImmutableDockerImage(image))
268
+ throw new Error("coding_worker_image_invalid");
269
+ }
270
+ }
271
+ for (const image of [options.claudeWorkerImage, options.claudeToolRunnerImage]) {
272
+ if (image !== undefined && !isImmutableDockerImage(image))
273
+ throw new Error("coding_worker_image_invalid");
274
+ }
275
+ if (!/^[A-Za-z][A-Za-z0-9_.:-]{0,199}$/.test(options.credentialRef)) {
276
+ throw new Error("coding_credential_ref_invalid");
277
+ }
278
+ if (options.anthropicCredentialRef !== undefined &&
279
+ !/^[A-Za-z][A-Za-z0-9_.:-]{0,199}$/.test(options.anthropicCredentialRef)) {
280
+ throw new Error("coding_credential_ref_invalid");
281
+ }
282
+ const { cpus, memoryMb, pids, diskMb } = options.limits;
283
+ if (!Number.isFinite(cpus) ||
284
+ cpus <= 0 ||
285
+ !Number.isSafeInteger(memoryMb) ||
286
+ memoryMb <= 0 ||
287
+ !Number.isSafeInteger(pids) ||
288
+ pids <= 0 ||
289
+ !Number.isSafeInteger(diskMb) ||
290
+ diskMb <= 0) {
291
+ throw new Error("coding_resource_limits_invalid");
292
+ }
293
+ this.sleep =
294
+ options.sleep ?? ((milliseconds) => new Promise((resolvePromise) => setTimeout(resolvePromise, milliseconds)));
295
+ this.now = options.now ?? (() => new Date());
296
+ this.observer = options.observer ?? codingRunObserver;
297
+ }
298
+ async start(runId) {
299
+ const current = this.active.get(runId);
300
+ if (current)
301
+ return current;
302
+ this.startedAt.set(runId, this.now().getTime());
303
+ this.emit({ stage: "queued", runId });
304
+ const execution = this.execute(runId).finally(async () => {
305
+ this.active.delete(runId);
306
+ if (!this.options.onSlotReleased)
307
+ return;
308
+ const after = await this.options.store.load(runId).catch(() => null);
309
+ if (after && TERMINAL_STATUSES.has(after.status))
310
+ this.options.onSlotReleased();
311
+ });
312
+ this.active.set(runId, execution);
313
+ return execution;
314
+ }
315
+ async stop(runId, reason = "cancelled") {
316
+ const run = await this.options.store.load(runId);
317
+ if (!run)
318
+ return;
319
+ if (TERMINAL_STATUSES.has(run.status))
320
+ return this.cleanupTerminal(run);
321
+ this.emit({ stage: "stopping", runId, jobId: run.jobHandle?.id });
322
+ // Persist the cancellation fence first so a concurrent finisher cannot publish.
323
+ const failure = this.failure("cancelled", { cancelled: true });
324
+ await this.options.store.terminate(runId, "cancelled", failure.error, failure.audit);
325
+ this.terminal(run, "cancelled", failure.audit);
326
+ if (run.proxySessionId)
327
+ await this.options.sessions.cancelSession(run.proxySessionId).catch(() => undefined);
328
+ if (run.jobHandle) {
329
+ await this.options.jobs.stop(run.jobHandle, reason).catch(() => undefined);
330
+ await this.options.jobs.remove(run.jobHandle).catch(() => undefined);
331
+ }
332
+ this.options.capabilities.delete(runId);
333
+ const input = this.preflightForCleanup(run);
334
+ const workspace = input ? await this.options.vcs.recoverWorkspace(input).catch(() => null) : null;
335
+ if (workspace) {
336
+ await this.options.vcs.cleanup(workspace).catch(() => undefined);
337
+ await this.options.vcs.notifyContinuationFinished?.(workspace, "failed", { agentName: run.agentName });
338
+ }
339
+ await rm(this.artifactPath(runId), { recursive: true, force: true }).catch(() => undefined);
340
+ this.emit({ stage: "cleanup", runId, jobId: run.jobHandle?.id, cleanupSucceeded: true });
341
+ }
342
+ async recover(handle) {
343
+ const run = await this.options.store.load(handle.runId);
344
+ if (!run)
345
+ return { state: "lost", reason: "coding_run_not_found" };
346
+ if (TERMINAL_STATUSES.has(run.status)) {
347
+ await this.cleanupTerminal(run);
348
+ return { state: "terminal" };
349
+ }
350
+ if (handle.backend === PROVISIONING_BACKEND || run.provisioningClaim) {
351
+ await this.abandon(run, "coding_ambiguous_provisioning");
352
+ return { state: "lost", reason: "coding_ambiguous_provisioning" };
353
+ }
354
+ if (!run.jobHandle || run.jobHandle.backend !== handle.backend || run.jobHandle.id !== handle.id) {
355
+ await this.abandon(run, "coding_job_handle_mismatch");
356
+ return { state: "lost", reason: "coding_job_handle_mismatch" };
357
+ }
358
+ let status;
359
+ try {
360
+ status = await this.options.jobs.status(run.jobHandle);
361
+ }
362
+ catch {
363
+ await this.abandon(run, "coding_job_status_unavailable");
364
+ return { state: "lost", reason: "coding_job_status_unavailable" };
365
+ }
366
+ if (status.state === "pending" || status.state === "running") {
367
+ await this.options.store.heartbeat(run.runId);
368
+ return { state: "active" };
369
+ }
370
+ await this.finishTerminal(run, run.jobHandle, status.state);
371
+ const finished = await this.options.store.load(run.runId);
372
+ return finished && TERMINAL_STATUSES.has(finished.status)
373
+ ? { state: "terminal" }
374
+ : { state: "lost", reason: "coding_recovery_incomplete" };
375
+ }
376
+ async execute(runId) {
377
+ let run = await this.options.store.load(runId);
378
+ if (!run)
379
+ throw new Error("coding_run_not_found");
380
+ if (TERMINAL_STATUSES.has(run.status))
381
+ return this.cleanupTerminal(run);
382
+ let workspace = null;
383
+ let sessionId = run.proxySessionId;
384
+ let handle = run.jobHandle;
385
+ let claimId = null;
386
+ let spendEnabled = Boolean(sessionId);
387
+ try {
388
+ const prepared = this.preflight(run);
389
+ if (!handle) {
390
+ if (run.provisioningClaim)
391
+ return;
392
+ claimId = randomUUID();
393
+ // "queued": every slot is taken; the run stays pending and
394
+ // drainCodingQueue starts it when one frees. "unavailable": another
395
+ // process owns provisioning, or the run is no longer active.
396
+ if ((await this.options.store.claimProvisioning(runId, claimId)) !== "claimed")
397
+ return;
398
+ }
399
+ try {
400
+ workspace = await this.options.vcs.recoverWorkspace(prepared);
401
+ if (!workspace && handle)
402
+ throw new Error("coding_workspace_lost");
403
+ if (!workspace)
404
+ workspace = await this.options.vcs.prepareWorkspace(prepared);
405
+ this.emit({ stage: "prepared", runId });
406
+ }
407
+ catch (error) {
408
+ if (!spendEnabled && !handle)
409
+ throw new PreflightError(safeError(error), { cause: error });
410
+ throw error;
411
+ }
412
+ await this.options.vcs.notifyContinuationStarted?.(workspace, { agentName: run.agentName });
413
+ if (!handle) {
414
+ if (sessionId)
415
+ throw new Error("coding_ambiguous_provisioning");
416
+ const beforeSession = await this.requireCurrent(runId);
417
+ if (TERMINAL_STATUSES.has(beforeSession.status))
418
+ throw new Error("coding_run_no_longer_active");
419
+ const deadlineAt = new Date(this.now().getTime() + run.timeoutSec * 1_000);
420
+ const session = await this.options.sessions.createSession({
421
+ runId,
422
+ credentialRef: this.credentialRef(run.provider),
423
+ protocol: proxyProtocol(run.provider),
424
+ allowedModels: [run.model],
425
+ deadlineAt,
426
+ budgetUsd: run.budgetUsd,
427
+ });
428
+ spendEnabled = true;
429
+ sessionId = session.id;
430
+ this.options.capabilities.set(runId, session.capability);
431
+ const inputArtifact = await this.writeInput(run, deadlineAt);
432
+ const beforeLaunch = await this.requireCurrent(runId);
433
+ if (TERMINAL_STATUSES.has(beforeLaunch.status))
434
+ throw new Error("coding_run_no_longer_active");
435
+ const spec = this.jobSpec(run, inputArtifact);
436
+ // Backends whose handle is derivable (Kubernetes) persist it first, so a crash between the
437
+ // launch and the write still leaves a handle `abandon()` can stop and remove the run with.
438
+ const planned = this.options.jobs.plannedHandle?.(spec);
439
+ if (planned)
440
+ await this.options.store.persistHandle(runId, claimId, planned);
441
+ handle = await this.whileHeartbeating(runId, () => this.options.jobs.launch(spec));
442
+ if (planned && (planned.backend !== handle.backend || planned.id !== handle.id)) {
443
+ throw new Error("coding_job_handle_mismatch");
444
+ }
445
+ await this.options.store.persistHandle(runId, claimId, handle);
446
+ this.options.capabilities.delete(runId);
447
+ this.emit({ stage: "launched", runId, jobId: handle.id, budgetReservedUsd: run.budgetUsd });
448
+ }
449
+ let delay = this.options.pollMinMs ?? 250;
450
+ const maximumDelay = this.options.pollMaxMs ?? 5_000;
451
+ let observedRunning = false;
452
+ for (;;) {
453
+ const status = await this.options.jobs.status(handle);
454
+ if (status.state !== "pending" && status.state !== "running") {
455
+ await this.finishTerminal(run, handle, status.state, workspace, sessionId);
456
+ return;
457
+ }
458
+ if (status.state === "running" && !observedRunning) {
459
+ observedRunning = true;
460
+ this.emit({ stage: "running", runId, jobId: handle.id });
461
+ }
462
+ await this.options.store.heartbeat(runId);
463
+ await this.sleep(delay);
464
+ delay = Math.min(maximumDelay, Math.max(delay + 1, Math.ceil(delay * 1.5)));
465
+ run = (await this.options.store.load(runId)) ?? run;
466
+ if (TERMINAL_STATUSES.has(run.status))
467
+ return;
468
+ }
469
+ }
470
+ catch (error) {
471
+ this.options.capabilities.delete(runId);
472
+ if (sessionId)
473
+ await this.options.sessions.cancelSession(sessionId).catch(() => undefined);
474
+ if (handle)
475
+ await this.options.jobs.stop(handle, "executor_failure").catch(() => undefined);
476
+ const status = !spendEnabled && error instanceof PreflightError ? "refused" : "failed";
477
+ const failure = this.failure(error);
478
+ this.emit({
479
+ stage: "stopping",
480
+ runId,
481
+ jobId: handle?.id,
482
+ failureCategory: failure.audit.failureCategory,
483
+ diagnosticId: failure.audit.diagnosticId,
484
+ });
485
+ await this.options.store.terminate(runId, status, failure.error, failure.audit);
486
+ this.terminal(run, status, failure.audit);
487
+ if (handle)
488
+ await this.options.jobs.remove(handle).catch(() => undefined);
489
+ if (workspace) {
490
+ await this.options.vcs.cleanup(workspace).catch(() => undefined);
491
+ await this.options.vcs.notifyContinuationFinished?.(workspace, "failed", { agentName: run.agentName });
492
+ }
493
+ this.emit({ stage: "cleanup", runId, jobId: handle?.id, cleanupSucceeded: true });
494
+ }
495
+ }
496
+ /**
497
+ * Runs `work` while keeping the run's heartbeat fresh.
498
+ *
499
+ * `jobs.launch` is one long await that legitimately takes minutes on a real
500
+ * cluster — the pod cannot be scheduled until an autoscaler has created a node
501
+ * for it — and nothing inside it beats the heartbeat. Meanwhile the reconciler
502
+ * sweeps any run whose heartbeat is older than HEARTBEAT_TIMEOUT_MS and
503
+ * declares it lost, which deletes the job out from under the launch that is
504
+ * still running. The launch then fails on its own pod having vanished, so a
505
+ * single healthy run produces two failures and neither names the real cause.
506
+ *
507
+ * Measured on GKE Autopilot (2026-09-23): a scheduled run whose pod was
508
+ * waiting on a cold gVisor node pool was declared lost at exactly 60s, and the
509
+ * launch it had interrupted failed 32s later with
510
+ * `kubernetes_isolation_unsupported:timeout`. This never appeared on kind,
511
+ * where a pod is scheduled in seconds and the heartbeat gap stays well inside
512
+ * the timeout. The gap is what is wrong, not the timeout: a run that is
513
+ * provisioning IS alive, and the executor is the only thing that knows it.
514
+ *
515
+ * Failures to beat are swallowed on purpose. A missed beat costs at worst the
516
+ * recovery this exists to prevent, while throwing here would fail a launch
517
+ * that is otherwise healthy.
518
+ */
519
+ async whileHeartbeating(runId, work) {
520
+ const interval = this.options.heartbeatIntervalMs ?? Math.floor(HEARTBEAT_TIMEOUT_MS / 3);
521
+ const timer = setInterval(() => {
522
+ void this.options.store.heartbeat(runId).catch(() => undefined);
523
+ }, interval);
524
+ // Never hold the process open for a heartbeat: shutdown should not wait on one.
525
+ timer.unref?.();
526
+ try {
527
+ return await work();
528
+ }
529
+ finally {
530
+ clearInterval(timer);
531
+ }
532
+ }
533
+ preflight(run) {
534
+ try {
535
+ if (run.status !== "pending" && run.status !== "running")
536
+ throw new Error("coding_run_status_invalid");
537
+ if (run.agentKind !== "coding" || !run.ownerId)
538
+ throw new Error("coding_run_ownership_invalid");
539
+ if (run.provider === "codex")
540
+ getModelPricing(run.model);
541
+ else if (run.provider === "claude-code")
542
+ getAnthropicPricing(run.model);
543
+ else
544
+ throw new Error("coding_provider_unsupported");
545
+ if (!Number.isFinite(run.budgetUsd) || run.budgetUsd <= 0 || run.costUsd > run.budgetUsd) {
546
+ throw new Error("coding_run_budget_invalid");
547
+ }
548
+ const profile = CodingProfileSchema.parse({
549
+ provider: run.provider,
550
+ repository: run.repository,
551
+ baseRef: run.baseRef,
552
+ defaultTask: null,
553
+ timeoutSec: run.timeoutSec,
554
+ allowedEgress: run.allowedEgress,
555
+ protectedPaths: run.protectedPaths,
556
+ });
557
+ const expectedHeadRunId = run.rootCodingRunId ?? run.runId;
558
+ if (run.headRef !== `wardby/run-${expectedHeadRunId}`)
559
+ throw new Error("coding_head_ref_invalid");
560
+ const continuationOf = run.rootCodingRunId ? { runId: run.rootCodingRunId } : undefined;
561
+ CodingTaskInputSchema.parse({
562
+ schemaVersion: CODING_PROTOCOL_VERSION,
563
+ runId: run.runId,
564
+ repository: profile.repository,
565
+ baseRef: profile.baseRef,
566
+ headRef: run.headRef,
567
+ task: run.task,
568
+ model: run.model,
569
+ budgetUsd: run.budgetUsd,
570
+ deadlineAt: new Date(this.now().getTime() + run.timeoutSec * 1_000).toISOString(),
571
+ continuationOf,
572
+ });
573
+ return {
574
+ runId: run.runId,
575
+ repository: profile.repository,
576
+ baseRef: profile.baseRef,
577
+ headRef: run.headRef,
578
+ protectedPaths: profile.protectedPaths,
579
+ continuation: run.rootCodingRunId ? { rootRunId: run.rootCodingRunId } : undefined,
580
+ };
581
+ }
582
+ catch (error) {
583
+ throw new PreflightError(safeError(error), { cause: error });
584
+ }
585
+ }
586
+ async finishTerminal(run, handle, jobState, existingWorkspace, existingSessionId) {
587
+ const sessionId = existingSessionId ?? run.proxySessionId;
588
+ if (sessionId)
589
+ await this.options.sessions.cancelSession(sessionId).catch(() => undefined);
590
+ // For notifyContinuationFinished in the `finally` below -- defaults to
591
+ // "failed" and is only flipped right before an actual success return.
592
+ let outcome = "failed";
593
+ let finishedSummary;
594
+ try {
595
+ if (jobState !== "succeeded") {
596
+ const collected = await this.options.jobs.collect(handle).catch(() => null);
597
+ const reason = collected?.diagnostic ?? collected?.reason ?? jobState;
598
+ this.emit({ stage: "collected", runId: run.runId, jobId: handle.id });
599
+ const status = jobState === "lost" ? "lost" : "failed";
600
+ const failure = this.failure(`job_${reason}`);
601
+ await this.options.store.terminate(run.runId, status, failure.error, failure.audit);
602
+ this.terminal(run, status, failure.audit);
603
+ return;
604
+ }
605
+ const collected = await this.options.jobs.collect(handle);
606
+ this.emit({ stage: "collected", runId: run.runId, jobId: handle.id });
607
+ if (collected.reason !== "completed" || !collected.resultArtifact)
608
+ throw new Error("coding_result_missing");
609
+ const output = parseCodingAgentOutputJson(collected.resultArtifact);
610
+ if (output.runId !== run.runId)
611
+ throw new Error("coding_result_run_mismatch");
612
+ let current = await this.requireCurrent(run.runId);
613
+ if (current.costUsd > current.budgetUsd || output.outcome === "budget_exhausted") {
614
+ this.emit({ stage: "budget_cutoff", runId: run.runId, jobId: handle.id });
615
+ await this.options.store.complete(run.runId, "budget_exhausted", this.resultFor(output, current, "budget_exhausted"));
616
+ this.terminal(current, "budget_exhausted");
617
+ return;
618
+ }
619
+ if (output.outcome === "no_changes") {
620
+ await this.options.store.complete(run.runId, "succeeded", this.resultFor(output, current));
621
+ this.terminal(current, "succeeded");
622
+ outcome = "succeeded";
623
+ finishedSummary = output.summary;
624
+ return;
625
+ }
626
+ const preparedInput = this.preflight(current);
627
+ const workspace = existingWorkspace ?? (await this.options.vcs.recoverWorkspace(preparedInput));
628
+ if (!workspace)
629
+ throw new Error("coding_workspace_lost");
630
+ await this.options.jobs.materializeWorkspace(handle, workspace.workspacePath);
631
+ current = await this.requireCurrent(run.runId);
632
+ if (current.costUsd > current.budgetUsd) {
633
+ this.emit({ stage: "budget_cutoff", runId: run.runId, jobId: handle.id });
634
+ await this.options.store.complete(run.runId, "budget_exhausted", this.resultFor(output, current, "budget_exhausted"));
635
+ this.terminal(current, "budget_exhausted");
636
+ return;
637
+ }
638
+ const finalized = await this.options.vcs.finalizeChanges(workspace, {
639
+ summary: output.summary,
640
+ tests: output.tests,
641
+ tag: output.tag,
642
+ });
643
+ const result = this.resultFor(output, current, finalized.outcome, finalized);
644
+ await this.options.store.complete(run.runId, "succeeded", result);
645
+ if (finalized.outcome === "pull_request_opened" || finalized.outcome === "pull_request_updated") {
646
+ this.emit({ stage: finalized.outcome, runId: run.runId, jobId: handle.id });
647
+ }
648
+ this.terminal(current, "succeeded");
649
+ outcome = "succeeded";
650
+ finishedSummary = output.summary;
651
+ }
652
+ catch (error) {
653
+ const failure = this.failure(error);
654
+ await this.options.store.terminate(run.runId, "failed", failure.error, failure.audit);
655
+ this.terminal(run, "failed", failure.audit);
656
+ }
657
+ finally {
658
+ await this.options.jobs.remove(handle).catch(() => undefined);
659
+ const input = this.preflightForCleanup(run);
660
+ const workspace = existingWorkspace ?? (input ? await this.options.vcs.recoverWorkspace(input).catch(() => null) : null);
661
+ if (workspace) {
662
+ await this.options.vcs.cleanup(workspace).catch(() => undefined);
663
+ await this.options.vcs.notifyContinuationFinished?.(workspace, outcome, {
664
+ summary: finishedSummary,
665
+ agentName: run.agentName,
666
+ });
667
+ }
668
+ await rm(this.artifactPath(run.runId), { recursive: true, force: true }).catch(() => undefined);
669
+ this.emit({ stage: "cleanup", runId: run.runId, jobId: handle.id, cleanupSucceeded: true });
670
+ }
671
+ }
672
+ resultFor(output, run, forcedOutcome, finalized) {
673
+ const outcome = forcedOutcome ?? (output.outcome === "budget_exhausted" ? "budget_exhausted" : "no_changes");
674
+ return CodingRunResultSchema.parse({
675
+ schemaVersion: CODING_PROTOCOL_VERSION,
676
+ outcome,
677
+ repository: run.repository,
678
+ baseRef: run.baseRef,
679
+ ...((outcome === "pull_request_opened" || outcome === "pull_request_updated") &&
680
+ (finalized?.outcome === "pull_request_opened" || finalized?.outcome === "pull_request_updated")
681
+ ? {
682
+ headRef: finalized.headRef,
683
+ commitSha: finalized.commitSha,
684
+ pullRequestUrl: finalized.pullRequestUrl,
685
+ pullRequestNumber: finalized.pullRequestNumber,
686
+ }
687
+ : {}),
688
+ summary: output.summary,
689
+ tests: output.tests,
690
+ tag: output.tag,
691
+ usage: { tokensIn: run.tokensIn, tokensOut: run.tokensOut, costUsd: run.costUsd },
692
+ });
693
+ }
694
+ resolveCodingWorkerImage(selector) {
695
+ if (selector.provider === "claude-code") {
696
+ if (selector.toolchain !== "node" || selector.toolchainVersion !== null) {
697
+ throw new Error("coding_toolchain_unsupported:claude-code");
698
+ }
699
+ const image = selector.workerImageRef ?? this.options.claudeWorkerImage;
700
+ if (!image || !this.options.claudeToolRunnerImage)
701
+ throw new Error("coding_provider_not_configured:claude-code");
702
+ if (!isImmutableDockerImage(image) || !isImmutableDockerImage(this.options.claudeToolRunnerImage)) {
703
+ throw new Error("coding_worker_image_invalid");
704
+ }
705
+ return image;
706
+ }
707
+ if (selector.provider !== "codex") {
708
+ throw new Error(`coding_provider_unsupported:${String(selector.provider)}`);
709
+ }
710
+ if (selector.workerImageRef) {
711
+ if (!isImmutableDockerImage(selector.workerImageRef))
712
+ throw new Error("coding_worker_image_invalid");
713
+ return selector.workerImageRef;
714
+ }
715
+ if (selector.toolchain === "node")
716
+ return this.options.workerImage;
717
+ const versions = this.options.additionalWorkerImages?.[selector.toolchain];
718
+ const image = selector.toolchainVersion ? versions?.[selector.toolchainVersion] : undefined;
719
+ if (!image) {
720
+ throw new Error(`No worker image for toolchain "${selector.toolchain}" version "${selector.toolchainVersion ?? "(none)"}" — refusing to guess. Add it to additionalWorkerImages.`);
721
+ }
722
+ return image;
723
+ }
724
+ async requireCurrent(runId) {
725
+ const current = await this.options.store.load(runId);
726
+ if (!current)
727
+ throw new Error("coding_run_not_found");
728
+ return current;
729
+ }
730
+ jobSpec(run, inputArtifact) {
731
+ const provider = run.provider === "claude-code" ? "claude-code" : "codex";
732
+ if (provider === "claude-code" && (!run.workerImage || !this.options.claudeToolRunnerImage)) {
733
+ throw new Error("coding_provider_not_configured:claude-code");
734
+ }
735
+ if (run.workspaceDiskMb && run.workspaceDiskMb > this.options.maxDiskMb) {
736
+ throw new Error("coding_workspace_disk_exceeds_limit");
737
+ }
738
+ return {
739
+ kind: "coding-agent",
740
+ runId: run.runId,
741
+ provider,
742
+ image: run.workerImage ?? this.options.workerImage,
743
+ ...(provider === "claude-code" ? { toolImage: this.options.claudeToolRunnerImage } : {}),
744
+ inputArtifact,
745
+ timeoutSec: run.timeoutSec,
746
+ limits: { ...this.options.limits, ...(run.workspaceDiskMb ? { diskMb: run.workspaceDiskMb } : {}) },
747
+ labels: {},
748
+ };
749
+ }
750
+ credentialRef(provider) {
751
+ if (provider === "codex")
752
+ return this.options.credentialRef;
753
+ if (provider === "claude-code" && this.options.anthropicCredentialRef)
754
+ return this.options.anthropicCredentialRef;
755
+ throw new Error("coding_provider_not_configured:claude-code");
756
+ }
757
+ async writeInput(run, deadlineAt) {
758
+ await mkdir(this.artifactRoot, { recursive: true, mode: 0o700 });
759
+ const rootReal = await realpath(this.artifactRoot);
760
+ const directory = resolve(rootReal, run.runId);
761
+ if (!directory.startsWith(`${rootReal}${sep}`))
762
+ throw new Error("coding_artifact_path_invalid");
763
+ await mkdir(directory, { recursive: true, mode: 0o700 });
764
+ if ((await realpath(directory)) !== directory)
765
+ throw new Error("coding_artifact_path_invalid");
766
+ const destination = join(directory, "input.json");
767
+ const temporary = join(directory, `.input-${randomUUID()}.tmp`);
768
+ const input = CodingTaskInputSchema.parse({
769
+ schemaVersion: CODING_PROTOCOL_VERSION,
770
+ runId: run.runId,
771
+ repository: run.repository,
772
+ baseRef: run.baseRef,
773
+ headRef: run.headRef,
774
+ task: run.task,
775
+ model: run.model,
776
+ budgetUsd: run.budgetUsd,
777
+ deadlineAt: deadlineAt.toISOString(),
778
+ continuationOf: run.rootCodingRunId ? { runId: run.rootCodingRunId } : undefined,
779
+ });
780
+ // The run directory is 0700; world-readable mode only crosses Docker's UID boundary.
781
+ await writeFile(temporary, JSON.stringify(input), { flag: "wx", mode: 0o444 });
782
+ await rename(temporary, destination);
783
+ return destination;
784
+ }
785
+ artifactPath(runId) {
786
+ return resolve(this.artifactRoot, runId);
787
+ }
788
+ preflightForCleanup(run) {
789
+ if (!Array.isArray(run.protectedPaths) || !run.protectedPaths.every((path) => typeof path === "string"))
790
+ return null;
791
+ return {
792
+ runId: run.runId,
793
+ repository: run.repository,
794
+ baseRef: run.baseRef,
795
+ headRef: run.headRef,
796
+ protectedPaths: run.protectedPaths,
797
+ continuation: run.rootCodingRunId ? { rootRunId: run.rootCodingRunId } : undefined,
798
+ };
799
+ }
800
+ async abandon(run, reason) {
801
+ const failure = this.failure(reason);
802
+ await this.options.store.terminate(run.runId, "lost", failure.error, failure.audit);
803
+ this.terminal(run, "lost", failure.audit);
804
+ if (run.proxySessionId)
805
+ await this.options.sessions.cancelSession(run.proxySessionId).catch(() => undefined);
806
+ if (run.jobHandle) {
807
+ await this.options.jobs.stop(run.jobHandle, reason).catch(() => undefined);
808
+ await this.options.jobs.collect(run.jobHandle).catch(() => undefined);
809
+ await this.options.jobs.remove(run.jobHandle).catch(() => undefined);
810
+ }
811
+ const input = this.preflightForCleanup(run);
812
+ const workspace = input ? await this.options.vcs.recoverWorkspace(input).catch(() => null) : null;
813
+ if (workspace) {
814
+ await this.options.vcs.cleanup(workspace).catch(() => undefined);
815
+ await this.options.vcs.notifyContinuationFinished?.(workspace, "failed", { agentName: run.agentName });
816
+ }
817
+ await rm(this.artifactPath(run.runId), { recursive: true, force: true }).catch(() => undefined);
818
+ this.emit({ stage: "cleanup", runId: run.runId, jobId: run.jobHandle?.id, cleanupSucceeded: true });
819
+ }
820
+ async cleanupTerminal(run) {
821
+ if (run.proxySessionId)
822
+ await this.options.sessions.cancelSession(run.proxySessionId).catch(() => undefined);
823
+ if (run.jobHandle) {
824
+ const status = await this.options.jobs.status(run.jobHandle).catch(() => null);
825
+ if (status?.state === "pending" || status?.state === "running") {
826
+ await this.options.jobs.stop(run.jobHandle, "terminal_cleanup").catch(() => undefined);
827
+ }
828
+ await this.options.jobs.collect(run.jobHandle).catch(() => undefined);
829
+ await this.options.jobs.remove(run.jobHandle).catch(() => undefined);
830
+ }
831
+ const input = this.preflightForCleanup(run);
832
+ const workspace = input ? await this.options.vcs.recoverWorkspace(input).catch(() => null) : null;
833
+ if (workspace) {
834
+ await this.options.vcs.cleanup(workspace).catch(() => undefined);
835
+ // Safety-net retry for an already-terminal run recovered later (e.g.
836
+ // a crash between an earlier store.complete/terminate and this
837
+ // notification actually firing) -- safe to call again because
838
+ // notifyContinuationFinished is find-and-update-or-no-op, never
839
+ // find-or-create.
840
+ await this.options.vcs.notifyContinuationFinished?.(workspace, run.status === "succeeded" ? "succeeded" : "failed", { agentName: run.agentName });
841
+ }
842
+ await rm(this.artifactPath(run.runId), { recursive: true, force: true }).catch(() => undefined);
843
+ this.emit({ stage: "cleanup", runId: run.runId, jobId: run.jobHandle?.id, cleanupSucceeded: true });
844
+ }
845
+ emit(event) {
846
+ try {
847
+ this.observer.emit(event);
848
+ }
849
+ catch {
850
+ // Telemetry cannot change a run's security or terminal behavior.
851
+ }
852
+ }
853
+ terminal(run, outcome, audit) {
854
+ this.emit({
855
+ stage: "terminal",
856
+ runId: run.runId,
857
+ jobId: run.jobHandle?.id,
858
+ outcome,
859
+ failureCategory: audit?.failureCategory,
860
+ diagnosticId: audit?.diagnosticId,
861
+ durationMs: this.duration(run.runId),
862
+ budgetActualUsd: run.costUsd,
863
+ ...(run.provider === "codex" || run.provider === "claude-code"
864
+ ? { workerProvider: run.provider, proxyProtocol: proxyProtocol(run.provider) }
865
+ : {}),
866
+ });
867
+ }
868
+ /**
869
+ * `cancelled` is passed by the one caller that knows — stop(). It is never
870
+ * inferred from the message: `failureCategory` substring-matches, and
871
+ * "cancel"/"Canceled" is routine phrasing in gRPC, Kubernetes, Docker and
872
+ * aborted-HTTP errors ("rpc error: code = Canceled desc = context canceled"),
873
+ * so inferring it would silence exactly the genuine failures this log exists
874
+ * to surface.
875
+ */
876
+ failure(error, options = {}) {
877
+ const category = failureCategory(error);
878
+ const diagnosticId = `coding_diag_${randomUUID()}`;
879
+ // The persisted error is deliberately opaque (it reaches the agent owner),
880
+ // so the operator needs the real reason somewhere: the control-plane log,
881
+ // keyed by the same diagnostic id. Token-shaped values are redacted, and a
882
+ // cause chain is kept because the outer message is often just a wrapper.
883
+ // A user asking to stop is not a failure, so it gets the same id at info.
884
+ const line = { diagnosticId, category, reason: describeFailure(error) };
885
+ if (options.cancelled === true)
886
+ containerLog.info(line, "coding run cancelled; the persisted error is its id only");
887
+ else
888
+ containerLog.warn(line, "coding run failed; the persisted error is the diagnostic id only");
889
+ return { error: `coding_failure_${category}:${diagnosticId}`, audit: { failureCategory: category, diagnosticId } };
890
+ }
891
+ duration(runId) {
892
+ const startedAt = this.startedAt.get(runId);
893
+ return startedAt === undefined ? undefined : Math.max(0, this.now().getTime() - startedAt);
894
+ }
895
+ }
896
+ /** Operator-facing failure text: the message plus its cause chain, redacted. */
897
+ export function describeFailure(error, depth = 0) {
898
+ if (depth > 4)
899
+ return "…";
900
+ const message = error instanceof Error ? error.message : typeof error === "string" ? error : "unknown";
901
+ const redacted = redactAndTruncate(message, 500);
902
+ const cause = error instanceof Error ? error.cause : undefined;
903
+ return cause === undefined || cause === null ? redacted : `${redacted} <- ${describeFailure(cause, depth + 1)}`;
904
+ }
905
+ function safeError(error) {
906
+ const message = error instanceof Error ? error.message : "unknown";
907
+ return `coding_executor:${redactAndTruncate(message, 200).replace(/[^A-Za-z0-9_.:-]/g, "_")}`;
908
+ }
909
+ function failureCategory(error) {
910
+ const message = safeError(error);
911
+ if (message.includes("preflight") || message.includes("ownership") || message.includes("image"))
912
+ return "preflight";
913
+ if (message.includes("budget"))
914
+ return "budget";
915
+ if (message.includes("artifact") || message.includes("result"))
916
+ return "artifact";
917
+ if (message.includes("workspace") || message.includes("git") || message.includes("vcs"))
918
+ return "workspace";
919
+ if (message.includes("job") || message.includes("container"))
920
+ return "job";
921
+ if (message.includes("cancel"))
922
+ return "cancelled";
923
+ return "executor";
924
+ }
925
+ function stableJson(value) {
926
+ if (Array.isArray(value))
927
+ return `[${value.map(stableJson).join(",")}]`;
928
+ if (value && typeof value === "object") {
929
+ const record = value;
930
+ return `{${Object.keys(record)
931
+ .sort()
932
+ .map((key) => `${JSON.stringify(key)}:${stableJson(record[key])}`)
933
+ .join(",")}}`;
934
+ }
935
+ return JSON.stringify(value) ?? "null";
936
+ }