@wardby/cli 0.7.0 → 0.8.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 (304) hide show
  1. package/.env.example +5 -5
  2. package/README.md +6 -1
  3. package/deploy/local/docker-compose.quickstart-coding.yml +6 -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 +61 -0
  9. package/dist/claude-plugin/command.js +376 -0
  10. package/dist/cli-help.d.ts +1 -1
  11. package/dist/cli-help.js +2 -1
  12. package/dist/cli.js +47 -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 +1 -1
  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 +36 -11
  46. package/dist/core/error-code.js +1 -0
  47. package/dist/core/grants.d.ts +28 -3
  48. package/dist/core/grants.js +46 -4
  49. package/dist/core/host-events.js +7 -3
  50. package/dist/core/host-status.js +2 -2
  51. package/dist/core/issue-status.js +4 -3
  52. package/dist/core/mark-ready.d.ts +1 -1
  53. package/dist/core/mark-ready.js +1 -1
  54. package/dist/core/mcp-client/client.d.ts +47 -0
  55. package/dist/core/mcp-client/client.js +141 -0
  56. package/dist/core/mcp-client/jsonrpc.d.ts +15 -0
  57. package/dist/core/mcp-client/jsonrpc.js +54 -0
  58. package/dist/core/mcp-client/results.d.ts +13 -0
  59. package/dist/core/mcp-client/results.js +42 -0
  60. package/dist/core/mcp-servers.d.ts +90 -0
  61. package/dist/core/mcp-servers.js +230 -0
  62. package/dist/core/mcp-tools.d.ts +108 -0
  63. package/dist/core/mcp-tools.js +287 -0
  64. package/dist/core/merge-order-check.js +38 -19
  65. package/dist/core/notification-dispatcher.js +2 -1
  66. package/dist/core/related-pull-requests.js +26 -10
  67. package/dist/core/review-fix.js +2 -2
  68. package/dist/core/review-host-tools.js +3 -1
  69. package/dist/core/runner.d.ts +66 -16
  70. package/dist/core/runner.js +281 -73
  71. package/dist/core/secret-broker-config.d.ts +2 -0
  72. package/dist/core/secret-broker-config.js +1 -1
  73. package/dist/core/secret-guard.d.ts +5 -2
  74. package/dist/core/secret-guard.js +22 -7
  75. package/dist/core/secrets.d.ts +11 -1
  76. package/dist/core/secrets.js +27 -1
  77. package/dist/core/serializable-retry.d.ts +18 -0
  78. package/dist/core/serializable-retry.js +43 -0
  79. package/dist/core/tool-admin.d.ts +5 -1
  80. package/dist/core/tool-admin.js +7 -2
  81. package/dist/core/tool-names.d.ts +13 -0
  82. package/dist/core/tool-names.js +20 -0
  83. package/dist/core/webhooks.d.ts +4 -0
  84. package/dist/core/webhooks.js +68 -49
  85. package/dist/core/workflow-run-events.js +2 -1
  86. package/dist/generated/prisma/browser.d.ts +31 -0
  87. package/dist/generated/prisma/client.d.ts +31 -0
  88. package/dist/generated/prisma/commonInputTypes.d.ts +50 -50
  89. package/dist/generated/prisma/internal/class.d.ts +66 -0
  90. package/dist/generated/prisma/internal/class.js +4 -4
  91. package/dist/generated/prisma/internal/prismaNamespace.d.ts +530 -1
  92. package/dist/generated/prisma/internal/prismaNamespace.js +75 -2
  93. package/dist/generated/prisma/internal/prismaNamespaceBrowser.d.ts +79 -0
  94. package/dist/generated/prisma/internal/prismaNamespaceBrowser.js +75 -2
  95. package/dist/generated/prisma/models/Agent.d.ts +271 -0
  96. package/dist/generated/prisma/models/AgentMcpServer.d.ts +1412 -0
  97. package/dist/generated/prisma/models/AgentMcpServer.js +1 -0
  98. package/dist/generated/prisma/models/AgentRepository.d.ts +40 -1
  99. package/dist/generated/prisma/models/AuthUser.d.ts +1 -1
  100. package/dist/generated/prisma/models/CodingAgentProfile.d.ts +0 -7
  101. package/dist/generated/prisma/models/CodingProxySession.d.ts +119 -2
  102. package/dist/generated/prisma/models/CodingRun.d.ts +76 -1
  103. package/dist/generated/prisma/models/HostProvider.d.ts +2176 -0
  104. package/dist/generated/prisma/models/HostProvider.js +1 -0
  105. package/dist/generated/prisma/models/HostProviderCredential.d.ts +1285 -0
  106. package/dist/generated/prisma/models/HostProviderCredential.js +1 -0
  107. package/dist/generated/prisma/models/HostProviderOperation.d.ts +1470 -0
  108. package/dist/generated/prisma/models/HostProviderOperation.js +1 -0
  109. package/dist/generated/prisma/models/HostProviderTrustedAccount.d.ts +1145 -0
  110. package/dist/generated/prisma/models/HostProviderTrustedAccount.js +1 -0
  111. package/dist/generated/prisma/models/McpServer.d.ts +1500 -0
  112. package/dist/generated/prisma/models/McpServer.js +1 -0
  113. package/dist/generated/prisma/models/NativeGatewayCall.d.ts +39 -1
  114. package/dist/generated/prisma/models/Principal.d.ts +167 -0
  115. package/dist/generated/prisma/models/ResourceGrant.d.ts +1 -1
  116. package/dist/generated/prisma/models/Secret.d.ts +387 -0
  117. package/dist/generated/prisma/models/Tool.d.ts +223 -1
  118. package/dist/generated/prisma/models.d.ts +6 -0
  119. package/dist/help/search.js +12 -3
  120. package/dist/help-index.json +719 -47
  121. package/dist/import/create.js +16 -1
  122. package/dist/import/neutral-schema.d.ts +1 -0
  123. package/dist/import/neutral-schema.js +2 -0
  124. package/dist/mcp/auth/grants-cli.d.ts +6 -4
  125. package/dist/mcp/auth/grants-cli.js +42 -3
  126. package/dist/mcp/auth/resource-server.d.ts +3 -1
  127. package/dist/mcp/auth/resource-server.js +17 -2
  128. package/dist/mcp/context.d.ts +3 -0
  129. package/dist/mcp/index.js +11 -0
  130. package/dist/mcp/tools/agents.js +100 -15
  131. package/dist/mcp/tools/grants.js +66 -28
  132. package/dist/mcp/tools/host-providers.d.ts +2 -0
  133. package/dist/mcp/tools/host-providers.js +215 -0
  134. package/dist/mcp/tools/mcp-servers.d.ts +26 -0
  135. package/dist/mcp/tools/mcp-servers.js +481 -0
  136. package/dist/mcp/tools/repositories.js +50 -10
  137. package/dist/mcp/tools/runs.js +178 -18
  138. package/dist/mcp/tools/secret-elicitation.d.ts +2 -0
  139. package/dist/mcp/tools/secret-elicitation.js +25 -2
  140. package/dist/mcp/tools/secrets.js +38 -4
  141. package/dist/mcp/tools/tools.d.ts +6 -4
  142. package/dist/mcp/tools/tools.js +40 -8
  143. package/dist/mcp/tools/trigger.js +28 -6
  144. package/dist/mcp/transport/http-limits.d.ts +2 -0
  145. package/dist/mcp/transport/http-limits.js +6 -2
  146. package/dist/mcp/webhooks/ingress.js +10 -0
  147. package/dist/native-worker/gateway-server.d.ts +14 -0
  148. package/dist/native-worker/gateway-server.js +28 -7
  149. package/dist/native-worker/gateway.d.ts +2 -0
  150. package/dist/native-worker/gateway.js +5 -1
  151. package/dist/native-worker/http-server.d.ts +4 -0
  152. package/dist/native-worker/http-server.js +51 -0
  153. package/dist/native-worker/ledger.d.ts +6 -2
  154. package/dist/native-worker/ledger.js +5 -4
  155. package/dist/native-worker/protocol.d.ts +1 -0
  156. package/dist/native-worker/protocol.js +4 -1
  157. package/dist/providers/coding-proxy/capability-hash.d.ts +2 -0
  158. package/dist/providers/coding-proxy/capability-hash.js +5 -0
  159. package/dist/providers/coding-proxy/mcp-route.d.ts +34 -0
  160. package/dist/providers/coding-proxy/mcp-route.js +166 -0
  161. package/dist/providers/coding-proxy/memory-ledger.d.ts +2 -0
  162. package/dist/providers/coding-proxy/memory-ledger.js +15 -0
  163. package/dist/providers/coding-proxy/prisma-ledger.d.ts +1 -0
  164. package/dist/providers/coding-proxy/prisma-ledger.js +30 -2
  165. package/dist/providers/coding-proxy/proxy.d.ts +7 -1
  166. package/dist/providers/coding-proxy/proxy.js +134 -34
  167. package/dist/providers/coding-proxy/runtime.d.ts +6 -0
  168. package/dist/providers/coding-proxy/runtime.js +32 -1
  169. package/dist/providers/coding-proxy/server.d.ts +3 -0
  170. package/dist/providers/coding-proxy/server.js +10 -0
  171. package/dist/providers/coding-proxy/types.d.ts +13 -0
  172. package/dist/providers/executor/composition.js +5 -0
  173. package/dist/providers/executor/container.d.ts +33 -1
  174. package/dist/providers/executor/container.js +76 -7
  175. package/dist/providers/host-tools/invoke.d.ts +37 -0
  176. package/dist/providers/host-tools/invoke.js +117 -0
  177. package/dist/providers/host-tools/operations.d.ts +65 -0
  178. package/dist/providers/host-tools/operations.js +149 -0
  179. package/dist/providers/host-tools/registry.d.ts +79 -0
  180. package/dist/providers/host-tools/registry.js +362 -0
  181. package/dist/providers/host-tools/request-guard.d.ts +17 -0
  182. package/dist/providers/host-tools/request-guard.js +99 -0
  183. package/dist/providers/host-tools/self-test.d.ts +64 -0
  184. package/dist/providers/host-tools/self-test.js +142 -0
  185. package/dist/providers/jobs/docker-isolation.d.ts +2 -2
  186. package/dist/providers/jobs/docker-isolation.js +30 -8
  187. package/dist/providers/jobs/docker.d.ts +8 -0
  188. package/dist/providers/jobs/docker.js +33 -9
  189. package/dist/providers/jobs/kubernetes-isolation.d.ts +3 -1
  190. package/dist/providers/jobs/kubernetes-isolation.js +15 -2
  191. package/dist/providers/jobs/kubernetes.d.ts +5 -0
  192. package/dist/providers/jobs/kubernetes.js +6 -1
  193. package/dist/providers/jobs/types.d.ts +6 -0
  194. package/dist/providers/review-host/types.d.ts +6 -4
  195. package/dist/providers/review-host/types.js +1 -1
  196. package/dist/providers/vcs/git.d.ts +6 -2
  197. package/dist/providers/vcs/git.js +15 -4
  198. package/dist/providers/vcs/github.d.ts +2 -71
  199. package/dist/providers/vcs/github.js +8 -11
  200. package/dist/providers/vcs/index.d.ts +1 -1
  201. package/dist/providers/vcs/index.js +1 -1
  202. package/dist/providers/vcs/remote.d.ts +4 -0
  203. package/dist/providers/vcs/remote.js +7 -1
  204. package/dist/providers/vcs/routing.d.ts +12 -6
  205. package/dist/providers/vcs/routing.js +21 -6
  206. package/dist/providers/vcs/types.d.ts +71 -3
  207. package/dist/quickstart/index.d.ts +5 -0
  208. package/dist/quickstart/index.js +21 -0
  209. package/dist/quickstart-images.json +1 -1
  210. package/dist/sandbox/eval-core.d.ts +12 -2
  211. package/dist/sandbox/eval-core.js +20 -4
  212. package/dist/sandbox/host-functions.d.ts +4 -0
  213. package/dist/sandbox/host-functions.js +7 -2
  214. package/dist/sandbox/limits.d.ts +4 -1
  215. package/dist/sandbox/limits.js +4 -1
  216. package/dist/sandbox/run-in-sandbox.d.ts +6 -0
  217. package/dist/sandbox/run-in-sandbox.js +14 -2
  218. package/dist/sandbox/safe-fetch.d.ts +2 -0
  219. package/dist/sandbox/safe-fetch.js +1 -1
  220. package/dist/sandbox/secret-broker.d.ts +1 -0
  221. package/dist/sandbox/secret-broker.js +1 -1
  222. package/dist/sandbox/tool-timeout.d.ts +32 -0
  223. package/dist/sandbox/tool-timeout.js +52 -0
  224. package/dist/sandbox/user-tool.d.ts +8 -1
  225. package/dist/sandbox/user-tool.js +9 -3
  226. package/dist/viewer/graph.js +2 -1
  227. package/dist/wardby-bin.js +5 -0
  228. package/docs/README.md +11 -1
  229. package/docs/agent-recipes.md +37 -4
  230. package/docs/architecture-runtime.md +1 -1
  231. package/docs/claude-code-plugin.md +164 -0
  232. package/docs/code-review-agents.md +6 -0
  233. package/docs/coding-agent-setup.md +77 -1
  234. package/docs/coding-worker-byo-images.md +4 -2
  235. package/docs/coding-worker-isolation.md +78 -7
  236. package/docs/debugging-agent-runs.md +242 -0
  237. package/docs/getting-started-gke.md +5 -4
  238. package/docs/getting-started-identity-provider.md +43 -37
  239. package/docs/getting-started.md +8 -0
  240. package/docs/host-providers.md +294 -0
  241. package/docs/mcp-servers.md +254 -0
  242. package/docs/native-sandbox.md +25 -9
  243. package/docs/security-deployment.md +110 -31
  244. package/docs/tool-secrets.md +3 -0
  245. package/docs/viewer-api.md +6 -0
  246. package/docs/viewer-api.openapi.yml +1 -1
  247. package/help/admin-viewer.md +4 -0
  248. package/help/agent-recipes.md +33 -2
  249. package/help/brokered-secrets.md +3 -0
  250. package/help/build-claude-code-image.md +1 -1
  251. package/help/claude-code-run-pane.md +40 -0
  252. package/help/errors/coding-mcp-pins-unavailable.md +35 -0
  253. package/help/errors/database-auth-invalid.md +1 -1
  254. package/help/errors/host-provider-error.md +40 -0
  255. package/help/errors/host-tool-failed.md +36 -0
  256. package/help/errors/mcp-call-limit.md +22 -0
  257. package/help/errors/mcp-call-timeout.md +22 -0
  258. package/help/errors/mcp-server-auth-failed.md +22 -0
  259. package/help/errors/mcp-server-credentials-unavailable.md +45 -0
  260. package/help/errors/mcp-server-error.md +28 -0
  261. package/help/errors/mcp-server-unreachable.md +22 -0
  262. package/help/errors/mcp-servers-off.md +21 -0
  263. package/help/errors/mcp-tool-not-allowed.md +26 -0
  264. package/help/errors/mcp-tool-removed.md +22 -0
  265. package/help/errors/mcp-unavailable.md +25 -0
  266. package/help/errors/native-sandbox-capacity.md +1 -1
  267. package/help/errors/native-sandbox-deadline-exceeded.md +1 -1
  268. package/help/errors/native-sandbox-docker-failed.md +1 -1
  269. package/help/errors/native-sandbox-gateway-error.md +1 -1
  270. package/help/errors/native-sandbox-gateway-unavailable.md +1 -1
  271. package/help/errors/native-sandbox-image-not-pinned.md +1 -1
  272. package/help/errors/native-sandbox-isolation-mismatch.md +1 -1
  273. package/help/errors/native-sandbox-network-unenforced.md +1 -1
  274. package/help/errors/native-sandbox-requires-catalog.md +1 -1
  275. package/help/errors/native-sandbox-unavailable.md +1 -1
  276. package/help/errors/native-sandbox-warm-delivery-failed.md +1 -1
  277. package/help/errors/native-sandbox-worker-exited.md +1 -1
  278. package/help/errors/native-sandbox-worker-lost.md +1 -1
  279. package/help/errors/native-sandbox-worker-unready.md +1 -1
  280. package/help/errors/repository-not-listed.md +36 -0
  281. package/help/errors/repository-required.md +36 -0
  282. package/help/errors/tool-timeout.md +38 -0
  283. package/help/host-providers.md +40 -0
  284. package/help/identity-and-access.md +4 -1
  285. package/help/mcp-servers.md +73 -0
  286. package/help/mcp.md +7 -2
  287. package/help/native-capabilities.md +27 -1
  288. package/help/native-sandbox.md +1 -1
  289. package/help/operating-agents.md +16 -0
  290. package/help/related-pull-requests.md +7 -3
  291. package/help/repo-instructions-and-skills.md +3 -1
  292. package/help/resume-runs-locally.md +3 -0
  293. package/help/stop-run.md +1 -1
  294. package/help/troubleshooting/coding-workers.md +9 -1
  295. package/help/troubleshooting/debugging-agent-runs.md +29 -0
  296. package/package.json +2 -1
  297. package/prisma/migrations/20261102000000_host_providers/migration.sql +56 -0
  298. package/prisma/migrations/20261104000000_coding_agent_repository_list/migration.sql +5 -0
  299. package/prisma/migrations/20261106000000_tool_timeout/migration.sql +4 -0
  300. package/prisma/migrations/20261107000000_mcp_servers/migration.sql +33 -0
  301. package/prisma/migrations/20261108000000_coding_mcp/migration.sql +5 -0
  302. package/prisma/migrations/20261109000000_task_run_index/migration.sql +3 -0
  303. package/prisma/schema.prisma +159 -14
  304. package/scripts/git-askpass.sh +1 -1
