@wardby/cli 0.6.0 → 0.7.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 (576) hide show
  1. package/deploy/local/docker-compose.quickstart-coding.yml +13 -0
  2. package/dist/claude-coding-worker/driver.d.ts +21 -3
  3. package/dist/claude-coding-worker/driver.js +125 -17
  4. package/dist/claude-coding-worker/main.js +34 -6
  5. package/dist/claude-coding-worker/sdk.js +12 -1
  6. package/dist/claude-coding-worker/tool-socket.d.ts +27 -0
  7. package/dist/claude-coding-worker/tool-socket.js +111 -0
  8. package/dist/claude-coding-worker/transcript.d.ts +18 -0
  9. package/dist/claude-coding-worker/transcript.js +77 -0
  10. package/dist/cli-help.d.ts +1 -1
  11. package/dist/cli-help.js +1 -0
  12. package/dist/cli.js +24 -10
  13. package/dist/coding/continuation-wording.d.ts +35 -0
  14. package/dist/coding/continuation-wording.js +73 -0
  15. package/dist/coding/linked-issue-note.d.ts +23 -0
  16. package/dist/coding/linked-issue-note.js +47 -0
  17. package/dist/coding/merge-wording.d.ts +28 -0
  18. package/dist/coding/merge-wording.js +78 -0
  19. package/dist/coding/pr-description-note.d.ts +9 -0
  20. package/dist/coding/pr-description-note.js +32 -0
  21. package/dist/coding/profile.d.ts +46 -126
  22. package/dist/coding/profile.js +45 -4
  23. package/dist/coding/protected-path-wording.d.ts +8 -0
  24. package/dist/coding/protected-path-wording.js +1 -1
  25. package/dist/coding/protocol.d.ts +264 -346
  26. package/dist/coding/protocol.js +235 -12
  27. package/dist/coding/redact.d.ts +3 -0
  28. package/dist/coding/redact.js +12 -0
  29. package/dist/coding/registry/allowlist.d.ts +12 -0
  30. package/dist/coding/registry/allowlist.js +14 -1
  31. package/dist/coding/registry/audit-mode.d.ts +153 -0
  32. package/dist/coding/registry/audit-mode.js +304 -0
  33. package/dist/coding/registry/lockfile-shared.d.ts +36 -0
  34. package/dist/coding/registry/lockfile-shared.js +128 -0
  35. package/dist/coding/registry/lockfiles.js +1 -1
  36. package/dist/coding/registry/npm-lockfile-formats.d.ts +11 -0
  37. package/dist/coding/registry/npm-lockfile-formats.js +41 -0
  38. package/dist/coding/registry/npm-lockfile.d.ts +8 -4
  39. package/dist/coding/registry/npm-lockfile.js +4 -6
  40. package/dist/coding/registry/npm.js +78 -8
  41. package/dist/coding/registry/pnpm-lockfile.d.ts +17 -0
  42. package/dist/coding/registry/pnpm-lockfile.js +189 -0
  43. package/dist/coding/registry/pypi-plan.d.ts +50 -0
  44. package/dist/coding/registry/pypi-plan.js +406 -0
  45. package/dist/coding/registry/pypi-requirements.d.ts +58 -0
  46. package/dist/coding/registry/pypi-requirements.js +155 -0
  47. package/dist/coding/registry/pypi.d.ts +3 -27
  48. package/dist/coding/registry/pypi.js +40 -154
  49. package/dist/coding/registry/refusal-hints.d.ts +20 -0
  50. package/dist/coding/registry/refusal-hints.js +61 -0
  51. package/dist/coding/registry/refusal-wording.d.ts +11 -0
  52. package/dist/coding/registry/refusal-wording.js +56 -0
  53. package/dist/coding/registry/report.d.ts +70 -8
  54. package/dist/coding/registry/report.js +112 -5
  55. package/dist/coding/registry/types.d.ts +58 -8
  56. package/dist/coding/registry/yarn-lockfile.d.ts +30 -0
  57. package/dist/coding/registry/yarn-lockfile.js +367 -0
  58. package/dist/coding/services/catalog.d.ts +37 -351
  59. package/dist/coding/transcript/claude-code.d.ts +13 -0
  60. package/dist/coding/transcript/claude-code.js +273 -0
  61. package/dist/coding/transcript/download-protocol.d.ts +30 -0
  62. package/dist/coding/transcript/download-protocol.js +18 -0
  63. package/dist/coding/transcript/format.d.ts +20 -0
  64. package/dist/coding/transcript/format.js +32 -0
  65. package/dist/coding/transcript/recorder.d.ts +51 -0
  66. package/dist/coding/transcript/recorder.js +225 -0
  67. package/dist/coding/transcript/resume/checkout.d.ts +31 -0
  68. package/dist/coding/transcript/resume/checkout.js +203 -0
  69. package/dist/coding/transcript/resume/command.d.ts +17 -0
  70. package/dist/coding/transcript/resume/command.js +176 -0
  71. package/dist/coding/transcript/resume/download.d.ts +20 -0
  72. package/dist/coding/transcript/resume/download.js +153 -0
  73. package/dist/coding/transcript/resume/launch.d.ts +38 -0
  74. package/dist/coding/transcript/resume/launch.js +128 -0
  75. package/dist/coding/transcript/resume/session-file.d.ts +13 -0
  76. package/dist/coding/transcript/resume/session-file.js +79 -0
  77. package/dist/coding/transcript/resume/stand-in.d.ts +11 -0
  78. package/dist/coding/transcript/resume/stand-in.js +49 -0
  79. package/dist/coding-worker/artifact.d.ts +8 -3
  80. package/dist/coding-worker/artifact.js +3 -1
  81. package/dist/coding-worker/debug-trace.d.ts +6 -5
  82. package/dist/coding-worker/debug-trace.js +13 -18
  83. package/dist/coding-worker/dependency-cache.d.mts +229 -0
  84. package/dist/coding-worker/dependency-cache.mjs +2123 -0
  85. package/dist/coding-worker/driver.d.ts +4 -3
  86. package/dist/coding-worker/driver.js +116 -38
  87. package/dist/coding-worker/errors.d.ts +1 -0
  88. package/dist/coding-worker/errors.js +32 -0
  89. package/dist/coding-worker/task-note.d.ts +11 -0
  90. package/dist/coding-worker/task-note.js +13 -0
  91. package/dist/coding-worker/types.d.ts +11 -0
  92. package/dist/config/dependency-cache.d.ts +23 -0
  93. package/dist/config/dependency-cache.js +32 -0
  94. package/dist/config/providers.d.ts +12 -3
  95. package/dist/config/providers.js +55 -5
  96. package/dist/config/test-simulators.d.ts +82 -0
  97. package/dist/config/test-simulators.js +305 -0
  98. package/dist/core/async-context.d.ts +2 -0
  99. package/dist/core/async-context.js +12 -0
  100. package/dist/core/budget-events.d.ts +108 -0
  101. package/dist/core/budget-events.js +346 -0
  102. package/dist/core/budget-groups.d.ts +50 -1
  103. package/dist/core/budget-groups.js +236 -27
  104. package/dist/core/budget-stopped-head.d.ts +46 -0
  105. package/dist/core/budget-stopped-head.js +81 -0
  106. package/dist/core/budget-topup.d.ts +51 -0
  107. package/dist/core/budget-topup.js +156 -0
  108. package/dist/core/budget-wording.d.ts +16 -2
  109. package/dist/core/budget-wording.js +40 -2
  110. package/dist/core/coding-queue.d.ts +2 -1
  111. package/dist/core/coding-queue.js +7 -0
  112. package/dist/core/cron.d.ts +19 -1
  113. package/dist/core/cron.js +17 -0
  114. package/dist/core/datastores.js +1 -1
  115. package/dist/core/deferred-coding.d.ts +14 -0
  116. package/dist/core/deferred-coding.js +62 -0
  117. package/dist/core/delegation-child.d.ts +25 -0
  118. package/dist/core/delegation-child.js +37 -0
  119. package/dist/core/dispatch.d.ts +42 -0
  120. package/dist/core/dispatch.js +131 -56
  121. package/dist/core/engine-native.js +1 -1
  122. package/dist/core/error-code.d.ts +38 -0
  123. package/dist/core/error-code.js +168 -0
  124. package/dist/core/grants.d.ts +19 -6
  125. package/dist/core/grants.js +55 -6
  126. package/dist/core/host-events.d.ts +18 -2
  127. package/dist/core/host-events.js +284 -24
  128. package/dist/core/host-status.d.ts +85 -3
  129. package/dist/core/host-status.js +298 -20
  130. package/dist/core/issue-bridge.d.ts +50 -6
  131. package/dist/core/issue-bridge.js +124 -33
  132. package/dist/core/issue-events.d.ts +1 -1
  133. package/dist/core/issue-events.js +4 -10
  134. package/dist/core/issue-status.d.ts +1 -1
  135. package/dist/core/issue-status.js +54 -26
  136. package/dist/core/logger.d.ts +5 -0
  137. package/dist/core/logger.js +14 -1
  138. package/dist/core/lost-run-children.d.ts +6 -0
  139. package/dist/core/lost-run-children.js +32 -0
  140. package/dist/core/mark-ready.d.ts +34 -0
  141. package/dist/core/mark-ready.js +152 -0
  142. package/dist/core/markdown-code.d.ts +8 -0
  143. package/dist/core/markdown-code.js +107 -0
  144. package/dist/core/memory-tools.js +7 -0
  145. package/dist/core/merge-order-check.d.ts +2 -1
  146. package/dist/core/merge-order-check.js +8 -1
  147. package/dist/core/notification-templates.d.ts +1 -7
  148. package/dist/core/notification-templates.js +36 -2
  149. package/dist/core/pull-request-state-sync.js +2 -2
  150. package/dist/core/reconciler.d.ts +4 -3
  151. package/dist/core/reconciler.js +8 -1
  152. package/dist/core/related-pull-requests.d.ts +11 -3
  153. package/dist/core/related-pull-requests.js +27 -14
  154. package/dist/core/reply-parts.d.ts +21 -0
  155. package/dist/core/reply-parts.js +73 -0
  156. package/dist/core/review-fix.d.ts +1 -1
  157. package/dist/core/review-fix.js +2 -1
  158. package/dist/core/review-host-checks.d.ts +1 -1
  159. package/dist/core/review-host-tools.d.ts +3 -0
  160. package/dist/core/review-host-tools.js +99 -13
  161. package/dist/core/review-reads.d.ts +55 -0
  162. package/dist/core/review-reads.js +233 -0
  163. package/dist/core/run-status.d.ts +12 -0
  164. package/dist/core/run-status.js +12 -0
  165. package/dist/core/runner.d.ts +80 -34
  166. package/dist/core/runner.js +439 -144
  167. package/dist/core/schedule-start.d.ts +12 -0
  168. package/dist/core/schedule-start.js +14 -0
  169. package/dist/core/secret-broker-config.d.ts +10 -74
  170. package/dist/core/secret-guard.d.ts +46 -0
  171. package/dist/core/secret-guard.js +104 -0
  172. package/dist/core/secret-guarded-stores.d.ts +12 -0
  173. package/dist/core/secret-guarded-stores.js +41 -0
  174. package/dist/core/secret-spans.d.ts +50 -0
  175. package/dist/core/secret-spans.js +94 -0
  176. package/dist/core/stop-run.d.ts +46 -0
  177. package/dist/core/stop-run.js +61 -0
  178. package/dist/core/workflow-events.d.ts +14 -2
  179. package/dist/core/workflow-events.js +4 -0
  180. package/dist/core/workflow-run-events.d.ts +5 -3
  181. package/dist/core/workflow-run-events.js +43 -8
  182. package/dist/core/zod-json-schema.d.ts +4 -0
  183. package/dist/core/zod-json-schema.js +39 -0
  184. package/dist/core/zod3-window.d.ts +35 -0
  185. package/dist/core/zod3-window.js +124 -0
  186. package/dist/generated/prisma/browser.d.ts +34 -3
  187. package/dist/generated/prisma/client.d.ts +34 -3
  188. package/dist/generated/prisma/commonInputTypes.d.ts +220 -60
  189. package/dist/generated/prisma/enums.d.ts +10 -0
  190. package/dist/generated/prisma/enums.js +8 -0
  191. package/dist/generated/prisma/internal/class.d.ts +44 -0
  192. package/dist/generated/prisma/internal/class.js +4 -4
  193. package/dist/generated/prisma/internal/prismaNamespace.d.ts +428 -9
  194. package/dist/generated/prisma/internal/prismaNamespace.js +92 -1
  195. package/dist/generated/prisma/internal/prismaNamespaceBrowser.d.ts +95 -0
  196. package/dist/generated/prisma/internal/prismaNamespaceBrowser.js +92 -1
  197. package/dist/generated/prisma/models/Agent.d.ts +106 -8
  198. package/dist/generated/prisma/models/AgentRepository.d.ts +42 -1
  199. package/dist/generated/prisma/models/CodingAgentProfile.d.ts +229 -1
  200. package/dist/generated/prisma/models/CodingProxyRequest.d.ts +207 -3
  201. package/dist/generated/prisma/models/CodingProxySession.d.ts +114 -1
  202. package/dist/generated/prisma/models/CodingRun.d.ts +1637 -1
  203. package/dist/generated/prisma/models/DependencySnapshot.d.ts +1668 -0
  204. package/dist/generated/prisma/models/DependencySnapshot.js +1 -0
  205. package/dist/generated/prisma/models/IssuePullRequest.d.ts +6 -2
  206. package/dist/generated/prisma/models/LocalReview.d.ts +40 -1
  207. package/dist/generated/prisma/models/NativeGatewayCall.d.ts +39 -1
  208. package/dist/generated/prisma/models/NativeGatewaySession.d.ts +35 -1
  209. package/dist/generated/prisma/models/RegistryAllowance.d.ts +41 -1
  210. package/dist/generated/prisma/models/RegistryApprovedVersion.d.ts +75 -1
  211. package/dist/generated/prisma/models/RegistryAuditRecord.d.ts +1364 -0
  212. package/dist/generated/prisma/models/RegistryAuditRecord.js +1 -0
  213. package/dist/generated/prisma/models/Run.d.ts +738 -2
  214. package/dist/generated/prisma/models/RunBudgetEvent.d.ts +1211 -0
  215. package/dist/generated/prisma/models/RunBudgetEvent.js +1 -0
  216. package/dist/generated/prisma/models/RunTranscript.d.ts +1339 -0
  217. package/dist/generated/prisma/models/RunTranscript.js +1 -0
  218. package/dist/generated/prisma/models.d.ts +4 -0
  219. package/dist/help-index.json +1320 -69
  220. package/dist/import/create.js +32 -3
  221. package/dist/import/neutral-schema.d.ts +50 -281
  222. package/dist/import/neutral-schema.js +5 -4
  223. package/dist/knowledge/check.d.ts +1 -1
  224. package/dist/knowledge/cli.d.ts +11 -1
  225. package/dist/knowledge/cli.js +37 -4
  226. package/dist/knowledge/concept.d.ts +3 -19
  227. package/dist/knowledge/location.d.ts +31 -0
  228. package/dist/knowledge/location.js +78 -0
  229. package/dist/mcp/auth/access.d.ts +10 -2
  230. package/dist/mcp/auth/access.js +15 -1
  231. package/dist/mcp/auth/ownership.d.ts +7 -7
  232. package/dist/mcp/auth/self-hosted/credentials.d.ts +3 -3
  233. package/dist/mcp/auth/self-hosted/session.d.ts +2 -2
  234. package/dist/mcp/host-events/github-ingress.d.ts +2 -0
  235. package/dist/mcp/host-events/github-ingress.js +2 -1
  236. package/dist/mcp/index.d.ts +6 -0
  237. package/dist/mcp/index.js +58 -10
  238. package/dist/mcp/server.d.ts +13 -0
  239. package/dist/mcp/server.js +33 -14
  240. package/dist/mcp/tools/agents.d.ts +4 -0
  241. package/dist/mcp/tools/agents.js +198 -16
  242. package/dist/mcp/tools/datastore.js +17 -8
  243. package/dist/mcp/tools/grants.js +9 -3
  244. package/dist/mcp/tools/memory.js +11 -1
  245. package/dist/mcp/tools/notification-channels.js +2 -2
  246. package/dist/mcp/tools/packages.d.ts +2 -0
  247. package/dist/mcp/tools/packages.js +195 -0
  248. package/dist/mcp/tools/repositories.js +18 -5
  249. package/dist/mcp/tools/run-access.d.ts +16 -0
  250. package/dist/mcp/tools/run-access.js +25 -0
  251. package/dist/mcp/tools/run-transcript-download.d.ts +30 -0
  252. package/dist/mcp/tools/run-transcript-download.js +129 -0
  253. package/dist/mcp/tools/run-transcript.d.ts +19 -0
  254. package/dist/mcp/tools/run-transcript.js +70 -0
  255. package/dist/mcp/tools/runs.js +34 -12
  256. package/dist/mcp/tools/scheduling.js +3 -1
  257. package/dist/mcp/tools/secret-elicitation-form.js +5 -0
  258. package/dist/mcp/tools/secret-elicitation-server.d.ts +6 -0
  259. package/dist/mcp/tools/secret-elicitation-server.js +10 -61
  260. package/dist/mcp/tools/secrets.js +2 -2
  261. package/dist/mcp/tools/stop-run.d.ts +2 -0
  262. package/dist/mcp/tools/stop-run.js +82 -0
  263. package/dist/mcp/tools/subagents.js +2 -2
  264. package/dist/mcp/tools/tools.d.ts +1 -1
  265. package/dist/mcp/tools/tools.js +25 -9
  266. package/dist/mcp/transport/stdio-link-host.d.ts +20 -0
  267. package/dist/mcp/transport/stdio-link-host.js +67 -0
  268. package/dist/mcp/transport/streamable-http.d.ts +13 -0
  269. package/dist/mcp/transport/streamable-http.js +41 -1
  270. package/dist/native-worker/docker-launcher.d.ts +38 -2
  271. package/dist/native-worker/docker-launcher.js +65 -5
  272. package/dist/native-worker/gateway-server.d.ts +41 -3
  273. package/dist/native-worker/gateway-server.js +124 -27
  274. package/dist/native-worker/gateway.d.ts +27 -1
  275. package/dist/native-worker/gateway.js +70 -4
  276. package/dist/native-worker/http-server.d.ts +3 -1
  277. package/dist/native-worker/http-server.js +11 -7
  278. package/dist/native-worker/http-transport.js +1 -1
  279. package/dist/native-worker/kubernetes-launcher.d.ts +7 -3
  280. package/dist/native-worker/kubernetes-launcher.js +43 -3
  281. package/dist/native-worker/launch.d.ts +18 -1
  282. package/dist/native-worker/launch.js +6 -1
  283. package/dist/native-worker/ledger.d.ts +18 -2
  284. package/dist/native-worker/ledger.js +57 -4
  285. package/dist/native-worker/loopback.js +3 -8
  286. package/dist/native-worker/main.d.ts +2 -1
  287. package/dist/native-worker/main.js +14 -3
  288. package/dist/native-worker/protocol.d.ts +129 -577
  289. package/dist/native-worker/protocol.js +47 -1
  290. package/dist/native-worker/sandbox-executor.d.ts +17 -2
  291. package/dist/native-worker/sandbox-executor.js +78 -4
  292. package/dist/native-worker/stderr-redaction.d.ts +77 -0
  293. package/dist/native-worker/stderr-redaction.js +237 -0
  294. package/dist/native-worker/stdio.d.ts +1 -1
  295. package/dist/native-worker/stdio.js +6 -5
  296. package/dist/native-worker/warm-pool.d.ts +2 -2
  297. package/dist/native-worker/warm-pool.js +4 -4
  298. package/dist/native-worker/worker.js +28 -2
  299. package/dist/observability/metrics.js +6 -1
  300. package/dist/providers/auth/self-hosted.d.ts +1 -1
  301. package/dist/providers/coding-proxy/dependency-cache/eviction.d.ts +29 -0
  302. package/dist/providers/coding-proxy/dependency-cache/eviction.js +87 -0
  303. package/dist/providers/coding-proxy/dependency-cache/key.d.ts +33 -0
  304. package/dist/providers/coding-proxy/dependency-cache/key.js +86 -0
  305. package/dist/providers/coding-proxy/dependency-cache/model-request-seal.d.ts +18 -0
  306. package/dist/providers/coding-proxy/dependency-cache/model-request-seal.js +40 -0
  307. package/dist/providers/coding-proxy/dependency-cache/prisma-store.d.ts +25 -0
  308. package/dist/providers/coding-proxy/dependency-cache/prisma-store.js +131 -0
  309. package/dist/providers/coding-proxy/dependency-cache/service.d.ts +70 -0
  310. package/dist/providers/coding-proxy/dependency-cache/service.js +409 -0
  311. package/dist/providers/coding-proxy/dependency-cache/store.d.ts +85 -0
  312. package/dist/providers/coding-proxy/dependency-cache/store.js +95 -0
  313. package/dist/providers/coding-proxy/hold.d.ts +32 -0
  314. package/dist/providers/coding-proxy/hold.js +86 -0
  315. package/dist/providers/coding-proxy/interrupted.d.ts +70 -0
  316. package/dist/providers/coding-proxy/interrupted.js +104 -0
  317. package/dist/providers/coding-proxy/memory-ledger.d.ts +15 -2
  318. package/dist/providers/coding-proxy/memory-ledger.js +84 -8
  319. package/dist/providers/coding-proxy/metering.d.ts +16 -0
  320. package/dist/providers/coding-proxy/metering.js +27 -4
  321. package/dist/providers/coding-proxy/prisma-ledger.d.ts +15 -2
  322. package/dist/providers/coding-proxy/prisma-ledger.js +179 -67
  323. package/dist/providers/coding-proxy/proxy.d.ts +14 -1
  324. package/dist/providers/coding-proxy/proxy.js +166 -14
  325. package/dist/providers/coding-proxy/registry/audit.d.ts +53 -10
  326. package/dist/providers/coding-proxy/registry/audit.js +94 -17
  327. package/dist/providers/coding-proxy/registry/lockfile-parse-worker.d.ts +36 -0
  328. package/dist/providers/coding-proxy/registry/lockfile-parse-worker.js +67 -0
  329. package/dist/providers/coding-proxy/registry/lockfile-parse.d.ts +16 -0
  330. package/dist/providers/coding-proxy/registry/lockfile-parse.js +110 -0
  331. package/dist/providers/coding-proxy/registry/plan.d.ts +9 -0
  332. package/dist/providers/coding-proxy/registry/plan.js +201 -48
  333. package/dist/providers/coding-proxy/registry/prisma-store.d.ts +11 -5
  334. package/dist/providers/coding-proxy/registry/prisma-store.js +120 -17
  335. package/dist/providers/coding-proxy/registry/service.d.ts +97 -16
  336. package/dist/providers/coding-proxy/registry/service.js +579 -112
  337. package/dist/providers/coding-proxy/registry/store.d.ts +84 -9
  338. package/dist/providers/coding-proxy/registry/store.js +54 -1
  339. package/dist/providers/coding-proxy/runtime.d.ts +4 -0
  340. package/dist/providers/coding-proxy/runtime.js +78 -16
  341. package/dist/providers/coding-proxy/server.d.ts +6 -1
  342. package/dist/providers/coding-proxy/server.js +45 -1
  343. package/dist/providers/coding-proxy/types.d.ts +65 -1
  344. package/dist/providers/datastore/types.d.ts +5 -0
  345. package/dist/providers/engine/types.d.ts +17 -2
  346. package/dist/providers/engine/types.js +17 -0
  347. package/dist/providers/executor/composition.d.ts +10 -3
  348. package/dist/providers/executor/composition.js +47 -7
  349. package/dist/providers/executor/container.d.ts +361 -8
  350. package/dist/providers/executor/container.js +1136 -91
  351. package/dist/providers/executor/dbos-status.d.ts +31 -0
  352. package/dist/providers/executor/dbos-status.js +64 -0
  353. package/dist/providers/executor/dbos.d.ts +50 -1
  354. package/dist/providers/executor/dbos.js +207 -20
  355. package/dist/providers/executor/deferred.d.ts +12 -24
  356. package/dist/providers/executor/deferred.js +10 -29
  357. package/dist/providers/executor/in-process.d.ts +9 -1
  358. package/dist/providers/executor/in-process.js +14 -4
  359. package/dist/providers/executor/linked-issue-reader.d.ts +16 -0
  360. package/dist/providers/executor/linked-issue-reader.js +30 -0
  361. package/dist/providers/executor/routing.d.ts +2 -0
  362. package/dist/providers/executor/routing.js +4 -0
  363. package/dist/providers/executor/types.d.ts +13 -0
  364. package/dist/providers/index.d.ts +2 -1
  365. package/dist/providers/issue-tracker/types.d.ts +17 -0
  366. package/dist/providers/issue-tracker/types.js +39 -1
  367. package/dist/providers/jobs/claude-tool-setup.d.ts +4 -0
  368. package/dist/providers/jobs/claude-tool-setup.js +5 -2
  369. package/dist/providers/jobs/docker-isolation.d.ts +8 -0
  370. package/dist/providers/jobs/docker-isolation.js +17 -0
  371. package/dist/providers/jobs/docker.d.ts +1 -0
  372. package/dist/providers/jobs/docker.js +27 -2
  373. package/dist/providers/jobs/fake-kubernetes-api.d.ts +2 -0
  374. package/dist/providers/jobs/fake-kubernetes-api.js +3 -0
  375. package/dist/providers/jobs/kubernetes-api.d.ts +8 -0
  376. package/dist/providers/jobs/kubernetes-api.js +10 -0
  377. package/dist/providers/jobs/kubernetes-client.js +10 -1
  378. package/dist/providers/jobs/kubernetes-isolation.d.ts +2 -0
  379. package/dist/providers/jobs/kubernetes-isolation.js +17 -1
  380. package/dist/providers/jobs/kubernetes.d.ts +6 -1
  381. package/dist/providers/jobs/kubernetes.js +105 -16
  382. package/dist/providers/jobs/types.d.ts +33 -0
  383. package/dist/providers/jobs/types.js +13 -1
  384. package/dist/providers/llm/anthropic.js +5 -1
  385. package/dist/providers/llm/bedrock.d.ts +0 -12
  386. package/dist/providers/llm/bedrock.js +4 -0
  387. package/dist/providers/llm/claude-messages.d.ts +3 -1
  388. package/dist/providers/llm/claude-messages.js +4 -1
  389. package/dist/providers/llm/claude-provider.d.ts +2 -2
  390. package/dist/providers/llm/claude-provider.js +2 -2
  391. package/dist/providers/llm/openai.d.ts +2 -2
  392. package/dist/providers/llm/openai.js +6 -2
  393. package/dist/providers/llm/routing.d.ts +2 -2
  394. package/dist/providers/llm/routing.js +2 -2
  395. package/dist/providers/llm/types.d.ts +12 -2
  396. package/dist/providers/review-host/github-events.d.ts +9 -1
  397. package/dist/providers/review-host/github-events.js +34 -12
  398. package/dist/providers/review-host/github.d.ts +29 -2
  399. package/dist/providers/review-host/github.js +224 -29
  400. package/dist/providers/review-host/local.d.ts +8 -4
  401. package/dist/providers/review-host/local.js +49 -31
  402. package/dist/providers/review-host/patch-pages.d.ts +41 -0
  403. package/dist/providers/review-host/patch-pages.js +176 -0
  404. package/dist/providers/review-host/review-format.d.ts +19 -1
  405. package/dist/providers/review-host/review-format.js +38 -7
  406. package/dist/providers/review-host/types.d.ts +134 -2
  407. package/dist/providers/review-host/types.js +8 -0
  408. package/dist/providers/storage/gcs.d.ts +63 -0
  409. package/dist/providers/storage/gcs.js +69 -0
  410. package/dist/providers/storage/index.d.ts +15 -0
  411. package/dist/providers/storage/index.js +14 -0
  412. package/dist/providers/storage/local.d.ts +24 -0
  413. package/dist/providers/storage/local.js +92 -0
  414. package/dist/providers/storage/meter.d.ts +14 -0
  415. package/dist/providers/storage/meter.js +34 -0
  416. package/dist/providers/storage/s3.d.ts +41 -0
  417. package/dist/providers/storage/s3.js +107 -0
  418. package/dist/providers/storage/types.d.ts +37 -3
  419. package/dist/providers/storage/types.js +21 -4
  420. package/dist/providers/vcs/git.d.ts +33 -2
  421. package/dist/providers/vcs/git.js +324 -22
  422. package/dist/providers/vcs/github-remote.d.ts +26 -3
  423. package/dist/providers/vcs/github-remote.js +124 -14
  424. package/dist/providers/vcs/github.d.ts +214 -6
  425. package/dist/providers/vcs/github.js +447 -45
  426. package/dist/providers/vcs/index.js +8 -1
  427. package/dist/providers/vcs/pr-description.d.ts +127 -0
  428. package/dist/providers/vcs/pr-description.js +270 -0
  429. package/dist/providers/vcs/remote.d.ts +33 -1
  430. package/dist/providers/vcs/routing.d.ts +4 -2
  431. package/dist/providers/vcs/routing.js +6 -0
  432. package/dist/providers/vcs/types.d.ts +80 -3
  433. package/dist/quickstart/coding-seed.d.ts +14 -3
  434. package/dist/quickstart/coding-seed.js +213 -4
  435. package/dist/quickstart/reviewer-prompt.d.ts +1 -1
  436. package/dist/quickstart/reviewer-prompt.js +6 -6
  437. package/dist/quickstart-images.json +1 -1
  438. package/dist/sandbox/generated/zod.bundle.js +62 -1
  439. package/dist/sandbox/generated/zod3.bundle.js +5 -0
  440. package/dist/sandbox/host-functions.d.ts +1 -0
  441. package/dist/sandbox/host-functions.js +6 -11
  442. package/dist/sandbox/secret-broker.js +3 -4
  443. package/dist/sandbox/tool-capabilities.d.ts +10 -30
  444. package/dist/sandbox/zod-params.d.ts +37 -5
  445. package/dist/sandbox/zod-params.js +181 -26
  446. package/dist/serve.js +8 -1
  447. package/dist/viewer/api-schema.d.ts +349 -2627
  448. package/dist/viewer/api-schema.js +7 -0
  449. package/dist/viewer/build-schemas.js +6 -2
  450. package/dist/viewer/openapi.js +2 -4
  451. package/dist/wardby-bin.js +10 -0
  452. package/docs/agent-recipes.md +58 -20
  453. package/docs/code-review-agents.md +551 -41
  454. package/docs/coding-agent-setup.md +489 -7
  455. package/docs/coding-packages.md +1033 -90
  456. package/docs/coding-services.md +27 -12
  457. package/docs/coding-worker-byo-images.md +20 -0
  458. package/docs/coding-worker-isolation.md +105 -30
  459. package/docs/getting-started-gke.md +124 -137
  460. package/docs/getting-started-identity-provider.md +10 -1
  461. package/docs/getting-started.md +9 -0
  462. package/docs/jira-agents.md +17 -8
  463. package/docs/knowledge/blob-not-found-semantics.md +41 -0
  464. package/docs/knowledge/bounded-file-descriptor.md +52 -0
  465. package/docs/knowledge/delegation-admission-slots.md +52 -0
  466. package/docs/knowledge/http-dispatcher-bootstrap.md +61 -0
  467. package/docs/knowledge/import-rename-target.md +52 -0
  468. package/docs/knowledge/index.md +57 -0
  469. package/docs/knowledge/log.md +9 -0
  470. package/docs/knowledge/mcp-cancelled-task-precedence.md +41 -0
  471. package/docs/knowledge/mcp-http-prompt-discovery.md +41 -0
  472. package/docs/knowledge/mcp-schema-module-boundary.md +41 -0
  473. package/docs/knowledge/nullable-tool-uniqueness.md +52 -0
  474. package/docs/knowledge/overview-cli.md +74 -0
  475. package/docs/knowledge/overview-coding-proxy.md +72 -0
  476. package/docs/knowledge/overview-coding.md +72 -0
  477. package/docs/knowledge/overview-config.md +60 -0
  478. package/docs/knowledge/overview-core.md +72 -0
  479. package/docs/knowledge/overview-deploy.md +83 -0
  480. package/docs/knowledge/overview-mcp.md +61 -0
  481. package/docs/knowledge/overview-prisma.md +72 -0
  482. package/docs/knowledge/overview-providers.md +89 -0
  483. package/docs/knowledge/overview-sandbox.md +83 -0
  484. package/docs/knowledge/overview-surfaces.md +83 -0
  485. package/docs/knowledge/private-datastore-shared-boundary.md +54 -0
  486. package/docs/knowledge/proxy-settlement-projections.md +41 -0
  487. package/docs/knowledge/run-pricing-first-writer.md +43 -0
  488. package/docs/knowledge/schedule-claim-transaction.md +41 -0
  489. package/docs/knowledge/secret-entry-replica-contract.md +52 -0
  490. package/docs/knowledge/terminal-run-first-writer.md +41 -0
  491. package/docs/knowledge/transactional-agent-authorization.md +52 -0
  492. package/docs/knowledge/viewer-notify-write-isolation.md +30 -0
  493. package/docs/knowledge/warm-input-publication.md +30 -0
  494. package/docs/knowledge/workspace-staging-failure.md +54 -0
  495. package/docs/knowledge.md +54 -14
  496. package/docs/native-sandbox.md +119 -23
  497. package/docs/observability.md +4 -0
  498. package/docs/security-deployment.md +33 -2
  499. package/docs/slack-notifications.md +35 -19
  500. package/docs/tool-secrets.md +42 -10
  501. package/docs/viewer-api.openapi.yml +1 -1
  502. package/help/admin-viewer.md +24 -2
  503. package/help/architecture-agent.md +11 -6
  504. package/help/build-worker-image.md +3 -1
  505. package/help/builder-agent.md +19 -6
  506. package/help/code-review-agents.md +177 -6
  507. package/help/coding-packages.md +156 -6
  508. package/help/cost-attribution.md +9 -5
  509. package/help/creating-agents.md +14 -1
  510. package/help/dependency-cache.md +143 -0
  511. package/help/deploy-gke.md +15 -8
  512. package/help/errors/budget-exhausted.md +187 -0
  513. package/help/errors/budget-group-exhausted.md +5 -1
  514. package/help/errors/coding-stream-quota-exceeded.md +33 -0
  515. package/help/errors/coding-workspace-disk-exceeds-limit.md +39 -0
  516. package/help/errors/continuation-closed.md +9 -12
  517. package/help/errors/delegation-conflict.md +24 -0
  518. package/help/errors/file-list-truncated.md +45 -0
  519. package/help/errors/full-read-required.md +49 -0
  520. package/help/errors/github-account-not-allowed.md +4 -0
  521. package/help/errors/graph-incomplete.md +43 -0
  522. package/help/errors/graph-limit.md +43 -0
  523. package/help/errors/merge-conflict.md +49 -0
  524. package/help/errors/native-sandbox-gateway-error.md +20 -0
  525. package/help/errors/native-sandbox-launch-abandoned.md +21 -0
  526. package/help/errors/native-sandbox-worker-exited.md +3 -2
  527. package/help/errors/package-not-allowed-pending.md +52 -0
  528. package/help/errors/package-not-allowed.md +51 -0
  529. package/help/errors/package-refusals.md +43 -0
  530. package/help/errors/patch-cursor-malformed.md +31 -0
  531. package/help/errors/run-lost.md +39 -0
  532. package/help/errors/run-transcript-download.md +44 -0
  533. package/help/errors/secret-in-value.md +69 -0
  534. package/help/errors/target-merge.md +40 -0
  535. package/help/fix-merge-conflicts.md +73 -0
  536. package/help/github-app-setup.md +132 -0
  537. package/help/github.md +22 -0
  538. package/help/identity-and-access.md +2 -0
  539. package/help/jira.md +23 -6
  540. package/help/knowledge.md +16 -7
  541. package/help/local-repositories.md +1 -1
  542. package/help/low-power-mode.md +7 -1
  543. package/help/merge-order-check.md +5 -0
  544. package/help/native-capabilities.md +29 -1
  545. package/help/native-sandbox.md +15 -1
  546. package/help/operating-agents.md +5 -0
  547. package/help/package-audit-mode.md +91 -0
  548. package/help/related-pull-requests.md +10 -7
  549. package/help/resume-runs-locally.md +131 -0
  550. package/help/review-fix-rounds.md +33 -1
  551. package/help/sharing-agents.md +59 -0
  552. package/help/slack-notifications.md +19 -5
  553. package/help/status-comments.md +31 -0
  554. package/help/stop-run.md +58 -0
  555. package/help/troubleshooting/budgets.md +28 -0
  556. package/help/troubleshooting/coding-workers.md +78 -7
  557. package/package.json +17 -10
  558. package/prisma/migrations/20261016000000_registry_approved_accepted_advisories/migration.sql +3 -0
  559. package/prisma/migrations/20261016000001_mark_ready_when_passing/migration.sql +3 -0
  560. package/prisma/migrations/20261017000000_debug_trace_max_mb/migration.sql +4 -0
  561. package/prisma/migrations/20261018000000_dependency_cache/migration.sql +53 -0
  562. package/prisma/migrations/20261019000000_claude_max_output_tokens/migration.sql +5 -0
  563. package/prisma/migrations/20261021000000_pr_description_hash/migration.sql +4 -0
  564. package/prisma/migrations/20261022000000_dependency_snapshot_validation_metadata/migration.sql +4 -0
  565. package/prisma/migrations/20261023000000_package_audit_mode/migration.sql +27 -0
  566. package/prisma/migrations/20261024000000_budget_events/migration.sql +26 -0
  567. package/prisma/migrations/20261025000000_cache_aware_holds/migration.sql +9 -0
  568. package/prisma/migrations/20261026000000_budget_overrun/migration.sql +4 -0
  569. package/prisma/migrations/20261027000000_coding_run_request/migration.sql +1 -0
  570. package/prisma/migrations/20261028000000_native_gateway_review_reads/migration.sql +6 -0
  571. package/prisma/migrations/20261029000000_lead_survives_deploy/migration.sql +6 -0
  572. package/prisma/migrations/20261030000000_local_review_covered_sha/migration.sql +2 -0
  573. package/prisma/migrations/20261031000000_coding_run_budget_limit/migration.sql +9 -0
  574. package/prisma/migrations/20261101000000_run_transcripts/migration.sql +22 -0
  575. package/prisma/schema.prisma +311 -97
  576. package/dist/sandbox/generated/zod-to-json-schema.bundle.js +0 -5
