@wardby/cli 0.2.1 → 0.3.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 (407) hide show
  1. package/.env.example +34 -4
  2. package/README.md +41 -4
  3. package/dist/claude-coding-worker/driver.d.ts +6 -1
  4. package/dist/claude-coding-worker/driver.js +27 -1
  5. package/dist/claude-coding-worker/main.js +2 -0
  6. package/dist/claude-coding-worker/tool-socket.d.ts +9 -0
  7. package/dist/claude-coding-worker/tool-socket.js +26 -0
  8. package/dist/cli-help.d.ts +1 -1
  9. package/dist/cli-help.js +13 -4
  10. package/dist/cli.d.ts +1 -1
  11. package/dist/cli.js +219 -29
  12. package/dist/coding/collect-exclude.d.ts +25 -0
  13. package/dist/coding/collect-exclude.js +76 -0
  14. package/dist/coding/profile.d.ts +67 -22
  15. package/dist/coding/profile.js +58 -25
  16. package/dist/coding/protected-path-wording.d.ts +24 -0
  17. package/dist/coding/protected-path-wording.js +55 -0
  18. package/dist/coding/protected-paths.d.ts +31 -0
  19. package/dist/coding/protected-paths.js +48 -0
  20. package/dist/coding/protocol.d.ts +75 -4
  21. package/dist/coding/protocol.js +107 -7
  22. package/dist/coding/registry/adapters.d.ts +2 -0
  23. package/dist/coding/registry/adapters.js +9 -0
  24. package/dist/coding/registry/allowlist.d.ts +9 -0
  25. package/dist/coding/registry/allowlist.js +28 -0
  26. package/dist/coding/registry/json-scan.d.ts +55 -0
  27. package/dist/coding/registry/json-scan.js +191 -0
  28. package/dist/coding/registry/lockfiles.d.ts +6 -0
  29. package/dist/coding/registry/lockfiles.js +89 -0
  30. package/dist/coding/registry/npm-lockfile.d.ts +9 -0
  31. package/dist/coding/registry/npm-lockfile.js +73 -0
  32. package/dist/coding/registry/npm-plan.d.ts +29 -0
  33. package/dist/coding/registry/npm-plan.js +289 -0
  34. package/dist/coding/registry/npm.d.ts +7 -0
  35. package/dist/coding/registry/npm.js +252 -0
  36. package/dist/coding/registry/pypi.d.ts +4 -0
  37. package/dist/coding/registry/pypi.js +210 -0
  38. package/dist/coding/registry/report.d.ts +37 -0
  39. package/dist/coding/registry/report.js +31 -0
  40. package/dist/coding/registry/token.d.ts +4 -0
  41. package/dist/coding/registry/token.js +7 -0
  42. package/dist/coding/registry/types.d.ts +286 -0
  43. package/dist/coding/registry/types.js +19 -0
  44. package/dist/coding/registry/worker-config.d.ts +14 -0
  45. package/dist/coding/registry/worker-config.js +31 -0
  46. package/dist/coding/services/builtins.d.ts +22 -0
  47. package/dist/coding/services/builtins.js +85 -0
  48. package/dist/coding/services/catalog.d.ts +443 -0
  49. package/dist/coding/services/catalog.js +159 -0
  50. package/dist/coding/services/declaration.d.ts +11 -0
  51. package/dist/coding/services/declaration.js +114 -0
  52. package/dist/coding/services/note.d.ts +8 -0
  53. package/dist/coding/services/note.js +15 -0
  54. package/dist/coding/services/resolve.d.ts +25 -0
  55. package/dist/coding/services/resolve.js +50 -0
  56. package/dist/coding/services/wording.d.ts +25 -0
  57. package/dist/coding/services/wording.js +70 -0
  58. package/dist/coding-proxy/main.js +24 -4
  59. package/dist/coding-worker/artifact.d.ts +6 -0
  60. package/dist/coding-worker/debug-trace.d.ts +37 -0
  61. package/dist/coding-worker/debug-trace.js +117 -0
  62. package/dist/coding-worker/driver.d.ts +13 -1
  63. package/dist/coding-worker/driver.js +102 -13
  64. package/dist/coding-worker/errors.js +8 -0
  65. package/dist/coding-worker/main.js +10 -2
  66. package/dist/coding-worker/sdk.d.ts +2 -2
  67. package/dist/coding-worker/sdk.js +7 -2
  68. package/dist/coding-worker/types.d.ts +3 -0
  69. package/dist/config/providers.d.ts +41 -0
  70. package/dist/config/providers.js +66 -0
  71. package/dist/core/budget-groups.d.ts +108 -5
  72. package/dist/core/budget-groups.js +125 -19
  73. package/dist/core/budget-wording.d.ts +22 -0
  74. package/dist/core/budget-wording.js +55 -0
  75. package/dist/core/datastores.js +18 -2
  76. package/dist/core/db.d.ts +5 -1
  77. package/dist/core/db.js +8 -2
  78. package/dist/core/dispatch.d.ts +33 -6
  79. package/dist/core/dispatch.js +260 -43
  80. package/dist/core/engine-native.js +27 -10
  81. package/dist/core/grants.d.ts +86 -0
  82. package/dist/core/grants.js +126 -0
  83. package/dist/core/host-events.d.ts +49 -0
  84. package/dist/core/host-events.js +257 -0
  85. package/dist/core/host-identity-links.d.ts +54 -0
  86. package/dist/core/host-identity-links.js +189 -0
  87. package/dist/core/host-status.d.ts +61 -0
  88. package/dist/core/host-status.js +211 -0
  89. package/dist/core/in-flight-runs.d.ts +8 -0
  90. package/dist/core/in-flight-runs.js +56 -0
  91. package/dist/core/provider-wording.d.ts +12 -0
  92. package/dist/core/provider-wording.js +44 -0
  93. package/dist/core/reconciler.d.ts +38 -2
  94. package/dist/core/reconciler.js +86 -2
  95. package/dist/core/repo-access.d.ts +99 -0
  96. package/dist/core/repo-access.js +136 -0
  97. package/dist/core/review-host-checks.d.ts +11 -0
  98. package/dist/core/review-host-checks.js +41 -0
  99. package/dist/core/review-host-tools.d.ts +47 -0
  100. package/dist/core/review-host-tools.js +347 -0
  101. package/dist/core/run-heartbeat.d.ts +27 -0
  102. package/dist/core/run-heartbeat.js +54 -0
  103. package/dist/core/runner.d.ts +30 -8
  104. package/dist/core/runner.js +237 -40
  105. package/dist/core/secrets.d.ts +11 -2
  106. package/dist/core/secrets.js +25 -6
  107. package/dist/core/subagent-memory-tools.d.ts +1 -1
  108. package/dist/core/subagent-memory-tools.js +16 -3
  109. package/dist/core/tool-admin.d.ts +81 -0
  110. package/dist/core/tool-admin.js +129 -0
  111. package/dist/core/tool-names.d.ts +42 -0
  112. package/dist/core/tool-names.js +64 -0
  113. package/dist/core/untrusted-content.d.ts +32 -0
  114. package/dist/core/untrusted-content.js +72 -0
  115. package/dist/core/webhooks.d.ts +8 -1
  116. package/dist/core/webhooks.js +23 -2
  117. package/dist/generated/prisma/browser.d.ts +100 -0
  118. package/dist/generated/prisma/client.d.ts +100 -0
  119. package/dist/generated/prisma/commonInputTypes.d.ts +30 -0
  120. package/dist/generated/prisma/enums.d.ts +6 -0
  121. package/dist/generated/prisma/enums.js +6 -1
  122. package/dist/generated/prisma/internal/class.d.ts +143 -0
  123. package/dist/generated/prisma/internal/class.js +4 -4
  124. package/dist/generated/prisma/internal/prismaNamespace.d.ts +1746 -577
  125. package/dist/generated/prisma/internal/prismaNamespace.js +179 -6
  126. package/dist/generated/prisma/internal/prismaNamespaceBrowser.d.ts +186 -0
  127. package/dist/generated/prisma/internal/prismaNamespaceBrowser.js +179 -6
  128. package/dist/generated/prisma/models/Agent.d.ts +294 -1
  129. package/dist/generated/prisma/models/AgentRepository.d.ts +1425 -0
  130. package/dist/generated/prisma/models/AgentRepository.js +1 -0
  131. package/dist/generated/prisma/models/AgentTool.d.ts +95 -1
  132. package/dist/generated/prisma/models/AuthUser.d.ts +56 -1
  133. package/dist/generated/prisma/models/CodingAgentProfile.d.ts +258 -7
  134. package/dist/generated/prisma/models/CodingProxySession.d.ts +123 -2
  135. package/dist/generated/prisma/models/CodingRun.d.ts +1276 -95
  136. package/dist/generated/prisma/models/CodingService.d.ts +1348 -0
  137. package/dist/generated/prisma/models/CodingService.js +1 -0
  138. package/dist/generated/prisma/models/HostEventDelivery.d.ts +946 -0
  139. package/dist/generated/prisma/models/HostEventDelivery.js +1 -0
  140. package/dist/generated/prisma/models/HostIdentity.d.ts +1232 -0
  141. package/dist/generated/prisma/models/HostIdentity.js +1 -0
  142. package/dist/generated/prisma/models/HostIdentityLinkRequest.d.ts +1473 -0
  143. package/dist/generated/prisma/models/HostIdentityLinkRequest.js +1 -0
  144. package/dist/generated/prisma/models/Principal.d.ts +455 -0
  145. package/dist/generated/prisma/models/RegistryAllowance.d.ts +1148 -0
  146. package/dist/generated/prisma/models/RegistryAllowance.js +1 -0
  147. package/dist/generated/prisma/models/RegistryApprovedVersion.d.ts +1219 -0
  148. package/dist/generated/prisma/models/RegistryApprovedVersion.js +1 -0
  149. package/dist/generated/prisma/models/RegistryFetch.d.ts +1428 -0
  150. package/dist/generated/prisma/models/RegistryFetch.js +1 -0
  151. package/dist/generated/prisma/models/RegistryPlanRefusal.d.ts +1294 -0
  152. package/dist/generated/prisma/models/RegistryPlanRefusal.js +1 -0
  153. package/dist/generated/prisma/models/RegistryVersionFact.d.ts +1085 -0
  154. package/dist/generated/prisma/models/RegistryVersionFact.js +1 -0
  155. package/dist/generated/prisma/models/ResourceGrant.d.ts +1437 -0
  156. package/dist/generated/prisma/models/ResourceGrant.js +1 -0
  157. package/dist/generated/prisma/models/Run.d.ts +389 -1
  158. package/dist/generated/prisma/models/RunHostCheck.d.ts +1239 -0
  159. package/dist/generated/prisma/models/RunHostCheck.js +1 -0
  160. package/dist/generated/prisma/models/RunHostStatus.d.ts +1315 -0
  161. package/dist/generated/prisma/models/RunHostStatus.js +1 -0
  162. package/dist/generated/prisma/models/Tool.d.ts +15 -3
  163. package/dist/generated/prisma/models.d.ts +13 -0
  164. package/dist/help/build.d.ts +1 -0
  165. package/dist/help/build.js +9 -0
  166. package/dist/help/catalog.d.ts +24 -0
  167. package/dist/help/catalog.js +160 -0
  168. package/dist/help/cli.d.ts +2 -0
  169. package/dist/help/cli.js +65 -0
  170. package/dist/help/runtime.d.ts +3 -0
  171. package/dist/help/runtime.js +44 -0
  172. package/dist/help/search.d.ts +9 -0
  173. package/dist/help/search.js +104 -0
  174. package/dist/help-index.json +660 -0
  175. package/dist/import/cli-args.js +3 -2
  176. package/dist/import/create.d.ts +5 -0
  177. package/dist/import/create.js +67 -13
  178. package/dist/import/index.js +19 -9
  179. package/dist/import/neutral-schema.d.ts +11 -11
  180. package/dist/mcp/auth/access.d.ts +70 -0
  181. package/dist/mcp/auth/access.js +58 -0
  182. package/dist/mcp/auth/grants-cli.d.ts +149 -0
  183. package/dist/mcp/auth/grants-cli.js +518 -0
  184. package/dist/mcp/auth/host-account-cli.d.ts +2 -0
  185. package/dist/mcp/auth/host-account-cli.js +47 -0
  186. package/dist/mcp/auth/ownership.d.ts +26 -52
  187. package/dist/mcp/auth/ownership.js +19 -14
  188. package/dist/mcp/auth/repo-authorization.d.ts +22 -0
  189. package/dist/mcp/auth/repo-authorization.js +51 -0
  190. package/dist/mcp/auth/resource-server.d.ts +31 -2
  191. package/dist/mcp/auth/resource-server.js +84 -3
  192. package/dist/mcp/auth/self-hosted/browser.js +2 -2
  193. package/dist/mcp/auth/self-hosted/cli.js +47 -13
  194. package/dist/mcp/auth/self-hosted/credentials.d.ts +20 -2
  195. package/dist/mcp/auth/self-hosted/credentials.js +62 -4
  196. package/dist/mcp/auth/self-hosted/session.d.ts +2 -1
  197. package/dist/mcp/context.d.ts +27 -1
  198. package/dist/mcp/errors.d.ts +25 -6
  199. package/dist/mcp/errors.js +98 -0
  200. package/dist/mcp/host-events/github-ingress.d.ts +36 -0
  201. package/dist/mcp/host-events/github-ingress.js +92 -0
  202. package/dist/mcp/host-events/github-user-callback.d.ts +18 -0
  203. package/dist/mcp/host-events/github-user-callback.js +50 -0
  204. package/dist/mcp/index.d.ts +8 -1
  205. package/dist/mcp/index.js +106 -16
  206. package/dist/mcp/server.js +27 -9
  207. package/dist/mcp/tools/agents.js +446 -48
  208. package/dist/mcp/tools/budget-groups.js +3 -3
  209. package/dist/mcp/tools/datastore.js +22 -15
  210. package/dist/mcp/tools/grants.d.ts +2 -0
  211. package/dist/mcp/tools/grants.js +239 -0
  212. package/dist/mcp/tools/help.d.ts +5 -0
  213. package/dist/mcp/tools/help.js +67 -0
  214. package/dist/mcp/tools/host-accounts.d.ts +2 -0
  215. package/dist/mcp/tools/host-accounts.js +114 -0
  216. package/dist/mcp/tools/memory.d.ts +7 -1
  217. package/dist/mcp/tools/memory.js +5 -5
  218. package/dist/mcp/tools/repositories.d.ts +2 -0
  219. package/dist/mcp/tools/repositories.js +181 -0
  220. package/dist/mcp/tools/runs.d.ts +6 -0
  221. package/dist/mcp/tools/runs.js +60 -6
  222. package/dist/mcp/tools/scheduling.js +3 -6
  223. package/dist/mcp/tools/secrets.js +18 -6
  224. package/dist/mcp/tools/services.d.ts +2 -0
  225. package/dist/mcp/tools/services.js +222 -0
  226. package/dist/mcp/tools/subagents.js +52 -16
  227. package/dist/mcp/tools/tools.d.ts +41 -0
  228. package/dist/mcp/tools/tools.js +201 -40
  229. package/dist/mcp/tools/trigger.js +36 -6
  230. package/dist/mcp/tools/webhooks.js +15 -4
  231. package/dist/mcp/transport/streamable-http.d.ts +10 -0
  232. package/dist/mcp/transport/streamable-http.js +32 -2
  233. package/dist/providers/auth/delegating.d.ts +19 -0
  234. package/dist/providers/auth/delegating.js +71 -0
  235. package/dist/providers/auth/index.d.ts +2 -0
  236. package/dist/providers/auth/index.js +11 -0
  237. package/dist/providers/auth/self-hosted.d.ts +2 -1
  238. package/dist/providers/auth/self-hosted.js +16 -4
  239. package/dist/providers/auth/types.d.ts +6 -0
  240. package/dist/providers/coding-proxy/memory-ledger.d.ts +3 -0
  241. package/dist/providers/coding-proxy/memory-ledger.js +21 -2
  242. package/dist/providers/coding-proxy/metering.d.ts +4 -0
  243. package/dist/providers/coding-proxy/metering.js +28 -2
  244. package/dist/providers/coding-proxy/prisma-ledger.d.ts +17 -0
  245. package/dist/providers/coding-proxy/prisma-ledger.js +50 -5
  246. package/dist/providers/coding-proxy/proxy.d.ts +13 -0
  247. package/dist/providers/coding-proxy/proxy.js +479 -18
  248. package/dist/providers/coding-proxy/registry/audit.d.ts +131 -0
  249. package/dist/providers/coding-proxy/registry/audit.js +380 -0
  250. package/dist/providers/coding-proxy/registry/bounded-fetch.d.ts +16 -0
  251. package/dist/providers/coding-proxy/registry/bounded-fetch.js +77 -0
  252. package/dist/providers/coding-proxy/registry/plan.d.ts +58 -0
  253. package/dist/providers/coding-proxy/registry/plan.js +304 -0
  254. package/dist/providers/coding-proxy/registry/prisma-store.d.ts +34 -0
  255. package/dist/providers/coding-proxy/registry/prisma-store.js +151 -0
  256. package/dist/providers/coding-proxy/registry/service.d.ts +213 -0
  257. package/dist/providers/coding-proxy/registry/service.js +1137 -0
  258. package/dist/providers/coding-proxy/registry/store.d.ts +127 -0
  259. package/dist/providers/coding-proxy/registry/store.js +70 -0
  260. package/dist/providers/coding-proxy/runtime.d.ts +17 -0
  261. package/dist/providers/coding-proxy/runtime.js +63 -0
  262. package/dist/providers/coding-proxy/secure-fetch.js +0 -1
  263. package/dist/providers/coding-proxy/server.d.ts +11 -0
  264. package/dist/providers/coding-proxy/server.js +163 -0
  265. package/dist/providers/coding-proxy/types.d.ts +15 -0
  266. package/dist/providers/engine/types.d.ts +10 -0
  267. package/dist/providers/executor/build.d.ts +2 -2
  268. package/dist/providers/executor/composition.d.ts +3 -0
  269. package/dist/providers/executor/composition.js +19 -0
  270. package/dist/providers/executor/container.d.ts +105 -3
  271. package/dist/providers/executor/container.js +296 -20
  272. package/dist/providers/executor/dbos.d.ts +2 -3
  273. package/dist/providers/executor/in-process.d.ts +2 -3
  274. package/dist/providers/executor/routing.d.ts +7 -0
  275. package/dist/providers/executor/routing.js +13 -0
  276. package/dist/providers/executor/types.d.ts +22 -0
  277. package/dist/providers/jobs/claude-tool-setup.d.ts +23 -0
  278. package/dist/providers/jobs/claude-tool-setup.js +50 -0
  279. package/dist/providers/jobs/collect-prune.d.ts +7 -0
  280. package/dist/providers/jobs/collect-prune.js +27 -0
  281. package/dist/providers/jobs/docker-isolation.d.ts +48 -3
  282. package/dist/providers/jobs/docker-isolation.js +213 -22
  283. package/dist/providers/jobs/docker-services.d.ts +35 -0
  284. package/dist/providers/jobs/docker-services.js +191 -0
  285. package/dist/providers/jobs/docker.d.ts +49 -2
  286. package/dist/providers/jobs/docker.js +279 -32
  287. package/dist/providers/jobs/fake-kubernetes-api.d.ts +1 -0
  288. package/dist/providers/jobs/fake-kubernetes-api.js +12 -3
  289. package/dist/providers/jobs/kubernetes-isolation.d.ts +17 -2
  290. package/dist/providers/jobs/kubernetes-isolation.js +238 -57
  291. package/dist/providers/jobs/kubernetes-platform.d.ts +10 -4
  292. package/dist/providers/jobs/kubernetes-platform.js +11 -5
  293. package/dist/providers/jobs/kubernetes-preflight.js +3 -0
  294. package/dist/providers/jobs/kubernetes.d.ts +21 -2
  295. package/dist/providers/jobs/kubernetes.js +112 -21
  296. package/dist/providers/jobs/types.d.ts +18 -0
  297. package/dist/providers/llm/anthropic.js +6 -2
  298. package/dist/providers/llm/bedrock.js +2 -1
  299. package/dist/providers/llm/claude-messages.d.ts +5 -1
  300. package/dist/providers/llm/claude-messages.js +1 -0
  301. package/dist/providers/llm/claude-provider.d.ts +3 -1
  302. package/dist/providers/llm/claude-provider.js +5 -1
  303. package/dist/providers/llm/index.d.ts +1 -1
  304. package/dist/providers/llm/index.js +1 -1
  305. package/dist/providers/llm/pricing-anthropic.d.ts +10 -0
  306. package/dist/providers/llm/pricing-anthropic.js +32 -14
  307. package/dist/providers/llm/pricing-bedrock-claude.d.ts +8 -0
  308. package/dist/providers/llm/pricing-bedrock-claude.js +9 -0
  309. package/dist/providers/llm/routing.d.ts +10 -1
  310. package/dist/providers/llm/routing.js +18 -0
  311. package/dist/providers/llm/types.d.ts +12 -0
  312. package/dist/providers/llm/types.js +8 -1
  313. package/dist/providers/review-host/diff-lines.d.ts +16 -0
  314. package/dist/providers/review-host/diff-lines.js +59 -0
  315. package/dist/providers/review-host/github-events.d.ts +6 -0
  316. package/dist/providers/review-host/github-events.js +180 -0
  317. package/dist/providers/review-host/github-user-auth.d.ts +38 -0
  318. package/dist/providers/review-host/github-user-auth.js +128 -0
  319. package/dist/providers/review-host/github.d.ts +57 -0
  320. package/dist/providers/review-host/github.js +567 -0
  321. package/dist/providers/review-host/index.d.ts +12 -0
  322. package/dist/providers/review-host/index.js +30 -0
  323. package/dist/providers/review-host/review-format.d.ts +19 -0
  324. package/dist/providers/review-host/review-format.js +59 -0
  325. package/dist/providers/review-host/types.d.ts +284 -0
  326. package/dist/providers/review-host/types.js +24 -0
  327. package/dist/providers/vcs/git.d.ts +16 -6
  328. package/dist/providers/vcs/git.js +77 -13
  329. package/dist/providers/vcs/github.d.ts +74 -3
  330. package/dist/providers/vcs/github.js +195 -15
  331. package/dist/providers/vcs/types.d.ts +42 -4
  332. package/dist/quickstart/index.js +13 -2
  333. package/dist/sandbox/fetch-policy.d.ts +20 -2
  334. package/dist/sandbox/fetch-policy.js +64 -3
  335. package/dist/sandbox/host-functions.d.ts +9 -1
  336. package/dist/sandbox/host-functions.js +12 -5
  337. package/dist/serve.js +1 -1
  338. package/dist/wardby-bin.js +6 -0
  339. package/docs/README.md +30 -0
  340. package/docs/architecture-runtime.md +90 -0
  341. package/docs/assets/brand/wardby-icon-512.png +0 -0
  342. package/docs/assets/brand/wardby-icon.svg +16 -0
  343. package/docs/assets/brand/wardby-mascot-profile-512.png +0 -0
  344. package/docs/assets/brand/wardby-mascot.png +0 -0
  345. package/docs/assets/brand/wardby-mascot.svg +5 -0
  346. package/docs/assets/wardby-workflow.svg +106 -0
  347. package/docs/code-review-agents.md +481 -0
  348. package/docs/coding-agent-setup.md +172 -0
  349. package/docs/coding-packages.md +455 -0
  350. package/docs/coding-services.md +300 -0
  351. package/docs/coding-worker-byo-images.md +98 -0
  352. package/docs/coding-worker-isolation.md +1061 -0
  353. package/docs/getting-started-gke.md +615 -0
  354. package/docs/getting-started-identity-provider.md +308 -0
  355. package/docs/getting-started.md +133 -0
  356. package/docs/observability.md +53 -0
  357. package/docs/release-verification.md +66 -0
  358. package/docs/security-deployment.md +657 -0
  359. package/help/code-review-agents.md +31 -0
  360. package/help/coding-packages.md +31 -0
  361. package/help/coding-services.md +71 -0
  362. package/help/creating-agents.md +68 -0
  363. package/help/deploy-gke.md +39 -0
  364. package/help/deployment-targets.md +39 -0
  365. package/help/errors/budget-group-exhausted.md +25 -0
  366. package/help/errors/docker-isolation-unsupported.md +26 -0
  367. package/help/errors/protected-path.md +45 -0
  368. package/help/errors/repo-access.md +27 -0
  369. package/help/errors/service-declaration-invalid.md +27 -0
  370. package/help/errors/service-declaration-unavailable.md +25 -0
  371. package/help/errors/service-launcher-unsupported.md +30 -0
  372. package/help/errors/service-not-allowed.md +27 -0
  373. package/help/errors/service-unknown.md +23 -0
  374. package/help/errors/service-unready.md +49 -0
  375. package/help/getting-started.md +31 -0
  376. package/help/github.md +30 -0
  377. package/help/identity-and-access.md +39 -0
  378. package/help/mcp.md +30 -0
  379. package/help/native-capabilities.md +30 -0
  380. package/help/observability.md +41 -0
  381. package/help/operating-agents.md +30 -0
  382. package/help/security.md +27 -0
  383. package/help/troubleshooting/budgets.md +26 -0
  384. package/help/troubleshooting/coding-workers.md +47 -0
  385. package/help/troubleshooting/repository-access.md +27 -0
  386. package/package.json +12 -3
  387. package/prisma/migrations/20260925010000_coding_collect_exclude/migration.sql +8 -0
  388. package/prisma/migrations/20260925015000_allowed_egress_default/migration.sql +8 -0
  389. package/prisma/migrations/20260925020000_coding_package_registry/migration.sql +55 -0
  390. package/prisma/migrations/20260925030000_tool_name_per_owner/migration.sql +11 -0
  391. package/prisma/migrations/20260926010000_run_trigger_host_event/migration.sql +7 -0
  392. package/prisma/migrations/20260926020000_code_review_hosts/migration.sql +53 -0
  393. package/prisma/migrations/20260926030000_auth_user_roles/migration.sql +9 -0
  394. package/prisma/migrations/20260926040000_agent_effort/migration.sql +6 -0
  395. package/prisma/migrations/20260926050000_registry_lockfile_plan/migration.sql +32 -0
  396. package/prisma/migrations/20260926050000_repo_access_authorization/migration.sql +82 -0
  397. package/prisma/migrations/20260926060000_registry_plan_refusal/migration.sql +19 -0
  398. package/prisma/migrations/20260926100000_registry_plan_refusal_published_at/migration.sql +5 -0
  399. package/prisma/migrations/20260926190000_run_host_status/migration.sql +18 -0
  400. package/prisma/migrations/20260926210000_run_host_status_at_dispatch/migration.sql +5 -0
  401. package/prisma/migrations/20260927010000_resource_grants/migration.sql +72 -0
  402. package/prisma/migrations/20260927020000_proxy_session_budget_exhausted/migration.sql +3 -0
  403. package/prisma/migrations/20260927030000_coding_debug_trace/migration.sql +4 -0
  404. package/prisma/migrations/20260927040000_proxy_session_upstream_failure/migration.sql +3 -0
  405. package/prisma/migrations/20260927050000_coding_run_services/migration.sql +74 -0
  406. package/prisma/migrations/20260928000000_coding_run_tool_image/migration.sql +5 -0
  407. package/prisma/schema.prisma +407 -21