@@ -0,0 +1,376 @@
1
+ /**
2
+ * `wardby claude-plugin install | uninstall | status`: sets up the wardby-runs
3
+ * Claude Code plugin (plugins/claude-code) on this machine. Install adds the
4
+ * plugin from a marketplace (wardby/wardby by default, or an operator's own),
5
+ * can register the wardby MCP server, and offers the allow rules auto mode
6
+ * needs for the pane's read-only tools. Uninstall removes the plugin and only
7
+ * the settings install added. Status reports all three.
8
+ *
9
+ * Runs without a database or any server configuration. It prints the commands
10
+ * it runs and their outcome, never a token: the MCP server's sign-in is Claude
11
+ * Code's own, at first use.
12
+ */
13
+ import { spawnSync } from "node:child_process";
14
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
15
+ import { homedir } from "node:os";
16
+ import { dirname, join } from "node:path";
17
+ import { createInterface } from "node:readline";
18
+ import { parseArgs } from "node:util";
19
+ export const PLUGIN = "wardby-runs";
20
+ export const DEFAULT_MARKETPLACE = "wardby/wardby";
21
+ export const MCP_SERVER = "wardby";
22
+ /** The pane's read-only tools on a server: what auto mode needs allowed. stop_run is never added: it changes a run. */
23
+ export function paneTools(server) {
24
+ return ["list_runs", "list_agents", "get_run", "get_run_transcript"].map((tool) => `mcp__${server}__${tool}`);
25
+ }
26
+ export const PANE_TOOLS = paneTools(MCP_SERVER);
27
+ /** How long one `claude` call may take: `claude mcp get` can check its server, which shouldn't stall doctor. */
28
+ const CLAUDE_TIMEOUT_MS = 20_000;
29
+ export const CLAUDE_PLUGIN_USAGE = `usage: wardby claude-plugin install [--marketplace <owner/repo|path>] [--server-url <url>]
30
+ [--allow-tools | --no-allow-tools] [--fullscreen] [--yes]
31
+ wardby claude-plugin uninstall
32
+ wardby claude-plugin status
33
+
34
+ Set up the wardby-runs Claude Code plugin: a pane listing your wardby runs.
35
+
36
+ --server-name <name> The MCP server the pane uses, as /mcp lists it (default ${MCP_SERVER}); its
37
+ allow rules follow the name
38
+ --marketplace <src> Where to install it from (default ${DEFAULT_MARKETPLACE}; an operator's own
39
+ marketplace repository or a local folder works too)
40
+ --server-url <url> Register the wardby MCP server at this URL first (for example
41
+ https://wardby.example.com/mcp) when Claude Code has none named ${MCP_SERVER}
42
+ --allow-tools Allow the pane's read-only wardby tools for auto mode without asking
43
+ --no-allow-tools Leave permission rules alone
44
+ --fullscreen Also switch Claude Code's terminal to its fullscreen view, where the pane
45
+ is a side bar that takes clicks (applies to every session)
46
+ --yes, -y Answer yes to every question`;
47
+ /** The real machine: spawns `claude`, asks on the terminal, uses CLAUDE_CONFIG_DIR or ~/.claude. */
48
+ export function claudePluginDeps() {
49
+ return {
50
+ run: (command, args) => {
51
+ const result = spawnSync(command, args, { encoding: "utf8", timeout: CLAUDE_TIMEOUT_MS });
52
+ return {
53
+ status: result.error ? 127 : (result.status ?? 1),
54
+ stdout: result.stdout ?? "",
55
+ stderr: result.stderr ?? "",
56
+ };
57
+ },
58
+ confirm: async (question) => {
59
+ if (!process.stdin.isTTY)
60
+ return false;
61
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
62
+ try {
63
+ const answer = await new Promise((resolve) => rl.question(`${question} [y/N] `, resolve));
64
+ return /^y(es)?$/i.test(answer.trim());
65
+ }
66
+ finally {
67
+ rl.close();
68
+ }
69
+ },
70
+ claudeConfigDir: process.env.CLAUDE_CONFIG_DIR?.trim() || join(homedir(), ".claude"),
71
+ wardbyDir: join(homedir(), ".wardby"),
72
+ out: (line) => process.stdout.write(`${line}\n`),
73
+ };
74
+ }
75
+ const recordPath = (deps) => join(deps.wardbyDir, "claude-plugin.json");
76
+ /** A settings file as an object: {} when absent; refuses a file that isn't a JSON object rather than overwrite it. */
77
+ function readSettings(path) {
78
+ if (!existsSync(path))
79
+ return {};
80
+ const text = readFileSync(path, "utf8");
81
+ let parsed;
82
+ try {
83
+ parsed = JSON.parse(text);
84
+ }
85
+ catch {
86
+ throw new Error(`${path} isn't valid JSON; fix it, then run this again. Nothing was changed.`);
87
+ }
88
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
89
+ throw new Error(`${path} isn't a JSON object; nothing was changed.`);
90
+ }
91
+ return parsed;
92
+ }
93
+ /** Writes a settings file whole, through a temporary file, so a failure never leaves half a file. */
94
+ function writeSettings(path, settings) {
95
+ mkdirSync(dirname(path), { recursive: true });
96
+ const temp = `${path}.wardby-${process.pid}.tmp`;
97
+ writeFileSync(temp, `${JSON.stringify(settings, null, 2)}\n`, { mode: 0o600 });
98
+ renameSync(temp, path);
99
+ }
100
+ function allowList(settings) {
101
+ const permissions = settings.permissions;
102
+ if (typeof permissions !== "object" || permissions === null)
103
+ return [];
104
+ const allow = permissions.allow;
105
+ return Array.isArray(allow) ? allow.filter((rule) => typeof rule === "string") : [];
106
+ }
107
+ function withAllowList(settings, allow) {
108
+ const permissions = typeof settings.permissions === "object" && settings.permissions !== null
109
+ ? settings.permissions
110
+ : {};
111
+ return { ...settings, permissions: { ...permissions, allow } };
112
+ }
113
+ function readRecord(deps) {
114
+ try {
115
+ const record = JSON.parse(readFileSync(recordPath(deps), "utf8"));
116
+ return typeof record.settingsPath === "string" && Array.isArray(record.rules) ? record : null;
117
+ }
118
+ catch {
119
+ return null;
120
+ }
121
+ }
122
+ function claudeAvailable(deps) {
123
+ return deps.run("claude", ["--version"]).status === 0;
124
+ }
125
+ function hasMcpServer(deps, server = MCP_SERVER) {
126
+ return deps.run("claude", ["mcp", "get", server]).status === 0;
127
+ }
128
+ function pluginInstalled(deps) {
129
+ const list = deps.run("claude", ["plugin", "list"]);
130
+ return list.status === 0 && new RegExp(`\\b${PLUGIN}@`).test(list.stdout);
131
+ }
132
+ /** The first line of a failed command's output, for the message. */
133
+ const firstLine = (result) => (result.stderr || result.stdout).trim().split("\n")[0]?.slice(0, 300) ?? "";
134
+ export async function installPlugin(options, deps) {
135
+ if (!claudeAvailable(deps)) {
136
+ deps.out("Claude Code (claude) wasn't found on PATH. Install it, then run this again.");
137
+ return 1;
138
+ }
139
+ const server = options.serverName ?? MCP_SERVER;
140
+ if (!/^[A-Za-z0-9_-]{1,64}$/.test(server)) {
141
+ deps.out(`--server-name "${server}" isn't a server name (letters, digits, - and _).`);
142
+ return 1;
143
+ }
144
+ const tools = paneTools(server);
145
+ // Read the settings file before changing anything, so a broken one stops the install cleanly.
146
+ const settingsPath = join(deps.claudeConfigDir, "settings.json");
147
+ let settings;
148
+ try {
149
+ settings = readSettings(settingsPath);
150
+ }
151
+ catch (error) {
152
+ deps.out(error instanceof Error ? error.message : String(error));
153
+ return 1;
154
+ }
155
+ // 1. The MCP server the pane talks to.
156
+ if (hasMcpServer(deps, server)) {
157
+ deps.out(`✓ Claude Code has an MCP server named ${server}.`);
158
+ }
159
+ else if (options.serverUrl) {
160
+ let url;
161
+ try {
162
+ url = new URL(options.serverUrl);
163
+ }
164
+ catch {
165
+ deps.out(`--server-url "${options.serverUrl}" isn't a URL.`);
166
+ return 1;
167
+ }
168
+ if (url.protocol !== "https:" &&
169
+ !(url.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname))) {
170
+ deps.out("--server-url must be https (or http on localhost).");
171
+ return 1;
172
+ }
173
+ const added = deps.run("claude", ["mcp", "add", "--transport", "http", "--scope", "user", server, url.href]);
174
+ if (added.status !== 0) {
175
+ deps.out(`Adding the ${server} MCP server failed: ${firstLine(added)}`);
176
+ return 1;
177
+ }
178
+ deps.out(`✓ Added the ${server} MCP server (${url.href}). Sign in with /mcp in Claude Code.`);
179
+ }
180
+ else {
181
+ deps.out(`! Claude Code has no MCP server named ${server}. The pane needs one: run this again with ` +
182
+ "--server-url <your wardby URL>/mcp, or run `wardby quickstart`.");
183
+ }
184
+ // 2. The plugin itself.
185
+ const installed = deps.run("claude", ["plugin", "install", PLUGIN, "--marketplace", options.marketplace]);
186
+ if (installed.status !== 0) {
187
+ deps.out(`Installing the ${PLUGIN} plugin from ${options.marketplace} failed: ${firstLine(installed)}`);
188
+ return 1;
189
+ }
190
+ deps.out(`✓ Installed the ${PLUGIN} plugin from ${options.marketplace}.`);
191
+ // 3. Allow rules for auto mode, and the fullscreen view: each only when asked for or agreed to.
192
+ const record = readRecord(deps) ?? { settingsPath, rules: [], tui: false };
193
+ let changed = false;
194
+ const allowTools = options.allowTools ??
195
+ (options.yes ||
196
+ (await deps.confirm(`Allow the pane's read-only wardby tools (${tools.map((t) => t.replace(`mcp__${server}__`, "")).join(", ")}) ` +
197
+ `in ${settingsPath}? Auto mode refuses the pane's calls without them.`)));
198
+ if (allowTools) {
199
+ const current = allowList(settings);
200
+ const missing = tools.filter((rule) => !current.includes(rule));
201
+ if (missing.length > 0) {
202
+ settings = withAllowList(settings, [...current, ...missing]);
203
+ record.rules = [...new Set([...record.rules, ...missing])];
204
+ changed = true;
205
+ deps.out(`✓ Allowed ${missing.join(", ")}.`);
206
+ }
207
+ else {
208
+ deps.out("✓ The pane's tools are already allowed.");
209
+ }
210
+ }
211
+ else {
212
+ deps.out(`Left permission rules alone. Under auto mode, allow ${tools.join(", ")} in /permissions.`);
213
+ }
214
+ if (options.fullscreen && settings.tui !== "fullscreen") {
215
+ settings = { ...settings, tui: "fullscreen" };
216
+ record.tui = true;
217
+ changed = true;
218
+ deps.out("✓ Switched Claude Code to its fullscreen view (restart Claude Code to see it).");
219
+ }
220
+ if (changed) {
221
+ writeSettings(settingsPath, settings);
222
+ mkdirSync(deps.wardbyDir, { recursive: true });
223
+ writeFileSync(recordPath(deps), `${JSON.stringify({ ...record, settingsPath }, null, 2)}\n`, { mode: 0o600 });
224
+ }
225
+ deps.out("Open the pane in Claude Code with /wardby-runs (in a new session, or after /reload-plugins).");
226
+ return 0;
227
+ }
228
+ export async function uninstallPlugin(deps) {
229
+ if (!claudeAvailable(deps)) {
230
+ deps.out("Claude Code (claude) wasn't found on PATH.");
231
+ return 1;
232
+ }
233
+ const removed = deps.run("claude", ["plugin", "uninstall", PLUGIN]);
234
+ if (removed.status === 0)
235
+ deps.out(`✓ Uninstalled the ${PLUGIN} plugin.`);
236
+ else
237
+ deps.out(`The ${PLUGIN} plugin wasn't uninstalled: ${firstLine(removed)}`);
238
+ // Only what install added: rules it added, and the fullscreen view if it switched it on. The file is
239
+ // written only when one of those is still there; a broken file is reported, not rewritten.
240
+ const record = readRecord(deps);
241
+ if (record && existsSync(record.settingsPath)) {
242
+ let settings;
243
+ try {
244
+ settings = readSettings(record.settingsPath);
245
+ }
246
+ catch (error) {
247
+ deps.out(`${error instanceof Error ? error.message : String(error)} Remove ${record.rules.join(", ")} yourself.`);
248
+ return 1;
249
+ }
250
+ const current = allowList(settings);
251
+ const kept = current.filter((rule) => !record.rules.includes(rule));
252
+ let changed = false;
253
+ if (kept.length !== current.length) {
254
+ settings = withAllowList(settings, kept);
255
+ changed = true;
256
+ deps.out(`✓ Removed the rules install added: ${current.filter((rule) => !kept.includes(rule)).join(", ")}.`);
257
+ }
258
+ if (record.tui && settings.tui === "fullscreen") {
259
+ const { tui: _tui, ...rest } = settings;
260
+ settings = rest;
261
+ changed = true;
262
+ deps.out("✓ Switched the fullscreen view back off.");
263
+ }
264
+ if (changed)
265
+ writeSettings(record.settingsPath, settings);
266
+ }
267
+ if (record)
268
+ writeFileSync(recordPath(deps), "{}\n", { mode: 0o600 });
269
+ deps.out(`The ${MCP_SERVER} MCP server was left in place (remove it with: claude mcp remove ${MCP_SERVER} -s user).`);
270
+ return removed.status === 0 ? 0 : 1;
271
+ }
272
+ export function pluginStatus(deps, serverName = MCP_SERVER) {
273
+ if (!claudeAvailable(deps)) {
274
+ deps.out("✗ Claude Code (claude) wasn't found on PATH.");
275
+ return 1;
276
+ }
277
+ const plugin = pluginInstalled(deps);
278
+ const server = hasMcpServer(deps, serverName);
279
+ const settingsPath = join(deps.claudeConfigDir, "settings.json");
280
+ let allowed = [];
281
+ try {
282
+ allowed = allowList(readSettings(settingsPath));
283
+ }
284
+ catch (error) {
285
+ deps.out(`! ${error instanceof Error ? error.message : String(error)}`);
286
+ }
287
+ const missing = paneTools(serverName).filter((rule) => !allowed.includes(rule));
288
+ deps.out(`${plugin ? "✓" : "✗"} ${PLUGIN} plugin ${plugin ? "installed" : "not installed"}`);
289
+ deps.out(`${server ? "✓" : "✗"} ${serverName} MCP server ${server ? "configured" : "not configured"}`);
290
+ deps.out(missing.length === 0
291
+ ? `✓ the pane's tools are allowed in ${settingsPath}`
292
+ : `! not allowed in ${settingsPath} (auto mode refuses them): ${missing.join(", ")}`);
293
+ return plugin && server ? 0 : 1;
294
+ }
295
+ /**
296
+ * Quickstart's offer of the pane, once it connected Claude Code: asks unless a flag answered it,
297
+ * never in a non-interactive run, and installs on a yes. A failed install is a warning, not a stop.
298
+ */
299
+ export async function quickstartPaneStep(options, steps) {
300
+ if (!options.mcpConfigured || (options.client !== "claude" && options.client !== "both"))
301
+ return "skipped";
302
+ const wanted = options.flag ??
303
+ (!options.nonInteractive && (await steps.ask("Add the wardby runs pane to Claude Code (the wardby-runs plugin)?")));
304
+ if (!wanted)
305
+ return "skipped";
306
+ const status = await steps.install({ marketplace: DEFAULT_MARKETPLACE, fullscreen: false, yes: options.yes });
307
+ if (status === 0)
308
+ return "installed";
309
+ steps.warn("! The runs pane wasn't installed; try again with: npx @wardby/cli@latest claude-plugin install");
310
+ return "failed";
311
+ }
312
+ /**
313
+ * `wardby doctor`'s lines for the pane: none without Claude Code. The pane is optional, so nothing
314
+ * here is a failure (✗): a missing plugin is "○", a missing server or rule a warning ("!").
315
+ */
316
+ export function claudePluginDoctorLines(deps, serverName = MCP_SERVER) {
317
+ if (!claudeAvailable(deps))
318
+ return [];
319
+ if (!pluginInstalled(deps)) {
320
+ return [`○ Claude Code runs pane not installed (optional: npx @wardby/cli@latest claude-plugin install)`];
321
+ }
322
+ const lines = [`✓ ${PLUGIN} Claude Code plugin installed`];
323
+ lines.push(hasMcpServer(deps, serverName)
324
+ ? `✓ ${serverName} MCP server configured in Claude Code`
325
+ : `! Claude Code has no MCP server named ${serverName}: the pane has nothing to show (claude-plugin install --server-url <url>)`);
326
+ const settingsPath = join(deps.claudeConfigDir, "settings.json");
327
+ let missing = paneTools(serverName);
328
+ try {
329
+ const allowed = allowList(readSettings(settingsPath));
330
+ missing = paneTools(serverName).filter((rule) => !allowed.includes(rule));
331
+ }
332
+ catch {
333
+ // An unreadable settings file reads as no rules: the warning below says what to allow.
334
+ }
335
+ lines.push(missing.length === 0
336
+ ? "✓ the pane's tools are allowed for auto mode"
337
+ : `! not allowed for auto mode in ${settingsPath}: ${missing.join(", ")} (needed only in auto mode)`);
338
+ return lines;
339
+ }
340
+ export async function claudePluginCommand(args, deps = claudePluginDeps()) {
341
+ const [sub, ...rest] = args;
342
+ if (sub === undefined || sub === "--help" || sub === "-h") {
343
+ deps.out(CLAUDE_PLUGIN_USAGE);
344
+ return sub === undefined ? 1 : 0;
345
+ }
346
+ if (sub === "install") {
347
+ const { values } = parseArgs({
348
+ args: rest,
349
+ allowNegative: true,
350
+ options: {
351
+ marketplace: { type: "string" },
352
+ "server-name": { type: "string" },
353
+ "server-url": { type: "string" },
354
+ "allow-tools": { type: "boolean" },
355
+ fullscreen: { type: "boolean" },
356
+ yes: { type: "boolean", short: "y" },
357
+ },
358
+ });
359
+ return installPlugin({
360
+ marketplace: values.marketplace ?? DEFAULT_MARKETPLACE,
361
+ serverName: values["server-name"],
362
+ serverUrl: values["server-url"],
363
+ allowTools: values["allow-tools"],
364
+ fullscreen: values.fullscreen ?? false,
365
+ yes: values.yes ?? false,
366
+ }, deps);
367
+ }
368
+ if (sub === "uninstall")
369
+ return uninstallPlugin(deps);
370
+ if (sub === "status") {
371
+ const { values } = parseArgs({ args: rest, options: { "server-name": { type: "string" } } });
372
+ return pluginStatus(deps, values["server-name"] ?? MCP_SERVER);
373
+ }
374
+ deps.out(`Unknown claude-plugin command "${sub}".\n${CLAUDE_PLUGIN_USAGE}`);
375
+ return 1;
376
+ }
@@ -1 +1 @@
1
- export declare const CLI_USAGE = "usage:\n wardby quickstart [--provider openai|anthropic] [--model <m>] [--budget <usd>] [--client none|codex|claude|both] [--skip-demo] [--non-interactive --yes]\n wardby doctor\n wardby status\n wardby logs [--tail N] [--follow]\n wardby down [--volumes]\n wardby knowledge check [dir] [--root <dir>] [--strict] [--json] Validate a knowledge bundle (offline)\n wardby resume - [--dir <path> | --here] [--force] [--print] Continue a coding run's Claude Code session locally (paste the get_run_transcript link)\n wardby help [list]\n wardby help search <terms>\n wardby help open <article-id>\n wardby agent create --name <n> --model <m> --prompt <p> --budget <usd> [--schedule \"<cron>\"] [--timezone <tz>] [--max-turns <n>] [--owner <subject>] [--public] [--native-execution-mode control-plane|sandbox]\n (owned by --owner or LOCAL_PRINCIPAL; --public shares it with everyone at execute;\n sandbox needs NATIVE_SANDBOX_LAUNCHER set in this environment)\n wardby agent list\n wardby agent mode <name> control-plane|sandbox (native agents; applies to runs created after the change)\n wardby agent schedule <name> --cron \"<expr>\" [--timezone <tz>] [--disable]\n wardby tool create --name <n> --description <d> --params <file> --code <file>\n wardby tool attach <tool-name|tool-id> <agent-name> (grants the capabilities in the agent owner's name)\n wardby tool detach <tool-name|tool-id> <agent-name>\n wardby tool update <tool-name|tool-id> [--description <d>] [--params <file>] [--code <file>]\n wardby tool delete <tool-name|tool-id> [--detach]\n wardby tool list [--agent <name>]\n wardby run <name>\n wardby runs [--agent <name>] [--limit N] [--status <s>]\n wardby coding preflight (JOB_LAUNCHER=docker or kubernetes)\n wardby coding cleanup --run-id <id>\n wardby scheduler [--scope default]\n wardby native-gateway (the native sandbox gateway only; NATIVE_GATEWAY_LISTEN, default 0.0.0.0:8790)\n wardby mcp (MCP_TRANSPORT=stdio|http selects the transport)\n wardby serve [--scope default] (mcp + scheduler + reconciler in one process; http only)\n wardby grants migration-report [--json]\n wardby grants adopt-public --owner <subject> [--dry-run] [--keep-everyone-execute]\n wardby grants prune-bindings [--dry-run]\n wardby import <bundle-dir> [--owner <subject>] [--public] [--include-secrets --transfer-key <pem>] [--default-budget <usd>] [--dry-run] [--prefix <p>] [--on-conflict fail|skip|rename] [--allow-open-fetch]\n\noptions:\n -h, --help Show this help\n -v, --version Show the installed Wardby version";
1
+ export declare const CLI_USAGE = "usage:\n wardby quickstart [--provider openai|anthropic] [--model <m>] [--budget <usd>] [--client none|codex|claude|both] [--claude-plugin | --no-claude-plugin] [--skip-demo] [--non-interactive --yes]\n wardby doctor\n wardby status\n wardby logs [--tail N] [--follow]\n wardby down [--volumes]\n wardby knowledge check [dir] [--root <dir>] [--strict] [--json] Validate a knowledge bundle (offline)\n wardby resume - [--dir <path> | --here] [--force] [--print] Continue a coding run's Claude Code session locally (paste the get_run_transcript link)\n wardby claude-plugin install|uninstall|status [options] Set up the wardby-runs Claude Code pane (plugin, MCP server, allow rules)\n wardby help [list]\n wardby help search <terms>\n wardby help open <article-id>\n wardby agent create --name <n> --model <m> --prompt <p> --budget <usd> [--schedule \"<cron>\"] [--timezone <tz>] [--max-turns <n>] [--owner <subject>] [--public] [--native-execution-mode control-plane|sandbox]\n (owned by --owner or LOCAL_PRINCIPAL; --public shares it with everyone at execute;\n sandbox needs NATIVE_SANDBOX_LAUNCHER set in this environment)\n wardby agent list\n wardby agent mode <name> control-plane|sandbox (native agents; applies to runs created after the change)\n wardby agent schedule <name> --cron \"<expr>\" [--timezone <tz>] [--disable]\n wardby tool create --name <n> --description <d> --params <file> --code <file>\n wardby tool attach <tool-name|tool-id> <agent-name> (grants the capabilities in the agent owner's name)\n wardby tool detach <tool-name|tool-id> <agent-name>\n wardby tool update <tool-name|tool-id> [--description <d>] [--params <file>] [--code <file>]\n wardby tool delete <tool-name|tool-id> [--detach]\n wardby tool list [--agent <name>]\n wardby run <name>\n wardby runs [--agent <name>] [--limit N] [--status <s>]\n wardby coding preflight (JOB_LAUNCHER=docker or kubernetes)\n wardby coding cleanup --run-id <id>\n wardby scheduler [--scope default]\n wardby native-gateway (the native sandbox gateway only; NATIVE_GATEWAY_LISTEN, default 0.0.0.0:8790)\n wardby mcp (MCP_TRANSPORT=stdio|http selects the transport)\n wardby serve [--scope default] (mcp + scheduler + reconciler in one process; http only)\n wardby grants migration-report [--json]\n wardby grants adopt-public --owner <subject> [--dry-run] [--keep-everyone-execute]\n wardby grants prune-bindings [--dry-run]\n wardby import <bundle-dir> [--owner <subject>] [--public] [--include-secrets --transfer-key <pem>] [--default-budget <usd>] [--dry-run] [--prefix <p>] [--on-conflict fail|skip|rename] [--allow-open-fetch]\n\noptions:\n -h, --help Show this help\n -v, --version Show the installed Wardby version";
package/dist/cli-help.js CHANGED
@@ -1,11 +1,12 @@
1
1
  export const CLI_USAGE = `usage:
2
- wardby quickstart [--provider openai|anthropic] [--model <m>] [--budget <usd>] [--client none|codex|claude|both] [--skip-demo] [--non-interactive --yes]
2
+ wardby quickstart [--provider openai|anthropic] [--model <m>] [--budget <usd>] [--client none|codex|claude|both] [--claude-plugin | --no-claude-plugin] [--skip-demo] [--non-interactive --yes]
3
3
  wardby doctor
4
4
  wardby status
5
5
  wardby logs [--tail N] [--follow]
6
6
  wardby down [--volumes]
7
7
  wardby knowledge check [dir] [--root <dir>] [--strict] [--json] Validate a knowledge bundle (offline)
8
8
  wardby resume - [--dir <path> | --here] [--force] [--print] Continue a coding run's Claude Code session locally (paste the get_run_transcript link)
9
+ wardby claude-plugin install|uninstall|status [options] Set up the wardby-runs Claude Code pane (plugin, MCP server, allow rules)
9
10
  wardby help [list]
10
11
  wardby help search <terms>
11
12
  wardby help open <article-id>
package/dist/cli.js CHANGED
@@ -60,9 +60,13 @@ import { logger } from "./core/logger.js";
60
60
  import { buildPower } from "./core/power-composition.js";
61
61
  import { deriveJsonSchema } from "./sandbox/zod-params.js";
62
62
  import { toolSubject, zod3ToolWarnings } from "./core/zod3-window.js";
63
- import { findSameNamedAttachedTool, reservedToolNameReason, resolveToolRef } from "./core/tool-names.js";
63
+ import { findSameNamedAttachedTool, findSameNamedMcpTool, reservedToolNameReason, resolveToolRef, sameNamedMcpToolMessage, } from "./core/tool-names.js";
64
64
  import { ToolAttachedError, deleteToolGuarded, formatAttachedAgents, formatToolLine, hasToolChanges, prepareToolUpdate, updateToolGuarded, } from "./core/tool-admin.js";
65
65
  import { ToolCapabilitiesPatchSchema } from "./sandbox/tool-capabilities.js";
66
+ import { toolTimeoutProblem } from "./sandbox/tool-timeout.js";
67
+ import { loadToolMaxTimeoutMs } from "./config/tool-timeout.js";
68
+ import { loadMcpServersMode } from "./config/mcp-servers.js";
69
+ import { loadMcpMaxCallsPerRun } from "./config/mcp-call-limit.js";
66
70
  import { startMcp, warmUpExecutor } from "./mcp/index.js";
67
71
  import { startServe } from "./serve.js";
68
72
  import { authCommand } from "./mcp/auth/self-hosted/cli.js";
@@ -226,6 +230,21 @@ async function agentCreate(args) {
226
230
  });
227
231
  console.log(agent.id);
228
232
  }
233
+ /**
234
+ * `--timeout-ms <ms>`: the tool's own time limit, up to WARDBY_TOOL_MAX_TIMEOUT_MS. On update,
235
+ * `--timeout-ms default` clears it (back to the default limit).
236
+ */
237
+ function cliToolTimeout(raw, allowClear) {
238
+ if (raw === undefined)
239
+ return undefined;
240
+ if (allowClear && raw === "default")
241
+ return null;
242
+ const value = /^\d+$/.test(raw.trim()) ? Number(raw.trim()) : NaN;
243
+ const problem = toolTimeoutProblem(value, loadToolMaxTimeoutMs());
244
+ if (problem)
245
+ fail(`invalid --timeout-ms: ${problem}`);
246
+ return value;
247
+ }
229
248
  async function toolCreate(args) {
230
249
  const { values } = parseArgs({
231
250
  args,
@@ -234,6 +253,7 @@ async function toolCreate(args) {
234
253
  description: { type: "string" },
235
254
  params: { type: "string" },
236
255
  code: { type: "string" },
256
+ "timeout-ms": { type: "string" },
237
257
  },
238
258
  });
239
259
  if (!values.name || !values.description || !values.params || !values.code) {
@@ -242,6 +262,7 @@ async function toolCreate(args) {
242
262
  const reserved = reservedToolNameReason(values.name);
243
263
  if (reserved)
244
264
  fail(reserved);
265
+ const timeoutMs = cliToolTimeout(values["timeout-ms"], false);
245
266
  let paramsZod;
246
267
  let code;
247
268
  try {
@@ -277,6 +298,7 @@ async function toolCreate(args) {
277
298
  paramsZod: paramsZod,
278
299
  jsonSchema: schemaResult.value,
279
300
  code: code,
301
+ ...(timeoutMs !== undefined ? { timeoutMs } : {}),
280
302
  },
281
303
  });
282
304
  for (const warning of await zod3ToolWarnings(toolSubject(tool.name), paramsZod))
@@ -336,6 +358,9 @@ async function toolAttach(args, detach) {
336
358
  if (clash) {
337
359
  fail(`agent "${agentName}" already has a different tool named "${tool.name}" attached (${clash.id}); detach it first.`);
338
360
  }
361
+ const mcpClash = await findSameNamedMcpTool(prisma, agent.id, tool.name);
362
+ if (mcpClash)
363
+ fail(sameNamedMcpToolMessage(agentName, tool.name, mcpClash.serverName));
339
364
  const patch = ToolCapabilitiesPatchSchema.safeParse({
340
365
  allowedSecrets: values["allow-secret"],
341
366
  allowedDatastorePrefixes: values["allow-datastore-prefix"],
@@ -422,6 +447,7 @@ async function toolUpdate(args) {
422
447
  description: { type: "string" },
423
448
  params: { type: "string" },
424
449
  code: { type: "string" },
450
+ "timeout-ms": { type: "string" },
425
451
  },
426
452
  });
427
453
  const [ref] = positionals;
@@ -441,9 +467,10 @@ async function toolUpdate(args) {
441
467
  description: values.description,
442
468
  paramsZod: readFile("--params", values.params),
443
469
  code: readFile("--code", values.code),
470
+ timeoutMs: cliToolTimeout(values["timeout-ms"], true),
444
471
  };
445
472
  if (!hasToolChanges(changes)) {
446
- fail("tool update requires at least one of --description, --params <file>, --code <file>.");
473
+ fail("tool update requires at least one of --description, --params <file>, --code <file>, --timeout-ms.");
447
474
  }
448
475
  const resolved = await resolveToolRef(prisma, ref);
449
476
  if (!resolved.ok)
@@ -792,6 +819,10 @@ async function scheduler(args) {
792
819
  cliLog.warn(`low_power_scale_forbidden: missing ${missing.join(", ")}`);
793
820
  }, (err) => cliLog.warn({ err }, "low_power_access_check_failed"));
794
821
  }
822
+ // Read again at each tool call; checked here so a bad value stops start-up, not every run.
823
+ loadToolMaxTimeoutMs();
824
+ loadMcpServersMode();
825
+ loadMcpMaxCallsPerRun();
795
826
  const executor = buildConfiguredExecutor({
796
827
  native: nativeExecutor,
797
828
  db: prisma,
@@ -858,6 +889,14 @@ async function scheduler(args) {
858
889
  */
859
890
  async function nativeGateway() {
860
891
  const listen = parseGatewayListen(process.env.NATIVE_GATEWAY_LISTEN) ?? { host: "0.0.0.0", port: 8790 };
892
+ try {
893
+ loadToolMaxTimeoutMs();
894
+ loadMcpServersMode();
895
+ loadMcpMaxCallsPerRun();
896
+ }
897
+ catch (err) {
898
+ fail(err instanceof Error ? err.message : String(err));
899
+ }
861
900
  let modelCatalog;
862
901
  try {
863
902
  modelCatalog = await startModelCatalog(prisma);
@@ -878,7 +917,12 @@ async function nativeGateway() {
878
917
  repoAccess: createRepoAccessGate({ db: prisma, hosts: reviewHosts }),
879
918
  executor: new DeferredExecutor(prisma),
880
919
  };
881
- const server = await startGatewayServer(listen, { db: prisma, providers });
920
+ const server = await startGatewayServer(listen, {
921
+ db: prisma,
922
+ providers,
923
+ // Coding runs' MCP calls, forwarded by the coding proxy's /mcp route (its own token auth).
924
+ codingMcp: { db: prisma, cipher: secrets, log: logger.child({ module: "coding-mcp" }) },
925
+ });
882
926
  // The deny port (NATIVE_GATEWAY_DENY_PORT, default 8791) serves nothing: a worker that can reach the
883
927
  // gateway port but not this one proves its NetworkPolicy is programmed and port-scoped.
884
928
  const denyPort = Number(process.env.NATIVE_GATEWAY_DENY_PORT ?? NATIVE_GATEWAY_DENY_PORT);
@@ -0,0 +1,16 @@
1
+ /** Whether `name` may be registered: it must also be what a `<name>:<workspace>/<repo>` id resolves to. */
2
+ export declare function isValidHostProviderName(name: string): boolean;
3
+ /** The provider that owns an already-normalized repository id. */
4
+ export declare function codeProviderOf(repository: string): string;
5
+ /**
6
+ * The canonical id of a repository on a host with pull requests: GitHub names,
7
+ * or `<provider>:<workspace>/<repo>` lower-cased. Throws for anything else,
8
+ * local repositories included, as normalizeGitHubRepository always has.
9
+ */
10
+ export declare function normalizeHostedRepository(value: string): string;
11
+ /**
12
+ * A pull request's web link, known here only for GitHub. A GitHub name never
13
+ * has a ":": anything that does gets no link, so no other host's id, valid or
14
+ * not, reaches a github.com URL.
15
+ */
16
+ export declare function pullRequestWebUrl(provider: string, repository: string, number: number): string | null;
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Which code host owns a repository, from the form of its id: `owner/name` is
3
+ * GitHub, `local:/abs/path` a local folder, `<provider>:<workspace>/<repo>` a
4
+ * registered host provider. Control-plane only: coding/protocol.ts ships in the
5
+ * worker images, so this stays out of it.
6
+ */
7
+ import { LOCAL_REPO_PREFIX, normalizeGitHubRepository } from "./protocol.js";
8
+ const HOSTED_REPOSITORY = /^([a-z][a-z0-9-]{1,30}):([A-Za-z0-9][A-Za-z0-9._-]{0,99})\/([A-Za-z0-9][A-Za-z0-9._-]{0,99})$/;
9
+ const HOST_PROVIDER_NAME = /^[a-z][a-z0-9-]{1,30}$/;
10
+ /** Never a registered provider: built-ins, URL schemes a repository string could start with, and an Object.prototype key. */
11
+ const RESERVED_PREFIXES = new Set([
12
+ "github",
13
+ "local",
14
+ "http",
15
+ "https",
16
+ "ssh",
17
+ "git",
18
+ "file",
19
+ "constructor",
20
+ ]);
21
+ /** Whether `name` may be registered: it must also be what a `<name>:<workspace>/<repo>` id resolves to. */
22
+ export function isValidHostProviderName(name) {
23
+ return HOST_PROVIDER_NAME.test(name) && !RESERVED_PREFIXES.has(name);
24
+ }
25
+ /** `a..b` could walk a path and `web.git` is a second name for `web`, so neither is a repository id. */
26
+ function isCanonicalPart(part) {
27
+ return !part.endsWith(".") && !part.includes("..") && !part.toLowerCase().endsWith(".git");
28
+ }
29
+ function hostedParts(value) {
30
+ const match = HOSTED_REPOSITORY.exec(value);
31
+ if (!match || RESERVED_PREFIXES.has(match[1]))
32
+ return null;
33
+ const [, provider, workspace, repository] = match;
34
+ return isCanonicalPart(workspace) && isCanonicalPart(repository) ? { provider, workspace, repository } : null;
35
+ }
36
+ /** The provider that owns an already-normalized repository id. */
37
+ export function codeProviderOf(repository) {
38
+ if (repository.startsWith(LOCAL_REPO_PREFIX))
39
+ return "local";
40
+ return hostedParts(repository)?.provider ?? "github";
41
+ }
42
+ /**
43
+ * The canonical id of a repository on a host with pull requests: GitHub names,
44
+ * or `<provider>:<workspace>/<repo>` lower-cased. Throws for anything else,
45
+ * local repositories included, as normalizeGitHubRepository always has.
46
+ */
47
+ export function normalizeHostedRepository(value) {
48
+ const hosted = hostedParts(value);
49
+ if (hosted)
50
+ return `${hosted.provider}:${hosted.workspace.toLowerCase()}/${hosted.repository.toLowerCase()}`;
51
+ return normalizeGitHubRepository(value);
52
+ }
53
+ /**
54
+ * A pull request's web link, known here only for GitHub. A GitHub name never
55
+ * has a ":": anything that does gets no link, so no other host's id, valid or
56
+ * not, reaches a github.com URL.
57
+ */
58
+ export function pullRequestWebUrl(provider, repository, number) {
59
+ return provider === "github" && !repository.includes(":") ? `https://github.com/${repository}/pull/${number}` : null;
60
+ }
@@ -0,0 +1,12 @@
1
+ export declare const MCP_TOKEN_ENV = "WARDBY_MCP_TOKEN";
2
+ /** The worker's MCP server key; Claude Code names its tools `mcp__<key>__<exposed name>`. */
3
+ export declare const WORKER_MCP_SERVER_KEY = "wardby_mcp";
4
+ export declare const CLAUDE_MCP_TOOL_PREFIX = "mcp__wardby_mcp__";
5
+ /** The 401 text of both /mcp (coding proxy) and /coding-mcp (native gateway). */
6
+ export declare const MCP_TOKEN_REQUIRED_MESSAGE = "A valid MCP token for an active run is required.";
7
+ /** Exactly what mintMcpToken makes; launchers refuse to deliver anything else. */
8
+ export declare function isMcpToken(value: string | undefined): value is string;
9
+ /** `rmt_` followed by 32 random bytes in base64url. */
10
+ export declare function mintMcpToken(): string;
11
+ /** The token's stored form: the same SHA-256 (base64url) the run capability uses. */
12
+ export declare function mcpTokenHash(token: string): string;
@@ -0,0 +1,26 @@
1
+ /**
2
+ * A coding run's MCP token (remote MCP servers, phase 2): the worker's bearer for the proxy's /mcp
3
+ * route only. The control plane mints one per run whose agent has MCP tools, stores only its hash
4
+ * on the proxy session, keeps the value in the run capability vault for the launch, and delivers it
5
+ * as WARDBY_MCP_TOKEN (never in a job spec, command line, or log).
6
+ */
7
+ import { randomBytes } from "node:crypto";
8
+ import { capabilityHash } from "../providers/coding-proxy/capability-hash.js";
9
+ export const MCP_TOKEN_ENV = "WARDBY_MCP_TOKEN";
10
+ /** The worker's MCP server key; Claude Code names its tools `mcp__<key>__<exposed name>`. */
11
+ export const WORKER_MCP_SERVER_KEY = "wardby_mcp";
12
+ export const CLAUDE_MCP_TOOL_PREFIX = `mcp__${WORKER_MCP_SERVER_KEY}__`;
13
+ /** The 401 text of both /mcp (coding proxy) and /coding-mcp (native gateway). */
14
+ export const MCP_TOKEN_REQUIRED_MESSAGE = "A valid MCP token for an active run is required.";
15
+ /** Exactly what mintMcpToken makes; launchers refuse to deliver anything else. */
16
+ export function isMcpToken(value) {
17
+ return value !== undefined && /^rmt_[A-Za-z0-9_-]{43}$/.test(value);
18
+ }
19
+ /** `rmt_` followed by 32 random bytes in base64url. */
20
+ export function mintMcpToken() {
21
+ return `rmt_${randomBytes(32).toString("base64url")}`;
22
+ }
23
+ /** The token's stored form: the same SHA-256 (base64url) the run capability uses. */
24
+ export function mcpTokenHash(token) {
25
+ return capabilityHash(token);
26
+ }
@@ -795,6 +795,8 @@ const TOKEN_PATTERNS = [
795
795
  /AKIA[0-9A-Z]{16}/g,
796
796
  /(?:Bearer|Basic)\s+[A-Za-z0-9._~+/=-]{16,}/gi,
797
797
  /rrp_[A-Za-z0-9_-]{32,}/g,
798
+ // A coding run's MCP token (src/coding/mcp-token.ts).
799
+ /rmt_[A-Za-z0-9_-]{20,}/g,
798
800
  /rv[a-z]_[0-9a-f-]{36}\.[A-Za-z0-9_-]{20,}/gi,
799
801
  // A JWT — an IdP access/ID token in delegating auth mode, and what a JWKS or
800
802
  // token-exchange failure is most likely to quote back. The HEADER segment is