@@ -11,11 +11,15 @@
11
11
  import { createHash } from "node:crypto";
12
12
  import { logger } from "../../../core/logger.js";
13
13
  import { matchRoot, parseAllowlist, resolvePolicy } from "../../../coding/registry/allowlist.js";
14
- import { PLAN_REFUSAL_FILENAME } from "../../../coding/registry/report.js";
14
+ import { acceptedAdvisoriesReason, LISTING_REFUSAL_FILENAME, PLAN_REFUSAL_FILENAME, } from "../../../coding/registry/report.js";
15
+ import { GRAPH_INCOMPLETE, GRAPH_LIMIT, graphIncompleteMessage, graphLimitMessage, notAllowedMessage, notAllowedPendingMessage, PACKAGE_NOT_ALLOWED, PACKAGE_NOT_ALLOWED_PENDING, refusalHint, } from "../../../coding/registry/refusal-hints.js";
15
16
  import { RegistryError, } from "../../../coding/registry/types.js";
16
17
  import { capabilityHash } from "../proxy.js";
18
+ import { DEPENDENCY_CACHE_ECOSYSTEM, MAX_DEPENDENCY_CACHE_LOCK_HASHES, sha256Hex } from "../dependency-cache/key.js";
17
19
  import { boundedMetadataFetch, DEFAULT_MAX_METADATA_BYTES, DEFAULT_METADATA_TIMEOUT_MS } from "./bounded-fetch.js";
