@wardby/cli 0.7.0 → 0.9.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 (367) hide show
  1. package/.env.example +5 -5
  2. package/README.md +6 -1
  3. package/deploy/local/docker-compose.quickstart-coding.yml +11 -0
  4. package/dist/claude-coding-worker/driver.d.ts +12 -0
  5. package/dist/claude-coding-worker/driver.js +29 -7
  6. package/dist/claude-coding-worker/main.js +5 -1
  7. package/dist/claude-coding-worker/sdk.js +17 -1
  8. package/dist/claude-plugin/command.d.ts +64 -0
  9. package/dist/claude-plugin/command.js +379 -0
  10. package/dist/cli-help.d.ts +1 -1
  11. package/dist/cli-help.js +2 -1
  12. package/dist/cli.js +71 -3
  13. package/dist/coding/code-host.d.ts +16 -0
  14. package/dist/coding/code-host.js +60 -0
  15. package/dist/coding/mcp-token.d.ts +12 -0
  16. package/dist/coding/mcp-token.js +26 -0
  17. package/dist/coding/protocol.js +2 -0
  18. package/dist/coding/redact.js +6 -1
  19. package/dist/coding/transcript/recorder.d.ts +5 -4
  20. package/dist/coding/transcript/recorder.js +3 -9
  21. package/dist/coding-worker/debug-trace.d.ts +3 -1
  22. package/dist/coding-worker/debug-trace.js +4 -2
  23. package/dist/coding-worker/driver.js +8 -1
  24. package/dist/coding-worker/main.js +5 -1
  25. package/dist/coding-worker/mcp-config.d.ts +27 -0
  26. package/dist/coding-worker/mcp-config.js +32 -0
  27. package/dist/coding-worker/sdk.d.ts +36 -0
  28. package/dist/coding-worker/sdk.js +28 -1
  29. package/dist/coding-worker/types.d.ts +4 -0
  30. package/dist/config/mcp-call-limit.d.ts +8 -0
  31. package/dist/config/mcp-call-limit.js +18 -0
  32. package/dist/config/mcp-servers.d.ts +8 -0
  33. package/dist/config/mcp-servers.js +12 -0
  34. package/dist/config/test-simulators.d.ts +10 -0
  35. package/dist/config/test-simulators.js +28 -0
  36. package/dist/config/tool-timeout.d.ts +1 -0
  37. package/dist/config/tool-timeout.js +18 -0
  38. package/dist/core/coding-mcp.d.ts +36 -0
  39. package/dist/core/coding-mcp.js +253 -0
  40. package/dist/core/coding-repository.d.ts +46 -0
  41. package/dist/core/coding-repository.js +92 -0
  42. package/dist/core/delegation-slots.d.ts +27 -0
  43. package/dist/core/delegation-slots.js +44 -0
  44. package/dist/core/dispatch.d.ts +6 -2
  45. package/dist/core/dispatch.js +39 -11
  46. package/dist/core/engine-native.d.ts +2 -0
  47. package/dist/core/engine-native.js +13 -0
  48. package/dist/core/error-code.js +2 -0
  49. package/dist/core/grants.d.ts +28 -3
  50. package/dist/core/grants.js +46 -4
  51. package/dist/core/host-events.js +7 -3
  52. package/dist/core/host-status.js +2 -2
  53. package/dist/core/issue-status.js +4 -3
  54. package/dist/core/listen-address.d.ts +10 -0
  55. package/dist/core/listen-address.js +14 -0
  56. package/dist/core/mark-ready.d.ts +1 -1
  57. package/dist/core/mark-ready.js +1 -1
  58. package/dist/core/mcp-client/client.d.ts +47 -0
  59. package/dist/core/mcp-client/client.js +141 -0
  60. package/dist/core/mcp-client/jsonrpc.d.ts +15 -0
  61. package/dist/core/mcp-client/jsonrpc.js +54 -0
  62. package/dist/core/mcp-client/results.d.ts +13 -0
  63. package/dist/core/mcp-client/results.js +42 -0
  64. package/dist/core/mcp-servers.d.ts +90 -0
  65. package/dist/core/mcp-servers.js +230 -0
  66. package/dist/core/mcp-tools.d.ts +108 -0
  67. package/dist/core/mcp-tools.js +287 -0
  68. package/dist/core/merge-order-check.js +38 -19
  69. package/dist/core/notification-dispatcher.js +2 -1
  70. package/dist/core/power-idle.d.ts +8 -2
  71. package/dist/core/power-idle.js +17 -3
  72. package/dist/core/power.d.ts +5 -0
  73. package/dist/core/power.js +5 -0
  74. package/dist/core/related-pull-requests.js +26 -10
  75. package/dist/core/retention-days.d.ts +5 -0
  76. package/dist/core/retention-days.js +14 -0
  77. package/dist/core/review-fix.js +2 -2
  78. package/dist/core/review-host-tools.js +3 -1
  79. package/dist/core/run-steps/config.d.ts +25 -0
  80. package/dist/core/run-steps/config.js +40 -0
  81. package/dist/core/run-steps/control-chars.d.ts +16 -0
  82. package/dist/core/run-steps/control-chars.js +26 -0
  83. package/dist/core/run-steps/detail.d.ts +32 -0
  84. package/dist/core/run-steps/detail.js +240 -0
  85. package/dist/core/run-steps/extract.d.ts +52 -0
  86. package/dist/core/run-steps/extract.js +467 -0
  87. package/dist/core/run-steps/ingress.d.ts +51 -0
  88. package/dist/core/run-steps/ingress.js +160 -0
  89. package/dist/core/run-steps/last-step.d.ts +15 -0
  90. package/dist/core/run-steps/last-step.js +35 -0
  91. package/dist/core/run-steps/native.d.ts +48 -0
  92. package/dist/core/run-steps/native.js +164 -0
  93. package/dist/core/run-steps/seen.d.ts +16 -0
  94. package/dist/core/run-steps/seen.js +35 -0
  95. package/dist/core/run-steps/summarize.d.ts +57 -0
  96. package/dist/core/run-steps/summarize.js +304 -0
  97. package/dist/core/run-steps/sweep.d.ts +21 -0
  98. package/dist/core/run-steps/sweep.js +41 -0
  99. package/dist/core/run-steps/types.d.ts +89 -0
  100. package/dist/core/run-steps/types.js +39 -0
  101. package/dist/core/run-steps/wire.d.ts +35 -0
  102. package/dist/core/run-steps/wire.js +47 -0
  103. package/dist/core/run-steps/writer.d.ts +49 -0
  104. package/dist/core/run-steps/writer.js +279 -0
  105. package/dist/core/runner.d.ts +70 -16
  106. package/dist/core/runner.js +296 -74
  107. package/dist/core/secret-broker-config.d.ts +2 -0
  108. package/dist/core/secret-broker-config.js +1 -1
  109. package/dist/core/secret-guard.d.ts +5 -2
  110. package/dist/core/secret-guard.js +22 -7
  111. package/dist/core/secrets.d.ts +11 -1
  112. package/dist/core/secrets.js +27 -1
  113. package/dist/core/serializable-retry.d.ts +18 -0
  114. package/dist/core/serializable-retry.js +43 -0
  115. package/dist/core/tool-admin.d.ts +5 -1
  116. package/dist/core/tool-admin.js +7 -2
  117. package/dist/core/tool-names.d.ts +13 -0
  118. package/dist/core/tool-names.js +20 -0
  119. package/dist/core/webhooks.d.ts +4 -0
  120. package/dist/core/webhooks.js +68 -49
  121. package/dist/core/workflow-run-events.js +2 -1
  122. package/dist/generated/prisma/browser.d.ts +39 -0
  123. package/dist/generated/prisma/client.d.ts +39 -0
  124. package/dist/generated/prisma/commonInputTypes.d.ts +80 -50
  125. package/dist/generated/prisma/enums.d.ts +6 -0
  126. package/dist/generated/prisma/enums.js +5 -0
  127. package/dist/generated/prisma/internal/class.d.ts +77 -0
  128. package/dist/generated/prisma/internal/class.js +4 -4
  129. package/dist/generated/prisma/internal/prismaNamespace.d.ts +632 -1
  130. package/dist/generated/prisma/internal/prismaNamespace.js +94 -3
  131. package/dist/generated/prisma/internal/prismaNamespaceBrowser.d.ts +98 -0
  132. package/dist/generated/prisma/internal/prismaNamespaceBrowser.js +94 -3
  133. package/dist/generated/prisma/models/Agent.d.ts +372 -1
  134. package/dist/generated/prisma/models/AgentMcpServer.d.ts +1412 -0
  135. package/dist/generated/prisma/models/AgentMcpServer.js +1 -0
  136. package/dist/generated/prisma/models/AgentRepository.d.ts +40 -1
  137. package/dist/generated/prisma/models/AuthUser.d.ts +1 -1
  138. package/dist/generated/prisma/models/CodingAgentProfile.d.ts +0 -7
  139. package/dist/generated/prisma/models/CodingProxySession.d.ts +159 -2
  140. package/dist/generated/prisma/models/CodingRun.d.ts +76 -1
  141. package/dist/generated/prisma/models/HostProvider.d.ts +2176 -0
  142. package/dist/generated/prisma/models/HostProvider.js +1 -0
  143. package/dist/generated/prisma/models/HostProviderCredential.d.ts +1285 -0
  144. package/dist/generated/prisma/models/HostProviderCredential.js +1 -0
  145. package/dist/generated/prisma/models/HostProviderOperation.d.ts +1470 -0
  146. package/dist/generated/prisma/models/HostProviderOperation.js +1 -0
  147. package/dist/generated/prisma/models/HostProviderTrustedAccount.d.ts +1145 -0
  148. package/dist/generated/prisma/models/HostProviderTrustedAccount.js +1 -0
  149. package/dist/generated/prisma/models/McpServer.d.ts +1500 -0
  150. package/dist/generated/prisma/models/McpServer.js +1 -0
  151. package/dist/generated/prisma/models/NativeGatewayCall.d.ts +39 -1
  152. package/dist/generated/prisma/models/Principal.d.ts +167 -0
  153. package/dist/generated/prisma/models/ResourceGrant.d.ts +1 -1
  154. package/dist/generated/prisma/models/Run.d.ts +503 -1
  155. package/dist/generated/prisma/models/RunStep.d.ts +1453 -0
  156. package/dist/generated/prisma/models/RunStep.js +1 -0
  157. package/dist/generated/prisma/models/Secret.d.ts +387 -0
  158. package/dist/generated/prisma/models/Tool.d.ts +223 -1
  159. package/dist/generated/prisma/models.d.ts +7 -0
  160. package/dist/help/search.js +12 -3
  161. package/dist/help-index.json +812 -52
  162. package/dist/import/create.js +16 -1
  163. package/dist/import/neutral-schema.d.ts +1 -0
  164. package/dist/import/neutral-schema.js +2 -0
  165. package/dist/mcp/auth/access.d.ts +1 -0
  166. package/dist/mcp/auth/grants-cli.d.ts +6 -4
  167. package/dist/mcp/auth/grants-cli.js +42 -3
  168. package/dist/mcp/auth/resource-server.d.ts +3 -1
  169. package/dist/mcp/auth/resource-server.js +17 -2
  170. package/dist/mcp/context.d.ts +6 -0
  171. package/dist/mcp/index.js +21 -0
  172. package/dist/mcp/tools/agents.js +135 -20
  173. package/dist/mcp/tools/grants.js +66 -28
  174. package/dist/mcp/tools/host-providers.d.ts +2 -0
  175. package/dist/mcp/tools/host-providers.js +215 -0
  176. package/dist/mcp/tools/mcp-servers.d.ts +26 -0
  177. package/dist/mcp/tools/mcp-servers.js +481 -0
  178. package/dist/mcp/tools/power.js +2 -2
  179. package/dist/mcp/tools/repositories.js +50 -10
  180. package/dist/mcp/tools/run-access.d.ts +7 -0
  181. package/dist/mcp/tools/run-access.js +14 -0
  182. package/dist/mcp/tools/run-steps.d.ts +16 -0
  183. package/dist/mcp/tools/run-steps.js +85 -0
  184. package/dist/mcp/tools/runs.js +186 -18
  185. package/dist/mcp/tools/secret-elicitation.d.ts +2 -0
  186. package/dist/mcp/tools/secret-elicitation.js +25 -2
  187. package/dist/mcp/tools/secrets.js +38 -4
  188. package/dist/mcp/tools/tools.d.ts +6 -4
  189. package/dist/mcp/tools/tools.js +40 -8
  190. package/dist/mcp/tools/trigger.js +28 -6
  191. package/dist/mcp/transport/http-limits.d.ts +2 -0
  192. package/dist/mcp/transport/http-limits.js +6 -2
  193. package/dist/mcp/webhooks/ingress.js +10 -0
  194. package/dist/native-worker/gateway-server.d.ts +18 -1
  195. package/dist/native-worker/gateway-server.js +38 -9
  196. package/dist/native-worker/gateway.d.ts +5 -0
  197. package/dist/native-worker/gateway.js +13 -1
  198. package/dist/native-worker/http-server.d.ts +6 -4
  199. package/dist/native-worker/http-server.js +53 -8
  200. package/dist/native-worker/ledger.d.ts +6 -2
  201. package/dist/native-worker/ledger.js +5 -4
  202. package/dist/native-worker/protocol.d.ts +1 -0
  203. package/dist/native-worker/protocol.js +4 -1
  204. package/dist/providers/coding-proxy/capability-hash.d.ts +2 -0
  205. package/dist/providers/coding-proxy/capability-hash.js +5 -0
  206. package/dist/providers/coding-proxy/mcp-route.d.ts +34 -0
  207. package/dist/providers/coding-proxy/mcp-route.js +166 -0
  208. package/dist/providers/coding-proxy/memory-ledger.d.ts +2 -0
  209. package/dist/providers/coding-proxy/memory-ledger.js +16 -0
  210. package/dist/providers/coding-proxy/prisma-ledger.d.ts +1 -0
  211. package/dist/providers/coding-proxy/prisma-ledger.js +36 -2
  212. package/dist/providers/coding-proxy/proxy.d.ts +33 -1
  213. package/dist/providers/coding-proxy/proxy.js +266 -75
  214. package/dist/providers/coding-proxy/run-step-forwarder.d.ts +42 -0
  215. package/dist/providers/coding-proxy/run-step-forwarder.js +206 -0
  216. package/dist/providers/coding-proxy/runtime.d.ts +13 -0
  217. package/dist/providers/coding-proxy/runtime.js +87 -4
  218. package/dist/providers/coding-proxy/server.d.ts +3 -0
  219. package/dist/providers/coding-proxy/server.js +10 -0
  220. package/dist/providers/coding-proxy/types.d.ts +18 -0
  221. package/dist/providers/engine/types.d.ts +26 -0
  222. package/dist/providers/executor/composition.js +5 -0
  223. package/dist/providers/executor/container.d.ts +47 -1
  224. package/dist/providers/executor/container.js +84 -8
  225. package/dist/providers/host-tools/invoke.d.ts +37 -0
  226. package/dist/providers/host-tools/invoke.js +117 -0
  227. package/dist/providers/host-tools/operations.d.ts +65 -0
  228. package/dist/providers/host-tools/operations.js +149 -0
  229. package/dist/providers/host-tools/registry.d.ts +79 -0
  230. package/dist/providers/host-tools/registry.js +362 -0
  231. package/dist/providers/host-tools/request-guard.d.ts +17 -0
  232. package/dist/providers/host-tools/request-guard.js +99 -0
  233. package/dist/providers/host-tools/self-test.d.ts +64 -0
  234. package/dist/providers/host-tools/self-test.js +142 -0
  235. package/dist/providers/jobs/docker-isolation.d.ts +2 -2
  236. package/dist/providers/jobs/docker-isolation.js +30 -8
  237. package/dist/providers/jobs/docker.d.ts +8 -0
  238. package/dist/providers/jobs/docker.js +33 -9
  239. package/dist/providers/jobs/kubernetes-isolation.d.ts +3 -1
  240. package/dist/providers/jobs/kubernetes-isolation.js +15 -2
  241. package/dist/providers/jobs/kubernetes.d.ts +13 -0
  242. package/dist/providers/jobs/kubernetes.js +28 -5
  243. package/dist/providers/jobs/types.d.ts +6 -0
  244. package/dist/providers/review-host/types.d.ts +6 -4
  245. package/dist/providers/review-host/types.js +1 -1
  246. package/dist/providers/vcs/git.d.ts +6 -2
  247. package/dist/providers/vcs/git.js +15 -4
  248. package/dist/providers/vcs/github.d.ts +2 -71
  249. package/dist/providers/vcs/github.js +8 -11
  250. package/dist/providers/vcs/index.d.ts +1 -1
  251. package/dist/providers/vcs/index.js +1 -1
  252. package/dist/providers/vcs/remote.d.ts +4 -0
  253. package/dist/providers/vcs/remote.js +7 -1
  254. package/dist/providers/vcs/routing.d.ts +12 -6
  255. package/dist/providers/vcs/routing.js +21 -6
  256. package/dist/providers/vcs/types.d.ts +71 -3
  257. package/dist/quickstart/index.d.ts +5 -0
  258. package/dist/quickstart/index.js +21 -0
  259. package/dist/quickstart-images.json +1 -1
  260. package/dist/sandbox/eval-core.d.ts +12 -2
  261. package/dist/sandbox/eval-core.js +20 -4
  262. package/dist/sandbox/host-functions.d.ts +4 -0
  263. package/dist/sandbox/host-functions.js +7 -2
  264. package/dist/sandbox/limits.d.ts +4 -1
  265. package/dist/sandbox/limits.js +4 -1
  266. package/dist/sandbox/run-in-sandbox.d.ts +6 -0
  267. package/dist/sandbox/run-in-sandbox.js +14 -2
  268. package/dist/sandbox/safe-fetch.d.ts +2 -0
  269. package/dist/sandbox/safe-fetch.js +1 -1
  270. package/dist/sandbox/secret-broker.d.ts +1 -0
  271. package/dist/sandbox/secret-broker.js +1 -1
  272. package/dist/sandbox/tool-timeout.d.ts +32 -0
  273. package/dist/sandbox/tool-timeout.js +52 -0
  274. package/dist/sandbox/user-tool.d.ts +8 -1
  275. package/dist/sandbox/user-tool.js +9 -3
  276. package/dist/serve.js +26 -18
  277. package/dist/viewer/api-schema.d.ts +66 -0
  278. package/dist/viewer/api-schema.js +31 -0
  279. package/dist/viewer/graph.js +2 -1
  280. package/dist/viewer/openapi.js +2 -1
  281. package/dist/viewer/run-detail.d.ts +2 -0
  282. package/dist/viewer/run-detail.js +26 -1
  283. package/dist/wardby-bin.js +5 -0
  284. package/docs/README.md +11 -1
  285. package/docs/agent-recipes.md +37 -4
  286. package/docs/architecture-runtime.md +1 -1
  287. package/docs/claude-code-plugin.md +208 -0
  288. package/docs/code-review-agents.md +6 -0
  289. package/docs/coding-agent-setup.md +94 -1
  290. package/docs/coding-worker-byo-images.md +4 -2
  291. package/docs/coding-worker-isolation.md +94 -7
  292. package/docs/debugging-agent-runs.md +354 -0
  293. package/docs/getting-started-gke.md +33 -11
  294. package/docs/getting-started-identity-provider.md +43 -37
  295. package/docs/getting-started.md +8 -0
  296. package/docs/host-providers.md +294 -0
  297. package/docs/mcp-servers.md +263 -0
  298. package/docs/native-sandbox.md +31 -9
  299. package/docs/security-deployment.md +161 -31
  300. package/docs/tool-secrets.md +3 -0
  301. package/docs/viewer-api.md +27 -12
  302. package/docs/viewer-api.openapi.yml +78 -2
  303. package/help/admin-viewer.md +5 -0
  304. package/help/agent-recipes.md +33 -2
  305. package/help/brokered-secrets.md +3 -0
  306. package/help/build-claude-code-image.md +1 -1
  307. package/help/claude-code-run-pane.md +63 -0
  308. package/help/deploy-gke.md +5 -0
  309. package/help/errors/coding-mcp-gateway-unavailable.md +46 -0
  310. package/help/errors/coding-mcp-pins-unavailable.md +35 -0
  311. package/help/errors/database-auth-invalid.md +1 -1
  312. package/help/errors/host-provider-error.md +40 -0
  313. package/help/errors/host-tool-failed.md +36 -0
  314. package/help/errors/mcp-call-limit.md +22 -0
  315. package/help/errors/mcp-call-timeout.md +22 -0
  316. package/help/errors/mcp-server-auth-failed.md +22 -0
  317. package/help/errors/mcp-server-credentials-unavailable.md +45 -0
  318. package/help/errors/mcp-server-error.md +28 -0
  319. package/help/errors/mcp-server-unreachable.md +22 -0
  320. package/help/errors/mcp-servers-off.md +21 -0
  321. package/help/errors/mcp-tool-not-allowed.md +26 -0
  322. package/help/errors/mcp-tool-removed.md +22 -0
  323. package/help/errors/mcp-unavailable.md +25 -0
  324. package/help/errors/native-sandbox-capacity.md +1 -1
  325. package/help/errors/native-sandbox-deadline-exceeded.md +1 -1
  326. package/help/errors/native-sandbox-docker-failed.md +1 -1
  327. package/help/errors/native-sandbox-gateway-error.md +1 -1
  328. package/help/errors/native-sandbox-gateway-unavailable.md +1 -1
  329. package/help/errors/native-sandbox-image-not-pinned.md +1 -1
  330. package/help/errors/native-sandbox-isolation-mismatch.md +1 -1
  331. package/help/errors/native-sandbox-network-unenforced.md +1 -1
  332. package/help/errors/native-sandbox-requires-catalog.md +1 -1
  333. package/help/errors/native-sandbox-unavailable.md +1 -1
  334. package/help/errors/native-sandbox-warm-delivery-failed.md +1 -1
  335. package/help/errors/native-sandbox-worker-exited.md +1 -1
  336. package/help/errors/native-sandbox-worker-lost.md +1 -1
  337. package/help/errors/native-sandbox-worker-unready.md +1 -1
  338. package/help/errors/repository-not-listed.md +36 -0
  339. package/help/errors/repository-required.md +36 -0
  340. package/help/errors/tool-timeout.md +38 -0
  341. package/help/host-providers.md +40 -0
  342. package/help/identity-and-access.md +4 -1
  343. package/help/low-power-mode.md +19 -8
  344. package/help/mcp-servers.md +73 -0
  345. package/help/mcp.md +9 -2
  346. package/help/native-capabilities.md +27 -1
  347. package/help/native-sandbox.md +1 -1
  348. package/help/operating-agents.md +16 -0
  349. package/help/related-pull-requests.md +7 -3
  350. package/help/repo-instructions-and-skills.md +3 -1
  351. package/help/resume-runs-locally.md +7 -0
  352. package/help/run-steps.md +140 -0
  353. package/help/stop-run.md +1 -1
  354. package/help/troubleshooting/coding-workers.md +9 -1
  355. package/help/troubleshooting/debugging-agent-runs.md +29 -0
  356. package/package.json +2 -1
  357. package/prisma/migrations/20261102000000_host_providers/migration.sql +56 -0
  358. package/prisma/migrations/20261104000000_coding_agent_repository_list/migration.sql +5 -0
  359. package/prisma/migrations/20261106000000_tool_timeout/migration.sql +4 -0
  360. package/prisma/migrations/20261107000000_mcp_servers/migration.sql +33 -0
  361. package/prisma/migrations/20261108000000_coding_mcp/migration.sql +5 -0
  362. package/prisma/migrations/20261109000000_task_run_index/migration.sql +3 -0
  363. package/prisma/migrations/20261110000000_run_steps/migration.sql +26 -0
  364. package/prisma/migrations/20261111000000_run_steps_viewer_notify/migration.sql +59 -0
  365. package/prisma/migrations/20261112000000_run_steps_detail/migration.sql +9 -0
  366. package/prisma/schema.prisma +211 -14
  367. package/scripts/git-askpass.sh +1 -1
