@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,339 @@
1
+ /**
2
+ * Fail-closed cluster preflight for JOB_LAUNCHER=kubernetes. Proves the
3
+ * namespace exists, the proxy Service is a usable enforcement witness (both
4
+ * ports exposed with a ready backend), the worker image is a registry digest,
5
+ * and — with a canary pod built from the real run pod and run NetworkPolicy —
6
+ * that DNS, the internet, the metadata server and **the proxy's deny port**
7
+ * are unreachable while the proxy itself is reachable. Any other state, error,
8
+ * or timeout fails the check; the whole preflight, cleanup included, is bounded.
9
+ */
10
+ import { randomBytes } from "node:crypto";
11
+ import { CODING_PROXY_DENY_PORT, CODING_PROXY_PORT } from "./docker-isolation.js";
12
+ import { KUBERNETES_ISOLATION_ERROR, WORKER_CONTAINER, buildRunNetworkPolicy, buildRunPod, isRegistryDigest, } from "./kubernetes-isolation.js";
13
+ import { KubernetesPlatformError, assertPlatformConfig, platformProfile } from "./kubernetes-platform.js";
14
+ import { readProxyWitness } from "./kubernetes-witness.js";
15
+ const DEFAULT_TIMEOUT_MS = 90_000;
16
+ const DEFAULT_CLEANUP_TIMEOUT_MS = 15_000;
17
+ const POLL_INTERVAL_MS = 500;
18
+ const LOG_TAIL_LINES = 20;
19
+ const LOG_LIMIT_BYTES = 4096;
20
+ const CANARY_LINE_PREFIX = '{"wardbyCanary":';
21
+ const EXPECTED = { dns: false, proxyDeny: false, internet: false, metadata: false, proxy: true };
22
+ const CANARY_KEYS = Object.keys(EXPECTED).sort();
23
+ /**
24
+ * Runs in the worker image under the run policy; prints exactly one `{"wardbyCanary":{...}}` line.
25
+ * `dns` checks in-pod resolution; `proxyDeny` checks the policy itself — a TCP connect to the *same*
26
+ * proxy pod on a port no run policy permits, which a namespace-wide allow-all policy would reopen.
27
+ * Probing one destination on two ports is what makes the result decisive: `proxy` true with
28
+ * `proxyDeny` false can only mean a policy is enforced and port-scoped.
29
+ *
30
+ * `proxyDeny` is false for exactly one socket outcome: a **timeout**, i.e. the packet was dropped.
31
+ * A refusal (RST) is not a denial — it proves the SYN reached the destination host, so the deny
32
+ * port is merely unserved — and a connect obviously is not either. This mirrors the launch gate's
33
+ * probe (`enforcementProbeScript`), which had to make the same distinction after a live cluster
34
+ * with no NetworkPolicy at all passed a boolean version of this check; preflight and gate must
35
+ * agree on what counts as evidence, or one of them is vacuous. `internet`, `metadata` and `proxy`
36
+ * stay plain reachability, where "connected" is the only thing that matters.
37
+ *
38
+ * CNIs program a new pod's policy a few seconds after it starts, so the script first waits (up to
39
+ * 20 s) for the deny-port connect to be *dropped*; if it never is, `proxyDeny` reports true.
40
+ */
41
+ export const CANARY_SCRIPT = [
42
+ 'const net = require("node:net");',
43
+ 'const dns = require("node:dns").promises;',
44
+ "const tcp = (host, port) =>",
45
+ " new Promise((done) => {",
46
+ " const socket = net.connect({ host, port, timeout: 3000 });",
47
+ ' socket.once("connect", () => {',
48
+ " socket.destroy();",
49
+ ' done("connect");',
50
+ " });",
51
+ ' socket.once("timeout", () => {',
52
+ " socket.destroy();",
53
+ ' done("timeout");',
54
+ " });",
55
+ ' socket.once("error", () => done("error"));',
56
+ " });",
57
+ "// Plain reachability, for the probes where only a completed connection matters.",
58
+ 'const reachable = async (host, port) => (await tcp(host, port)) === "connect";',
59
+ "// Only a dropped packet is a policy denial: a refusal proves the SYN reached the host.",
60
+ 'const dropped = async (host, port) => (await tcp(host, port)) === "timeout";',
61
+ "const settle = async () => {",
62
+ " const until = Date.now() + 20000;",
63
+ " while (Date.now() < until) {",
64
+ ` if (await dropped(process.env.WARDBY_CANARY_PROXY_IP, ${CODING_PROXY_DENY_PORT})) return;`,
65
+ " await new Promise((wake) => setTimeout(wake, 500));",
66
+ " }",
67
+ "};",
68
+ "(async () => {",
69
+ " await settle();",
70
+ " const wardbyCanary = {",
71
+ ' dns: await dns.lookup("kubernetes.default.svc.cluster.local").then(',
72
+ " () => true,",
73
+ " () => false,",
74
+ " ),",
75
+ ` proxyDeny: !(await dropped(process.env.WARDBY_CANARY_PROXY_IP, ${CODING_PROXY_DENY_PORT})),`,
76
+ ' internet: await reachable("1.1.1.1", 443),',
77
+ ' metadata: await reachable("169.254.169.254", 80),',
78
+ ` proxy: await reachable(process.env.WARDBY_CANARY_PROXY_IP, ${CODING_PROXY_PORT}),`,
79
+ " };",
80
+ " console.log(JSON.stringify({ wardbyCanary }));",
81
+ "})();",
82
+ ].join("\n");
83
+ /** A failed check. Carries no cause for canary-output failures so no log text can leave this module. */
84
+ class PreflightFailure extends Error {
85
+ }
86
+ function failure(check, cause) {
87
+ const message = `${KUBERNETES_ISOLATION_ERROR}:${check}`;
88
+ return cause === undefined ? new PreflightFailure(message) : new PreflightFailure(message, { cause });
89
+ }
90
+ /** Runs one check; any error that is not already a check failure becomes this check's failure. */
91
+ async function runCheck(check, action) {
92
+ try {
93
+ return await action();
94
+ }
95
+ catch (error) {
96
+ if (error instanceof PreflightFailure)
97
+ throw error;
98
+ throw failure(check, error);
99
+ }
100
+ }
101
+ /** Resolves with the action's result, or rejects with `onTimeout()` after `ms`. Never leaves a timer behind. */
102
+ async function within(action, ms, onTimeout) {
103
+ let timer;
104
+ const expired = new Promise((_, reject) => {
105
+ timer = setTimeout(() => reject(onTimeout()), ms);
106
+ });
107
+ try {
108
+ return await Promise.race([action, expired]);
109
+ }
110
+ finally {
111
+ clearTimeout(timer);
112
+ }
113
+ }
114
+ /** Parses only the fixed canary line and requires exactly the five booleans. */
115
+ function parseCanary(log) {
116
+ const lines = log
117
+ .split("\n")
118
+ .map((line) => line.trim())
119
+ .filter((line) => line.startsWith(CANARY_LINE_PREFIX));
120
+ if (lines.length !== 1)
121
+ return undefined;
122
+ let parsed;
123
+ try {
124
+ parsed = JSON.parse(lines[0]);
125
+ }
126
+ catch {
127
+ return undefined;
128
+ }
129
+ if (typeof parsed !== "object" || parsed === null || Object.keys(parsed).length !== 1)
130
+ return undefined;
131
+ const result = parsed.wardbyCanary;
132
+ if (typeof result !== "object" || result === null || Array.isArray(result))
133
+ return undefined;
134
+ const keys = Object.keys(result).sort();
135
+ if (keys.join(",") !== CANARY_KEYS.join(","))
136
+ return undefined;
137
+ const record = result;
138
+ if (!keys.every((key) => typeof record[key] === "boolean"))
139
+ return undefined;
140
+ return record;
141
+ }
142
+ function canaryPasses(result) {
143
+ return (result !== undefined &&
144
+ result.dns === EXPECTED.dns &&
145
+ result.proxyDeny === EXPECTED.proxyDeny &&
146
+ result.internet === EXPECTED.internet &&
147
+ result.metadata === EXPECTED.metadata &&
148
+ result.proxy === EXPECTED.proxy);
149
+ }
150
+ /** Deletes the pod (grace 0), and only then its policy, so the canary never runs unpoliced. */
151
+ async function deleteCanary(api, state, cleanupTimeoutMs) {
152
+ const timedOut = () => new Error("canary_cleanup_timeout");
153
+ if (state.podName)
154
+ await within(api.deletePod(state.namespace, state.podName, 0), cleanupTimeoutMs, timedOut);
155
+ if (state.policyName) {
156
+ await within(api.deleteNetworkPolicy(state.namespace, state.policyName), cleanupTimeoutMs, timedOut);
157
+ }
158
+ }
159
+ /** Issues one create; a create that settles after the preflight finished removes what it made. */
160
+ async function tracked(state, create, lateCleanup) {
161
+ if (state.finished)
162
+ throw failure("timeout");
163
+ state.createInFlight = true;
164
+ try {
165
+ return await create();
166
+ }
167
+ finally {
168
+ state.createInFlight = false;
169
+ if (state.finished)
170
+ await lateCleanup().catch(() => undefined);
171
+ }
172
+ }
173
+ async function runCanary(options, state, proxyIp, timeoutMs, cleanupTimeoutMs) {
174
+ const { api, config, workerImage } = options;
175
+ const now = options.now ?? Date.now;
176
+ const sleep = options.sleep ?? ((ms) => new Promise((done) => setTimeout(done, ms)));
177
+ const namespace = config.namespace;
178
+ const timeoutSec = Math.max(1, Math.ceil(timeoutMs / 1000));
179
+ const spec = {
180
+ kind: "coding-agent",
181
+ runId: `preflight-${randomBytes(8).toString("hex")}`,
182
+ provider: "codex",
183
+ image: workerImage,
184
+ inputArtifact: "",
185
+ timeoutSec,
186
+ limits: { cpus: 0.25, memoryMb: 128, pids: 64, diskMb: 64 },
187
+ labels: {},
188
+ };
189
+ const { pod, policy } = await runCheck("canary", async () => {
190
+ const pod = buildRunPod(spec, {
191
+ namespace,
192
+ proxyIp,
193
+ runtimeClassName: config.runtimeClassName,
194
+ platform: config.platform,
195
+ });
196
+ const podSpec = pod.spec;
197
+ const worker = podSpec.containers.find((container) => container.name === WORKER_CONTAINER);
198
+ if (!worker)
199
+ throw failure("canary");
200
+ worker.env = [{ name: "WARDBY_CANARY_PROXY_IP", value: proxyIp }];
201
+ worker.command = ["node", "-e", CANARY_SCRIPT];
202
+ podSpec.containers = [worker];
203
+ // The canary has no collection window: Kubernetes kills it at the preflight's own deadline.
204
+ podSpec.activeDeadlineSeconds = timeoutSec;
205
+ return { pod, policy: buildRunNetworkPolicy(spec, namespace) };
206
+ });
207
+ const podName = pod.metadata.name;
208
+ const policyName = policy.metadata.name;
209
+ const lateCleanup = () => deleteCanary(api, state, cleanupTimeoutMs);
210
+ await runCheck("canary", async () => {
211
+ // The policy exists before the pod so the canary never runs unpoliced.
212
+ state.policyName = policyName;
213
+ await tracked(state, () => api.createNetworkPolicy(namespace, policy), lateCleanup);
214
+ state.podName = podName;
215
+ await tracked(state, () => api.createPod(namespace, pod), lateCleanup);
216
+ const deadline = now() + timeoutMs;
217
+ for (;;) {
218
+ if (state.finished)
219
+ throw failure("timeout");
220
+ const current = await api.readPod(namespace, podName);
221
+ if (!current)
222
+ throw failure("canary");
223
+ const status = current.status?.containerStatuses?.find((entry) => entry.name === WORKER_CONTAINER);
224
+ const terminated = status?.state?.terminated;
225
+ if (terminated) {
226
+ if (terminated.exitCode !== 0)
227
+ throw failure("canary");
228
+ break;
229
+ }
230
+ if (current.status?.phase === "Failed" || current.status?.phase === "Succeeded")
231
+ throw failure("canary");
232
+ if (now() >= deadline)
233
+ throw failure("timeout");
234
+ await sleep(POLL_INTERVAL_MS);
235
+ }
236
+ let log;
237
+ try {
238
+ log = await api.readLogTail(namespace, podName, WORKER_CONTAINER, LOG_TAIL_LINES, LOG_LIMIT_BYTES);
239
+ }
240
+ catch {
241
+ throw failure("canary");
242
+ }
243
+ if (!canaryPasses(parseCanary(log)))
244
+ throw failure("canary");
245
+ });
246
+ }
247
+ async function runChecks(options, state, passed, timeoutMs, cleanupTimeoutMs) {
248
+ const { api, config, workerImage } = options;
249
+ // Pure configuration, checked first: a deployment that cannot work is refused before
250
+ // a single API call, with a message naming the setting and the limit.
251
+ await runCheck("platform", async () => {
252
+ assertPlatformConfig(platformProfile(config.platform), {
253
+ runtimeClassName: config.runtimeClassName,
254
+ maxDiskMb: options.maxDiskMb,
255
+ });
256
+ });
257
+ passed.push("platform");
258
+ await runCheck("namespace", async () => {
259
+ if (!(await api.readNamespace(config.namespace)))
260
+ throw failure("namespace");
261
+ });
262
+ passed.push("namespace");
263
+ const proxyIp = await runCheck("proxy-service", async () => {
264
+ const witness = await readProxyWitness(api, config.namespace, config.proxyService);
265
+ return witness.clusterIp;
266
+ });
267
+ passed.push("proxy-service");
268
+ if (!isRegistryDigest(workerImage))
269
+ throw failure("worker-image");
270
+ passed.push("worker-image");
271
+ await runCanary(options, state, proxyIp, timeoutMs, cleanupTimeoutMs);
272
+ passed.push("canary");
273
+ return proxyIp;
274
+ }
275
+ /** Throws kubernetes_isolation_unsupported:<check> on the first failed check; returns the checks that passed. */
276
+ export async function kubernetesPreflight(options) {
277
+ return (await runKubernetesPreflight(options)).checks;
278
+ }
279
+ /**
280
+ * Throws kubernetes_isolation_unsupported:<check> on the first failed check
281
+ * (`:timeout` when the whole preflight exceeds `timeoutMs`); returns the checks that passed and
282
+ * the proxy ClusterIP it validated.
283
+ */
284
+ export async function runKubernetesPreflight(options) {
285
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
286
+ const cleanupTimeoutMs = options.cleanupTimeoutMs ?? DEFAULT_CLEANUP_TIMEOUT_MS;
287
+ const state = { namespace: options.config.namespace, createInFlight: false, finished: false };
288
+ const passed = [];
289
+ let outcome;
290
+ let failed = false;
291
+ let proxyIp = "";
292
+ try {
293
+ proxyIp = await within(runChecks(options, state, passed, timeoutMs, cleanupTimeoutMs), timeoutMs, () => failure("timeout"));
294
+ }
295
+ catch (error) {
296
+ failed = true;
297
+ outcome = error;
298
+ }
299
+ finally {
300
+ state.finished = true;
301
+ // A create still in flight owns cleanup (see `tracked`); otherwise delete what exists now.
302
+ if (!state.createInFlight) {
303
+ try {
304
+ await deleteCanary(options.api, state, cleanupTimeoutMs);
305
+ }
306
+ catch {
307
+ // A canary that cannot be cleaned up is an unexpected cluster state: fail closed.
308
+ if (!failed) {
309
+ failed = true;
310
+ outcome = failure("canary");
311
+ }
312
+ }
313
+ }
314
+ }
315
+ if (failed)
316
+ throw outcome;
317
+ return { checks: passed, proxyIp };
318
+ }
319
+ function shortMessage(error) {
320
+ const message = error instanceof Error ? error.message : String(error);
321
+ const firstLine = message.split("\n", 1)[0].trim();
322
+ // A KubernetesPlatformError is single-line by construction and never carries canary output (it is
323
+ // thrown from pure configuration checks before any cluster call), so the 200-char cap that exists
324
+ // to keep arbitrary API-error text and canary noise out of an operator-facing line does not need to
325
+ // apply here — and truncating it cuts the remediation clause an operator needs most.
326
+ if (error instanceof KubernetesPlatformError)
327
+ return firstLine || "unknown error";
328
+ return firstLine.length > 200 ? `${firstLine.slice(0, 200)}…` : firstLine || "unknown error";
329
+ }
330
+ /**
331
+ * One operator-facing line for a failed preflight: the check code, plus the
332
+ * short message of a cause only where the preflight attached one (API errors;
333
+ * never canary output). Anything else (e.g. a kubeconfig load error) is shown briefly.
334
+ */
335
+ export function describePreflightFailure(error) {
336
+ if (!(error instanceof PreflightFailure))
337
+ return shortMessage(error);
338
+ return error.cause === undefined ? error.message : `${error.message} (${shortMessage(error.cause)})`;
339
+ }
@@ -0,0 +1,10 @@
1
+ import type { KubernetesApi } from "./kubernetes-api.js";
2
+ export declare const PROXY_WITNESS_UNUSABLE = "kubernetes_proxy_witness_unusable";
3
+ export declare class ProxyWitnessError extends Error {
4
+ constructor(detail: string);
5
+ }
6
+ export interface ProxyWitness {
7
+ /** The proxy Service's ClusterIP: the address both probed ports live on. */
8
+ clusterIp: string;
9
+ }
10
+ export declare function readProxyWitness(api: KubernetesApi, namespace: string, service: string): Promise<ProxyWitness>;
@@ -0,0 +1,193 @@
1
+ /**
2
+ * The proxy Service as the NetworkPolicy enforcement witness.
3
+ *
4
+ * The preflight canary and the per-launch gate both prove a run's policy is
5
+ * enforced by requiring a connect to the proxy's deny port to be *refused*
6
+ * while a connect to the proxy port succeeds. This read is the configuration
7
+ * cross-check for that: the Service must expose both ports, and at least one
8
+ * ready endpoint must serve both, or the deployment is misconfigured and says
9
+ * so by name rather than failing later at a gate.
10
+ *
11
+ * This is a control-plane cross-check only: it does not itself keep a probe
12
+ * from being vacuous. That is what the two-port probe (measured in one exec,
13
+ * at the instant of the blocked observation) proves; see kubernetes.ts.
14
+ *
15
+ * It also checks the one thing the probe cannot observe about itself: WHO
16
+ * dropped the packet. A timeout on the deny port proves the SYN was dropped
17
+ * somewhere on the path, not that the run pod's own egress policy dropped it.
18
+ * The attribution follows from the proxy's own NetworkPolicy admitting run
19
+ * pods on the deny port at its ingress — because ingress is enforced at the
20
+ * destination, a proxy policy that did NOT admit 8788 would drop the packet
21
+ * there and every run would read "blocked" with no egress policy at all. That
22
+ * was falsified on a live cluster: a proxy policy admitting 8787 only made a
23
+ * prober with no policy and full internet egress read PROVEN. The rule was
24
+ * asserted by manifest and checked nowhere, so one deleted line in an overlay
25
+ * silently voided the gate. It is verified here instead.
26
+ *
27
+ * Both reads are in wardby's own namespace, which the launcher Role permits.
28
+ * (EndpointSlice is the durable successor to the core/v1 Endpoints read here;
29
+ * migrating is a follow-up and needs its own resourceNames grant.)
30
+ */
31
+ import { isIP } from "node:net";
32
+ import { CODING_PROXY_DENY_PORT, CODING_PROXY_PORT } from "./docker-isolation.js";
33
+ import { PROXY_POD_LABEL, RUN_COMPONENT_LABEL } from "./kubernetes-isolation.js";
34
+ export const PROXY_WITNESS_UNUSABLE = "kubernetes_proxy_witness_unusable";
35
+ export class ProxyWitnessError extends Error {
36
+ constructor(detail) {
37
+ super(`${PROXY_WITNESS_UNUSABLE}: ${detail}`);
38
+ this.name = "ProxyWitnessError";
39
+ }
40
+ }
41
+ const REQUIRED_PORTS = [CODING_PROXY_PORT, CODING_PROXY_DENY_PORT];
42
+ /** core/v1 defaults an omitted port protocol to TCP; only count a port toward either
43
+ * required-port check when it is TCP (or silent on protocol), so a UDP/SCTP declaration
44
+ * of 8788 can never satisfy a check that exists to prove something is TCP-listening there. */
45
+ function isTcp(protocol) {
46
+ return protocol === undefined || protocol === "TCP";
47
+ }
48
+ /**
49
+ * Whether a NetworkPolicy's `ingress[]` section has any bearing on ingress traffic at all.
50
+ * Per `V1NetworkPolicySpec`'s generated docs: if `policyTypes` is absent it defaults based on the
51
+ * rules present, and "all policies (whether or not they contain an ingress section) are assumed to
52
+ * affect ingress" — so an absent list still counts. But an explicit list that omits `"Ingress"`
53
+ * (e.g. an egress-only `["Egress"]` policy) makes any `ingress[]` content inert: Kubernetes never
54
+ * consults it, so a rule that reads as "admits the deny port" there proves nothing about what is
55
+ * actually admitted.
56
+ */
57
+ function affectsIngress(policyTypes) {
58
+ return policyTypes === undefined || policyTypes.includes("Ingress");
59
+ }
60
+ /** Whether a Service selector requires every key/value in `labels` (extra keys on the selector are
61
+ * fine — they only narrow which pods it routes to further). Service selectors are plain label maps,
62
+ * not `LabelSelector` objects, so this is direct equality-per-key, not `selectsPodLabeled`'s
63
+ * matchLabels/matchExpressions shape. */
64
+ function selectorRequires(selector, labels) {
65
+ return Object.entries(labels).every(([key, value]) => selector?.[key] === value);
66
+ }
67
+ /** A selector we can evaluate: plain matchLabels, no matchExpressions we would have to guess at. */
68
+ function selectsPodLabeled(selector, labels) {
69
+ if (!selector)
70
+ return false;
71
+ // An expression-based selector may well be correct, but we cannot prove it here: fail closed.
72
+ if (Array.isArray(selector.matchExpressions) && selector.matchExpressions.length > 0)
73
+ return false;
74
+ const required = Object.entries(selector.matchLabels ?? {});
75
+ return required.every(([key, value]) => labels[key] === value);
76
+ }
77
+ /** Whether a peer admits pods carrying `labels` in this same namespace. */
78
+ function peerAdmits(peer, labels) {
79
+ // A namespaceSelector re-scopes the peer to other namespaces; run pods are in this one. An
80
+ // ipBlock never matches a pod selector at all. Neither can be counted as admitting run pods.
81
+ if (peer.namespaceSelector || peer.ipBlock)
82
+ return false;
83
+ return selectsPodLabeled(peer.podSelector, labels);
84
+ }
85
+ /** Whether a rule's port list covers `port` over TCP (absent/empty = every port, per the API). */
86
+ function ruleCoversPort(rule, port) {
87
+ const ports = rule.ports ?? [];
88
+ if (ports.length === 0)
89
+ return true;
90
+ return ports.some((entry) => {
91
+ if (!isTcp(entry.protocol))
92
+ return false;
93
+ // `port` may be a named port, which we cannot resolve to a number here: fail closed.
94
+ if (typeof entry.port !== "number")
95
+ return false;
96
+ const endPort = typeof entry.endPort === "number" ? entry.endPort : entry.port;
97
+ return entry.port <= port && port <= endPort;
98
+ });
99
+ }
100
+ /**
101
+ * The rule's source list, under either spelling. `@kubernetes/client-node` deserializes the wire
102
+ * field `from` to `_from` (it collides with a TypeScript keyword), so a policy read back from a
103
+ * real API server has `_from` while a hand-written manifest object has `from`. Reading only one
104
+ * spelling would leave the other `undefined`, which this function reads as "no restriction" — so
105
+ * getting this wrong fails OPEN and would let any policy at all satisfy the check.
106
+ */
107
+ function ruleSources(rule) {
108
+ return rule._from ?? rule.from ?? [];
109
+ }
110
+ /** Whether a rule admits pods carrying `labels` on `port` (absent/empty `from` = every source). */
111
+ function ruleAdmits(rule, labels, port) {
112
+ if (!ruleCoversPort(rule, port))
113
+ return false;
114
+ const from = ruleSources(rule);
115
+ return from.length === 0 || from.some((peer) => peerAdmits(peer, labels));
116
+ }
117
+ /**
118
+ * Proves the attribution precondition: the proxy's own policy admits run pods on the deny port,
119
+ * so the run pod's egress policy is the only thing left that can drop that packet.
120
+ */
121
+ async function assertDenyPortAdmitted(api, namespace, service) {
122
+ const where = `NetworkPolicy ${namespace}/${service}`;
123
+ const policy = await api.readNetworkPolicy(namespace, service);
124
+ if (!policy) {
125
+ throw new ProxyWitnessError(`${where} was not found, so nothing proves the proxy admits run pods on port ${CODING_PROXY_DENY_PORT}; ` +
126
+ "without that rule the proxy's own ingress drops the probe and every run reads as enforced");
127
+ }
128
+ if (!affectsIngress(policy.spec?.policyTypes)) {
129
+ throw new ProxyWitnessError(`${where} has policyTypes ${JSON.stringify(policy.spec?.policyTypes)} without "Ingress", so its ingress ` +
130
+ "rules are inert and prove nothing about what the proxy admits at its ingress");
131
+ }
132
+ if (!selectsPodLabeled(policy.spec?.podSelector, PROXY_POD_LABEL)) {
133
+ throw new ProxyWitnessError(`${where} does not select the proxy pods (${JSON.stringify(PROXY_POD_LABEL)}), so its rules govern nothing`);
134
+ }
135
+ const admitted = (policy.spec?.ingress ?? []).some((rule) => ruleAdmits(rule, RUN_COMPONENT_LABEL, CODING_PROXY_DENY_PORT));
136
+ if (!admitted) {
137
+ throw new ProxyWitnessError(`${where} has no ingress rule that admits ${JSON.stringify(RUN_COMPONENT_LABEL)} on port ` +
138
+ `${CODING_PROXY_DENY_PORT}/TCP. The gate reads a dropped connect to that port as proof the run's ` +
139
+ "own egress policy is enforced; if the proxy does not admit it, the proxy's ingress drops it instead " +
140
+ "and every run reads as enforced while having no egress policy at all");
141
+ }
142
+ }
143
+ export async function readProxyWitness(api, namespace, service) {
144
+ const where = `Service ${namespace}/${service}`;
145
+ const found = await api.readService(namespace, service);
146
+ if (!found)
147
+ throw new ProxyWitnessError(`${where} was not found`);
148
+ if (!selectorRequires(found.spec?.selector, PROXY_POD_LABEL)) {
149
+ throw new ProxyWitnessError(`${where} has selector ${JSON.stringify(found.spec?.selector ?? {})}, which does not require ` +
150
+ `${JSON.stringify(PROXY_POD_LABEL)}; the endpoints it routes to are not provably the pods the checked ` +
151
+ "NetworkPolicy governs");
152
+ }
153
+ const clusterIp = found.spec?.clusterIP;
154
+ if (!clusterIp || clusterIp === "None" || isIP(clusterIp) === 0) {
155
+ throw new ProxyWitnessError(`${where} has no usable ClusterIP`);
156
+ }
157
+ const exposed = new Set((found.spec?.ports ?? []).filter((port) => isTcp(port.protocol)).map((port) => port.port));
158
+ for (const required of REQUIRED_PORTS) {
159
+ if (!exposed.has(required))
160
+ throw new ProxyWitnessError(`${where} does not expose port ${required}`);
161
+ }
162
+ const endpoints = await api.readEndpoints(namespace, service);
163
+ if (!endpoints)
164
+ throw new ProxyWitnessError(`${where} has no Endpoints`);
165
+ // Endpoints subset ports are the container's targetPorts, not the Service's own port
166
+ // numbers checked above. Comparing them directly here is only correct because the
167
+ // manifest keeps targetPort == port for both the proxy and deny ports; a future re-map
168
+ // (Service port != container targetPort) fails closed here rather than trusting a port
169
+ // that merely happens to share a number with the deny port.
170
+ let hasReadyAddress = false;
171
+ const readyPorts = new Set();
172
+ for (const subset of endpoints.subsets ?? []) {
173
+ const ready = (subset.addresses ?? []).some((address) => typeof address.ip === "string" && isIP(address.ip) !== 0);
174
+ if (!ready)
175
+ continue;
176
+ hasReadyAddress = true;
177
+ for (const port of subset.ports ?? []) {
178
+ if (typeof port.port === "number" && isTcp(port.protocol))
179
+ readyPorts.add(port.port);
180
+ }
181
+ }
182
+ if (!hasReadyAddress)
183
+ throw new ProxyWitnessError(`${where} has no ready endpoint address`);
184
+ if (readyPorts.size === 0)
185
+ throw new ProxyWitnessError(`${where} has a ready endpoint that declares no ports`);
186
+ for (const required of REQUIRED_PORTS) {
187
+ if (!readyPorts.has(required)) {
188
+ throw new ProxyWitnessError(`${where} has no ready endpoint serving port ${required}`);
189
+ }
190
+ }
191
+ await assertDenyPortAdmitted(api, namespace, service);
192
+ return { clusterIp };
193
+ }
@@ -0,0 +1,127 @@
1
+ import { type Readable } from "node:stream";
2
+ import type { KubernetesJobConfig } from "../../config/providers.js";
3
+ import { type KubernetesApi } from "./kubernetes-api.js";
4
+ import type { JobHandle, JobResult, JobSpec, JobStatus, WorkspaceJobLauncher } from "./types.js";
5
+ export interface KubernetesClusterInfo {
6
+ /** The proxy Service's ClusterIP: the enforcement witness's address (8787 reachable, 8788 not). */
7
+ proxyIp: string;
8
+ }
9
+ export interface KubernetesJobLauncherOptions {
10
+ api: KubernetesApi;
11
+ config: KubernetesJobConfig;
12
+ workspaceRoot: string;
13
+ resolveCapability: (runId: string) => Promise<string>;
14
+ /**
15
+ * Cluster preflight run once before the first launch; failure fails every launch. It may return the
16
+ * proxy ClusterIP it validated; otherwise the launcher reads the proxy witness once itself. Either way
17
+ * the gate probes the address `provision` re-reads for that launch, not this memoized one.
18
+ */
19
+ preflight?: () => Promise<KubernetesClusterInfo | void>;
20
+ now?: () => number;
21
+ sleep?: (milliseconds: number) => Promise<void>;
22
+ readyTimeoutMs?: number;
23
+ /** How long to wait for the run's NetworkPolicy to be enforced before seeding. Default 30_000. */
24
+ enforcementTimeoutMs?: number;
25
+ createArchive?: (directory: string) => {
26
+ stream: Readable;
27
+ done: Promise<number>;
28
+ };
29
+ onWarning?: (message: string) => void;
30
+ }
31
+ /** Archives a trusted local directory with the host's `tar`, under a minimal environment. */
32
+ export declare function hostTarArchive(directory: string): {
33
+ stream: Readable;
34
+ done: Promise<number>;
35
+ };
36
+ export declare class KubernetesJobLauncher implements WorkspaceJobLauncher {
37
+ private readonly options;
38
+ private readonly api;
39
+ private readonly namespace;
40
+ private readonly workspaceRoot;
41
+ private readonly now;
42
+ private readonly sleep;
43
+ private readonly readyTimeoutMs;
44
+ private readonly createArchive;
45
+ private readonly warn;
46
+ private readonly enforcementTimeoutMs;
47
+ private preflightResult?;
48
+ constructor(options: KubernetesJobLauncherOptions);
49
+ /**
50
+ * Every per-run object name derives from the run id, so the handle is known before any cluster call.
51
+ * The executor persists it first; a crash mid-launch then still has a handle to clean up with (the
52
+ * launcher itself has no `list` permission and so can never rediscover stray objects).
53
+ */
54
+ plannedHandle(spec: JobSpec): JobHandle | undefined;
55
+ launch(spec: JobSpec): Promise<JobHandle>;
56
+ status(handle: JobHandle): Promise<JobStatus>;
57
+ collect(handle: JobHandle): Promise<JobResult>;
58
+ materializeWorkspace(handle: JobHandle, destination: string): Promise<void>;
59
+ stop(handle: JobHandle, _reason?: string): Promise<void>;
60
+ remove(handle: JobHandle): Promise<void>;
61
+ /** Memoized: the preflight (or, without one, a single proxy-witness read) runs once per launcher. */
62
+ private runPreflight;
63
+ /**
64
+ * Waits until the run's NetworkPolicy is enforced on this pod: the keeper (same network namespace
65
+ * as the worker) must, on ENFORCEMENT_BLOCKED_STREAK consecutive probes 500 ms apart, reach the
66
+ * proxy on CODING_PROXY_PORT and fail to reach it on CODING_PROXY_DENY_PORT. Anything else resets
67
+ * the streak; the wall-clock bound still applies.
68
+ *
69
+ * "Fail to reach" means the connect *timed out* — the packet was dropped. A refusal (RST) is not
70
+ * a denial: it proves the SYN reached the destination host, so the deny port is merely unserved
71
+ * and witnesses nothing. That is its own verdict, below.
72
+ *
73
+ * The verdict at the bound is taken from the *last* probe, not from whether any probe was ever
74
+ * unavailable: an early blip while the pod's networking came up must not send an operator looking
75
+ * at the proxy when the real problem is an unenforced policy. The three non-proven outcomes stay
76
+ * distinct because they send an operator to three different places: the CNI, the proxy pod, and
77
+ * the deny listener.
78
+ *
79
+ * `stage` only changes which error codes a bound produces (see `EnforcementStage`); the probe
80
+ * machinery itself — the script, the streak, the poll interval, the wall-clock bound — is identical
81
+ * for the initial proof and for `provision`'s later re-confirmation immediately before the marker
82
+ * write. That re-confirmation deliberately requires the same full `ENFORCEMENT_BLOCKED_STREAK`
83
+ * consecutive proven probes, not a single one-shot probe: seeding (the window it's guarding) can run
84
+ * for real hours, so the few hundred extra milliseconds a streak costs in the common, still-enforced
85
+ * case is negligible, while a single probe could pass on a lucky sample the same way one dropped SYN
86
+ * must not open the gate at the initial proof either.
87
+ */
88
+ private waitForPolicyEnforcement;
89
+ private existingLaunch;
90
+ private provision;
91
+ private createIfMissing;
92
+ private waitForKeeper;
93
+ private runWorkspace;
94
+ /** Streams a trusted local directory into `destination` inside the keeper. */
95
+ private seedDirectory;
96
+ private seedInput;
97
+ private cleanupRun;
98
+ /** Observes a non-terminal run against its pod and persists any transition; returns the stored record. */
99
+ private refresh;
100
+ /** Reads only a fixed diagnostic code from a failed worker's bounded log tail; never keeps anything else. */
101
+ private readWorkerDiagnostic;
102
+ private readResultArtifact;
103
+ /**
104
+ * Streams the keeper's workspace through the strict extractor into `staging`.
105
+ * The transfer is bounded by `timeoutMs` (see transferBudgetMs): on expiry the
106
+ * extraction is aborted. On any failure the output stream this method created
107
+ * is destroyed, since the exec's own timeout does not end it.
108
+ */
109
+ private extractWorkspace;
110
+ /**
111
+ * Time allowed for one workspace/input transfer: the run's full timeout
112
+ * (deadlineAt - createdAt), measured from when the transfer starts. The
113
+ * remaining run deadline is not used because collection legitimately
114
+ * happens after a run has finished close to (or past) its deadline.
115
+ */
116
+ private transferBudgetMs;
117
+ private namesFor;
118
+ private recordConfigMap;
119
+ private readRecord;
120
+ /**
121
+ * Applies a transition with optimistic concurrency, retrying on conflict.
122
+ * `transition` sees the freshly read record and returns the new phase/result
123
+ * (or `undefined` for no change). A terminal record never moves to another
124
+ * phase except `removed`; it may only have its result filled in.
125
+ */
126
+ private updateRecord;
127
+ }