20
+ import { parseLockfileOffThread } from "./lockfile-parse.js";
18
21
  import { DEFAULT_PLAN_MAX_ENTRIES, DEFAULT_PLAN_TIMEOUT_MS, verifyLockfilePlan } from "./plan.js";
22
+ import { AUDITED_VIA_UNKNOWN } from "./store.js";
19
23
  const registryLog = logger.child({ module: "coding-registry" });
20
24
  /** Server-side record of a registry failure. The worker only ever sees the
21
25
  * RegistryError's code and message; the operator needs the cause. Without
@@ -57,9 +61,26 @@ const GRAPH_WALK_CONCURRENCY = 2;
57
61
  * walk cannot starve the others' upstream fetches -- or the proxy's event
58
62
  * loop: parsing packuments is CPU work on the same loop that carries model
59
63
  * traffic (per-walk 2 and global 3 since a live install at 4/6 on 500m
60
- * expired a ledger transaction). The walk is only the fallback now; lockfile
61
- * verification answers most installs without it. */
64
+ * expired a ledger transaction). The walk is only the fallback now: a
65
+ * download of a version the run has not resolved starts one (and a
66
+ * metadata miss, only after a failed or partial lockfile plan), and
67
+ * lockfile verification answers most of those without it. */
62
68
  export const DEFAULT_MAX_GRAPH_CONCURRENCY = 3;