@@ -1,14 +1,14 @@
1
1
  /**
2
- * Single seam for ownership decisions across Agent/BudgetGroup/Tool/Secret/
3
- * Webhook/Task (and anything else that gains a nullable owner column
4
- * later). A null owner means "public": readable AND mutable by anyone —
5
- * there is no gatekeeper, so no principal is turned away. `isOwner` stays
2
+ * Ownership decisions for the resource types that don't have grants yet:
3
+ * BudgetGroup/Tool/Secret/Datastore/Webhook/Task. A null owner still means
4
+ * "public" here: readable AND mutable by anyone. Tool and budget-group
5
+ * grants replace that in the resource-sharing grants Phases 2 and 3.
6
+ *
7
+ * Agents are NOT decided here any more: they use explicit grants (owner, or
8
+ * read < execute < write), and an owner-less agent is reachable only
9
+ * through its grants -- see ./access.ts and core/grants.ts. `isOwner` stays
6
10
  * available (strict, no null-passthrough) for checks that must stay
7
- * owner-only regardless of publicness — e.g. a future claim-on-edit
8
- * feature would use it to tell "already owned" apart from "public" before
9
- * deciding whether to stamp a new owner. Every tool file routes through
10
- * this module instead of re-deriving the null-handling itself, so changing
11
- * the policy again later is a change here, not an N-file sweep.
11
+ * owner-only regardless of publicness.
12
12
  */