@@ -0,0 +1,16 @@
1
+ /**
2
+ * get_run_steps: what a run did, one line per step, oldest first. Same access as get_run. Steps
3
+ * are stored already redacted (src/core/run-steps/writer.ts) and returned as stored; a step's
4
+ * callId is never returned; its detail text (the detail level) only to the agent's owner, the run's
5
+ * starter and the stdio operator (canReadRunDetail).
6
+ */
7
+ import { type RunStepsLevel } from "../../core/run-steps/config.js";
8
+ import type { WardbyMcpServer } from "../server.js";
9
+ export declare const RUN_STEPS_DEFAULT_LIMIT = 50;
10
+ export declare const RUN_STEPS_MAX_LIMIT = 200;
11
+ export interface RunStepsToolOptions {
12
+ level: RunStepsLevel;
13
+ retentionDays: number;
14
+ now?: () => Date;
15
+ }
16
+ export declare function registerRunStepsTool(mcp: WardbyMcpServer, opts: RunStepsToolOptions): void;
@@ -0,0 +1,85 @@
1
+ /**
2
+ * get_run_steps: what a run did, one line per step, oldest first. Same access as get_run. Steps
3
+ * are stored already redacted (src/core/run-steps/writer.ts) and returned as stored; a step's
4
+ * callId is never returned; its detail text (the detail level) only to the agent's owner, the run's
5
+ * starter and the stdio operator (canReadRunDetail).
6
+ */
7
+ import { recordedRunStepsLevel } from "../../core/run-steps/config.js";
8
+ import { canReadRunDetail, findReadableRun } from "./run-access.js";
9
+ import { textResult } from "./text-result.js";
10
+ export const RUN_STEPS_DEFAULT_LIMIT = 50;
11
+ export const RUN_STEPS_MAX_LIMIT = 200;
12
+ const DAY_MS = 24 * 60 * 60 * 1000;
13
+ const EXPIRED_MEANING = "No steps are kept for this run: steps are deleted RUN_STEP_RETENTION_DAYS after they are recorded.";
14
+ export function registerRunStepsTool(mcp, opts) {
15
+ mcp.registerTool({
16
+ name: "get_run_steps",
17
+ scope: "agents:read",
18
+ description: "What a run did, one line per step, oldest first: each tool call (the tool and its command, file or first " +
19
+ "argument, cut short) with its status once known (ok, error or exit N), and what the model said, cut short. " +
20
+ "At the detail level (the instance's WARDBY_RUN_STEPS and the agent's runSteps both detail), the agent's owner " +
21
+ "and the run's starter also get each step's detail: the tool's input, the end of its output, or the model's " +
22
+ "text, redacted. Others get the lines only. level is the run's own, fixed when it started. " +
23
+ "To follow a running run, call again with after set to the last seq you have. " +
24
+ `next is the after for the next page, or null. limit defaults to ${RUN_STEPS_DEFAULT_LIMIT}, at most ` +
25
+ `${RUN_STEPS_MAX_LIMIT}. dropped counts steps past the per-run limit. Same access as get_run.`,
26
+ inputSchema: {
27
+ type: "object",
28
+ properties: {
29
+ runId: { type: "string" },
30
+ after: { type: "number", description: "Only steps with a larger seq" },
31
+ limit: {
32
+ type: "number",
33
+ description: `Steps to return (default ${RUN_STEPS_DEFAULT_LIMIT}, at most ${RUN_STEPS_MAX_LIMIT})`,
34
+ },
35
+ },
36
+ required: ["runId"],
37
+ },
38
+ handler: async (args, ctx) => {
39
+ const run = await findReadableRun(ctx, args.runId);
40
+ const showDetail = await canReadRunDetail(ctx.db, run, {
41
+ principalId: ctx.principal.id,
42
+ operator: ctx.operator,
43
+ });
44
+ const after = Number.isFinite(args.after) ? Math.max(0, Math.floor(args.after)) : 0;
45
+ const requested = Number.isFinite(args.limit) ? Math.floor(args.limit) : RUN_STEPS_DEFAULT_LIMIT;
46
+ const take = Math.min(Math.max(1, requested), RUN_STEPS_MAX_LIMIT);
47
+ const rows = await ctx.db.runStep.findMany({
48
+ where: { runId: run.id, seq: { gt: after } },
49
+ orderBy: { seq: "asc" },
50
+ take: take + 1,
51
+ select: {
52
+ seq: true,
53
+ at: true,
54
+ kind: true,
55
+ tool: true,
56
+ summary: true,
57
+ status: true,
58
+ ...(showDetail ? { detail: true } : {}),
59
+ },
60
+ });
61
+ const page = rows.slice(0, take);
62
+ const now = (opts.now ?? (() => new Date()))();
63
+ const expired = page.length === 0 &&
64
+ after === 0 &&
65
+ run.finishedAt != null &&
66
+ run.finishedAt.getTime() + opts.retentionDays * DAY_MS < now.getTime();
67
+ return textResult({
68
+ runId: run.id,
69
+ level: recordedRunStepsLevel(run.runStepsLevel, opts.level),
70
+ steps: page.map((s) => ({
71
+ seq: s.seq,
72
+ at: s.at.toISOString(),
73
+ kind: s.kind,
74
+ tool: s.tool,
75
+ summary: s.summary,
76
+ status: s.status,
77
+ ...(showDetail ? { detail: s.detail ?? null } : {}),
78
+ })),
79
+ next: rows.length > take ? page[page.length - 1].seq : null,
80
+ dropped: run.stepsDropped,
81
+ ...(expired ? { expired: true, meaning: EXPIRED_MEANING } : {}),
82
+ });
83
+ },
84
+ });
85
+ }
@@ -1,11 +1,19 @@
1
- import { requireAgentAccess } from "../auth/access.js";
1
+ import { agentAccessResolver, readableAgentsWhere, requireAgentAccess } from "../auth/access.js";
2
+ import { requireScope } from "../auth/resource-server.js";
3
+ import { McpError } from "../errors.js";
4
+ import { loadGraphRuns } from "../../viewer/graph.js";
5
+ import { loadJiraConfig } from "../../config/providers.js";
2
6
  import { findReadableRun } from "./run-access.js";