69
+ /** Byte budget of the graph walk's memo of expanded packages (kept
70
+ * versions and their declared dependencies), across every run. */
71
+ export const DEFAULT_GRAPH_MEMO_BYTES = 32 * 1024 * 1024;
72
+ /** How long a memoized expansion is reused. Safe for that long: a kept
73
+ * version depends on the release-age cutoff, which only moves forward (a
74
+ * stale entry can only omit a version that has since become old enough,
75
+ * which fails closed), and on the OSV audit, which is itself cached for an
76
+ * hour (audit.ts). The walk's view of advisories can therefore lag by the
77
+ * audit cache TTL plus 15 minutes; that only affects which names the walk
78
+ * allows, because every served version is re-audited through keptVersions
79
+ * and malware is never allowed. A version published upstream within the TTL is
80
+ * too new to be kept under any non-zero minReleaseAgeDays; with
81
+ * minReleaseAgeDays 0 it is omitted until the entry expires (fails
82
+ * closed). */
83
+ export const GRAPH_MEMO_TTL_MS = 15 * 60_000;
63
84
  /** A node whose metadata or audit fails transiently is retried once more
64
85
  * in the same walk, then parked; a later miss retries it again, up to
65
86
  * this many attempts per node per run. */
@@ -82,6 +103,17 @@ export const DEFAULT_PLAN_MAX_PER_RUN = 20;
82
103
  export const PLAN_FETCH_FILENAME = PLAN_REFUSAL_FILENAME;
83
104
  /** Largest lockfile a plan reads (the HTTP layer enforces it: 413). */
84
105
  export const MAX_PLAN_BODY_BYTES = 20 * 1024 * 1024;
106
+ /** Approximate retained size of a memoized expansion, for the memo's byte
107
+ * budget. Deliberately generous. */
108
+ function keptBytes(key, kept) {
109
+ let bytes = 128 + 2 * key.length;
110
+ for (const [version, dependencies] of kept) {
111
+ bytes += 64 + 2 * version.length;
112
+ for (const spec of dependencies)
113
+ bytes += 48 + 2 * (spec.name.length + spec.range.length);
114
+ }
115
+ return bytes;
116
+ }
85
117
  /** A small counting semaphore. */
86
118
  class Slots {
87
119
  size;
@@ -108,10 +140,92 @@ class Slots {
108
140
  }
109
141
  }
110
142
  const edgeKey = (edge) => `${edge.name}\0${edge.range}`;
143
+ /** Whether a parked failure may be retried. */
144
+ function retriable(walk, name) {
145
+ return (walk.attempts.get(name) ?? 0) < MAX_GRAPH_NODE_ATTEMPTS;
146
+ }
147
+ /** Names with an edge the walk has not expanded yet: pending, or parked in
148
+ * `failed` with attempts left. Read only between batches (a running
149
+ * batch is off the queue). */
150
+ function unexpandedNames(walk) {
151
+ const names = new Set(walk.pending.keys());
152
+ for (const name of walk.failed.keys())
153
+ if (retriable(walk, name))
154
+ names.add(name);
155
+ return names;
156
+ }
157
+ /** Packages walked so far, and the distinct names with an unexpanded edge
158
+ * that are not yet expanded themselves (a further edge to an expanded
159
+ * package adds no new package). */
160
+ function walkProgress(walk) {
161
+ const queued = unexpandedNames(walk);
162
+ for (const name of walk.expandedVersions.keys())
163
+ queued.delete(name);
164
+ return { walked: walk.expandedVersions.size, queued: queued.size };
165
+ }
166
+ /** Unexpanded edges, for the progress log. */
167
+ function unexpandedEdgeCount(walk) {
168
+ let count = 0;
169
+ for (const edges of walk.pending.values())
170
+ count += edges.length;
171
+ for (const [name, entry] of walk.failed)
172
+ if (retriable(walk, name))
173
+ count += entry.edges.length;
174
+ return count;
175
+ }
176
+ /** Adds edges to `name`'s pending group (at the end of the order, or at
177
+ * the front with `first`). */
178
+ function addPending(walk, name, edges, first = false) {
179
+ const group = walk.pending.get(name);
180
+ if (group) {
181
+ group.push(...edges);
182
+ if (first)
183
+ moveToFront(walk, name);
184
+ return;
185
+ }
186
+ walk.pending.set(name, [...edges]);
187
+ if (first)
188
+ walk.order.unshift(name);
189
+ else
190
+ walk.order.push(name);
191
+ }
192
+ function moveToFront(walk, name) {
193
+ const index = walk.order.indexOf(name);
194
+ if (index <= 0)
195
+ return;
196
+ walk.order.splice(index, 1);
197
+ walk.order.unshift(name);
198
+ }
111
199
  export const DEFAULT_MAX_REFUSAL_RECORDS = 500;
200
+ /** The most refusals `GET /registry/-/refusals` returns (distinct, newest first). */
201
+ export const MAX_LISTED_REFUSALS = 20;
202
+ const DEFAULT_REFUSALS_WINDOW_MS = 15 * 60_000;
112
203
  function contentTypeOf(route) {
113
204
  return route.kind === "file-metadata" ? "text/plain" : "application/octet-stream";
114
205
  }