13
13
  import type { PrismaClient, Tool } from "#prisma";
14
14
  export declare function canRead(ownerId: string | null, principalId: string): boolean;
@@ -24,74 +24,48 @@ export declare function visibleToPrincipal(principalId: string): {
24
24
  ownerId: null;
25
25
  })[];
26
26
  };
27
- export declare function requireOwnedAgent(db: PrismaClient, id: string, principalId: string): Promise<{
28
- name: string;
29
- model: string;
30
- budgetUsd: import("@prisma/client-runtime-utils").Decimal;
31
- id: string;
32
- systemPrompt: string;
33
- createdAt: Date;
34
- updatedAt: Date;
35
- kind: import("#prisma").AgentKind;
36
- schedule: string | null;
37
- timezone: string;
38
- scheduleEnabled: boolean;
39
- lastScheduledAt: Date | null;
40
- maxTurns: number;
41
- ownerId: string | null;
42
- budgetGroupId: string | null;
43
- memoryEnabled: boolean;
44
- }>;
45
- export declare function requireReadableAgent(db: PrismaClient, id: string, principalId: string): Promise<{
46
- name: string;
47
- model: string;
48
- budgetUsd: import("@prisma/client-runtime-utils").Decimal;
49
- id: string;
50
- systemPrompt: string;
51
- createdAt: Date;
52
- updatedAt: Date;
53
- kind: import("#prisma").AgentKind;
54
- schedule: string | null;
55
- timezone: string;
56
- scheduleEnabled: boolean;
57
- lastScheduledAt: Date | null;
58
- maxTurns: number;
59
- ownerId: string | null;
60
- budgetGroupId: string | null;
61
- memoryEnabled: boolean;
62
- }>;
63
27
  export declare function requireOwnedBudgetGroup(db: PrismaClient, id: string, principalId: string): Promise<{
64
28
  name: string;
65
29
  id: string;
66
- createdAt: Date;
67
- updatedAt: Date;
68
30
  ownerId: string | null;
69
31
  dailyBudgetUsd: import("@prisma/client-runtime-utils").Decimal | null;
70
32
  weeklyBudgetUsd: import("@prisma/client-runtime-utils").Decimal | null;
71
33
  monthlyBudgetUsd: import("@prisma/client-runtime-utils").Decimal | null;
72
34
  warnThresholdRatio: import("@prisma/client-runtime-utils").Decimal;
35
+ createdAt: Date;
36
+ updatedAt: Date;
73
37
  }>;