3
- import { publicCodingRunResult, sanitizeDependencyCacheReport } from "../../coding/protocol.js";
7
+ import { publicCodingRunResult, redactTokenShapedValues, sanitizeDependencyCacheReport, } from "../../coding/protocol.js";
4
8
  import { countPlanRefusals, summarizeRegistryFetches } from "../../coding/registry/report.js";
5
9
  import { summarizePackageAudit } from "../../coding/registry/audit-mode.js";
6
10
  import { storedServiceLabels } from "../../coding/services/catalog.js";
7
11
  import { textResult } from "./text-result.js";
8
12
  import { loadBudgetEvents } from "../../core/budget-events.js";
13
+ import { loadAgentSecretValues, maskNamedSecrets } from "../../core/secret-guard.js";
14
+ import { logger } from "../../core/logger.js";
15
+ import { loadLastSteps } from "../../core/run-steps/last-step.js";
16
+ const runsLog = logger.child({ module: "mcp-runs" });
9
17
  /**
10
18
  * Run visibility (resource-sharing grants spec §3.5, A6): a run's output is
11
19
  * the triggerer's and the agent owner's, never every reader's. The owner
@@ -17,45 +25,195 @@ function withOperatorMode(run) {
17
25
  const mode = run.nativeExecutionMode;
18
26
  return { ...run, nativeExecutionMode: mode == null ? null : mode === "sandbox" ? "sandbox" : "control-plane" };
19
27
  }
28
+ /**
29
+ * A coding run's request as its readers may see it. It can be text a caller or an issue wrote, so
30
+ * the agent's secret values and token-shaped values are masked here on every read. A secret that
31
+ * can't be read means the text can't be checked, so it is withheld; the log carries the error name only.
32
+ */
33
+ async function requestForReader(ctx, run, request) {
34
+ try {
35
+ const named = await loadAgentSecretValues(ctx.db, ctx.providers.secrets, run.agentId);
36
+ return { request: redactTokenShapedValues(maskNamedSecrets(request, named)) };
37
+ }
38
+ catch (err) {
39
+ runsLog.warn({ runId: run.id, errorName: err instanceof Error ? err.name : typeof err }, "could not read the agent's secrets to check a run's request; withholding it");
40
+ return { requestUnavailable: true };
41
+ }
42
+ }
43
+ /** The issue-tracker sites issue links are built from (the admin viewer's own source); none when unset or invalid. */
44
+ function issueSites() {
45
+ try {
46
+ const jira = loadJiraConfig();
47
+ return jira ? { jira: jira.siteUrl } : {};
48
+ }
49
+ catch {
50
+ return {};
51
+ }
52
+ }
53
+ /**
54
+ * What started each run and what it produced, in the admin viewer API's shapes (docs/viewer-api.md):
55
+ * `triggerInfo` (the pull request, issue, schedule or lead behind it), `outcomes` (pull requests opened,
56
+ * comments, checks) and `serviceReadiness` (each coding-run service's state). Only ids and links:
57
+ * repositories, numbers, issue keys, URLs, states and times, never free text. A service's failure
58
+ * `reason` is left out, since it can quote what the service printed.
59
+ */
60
+ async function runGraphFields(ctx, runIds) {
61
+ const graph = await loadGraphRuns(ctx.db, runIds, issueSites());
62
+ return new Map(graph.map((run) => [
63
+ run.id,
64
+ {
65
+ triggerInfo: run.trigger,
66
+ outcomes: run.outcomes,
67
+ serviceReadiness: run.services.map(({ name, state, attempts, readyAt, failedAt }) => ({
68
+ name,
69
+ state,
70
+ attempts,
71
+ readyAt,
72
+ failedAt,
73
+ })),
74
+ },
75
+ ]));
76
+ }
77
+ /** Statuses `active: true` keeps, and that `since` always keeps. */
78
+ const ACTIVE_STATUSES = ["pending", "running"];
79
+ /** Rows returned without an agentId: a default and a cap, since it spans every agent the caller can see. */
80
+ const ACROSS_AGENTS_DEFAULT_LIMIT = 50;
81
+ const ACROSS_AGENTS_MAX_LIMIT = 200;
82
+ /** The `active` and `since` filters, as clauses to AND onto the visibility rule. */
83
+ function runFilters(args) {
84
+ const filters = [];
85
+ if (args.active === true)
86
+ filters.push({ status: { in: ACTIVE_STATUSES } });
87
+ if (args.since !== undefined) {
88
+ const since = new Date(args.since);
89
+ if (Number.isNaN(since.getTime()))
90
+ throw new McpError(400, "since must be an ISO date or time");
91
+ filters.push({
92
+ OR: [{ startedAt: { gte: since } }, { finishedAt: { gte: since } }, { status: { in: ACTIVE_STATUSES } }],
93
+ });
94
+ }
95
+ return filters;
96
+ }
97
+ /**
98
+ * Runs across every agent the caller can read, under the same rule as one agent's listing: all runs of
99
+ * agents they own, only the runs they triggered on the rest. With `all` (admin:view, checked by the
100
+ * caller), every run in the deployment, as the admin viewer shows. Rows name their agent and leave
101
+ * out finalText, which can be long and is one get_run away.
102
+ */
103
+ async function listRunsAcrossAgents(ctx, filters, opts) {
104
+ const [where, accessOf] = await Promise.all([
105
+ opts.all ? Promise.resolve({}) : readableAgentsWhere(ctx),
106
+ agentAccessResolver(ctx),
107
+ ]);
108
+ const agents = await ctx.db.agent.findMany({
109
+ where,
110
+ select: { id: true, ownerId: true, name: true, budgetUsd: true },
111
+ });
112
+ const owned = agents.filter((agent) => opts.all || accessOf(agent) === "owner").map((agent) => agent.id);
113
+ const others = agents.filter((agent) => !opts.all && accessOf(agent) !== "owner").map((agent) => agent.id);
114
+ const byId = new Map(agents.map((agent) => [agent.id, agent]));
115
+ // A limit that isn't a finite number falls back to the default rather than reaching the query as NaN.
116
+ const requested = Number.isFinite(opts.limit) ? Math.floor(opts.limit) : ACROSS_AGENTS_DEFAULT_LIMIT;
117
+ const take = Math.min(Math.max(1, requested), ACROSS_AGENTS_MAX_LIMIT);
118
+ const status = opts.status;
119
+ const runs = await ctx.db.run.findMany({
120
+ where: {
121
+ OR: [{ agentId: { in: owned } }, { agentId: { in: others }, triggeredById: ctx.principal.id }],
122
+ ...(status ? { status: status } : {}),
123
+ ...(filters.length > 0 ? { AND: filters } : {}),
124
+ },
125
+ omit: { finalText: true, pricingSnapshot: true },
126
+ take,
127
+ orderBy: { startedAt: "desc" },
128
+ });
129
+ const graph = await runGraphFields(ctx, runs.map((run) => run.id));
130
+ // The newest step of each run (the runs pane's one line), when it has any.
131
+ const lastSteps = await loadLastSteps(ctx.db, runs.map((run) => run.id));
132
+ return runs.map((run) => {
133
+ const agent = byId.get(run.agentId);
134
+ return {
135
+ ...run,
136
+ agentName: agent?.name ?? null,
137
+ agentBudgetUsd: agent ? Number(agent.budgetUsd) : null,
138
+ ...(graph.get(run.id) ?? { triggerInfo: null, outcomes: [], serviceReadiness: [] }),
139
+ ...(lastSteps.has(run.id) ? { lastStep: lastSteps.get(run.id) } : {}),
140
+ };
141
+ });
142
+ }
143
+ /** Same rule as get_run: a pending coding run with queuedAt is waiting for a concurrency slot (CODING_MAX_CONCURRENT), not stuck. */
144
+ async function withQueuedAt(ctx, runs) {
145
+ const pendingIds = runs.filter((run) => run.status === "pending").map((run) => run.id);
146
+ const queued = pendingIds.length > 0
147
+ ? await ctx.db.codingRun.findMany({
148
+ where: { runId: { in: pendingIds }, queuedAt: { not: null } },
149
+ select: { runId: true, queuedAt: true },
150
+ })
151
+ : [];
152
+ const queuedAtByRun = new Map(queued.map((row) => [row.runId, row.queuedAt?.toISOString()]));
153
+ return runs.map((run) => {
154
+ const codingQueuedAt = queuedAtByRun.get(run.id);
155
+ return codingQueuedAt ? { ...withOperatorMode(run), codingQueuedAt } : withOperatorMode(run);
156
+ });
157
+ }
20
158
  export function registerRunTools(mcp) {
21
159
  mcp.registerTool({
22
160
  name: "list_runs",
23
161
  scope: "agents:read",
162
+ description: "Lists runs, newest first. With agentId, that agent's runs; without it, runs across every agent you can see " +
163
+ "(each row adds agentName, agentBudgetUsd, triggerInfo, outcomes, serviceReadiness and lastStep (the run's newest step as one line, when it has steps; the agentId listing has no lastStep), and leaves out finalText " +
164
+ `and pricingSnapshot; default limit ${ACROSS_AGENTS_DEFAULT_LIMIT}, ` +
165
+ `at most ${ACROSS_AGENTS_MAX_LIMIT}). You see all runs of agents you own and only the runs you triggered on others. ` +
166
+ "active: true keeps pending and running runs; since (ISO time) keeps runs that started or finished then or later, " +
167
+ "plus any still pending or running. all: true (no agentId) lists every run in the deployment (admins only, admin:view). " +
168
+ "Use get_run for one run's detail.",
24
169
  inputSchema: {
25
170
  type: "object",
26
- properties: { agentId: { type: "string" }, status: { type: "string" }, limit: { type: "number" } },
27
- required: ["agentId"],
171
+ properties: {
172
+ agentId: { type: "string" },
173
+ status: { type: "string" },
174
+ active: { type: "boolean" },
175
+ since: { type: "string", description: "ISO date or time" },
176
+ all: { type: "boolean" },
177
+ limit: { type: "number" },
178
+ },
28
179
  },
29
180
  handler: async (args, ctx) => {
181
+ const filters = runFilters(args);
182
+ if (args.all === true) {
183
+ if (args.agentId !== undefined)
184
+ throw new McpError(400, "all lists every agent's runs; leave out agentId");
185
+ if (!ctx.operator)
186
+ requireScope(ctx, ctx.canonicalUri, "admin:view");
187
+ }
188
+ if (args.agentId === undefined) {
189
+ const runs = await listRunsAcrossAgents(ctx, filters, {
190
+ status: args.status,
191
+ limit: args.limit,
192
+ all: args.all === true,
193
+ });
194
+ return textResult(await withQueuedAt(ctx, runs));
195
+ }
30
196
  const { access } = await requireAgentAccess(ctx, args.agentId, "read");
31
197
  const runs = await ctx.db.run.findMany({
32
198
  where: {
33
199
  agentId: args.agentId,
34
200
  ...(args.status ? { status: args.status } : {}),
35
201
  ...(access === "owner" ? {} : { triggeredById: ctx.principal.id }),
202
+ ...(filters.length > 0 ? { AND: filters } : {}),
36
203
  },
37
204
  take: args.limit,
38
205
  orderBy: { startedAt: "desc" },
39
206
  });
40
- // Same rule as get_run: a pending coding run with queuedAt is waiting
41
- // for a concurrency slot (CODING_MAX_CONCURRENT), not stuck.
42
- const pendingIds = runs.filter((run) => run.status === "pending").map((run) => run.id);
43
- const queued = pendingIds.length > 0
44
- ? await ctx.db.codingRun.findMany({
45
- where: { runId: { in: pendingIds }, queuedAt: { not: null } },
46
- select: { runId: true, queuedAt: true },
47
- })
48
- : [];
49
- const queuedAtByRun = new Map(queued.map((row) => [row.runId, row.queuedAt?.toISOString()]));
50
- return textResult(runs.map((run) => {
51
- const codingQueuedAt = queuedAtByRun.get(run.id);
52
- return codingQueuedAt ? { ...withOperatorMode(run), codingQueuedAt } : withOperatorMode(run);
53
- }));
207
+ return textResult(await withQueuedAt(ctx, runs));
54
208
  },
55
209
  });
56
210
  mcp.registerTool({
57
211
  name: "get_run",
58
212
  scope: "agents:read",
213
+ description: "One run in full: status, cost, request, result, errors and budget events, plus triggerInfo (what started it: " +
214
+ "a pull request review or mention, an issue, a schedule or a lead run), outcomes (pull requests opened, comments, " +
215
+ "checks) and serviceReadiness. A run's pull-request outcome matching another run's triggerInfo links the two. " +
216
+ "lastStep is the newest step; get_run_steps lists them all.",
59
217
  inputSchema: { type: "object", properties: { runId: { type: "string" } }, required: ["runId"] },
60
218
  handler: async (args, ctx) => {
61
219
  // Its triggerer keeps seeing it even after losing access to the agent;
@@ -65,6 +223,7 @@ export function registerRunTools(mcp) {
65
223
  where: { runId: run.id },
66
224
  select: {
67
225
  result: true,
226
+ request: true,
68
227
  queuedAt: true,
69
228
  failureCategory: true,
70
229
  diagnosticId: true,
@@ -123,8 +282,17 @@ export function registerRunTools(mcp) {
123
282
  : undefined;
124
283
  // Well-formed refusals only, with group, lead and agent budget filled as the run error's are.
125
284
  const budgetEvents = await loadBudgetEvents(ctx.db, run.id);
285
+ // What started the run and what it produced, as the admin viewer shows them.
286
+ const graph = (await runGraphFields(ctx, [run.id])).get(run.id);
287
+ // The newest step (get_run_steps has them all).
288
+ const lastStep = (await loadLastSteps(ctx.db, [run.id])).get(run.id);
289
+ // What the run was asked to do: the trigger's or lead's task, without the agent's standing instructions.
290
+ const request = codingRun?.request ? await requestForReader(ctx, run, codingRun.request) : {};
126
291
  return textResult({
127
292
  ...withOperatorMode(run),
293
+ ...request,
294
+ ...(graph ?? {}),
295
+ ...(lastStep ? { lastStep } : {}),
128
296
  ...(budgetEvents.length > 0 ? { budgetEvents } : {}),
129
297
  ...(codingRun?.budgetReservedUsd != null ? { budgetReservedUsd: Number(codingRun.budgetReservedUsd) } : {}),
130
298
  ...(codingRun?.budgetBaseUsd != null ? { budgetBaseUsd: Number(codingRun.budgetBaseUsd) } : {}),
@@ -38,6 +38,8 @@ export interface SecretElicitationPayload {
38
38
  * removal is refused if the secret's config has changed since (or the link has none).
39
39
  */
40
40
  brokerHash?: string;
41
+ /** The caller passed the hosts:admin check when the link was minted. */
42
+ hostManager?: boolean;
41
43
  }
42
44
  export type SecretElicitationOutcome = {
43
45
  ok: true;
@@ -1,4 +1,4 @@
1
- import { createSecret, listSecrets, setSecretBroker } from "../../core/secrets.js";
1
+ import { createSecret, hostProvidersUsingSecret, listSecrets, setSecretBroker, } from "../../core/secrets.js";
2
2
  import { parseSecretBrokerConfig, SECRET_BROKER_CHANGED, } from "../../core/secret-broker-config.js";
3
3
  /**
4
4
  * A link only does what it was minted for: a create link can't confirm an
@@ -19,6 +19,18 @@ function kindOf(outcome) {
19
19
  return outcome.kind ?? "create";
20
20
  }
21
21
  const ELICITATION_TTL_MS = 600_000;
22
+ const HOST_PROVIDER_LINK_STALE = "host_provider_secret_link_stale: a host provider now uses this secret; request a new link with hosts:admin";
23
+ /**
24
+ * A link minted before the secret joined a host provider carries no hosts:admin check.
25
+ * Refused without recording an outcome, so a fresh link from a host manager still works.
26
+ */
27
+ async function isStaleForHostProvider(db, payload) {
28
+ if (payload.hostManager)
29
+ return false;
30
+ const { ownerId, secretName: name } = payload;
31
+ const secret = await db.secret.findUnique({ where: { ownerId_name: { ownerId, name } }, select: { id: true } });
32
+ return secret !== null && (await hostProvidersUsingSecret(db, secret.id)).providers.length > 0;
33
+ }
22
34
  /** Sweeps every expired entry, not just the one being looked up, so a set-and-never-polled-again entry doesn't linger forever. */
23
35
  async function pruneExpired(db, now) {
24
36
  await db.secretElicitationOutcome.deleteMany({ where: { expiresAt: { lte: now } } });
@@ -58,6 +70,9 @@ export async function fulfillSecretElicitation(payload, value, cipher, db, broke
58
70
  });
59
71
  if (existing && kindOf(existing.outcome) === "create")
60
72
  return existing.outcome;
73
+ if (await isStaleForHostProvider(db, payload)) {
74
+ return { ok: false, kind: "create", error: HOST_PROVIDER_LINK_STALE };
75
+ }
61
76
  let outcome;
62
77
  try {
63
78
  const secret = await createSecret(secretName, value, ownerId, cipher, db, {
@@ -89,7 +104,12 @@ export async function fulfillSecretElicitation(payload, value, cipher, db, broke
89
104
  await recordOutcome(db, ownerId, secretName, outcome, now);
90
105
  return outcome;
91
106
  }
92
- const CORRECTABLE_ERRORS = ["secret_broker_value_invalid", "secret_broker_config_invalid", "bridge_input_limit"];
107
+ const CORRECTABLE_ERRORS = [
108
+ "secret_broker_value_invalid",
109
+ "secret_broker_config_invalid",
110
+ "bridge_input_limit",
111
+ "host_provider_webhook_secret_short",
112
+ ];
93
113
  function isCorrectableInput(message) {
94
114
  return CORRECTABLE_ERRORS.some((code) => message.startsWith(code));
95
115
  }
@@ -115,6 +135,9 @@ export async function fulfillUnbrokerElicitation(payload, cipher, db) {
115
135
  });
116
136
  if (existing && kindOf(existing.outcome) === "unbroker")
117
137
  return existing.outcome;
138
+ if (await isStaleForHostProvider(db, payload)) {
139
+ return { ok: false, kind: "unbroker", error: HOST_PROVIDER_LINK_STALE };
140
+ }
118
141
  let outcome;
119
142
  try {
120
143
  await setSecretBroker(db, cipher, {
@@ -4,10 +4,12 @@
4
4
  * value/ciphertext at the source); this file adds ownership on top.
5
5
  */
6
6
  import { inputRequired, inputResponse } from "@modelcontextprotocol/server";
7
- import { createSecret, listSecrets, attachSecret, detachSecret, deleteSecret, setSecretBroker, } from "../../core/secrets.js";
7
+ import { createSecret, listSecrets, attachSecret, detachSecret, deleteSecret, hostProvidersUsingSecret, setSecretBroker, WEBHOOK_SECRET_ATTACHED, } from "../../core/secrets.js";
8
+ import { mcpServersUsingSecret } from "../../core/mcp-servers.js";
8
9
  import { brokerConfigHash, parseSecretBrokerConfig } from "../../core/secret-broker-config.js";
9
10
  import { McpError } from "../errors.js";
10
11
  import { requireOwnedSecret } from "../auth/ownership.js";
12
+ import { requireScope } from "../auth/resource-server.js";
11
13
  import { requireAgentAccess, requireBindable, requireBindingOwner } from "../auth/access.js";
12
14
  import { clearUnbrokerOutcome, getSecretElicitationOutcome, } from "./secret-elicitation.js";
13
15
  import { allowedSecretNames, brokerCompatibilityWarnings } from "./secret-broker-compat.js";
@@ -27,7 +29,9 @@ function mapSecretError(err) {
27
29
  const message = err instanceof Error ? err.message : String(err);
28
30
  if (message.startsWith("secret_not_found"))
29
31
  throw new McpError(404, message);
30
- if (message.startsWith("secret_broker_config_invalid") || message.startsWith("secret_broker_value_invalid")) {
32
+ if (message.startsWith("secret_broker_config_invalid") ||
33
+ message.startsWith("secret_broker_value_invalid") ||
34
+ message.startsWith("host_provider_webhook_secret_short")) {
31
35
  throw new McpError(400, message);
32
36
  }
33
37
  throw err;
@@ -70,11 +74,26 @@ async function findOwnSecret(ctx, name) {
70
74
  throw new McpError(404, `secret_not_found: no secret named "${name}"`);
71
75
  return secret;
72
76
  }
77
+ /**
78
+ * A host provider's secret needs hosts:admin as well as secrets:write: replacing a credential
79
+ * or its broker config changes what the provider does. True when the caller passed that check,
80
+ * and its links say so; checked before any link is minted, and again when one is submitted.
81
+ */
82
+ async function requireHostsAdminIfProviderSecret(ctx, name) {
83
+ const secret = await ctx.db.secret.findUnique({
84
+ where: { ownerId_name: { ownerId: ctx.principal.id, name } },
85
+ select: { id: true },
86
+ });
87
+ if (!secret || (await hostProvidersUsingSecret(ctx.db, secret.id)).providers.length === 0)
88
+ return false;
89
+ requireScope(ctx, ctx.canonicalUri, "hosts:admin");
90
+ return true;
91
+ }
73
92
  export function registerSecretsTools(mcp, opts) {
74
93
  mcp.registerTool({
75
94
  name: "create_secret",
76
95
  scope: "secrets:write",
77
- description: 'Creates a secret. Omit "value" to enter it securely via a one-time browser link instead of passing it as a plaintext argument. Pass "broker" ({ hosts, pathPrefixes?, placement }) to make it brokered: tools can then only send it to those hosts with fetch(url, { secrets: [name] }) and can never read it. See the help article "brokered-secrets".',
96
+ description: 'Creates a secret. Omit "value" to enter it securely via a one-time browser link instead of passing it as a plaintext argument. Pass "broker" ({ hosts, pathPrefixes?, placement }) to make it brokered: tools can then only send it to those hosts with fetch(url, { secrets: [name] }) and can never read it. A secret a host provider uses also needs hosts:admin. See the help article "brokered-secrets".',
78
97
  inputSchema: {
79
98
  type: "object",
80
99
  properties: {
@@ -85,6 +104,7 @@ export function registerSecretsTools(mcp, opts) {
85
104
  required: ["name"],
86
105
  },
87
106
  handler: async (args, ctx) => {
107
+ const hostManager = await requireHostsAdminIfProviderSecret(ctx, args.name);
88
108
  const broker = args.broker === undefined ? undefined : parseBrokerArg(args.broker);
89
109
  if (args.value !== undefined) {
90
110
  let secret;
@@ -131,6 +151,7 @@ export function registerSecretsTools(mcp, opts) {
131
151
  secretName: args.name,
132
152
  kind: "create",
133
153
  ...(broker ? { broker } : {}),
154
+ ...(hostManager ? { hostManager } : {}),
134
155
  };
135
156
  const token = await mcp.mintRequestState(payload);
136
157
  const url = await opts.buildElicitationUrl(token);
@@ -157,7 +178,7 @@ export function registerSecretsTools(mcp, opts) {
157
178
  mcp.registerTool({
158
179
  name: "set_secret_broker",
159
180
  scope: "secrets:write",
160
- description: 'Sets or changes a secret\'s broker config ({ hosts, pathPrefixes?, placement }), so tools can only send it to those hosts with fetch(url, { secrets: [name] }) and never read it. broker: null removes brokering, which must be confirmed by a person in the browser link this returns. Every change is audited. See the help article "brokered-secrets".',
181
+ description: 'Sets or changes a secret\'s broker config ({ hosts, pathPrefixes?, placement }), so tools can only send it to those hosts with fetch(url, { secrets: [name] }) and never read it. broker: null removes brokering, which must be confirmed by a person in the browser link this returns. Every change is audited. A secret a host provider uses also needs hosts:admin. See the help article "brokered-secrets".',
161
182
  inputSchema: {
162
183
  type: "object",
163
184
  properties: {
@@ -171,6 +192,7 @@ export function registerSecretsTools(mcp, opts) {
171
192
  },
172
193
  handler: async (args, ctx) => {
173
194
  const ownerId = ctx.principal.id;
195
+ const hostManager = await requireHostsAdminIfProviderSecret(ctx, args.name);
174
196
  if (args.broker !== null) {
175
197
  const broker = parseBrokerArg(args.broker);
176
198
  await findOwnSecret(ctx, args.name);
@@ -222,6 +244,7 @@ export function registerSecretsTools(mcp, opts) {
222
244
  secretName: args.name,
223
245
  kind: "unbroker",
224
246
  brokerHash: brokerConfigHash(parseSecretBrokerConfig(secret.broker)),
247
+ ...(hostManager ? { hostManager } : {}),
225
248
  };
226
249
  const token = await mcp.mintRequestState(payload);
227
250
  const url = await opts.buildElicitationUrl(token);
@@ -276,6 +299,8 @@ export function registerSecretsTools(mcp, opts) {
276
299
  await attachSecret(args.agentId, args.name, ownerId, ctx.db, args.alias ?? args.name);
277
300
  }
278
301
  catch (err) {
302
+ if (err instanceof Error && err.message === WEBHOOK_SECRET_ATTACHED)
303
+ throw new McpError(409, err.message);
279
304
  throw new McpError(404, err instanceof Error ? err.message : String(err));
280
305
  }
281
306
  return textResult({ attached: true });
@@ -306,6 +331,15 @@ export function registerSecretsTools(mcp, opts) {
306
331
  inputSchema: { type: "object", properties: { id: { type: "string" } }, required: ["id"] },
307
332
  handler: async (args, ctx) => {
308
333
  await requireOwnedSecret(ctx.db, args.id, ctx.principal.id);
334
+ const { providers } = await hostProvidersUsingSecret(ctx.db, args.id);
335
+ if (providers.length > 0) {
336
+ throw new McpError(409, `This secret is used by host provider ${providers.map((p) => `"${p}"`).join(", ")}; re-register it or delete_host_provider first.`);
337
+ }
338
+ const secret = await ctx.db.secret.findUnique({ where: { id: args.id }, select: { name: true } });
339
+ const servers = secret ? await mcpServersUsingSecret(ctx.db, ctx.principal.id, secret.name) : [];
340
+ if (servers.length > 0) {
341
+ throw new McpError(409, `Secret "${secret.name}" is used by MCP server(s) ${servers.join(", ")}; update or delete them first.`);
342
+ }
309
343
  await deleteSecret(args.id, ctx.db);
310
344
  return textResult({ deleted: args.id });
311
345
  },
@@ -21,6 +21,7 @@ export declare function projectTool(tool: Tool, principalId: string): {
21
21
  name: string;
22
22
  updatedAt: Date;
23
23
  code: string;
24
+ timeoutMs: number | null;
24
25
  createdAt: Date;
25
26
  id: string;
26
27
  ownerId: string | null;
@@ -28,16 +29,17 @@ export declare function projectTool(tool: Tool, principalId: string): {
28
29
  jsonSchema: import("@prisma/client/runtime/client").JsonValue;
29
30
  paramsZod: string;
30
31
  } | {
32
+ public: boolean;
31
33
  id: string;
32
34
  name: string;
33
35
  description: string;
34
- public: boolean;
35
- ownerIsCaller?: undefined;
36
+ timeoutMs: number | null;
36
37
  } | {
38
+ public: boolean;
39
+ ownerIsCaller: boolean;
37
40
  id: string;
38
41
  name: string;
39
42
  description: string;
40
- public: boolean;
41
- ownerIsCaller: boolean;
43
+ timeoutMs: number | null;
42
44
  };
43
45
  export declare function registerToolAuthoringTools(mcp: WardbyMcpServer): void;