@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,607 @@
1
+ /**
2
+ * Canonical Kubernetes isolation policy for one Codex coding run, mirroring
3
+ * docker-isolation.ts. Nothing else constructs run pods or policies; the
4
+ * launcher reads every object back and attests it against these builders
5
+ * before the worker is allowed to start (the seeded-marker gate).
6
+ */
7
+ import { createHash } from "node:crypto";
8
+ import { isIP } from "node:net";
9
+ import { CODING_PROXY_ALIAS, CODING_PROXY_DENY_PORT, CODING_PROXY_PORT, CODING_WORKER_GID, CODING_WORKER_UID, WORKER_STOP_GRACE_SECONDS, isRepositoryDigest, } from "./docker-isolation.js";
10
+ import { GVISOR_RUNTIME_CLASS, KubernetesPlatformError, STORAGE_INIT_EPHEMERAL_MIB, WORKER_EPHEMERAL_MIB, conformResources, describeMib, normalizePlatformMetadata, platformProfile, podEphemeralStorageMib, } from "./kubernetes-platform.js";
11
+ export const KUBERNETES_ISOLATION_ERROR = "kubernetes_isolation_unsupported";
12
+ export const KUBERNETES_PROVIDER_UNSUPPORTED = "kubernetes_provider_unsupported";
13
+ /** Extra seconds past timeoutSec before Kubernetes kills the pod (keeper included). */
14
+ export const POD_DEADLINE_GRACE_SECONDS = 300;
15
+ export const KEEPER_CONTAINER = "keeper";
16
+ export const STORAGE_INIT_CONTAINER = "storage-init";
17
+ export const WORKER_CONTAINER = "worker";
18
+ export const STORAGE_ROOT = "/run/wardby/storage";
19
+ export const KEEPER_SEEDED_MARKER = `${STORAGE_ROOT}/input/.seeded`;
20
+ export const PROXY_POD_LABEL = { "app.kubernetes.io/name": "wardby-coding-proxy" };
21
+ /**
22
+ * What every run pod carries and what the proxy's own ingress rule must admit on the deny port.
23
+ * Single source: `runLabels` stamps it, `readProxyWitness` checks the proxy admits it.
24
+ */
25
+ export const RUN_COMPONENT_LABEL = { "wardby.io/component": "coding-run" };
26
+ /** The probe proved enforcement: the proxy port connected and the deny port was blocked. */
27
+ export const ENFORCEMENT_PROBE_PROVEN = 0;
28
+ /** The deny port was reachable: no policy is blocking it, or the policy is not port-scoped. */
29
+ export const ENFORCEMENT_PROBE_DENY_REACHABLE = 3;
30
+ /** The proxy port itself was unreachable: nothing could be witnessed (proxy down, or only the namespace default-deny is programmed). */
31
+ export const ENFORCEMENT_PROBE_PROXY_UNREACHABLE = 4;
32
+ /**
33
+ * The deny port answered with an RST: the SYN reached the destination host, so nothing is
34
+ * blocking the path — the port is reachable-but-unserved and witnesses nothing.
35
+ */
36
+ export const ENFORCEMENT_PROBE_DENY_REFUSED = 5;
37
+ const WORKER_SERVICE_ACCOUNT = "wardby-coding-worker";
38
+ const RUN_ID = /^[a-zA-Z0-9][a-zA-Z0-9_.:-]{0,199}$/;
39
+ /** The worker waits for the launcher's seeded marker, then runs the image's normal entrypoint. */
40
+ const WORKER_GATE = [
41
+ 'const fs = require("node:fs");',
42
+ 'const marker = "/run/wardby/input/.seeded";',
43
+ "(function wait() {",
44
+ " if (fs.existsSync(marker)) {",
45
+ ' import("/opt/wardby/coding-worker/main.js").catch(() => { process.exitCode = 1; });',
46
+ " } else {",
47
+ " setTimeout(wait, 250);",
48
+ " }",
49
+ "})();",
50
+ ].join("\n");
51
+ /**
52
+ * Creates the worker's subPath mount sources, owned by the run uid, before any regular container
53
+ * starts. Otherwise the kubelet creates them root-owned while setting up the worker's subPath mounts,
54
+ * and the keeper (uid 10001, no capabilities) can't chmod them. Idempotent; no shell.
55
+ */
56
+ const STORAGE_INIT_SCRIPT = [
57
+ 'const fs = require("node:fs");',
58
+ 'for (const name of ["workspace", "input", "output"]) {',
59
+ ` const path = ${JSON.stringify(STORAGE_ROOT)} + "/" + name;`,
60
+ " fs.mkdirSync(path, { recursive: true, mode: 0o700 });",
61
+ " fs.chmodSync(path, 0o700);",
62
+ "}",
63
+ ].join("\n");
64
+ function isolationError() {
65
+ return new Error(KUBERNETES_ISOLATION_ERROR);
66
+ }
67
+ function sha256(value) {
68
+ return createHash("sha256").update(value).digest("hex");
69
+ }
70
+ /**
71
+ * The per-run object names for a run token. The only place the naming scheme lives: the launcher
72
+ * resolves a handle's token through this, so names can never drift between creation and cleanup.
73
+ */
74
+ export function kubernetesRunNamesForToken(token) {
75
+ const base = `wardby-run-${token}`;
76
+ return { token, pod: base, policy: base, record: base, secret: `${base}-cap` };
77
+ }
78
+ export function kubernetesRunNames(runId) {
79
+ if (!RUN_ID.test(runId))
80
+ throw isolationError();
81
+ const digest = sha256(runId);
82
+ return { ...kubernetesRunNamesForToken(digest.slice(0, 20)), runSha: digest.slice(0, 40) };
83
+ }
84
+ export function runLabels(runId) {
85
+ return {
86
+ "app.kubernetes.io/managed-by": "wardby",
87
+ ...RUN_COMPONENT_LABEL,
88
+ "wardby.io/run-sha256": kubernetesRunNames(runId).runSha,
89
+ };
90
+ }
91
+ /** Same grammar as Docker's repository digests; a cluster cannot pull a bare local image ID. */
92
+ export function isRegistryDigest(image) {
93
+ return isRepositoryDigest(image);
94
+ }
95
+ function inRange(value, min, max, integer) {
96
+ return Number.isFinite(value) && value >= min && value <= max && (!integer || Number.isInteger(value));
97
+ }
98
+ /** Whether `value` is within floating-point rounding error of an integer. */
99
+ function isNearInteger(value, tolerance = 1e-9) {
100
+ return Math.abs(Math.round(value) - value) < tolerance;
101
+ }
102
+ /** Kubernetes CPU requests/limits are always whole millicores; a fractional millicore can't be expressed. */
103
+ function isWholeMillicores(cpus) {
104
+ return isNearInteger(cpus * 1000);
105
+ }
106
+ export function validateKubernetesSpec(spec) {
107
+ if (spec.kind !== "coding-agent" || !RUN_ID.test(spec.runId))
108
+ throw isolationError();
109
+ if (spec.provider === "claude-code")
110
+ throw new Error(KUBERNETES_PROVIDER_UNSUPPORTED);
111
+ if (spec.provider !== undefined && spec.provider !== "codex")
112
+ throw isolationError();
113
+ if (spec.toolImage !== undefined || !isRegistryDigest(spec.image))
114
+ throw isolationError();
115
+ const { cpus, memoryMb, pids, diskMb } = spec.limits;
116
+ if (!inRange(cpus, 0.1, 32, false) ||
117
+ !isWholeMillicores(cpus) ||
118
+ !inRange(memoryMb, 128, 65_536, true) ||
119
+ !inRange(pids, 16, 4_096, true) ||
120
+ !inRange(diskMb, 64, 32_768, true) ||
121
+ !inRange(spec.timeoutSec, 1, 86_400, true)) {
122
+ throw isolationError();
123
+ }
124
+ }
125
+ function containerSecurity() {
126
+ return {
127
+ allowPrivilegeEscalation: false,
128
+ privileged: false,
129
+ readOnlyRootFilesystem: true,
130
+ runAsNonRoot: true,
131
+ capabilities: { drop: ["ALL"] },
132
+ };
133
+ }
134
+ export function buildRunPod(spec, options) {
135
+ validateKubernetesSpec(spec);
136
+ const profile = platformProfile(options.platform ?? "generic");
137
+ // Symmetric with the ceiling check below: the builder is the one place that actually emits the
138
+ // pod, so a platform that requires gVisor must refuse to build one without it here too, not rely
139
+ // solely on composition/preflight having already checked. Otherwise buildRunPod(spec, { platform:
140
+ // "gke-autopilot" }) with no runtimeClassName would silently emit runtimeClassName: undefined.
141
+ if (profile.requiresGvisor && options.runtimeClassName !== GVISOR_RUNTIME_CLASS) {
142
+ throw new KubernetesPlatformError(`platform ${profile.name} requires runtimeClassName=${GVISOR_RUNTIME_CLASS} (found ${options.runtimeClassName ?? "unset"})`);
143
+ }
144
+ // conformResources range-checks one container at a time; only this function sees every container,
145
+ // so the SUMMED pod total is checked here — before submission, so an over-large workspace fails
146
+ // closed rather than being rewritten by the platform (which attestation would then reject anyway).
147
+ const ceiling = profile.resources.ephemeralStorageCeilingMib;
148
+ const podEphemeral = podEphemeralStorageMib(spec.limits.diskMb);
149
+ if (ceiling !== undefined && podEphemeral > ceiling) {
150
+ throw new KubernetesPlatformError(`a ${spec.limits.diskMb} MiB workspace needs ${podEphemeral} MiB of pod ephemeral storage, over the ${describeMib(ceiling)} ceiling of platform ${profile.name}`);
151
+ }
152
+ const names = kubernetesRunNames(spec.runId);
153
+ const scratchMb = Math.max(16, Math.min(64, Math.floor(spec.limits.memoryMb / 8)));
154
+ const sidecarCpuMillicores = 250;
155
+ const sidecarMemoryMib = 128;
156
+ const storageInit = {
157
+ name: STORAGE_INIT_CONTAINER,
158
+ image: spec.image,
159
+ command: ["node", "-e", STORAGE_INIT_SCRIPT],
160
+ securityContext: containerSecurity(),
161
+ resources: conformResources(profile, {
162
+ cpuMillicores: sidecarCpuMillicores,
163
+ memoryMib: sidecarMemoryMib,
164
+ ephemeralStorageMib: STORAGE_INIT_EPHEMERAL_MIB,
165
+ }),
166
+ volumeMounts: [{ name: "storage", mountPath: STORAGE_ROOT }],
167
+ };
168
+ const keeper = {
169
+ name: KEEPER_CONTAINER,
170
+ image: spec.image,
171
+ command: ["node", "/opt/wardby/coding-worker/keeper.js"],
172
+ securityContext: containerSecurity(),
173
+ resources: conformResources(profile, {
174
+ cpuMillicores: sidecarCpuMillicores,
175
+ memoryMib: sidecarMemoryMib,
176
+ // The keeper owns the storage volume: seeding and collection stream through it.
177
+ ephemeralStorageMib: spec.limits.diskMb,
178
+ }),
179
+ volumeMounts: [{ name: "storage", mountPath: STORAGE_ROOT }],
180
+ readinessProbe: { exec: { command: ["test", "-d", `${STORAGE_ROOT}/output`] }, periodSeconds: 1 },
181
+ };
182
+ const worker = {
183
+ name: WORKER_CONTAINER,
184
+ image: spec.image,
185
+ command: ["node", "-e", WORKER_GATE],
186
+ env: [
187
+ { name: "WARDBY_PROXY_URL", value: `http://${CODING_PROXY_ALIAS}:${CODING_PROXY_PORT}` },
188
+ { name: "WARDBY_RUN_CAPABILITY", valueFrom: { secretKeyRef: { name: names.secret, key: "capability" } } },
189
+ ],
190
+ securityContext: containerSecurity(),
191
+ resources: conformResources(profile, {
192
+ cpuMillicores: Math.round(spec.limits.cpus * 1000),
193
+ memoryMib: spec.limits.memoryMb,
194
+ ephemeralStorageMib: WORKER_EPHEMERAL_MIB,
195
+ }),
196
+ volumeMounts: [
197
+ { name: "storage", mountPath: "/workspace", subPath: "workspace" },
198
+ { name: "storage", mountPath: "/run/wardby/input", subPath: "input", readOnly: true },
199
+ { name: "storage", mountPath: "/run/wardby/output", subPath: "output" },
200
+ { name: "tmp", mountPath: "/tmp" },
201
+ { name: "home", mountPath: "/home/wardby" },
202
+ ],
203
+ };
204
+ return {
205
+ apiVersion: "v1",
206
+ kind: "Pod",
207
+ metadata: {
208
+ name: names.pod,
209
+ namespace: options.namespace,
210
+ labels: runLabels(spec.runId),
211
+ annotations: { "wardby.io/run-id": spec.runId },
212
+ },
213
+ spec: {
214
+ restartPolicy: "Never",
215
+ automountServiceAccountToken: false,
216
+ serviceAccountName: WORKER_SERVICE_ACCOUNT,
217
+ enableServiceLinks: false,
218
+ hostNetwork: false,
219
+ hostPID: false,
220
+ hostIPC: false,
221
+ shareProcessNamespace: false,
222
+ // Backstop only: the launcher enforces the wall-clock deadline; the grace leaves a collection window.
223
+ activeDeadlineSeconds: spec.timeoutSec + POD_DEADLINE_GRACE_SECONDS,
224
+ terminationGracePeriodSeconds: WORKER_STOP_GRACE_SECONDS,
225
+ dnsPolicy: "None",
226
+ dnsConfig: { nameservers: ["127.0.0.1"] },
227
+ hostAliases: [{ ip: options.proxyIp, hostnames: [CODING_PROXY_ALIAS] }],
228
+ ...(options.runtimeClassName ? { runtimeClassName: options.runtimeClassName } : {}),
229
+ securityContext: {
230
+ runAsNonRoot: true,
231
+ runAsUser: CODING_WORKER_UID,
232
+ runAsGroup: CODING_WORKER_GID,
233
+ fsGroup: CODING_WORKER_GID,
234
+ seccompProfile: { type: "RuntimeDefault" },
235
+ },
236
+ volumes: [
237
+ { name: "storage", emptyDir: { sizeLimit: `${spec.limits.diskMb}Mi` } },
238
+ { name: "tmp", emptyDir: { medium: "Memory", sizeLimit: `${scratchMb}Mi` } },
239
+ { name: "home", emptyDir: { medium: "Memory", sizeLimit: `${scratchMb}Mi` } },
240
+ ],
241
+ initContainers: [storageInit],
242
+ containers: [keeper, worker],
243
+ },
244
+ };
245
+ }
246
+ export function buildRunNetworkPolicy(spec, namespace) {
247
+ const names = kubernetesRunNames(spec.runId);
248
+ return {
249
+ apiVersion: "networking.k8s.io/v1",
250
+ kind: "NetworkPolicy",
251
+ metadata: { name: names.policy, namespace, labels: runLabels(spec.runId) },
252
+ spec: {
253
+ podSelector: { matchLabels: runLabels(spec.runId) },
254
+ policyTypes: ["Ingress", "Egress"],
255
+ ingress: [],
256
+ egress: [
257
+ {
258
+ to: [{ podSelector: { matchLabels: { ...PROXY_POD_LABEL } } }],
259
+ ports: [{ protocol: "TCP", port: CODING_PROXY_PORT }],
260
+ },
261
+ ],
262
+ },
263
+ };
264
+ }
265
+ export function buildCapabilitySecret(spec, namespace, capability) {
266
+ const names = kubernetesRunNames(spec.runId);
267
+ return {
268
+ apiVersion: "v1",
269
+ kind: "Secret",
270
+ type: "Opaque",
271
+ metadata: { name: names.secret, namespace, labels: runLabels(spec.runId) },
272
+ stringData: { capability },
273
+ };
274
+ }
275
+ /**
276
+ * Deny-by-default attestation. Rather than allowlisting the fields we expect
277
+ * to see (which silently accepts anything the allowlist forgot — lifecycle
278
+ * hooks, probes that reach the node's link-local metadata endpoint,
279
+ * seLinuxOptions/appArmorProfile/procMount escapes, extra tolerations,
280
+ * stray annotations, ...), we deep-compare the *entire* spec, labels, and
281
+ * annotations, and only normalize the exact fields the Kubernetes API
282
+ * server itself is known to default or reorder on read-back. Every other
283
+ * field — every security-relevant one included — must match exactly.
284
+ */
285
+ /**
286
+ * CPU quantities are always whole millicores; normalize "1", "1.0", and
287
+ * "1000m" to the same string. Rounding only absorbs float noise (the
288
+ * `isNearInteger` tolerance) — a value that's genuinely fractional at the
289
+ * millicore scale (e.g. "1000.4m", "0.9996") is not a legitimate Kubernetes
290
+ * quantity for something we build, so it's mapped to a sentinel that can
291
+ * never equal a real builder value, making the comparison fail closed
292
+ * instead of silently rounding two different resource requests together.
293
+ */
294
+ function cpuMillicores(value) {
295
+ const text = String(value).trim();
296
+ const milli = /^([0-9]*\.?[0-9]+)m$/.exec(text);
297
+ if (milli) {
298
+ const millis = Number(milli[1]);
299
+ return isNearInteger(millis) ? `${Math.round(millis)}m` : `invalid:${text}`;
300
+ }
301
+ const plain = /^([0-9]*\.?[0-9]+)$/.exec(text);
302
+ if (plain) {
303
+ const millis = Number(plain[1]) * 1000;
304
+ return isNearInteger(millis) ? `${Math.round(millis)}m` : `invalid:${text}`;
305
+ }
306
+ return text;
307
+ }
308
+ const MEMORY_BINARY_UNITS = {
309
+ Ki: 1024,
310
+ Mi: 1024 ** 2,
311
+ Gi: 1024 ** 3,
312
+ Ti: 1024 ** 4,
313
+ Pi: 1024 ** 5,
314
+ Ei: 1024 ** 6,
315
+ };
316
+ const MEMORY_DECIMAL_UNITS = { K: 1e3, M: 1e6, G: 1e9, T: 1e12, P: 1e15, E: 1e18 };
317
+ /**
318
+ * Byte-denominated quantities (memory and ephemeral-storage) can use binary or
319
+ * decimal suffixes; normalize every form to a byte count, failing closed on a
320
+ * non-integer byte count (see `cpuMillicores`).
321
+ */
322
+ function memoryBytes(value) {
323
+ const text = String(value).trim();
324
+ const match = /^([0-9]*\.?[0-9]+)(Ki|Mi|Gi|Ti|Pi|Ei|K|M|G|T|P|E)?$/.exec(text);
325
+ if (!match)
326
+ return text;
327
+ const amount = Number(match[1]);
328
+ const unit = match[2];
329
+ let bytes;
330
+ if (unit && unit in MEMORY_BINARY_UNITS)
331
+ bytes = amount * MEMORY_BINARY_UNITS[unit];
332
+ else if (unit && unit in MEMORY_DECIMAL_UNITS)
333
+ bytes = amount * MEMORY_DECIMAL_UNITS[unit];
334
+ else
335
+ bytes = amount;
336
+ return isNearInteger(bytes) ? String(Math.round(bytes)) : `invalid:${text}`;
337
+ }
338
+ /** The two node-health tolerations every pod gets by admission-time default; anything else must match exactly. */
339
+ const DEFAULT_TOLERATIONS = [
340
+ { key: "node.kubernetes.io/not-ready", operator: "Exists", effect: "NoExecute", tolerationSeconds: 300 },
341
+ { key: "node.kubernetes.io/unreachable", operator: "Exists", effect: "NoExecute", tolerationSeconds: 300 },
342
+ ];
343
+ function isDefaultToleration(t) {
344
+ return DEFAULT_TOLERATIONS.some((d) => t.key === d.key &&
345
+ t.operator === d.operator &&
346
+ t.effect === d.effect &&
347
+ t.tolerationSeconds === d.tolerationSeconds);
348
+ }
349
+ /**
350
+ * Normalizes only the *rendering* of a quantity, never its value: two spellings
351
+ * of the same number compare equal, two different numbers never do.
352
+ *
353
+ * `ephemeral-storage` is canonicalized for the same reason memory is, and the
354
+ * reason is not cosmetic. Go's `resource.Quantity` keeps the string it was
355
+ * parsed from and re-serializes it verbatim — but only while that cached string
356
+ * survives. Any mutation of the resource block drops it, and the value is then
357
+ * re-rendered in canonical binary form, so the `1024Mi` we submit comes back as
358
+ * `1Gi`. A platform whose admission controller rewrites resources by design
359
+ * (Autopilot's warden) would therefore fail attestation on a quantity that never
360
+ * actually changed, with nothing but `kubernetes_isolation_unsupported` to go on.
361
+ * Comparing byte counts removes that failure mode without losing any strictness:
362
+ * a genuinely different reservation is still a different number.
363
+ */
364
+ function normalizeResources(r) {
365
+ if (!r)
366
+ return;
367
+ for (const bag of [r.requests, r.limits]) {
368
+ if (!bag)
369
+ continue;
370
+ if (bag.cpu !== undefined)
371
+ bag.cpu = cpuMillicores(bag.cpu);
372
+ if (bag.memory !== undefined)
373
+ bag.memory = memoryBytes(bag.memory);
374
+ if (bag["ephemeral-storage"] !== undefined)
375
+ bag["ephemeral-storage"] = memoryBytes(bag["ephemeral-storage"]);
376
+ }
377
+ }
378
+ function normalizeProbe(p) {
379
+ if (!p)
380
+ return;
381
+ p.timeoutSeconds ??= 1;
382
+ p.successThreshold ??= 1;
383
+ p.failureThreshold ??= 3;
384
+ p.periodSeconds ??= 10;
385
+ }
386
+ function normalizeContainer(c) {
387
+ delete c.terminationMessagePath;
388
+ delete c.terminationMessagePolicy;
389
+ delete c.imagePullPolicy;
390
+ normalizeResources(c.resources);
391
+ normalizeProbe(c.readinessProbe);
392
+ normalizeProbe(c.livenessProbe);
393
+ normalizeProbe(c.startupProbe);
394
+ for (const mount of c.volumeMounts ?? []) {
395
+ if (mount.mountPropagation === "None")
396
+ delete mount.mountPropagation;
397
+ }
398
+ }
399
+ function normalizeVolume(v) {
400
+ if (v.emptyDir) {
401
+ if (v.emptyDir.medium === "")
402
+ delete v.emptyDir.medium;
403
+ if (v.emptyDir.sizeLimit !== undefined)
404
+ v.emptyDir.sizeLimit = memoryBytes(v.emptyDir.sizeLimit);
405
+ }
406
+ }
407
+ /**
408
+ * Applies only the normalizations the Kubernetes API server itself performs
409
+ * (defaulting or aliasing fields on write/read). Everything else in the
410
+ * spec — including every security-relevant field — is left untouched so
411
+ * the caller's deep-equality check sees it.
412
+ */
413
+ function normalizeSpec(spec) {
414
+ if (!Array.isArray(spec.containers))
415
+ throw isolationError();
416
+ delete spec.schedulerName;
417
+ delete spec.nodeName;
418
+ delete spec.priority;
419
+ delete spec.preemptionPolicy;
420
+ // Go's `omitempty` drops a plain bool at its zero value (false) on serialization, so a
421
+ // genuine API read-back never has these fields when they're false — only when true.
422
+ if (spec.hostNetwork === false)
423
+ delete spec.hostNetwork;
424
+ if (spec.hostPID === false)
425
+ delete spec.hostPID;
426
+ if (spec.hostIPC === false)
427
+ delete spec.hostIPC;
428
+ if (spec.serviceAccount !== undefined) {
429
+ if (spec.serviceAccount !== spec.serviceAccountName)
430
+ throw isolationError();
431
+ delete spec.serviceAccount;
432
+ }
433
+ if (spec.tolerations) {
434
+ const remaining = spec.tolerations.filter((t) => !isDefaultToleration(t));
435
+ if (remaining.length === 0)
436
+ delete spec.tolerations;
437
+ else
438
+ spec.tolerations = remaining;
439
+ }
440
+ for (const container of spec.containers)
441
+ normalizeContainer(container);
442
+ for (const container of spec.initContainers ?? [])
443
+ normalizeContainer(container);
444
+ for (const volume of spec.volumes ?? [])
445
+ normalizeVolume(volume);
446
+ }
447
+ /**
448
+ * A copy of `pod` with ONLY the API server's own defaulting and aliasing undone
449
+ * — the same `normalizeSpec` pass `assertRunPodMatches` runs, and nothing else.
450
+ *
451
+ * Exported for src/tools/capture-fixture.ts, which runs it over both the
452
+ * submitted and the returned pod before diffing them. Without it the diff is
453
+ * dominated by fields every API server fills in on every create
454
+ * (schedulerName, priority, terminationMessagePath on each container, probe
455
+ * defaults, `2048Mi` requantized to `2Gi`, ...); because the differ replaces
456
+ * arrays wholesale those collapse into opaque `replace /spec/containers`
457
+ * blobs, and a genuine platform rewrite *inside* a container would be
458
+ * indistinguishable from the noise.
459
+ *
460
+ * It deliberately does NOT apply `normalizePlatformMetadata`: that deletes
461
+ * exactly the platform-injected keys a capture exists to record. Fails closed
462
+ * (throws) on a spec the normalizer cannot make sense of.
463
+ */
464
+ export function undoApiServerDefaults(pod) {
465
+ if (!pod.spec)
466
+ throw isolationError();
467
+ const spec = structuredClone(pod.spec);
468
+ normalizeSpec(spec);
469
+ return { ...structuredClone(pod), spec };
470
+ }
471
+ function normalizePod(pod, profile) {
472
+ if (!pod.spec)
473
+ throw isolationError();
474
+ // Every field the normalizer may touch must already be a copy: normalizePlatformMetadata
475
+ // mutates `view.spec` in place, and replaces the two bags with copies of its own.
476
+ const spec = structuredClone(pod.spec);
477
+ normalizeSpec(spec);
478
+ const view = {
479
+ labels: structuredClone(pod.metadata?.labels ?? {}),
480
+ annotations: structuredClone(pod.metadata?.annotations ?? {}),
481
+ spec,
482
+ };
483
+ // Applied to BOTH operands, symmetrically: deletes only the keys the profile names,
484
+ // and the profile for "generic" names none. It never skips a field the comparison
485
+ // would otherwise see, so everything left is still deep-compared below.
486
+ normalizePlatformMetadata(profile, view);
487
+ return view;
488
+ }
489
+ /**
490
+ * Recursively sorts object keys and drops `undefined` values so key order and API-omitted
491
+ * fields never matter; array order is preserved.
492
+ *
493
+ * Exported for kubernetes-dry-run-fixture.ts's `diffMutations`: a captured pod's nested objects
494
+ * (container resource requests, volume definitions, host aliases, ...) come back from a real
495
+ * cluster with different key insertion order than `buildRunPod`'s own object literals, even when
496
+ * every value is identical. Without canonicalizing first, `diffMutations`' array branch treats
497
+ * that key-order difference as a genuine change and replaces the WHOLE array (every container,
498
+ * every volume) with an opaque blob — exactly the failure mode its own doc comment warns a real
499
+ * platform rewrite could hide inside. `assertRunPodMatches` below is unaffected by this (it
500
+ * already canonicalizes before comparing); only the capture tool's mutation list was at risk.
501
+ */
502
+ export function canonical(value) {
503
+ if (Array.isArray(value))
504
+ return value.map((item) => canonical(item));
505
+ if (value !== null && typeof value === "object") {
506
+ const entries = Object.entries(value)
507
+ .filter(([, v]) => v !== undefined)
508
+ .map(([key, v]) => [key, canonical(v)])
509
+ .sort(([a], [b]) => a.localeCompare(b));
510
+ return Object.fromEntries(entries);
511
+ }
512
+ return value;
513
+ }
514
+ /**
515
+ * Deep-compares the pod the API server read back against the pod wardby built.
516
+ * ANY surviving difference fails the run closed.
517
+ *
518
+ * `platform` defaults to "generic" on purpose: a caller that forgets it gets the
519
+ * strictest behaviour, never the most permissive. A profile can only narrow what
520
+ * counts as an expected difference (by deleting named keys from both operands);
521
+ * it can never disable or short-circuit the comparison.
522
+ */
523
+ export function assertRunPodMatches(actual, expected, platform = "generic") {
524
+ const profile = platformProfile(platform);
525
+ const a = canonical(normalizePod(actual, profile));
526
+ const e = canonical(normalizePod(expected, profile));
527
+ if (JSON.stringify(a) !== JSON.stringify(e))
528
+ throw isolationError();
529
+ }
530
+ /** Go's `omitempty` drops an empty slice on serialization, so a genuine API read-back omits `ingress` when it's `[]` — only a populated ingress rule list survives. */
531
+ function normalizeNetworkPolicySpec(spec) {
532
+ if (Array.isArray(spec.ingress) && spec.ingress.length === 0)
533
+ delete spec.ingress;
534
+ }
535
+ export function assertRunNetworkPolicyMatches(actual, expected) {
536
+ const view = (p) => {
537
+ const spec = structuredClone(p.spec ?? {});
538
+ normalizeNetworkPolicySpec(spec);
539
+ return { labels: p.metadata?.labels ?? {}, spec };
540
+ };
541
+ if (JSON.stringify(canonical(view(actual))) !== JSON.stringify(canonical(view(expected))))
542
+ throw isolationError();
543
+ }
544
+ /**
545
+ * A `node -e` script (argv only, never a shell) that measures BOTH of the proxy's
546
+ * ports in one pass, with a 3 s connect timeout each (above Linux's 1 s initial
547
+ * SYN retransmission, so one dropped SYN on an allowed path still connects).
548
+ *
549
+ * Measuring both is what makes the result decisive. A NetworkPolicy denial drops
550
+ * the packet rather than rejecting it — GKE Dataplane V2, which Autopilot runs,
551
+ * always drops — so "the deny port did not answer" is equally consistent with
552
+ * "the proxy is gone and no policy exists at all". Requiring the proxy port to
553
+ * connect in the *same* probe turns "something is listening" from a control-plane
554
+ * inference into a fact this pod just observed.
555
+ *
556
+ * That pairing alone is still not enough, and this was reproduced on a live
557
+ * cluster: in a namespace with NO NetworkPolicy at all, against a pod listening
558
+ * on 8787 and serving nothing on 8788, a probe that collapsed `error` and
559
+ * `timeout` into one "not reachable" exited PROVEN while it had full internet
560
+ * egress. The pairing only rules out "the whole proxy pod is dead"; whenever the
561
+ * deny *listener specifically* is unserved, "blocked" and "nothing there" are
562
+ * the same observation. No control-plane read fixes this — Endpoints subset
563
+ * ports come from the Service's numeric targetPort, not from anything actually
564
+ * binding — so the distinction has to be made in the dataplane, here:
565
+ *
566
+ * - deny port **times out** → the packet was dropped somewhere on the path → the only outcome
567
+ * that can prove a policy. Note what it does NOT prove on its own: *which* hop dropped it. That
568
+ * it was the run pod's own egress policy follows from the proxy admitting run pods on the deny
569
+ * port at its own ingress, leaving no other hop that would drop it — a precondition the
570
+ * preflight's `proxy-service` check now verifies (readProxyWitness), after a live cluster
571
+ * falsified the assumption: with the drop moved to the destination, a prober holding no policy
572
+ * at all and with full internet egress read PROVEN.
573
+ * - deny port **refused** (RST / ECONNREFUSED) → the SYN reached the destination host, so
574
+ * nothing blocked the path. This holds on every dataplane, drop-based ones included, because
575
+ * a drop cannot produce an RST. Reported as ENFORCEMENT_PROBE_DENY_REFUSED, never as proven.
576
+ * - proxy port must still CONNECT for anything to count at all.
577
+ *
578
+ * Consequence, intended: on a **reject-style** CNI a genuine policy denial also arrives as an
579
+ * RST, so such a cluster now fails closed here rather than passing vacuously. Failing closed on
580
+ * a cluster whose refusals are ambiguous is the correct direction — a witness that cannot tell
581
+ * "denied" from "unserved" is not a witness — and such a cluster needs a different one.
582
+ *
583
+ * Exit codes are ENFORCEMENT_PROBE_PROVEN / _DENY_REACHABLE / _PROXY_UNREACHABLE / _DENY_REFUSED.
584
+ * The IP is validated and embedded as a JSON string literal.
585
+ */
586
+ export function enforcementProbeScript(proxyIp) {
587
+ if (isIP(proxyIp) === 0)
588
+ throw isolationError();
589
+ return [
590
+ 'const net = require("node:net");',
591
+ "const tcp = (port) =>",
592
+ " new Promise((done) => {",
593
+ " const socket = net.connect({ host: " + JSON.stringify(proxyIp) + ", port, timeout: 3000 });",
594
+ ' socket.once("connect", () => { socket.destroy(); done("connect"); });',
595
+ ' socket.once("timeout", () => { socket.destroy(); done("timeout"); });',
596
+ ' socket.once("error", () => done("error"));',
597
+ " });",
598
+ "(async () => {",
599
+ ` const allowed = await tcp(${CODING_PROXY_PORT});`,
600
+ ` const denied = await tcp(${CODING_PROXY_DENY_PORT});`,
601
+ ` if (allowed !== "connect") process.exit(${ENFORCEMENT_PROBE_PROXY_UNREACHABLE});`,
602
+ ` if (denied === "connect") process.exit(${ENFORCEMENT_PROBE_DENY_REACHABLE});`,
603
+ ` if (denied === "error") process.exit(${ENFORCEMENT_PROBE_DENY_REFUSED});`,
604
+ ` process.exit(${ENFORCEMENT_PROBE_PROVEN});`,
605
+ "})();",
606
+ ].join("\n");
607
+ }