206
+ /**
207
+ * Why kept versions leave nothing to download, or null when some file is
208
+ * servable: every allowed file newer than the release-age limit
209
+ * (`wardby_version_filtered`), or no allowed file at all, such as a Python
210
+ * package with only source distributions (`wardby_file_not_allowed`).
211
+ */
212
+ export function unservableReason(meta, keep, keptFiles) {
213
+ let anyFile = false;
214
+ let anyAllowed = false;
215
+ for (const version of keep) {
216
+ for (const file of meta.versions.get(version)?.files ?? []) {
217
+ anyFile = true;
218
+ if (!file.allowed)
219
+ continue;
220
+ if (keptFiles.has(file.filename))
221
+ return null;
222
+ anyAllowed = true;
223
+ }
224
+ }
225
+ if (anyAllowed)
226
+ return "wardby_version_filtered";
227
+ return anyFile ? "wardby_file_not_allowed" : null;
228
+ }
115
229
  export function errorResponse(error) {
116
230
  return {
117
231
  status: error.status,
@@ -217,6 +331,17 @@ export class RegistryService {
217
331
  * across replicas. */
218
332
  inFlight = new Map();
219
333
  tallies = new Map();
334
+ /** Expanded packages of the graph walks (`runId\0ecosystem\0name`),
335
+ * least recently used first, so a package reached again under a new
336
+ * range, in a later request of the same run, is not read again.
337
+ * Run-scoped by key: a package's kept versions depend on the run's
338
+ * policy and allowlist range. Bounded to `graphMemoBytes`; entries
339
+ * expire GRAPH_MEMO_TTL_MS after they are computed, and a run's entries
340
+ * are dropped when the run is released. */
341
+ graphMemo = new Map();
342
+ /** Sum of the memo entries' `bytes`. */
343
+ graphMemoTotal = 0;
344
+ graphMemoBytes = DEFAULT_GRAPH_MEMO_BYTES;
220
345
  walkSlots;
221
346
  planSlots;
222
347
  versionUpstream;
@@ -237,6 +362,49 @@ export class RegistryService {
237
362
  maxBytes: MAX_VERSION_DOCUMENT_BYTES,
238
363
  });
239
364
  }
365
+ /**
366
+ * `GET /registry/-/refusals`: the run's recent refusals (package, code and the
367
+ * shared hint), for a client that does not print the registry's 403 body
368
+ * (pip, uv). Authenticated with the run's registry token. Names come from the
369
+ * run's own requests; nothing else (no URL, no token) is returned.
370
+ */
371
+ async refusals(request) {
372
+ try {
373
+ const context = await this.options.store.findRunByRegistryTokenHash(capabilityHash(request.token), this.now());
374
+ if (!context)
375
+ throw new RegistryError(401, "invalid_capability", "the registry token is not valid for a live run");
376
+ const since = request.since && !Number.isNaN(request.since.getTime())
377
+ ? request.since
378
+ : new Date(this.now().getTime() - DEFAULT_REFUSALS_WINDOW_MS);
379
+ const rows = await this.options.store.listRecentRefusals(context.runId, since, MAX_LISTED_REFUSALS * 5);
380
+ const seen = new Set();
381
+ const refusals = [];
382
+ for (const row of rows) {
383
+ const code = row.reason ?? "wardby_refused";
384
+ const key = `${row.ecosystem}\0${row.name}\0${row.version ?? ""}\0${code}`;
385
+ if (seen.has(key))
386
+ continue;
387
+ seen.add(key);
388
+ const hint = refusalHint(code);
389
+ refusals.push({
390
+ ecosystem: row.ecosystem,
391
+ name: row.name,
392
+ ...(row.version ? { version: row.version } : {}),
393
+ code,
394
+ ...(hint ? { hint } : {}),
395
+ });
396
+ if (refusals.length >= MAX_LISTED_REFUSALS)
397
+ break;
398
+ }
399
+ return { status: 200, contentType: "application/json", body: JSON.stringify({ refusals }) };
400
+ }
401
+ catch (error) {
402
+ logRegistryFailure("refusals", "-", error);
403
+ if (error instanceof RegistryError)
404
+ return errorResponse(error);
405
+ return errorResponse(new RegistryError(502, "wardby_upstream_error", "the package registry request failed"));
406
+ }
407
+ }
240
408
  /** `POST /registry/<ecosystem>/-/plan`: verifies a lockfile against the
241
409
  * registry and approves exactly its verified, reachable name@version
242
410
  * pairs for the run (plan.ts). Answers `{ approved, refused }`. */
@@ -263,6 +431,15 @@ export class RegistryService {
263
431
  if (!support || !auditVersions)
264
432
  throw new RegistryError(404, "wardby_plan_unsupported", `the ${adapter.id} registry does not verify lockfiles`);
265
433
  const tally = this.tally(context);
434
+ try {
435
+ return await this.runPlan(request, adapter, support, auditVersions, context, tally);
436
+ }
437
+ catch (error) {
438
+ (tally.planFallback ??= new Set()).add(adapter.id);
439
+ throw error;
440
+ }
441
+ }
442
+ async runPlan(request, adapter, support, auditVersions, context, tally) {
266
443
  if (tally.planning)
267
444
  throw new RegistryError(429, "wardby_plan_in_progress", "a lockfile plan is already running for this run; wait for it to finish");
268
445
  const maxPlans = this.options.planMaxPerRun ?? DEFAULT_PLAN_MAX_PER_RUN;
@@ -274,20 +451,32 @@ export class RegistryService {
274
451
  // The body is read only while a slot is held: a queued plan holds no
275
452
  // lockfile text, and at most `planConcurrency` bodies exist at once.
276
453
  return await this.planSlots.run(async () => {
277
- const lockfile = support.parse(await request.readBody(), {
454
+ const body = await request.readBody();
455
+ // The dependency cache's lockHash: computed here, from the bytes the
456
+ // proxy itself verified, never from anything a client claims.
457
+ const lockHash = context.dependencyCache && adapter.id === DEPENDENCY_CACHE_ECOSYSTEM ? sha256Hex(body) : null;
458
+ const parseOptions = {
278
459
  maxEntries: this.options.planMaxEntries ?? DEFAULT_PLAN_MAX_ENTRIES,
279
460
  proxyRegistryUrl: `${this.options.proxyBase}${adapter.id}/`,
280
- });
281
- return this.verifyPlan(request, adapter, support, lockfile, auditVersions, context);
461
+ };
462
+ // A YAML or yarn lockfile is read in a worker thread with a time
463
+ // bound (lockfile-parse.ts); npm's JSON stays on the event loop.
464
+ const lockfile = support.parsesOffThread?.(body)
465
+ ? await parseLockfileOffThread(adapter.id, body, parseOptions, {
466
+ timeoutMs: this.options.planParseTimeoutMs,
467
+ signal: request.signal,
468
+ })
469
+ : support.parse(body, parseOptions);
470
+ return this.verifyPlan(request, adapter, support, lockfile, auditVersions, context, lockHash);
282
471
  });
283
472
  }
284
473
  finally {
285
474
  tally.planning = false;
286
475
  }
287
476
  }
288
- async verifyPlan(request, adapter, support, lockfile, auditVersions, context) {
477
+ async verifyPlan(request, adapter, support, lockfile, auditVersions, context, lockHash) {
289
478
  const entries = parseAllowlist(context.allowlist, this.options.adapters).get(adapter.id) ?? [];
290
- const { minReleaseAgeDays } = resolvePolicy(context.policy);
479
+ const { minReleaseAgeDays, acceptedAdvisories } = resolvePolicy(context.policy);
291
480
  const timeoutMs = this.options.planTimeoutMs ?? DEFAULT_PLAN_TIMEOUT_MS;
292
481
  const signal = AbortSignal.any([AbortSignal.timeout(timeoutMs), request.signal]);
293
482
  const store = this.options.store;
@@ -306,6 +495,8 @@ export class RegistryService {
306
495
  upstream: this.versionUpstream,
307
496
  streamUpstream: this.options.upstream,
308
497
  audit: (versions, auditSignal) => auditVersions(adapter, versions, auditSignal),
498
+ accepted: new Set(acceptedAdvisories),
499
+ auditMode: context.packageAudit,
309
500
  hostAllowed: (url) => this.hostAllowed(adapter, url),
310
501
  signal,
311
502
  });
@@ -318,7 +509,33 @@ export class RegistryService {
318
509
  ? "the client went away before the lockfile was verified"
319
510
  : `the lockfile was not verified within ${Math.round(timeoutMs / 1000)} s (REGISTRY_PLAN_TIMEOUT_MS); what was verified is kept, so try again`);
320
511
  }
321
- await store.approveVersions(context.runId, adapter.id, result.approved);
512
+ // An approved name is proven part of the approved graph, so it is an
513
+ // allowance too: its metadata (which npm re-reads for packages already
514
+ // in the lockfile) is served without a graph walk. Written before the
515
+ // approvals: a name allowance left behind by a failed approval write is
516
+ // only what a walk would conclude.
517
+ // In package audit mode an approval outside the allowlist is an audited
518
+ // allowance, tagged with the top-level audited package that brought it
519
+ // (one of unknown relation with AUDITED_VIA_UNKNOWN, never itself).
520
+ const plain = result.approved.filter((v) => !v.audit || v.audit.relation === "allowed");
521
+ await store.addAllowances(context.runId, adapter.id, [...new Set(plain.map((v) => v.name))]);
522
+ const byVia = new Map();
523
+ for (const v of result.approved) {
524
+ if (!v.audit || v.audit.relation === "allowed")
525
+ continue;
526
+ const via = v.audit.relation === "unknown" ? AUDITED_VIA_UNKNOWN : (v.audit.via ?? v.name);
527
+ byVia.set(via, (byVia.get(via) ?? new Set()).add(v.name));
528
+ }
529
+ for (const [via, names] of byVia)
530
+ await store.addAuditedAllowances(context.runId, adapter.id, [...names], via);
531
+ // A registry whose approvals are names (PyPI) keeps every download on the
532
+ // usual per-file path: no exact version is served from an approval.
533
+ if (support.approves !== "names")
534
+ await store.approveVersions(context.runId, adapter.id, result.approved);
535
+ // A plan that refused anything leaves some lockfile packages without an
536
+ // allowance: metadata misses walk for the rest of the run.
537
+ if (result.refused.length > 0)
538
+ (this.tally(context).planFallback ??= new Set()).add(adapter.id);
322
539
  // Only verdicts about the registry's name@version itself answer later
323
540
  // downloads; a lockfile-dependent one (unreachable, an integrity or
324
541
  // source the lockfile claims) must not block the same version reached
@@ -327,12 +544,19 @@ export class RegistryService {
327
544
  .filter((refusal) => refusal.intrinsic)
328
545
  .map(({ name, version, code, reason, publishedAt }) => ({ name, version, code, reason, publishedAt })));
329
546
  await this.recordPlanRefusals(context, adapter, result.refused);
547
+ // Only a lockfile approved whole may be restored from the dependency
548
+ // cache; the store ignores it once the session is sealed or full. A plan
549
+ // that audited anything is never cached: a restore downloads nothing,
550
+ // so nothing it installs would be recorded.
551
+ if (lockHash && result.refused.length === 0 && result.audited === 0)
552
+ await store.recordDependencyCacheLockHash(context.runId, lockHash, MAX_DEPENDENCY_CACHE_LOCK_HASHES);
330
553
  return {
331
554
  status: 200,
332
555
  contentType: "application/json",
333
556
  body: JSON.stringify({
334
557
  approved: result.approved.length,
335
558
  refused: result.refused.map(({ name, version, code, reason }) => ({ name, version, code, reason })),
559
+ ...(context.packageAudit ? { audited: result.audited } : {}),
336
560
  }),
337
561
  };
338
562
  }
@@ -407,7 +631,9 @@ export class RegistryService {
407
631
  }
408
632
  if (request.method === "HEAD")
409
633
  return { status: 200, contentType: contentTypeOf(route), body: "" };
410
- return this.download(adapter, context, name, route, file, request.signal);
634
+ // The plan's waiver (if any) is recorded on the served row, and its
635
+ // audit verdict (audit-mode plans only) as the run's audit record.
636
+ return this.download(adapter, context, name, route, file, request.signal, acceptedAdvisoriesReason(approved.acceptedAdvisories), approved.audit);
411
637
  }
412
638
  // A version the plan definitively refused is answered from the plan,
413
639
  // with its code and reason: no metadata, no graph walk.
@@ -428,7 +654,7 @@ export class RegistryService {
428
654
  throw new RegistryError(403, refused.code, `"${name}" ${route.version} was refused by this run's lockfile verification: ${refused.reason}`);
429
655
  }
430
656
  }
431
- const root = await this.authorize(adapter, context, name);
657
+ const { root, audit } = await this.authorize(adapter, context, name, route.kind);
432
658
  let meta;
