@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,710 @@
1
+ import { spawn } from "node:child_process";
2
+ import { lstat, mkdir, opendir, readFile, readlink, realpath, rm } from "node:fs/promises";
3
+ import { isAbsolute, resolve, sep } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { MAX_REDACTED_SPAN, redactTokenShapedValues, normalizeGitHubRepository, normalizeGitRef, } from "../../coding/protocol.js";
6
+ import { isSafeGitHubInstallationToken } from "./github.js";
7
+ export const DEFAULT_MAX_CHANGED_FILES = 1_000;
8
+ export const DEFAULT_MAX_DIFF_BYTES = 1024 * 1024;
9
+ const MAX_GIT_OUTPUT_BYTES = 2 * 1024 * 1024;
10
+ const MAX_WORKSPACE_ENTRIES = 100_000;
11
+ const SAFE_COMMIT_SHA = /^[0-9a-f]{40}$/;
12
+ const SAFE_RUN_ID = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
13
+ const HARDENED_GIT_CONFIG = [
14
+ "-c",
15
+ "core.hooksPath=/dev/null",
16
+ "-c",
17
+ "commit.gpgSign=false",
18
+ "-c",
19
+ "tag.gpgSign=false",
20
+ "-c",
21
+ "credential.helper=",
22
+ "-c",
23
+ "protocol.allow=never",
24
+ "-c",
25
+ "protocol.https.allow=always",
26
+ ];
27
+ export class GitCommandError extends Error {
28
+ exitCode;
29
+ constructor(exitCode, detail) {
30
+ super(`git_command_failed:${exitCode ?? "spawn"}${detail ? `:${detail}` : ""}`);
31
+ this.exitCode = exitCode;
32
+ }
33
+ }
34
+ const GIT_OUTPUT_DISPLAY_BYTES = 8 * 1024;
35
+ export function redactGitOutput(value, secrets = []) {
36
+ // Only the first 8 KiB is ever kept, so redaction works on that plus one
37
+ // MAX_REDACTED_SPAN of margin rather than on all 2 MiB git may have produced
38
+ // (see redactAndTruncate: the margin is what keeps truncation safe). The rest
39
+ // is discarded here and never reaches a log or the database.
40
+ let output = value.slice(0, GIT_OUTPUT_DISPLAY_BYTES + MAX_REDACTED_SPAN);
41
+ for (const secret of secrets) {
42
+ if (secret)
43
+ output = output.split(secret).join("[REDACTED]");
44
+ }
45
+ return redactTokenShapedValues(output).slice(0, GIT_OUTPUT_DISPLAY_BYTES);
46
+ }
47
+ /** Runs Git without a shell or inherited application secrets. */
48
+ export class NodeGitCommandRunner {
49
+ options;
50
+ gitBinary;
51
+ path;
52
+ askPassPath;
53
+ constructor(options) {
54
+ this.options = options;
55
+ this.gitBinary = options.gitBinary ?? "git";
56
+ this.path = options.path ?? "/usr/bin:/bin";
57
+ this.askPassPath =
58
+ options.askPassPath ?? fileURLToPath(new URL("../../../scripts/git-askpass.sh", import.meta.url));
59
+ }
60
+ async run(args, options = {}) {
61
+ if (options.authToken !== undefined && !isSafeGitHubInstallationToken(options.authToken)) {
62
+ throw new Error("git_auth_token_invalid");
63
+ }
64
+ const env = {
65
+ PATH: this.path,
66
+ HOME: this.options.homeDir,
67
+ XDG_CONFIG_HOME: this.options.homeDir,
68
+ LANG: "C",
69
+ LC_ALL: "C",
70
+ GIT_CONFIG_NOSYSTEM: "1",
71
+ GIT_CONFIG_GLOBAL: "/dev/null",
72
+ GIT_CONFIG_SYSTEM: "/dev/null",
73
+ GIT_TERMINAL_PROMPT: "0",
74
+ GCM_INTERACTIVE: "never",
75
+ GIT_ASKPASS: options.authToken ? this.askPassPath : "/bin/false",
76
+ SSH_ASKPASS: "/bin/false",
77
+ GIT_LFS_SKIP_SMUDGE: "1",
78
+ };
79
+ const maxBytes = options.maxOutputBytes ?? MAX_GIT_OUTPUT_BYTES;
80
+ const secrets = options.authToken ? [options.authToken] : [];
81
+ return new Promise((resolvePromise, rejectPromise) => {
82
+ const child = spawn(this.gitBinary, [...args], {
83
+ cwd: options.cwd,
84
+ env,
85
+ shell: false,
86
+ stdio: ["ignore", "pipe", "pipe", options.authToken ? "pipe" : "ignore"],
87
+ });
88
+ const childStdout = child.stdout;
89
+ const childStderr = child.stderr;
90
+ if (!childStdout || !childStderr) {
91
+ child.kill("SIGKILL");
92
+ rejectPromise(new GitCommandError(null, "git_stdio_unavailable"));
93
+ return;
94
+ }
95
+ if (options.authToken) {
96
+ const authPipe = child.stdio[3];
97
+ authPipe.on("error", () => undefined);
98
+ authPipe.end(`${options.authToken}\n`);
99
+ }
100
+ const stdout = [];
101
+ const stderr = [];
102
+ let outputBytes = 0;
103
+ let outputExceeded = false;
104
+ let spawnError = false;
105
+ const capture = (chunks) => (chunk) => {
106
+ outputBytes += chunk.length;
107
+ if (outputBytes > maxBytes) {
108
+ outputExceeded = true;
109
+ child.kill("SIGKILL");
110
+ return;
111
+ }
112
+ chunks.push(chunk);
113
+ };
114
+ childStdout.on("data", capture(stdout));
115
+ childStderr.on("data", capture(stderr));
116
+ child.once("error", () => {
117
+ spawnError = true;
118
+ });
119
+ child.once("close", (code) => {
120
+ if (outputExceeded) {
121
+ rejectPromise(new GitCommandError(code, "git_output_limit"));
122
+ return;
123
+ }
124
+ if (spawnError) {
125
+ rejectPromise(new GitCommandError(null, "git_spawn_failed"));
126
+ return;
127
+ }
128
+ const result = {
129
+ stdout: Buffer.concat(stdout).toString("utf8"),
130
+ stderr: redactGitOutput(Buffer.concat(stderr).toString("utf8"), secrets),
131
+ };
132
+ if (code !== 0) {
133
+ rejectPromise(new GitCommandError(code, result.stderr.trim()));
134
+ return;
135
+ }
136
+ resolvePromise(result);
137
+ });
138
+ });
139
+ }
140
+ }
141
+ function validateProtectedPath(value) {
142
+ const path = value.trim();
143
+ if (!path ||
144
+ Buffer.byteLength(path, "utf8") > 512 ||
145
+ path.startsWith("/") ||
146
+ path.startsWith("./") ||
147
+ path.includes("\\") ||
148
+ path.split("/").some((part) => !part || part === "." || part === "..")) {
149
+ throw new Error("vcs_protected_path_invalid");
150
+ }
151
+ return path;
152
+ }
153
+ function globRegex(pattern) {
154
+ let source = "^";
155
+ for (let index = 0; index < pattern.length; index += 1) {
156
+ const character = pattern[index];
157
+ if (character === "*") {
158
+ if (pattern[index + 1] === "*") {
159
+ source += ".*";
160
+ index += 1;
161
+ }
162
+ else {
163
+ source += "[^/]*";
164
+ }
165
+ }
166
+ else if (character === "?") {
167
+ source += "[^/]";
168
+ }
169
+ else {
170
+ source += character.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
171
+ }
172
+ }
173
+ return new RegExp(`${source}$`);
174
+ }
175
+ function validateChangedPath(path) {
176
+ if (!path ||
177
+ isAbsolute(path) ||
178
+ path.includes("\\") ||
179
+ path.split("/").some((part) => !part || part === "." || part === "..")) {
180
+ throw new Error("vcs_changed_path_invalid");
181
+ }
182
+ return path;
183
+ }
184
+ function nullSeparated(value) {
185
+ const values = value.split("\0");
186
+ if (values.at(-1) === "")
187
+ values.pop();
188
+ return values.map(validateChangedPath);
189
+ }
190
+ function directChild(root, child) {
191
+ return child.startsWith(`${root}${sep}`) && !child.slice(root.length + 1).includes(sep);
192
+ }
193
+ /** Human-readable label for continuation status notifications -- falls back to the opaque run id alone when no agent name is known. */
194
+ function runLabel(agentName, runId) {
195
+ return agentName ? `${agentName} (wardby run ${runId})` : `wardby run ${runId}`;
196
+ }
197
+ export class GitVcsProvider {
198
+ options;
199
+ rootDir;
200
+ git;
201
+ maxChangedFiles;
202
+ maxDiffBytes;
203
+ cloneUrlForRepository;
204
+ constructor(options) {
205
+ this.options = options;
206
+ this.rootDir = resolve(options.rootDir);
207
+ if (this.rootDir === resolve("/"))
208
+ throw new Error("vcs_root_invalid");
209
+ this.git = options.git ?? new NodeGitCommandRunner({ homeDir: resolve(this.rootDir, ".home") });
210
+ this.maxChangedFiles = options.maxChangedFiles ?? DEFAULT_MAX_CHANGED_FILES;
211
+ this.maxDiffBytes = options.maxDiffBytes ?? DEFAULT_MAX_DIFF_BYTES;
212
+ if (!Number.isSafeInteger(this.maxChangedFiles) ||
213
+ this.maxChangedFiles <= 0 ||
214
+ !Number.isSafeInteger(this.maxDiffBytes) ||
215
+ this.maxDiffBytes <= 0) {
216
+ throw new Error("vcs_limits_invalid");
217
+ }
218
+ this.cloneUrlForRepository =
219
+ options.cloneUrlForRepository ?? ((repository) => `https://github.com/${repository}.git`);
220
+ }
221
+ async prepareWorkspace(input) {
222
+ const normalized = this.validateInput(input);
223
+ await mkdir(this.rootDir, { recursive: true, mode: 0o700 });
224
+ await mkdir(resolve(this.rootDir, ".home"), { recursive: true, mode: 0o700 });
225
+ const runRoot = resolve(this.rootDir, normalized.runId);
226
+ if (!directChild(this.rootDir, runRoot))
227
+ throw new Error("vcs_workspace_path_invalid");
228
+ try {
229
+ await mkdir(runRoot, { mode: 0o700 });
230
+ }
231
+ catch (error) {
232
+ if (error.code === "EEXIST")
233
+ throw new Error("vcs_workspace_exists", { cause: error });
234
+ throw error;
235
+ }
236
+ const workspacePath = resolve(runRoot, "workspace");
237
+ const gitMetadataPath = resolve(runRoot, "git");
238
+ const cloneUrl = this.cloneUrlForRepository(normalized.repository);
239
+ if (!this.options.cloneUrlForRepository && cloneUrl !== `https://github.com/${normalized.repository}.git`) {
240
+ throw new Error("vcs_remote_invalid");
241
+ }
242
+ // Revision-in-place: a continuation clones the EXISTING headRef branch
243
+ // directly (its current tip becomes baseCommit below) instead of
244
+ // baseRef -- everything after this line (branch/checkout/reset, and
245
+ // every check in finalizeChanges) is unchanged either way, since
246
+ // baseCommit already means exactly what it needs to: "where this
247
+ // round started."
248
+ const cloneBranch = normalized.continuation ? normalized.headRef : normalized.baseRef;
249
+ try {
250
+ await this.options.github.withRepositoryToken(normalized.repository, async (token) => {
251
+ await this.git.run([
252
+ ...HARDENED_GIT_CONFIG,
253
+ "clone",
254
+ "--no-checkout",
255
+ "--single-branch",
256
+ "--no-tags",
257
+ // History is never needed: the worker gets no Git metadata, the
258
+ // finalizer compares only against baseCommit and the new commit's
259
+ // parent, and pushOnce compares SHAs. Depth 1 keeps large
260
+ // repositories' history off the control plane's disk.
261
+ "--depth",
262
+ "1",
263
+ "--branch",
264
+ cloneBranch,
265
+ "--separate-git-dir",
266
+ gitMetadataPath,
267
+ cloneUrl,
268
+ workspacePath,
269
+ ], { cwd: runRoot, authToken: token });
270
+ });
271
+ await rm(resolve(workspacePath, ".git"), { force: true });
272
+ const baseCommit = await this.revParseRaw(gitMetadataPath, workspacePath, `refs/remotes/origin/${cloneBranch}^{commit}`);
273
+ await this.gitForPaths(gitMetadataPath, workspacePath, ["config", "--local", "core.hooksPath", "/dev/null"]);
274
+ await this.gitForPaths(gitMetadataPath, workspacePath, ["config", "--local", "commit.gpgSign", "false"]);
275
+ await this.gitForPaths(gitMetadataPath, workspacePath, ["config", "--local", "tag.gpgSign", "false"]);
276
+ await this.gitForPaths(gitMetadataPath, workspacePath, ["config", "--local", "user.name", "Wardby"]);
277
+ await this.gitForPaths(gitMetadataPath, workspacePath, [
278
+ "config",
279
+ "--local",
280
+ "user.email",
281
+ "wardby@users.noreply.github.com",
282
+ ]);
283
+ // Revision-in-place: a continuation's clone (--branch === headRef, above)
284
+ // already leaves headRef checked out locally, pointing at exactly
285
+ // baseCommit -- `branch --force` on an already-checked-out branch is
286
+ // both unnecessary here and something Git refuses outright ("cannot
287
+ // force update the branch ... checked out"). Only a fresh run (cloned
288
+ // baseRef, a DIFFERENT name from headRef) needs this to create headRef.
289
+ if (!normalized.continuation) {
290
+ await this.gitForPaths(gitMetadataPath, workspacePath, ["branch", "--force", normalized.headRef, baseCommit]);
291
+ }
292
+ await this.gitForPaths(gitMetadataPath, workspacePath, [
293
+ "symbolic-ref",
294
+ "HEAD",
295
+ `refs/heads/${normalized.headRef}`,
296
+ ]);
297
+ await this.gitForPaths(gitMetadataPath, workspacePath, ["read-tree", "--reset", "-u", baseCommit]);
298
+ await this.inspectWorkspace(workspacePath);
299
+ return {
300
+ id: `vcs-${normalized.runId}`,
301
+ ...normalized,
302
+ baseCommit,
303
+ workspacePath,
304
+ gitMetadataPath,
305
+ };
306
+ }
307
+ catch (error) {
308
+ await rm(runRoot, { recursive: true, force: true });
309
+ throw error;
310
+ }
311
+ }
312
+ async recoverWorkspace(input) {
313
+ const normalized = this.validateInput(input);
314
+ const expected = this.expectedPaths(normalized.runId);
315
+ try {
316
+ const metadata = await lstat(expected.runRoot);
317
+ if (!metadata.isDirectory() || metadata.isSymbolicLink())
318
+ throw new Error("vcs_workspace_path_invalid");
319
+ }
320
+ catch (error) {
321
+ if (error.code === "ENOENT")
322
+ return null;
323
+ throw error;
324
+ }
325
+ const cloneBranch = normalized.continuation ? normalized.headRef : normalized.baseRef;
326
+ const baseCommit = await this.revParseRaw(expected.gitMetadataPath, expected.workspacePath, `refs/remotes/origin/${cloneBranch}^{commit}`);
327
+ const recovered = {
328
+ id: `vcs-${normalized.runId}`,
329
+ ...normalized,
330
+ baseCommit,
331
+ workspacePath: expected.workspacePath,
332
+ gitMetadataPath: expected.gitMetadataPath,
333
+ };
334
+ const prepared = await this.validatePrepared(recovered);
335
+ await this.inspectWorkspace(prepared.workspacePath);
336
+ await this.assertRemote(prepared);
337
+ await this.assertSafeLocalConfig(prepared.gitMetadataPath);
338
+ const symbolicHead = (await this.gitFor(prepared, ["symbolic-ref", "--short", "HEAD"])).stdout.trim();
339
+ if (symbolicHead !== prepared.headRef)
340
+ throw new Error("vcs_head_ref_mismatch");
341
+ const currentHead = await this.revParse(prepared, "HEAD");
342
+ if (currentHead !== prepared.baseCommit) {
343
+ const parent = await this.revParse(prepared, "HEAD^");
344
+ if (parent !== prepared.baseCommit)
345
+ throw new Error("vcs_commit_history_invalid");
346
+ }
347
+ return prepared;
348
+ }
349
+ async finalizeChanges(workspace, details) {
350
+ const prepared = await this.validatePrepared(workspace);
351
+ await this.inspectWorkspace(prepared.workspacePath);
352
+ await this.assertRemote(prepared);
353
+ await this.assertSafeLocalConfig(prepared.gitMetadataPath);
354
+ await this.gitFor(prepared, ["add", "--all", "--", ":/"]);
355
+ const changed = nullSeparated((await this.gitFor(prepared, [
356
+ "diff",
357
+ "--cached",
358
+ "--name-only",
359
+ "-z",
360
+ "--no-renames",
361
+ prepared.baseCommit,
362
+ "--",
363
+ ])).stdout);
364
+ if (changed.length > this.maxChangedFiles)
365
+ throw new Error("vcs_changed_file_limit");
366
+ const protectedMatchers = prepared.protectedPaths.map((path) => ({ path, matcher: globRegex(path) }));
367
+ const protectedChange = changed.find((path) => protectedMatchers.some(({ matcher }) => matcher.test(path)));
368
+ if (protectedChange)
369
+ throw new Error(`vcs_protected_path:${protectedChange}`);
370
+ let diff;
371
+ try {
372
+ diff = (await this.gitFor(prepared, ["diff", "--cached", "--binary", "--no-ext-diff", "--no-textconv", prepared.baseCommit, "--"], { maxOutputBytes: this.maxDiffBytes })).stdout;
373
+ }
374
+ catch (error) {
375
+ if (error instanceof GitCommandError && error.message.includes("git_output_limit")) {
376
+ throw new Error("vcs_diff_size_limit", { cause: error });
377
+ }
378
+ throw error;
379
+ }
380
+ if (Buffer.byteLength(diff, "utf8") > this.maxDiffBytes)
381
+ throw new Error("vcs_diff_size_limit");
382
+ const currentHead = await this.revParse(prepared, "HEAD");
383
+ if (changed.length === 0 && currentHead === prepared.baseCommit) {
384
+ return {
385
+ outcome: "no_changes",
386
+ repository: prepared.repository,
387
+ baseRef: prepared.baseRef,
388
+ baseCommit: prepared.baseCommit,
389
+ };
390
+ }
391
+ const symbolicHead = (await this.gitFor(prepared, ["symbolic-ref", "--short", "HEAD"])).stdout.trim();
392
+ if (symbolicHead !== prepared.headRef)
393
+ throw new Error("vcs_head_ref_mismatch");
394
+ let commitSha;
395
+ if (currentHead === prepared.baseCommit) {
396
+ await this.gitFor(prepared, ["commit", "--no-verify", "--no-gpg-sign", "-m", `Wardby run ${prepared.runId}`]);
397
+ commitSha = await this.revParse(prepared, "HEAD");
398
+ }
399
+ else {
400
+ const parent = await this.revParse(prepared, "HEAD^");
401
+ if (parent !== prepared.baseCommit)
402
+ throw new Error("vcs_commit_history_invalid");
403
+ const afterCommitChanges = nullSeparated((await this.gitFor(prepared, ["diff", "--cached", "--name-only", "-z", "HEAD", "--"])).stdout);
404
+ if (afterCommitChanges.length > 0)
405
+ throw new Error("vcs_finalized_workspace_changed");
406
+ commitSha = currentHead;
407
+ }
408
+ await this.pushOnce(prepared, commitSha);
409
+ // Revision-in-place: identify the PR by the run that originally opened
410
+ // it (createOrFindDraftPullRequest's marker-based lookup keys on that
411
+ // run's id), not this run's own -- this finds the existing open PR and
412
+ // returns it rather than creating a new one, since headRef/baseRef
413
+ // already match it exactly. No other change needed here: pushing a new
414
+ // commit onto that branch already updates the PR natively.
415
+ const pullRequest = await this.options.github.createOrFindDraftPullRequest({
416
+ runId: prepared.continuation?.rootRunId ?? prepared.runId,
417
+ repository: prepared.repository,
418
+ baseRef: prepared.baseRef,
419
+ headRef: prepared.headRef,
420
+ summary: details?.summary,
421
+ tests: details?.tests,
422
+ tag: details?.tag,
423
+ });
424
+ return {
425
+ outcome: prepared.continuation ? "pull_request_updated" : "pull_request_opened",
426
+ repository: prepared.repository,
427
+ baseRef: prepared.baseRef,
428
+ baseCommit: prepared.baseCommit,
429
+ headRef: prepared.headRef,
430
+ commitSha,
431
+ pullRequestNumber: pullRequest.number,
432
+ pullRequestUrl: pullRequest.url,
433
+ };
434
+ }
435
+ async cleanup(workspace) {
436
+ const expected = this.expectedPaths(workspace.runId);
437
+ if (workspace.id !== `vcs-${workspace.runId}` ||
438
+ workspace.workspacePath !== expected.workspacePath ||
439
+ workspace.gitMetadataPath !== expected.gitMetadataPath) {
440
+ throw new Error("vcs_workspace_handle_invalid");
441
+ }
442
+ await rm(expected.runRoot, { recursive: true, force: true });
443
+ }
444
+ /**
445
+ * Best-effort "wardby is working on this PR" signal (see VcsProvider) --
446
+ * whole body wrapped so this can NEVER throw or otherwise affect the
447
+ * real coding run, mirroring docker.ts's readWorkerFailureDiagnostic
448
+ * ("diagnostics are optional and must never affect terminal cleanup").
449
+ * No-op for a fresh (non-continuation) workspace: there's no PR to
450
+ * attach anything to until its one commit lands.
451
+ */
452
+ async notifyContinuationStarted(workspace, details) {
453
+ if (!workspace.continuation)
454
+ return;
455
+ try {
456
+ const identity = {
457
+ runId: workspace.runId,
458
+ rootRunId: workspace.continuation.rootRunId,
459
+ repository: workspace.repository,
460
+ baseRef: workspace.baseRef,
461
+ headRef: workspace.headRef,
462
+ };
463
+ await Promise.allSettled([
464
+ this.options.github.upsertContinuationStatusComment({
465
+ ...identity,
466
+ body: `🔄 ${runLabel(details?.agentName, workspace.runId)} is working on this PR...`,
467
+ }),
468
+ this.options.github.createContinuationCheckRun({
469
+ repository: workspace.repository,
470
+ headSha: workspace.baseCommit,
471
+ runId: workspace.runId,
472
+ }),
473
+ ]);
474
+ }
475
+ catch {
476
+ // Best-effort observability only -- must never affect the real run.
477
+ }
478
+ }
479
+ /**
480
+ * Companion to notifyContinuationStarted -- finds and updates whatever
481
+ * that call created, never creates fresh state itself (see
482
+ * upsertContinuationStatusComment vs updateContinuationStatusComment,
483
+ * and createContinuationCheckRun vs completeContinuationCheckRun in
484
+ * github.ts). Safe to call more than once for the same run. Same
485
+ * never-throw contract as notifyContinuationStarted.
486
+ */
487
+ async notifyContinuationFinished(workspace, outcome, details) {
488
+ if (!workspace.continuation)
489
+ return;
490
+ try {
491
+ const identity = {
492
+ runId: workspace.runId,
493
+ rootRunId: workspace.continuation.rootRunId,
494
+ repository: workspace.repository,
495
+ baseRef: workspace.baseRef,
496
+ headRef: workspace.headRef,
497
+ };
498
+ const label = runLabel(details?.agentName, workspace.runId);
499
+ const summarySuffix = details?.summary ? `\n\n${details.summary}` : "";
500
+ const body = outcome === "succeeded" ? `✅ ${label} finished.${summarySuffix}` : `❌ ${label} failed.${summarySuffix}`;
501
+ await Promise.allSettled([
502
+ this.options.github.updateContinuationStatusComment({ ...identity, body }),
503
+ this.options.github.completeContinuationCheckRun({
504
+ repository: workspace.repository,
505
+ headSha: workspace.baseCommit,
506
+ runId: workspace.runId,
507
+ outcome,
508
+ }),
509
+ ]);
510
+ }
511
+ catch {
512
+ // Best-effort observability only -- must never affect the real run.
513
+ }
514
+ }
515
+ validateInput(input) {
516
+ if (!SAFE_RUN_ID.test(input.runId))
517
+ throw new Error("vcs_run_id_invalid");
518
+ const repository = normalizeGitHubRepository(input.repository);
519
+ const baseRef = normalizeGitRef(input.baseRef);
520
+ const headRef = normalizeGitRef(input.headRef);
521
+ // Revision-in-place: a continuation's headRef legitimately belongs to a
522
+ // DIFFERENT run (the one that originally opened the PR), so it can't
523
+ // match this run's own derived pattern -- it must instead match that
524
+ // run's. The caller (dispatch.ts) is responsible for having verified,
525
+ // against the database, that this run is actually allowed to continue
526
+ // that branch before ever setting `continuation`; this is a structural
527
+ // check only, not the security boundary.
528
+ let continuation;
529
+ if (input.continuation) {
530
+ if (!SAFE_RUN_ID.test(input.continuation.rootRunId))
531
+ throw new Error("vcs_root_run_id_invalid");
532
+ if (headRef !== `wardby/run-${input.continuation.rootRunId}`)
533
+ throw new Error("vcs_head_ref_invalid");
534
+ continuation = { rootRunId: input.continuation.rootRunId };
535
+ }
536
+ else if (headRef !== `wardby/run-${input.runId}`) {
537
+ throw new Error("vcs_head_ref_invalid");
538
+ }
539
+ const protectedPaths = [...new Set(input.protectedPaths.map(validateProtectedPath))];
540
+ if (protectedPaths.length === 0 || protectedPaths.length > 128)
541
+ throw new Error("vcs_protected_paths_invalid");
542
+ return { runId: input.runId, repository, baseRef, headRef, protectedPaths, continuation };
543
+ }
544
+ expectedPaths(runId) {
545
+ if (!SAFE_RUN_ID.test(runId))
546
+ throw new Error("vcs_workspace_handle_invalid");
547
+ const runRoot = resolve(this.rootDir, runId);
548
+ if (!directChild(this.rootDir, runRoot))
549
+ throw new Error("vcs_workspace_handle_invalid");
550
+ return {
551
+ runRoot,
552
+ workspacePath: resolve(runRoot, "workspace"),
553
+ gitMetadataPath: resolve(runRoot, "git"),
554
+ };
555
+ }
556
+ async validatePrepared(workspace) {
557
+ const normalized = this.validateInput(workspace);
558
+ const expected = this.expectedPaths(normalized.runId);
559
+ if (workspace.id !== `vcs-${normalized.runId}` ||
560
+ !SAFE_COMMIT_SHA.test(workspace.baseCommit) ||
561
+ workspace.repository !== normalized.repository ||
562
+ workspace.baseRef !== normalized.baseRef ||
563
+ workspace.headRef !== normalized.headRef ||
564
+ workspace.workspacePath !== expected.workspacePath ||
565
+ workspace.gitMetadataPath !== expected.gitMetadataPath ||
566
+ workspace.protectedPaths.length !== normalized.protectedPaths.length ||
567
+ workspace.protectedPaths.some((path, index) => path !== normalized.protectedPaths[index]) ||
568
+ workspace.continuation?.rootRunId !== normalized.continuation?.rootRunId) {
569
+ throw new Error("vcs_workspace_handle_invalid");
570
+ }
571
+ const [rootReal, runRootReal, workspaceReal, metadataReal] = await Promise.all([
572
+ realpath(this.rootDir),
573
+ realpath(expected.runRoot),
574
+ realpath(expected.workspacePath),
575
+ realpath(expected.gitMetadataPath),
576
+ ]);
577
+ if (runRootReal !== resolve(rootReal, workspace.runId) ||
578
+ workspaceReal !== resolve(runRootReal, "workspace") ||
579
+ metadataReal !== resolve(runRootReal, "git")) {
580
+ throw new Error("vcs_workspace_path_invalid");
581
+ }
582
+ return { ...workspace, ...normalized };
583
+ }
584
+ async inspectWorkspace(workspacePath) {
585
+ const rootStat = await lstat(workspacePath);
586
+ if (!rootStat.isDirectory() || rootStat.isSymbolicLink())
587
+ throw new Error("vcs_workspace_path_invalid");
588
+ let entries = 0;
589
+ const visit = async (directory) => {
590
+ const handle = await opendir(directory);
591
+ for await (const entry of handle) {
592
+ entries += 1;
593
+ if (entries > MAX_WORKSPACE_ENTRIES)
594
+ throw new Error("vcs_workspace_entry_limit");
595
+ if (entry.name.toLowerCase() === ".git")
596
+ throw new Error("vcs_nested_repository");
597
+ const fullPath = resolve(directory, entry.name);
598
+ const stat = await lstat(fullPath);
599
+ if (stat.isSymbolicLink()) {
600
+ const target = await readlink(fullPath);
601
+ const resolvedTarget = resolve(directory, target);
602
+ if (isAbsolute(target) ||
603
+ (resolvedTarget !== workspacePath && !resolvedTarget.startsWith(`${workspacePath}${sep}`))) {
604
+ throw new Error("vcs_symlink_escape");
605
+ }
606
+ }
607
+ else if (stat.isDirectory()) {
608
+ await visit(fullPath);
609
+ }
610
+ else if (!stat.isFile()) {
611
+ throw new Error("vcs_special_file");
612
+ }
613
+ }
614
+ };
615
+ await visit(workspacePath);
616
+ }
617
+ async assertRemote(workspace) {
618
+ const expectedUrl = this.cloneUrlForRepository(workspace.repository);
619
+ const remotes = (await this.gitFor(workspace, ["remote"])).stdout.trim().split("\n").filter(Boolean);
620
+ const fetchUrls = (await this.gitFor(workspace, ["remote", "get-url", "--all", "origin"])).stdout
621
+ .trim()
622
+ .split("\n")
623
+ .filter(Boolean);
624
+ const pushUrls = (await this.gitFor(workspace, ["remote", "get-url", "--push", "--all", "origin"])).stdout
625
+ .trim()
626
+ .split("\n")
627
+ .filter(Boolean);
628
+ if (remotes.length !== 1 ||
629
+ remotes[0] !== "origin" ||
630
+ fetchUrls.length !== 1 ||
631
+ fetchUrls[0] !== expectedUrl ||
632
+ pushUrls.length !== 1 ||
633
+ pushUrls[0] !== expectedUrl) {
634
+ throw new Error("vcs_remote_invalid");
635
+ }
636
+ }
637
+ /**
638
+ * Deliberately NOT windowed, unlike every other redaction caller: here
639
+ * `redactTokenShapedValues` is a DETECTOR, not a formatter — nothing from the
640
+ * config is ever emitted, only the fixed `vcs_git_config_unsafe` — so
641
+ * redacting a prefix would narrow a security predicate over an
642
+ * attacker-controlled file (a cloned repo's `.git/config`) to buy speed. The
643
+ * input is already bounded: the size check short-circuits above it, so
644
+ * redaction never sees more than 64 KiB, and with every pattern linear the
645
+ * worst adversarial 64 KiB shape measures 22 ms (it was 1.5 s while the JWT
646
+ * pattern was quadratic — that bug, not the breadth of this scan, was the
647
+ * problem).
648
+ */
649
+ async assertSafeLocalConfig(gitMetadataPath) {
650
+ const config = await readFile(resolve(gitMetadataPath, "config"), "utf8");
651
+ if (Buffer.byteLength(config, "utf8") > 64 * 1024 ||
652
+ /^\s*\[(?:credential|filter\b|http\b|url\b)/im.test(config) ||
653
+ /^\s*(?:extraheader|helper|insteadof|sshcommand|textconv|clean|smudge)\s*=/im.test(config) ||
654
+ redactTokenShapedValues(config) !== config) {
655
+ throw new Error("vcs_git_config_unsafe");
656
+ }
657
+ }
658
+ async pushOnce(workspace, commitSha) {
659
+ await this.options.github.withRepositoryToken(workspace.repository, async (token) => {
660
+ const remote = await this.remoteHead(workspace, token);
661
+ if (remote === commitSha)
662
+ return;
663
+ // A continuation's remote branch legitimately already sits at
664
+ // baseCommit (that's the tip we cloned and committed on top of) --
665
+ // that's the expected fast-forward pre-push state, not a conflict.
666
+ // Any OTHER non-null value means something else moved the branch
667
+ // since we cloned (a human push, a race), which is a real conflict.
668
+ if (remote && remote !== workspace.baseCommit)
669
+ throw new Error("vcs_head_ref_conflict");
670
+ try {
671
+ await this.gitFor(workspace, ["push", "origin", `${commitSha}:refs/heads/${workspace.headRef}`], {
672
+ authToken: token,
673
+ });
674
+ }
675
+ catch (error) {
676
+ if ((await this.remoteHead(workspace, token)) === commitSha)
677
+ return;
678
+ throw error;
679
+ }
680
+ });
681
+ }
682
+ async remoteHead(workspace, token) {
683
+ const result = await this.gitFor(workspace, ["ls-remote", "--heads", "origin", `refs/heads/${workspace.headRef}`], {
684
+ authToken: token,
685
+ });
686
+ const line = result.stdout.trim();
687
+ if (!line)
688
+ return null;
689
+ const [sha, ref, ...rest] = line.split(/\s+/);
690
+ if (rest.length > 0 || !SAFE_COMMIT_SHA.test(sha) || ref !== `refs/heads/${workspace.headRef}`) {
691
+ throw new Error("vcs_remote_response_invalid");
692
+ }
693
+ return sha;
694
+ }
695
+ async revParse(workspace, revision) {
696
+ return this.revParseRaw(workspace.gitMetadataPath, workspace.workspacePath, revision);
697
+ }
698
+ async revParseRaw(gitMetadataPath, workspacePath, revision) {
699
+ const sha = (await this.gitForPaths(gitMetadataPath, workspacePath, ["rev-parse", "--verify", revision])).stdout.trim();
700
+ if (!SAFE_COMMIT_SHA.test(sha))
701
+ throw new Error("vcs_commit_sha_invalid");
702
+ return sha;
703
+ }
704
+ gitFor(workspace, args, options = {}) {
705
+ return this.gitForPaths(workspace.gitMetadataPath, workspace.workspacePath, args, options);
706
+ }
707
+ gitForPaths(gitMetadataPath, workspacePath, args, options = {}) {
708
+ return this.git.run([`--git-dir=${gitMetadataPath}`, `--work-tree=${workspacePath}`, ...HARDENED_GIT_CONFIG, ...args], { ...options, cwd: workspacePath });
709
+ }
710
+ }