@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,1027 @@
1
+ import { createHash } from "node:crypto";
2
+ import { spawn } from "node:child_process";
3
+ import { lstat, mkdir, mkdtemp, open, opendir, readdir, readFile, readlink, realpath, rename, rm, unlink, } from "node:fs/promises";
4
+ import { dirname, isAbsolute, join, resolve, sep } from "node:path";
5
+ import { logger } from "../../core/logger.js";
6
+ import { parseCodingAgentOutputJson, redactAndTruncate, MAX_CODING_ARTIFACT_BYTES } from "../../coding/protocol.js";
7
+ import { assertDockerHostSupportsIsolation, assertClaudeAgentContainerInspection, assertClaudeToolRunnerContainerInspection, assertIsolationNetworkInspection, assertKeeperContainerInspection, assertProxyContainerInspection, assertStorageVolumeInspection, assertWorkerContainerInspection, buildDockerIsolationPlan, } from "./docker-isolation.js";
8
+ import { replaceDirectoryFromStaging } from "./workspace-swap.js";
9
+ const BACKEND = "docker";
10
+ const STATE_SCHEMA_VERSION = 1;
11
+ const MAX_DOCKER_OUTPUT_BYTES = 1024 * 1024;
12
+ const MAX_DOCKER_SEED_DIAGNOSTIC_BYTES = 4 * 1024;
13
+ const MAX_WORKSPACE_ENTRIES = 100_000;
14
+ const TERMINAL_PHASES = new Set(["succeeded", "failed", "stopped", "lost", "removed"]);
15
+ export const SAFE_WORKER_DIAGNOSTIC = /^(?:worker_[a-z_]+|coding_[a-z_]+|wardby_[a-z_]+)$/;
16
+ const dockerLog = logger.child({ module: "docker-jobs" });
17
+ /**
18
+ * Docker failures never include daemon output in their public message -- that
19
+ * message reaches the agent owner. The operator still needs the reason, so the
20
+ * failing subcommand and a redacted, bounded slice of stderr ride along as the
21
+ * `cause`, which only the control-plane log walks (see describeFailure in
22
+ * ../executor/container.ts). Without it a provisioning failure is just
23
+ * `docker_command_failed:1`, which cost a live smoke two blind runs.
24
+ */
25
+ export class DockerCommandError extends Error {
26
+ exitCode;
27
+ notFound;
28
+ outputExceeded;
29
+ constructor(exitCode, notFound = false, outputExceeded = false, detail) {
30
+ super(outputExceeded ? "docker_output_limit" : `docker_command_failed:${exitCode ?? "spawn"}`, {
31
+ cause: detail === undefined
32
+ ? undefined
33
+ : new Error(`docker ${detail.subcommand ?? "?"}: ${redactAndTruncate(detail.stderr?.trim() || "(no stderr)", 2_048)}`),
34
+ });
35
+ this.exitCode = exitCode;
36
+ this.notFound = notFound;
37
+ this.outputExceeded = outputExceeded;
38
+ }
39
+ }
40
+ export function isMissingDockerResource(stderr) {
41
+ return /no such (?:container|network|volume|object)|network .+ not found|is not connected to network/i.test(stderr);
42
+ }
43
+ /** Runs Docker with argument arrays and an explicit, minimal environment. */
44
+ export class NodeDockerCommandRunner {
45
+ options;
46
+ dockerBinary;
47
+ path;
48
+ constructor(options) {
49
+ this.options = options;
50
+ this.dockerBinary = options.dockerBinary ?? "docker";
51
+ this.path = options.path ?? "/usr/local/bin:/usr/bin:/bin";
52
+ }
53
+ async run(args, options = {}) {
54
+ if (args.some((argument) => argument.includes("\0")))
55
+ throw new Error("docker_argument_invalid");
56
+ const extraEnv = options.env ?? {};
57
+ if (Object.keys(extraEnv).some((key) => key !== "WARDBY_RUN_CAPABILITY"))
58
+ throw new Error("docker_environment_invalid");
59
+ const env = {
60
+ PATH: this.path,
61
+ HOME: this.options.homeDir,
62
+ LANG: "C",
63
+ LC_ALL: "C",
64
+ ...(this.options.dockerHost ? { DOCKER_HOST: this.options.dockerHost } : {}),
65
+ ...(this.options.dockerContext ? { DOCKER_CONTEXT: this.options.dockerContext } : {}),
66
+ ...extraEnv,
67
+ };
68
+ const maxBytes = options.maxOutputBytes ?? MAX_DOCKER_OUTPUT_BYTES;
69
+ return new Promise((resolvePromise, rejectPromise) => {
70
+ const child = spawn(this.dockerBinary, [...args], { env, shell: false, stdio: ["ignore", "pipe", "pipe"] });
71
+ if (!child.stdout || !child.stderr) {
72
+ child.kill("SIGKILL");
73
+ rejectPromise(new DockerCommandError(null));
74
+ return;
75
+ }
76
+ const stdout = [];
77
+ const stderr = [];
78
+ let bytes = 0;
79
+ let exceeded = false;
80
+ let spawnFailed = false;
81
+ const capture = (chunks) => (chunk) => {
82
+ bytes += chunk.length;
83
+ if (bytes > maxBytes) {
84
+ exceeded = true;
85
+ child.kill("SIGKILL");
86
+ return;
87
+ }
88
+ chunks.push(chunk);
89
+ };
90
+ child.stdout.on("data", capture(stdout));
91
+ child.stderr.on("data", capture(stderr));
92
+ child.once("error", () => {
93
+ spawnFailed = true;
94
+ });
95
+ child.once("close", (code) => {
96
+ if (exceeded)
97
+ return rejectPromise(new DockerCommandError(code, false, true));
98
+ if (spawnFailed)
99
+ return rejectPromise(new DockerCommandError(null));
100
+ const result = {
101
+ stdout: Buffer.concat(stdout).toString("utf8"),
102
+ stderr: Buffer.concat(stderr).toString("utf8"),
103
+ };
104
+ if (code !== 0) {
105
+ return rejectPromise(new DockerCommandError(code, isMissingDockerResource(result.stderr), false, {
106
+ subcommand: args[0],
107
+ stderr: result.stderr,
108
+ }));
109
+ }
110
+ resolvePromise(result);
111
+ });
112
+ });
113
+ }
114
+ }
115
+ export function dockerTransferEnvironment(path) {
116
+ // Docker CLI needs a home directory for its isolated client configuration.
117
+ return { PATH: path, HOME: "/tmp", LANG: "C", LC_ALL: "C", COPYFILE_DISABLE: "1" };
118
+ }
119
+ function dockerSeedFailure(stage, exitCode, stderr) {
120
+ const output = Buffer.concat(stderr).toString("utf8");
121
+ let diagnostic = exitCode === null ? "spawn" : `exit_${exitCode}`;
122
+ if (/permission denied/i.test(output))
123
+ diagnostic = "permission_denied";
124
+ else if (/executable file not found|command not found/i.test(output))
125
+ diagnostic = "command_missing";
126
+ else if (/no such file or directory/i.test(output))
127
+ diagnostic = "path_missing";
128
+ // CI enables this only for the synthetic Vitest fixture, never for production runs.
129
+ const debugOutput = process.env.NODE_ENV === "test" && process.env.WARDBY_DOCKER_SEED_DEBUG === "1";
130
+ const details = debugOutput ? `:stderr=${JSON.stringify(output.trim() || "<empty>")}` : "";
131
+ return new Error(`docker_seed_failed:${stage}:${diagnostic}${details}`);
132
+ }
133
+ /** Streams tar archives into the unprivileged keeper; no host bind mounts are used. */
134
+ export class NodeDockerArtifactTransfer {
135
+ dockerBinary;
136
+ path;
137
+ constructor(dockerBinary = "docker", path = "/usr/local/bin:/usr/bin:/bin") {
138
+ this.dockerBinary = dockerBinary;
139
+ this.path = path;
140
+ }
141
+ async seedDirectory(sourceDirectory, container, destination) {
142
+ const metadata = await lstat(sourceDirectory);
143
+ if (!metadata.isDirectory() || metadata.isSymbolicLink())
144
+ throw new Error("docker_seed_directory_invalid");
145
+ await new Promise((resolvePromise, rejectPromise) => {
146
+ const archive = spawn("tar", ["-C", sourceDirectory, "-cf", "-", "."], {
147
+ env: dockerTransferEnvironment(this.path),
148
+ shell: false,
149
+ stdio: ["ignore", "pipe", "pipe"],
150
+ });
151
+ const extract = spawn(this.dockerBinary, [
152
+ "container",
153
+ "exec",
154
+ "--interactive",
155
+ "--user",
156
+ "10001:10001",
157
+ "--workdir",
158
+ "/",
159
+ container,
160
+ "tar",
161
+ "-C",
162
+ destination,
163
+ "--no-same-owner",
164
+ "--no-same-permissions",
165
+ "-xf",
166
+ "-",
167
+ ], { env: dockerTransferEnvironment(this.path), shell: false, stdio: ["pipe", "ignore", "pipe"] });
168
+ let failed = false;
169
+ let archiveExit;
170
+ let extractExit;
171
+ const archiveStderr = [];
172
+ const extractStderr = [];
173
+ const capture = (chunks) => {
174
+ let capturedBytes = 0;
175
+ return (chunk) => {
176
+ const remaining = MAX_DOCKER_SEED_DIAGNOSTIC_BYTES - capturedBytes;
177
+ if (remaining <= 0)
178
+ return;
179
+ const captured = chunk.subarray(0, remaining);
180
+ chunks.push(captured);
181
+ capturedBytes += captured.length;
182
+ };
183
+ };
184
+ const fail = (error) => {
185
+ if (failed)
186
+ return;
187
+ failed = true;
188
+ archive.kill("SIGKILL");
189
+ extract.kill("SIGKILL");
190
+ rejectPromise(error);
191
+ };
192
+ const finish = () => {
193
+ if (failed || archiveExit === undefined || extractExit === undefined)
194
+ return;
195
+ if (archiveExit !== 0)
196
+ return fail(dockerSeedFailure("archive", archiveExit, archiveStderr));
197
+ if (extractExit !== 0)
198
+ return fail(dockerSeedFailure("extract", extractExit, extractStderr));
199
+ resolvePromise();
200
+ };
201
+ archive.once("error", () => fail(dockerSeedFailure("archive", null, archiveStderr)));
202
+ extract.once("error", () => fail(dockerSeedFailure("extract", null, extractStderr)));
203
+ if (!archive.stdout || !archive.stderr || !extract.stdin || !extract.stderr)
204
+ return fail(dockerSeedFailure("extract", null, extractStderr));
205
+ archive.stdout.pipe(extract.stdin);
206
+ archive.stderr.on("data", capture(archiveStderr));
207
+ extract.stderr.on("data", capture(extractStderr));
208
+ archive.stdout.once("error", () => fail(dockerSeedFailure("archive", null, archiveStderr)));
209
+ extract.stdin.once("error", () => fail(dockerSeedFailure("extract", null, extractStderr)));
210
+ archive.once("close", (code) => {
211
+ archiveExit = code;
212
+ finish();
213
+ });
214
+ extract.once("close", (code) => {
215
+ extractExit = code;
216
+ finish();
217
+ });
218
+ });
219
+ }
220
+ async seedInput(sourceFile, container) {
221
+ const metadata = await lstat(sourceFile);
222
+ if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.size > MAX_CODING_ARTIFACT_BYTES) {
223
+ throw new Error("docker_input_artifact_invalid");
224
+ }
225
+ const docker = new NodeDockerCommandRunner({
226
+ dockerBinary: this.dockerBinary,
227
+ homeDir: "/tmp",
228
+ path: this.path,
229
+ });
230
+ await docker.run(["container", "cp", sourceFile, `${container}:/run/wardby/storage/input/input.json`]);
231
+ }
232
+ async materializeDirectory(container, source, destination, maxBytes) {
233
+ await replaceDirectoryFromStaging(destination, maxBytes, "docker_workspace_destination_invalid", async (staging) => {
234
+ await new NodeDockerCommandRunner({ dockerBinary: this.dockerBinary, homeDir: "/tmp", path: this.path }).run([
235
+ "container",
236
+ "cp",
237
+ `${container}:${source}/.`,
238
+ staging,
239
+ ]);
240
+ });
241
+ }
242
+ }
243
+ export async function validateMaterializedWorkspace(root, maxBytes) {
244
+ if (!Number.isSafeInteger(maxBytes) || maxBytes <= 0)
245
+ throw new Error("docker_workspace_limit_invalid");
246
+ let entries = 0;
247
+ let bytes = 0;
248
+ const visit = async (directory) => {
249
+ const handle = await opendir(directory);
250
+ for await (const entry of handle) {
251
+ entries += 1;
252
+ if (entries > MAX_WORKSPACE_ENTRIES)
253
+ throw new Error("docker_workspace_entry_limit");
254
+ if (entry.name.toLowerCase() === ".git")
255
+ throw new Error("docker_workspace_nested_repository");
256
+ const fullPath = resolve(directory, entry.name);
257
+ const metadata = await lstat(fullPath);
258
+ if (metadata.isSymbolicLink()) {
259
+ const target = await readlink(fullPath);
260
+ const resolvedTarget = resolve(directory, target);
261
+ if (isAbsolute(target) || (resolvedTarget !== root && !resolvedTarget.startsWith(`${root}${sep}`))) {
262
+ throw new Error("docker_workspace_symlink_escape");
263
+ }
264
+ }
265
+ else if (metadata.isDirectory()) {
266
+ await visit(fullPath);
267
+ }
268
+ else if (metadata.isFile()) {
269
+ bytes += metadata.size;
270
+ if (bytes > maxBytes)
271
+ throw new Error("docker_workspace_size_limit");
272
+ }
273
+ else {
274
+ throw new Error("docker_workspace_special_file");
275
+ }
276
+ }
277
+ };
278
+ await visit(root);
279
+ }
280
+ function hash(value) {
281
+ return createHash("sha256").update(value).digest("hex");
282
+ }
283
+ function stableSpecHash(spec) {
284
+ return hash(JSON.stringify({
285
+ ...spec,
286
+ labels: Object.fromEntries(Object.entries(spec.labels).sort(([left], [right]) => left.localeCompare(right))),
287
+ }));
288
+ }
289
+ function clone(value) {
290
+ return structuredClone(value);
291
+ }
292
+ function isTerminal(record) {
293
+ return TERMINAL_PHASES.has(record.phase);
294
+ }
295
+ function statusFor(record) {
296
+ switch (record.phase) {
297
+ case "provisioning":
298
+ return { state: "pending" };
299
+ case "active":
300
+ return { state: "running" };
301
+ case "succeeded":
302
+ return { state: "succeeded" };
303
+ case "failed":
304
+ return { state: "failed", ...(record.result?.reason === "timed_out" ? { reason: "timed_out" } : {}) };
305
+ case "stopped":
306
+ return { state: "stopped" };
307
+ case "lost":
308
+ return { state: "lost" };
309
+ case "removed":
310
+ throw new Error("job_removed");
311
+ }
312
+ }
313
+ function resultFor(phase) {
314
+ switch (phase) {
315
+ case "succeeded":
316
+ return { exitCode: 0, reason: "completed" };
317
+ case "failed":
318
+ return { exitCode: 1, reason: "failed" };
319
+ case "stopped":
320
+ return { exitCode: 143, reason: "stopped" };
321
+ case "lost":
322
+ return { exitCode: 1, reason: "lost" };
323
+ }
324
+ }
325
+ function ensureCapability(value) {
326
+ if (!/^rrp_[A-Za-z0-9_-]{16,512}$/.test(value))
327
+ throw new Error("docker_capability_invalid");
328
+ return value;
329
+ }
330
+ function appendLabels(args, labels) {
331
+ const entrypointIndex = args.indexOf("--entrypoint");
332
+ const imageIndex = entrypointIndex >= 0 ? entrypointIndex + 2 : args.length - 1;
333
+ if (!args[imageIndex])
334
+ throw new Error("docker_plan_invalid");
335
+ const trusted = Object.entries(labels).flatMap(([key, value]) => ["--label", `${key}=${value}`]);
336
+ return [...args.slice(0, imageIndex), ...trusted, ...args.slice(imageIndex)];
337
+ }
338
+ function stateFileName(runId) {
339
+ return `${hash(runId)}.json`;
340
+ }
341
+ function resourceLabels(record) {
342
+ return {
343
+ "io.wardby.managed": "true",
344
+ "io.wardby.component": "coding-worker",
345
+ "io.wardby.run-sha256": hash(record.runId),
346
+ "io.wardby.run-id": record.runId,
347
+ "io.wardby.job-id": record.jobId,
348
+ "io.wardby.spec-sha256": record.specHash,
349
+ "io.wardby.created-at": new Date(record.createdAt).toISOString(),
350
+ "io.wardby.schema": String(STATE_SCHEMA_VERSION),
351
+ };
352
+ }
353
+ function labelsMatch(actual, expected) {
354
+ return Object.entries(expected).every(([key, value]) => actual?.[key] === value);
355
+ }
356
+ function capabilityFromInspection(container) {
357
+ return container.Config?.Env?.find((value) => value.startsWith("WARDBY_RUN_CAPABILITY="))?.slice("WARDBY_RUN_CAPABILITY=".length);
358
+ }
359
+ function dockerState(inspection) {
360
+ const state = inspection.State;
361
+ if (!state)
362
+ return undefined;
363
+ if (state.Running || state.Status === "restarting")
364
+ return "active";
365
+ if (state.Status === "created")
366
+ return "provisioning";
367
+ if (state.Status === "exited") {
368
+ if (state.OOMKilled)
369
+ return "failed";
370
+ return state.ExitCode === 0 ? "succeeded" : "failed";
371
+ }
372
+ return undefined;
373
+ }
374
+ function terminalResult(phase, inspection) {
375
+ if (phase === "succeeded")
376
+ return { exitCode: 0, reason: "completed" };
377
+ if (phase === "failed")
378
+ return {
379
+ exitCode: inspection?.State?.OOMKilled ? 137 : Math.max(1, inspection?.State?.ExitCode ?? 1),
380
+ reason: "failed",
381
+ };
382
+ if (phase === "stopped" || phase === "lost")
383
+ return resultFor(phase);
384
+ return undefined;
385
+ }
386
+ export class DockerJobLauncher {
387
+ options;
388
+ stateRoot;
389
+ workspaceRoot;
390
+ docker;
391
+ transfer;
392
+ now;
393
+ locks = new Map();
394
+ timers = new Map();
395
+ ready;
396
+ constructor(options) {
397
+ this.options = options;
398
+ this.stateRoot = resolve(options.stateRoot);
399
+ this.workspaceRoot = resolve(options.workspaceRoot);
400
+ if (this.stateRoot === resolve("/") || this.workspaceRoot === resolve("/"))
401
+ throw new Error("docker_launcher_path_invalid");
402
+ this.docker = options.docker ?? new NodeDockerCommandRunner({ homeDir: join(this.stateRoot, ".home") });
403
+ this.transfer = options.transfer ?? new NodeDockerArtifactTransfer();
404
+ this.now = options.now ?? Date.now;
405
+ this.ready = this.initialize();
406
+ }
407
+ async launch(spec) {
408
+ await this.ready;
409
+ return this.withRun(spec.runId, async () => this.launchLocked(spec));
410
+ }
411
+ async status(handle) {
412
+ await this.ready;
413
+ const record = await this.findRecord(handle);
414
+ if (!record)
415
+ throw new Error("job_not_found");
416
+ return this.withRun(record.runId, async () => {
417
+ const current = await this.readRecord(record.runId);
418
+ if (!current)
419
+ throw new Error("job_not_found");
420
+ if (current.phase === "removed")
421
+ throw new Error("job_removed");
422
+ await this.refresh(current);
423
+ return clone(statusFor(current));
424
+ });
425
+ }
426
+ async collect(handle) {
427
+ await this.ready;
428
+ const record = await this.findRecord(handle);
429
+ if (!record)
430
+ throw new Error("job_not_found");
431
+ return this.withRun(record.runId, async () => {
432
+ const current = await this.requireRecord(record.runId);
433
+ if (current.phase === "removed")
434
+ throw new Error("job_removed");
435
+ await this.refresh(current);
436
+ if (!isTerminal(current))
437
+ throw new Error("job_not_terminal");
438
+ if (!current.result)
439
+ current.result = resultFor(current.phase);
440
+ if (current.phase === "failed" && !current.result.diagnostic) {
441
+ const diagnostic = await this.readWorkerFailureDiagnostic(current);
442
+ if (diagnostic) {
443
+ current.result = { ...current.result, diagnostic };
444
+ await this.writeRecord(current);
445
+ }
446
+ }
447
+ if (current.phase === "succeeded" && !current.result.resultArtifact) {
448
+ const artifact = await this.readResultArtifact(current);
449
+ current.result = { ...current.result, resultArtifact: artifact };
450
+ await this.writeRecord(current);
451
+ }
452
+ return clone(current.result);
453
+ });
454
+ }
455
+ async materializeWorkspace(handle, destination) {
456
+ await this.ready;
457
+ const record = await this.findRecord(handle);
458
+ if (!record)
459
+ throw new Error("job_not_found");
460
+ await this.withRun(record.runId, async () => {
461
+ const current = await this.requireRecord(record.runId);
462
+ await this.refresh(current);
463
+ if (current.phase !== "succeeded")
464
+ throw new Error("job_not_succeeded");
465
+ const expected = resolve(this.workspaceRoot, current.runId, "workspace");
466
+ if (resolve(destination) !== expected || !expected.startsWith(`${this.workspaceRoot}${sep}`)) {
467
+ throw new Error("docker_workspace_destination_invalid");
468
+ }
469
+ await this.transfer.materializeDirectory(this.keeperName(current), "/run/wardby/storage/workspace", expected, current.spec.limits.diskMb * 1024 * 1024);
470
+ });
471
+ }
472
+ async stop(handle, _reason) {
473
+ await this.ready;
474
+ const record = await this.findRecord(handle);
475
+ if (!record)
476
+ return;
477
+ await this.withRun(record.runId, async () => {
478
+ const current = await this.readRecord(record.runId);
479
+ if (!current || isTerminal(current))
480
+ return;
481
+ await this.stopRecord(current, false);
482
+ });
483
+ }
484
+ async remove(handle) {
485
+ await this.ready;
486
+ const record = await this.findRecord(handle);
487
+ if (!record)
488
+ return;
489
+ await this.withRun(record.runId, async () => {
490
+ const current = await this.requireRecord(record.runId);
491
+ if (current.phase === "removed")
492
+ return;
493
+ await this.refresh(current);
494
+ if (!isTerminal(current))
495
+ throw new Error("job_not_terminal");
496
+ await this.cleanupResources(current);
497
+ current.phase = "removed";
498
+ await this.writeRecord(current);
499
+ this.clearDeadline(current);
500
+ });
501
+ }
502
+ async initialize() {
503
+ await mkdir(this.stateRoot, { recursive: true, mode: 0o700 });
504
+ await mkdir(join(this.stateRoot, ".home"), { recursive: true, mode: 0o700 });
505
+ await realpath(this.stateRoot);
506
+ const records = await this.allRecords();
507
+ for (const record of records) {
508
+ if (record.phase === "removed")
509
+ continue;
510
+ try {
511
+ if (record.deadlineAt <= this.now()) {
512
+ const active = await this.options.isRunActive(record.runId, clone(record.handle));
513
+ if (active)
514
+ await this.withRun(record.runId, async () => this.stopRecord(record, true));
515
+ else
516
+ await this.withRun(record.runId, async () => this.sweepExpired(record));
517
+ }
518
+ else if (!isTerminal(record)) {
519
+ this.scheduleDeadline(record);
520
+ }
521
+ }
522
+ catch (error) {
523
+ // One record's startup recovery must never abort the whole launcher:
524
+ // a stale container/network another process already reaped, a
525
+ // transient daemon hiccup, or an unrecognized Docker error string
526
+ // should surface as a logged recovery failure, not crash the server.
527
+ dockerLog.warn({ err: error, runId: record.runId }, "docker_startup_recovery_failed");
528
+ }
529
+ }
530
+ }
531
+ async launchLocked(spec) {
532
+ const plan = buildDockerIsolationPlan(spec, this.options.proxyContainer);
533
+ const specHash = stableSpecHash(spec);
534
+ const existing = await this.readRecord(spec.runId);
535
+ if (existing) {
536
+ if (existing.specHash !== specHash)
537
+ throw new Error("job_spec_conflict");
538
+ if (existing.phase === "removed" || isTerminal(existing))
539
+ return clone(existing.handle);
540
+ if (existing.phase === "provisioning") {
541
+ // Provisioning can create some resources before a host crash. Never
542
+ // guess which side effects completed or launch another agent pair.
543
+ await this.cleanupResources(existing).catch(() => undefined);
544
+ existing.phase = "lost";
545
+ existing.result = resultFor("lost");
546
+ await this.writeRecord(existing);
547
+ this.clearDeadline(existing);
548
+ return clone(existing.handle);
549
+ }
550
+ const capability = ensureCapability(await this.options.resolveCapability(spec.runId));
551
+ if (hash(capability) !== existing.capabilityHash)
552
+ throw new Error("docker_capability_changed");
553
+ if (existing.phase === "active") {
554
+ await this.refresh(existing);
555
+ return clone(existing.handle);
556
+ }
557
+ return this.provision(existing, plan, capability);
558
+ }
559
+ await this.validateInputArtifact(spec.inputArtifact);
560
+ const capability = ensureCapability(await this.options.resolveCapability(spec.runId));
561
+ const record = {
562
+ schemaVersion: STATE_SCHEMA_VERSION,
563
+ runId: spec.runId,
564
+ spec: clone(spec),
565
+ specHash,
566
+ handle: { backend: BACKEND, id: plan.names.workerContainer },
567
+ jobId: `job-${hash(`${spec.runId}:${specHash}`).slice(0, 24)}`,
568
+ createdAt: this.now(),
569
+ deadlineAt: this.now() + plan.deadlineMs,
570
+ capabilityHash: hash(capability),
571
+ phase: "provisioning",
572
+ };
573
+ // This durable record is the idempotency fence before any Docker side effect.
574
+ await this.writeRecord(record);
575
+ try {
576
+ return await this.provision(record, plan, capability);
577
+ }
578
+ catch (error) {
579
+ await this.options
580
+ .onProvisionFailure?.({ runId: record.runId, keeperContainer: plan.names.keeperContainer })
581
+ .catch(() => undefined);
582
+ await this.cleanupResources(record).catch(() => undefined);
583
+ record.phase = "lost";
584
+ record.result = resultFor("lost");
585
+ await this.writeRecord(record).catch(() => undefined);
586
+ throw error;
587
+ }
588
+ }
589
+ async provision(record, plan, capability) {
590
+ await this.assertHost();
591
+ const labels = resourceLabels(record);
592
+ await this.createAndAssert(appendLabels(plan.networkCreateArgs, labels), ["network", "inspect", plan.names.network], (value) => {
593
+ const inspected = value;
594
+ assertIsolationNetworkInspection(inspected, record.runId);
595
+ if (!labelsMatch(inspected.Labels, labels))
596
+ throw new Error("docker_resource_attestation_failed");
597
+ });
598
+ await this.createAndAssert(appendLabels(plan.storageVolumeCreateArgs, labels), ["volume", "inspect", plan.names.storageVolume], (value) => {
599
+ const inspected = value;
600
+ assertStorageVolumeInspection(inspected, record.spec);
601
+ if (!labelsMatch(inspected.Labels, labels))
602
+ throw new Error("docker_resource_attestation_failed");
603
+ });
604
+ await this.createAndAssert(appendLabels(plan.keeperCreateArgs, labels), ["container", "inspect", plan.names.keeperContainer], (value) => {
605
+ const inspected = value;
606
+ assertKeeperContainerInspection(inspected, record.spec);
607
+ if (!labelsMatch(inspected.Config?.Labels, labels))
608
+ throw new Error("docker_resource_attestation_failed");
609
+ });
610
+ await this.startContainer(plan.names.keeperContainer);
611
+ await this.waitForStorage(plan.names.keeperContainer);
612
+ await this.seedRun(record, plan.names.keeperContainer);
613
+ await this.run(plan.proxyNetworkConnectArgs);
614
+ const proxy = await this.inspect(["container", "inspect", this.options.proxyContainer]);
615
+ assertProxyContainerInspection(proxy, record.runId);
616
+ if (plan.toolCreateArgs) {
617
+ await this.createAndAssert(appendLabels(plan.toolCreateArgs, labels), ["container", "inspect", plan.names.toolContainer], (value) => {
618
+ assertClaudeToolRunnerContainerInspection(value, record.spec);
619
+ if (!labelsMatch(value.Config?.Labels, labels)) {
620
+ throw new Error("docker_resource_attestation_failed");
621
+ }
622
+ });
623
+ await this.startContainer(plan.names.toolContainer);
624
+ await this.waitForToolRunner(plan.names.toolContainer);
625
+ }
626
+ const workerArgs = appendLabels(plan.workerCreateArgs, labels);
627
+ await this.createAndAssert(workerArgs, ["container", "inspect", plan.names.workerContainer], (value) => {
628
+ const inspected = value;
629
+ if (record.spec.provider === "claude-code") {
630
+ assertClaudeAgentContainerInspection(inspected, record.spec, capability);
631
+ }
632
+ else {
633
+ assertWorkerContainerInspection(inspected, record.spec, capability);
634
+ }
635
+ if (!labelsMatch(inspected.Config?.Labels, labels))
636
+ throw new Error("docker_resource_attestation_failed");
637
+ }, { env: { WARDBY_RUN_CAPABILITY: capability } });
638
+ await this.startContainer(plan.names.workerContainer);
639
+ record.phase = "active";
640
+ await this.writeRecord(record);
641
+ this.scheduleDeadline(record);
642
+ return clone(record.handle);
643
+ }
644
+ async refresh(record) {
645
+ if (isTerminal(record))
646
+ return;
647
+ if (record.deadlineAt <= this.now()) {
648
+ await this.stopRecord(record, true);
649
+ return;
650
+ }
651
+ let inspection;
652
+ try {
653
+ inspection = await this.inspect(["container", "inspect", record.handle.id]);
654
+ }
655
+ catch (error) {
656
+ if (error instanceof DockerCommandError && error.notFound) {
657
+ record.phase = "lost";
658
+ record.result = resultFor("lost");
659
+ await this.writeRecord(record);
660
+ this.clearDeadline(record);
661
+ return;
662
+ }
663
+ throw new Error("docker_status_unavailable", { cause: error });
664
+ }
665
+ if (!labelsMatch(inspection.Config?.Labels, resourceLabels(record)))
666
+ throw new Error("docker_resource_attestation_failed");
667
+ const capability = capabilityFromInspection(inspection);
668
+ if (!capability || hash(capability) !== record.capabilityHash)
669
+ throw new Error("docker_resource_attestation_failed");
670
+ if (record.spec.provider === "claude-code") {
671
+ assertClaudeAgentContainerInspection(inspection, record.spec, capability);
672
+ const tool = await this.inspect([
673
+ "container",
674
+ "inspect",
675
+ buildDockerIsolationPlan(record.spec, this.options.proxyContainer).names.toolContainer,
676
+ ]);
677
+ assertClaudeToolRunnerContainerInspection(tool, record.spec);
678
+ if (tool.State?.Running !== true || tool.State?.Status !== "running") {
679
+ record.phase = "failed";
680
+ record.result = { exitCode: 1, reason: "failed", diagnostic: "worker_tool_runner_failed" };
681
+ await this.writeRecord(record);
682
+ this.clearDeadline(record);
683
+ return;
684
+ }
685
+ }
686
+ else {
687
+ assertWorkerContainerInspection(inspection, record.spec, capability);
688
+ }
689
+ const phase = dockerState(inspection);
690
+ if (!phase || phase === "provisioning" || phase === "active")
691
+ return;
692
+ record.phase = phase;
693
+ record.result = terminalResult(phase, inspection);
694
+ await this.writeRecord(record);
695
+ this.clearDeadline(record);
696
+ }
697
+ async stopRecord(record, timedOut) {
698
+ if (isTerminal(record))
699
+ return;
700
+ try {
701
+ await this.run(["container", "stop", "--time", "10", record.handle.id]);
702
+ }
703
+ catch (error) {
704
+ if (!(error instanceof DockerCommandError && error.notFound))
705
+ throw new Error("docker_stop_failed", { cause: error });
706
+ }
707
+ if (record.spec.provider === "claude-code") {
708
+ const tool = buildDockerIsolationPlan(record.spec, this.options.proxyContainer).names.toolContainer;
709
+ try {
710
+ await this.run(["container", "stop", "--time", "10", tool]);
711
+ }
712
+ catch (error) {
713
+ if (!(error instanceof DockerCommandError && error.notFound))
714
+ throw new Error("docker_stop_failed", { cause: error });
715
+ }
716
+ }
717
+ record.phase = timedOut ? "failed" : "stopped";
718
+ record.result = timedOut ? { exitCode: 124, reason: "timed_out" } : resultFor("stopped");
719
+ await this.writeRecord(record);
720
+ this.clearDeadline(record);
721
+ }
722
+ async sweepExpired(record) {
723
+ // The DB verifier ran immediately before this call; never sweep by label alone.
724
+ try {
725
+ await this.cleanupResources(record);
726
+ }
727
+ catch (error) {
728
+ if (!(error instanceof DockerCommandError && error.notFound))
729
+ throw error;
730
+ }
731
+ record.phase = "removed";
732
+ await this.writeRecord(record);
733
+ this.clearDeadline(record);
734
+ }
735
+ async cleanupResources(record) {
736
+ const plan = buildDockerIsolationPlan(record.spec, this.options.proxyContainer);
737
+ const labels = resourceLabels(record);
738
+ await this.removeContainerIfAttested(plan.names.workerContainer, labels);
739
+ if (plan.toolCreateArgs)
740
+ await this.removeContainerIfAttested(plan.names.toolContainer, labels);
741
+ await this.runIgnoreMissing(["network", "disconnect", plan.names.network, this.options.proxyContainer]);
742
+ await this.removeContainerIfAttested(plan.names.keeperContainer, labels);
743
+ await this.removeNetworkIfAttested(plan.names.network, labels);
744
+ await this.removeVolumeIfAttested(plan.names.storageVolume, labels);
745
+ }
746
+ async removeContainerIfAttested(name, labels) {
747
+ try {
748
+ const inspected = await this.inspect(["container", "inspect", name]);
749
+ if (!labelsMatch(inspected.Config?.Labels, labels))
750
+ throw new Error("docker_resource_attestation_failed");
751
+ await this.runIgnoreMissing(["container", "rm", "--force", name]);
752
+ }
753
+ catch (error) {
754
+ if (!(error instanceof DockerCommandError && error.notFound))
755
+ throw error;
756
+ }
757
+ }
758
+ async removeNetworkIfAttested(name, labels) {
759
+ try {
760
+ const inspected = await this.inspect(["network", "inspect", name]);
761
+ if (!labelsMatch(inspected.Labels, labels))
762
+ throw new Error("docker_resource_attestation_failed");
763
+ await this.runIgnoreMissing(["network", "rm", name]);
764
+ }
765
+ catch (error) {
766
+ if (!(error instanceof DockerCommandError && error.notFound))
767
+ throw error;
768
+ }
769
+ }
770
+ async removeVolumeIfAttested(name, labels) {
771
+ try {
772
+ const inspected = await this.inspect(["volume", "inspect", name]);
773
+ if (!labelsMatch(inspected.Labels, labels))
774
+ throw new Error("docker_resource_attestation_failed");
775
+ await this.runIgnoreMissing(["volume", "rm", name]);
776
+ }
777
+ catch (error) {
778
+ if (!(error instanceof DockerCommandError && error.notFound))
779
+ throw error;
780
+ }
781
+ }
782
+ async seedRun(record, keeper) {
783
+ const runRoot = resolve(this.workspaceRoot, record.runId);
784
+ if (!runRoot.startsWith(`${this.workspaceRoot}/`))
785
+ throw new Error("docker_workspace_path_invalid");
786
+ await this.transfer.seedDirectory(resolve(runRoot, "workspace"), keeper, "/run/wardby/storage/workspace");
787
+ await this.transfer.seedDirectory(resolve(runRoot, "git"), keeper, "/run/wardby/storage/git");
788
+ await this.transfer.seedInput(record.spec.inputArtifact, keeper);
789
+ }
790
+ async readResultArtifact(record) {
791
+ const staging = await mkdtemp(join(this.stateRoot, ".wardby-result-"));
792
+ const target = join(staging, "result.json");
793
+ try {
794
+ await this.run(["container", "cp", `${this.keeperName(record)}:/run/wardby/storage/output/result.json`, target]);
795
+ const metadata = await lstat(target);
796
+ if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.size > MAX_CODING_ARTIFACT_BYTES) {
797
+ throw new Error("docker_result_artifact_invalid");
798
+ }
799
+ const raw = await readFile(target, "utf8");
800
+ if (Buffer.byteLength(raw, "utf8") > MAX_CODING_ARTIFACT_BYTES)
801
+ throw new Error("docker_result_artifact_invalid");
802
+ const output = parseCodingAgentOutputJson(raw);
803
+ if (output.runId !== record.runId)
804
+ throw new Error("docker_result_run_mismatch");
805
+ return JSON.stringify(output);
806
+ }
807
+ catch (error) {
808
+ if (error instanceof Error && error.message === "docker_result_run_mismatch")
809
+ throw error;
810
+ throw new Error("docker_result_artifact_invalid", { cause: error });
811
+ }
812
+ finally {
813
+ await rm(staging, { recursive: true, force: true });
814
+ }
815
+ }
816
+ async readWorkerFailureDiagnostic(record) {
817
+ try {
818
+ const logs = await this.run(["container", "logs", "--tail", "8", record.handle.id]);
819
+ const raw = `${logs.stdout}\n${logs.stderr}`;
820
+ for (const line of raw.split("\n").reverse()) {
821
+ try {
822
+ const value = JSON.parse(line);
823
+ if (typeof value.error === "string" && SAFE_WORKER_DIAGNOSTIC.test(value.error))
824
+ return value.error;
825
+ }
826
+ catch {
827
+ // Worker output is untrusted; only parse one fixed JSON shape.
828
+ }
829
+ }
830
+ }
831
+ catch {
832
+ // Diagnostics are optional and must never affect terminal cleanup.
833
+ }
834
+ return undefined;
835
+ }
836
+ keeperName(record) {
837
+ return buildDockerIsolationPlan(record.spec, this.options.proxyContainer).names.keeperContainer;
838
+ }
839
+ async assertHost() {
840
+ const info = await this.inspect(["info", "--format", "{{json .}}"]);
841
+ assertDockerHostSupportsIsolation(info);
842
+ }
843
+ async createAndAssert(createArgs, inspectArgs, assert, options) {
844
+ try {
845
+ await this.run(createArgs, options);
846
+ }
847
+ catch {
848
+ // A retry may find a resource created before the previous process crashed.
849
+ }
850
+ assert(await this.inspect(inspectArgs));
851
+ }
852
+ async waitForStorage(keeper) {
853
+ // The keeper emits this fixed marker only after creating every storage
854
+ // directory. Reading logs avoids consuming one of its 16 PID slots.
855
+ for (let attempt = 0; attempt < 100; attempt += 1) {
856
+ try {
857
+ const logs = await this.run(["container", "logs", "--tail", "8", keeper]);
858
+ if (`${logs.stdout}\n${logs.stderr}`.split("\n").includes("wardby_storage_ready"))
859
+ return;
860
+ }
861
+ catch {
862
+ // A just-started container can reject log reads before its log driver initializes.
863
+ }
864
+ await new Promise((resolvePromise) => setTimeout(resolvePromise, 50));
865
+ }
866
+ throw new Error("docker_storage_not_ready");
867
+ }
868
+ async waitForToolRunner(tool) {
869
+ // The runner prints this fixed marker only after binding its Unix socket.
870
+ // Reading logs avoids adding a process to its tightly bounded PID cgroup.
871
+ for (let attempt = 0; attempt < 100; attempt += 1) {
872
+ try {
873
+ const logs = await this.run(["container", "logs", "--tail", "8", tool]);
874
+ if (`${logs.stdout}\n${logs.stderr}`.split("\n").includes("wardby_tool_runner_ready"))
875
+ return;
876
+ }
877
+ catch {
878
+ // A just-started container can reject log reads before its log driver initializes.
879
+ }
880
+ await new Promise((resolvePromise) => setTimeout(resolvePromise, 50));
881
+ }
882
+ throw new Error("docker_tool_runner_not_ready");
883
+ }
884
+ async validateInputArtifact(path) {
885
+ const metadata = await lstat(path);
886
+ if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.size > MAX_CODING_ARTIFACT_BYTES) {
887
+ throw new Error("docker_input_artifact_invalid");
888
+ }
889
+ }
890
+ async run(args, options) {
891
+ return this.docker.run(args, options);
892
+ }
893
+ async runIgnoreMissing(args) {
894
+ try {
895
+ await this.run(args);
896
+ }
897
+ catch (error) {
898
+ if (!(error instanceof DockerCommandError && error.notFound))
899
+ throw error;
900
+ }
901
+ }
902
+ async startContainer(name) {
903
+ try {
904
+ await this.run(["container", "start", name]);
905
+ }
906
+ catch (error) {
907
+ if (!(error instanceof DockerCommandError))
908
+ throw error;
909
+ const inspection = await this.inspect(["container", "inspect", name]);
910
+ if (inspection.State?.Running !== true)
911
+ throw new Error("docker_start_failed", { cause: error });
912
+ }
913
+ }
914
+ async inspect(args) {
915
+ const output = (await this.run(args, { maxOutputBytes: MAX_DOCKER_OUTPUT_BYTES })).stdout;
916
+ let decoded;
917
+ try {
918
+ decoded = JSON.parse(output);
919
+ }
920
+ catch {
921
+ throw new Error("docker_inspection_invalid");
922
+ }
923
+ return (Array.isArray(decoded) ? decoded[0] : decoded);
924
+ }
925
+ async findRecord(handle) {
926
+ if (handle.backend !== BACKEND)
927
+ throw new Error("job_backend_mismatch");
928
+ return (await this.allRecords()).find((record) => record.handle.id === handle.id);
929
+ }
930
+ async requireRecord(runId) {
931
+ const record = await this.readRecord(runId);
932
+ if (!record)
933
+ throw new Error("job_not_found");
934
+ return record;
935
+ }
936
+ async readRecord(runId) {
937
+ try {
938
+ const decoded = JSON.parse(await readFile(join(this.stateRoot, stateFileName(runId)), "utf8"));
939
+ if (decoded.schemaVersion !== STATE_SCHEMA_VERSION ||
940
+ decoded.runId !== runId ||
941
+ decoded.handle?.backend !== BACKEND ||
942
+ !decoded.handle.id ||
943
+ !decoded.specHash ||
944
+ !decoded.capabilityHash) {
945
+ throw new Error("docker_state_invalid");
946
+ }
947
+ return decoded;
948
+ }
949
+ catch (error) {
950
+ if (error.code === "ENOENT")
951
+ return undefined;
952
+ throw error;
953
+ }
954
+ }
955
+ async allRecords() {
956
+ const entries = await readdir(this.stateRoot, { withFileTypes: true });
957
+ if (entries.length > 10_000)
958
+ throw new Error("docker_state_limit");
959
+ const records = await Promise.all(entries
960
+ .filter((entry) => entry.isFile() && /^[a-f0-9]{64}\.json$/.test(entry.name))
961
+ .map(async (entry) => JSON.parse(await readFile(join(this.stateRoot, entry.name), "utf8"))));
962
+ if (records.some((record) => record.schemaVersion !== STATE_SCHEMA_VERSION || stateFileName(record.runId) !== hash(record.runId) + ".json")) {
963
+ throw new Error("docker_state_invalid");
964
+ }
965
+ return records;
966
+ }
967
+ async writeRecord(record) {
968
+ const path = join(this.stateRoot, stateFileName(record.runId));
969
+ const temporary = `${path}.${process.pid}.${Date.now()}.tmp`;
970
+ const file = await open(temporary, "wx", 0o600);
971
+ try {
972
+ await file.writeFile(`${JSON.stringify(record)}\n`);
973
+ await file.sync();
974
+ await file.close();
975
+ await rename(temporary, path);
976
+ const directory = await open(dirname(path), "r");
977
+ try {
978
+ await directory.sync();
979
+ }
980
+ finally {
981
+ await directory.close();
982
+ }
983
+ }
984
+ catch (error) {
985
+ await file.close().catch(() => undefined);
986
+ await unlink(temporary).catch(() => undefined);
987
+ throw error;
988
+ }
989
+ }
990
+ async withRun(runId, action) {
991
+ const previous = this.locks.get(runId) ?? Promise.resolve();
992
+ let release = () => undefined;
993
+ const next = new Promise((resolvePromise) => {
994
+ release = resolvePromise;
995
+ });
996
+ const queued = previous.then(() => next);
997
+ this.locks.set(runId, queued);
998
+ await previous;
999
+ try {
1000
+ return await action();
1001
+ }
1002
+ finally {
1003
+ release();
1004
+ if (this.locks.get(runId) === queued)
1005
+ this.locks.delete(runId);
1006
+ }
1007
+ }
1008
+ scheduleDeadline(record) {
1009
+ this.clearDeadline(record);
1010
+ const delay = Math.max(0, record.deadlineAt - this.now());
1011
+ const timer = setTimeout(() => {
1012
+ void this.withRun(record.runId, async () => {
1013
+ const current = await this.readRecord(record.runId);
1014
+ if (current && !isTerminal(current) && current.deadlineAt <= this.now())
1015
+ await this.stopRecord(current, true);
1016
+ }).catch(() => undefined);
1017
+ }, delay);
1018
+ timer.unref();
1019
+ this.timers.set(record.runId, timer);
1020
+ }
1021
+ clearDeadline(record) {
1022
+ const timer = this.timers.get(record.runId);
1023
+ if (timer)
1024
+ clearTimeout(timer);
1025
+ this.timers.delete(record.runId);
1026
+ }
1027
+ }