433
659
  try {
434
660
  // Only a metadata request renders a document; a download, the version
@@ -445,8 +671,10 @@ export class RegistryService {
445
671
  }
446
672
  let keep;
447
673
  let keptFiles;
674
+ let waivers;
675
+ let auditAdvisories;
448
676
  try {
449
- ({ keep, keptFiles } = await this.keptVersions(adapter, context, meta, root));
677
+ ({ keep, keptFiles, waivers, auditAdvisories } = await this.keptVersions(adapter, context, meta, root));
450
678
  }
451
679
  catch (error) {
452
680
  // Every refusal gets a row, including one the run never chose: the
@@ -456,12 +684,37 @@ export class RegistryService {
456
684
  }
457
685
  throw error;
458
686
  }
687
+ if (route.kind === "metadata" && route.version !== undefined) {
688
+ // One version's document: the same checks as the whole package's,
689
+ // narrowed to that version.
690
+ const document = keep.has(route.version)
691
+ ? (adapter.renderVersion?.(meta, route.version, keptFiles, `${this.options.proxyBase}${adapter.id}/`) ?? null)
692
+ : null;
693
+ if (!document) {
694
+ await this.refuse(context, adapter, name, "wardby_version_filtered", {
695
+ version: route.version,
696
+ filename: `${route.version}.tgz`,
697
+ });
698
+ throw new RegistryError(404, "wardby_version_filtered", `"${name}" ${route.version} is outside the allowlisted range, newer than the release-age limit, has a high-severity advisory, or is not published`);
699
+ }
700
+ await this.allowServed(context, adapter, name, keptDependencies(adapter, meta, new Set([route.version])), audit);
701
+ return { status: 200, contentType: document.contentType, body: document.body };
702
+ }
459
703
  if (route.kind === "metadata") {
460
704
  if (keep.size === 0) {
461
705
  await this.refuse(context, adapter, name, "wardby_version_filtered");
462
706
  throw new RegistryError(404, "wardby_version_filtered", `every matching version of "${name}" is outside the allowlisted range, newer than the release-age limit, or has a high-severity advisory`);
463
707
  }
464
- await this.options.store.addAllowances(context.runId, adapter.id, keptDependencies(adapter, meta, keep));
708
+ await this.allowServed(context, adapter, name, keptDependencies(adapter, meta, keep), audit);
709
+ // Versions kept but no file the client may download: a Python package
710
+ // published only as an sdist, or whose every wheel is newer than the
711
+ // release-age limit. The (empty) index is still served, and pip and uv
712
+ // only say "no versions", so the refusal is recorded to say why, marked
713
+ // as a listing's: the installer may not have needed it (see
714
+ // LISTING_REFUSAL_FILENAME for how it counts).
715
+ const unservable = unservableReason(meta, keep, keptFiles);
716
+ if (unservable)
717
+ await this.refuse(context, adapter, name, unservable, { filename: LISTING_REFUSAL_FILENAME });
465
718
  const document = adapter.renderMetadata(meta, keep, keptFiles, `${this.options.proxyBase}${adapter.id}/`);
466
719
  return { status: 200, contentType: document.contentType, body: document.body };
467
720
  }
@@ -474,7 +727,9 @@ export class RegistryService {
474
727
  // (route.filename), since keptFiles holds wheel filenames.
475
728
  const agedFile = route.kind === "file-metadata" ? route.filename : file?.filename;
476
729
  if (!file || !keep.has(file.version) || !agedFile || !keptFiles.has(agedFile)) {
477
- await this.refuse(context, adapter, name, "wardby_version_filtered", file ?? undefined);
730
+ // A download that does not resolve is still a refusal of the version asked for, not of the whole
731
+ // name (which any version served later would clear; see summarizeRegistryFetches).
732
+ await this.refuse(context, adapter, name, "wardby_version_filtered", file ?? (route.kind === "download" ? { version: route.version, filename: route.filename } : undefined));
478
733
  const versionSuffix = route.kind === "download" ? ` ${route.version}` : "";
479
734
  throw new RegistryError(404, "wardby_version_filtered", `"${name}"${versionSuffix} is not available to this run`);
480
735
  }
@@ -492,7 +747,30 @@ export class RegistryService {
492
747
  // reservation, nothing recorded as served.
493
748
  if (request.method === "HEAD")
494
749
  return { status: 200, contentType: contentTypeOf(route), body: "" };
495
- return this.download(adapter, context, name, route, file, request.signal);
750
+ // A version kept only by waiving accepted advisories records them on
751
+ // its served row. In package audit mode, a package outside the
752
+ // allowlist, or a version kept only past advisories (full), is recorded
753
+ // as the run's audit record.
754
+ const advisories = auditAdvisories.get(file.version) ?? [];
755
+ const verdict = audit
756
+ ? { ...audit, ...(advisories.length > 0 ? { advisories } : {}) }
757
+ : advisories.length > 0
758
+ ? { relation: "allowed", advisories }
759
+ : undefined;
760
+ return this.download(adapter, context, name, route, file, request.signal, acceptedAdvisoriesReason(waivers.get(file.version) ?? []), verdict);
761
+ }
762
+ /** The allowances serving a package's metadata grows: its kept versions'
763
+ * dependencies, and, for an audited package, each tagged with the
764
+ * top-level audited package that declared it (its own name for a
765
+ * top-level one, which is also recorded for itself so its later
766
+ * downloads read as top-level). */
767
+ async allowServed(context, adapter, name, dependencies, audit) {
768
+ const store = this.options.store;
769
+ if (!audit)
770
+ return store.addAllowances(context.runId, adapter.id, dependencies);
771
+ if (audit.relation === "top-level")
772
+ await store.addAuditedAllowances(context.runId, adapter.id, [name], name);
773
+ await store.addAuditedAllowances(context.runId, adapter.id, dependencies, audit.via ?? name);
496
774
  }
497
775
  hostAllowed(adapter, url) {
498
776
  try {
@@ -513,32 +791,71 @@ export class RegistryService {
513
791
  }
514
792
  return tally.served;
515
793
  }
516
- async authorize(adapter, context, name) {
794
+ async authorize(adapter, context, name, kind) {
517
795
  const entries = parseAllowlist(context.allowlist, this.options.adapters).get(adapter.id) ?? [];
796
+ // A root keeps its own range in every mode: audit mode never serves a
797
+ // version an entry's range excludes (keptVersions applies root.range).
518
798
  const root = matchRoot(entries, name);
519
- if (root || (await this.options.store.isAllowedDependency(context.runId, adapter.id, name)))
520
- return root;
521
- // Not a root and not yet an allowance: the name may still be in the
522
- // approved graph, unrecorded only because the client never asked for
523
- // its parent's metadata (a lockfile install requests tarballs
524
- // directly). Resolve the graph before refusing.
799
+ if (root)
800
+ return { root };
801
+ const allowance = await this.options.store.findAllowance(context.runId, adapter.id, name);
802
+ if (allowance) {
803
+ if (allowance.auditedVia === null)
804
+ return { root: undefined };
805
+ if (allowance.auditedVia === AUDITED_VIA_UNKNOWN)
806
+ return { root: undefined, audit: { relation: "unknown" } };
807
+ return allowance.auditedVia === name
808
+ ? { root: undefined, audit: { relation: "top-level" } }
809
+ : { root: undefined, audit: { relation: "transitive", via: allowance.auditedVia } };
810
+ }
811
+ if (context.packageAudit) {
812
+ // Package audit mode: a miss is never refused and never walks; it is
813
+ // served (through every other check) and recorded. A download with
814
+ // no metadata first, or a metadata miss once a lockfile plan of the
815
+ // run fell back, cannot say how the package reached the run.
816
+ const fellBack = this.tally(context).planFallback?.has(adapter.id) ?? false;
817
+ return { root: undefined, audit: { relation: kind === "download" || fellBack ? "unknown" : "top-level" } };
818
+ }
819
+ // A package manager resolves top-down: it reads a parent's metadata,
820
+ // which records the parent's declared dependencies as allowances,
821
+ // before it asks for a child. A metadata request for a name that is
822
+ // neither a root nor an allowance is therefore refused at once, unless
823
+ // a lockfile plan of this run failed or refused an entry (then the
824
+ // lockfile's packages may lack allowances, and the miss walks).
825
+ if (kind !== "download" && !this.tally(context).planFallback?.has(adapter.id)) {
826
+ await this.refuse(context, adapter, name, PACKAGE_NOT_ALLOWED);
827
+ throw new RegistryError(403, PACKAGE_NOT_ALLOWED, notAllowedMessage(name, adapter.id, entries.length > 0));
828
+ }
829
+ // A download (or, after a failed or partial plan, a metadata request)
830
+ // that is not yet an allowance: the name may still be in the approved
831
+ // graph, unrecorded only because the client never asked for its
832
+ // parent's metadata (a lockfile install requests tarballs directly).
833
+ // Resolve the graph before refusing.
525
834
  const graph = await this.resolveInGraph(adapter, context, entries, name);
526
835
  if (graph === "found")
527
- return undefined;
836
+ return { root: undefined };
528
837
  if (typeof graph === "object" && "cutShort" in graph) {
529
838
  if (graph.cutShort === "limit") {
530
839
  // The bound is permanent for the rest of the run, so a retry can
531
840
  // never succeed: answer with a definitive 4xx (npm does not retry
532
841
  // it) rather than a 503 that a client or agent would retry forever.
533
842
  // Still not "not allowed": the name was not proven absent.
534
- await this.refuse(context, adapter, name, "wardby_graph_limit");
535
- throw new RegistryError(403, "wardby_graph_limit", `"${name}" was not reached before this agent's ${adapter.id} dependency graph walk hit its package limit (REGISTRY_MAX_GRAPH_PACKAGES) for this run; retrying will not help: allowlist the package directly, or ask the operator to raise the limit`);
843
+ await this.refuse(context, adapter, name, GRAPH_LIMIT);
844
+ throw new RegistryError(403, GRAPH_LIMIT, graphLimitMessage(name, adapter.id, graph.limit));
536
845
  }
537
- // Not proven absent, so never "not allowed": a timed-out walk resumes
538
- // where it stopped on the next miss, so a retry (npm retries a 5xx on
539
- // its own) can find the name.
540
- await this.refuse(context, adapter, name, "wardby_graph_incomplete");
541
- throw new RegistryError(503, "wardby_graph_incomplete", `"${name}" was not reached before this agent's ${adapter.id} dependency graph walk was cut short by its time limit (REGISTRY_GRAPH_TIMEOUT_MS); the walk continues where it stopped: try again`);
846
+ if (graph.frontier) {
847
+ // An expanded version declares the name and only its own versions
848
+ // are left to check: the walk resumes there on the next miss, so a
849
+ // retry (npm retries a 5xx on its own) can find it.
850
+ await this.refuse(context, adapter, name, GRAPH_INCOMPLETE);
851
+ throw new RegistryError(503, GRAPH_INCOMPLETE, graphIncompleteMessage(name, adapter.id, graph.progress));
852
+ }
853
+ // The walk never reached the name. Not proven absent, so not the
854
+ // final "not allowed", but definite (a 4xx npm does not retry): a
855
+ // later miss may still resume the walk and find it, which is no
856
+ // reason to keep the agent waiting on retries now.
857
+ await this.refuse(context, adapter, name, PACKAGE_NOT_ALLOWED_PENDING);
858
+ throw new RegistryError(403, PACKAGE_NOT_ALLOWED_PENDING, notAllowedPendingMessage(name, adapter.id, graph.progress));
542
859
  }
543
860
  if (typeof graph === "object") {
544
861
  // Some part of the graph could not be read, so whether the name is in
@@ -549,19 +866,18 @@ export class RegistryService {
549
866
  await this.refuse(context, adapter, name, code);
550
867
  throw new RegistryError(status, code, `"${name}" could not be checked against this agent's approved ${adapter.id} dependency graph because ${source} was unavailable while resolving it; try again`);
551
868
  }
552
- await this.refuse(context, adapter, name, "wardby_package_not_allowed");
553
- const hint = entries.length === 0
554
- ? `this agent has no ${adapter.id} package allowlist`
555
- : `"${name}" is not on this agent's ${adapter.id} package allowlist or in its dependency graph`;
556
- throw new RegistryError(403, "wardby_package_not_allowed", hint);
869
+ await this.refuse(context, adapter, name, PACKAGE_NOT_ALLOWED);
870
+ throw new RegistryError(403, PACKAGE_NOT_ALLOWED, notAllowedMessage(name, adapter.id, entries.length > 0));
557
871
  }
558
872
  /** Whether `name` is in the run's approved graph for this ecosystem:
559
873
  * the allowlist's exact roots plus the dependencies of every kept
560
874
  * version (the same filter the metadata path applies: root range,
561
875
  * release age, advisories), transitively. Each newly found name is
562
876
  * recorded as an allowance as the walk goes, exactly as if its
563
- * parent's metadata had been served. A scoped wildcard root cannot be
564
- * enumerated, so it is never a starting point. */
877
+ * parent's metadata had been served. Only a download asks, or a
878
+ * metadata miss after a failed or partial lockfile plan (see
879
+ * `authorize`). A scoped wildcard root cannot be enumerated, so it is
880
+ * never a starting point. */
565
881
  async resolveInGraph(adapter, context, entries, name) {
566
882
  if (!adapter.dependenciesInMetadata)
567
883
  return "absent";
@@ -573,13 +889,16 @@ export class RegistryService {
573
889
  const edges = roots.map((root) => ({ name: root, range: "*" }));
574
890
  walk = {
575
891
  found: new Set(roots),
576
- queue: edges,
892
+ order: [],
893
+ pending: new Map(),
577
894
  seen: new Set(edges.map(edgeKey)),
578
895
  expandedVersions: new Map(),
579
896
  exhausted: false,
580
897
  attempts: new Map(),
581
898
  failed: new Map(),
582
899
  };
900
+ for (const edge of edges)
901
+ addPending(walk, edge.name, [edge]);
583
902
  tally.graphs.set(adapter.id, walk);
584
903
  }
585
904
  const current = walk;
@@ -588,7 +907,11 @@ export class RegistryService {
588
907
  if (current.found.has(name))
589
908
  return "found";
590
909
  if (current.exhausted)
591
- return { cutShort: "limit" };
910
+ return {
911
+ cutShort: "limit",
912
+ progress: walkProgress(current),
913
+ limit: this.options.maxGraphPackages ?? DEFAULT_MAX_GRAPH_PACKAGES,
914
+ };
592
915
  // Single-flight: a miss while a walk is running waits for it rather
593
916
  // than starting a second one, then re-checks (that walk may have
594
917
  // stopped on finding a different name). Checked before the queue: a
@@ -597,17 +920,19 @@ export class RegistryService {
597
920
  // starts at most one walk of its own, so it spends at most one
598
921
  // round of retries on failed nodes.
599
922
  if (!current.running) {
600
- const retriable = [...current.failed.keys()].some((key) => (current.attempts.get(key) ?? 0) < MAX_GRAPH_NODE_ATTEMPTS);
601
- if (started || (current.queue.length === 0 && !retriable))
923
+ const anyRetriable = [...current.failed.keys()].some((key) => retriable(current, key));
924
+ if (started || (current.pending.size === 0 && !anyRetriable))
602
925
  return this.graphMiss(current);
603
926
  started = true;
604
927
  current.running = this.walkGraph(adapter, context, entries, current, name).finally(() => {
605
928
  current.running = undefined;
606
929
  });
607
930
  }
608
- const end = await current.running;
609
- if (end === "timeout" && !current.found.has(name))
610
- return { cutShort: "timeout" };
931
+ const result = await current.running;
932
+ // Read from the walk's own snapshot, never from the live walk: a
933
+ // miss in the meantime may have started the next walk already.
934
+ if (result.end === "timeout" && !current.found.has(name))
935
+ return { cutShort: "timeout", progress: result.progress, frontier: result.frontier?.has(name) ?? false };
611
936
  }
612
937
  }
613
938
  /** The answer for a name the walk did not find: unavailable while any
@@ -618,21 +943,42 @@ export class RegistryService {
618
943
  return "absent";
619
944
  return { unavailable: [...walk.failed.values()].some((entry) => entry.cause === "audit") ? "audit" : "upstream" };
620
945
  }
946
+ /** One walk, logged once when it stops, with its progress (and, on a
947
+ * timeout, its frontier) taken before `running` is cleared. */
621
948
  async walkGraph(adapter, context, entries, walk, target) {
949
+ const startedAt = this.now().getTime();
950
+ const loadsBefore = walk.loads ?? 0;
951
+ const end = await this.expandGraph(adapter, context, entries, walk, target);
952
+ const progress = walkProgress(walk);
953
+ registryLog.info({
954
+ event: "registry.graph_walk",
955
+ runId: context.runId,
956
+ ecosystem: adapter.id,
957
+ end,
958
+ walked: progress.walked,
959
+ queued: progress.queued,
960
+ queuedEdges: unexpandedEdgeCount(walk),
961
+ loads: (walk.loads ?? 0) - loadsBefore,
962
+ durationMs: this.now().getTime() - startedAt,
963
+ }, "coding registry graph walk stopped");
964
+ return end === "timeout" ? { end, progress, frontier: unexpandedNames(walk) } : { end, progress };
965
+ }
966
+ async expandGraph(adapter, context, entries, walk, target) {
622
967
  const maxPackages = this.options.maxGraphPackages ?? DEFAULT_MAX_GRAPH_PACKAGES;
623
968
  const deadline = this.now().getTime() + (this.options.graphTimeoutMs ?? DEFAULT_GRAPH_TIMEOUT_MS);
624
- // Parked failures with attempts left go first: a later miss is this
625
- // walk's retry of them.
626
- const retry = [...walk.failed.entries()].filter(([key]) => (walk.attempts.get(key) ?? 0) < MAX_GRAPH_NODE_ATTEMPTS);
627
- for (const [key] of retry)
628
- walk.failed.delete(key);
629
- walk.queue.unshift(...retry.map(([, entry]) => entry.edge));
969
+ // Parked failures with attempts left go first (a later miss is this
970
+ // walk's retry of them), and the requested name before everything:
971
+ // order changes only how soon an answer comes, never which edges are
972
+ // followed or what they allow.
973
+ const retry = [...walk.failed.entries()].filter(([name]) => retriable(walk, name));
974
+ for (const [name] of retry)
975
+ walk.failed.delete(name);
976
+ for (const [name, entry] of retry.reverse())
977
+ addPending(walk, name, entry.edges, true);
978
+ if (walk.pending.has(target))
979
+ moveToFront(walk, target);
630
980
  const attemptsThisWalk = new Map();
631
- // Per-walk memo: a walk lasts at most the graph timeout, well inside
632
- // the metadata and audit cache lifetimes, so this changes no decision.
633
- // A failed computation is not memoized, so its retry really retries.
634
- const kept = new Map();
635
- while (walk.queue.length > 0) {
981
+ while (walk.pending.size > 0) {
636
982
  if (walk.found.has(target))
637
983
  return "found";
638
984
  if (walk.released)
@@ -645,65 +991,68 @@ export class RegistryService {
645
991
  walk.exhausted = true;
646
992
  return "limit";
647
993
  }
648
- // Each edge adds at most one new package, so a batch no larger than
649
- // the room left can never overshoot the bound.
650
- const batch = walk.queue.splice(0, Math.min(GRAPH_WALK_CONCURRENCY, room));
651
- const expansions = Promise.all(batch.map((edge) => this.graphNode(adapter, context, entries, edge, kept)));
994
+ // Each name adds at most one new package, so a batch no larger than
995
+ // the room left can never overshoot the bound. A name's node takes
996
+ // every edge pending for it.
997
+ const batch = walk.order.splice(0, Math.min(GRAPH_WALK_CONCURRENCY, room)).map((name) => {
998
+ const edges = walk.pending.get(name) ?? [];
999
+ walk.pending.delete(name);
1000
+ return { name, edges };
1001
+ });
1002
+ const expansions = Promise.all(batch.map(({ name, edges }) => this.graphNode(adapter, context, entries, name, edges, walk)));
652
1003
  let timer;
653
1004
  const timedOut = new Promise((resolve) => {
654
1005
  timer = setTimeout(() => resolve("timeout"), remaining);
655
1006
  });
656
1007
  const results = await Promise.race([expansions, timedOut]).finally(() => clearTimeout(timer));
657
1008
  if (results === "timeout") {
658
- // Not expanded: put the batch back so a later miss retries it (its
659
- // metadata is likely cached by then).
660
- walk.queue.unshift(...batch);
1009
+ // Not expanded: put the whole batch back, in order, so a later
1010
+ // miss retries it (its node is likely memoized by then).
1011
+ for (const { name, edges } of [...batch].reverse())
1012
+ addPending(walk, name, edges, true);
661
1013
  return "timeout";
662
1014
  }
663
1015
  const expanded = [];
664
- batch.forEach((edge, index) => {
1016
+ batch.forEach(({ name, edges }, index) => {
665
1017
  const result = results[index];
666
1018
  if (result.ok) {
667
- expanded.push({ edge, selected: result.selected });
1019
+ expanded.push({ name, edges, selected: result.selected });
668
1020
  return;
669
1021
  }
670
- // A transient failure is never counted as expanded: the edge is
1022
+ // A transient failure is never counted as expanded: the node is
671
1023
  // retried (once more in this walk, then by later misses) instead
672
1024
  // of silently closing its whole subtree for the run.
673
- const key = edgeKey(edge);
674
- walk.attempts.set(key, (walk.attempts.get(key) ?? 0) + 1);
675
- attemptsThisWalk.set(key, (attemptsThisWalk.get(key) ?? 0) + 1);
676
- if ((attemptsThisWalk.get(key) ?? 0) < GRAPH_NODE_ATTEMPTS_PER_WALK &&
677
- (walk.attempts.get(key) ?? 0) < MAX_GRAPH_NODE_ATTEMPTS)
678
- walk.queue.push(edge);
1025
+ walk.attempts.set(name, (walk.attempts.get(name) ?? 0) + 1);
1026
+ attemptsThisWalk.set(name, (attemptsThisWalk.get(name) ?? 0) + 1);
1027
+ if ((attemptsThisWalk.get(name) ?? 0) < GRAPH_NODE_ATTEMPTS_PER_WALK && retriable(walk, name))
1028
+ addPending(walk, name, edges);
679
1029
  else
680
- walk.failed.set(key, { edge, cause: result.cause });
1030
+ walk.failed.set(name, { edges, cause: result.cause });
681
1031
  });
682
1032
  // A package joins the graph (and gets its allowance) only when some
683
1033
  // kept version of it satisfies a range that reached it. Recorded
684
1034
  // before it counts as found, so `found` never holds a name the store
685
1035
  // would refuse.
686
- const discovered = [
687
- ...new Set(expanded
688
- .filter(({ edge, selected }) => selected.length > 0 && !walk.found.has(edge.name))
689
- .map(({ edge }) => edge.name)),
690
- ];
1036
+ const discovered = expanded
1037
+ .filter(({ name, selected }) => selected.length > 0 && !walk.found.has(name))
1038
+ .map(({ name }) => name);
691
1039
  try {
692
1040
  if (discovered.length > 0)
693
1041
  await this.options.store.addAllowances(context.runId, adapter.id, discovered);
694
1042
  }
695
1043
  catch (error) {
696
- // The store write failed: put the expanded edges back so a later
1044
+ // The store write failed: put the expanded groups back so a later
697
1045
  // miss re-expands them, rather than leaving the graph silently short.
698
- walk.queue.unshift(...expanded.map(({ edge }) => edge));
1046
+ for (const { name, edges } of [...expanded].reverse())
1047
+ addPending(walk, name, edges, true);
699
1048
  throw error;
700
1049
  }
701
1050
  for (const name of discovered)
702
1051
  walk.found.add(name);
703
- for (const { edge, selected } of expanded) {
704
- let versions = walk.expandedVersions.get(edge.name);
1052
+ for (const { name, selected } of expanded) {
1053
+ let versions = walk.expandedVersions.get(name);
705
1054
  if (!versions)
706
- walk.expandedVersions.set(edge.name, (versions = new Set()));
1055
+ walk.expandedVersions.set(name, (versions = new Set()));
707
1056
  for (const { version, dependencies } of selected) {
708
1057
  if (versions.has(version))
709
1058
  continue;
@@ -713,45 +1062,43 @@ export class RegistryService {
713
1062
  if (walk.seen.has(key))
714
1063
  continue;
715
1064
  walk.seen.add(key);
716
- walk.queue.push(dependency);
1065
+ // A package parked after failing waits with its edges and is
1066
+ // retried with them.
1067
+ const parked = walk.failed.get(dependency.name);
1068
+ if (parked)
1069
+ parked.edges.push(dependency);
1070
+ else
1071
+ addPending(walk, dependency.name, [dependency], dependency.name === target);
717
1072
  }
718
1073
  }
719
1074
  }
720
1075
  }
721
1076
  return walk.found.has(target) ? "found" : "complete";
722
1077
  }
723
- /** The kept versions of one package inside the edge's range, and each
724
- * one's declared dependencies, through the same metadata cache and
725
- * `keptVersions` filter the metadata path uses (a root's allowlist range
726
- * applies there), within the process-wide walk concurrency cap. `"*"`
727
- * selects every kept version; a range no kept version satisfies selects
728
- * none. A package npm doesn't have, or whose metadata is too large,
729
- * definitively selects nothing; any other failure (upstream error or
730
- * timeout, audit unavailable) is transient and reported for retry. */
731
- graphNode(adapter, context, entries, edge, memo) {
1078
+ /** The kept versions of one package inside any of the ranges that reached
1079
+ * it, and each one's declared dependencies, through the same metadata
1080
+ * cache and `keptVersions` filter the metadata path uses (a root's
1081
+ * allowlist range applies there), within the process-wide walk
1082
+ * concurrency cap. A `"*"` edge selects every kept version; a range no
1083
+ * kept version satisfies selects none. A package npm doesn't have, or
1084
+ * whose metadata is too large, definitively selects nothing; any other
1085
+ * failure (upstream error or timeout, audit unavailable) is transient
1086
+ * and reported for retry. */
1087
+ graphNode(adapter, context, entries, name, edges, walk) {
732
1088
  return this.walkSlots.run(async () => {
733
1089
  try {
734
- let load = memo.get(edge.name);
735
- if (!load) {
736
- load = (async () => {
737
- const meta = await this.metadata(adapter, edge.name, false);
738
- const { keep } = await this.keptVersions(adapter, context, meta, matchRoot(entries, edge.name));
739
- return new Map([...keep].map((version) => [version, versionDependencies(adapter, meta, version)]));
740
- })();
741
- memo.set(edge.name, load);
742
- load.catch(() => memo.delete(edge.name));
743
- }
744
- const keep = await load;
745
- const inRange = (version) => {
746
- if (edge.range === "*")
1090
+ const keep = await this.keptForGraph(adapter, context, entries, name, walk);
1091
+ const ranges = [...new Set(edges.map((edge) => edge.range))];
1092
+ const inRange = (version) => ranges.some((range) => {
1093
+ if (range === "*")
747
1094
  return true;
748
1095
  try {
749
- return adapter.satisfies(version, edge.range);
1096
+ return adapter.satisfies(version, range);
750
1097
  }
751
1098
  catch {
752
1099
  return false;
753
1100
  }
754
- };
1101
+ });
755
1102
  const selected = [];
756
1103
  for (const [version, dependencies] of keep)
757
1104
  if (inRange(version))
@@ -766,6 +1113,56 @@ export class RegistryService {
766
1113
  }
767
1114
  });
768
1115
  }
1116
+ /** A package's kept versions for the run's walk, from the memo or
1117
+ * computed (and memoized). A failed computation is not memoized, so its
1118
+ * retry really retries. */
1119
+ async keptForGraph(adapter, context, entries, name, walk) {
1120
+ const key = `${context.runId}\0${adapter.id}\0${name}`;
1121
+ const now = this.now().getTime();
1122
+ const memoized = this.graphMemo.get(key);
1123
+ if (memoized) {
1124
+ this.dropMemo(key);
1125
+ if (memoized.expires > now) {
1126
+ this.putMemo(key, memoized); // most recently used
1127
+ return memoized.kept;
1128
+ }
1129
+ }
1130
+ if (walk)
1131
+ walk.loads = (walk.loads ?? 0) + 1;
1132
+ const meta = await this.metadata(adapter, name, false);
1133
+ const { keep } = await this.keptVersions(adapter, context, meta, matchRoot(entries, name));
1134
+ const kept = new Map([...keep].map((version) => [version, versionDependencies(adapter, meta, version)]));
1135
+ // A run released meanwhile (its walk stopped) keeps nothing here.
1136
+ if (this.tallies.has(context.runId))
1137
+ this.memoize(key, kept);
1138
+ return kept;
1139
+ }
1140
+ dropMemo(key) {
1141
+ const entry = this.graphMemo.get(key);
1142
+ if (!entry)
1143
+ return;
1144
+ this.graphMemo.delete(key);
1145
+ this.graphMemoTotal -= entry.bytes;
1146
+ }
1147
+ putMemo(key, entry) {
1148
+ this.graphMemo.set(key, entry);
1149
+ this.graphMemoTotal += entry.bytes;
1150
+ }
1151
+ /** Memoizes an expansion, least recently used evicted first, within the
1152
+ * byte budget. An entry larger than the whole budget is not kept. */
1153
+ memoize(key, kept) {
1154
+ const now = this.now().getTime();
1155
+ for (const [memoKey, entry] of this.graphMemo)
1156
+ if (entry.expires <= now)
1157
+ this.dropMemo(memoKey);
1158
+ this.dropMemo(key);
1159
+ const bytes = keptBytes(key, kept);
1160
+ if (bytes > this.graphMemoBytes)
1161
+ return;
1162
+ while (this.graphMemo.size > 0 && this.graphMemoTotal + bytes > this.graphMemoBytes)
1163
+ this.dropMemo(this.graphMemo.keys().next().value);
1164
+ this.putMemo(key, { expires: now + GRAPH_MEMO_TTL_MS, bytes, kept });
1165
+ }
769
1166
  /** A package's metadata, cached. `render` asks for the form
770
1167
  * `renderMetadata` can serve; a cached entry fetched without it (by the
771
1168
  * graph walk or a download) is fetched again with it, and replaces it. */
@@ -836,11 +1233,20 @@ export class RegistryService {
836
1233
  this.putCached(key, { expires: now + this.metadataTtlMs, bytes, meta });
837
1234
  }
838
1235
  async keptVersions(adapter, context, meta, root) {
839
- const { minReleaseAgeDays } = resolvePolicy(context.policy);
1236
+ const { minReleaseAgeDays, acceptedAdvisories } = resolvePolicy(context.policy);
1237
+ // Package audit mode `full` lets non-malware blocking advisories through;
1238
+ // malware is withheld whatever the mode (audit.ts splitWaived).
1239
+ const auditAll = context.packageAudit === "full";
840
1240
  const cutoff = this.now().getTime() - minReleaseAgeDays * DAY_MS;
1241
+ // The run's accepted advisories waive the blocking ones they name
1242
+ // (never malware); fixed for the run (snapshotted at dispatch).
1243
+ const accepted = new Set(acceptedAdvisories);
841
1244
  const advisories = await this.options.audit.audit(adapter, meta.name);
842
1245
  const keep = new Set();
843
1246
  const keptFiles = new Set();
1247
+ /** Kept version -> the advisories waived to keep it (only non-empty). */
1248
+ const waivers = new Map();
1249
+ const auditAdvisories = new Map();
844
1250
  for (const info of meta.versions.values()) {
845
1251
  if (root?.range && !adapter.satisfies(info.version, root.range))
846
1252
  continue;
@@ -850,13 +1256,23 @@ export class RegistryService {
850
1256
  const oldEnough = info.files.filter((file) => file.publishedAt && file.publishedAt.getTime() <= cutoff);
851
1257
  if (oldEnough.length === 0)
852
1258
  continue;
853
- if (advisories.withheld(info.version).length > 0)
1259
+ if (advisories.withheld(info.version, accepted, auditAll).length > 0)
854
1260
  continue;
855
1261
  keep.add(info.version);
1262
+ if (auditAll) {
1263
+ const audited = advisories.audited?.(info.version, accepted) ?? [];
1264
+ if (audited.length > 0)
1265
+ auditAdvisories.set(info.version, [...audited]);
1266
+ }
856
1267
  for (const file of oldEnough)
857
1268
  keptFiles.add(file.filename);
1269
+ if (accepted.size > 0) {
1270
+ const waived = advisories.waived(info.version, accepted);
1271
+ if (waived.length > 0)
1272
+ waivers.set(info.version, waived);
1273
+ }
858
1274
  }
859
- return { keep, keptFiles };
1275
+ return { keep, keptFiles, waivers, auditAdvisories };
860
1276
  }
861
1277
  tally(context) {
862
1278
  const now = this.now().getTime();
@@ -896,6 +1312,10 @@ export class RegistryService {
896
1312
  for (const walk of tally.graphs?.values() ?? [])
897
1313
  walk.released = true;
898
1314
  tally.graphs?.clear();
1315
+ const prefix = `${runId}\0`;
1316
+ for (const key of [...this.graphMemo.keys()])
1317
+ if (key.startsWith(prefix))
1318
+ this.dropMemo(key);
899
1319
  }
900
1320
  /** Releases every run's in-process state (timers included), e.g. when the
901
1321
  * server shuts down. */
@@ -972,7 +1392,40 @@ export class RegistryService {
972
1392
  extras.add(extra);
973
1393
  return [...extras].sort();
974
1394
  }
975
- async download(adapter, context, name, route, file, signal) {
1395
+ /** One RegistryAuditRecord (the first served file of a version wins) and
1396
+ * its `registry.audited` log line. */
1397
+ async recordAudit(context, adapter, name, version, sizeBytes, audit) {
1398
+ const advisories = audit.advisories ?? [];
1399
+ await this.options.store.recordAudit({
1400
+ runId: context.runId,
1401
+ ecosystem: adapter.id,
1402
+ name,
1403
+ version,
1404
+ relation: audit.relation,
1405
+ ...(audit.via ? { via: audit.via } : {}),
1406
+ sizeBytes,
1407
+ advisories,
1408
+ });
1409
+ registryLog.info({
1410
+ event: "registry.audited",
1411
+ runId: context.runId,
1412
+ ecosystem: adapter.id,
1413
+ name,
1414
+ version,
1415
+ sizeBytes,
1416
+ relation: audit.relation,
1417
+ ...(audit.via ? { via: audit.via } : {}),
1418
+ mode: context.packageAudit,
1419
+ advisories: advisories.map((advisory) => advisory.id),
1420
+ }, "coding registry served a package under package audit mode");
1421
+ }
1422
+ async download(adapter, context, name, route, file, signal,
1423
+ /** The served row's reason: set only when serving it waived accepted
1424
+ * advisories (`accepted advisories: <ids>`). */
1425
+ servedReason,
1426
+ /** Set when package audit mode is why it is served: recorded once the
1427
+ * file completes, beside its served row. */
1428
+ audit) {
976
1429
  const { limits, store } = this.options;
977
1430
  const runId = context.runId;
978
1431
  const served = await this.servedUsage(context);
@@ -1100,6 +1553,7 @@ export class RegistryService {
1100
1553
  integrity: file.integrity ? `${file.integrity.algorithm}:${file.integrity.hex}` : undefined,
1101
1554
  sizeBytes: bytes,
1102
1555
  outcome: "served",
1556
+ ...(servedReason ? { reason: servedReason } : {}),
1103
1557
  });
1104
1558
  // Counted before release() drops the reservation, so no window
1105
1559
  // exists where neither the tally nor inFlight holds this file.
@@ -1109,7 +1563,16 @@ export class RegistryService {
1109
1563
  const body = Buffer.concat(buffered);
1110
1564
  const extras = await this.allowedExtras(adapter, context, name);
1111
1565
  const dependencies = (await adapter.dependenciesFromFile(route, body, extras).catch(() => [])).map((dependency) => ({ name: adapter.normalizeName(dependency.name), extras: dependency.extras }));
1112
- await store.addAllowances(context.runId, adapter.id, dependencies.map((dependency) => dependency.name));
1566
+ const names = dependencies.map((dependency) => dependency.name);
1567
+ // A file's dependencies are audited too when the package is
1568
+ // outside the allowlist (never for one kept only past advisories).
1569
+ if (audit && audit.relation !== "allowed") {
1570
+ if (audit.relation === "top-level")
1571
+ await store.addAuditedAllowances(context.runId, adapter.id, [name], name);
1572
+ await store.addAuditedAllowances(context.runId, adapter.id, names, audit.via ?? name);
1573
+ }
1574
+ else
1575
+ await store.addAllowances(context.runId, adapter.id, names);
1113
1576
  // Dependency extras are followed only below a package the run
1114
1577
  // allows with extras (an entry naming extras, or a dependency
1115
1578
  // such a package asked for with extras): a plain entry follows
@@ -1117,6 +1580,10 @@ export class RegistryService {
1117
1580
  if (extras.length > 0)
1118
1581
  await store.addExtraAllowances(context.runId, adapter.id, dependencies);
1119
1582
  }
1583
+ // Recorded before the stream closes: a failed write errors it
1584
+ // like a failed served row, so nothing is installed unrecorded.
1585
+ if (audit)
1586
+ await this.recordAudit(context, adapter, name, file.version, bytes, audit);
1120
1587
  controller.close();
1121
1588
  }
1122
1589
  catch (error) {