74
- export declare function requireReadableBudgetGroup(db: PrismaClient, id: string, principalId: string): Promise<{
38
+ export declare function requireReadableBudgetGroup(db: Pick<PrismaClient, "budgetGroup">, id: string, principalId: string): Promise<{
75
39
  name: string;
76
40
  id: string;
77
- createdAt: Date;
78
- updatedAt: Date;
79
41
  ownerId: string | null;
80
42
  dailyBudgetUsd: import("@prisma/client-runtime-utils").Decimal | null;
81
43
  weeklyBudgetUsd: import("@prisma/client-runtime-utils").Decimal | null;
82
44
  monthlyBudgetUsd: import("@prisma/client-runtime-utils").Decimal | null;
83
45
  warnThresholdRatio: import("@prisma/client-runtime-utils").Decimal;
46
+ createdAt: Date;
47
+ updatedAt: Date;
84
48
  }>;
85
49
  export declare function requireOwnedSecret(db: PrismaClient, id: string, principalId: string): Promise<void>;
86
50
  export declare function requireOwnedWebhook(db: PrismaClient, id: string, principalId: string): Promise<void>;
87
51
  export declare function requireOwnedDatastore(db: PrismaClient, id: string, principalId: string): Promise<{
88
52
  name: string;
89
53
  id: string;
54
+ ownerId: string | null;
90
55
  createdAt: Date;
91
56
  updatedAt: Date;
92
- ownerId: string | null;
93
57
  }>;
94
58
  export declare function requireOwnedTool(db: Pick<PrismaClient, "tool">, id: string, principalId: string): Promise<Tool>;
59
+ /**
60
+ * Owner-only, with no null-is-public passthrough: for changing or removing
61
+ * a tool's code (update_tool/delete_tool), where requireOwnedTool's rule
62
+ * would let anyone rewrite a public tool. A public tool is readable but
63
+ * immutable over MCP (403) -- only an operator can change one, via the CLI.
64
+ * A tool the caller can't read is 404, the same as a missing one.
65
+ */
66
+ export declare function requireStrictlyOwnedTool(db: Pick<PrismaClient, "tool">, id: string, principalId: string): Promise<Tool>;
67
+ /** requireStrictlyOwnedTool's rule for a row already read (e.g. inside core/tool-admin.ts's transaction). */
68
+ export declare function assertStrictlyOwnedTool(tool: Tool | null, id: string, principalId: string): Tool;
95
69
  /**
96
70
  * A Task's principalId is the caller who triggered it, not the underlying
97
71
  * agent's owner — a public agent's runs are still private to whoever
@@ -100,12 +74,12 @@ export declare function requireOwnedTool(db: Pick<PrismaClient, "tool">, id: str
100
74
  export declare function requireOwnedTask(db: PrismaClient, taskId: string, principalId: string): Promise<{
101
75
  status: import("#prisma").TaskStatus;
102
76
  runId: string | null;
77
+ kind: import("#prisma").TaskKind;
103
78
  error: import("@prisma/client/runtime/client").JsonValue | null;
104
79
  result: import("@prisma/client/runtime/client").JsonValue | null;
105
80
  id: string;
106
81
  createdAt: Date;
107
82
  updatedAt: Date;
108
- kind: import("#prisma").TaskKind;
109
83
  principalId: string | null;
110
84
  inputRequests: import("@prisma/client/runtime/client").JsonValue | null;
111
85
  inputResponses: import("@prisma/client/runtime/client").JsonValue | null;
@@ -17,20 +17,6 @@ export function assertCanMutate(ownerId, principalId, notFoundMessage) {
17
17
  export function visibleToPrincipal(principalId) {
18
18
  return { OR: [{ ownerId: principalId }, { ownerId: null }] };
19
19
  }
20
- export async function requireOwnedAgent(db, id, principalId) {
21
- const agent = await db.agent.findUnique({ where: { id } });
22
- if (!agent)
23
- throw new McpError(404, `Agent "${id}" not found.`);
24
- assertCanMutate(agent.ownerId, principalId, `Agent "${id}" is not owned by the caller.`);
25
- return agent;
26
- }
27
- export async function requireReadableAgent(db, id, principalId) {
28
- const agent = await db.agent.findUnique({ where: { id } });
29
- if (!agent || !canRead(agent.ownerId, principalId)) {
30
- throw new McpError(404, `Agent "${id}" not found.`);
31
- }
32
- return agent;
33
- }
34
20
  export async function requireOwnedBudgetGroup(db, id, principalId) {
35
21
  const group = await db.budgetGroup.findUnique({ where: { id } });
36
22
  if (!group)
@@ -71,6 +57,25 @@ export async function requireOwnedTool(db, id, principalId) {
71
57
  assertCanMutate(tool.ownerId, principalId, `Tool "${id}" is not owned by the caller.`);
72
58
  return tool;
73
59
  }
60
+ /**
61
+ * Owner-only, with no null-is-public passthrough: for changing or removing
62
+ * a tool's code (update_tool/delete_tool), where requireOwnedTool's rule
63
+ * would let anyone rewrite a public tool. A public tool is readable but
64
+ * immutable over MCP (403) -- only an operator can change one, via the CLI.
65
+ * A tool the caller can't read is 404, the same as a missing one.
66
+ */
67
+ export async function requireStrictlyOwnedTool(db, id, principalId) {
68
+ return assertStrictlyOwnedTool(await db.tool.findUnique({ where: { id } }), id, principalId);
69
+ }
70
+ /** requireStrictlyOwnedTool's rule for a row already read (e.g. inside core/tool-admin.ts's transaction). */
71
+ export function assertStrictlyOwnedTool(tool, id, principalId) {
72
+ if (!tool || !canRead(tool.ownerId, principalId))
73
+ throw new McpError(404, `Tool "${id}" not found.`);
74
+ if (!isOwner(tool.ownerId, principalId)) {
75
+ throw new McpError(403, `Tool "${id}" is public; public tools can't be changed or deleted over MCP.`);
76
+ }
77
+ return tool;
78
+ }
74
79
  /**
75
80
  * A Task's principalId is the caller who triggered it, not the underlying
76
81
  * agent's owner — a public agent's runs are still private to whoever
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Set-time repository authorization for the MCP tools that give an agent
3
+ * authority over a repository (link_repository; create_agent/update_agent's
4
+ * codingProfile.repository). Returns the stamp to store on the granting row,
5
+ * or throws an actionable McpError. See core/repo-access.ts for the use-time
6
+ * side and the rules; the host call happens here, before any transaction.
7
+ */
8
+ import { type AuthorizedVia, type RepoAccessKind } from "../../core/repo-access.js";
9
+ import type { McpRequestContext } from "../context.js";
10
+ export interface RepositoryAuthorization {
11
+ authorizedVia: AuthorizedVia;
12
+ authorizedById: string;
13
+ authorizedAt: Date;
14
+ }
15
+ export declare function authorizeRepositoryForSet(ctx: McpRequestContext, input: {
16
+ /** The agent's owner. The caller is always the owner here (or creating the agent). */
17
+ ownerId: string | null;
18
+ provider: string;
19
+ repository: string;
20
+ kind: RepoAccessKind;
21
+ adminOverride?: boolean;
22
+ }): Promise<RepositoryAuthorization>;
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Set-time repository authorization for the MCP tools that give an agent
3
+ * authority over a repository (link_repository; create_agent/update_agent's
4
+ * codingProfile.repository). Returns the stamp to store on the granting row,
5
+ * or throws an actionable McpError. See core/repo-access.ts for the use-time
6
+ * side and the rules; the host call happens here, before any transaction.
7
+ */
8
+ import { requiredLevel } from "../../core/repo-access.js";
9
+ import { McpError } from "../errors.js";
10
+ import { requireScope } from "./resource-server.js";
11
+ export async function authorizeRepositoryForSet(ctx, input) {
12
+ if (!input.ownerId) {
13
+ throw new McpError(400, "owner_required: agents without an owner can't be given a repository, because an owner-less agent has no owner " +
14
+ "whose GitHub access can be checked. An admin can assign an owner with make_owner first.");
15
+ }
16
+ const stamp = (authorizedVia) => ({
17
+ authorizedVia,
18
+ authorizedById: ctx.principal.id,
19
+ authorizedAt: new Date(),
20
+ });
21
+ if (input.adminOverride) {
22
+ // Role-gated: the scope alone never suffices (resource-server.ts).
23
+ requireScope(ctx, ctx.canonicalUri, "agents:admin");
24
+ return stamp("admin");
25
+ }
26
+ const gate = ctx.providers.repoAccess;
27
+ if (!gate) {
28
+ throw new McpError(503, "Repository access can't be checked on this deployment (no GitHub App is configured). " +
29
+ "An admin can approve it explicitly with adminOverride.");
30
+ }
31
+ const required = requiredLevel(input.kind);
32
+ const decision = await gate.authorizePrincipal({
33
+ principalId: input.ownerId,
34
+ provider: input.provider,
35
+ repository: input.repository,
36
+ required,
37
+ fresh: true,
38
+ });
39
+ if (decision.ok)
40
+ return stamp("host_permission");
41
+ switch (decision.reason) {
42
+ case "identity_not_linked":
43
+ throw new McpError(403, "Link your GitHub account first (link_host_account): wardby checks that your own GitHub account can " +
44
+ `access ${input.repository} before an agent you own may use it.`);
45
+ case "insufficient_permission":
46
+ throw new McpError(403, `Your linked GitHub account has ${decision.level ?? "no"} access to ${input.repository}; ` +
47
+ `this needs ${required}. Ask a repository admin for access, or a wardby admin to approve it (adminOverride).`);
48
+ default:
49
+ throw new McpError(503, `Could not verify your GitHub access to ${input.repository} right now (is the GitHub App installed on it?). Try again.`);
50
+ }
51
+ }
@@ -6,10 +6,33 @@
6
6
  */
7
7
  import type { PrismaClient } from "#prisma";
8
8
  import type { AuthProvider } from "../../providers/auth/types.js";
9
- import { McpError } from "../errors.js";
9
+ import { McpError, insufficientScope } from "../errors.js";
10
10
  import type { McpRequestContext, McpProviders } from "../context.js";
11
11
  /** The initial capability tiers (§Scopes → capability tiers in the design doc). */
12
12
  export declare const SCOPES_SUPPORTED: string[];
13
+ /**
14
+ * Scopes whose operations reach beyond the caller's own (or public)
15
+ * resources — they double as the permission names roles grant:
16
+ * agents:admin reassigns ANY agent's owner (make_owner) and sets a BYO
17
+ * workerImageRef; packages:approve widens coding agents' package allowlists;
18
+ * services:manage changes the coding-run service catalog, which decides what
19
+ * runs next to every coding run that names an entry.
20
+ * A token scope only DELEGATES — it never authorizes on its own:
21
+ * requireScope/requireAnyScope honour one only when one of the caller's
22
+ * roles (McpRequestContext.roles, resolved live per request) grants it.
23
+ */
24
+ export declare const PRIVILEGED_SCOPES: readonly string[];
25
+ /**
26
+ * The built-in roles and the permissions (privileged scope names) each
27
+ * grants. No roles = member: every non-privileged scope, nothing privileged.
28
+ */
29
+ export declare const ROLE_PERMISSIONS: Readonly<Record<string, readonly string[]>>;
30
+ export declare const ROLE_NAMES: readonly string[];
31
+ export declare function isRoleName(name: string): boolean;
32
+ /** The permissions granted by `roles`; unknown role names grant nothing. */
33
+ export declare function permissionsOf(roles: readonly string[] | null | undefined): Set<string>;
34
+ /** Intersects a space-delimited scope string with `allowed`, deduplicated, keeping request order. */
35
+ export declare function limitScope(scope: string, allowed: readonly string[]): string;
13
36
  export interface ProtectedResourceMetadata {
14
37
  resource: string;
15
38
  authorization_servers: string[];
@@ -44,4 +67,10 @@ export declare function authenticate(headers: AuthenticateHeaders, deps: Authent
44
67
  * per missing scope.
45
68
  */
46
69
  export declare function requireScope(ctx: McpRequestContext, canonicalUri: string, ...scopes: string[]): void;
47
- export { McpError };
70
+ /**
71
+ * Like requireScope, but any ONE of `alternatives` suffices (e.g. package
72
+ * approval: packages:approve or agents:admin). The first alternative is the
73
+ * one named in the insufficient_scope challenge.
74
+ */
75
+ export declare function requireAnyScope(ctx: McpRequestContext, canonicalUri: string, ...alternatives: string[]): void;
76
+ export { McpError, insufficientScope };
@@ -10,11 +10,51 @@ export const SCOPES_SUPPORTED = [
10
10
  "secrets:write",
11
11
  "webhooks:write",
12
12
  "budget_groups:write",
13
+ // Approve coding agents' package allowlists (see docs/coding-packages.md).
14
+ "packages:approve",
15
+ // Create, update and delete coding-run service catalog entries (see docs/coding-services.md).
16
+ // Reading the catalog is agents:read.
17
+ "services:manage",
13
18
  // Reassigns an agent's owner regardless of who currently owns it (or
14
19
  // whether it's public) — a step above agents:write, which only ever lets
15
20
  // a caller act on agents they already own or that are unowned.
16
21
  "agents:admin",
22
+ // set_agent_memory / delete_agent_memory (reading memory is agents:read).
23
+ "memory:write",
17
24
  ];
25
+ /**
26
+ * Scopes whose operations reach beyond the caller's own (or public)
27
+ * resources — they double as the permission names roles grant:
28
+ * agents:admin reassigns ANY agent's owner (make_owner) and sets a BYO
29
+ * workerImageRef; packages:approve widens coding agents' package allowlists;
30
+ * services:manage changes the coding-run service catalog, which decides what
31
+ * runs next to every coding run that names an entry.
32
+ * A token scope only DELEGATES — it never authorizes on its own:
33
+ * requireScope/requireAnyScope honour one only when one of the caller's
34
+ * roles (McpRequestContext.roles, resolved live per request) grants it.
35
+ */
36
+ export const PRIVILEGED_SCOPES = ["agents:admin", "packages:approve", "services:manage"];
37
+ /**
38
+ * The built-in roles and the permissions (privileged scope names) each
39
+ * grants. No roles = member: every non-privileged scope, nothing privileged.
40
+ */
41
+ export const ROLE_PERMISSIONS = {
42
+ admin: ["agents:admin", "packages:approve", "services:manage"],
43
+ "package-approver": ["packages:approve"],
44
+ "service-manager": ["services:manage"],
45
+ };
46
+ export const ROLE_NAMES = Object.keys(ROLE_PERMISSIONS);
47
+ export function isRoleName(name) {
48
+ return Object.prototype.hasOwnProperty.call(ROLE_PERMISSIONS, name);
49
+ }
50
+ /** The permissions granted by `roles`; unknown role names grant nothing. */
51
+ export function permissionsOf(roles) {
52
+ return new Set((roles ?? []).filter(isRoleName).flatMap((r) => ROLE_PERMISSIONS[r]));
53
+ }
54
+ /** Intersects a space-delimited scope string with `allowed`, deduplicated, keeping request order. */
55
+ export function limitScope(scope, allowed) {
56
+ return [...new Set(scope.split(/\s+/).filter((s) => allowed.includes(s)))].join(" ");
57
+ }
18
58
  /** RFC 9728 §3.1: insert `/.well-known/oauth-protected-resource` before the resource's path. */
19
59
  export function protectedResourceMetadataUrl(canonicalUri) {
20
60
  const url = new URL(canonicalUri);
@@ -55,6 +95,9 @@ export async function authenticate(headers, deps) {
55
95
  return {
56
96
  principal,
57
97
  scopes: new Set(verified.scopes),
98
+ // Decided by the provider on THIS request (self-hosted: AuthUser.roles
99
+ // read from the database; delegating: the configured signed role claim).
100
+ roles: (verified.wardbyRoles ?? []).filter(isRoleName),
58
101
  canonicalUri: deps.canonicalUri,
59
102
  providers: deps.providers,
60
103
  db: deps.db,
@@ -75,8 +118,46 @@ export async function authenticate(headers, deps) {
75
118
  */
76
119
  export function requireScope(ctx, canonicalUri, ...scopes) {
77
120
  const held = scopes.every((s) => ctx.scopes.has(s));
78
- if (held)
121
+ if (!held)
122
+ throw insufficientScope(scopes, protectedResourceMetadataUrl(canonicalUri));
123
+ requirePermission(ctx, scopes);
124
+ }
125
+ /**
126
+ * Like requireScope, but any ONE of `alternatives` suffices (e.g. package
127
+ * approval: packages:approve or agents:admin). The first alternative is the
128
+ * one named in the insufficient_scope challenge.
129
+ */
130
+ export function requireAnyScope(ctx, canonicalUri, ...alternatives) {
131
+ const held = alternatives.filter((s) => ctx.scopes.has(s));
132
+ if (held.length === 0)
133
+ throw insufficientScope(alternatives.slice(0, 1), protectedResourceMetadataUrl(canonicalUri));
134
+ // One held alternative that is non-privileged, or that a role permits, suffices.
135
+ const permitted = permissionsOf(ctx.roles);
136
+ if (held.some((s) => !PRIVILEGED_SCOPES.includes(s) || permitted.has(s)))
79
137
  return;
80
- throw insufficientScope(scopes, protectedResourceMetadataUrl(canonicalUri));
138
+ // A role DOES grant one of the unheld alternatives: re-authorizing for that
139
+ // scope is the fix, so say so with a scope challenge, not a role 403.
140
+ const reachable = alternatives.find((s) => !ctx.scopes.has(s) && permitted.has(s));
141
+ if (reachable)
142
+ throw insufficientScope([reachable], protectedResourceMetadataUrl(canonicalUri));
143
+ throw forbidden(held);
144
+ }
145
+ /**
146
+ * The one role check: a privileged scope is honoured only when one of the
147
+ * caller's roles grants it as a permission. This is authorization, not
148
+ * delegation, so the 403 carries no scope challenge — a client re-authorizing
149
+ * for more scopes can't fix it; an operator must grant a role.
150
+ */
151
+ function requirePermission(ctx, scopes) {
152
+ const permitted = permissionsOf(ctx.roles);
153
+ const missing = scopes.filter((s) => PRIVILEGED_SCOPES.includes(s) && !permitted.has(s));
154
+ if (missing.length > 0)
155
+ throw forbidden(missing);
156
+ }
157
+ function forbidden(permissions) {
158
+ const grantedBy = ROLE_NAMES.filter((r) => permissions.some((p) => ROLE_PERMISSIONS[r].includes(p)));
159
+ return new McpError(403, `Forbidden: ${permissions.join(" ")} requires a role that grants it (${grantedBy.join(" or ")}); ` +
160
+ "the token's scope alone is not enough. An operator grants roles (self-hosted: " +
161
+ "`wardby auth user grant --subject <subject> --role <role>`; delegating: AUTH_ROLE_CLAIM/AUTH_ROLE_MAP).");
81
162
  }
82
- export { McpError };
163
+ export { McpError, insufficientScope };
@@ -197,14 +197,14 @@ export function browserHandler(provider) {
197
197
  }
198
198
  if (req.method === "GET" && url.pathname === "/consent") {
199
199
  const id = url.searchParams.get("interaction") ?? "";
200
- const { interaction, challenge } = await provider.consentPage(cookies.get(sessionName) ?? "", id);
200
+ const { interaction, challenge, scopes } = await provider.consentPage(cookies.get(sessionName) ?? "", id);
201
201
  const name = interaction.client.metadata.client_name;
202
202
  html(res, "<h1>Authorize " +
203
203
  escape(name) +
204
204
  "</h1><p>Resource: " +
205
205
  escape(interaction.resource) +
206
206
  "</p><p>Scopes: " +
207
- escape(interaction.requestedScope) +
207
+ escape(scopes.length ? scopes.join(" ") : "(none)") +
208
208
  '</p><form action="/consent" method="post">' +
209
209
  hidden("interaction", id) +
210
210
  hidden("csrf", challenge) +
@@ -1,39 +1,73 @@
1
1
  import { parseArgs } from "node:util";
2
2
  import { IdentityService } from "./credentials.js";
3
+ const USAGE = "Use auth user create --subject <s> [--role <role>]... | user list | " +
4
+ "user grant --subject <s> [--role <role>]... [--revoke-role <role>]... | user disable --subject <s> | " +
5
+ "key create|list --subject <s> | key revoke <key-id>.";
6
+ /** The flags each command accepts. Anything else is rejected, never silently ignored. */
7
+ const COMMANDS = {
8
+ "user create": ["subject", "role"],
9
+ "user list": [],
10
+ "user grant": ["subject", "role", "revoke-role"],
11
+ "user disable": ["subject"],
12
+ "key create": ["subject"],
13
+ "key list": ["subject"],
14
+ };
15
+ const FLAG_OPTIONS = {
16
+ subject: { type: "string" },
17
+ role: { type: "string", multiple: true },
18
+ "revoke-role": { type: "string", multiple: true },
19
+ };
3
20
  export async function authCommand(args, db, hashKey, output = console.log) {
4
21
  const [kind, action, ...rest] = args;
5
22
  const service = new IdentityService(db, hashKey);
6
23
  if (hashKey.toLowerCase() === process.env.SECRET_APP_KEY?.toLowerCase() ||
7
24
  hashKey.toLowerCase() === process.env.AUTH_SIGNING_KEY?.toLowerCase())
8
25
  throw new Error("Authentication keys must be independent.");
9
- if (kind === "key" && action === "revoke" && rest.length === 1) {
26
+ const command = `${kind} ${action}`;
27
+ if (command === "key revoke") {
28
+ if (rest.length !== 1 || rest[0].startsWith("-"))
29
+ throw new Error("Use auth key revoke <key-id> (no flags).");
10
30
  await service.revokeKey(rest[0]);
11
31
  output("Key, sessions, and grants revoked.");
12
32
  return;
13
33
  }
14
- const { values } = parseArgs({ args: rest, options: { subject: { type: "string" } } });
15
- if (kind === "user" && action === "list") {
34
+ const allowed = COMMANDS[command];
35
+ if (!allowed)
36
+ throw new Error(USAGE);
37
+ // strict parsing throws on any flag not declared for THIS command, so a
38
+ // flag another command takes (e.g. `key create --role admin`) is an error.
39
+ const { values } = parseArgs({
40
+ args: rest,
41
+ strict: true,
42
+ allowPositionals: false,
43
+ options: Object.fromEntries(allowed.map((flag) => [flag, FLAG_OPTIONS[flag]])),
44
+ });
45
+ if (command === "user list") {
16
46
  output(JSON.stringify(await service.listUsers(), null, 2));
17
47
  return;
18
48
  }
19
49
  if (!values.subject)
20
- throw new Error("auth requires --subject (except user list and key revoke <key-id>).");
21
- if (kind === "user" && action === "create") {
22
- output(JSON.stringify(await service.createUser(values.subject)));
50
+ throw new Error(`auth ${command} requires --subject.`);
51
+ if (command === "user create") {
52
+ output(JSON.stringify(await service.createUser(values.subject, values.role ?? [])));
23
53
  return;
24
54
  }
25
- if (kind === "user" && action === "disable") {
55
+ if (command === "user grant") {
56
+ const add = values.role ?? [];
57
+ const remove = values["revoke-role"] ?? [];
58
+ if (add.length + remove.length === 0)
59
+ throw new Error("auth user grant requires --role <role> and/or --revoke-role <role> (each repeatable).");
60
+ output(JSON.stringify(await service.changeRoles(values.subject, add, remove)));
61
+ return;
62
+ }
63
+ if (command === "user disable") {
26
64
  await service.disableUser(values.subject);
27
65
  output("User disabled; credentials, sessions, and grants revoked.");
28
66
  return;
29
67
  }
30
- if (kind === "key" && action === "create") {
68
+ if (command === "key create") {
31
69
  output(await service.createKey(values.subject));
32
70
  return;
33
71
  }
34
- if (kind === "key" && action === "list") {
35
- output(JSON.stringify(await service.listKeys(values.subject), null, 2));
36
- return;
37
- }
38
- throw new Error("Use auth user create|list|disable or auth key create|list|revoke.");
72
+ output(JSON.stringify(await service.listKeys(values.subject), null, 2));
39
73
  }
@@ -1,4 +1,6 @@
1
1
  import type { Prisma, PrismaClient } from "#prisma";
2
+ /** Rejects unknown role names loudly; returns the deduplicated list. */
3
+ export declare function validateRoles(roles: readonly string[]): string[];
2
4
  export type AuthDb = Prisma.TransactionClient;
3
5
  export declare const DAY = 86400000;
4
6
  export declare function decodeKey(value: string, name: string): Buffer;
@@ -26,19 +28,34 @@ export declare function lockUser(tx: AuthDb, id: string): Promise<{
26
28
  createdAt: Date;
27
29
  updatedAt: Date;
28
30
  principalId: string;
31
+ roles: string[];
29
32
  displayName: string | null;
30
33
  }>;
31
34
  export declare class IdentityService {
32
35
  readonly db: PrismaClient;
33
36
  readonly credentials: Credentials;
34
37
  constructor(db: PrismaClient, hashKey: string);
35
- createUser(subject: string): Promise<{
38
+ createUser(subject: string, roles?: readonly string[]): Promise<{
36
39
  userId: string;
37
40
  subject: string;
41
+ roles: string[];
38
42
  loginKey: string;
39
43
  }>;
40
44
  createKey(subject: string): Promise<string>;
41
45
  disableUser(subject: string): Promise<void>;
46
+ /**
47
+ * Adds and/or removes roles on an existing user (never creates one). Any
48
+ * actual change signs the user out in the same transaction: their OAuth
49
+ * grant families, refresh grants, browser sessions and unexchanged
50
+ * authorization codes are revoked, and they sign in and authorize afresh.
51
+ * (Roles are also read live on every request, so even a direct database
52
+ * edit applies to live tokens at once.) A no-op change revokes nothing.
53
+ */
54
+ changeRoles(subject: string, add: readonly string[], remove: readonly string[]): Promise<{
55
+ subject: string;
56
+ roles: string[];
57
+ revokedGrants: boolean;
58
+ }>;
42
59
  revokeKey(keyId: string): Promise<void>;
43
60
  listUsers(): Prisma.PrismaPromise<{
44
61
  status: string;
@@ -46,13 +63,14 @@ export declare class IdentityService {
46
63
  subject: string;
47
64
  };
48
65
  id: string;
66
+ roles: string[];
49
67
  displayName: string | null;
50
68
  }[]>;
51
69
  listKeys(subject: string): Prisma.PrismaPromise<{
52
70
  createdAt: Date;
53
71
  keyId: string;
54
- lastUsedAt: Date | null;
55
72
  expiresAt: Date;
73
+ lastUsedAt: Date | null;
56
74
  revokedAt: Date | null;
57
75
  }[]>;
58
76
  }