@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
@@ -4,6 +4,7 @@ export declare class MemoryProxyLedger implements ProxyLedger {
4
4
  private readonly options;
5
5
  private readonly sessions;
6
6
  private readonly sessionByCapability;
7
+ private readonly sessionByMcpToken;
7
8
  private readonly requests;
8
9
  private readonly requestBySessionKey;
9
10
  private readonly startedAt;
@@ -11,6 +12,7 @@ export declare class MemoryProxyLedger implements ProxyLedger {
11
12
  now?: () => Date;
12
13
  });
13
14
  createSession(input: CreateProxySessionInput): Promise<void>;
15
+ findSessionByMcpTokenHash(mcpTokenHash: string): Promise<ProxySession | undefined>;
14
16
  findSessionByCapabilityHash(capabilityHash: string): Promise<ProxySession | null>;
15
17
  reserve(input: ReserveProxyRequestInput): Promise<ReserveProxyRequestResult>;
16
18
  complete(requestId: string, usage: ProxyUsage, actualCostUsd: number, upstreamStatus: number): Promise<ProxyRequest>;
@@ -10,6 +10,7 @@ function cloneSession(session) {
10
10
  terms: session.terms
11
11
  ? { version: session.terms.version, entry: { ...session.terms.entry, efforts: [...session.terms.entry.efforts] } }
12
12
  : null,
13
+ mcpTools: session.mcpTools ? structuredClone(session.mcpTools) : null,
13
14
  };
14
15
  }
15
16
  function cloneRequest(request) {
@@ -24,6 +25,7 @@ export class MemoryProxyLedger {
24
25
  options;
25
26
  sessions = new Map();
26
27
  sessionByCapability = new Map();
28
+ sessionByMcpToken = new Map();
27
29
  requests = new Map();
28
30
  requestBySessionKey = new Map();
29
31
  startedAt = new Map();
@@ -36,14 +38,27 @@ export class MemoryProxyLedger {
36
38
  }
37
39
  if (this.sessionByCapability.has(input.capabilityHash))
38
40
  throw new Error("proxy_capability_collision");
41
+ if (input.mcpTokenHash !== undefined && this.sessionByMcpToken.has(input.mcpTokenHash)) {
42
+ throw new Error("proxy_mcp_token_collision");
43
+ }
39
44
  const session = {
40
45
  ...input,
41
46
  allowedModels: [...input.allowedModels],
42
47
  status: "active",
43
48
  terms: input.terms ? normalizeProxyModelTerms(input.terms) : null,
49
+ mcpTools: input.mcpTools ? structuredClone(input.mcpTools) : null,
50
+ mcpTokenHash: input.mcpTokenHash ?? null,
51
+ mcpCallCount: 0,
44
52
  };
45
53
  this.sessions.set(session.id, session);
46
54
  this.sessionByCapability.set(session.capabilityHash, session.id);
55
+ if (session.mcpTokenHash !== null)
56
+ this.sessionByMcpToken.set(session.mcpTokenHash, session.id);
57
+ }
58
+ async findSessionByMcpTokenHash(mcpTokenHash) {
59
+ const id = this.sessionByMcpToken.get(mcpTokenHash);
60
+ const session = id ? this.sessions.get(id) : undefined;
61
+ return session ? cloneSession(session) : undefined;
47
62
  }
48
63
  async findSessionByCapabilityHash(capabilityHash) {
49
64
  const id = this.sessionByCapability.get(capabilityHash);
@@ -20,6 +20,7 @@ export declare class PrismaProxyLedger implements ProxyLedger {
20
20
  private transaction;
21
21
  createSession(input: CreateProxySessionInput): Promise<void>;
22
22
  findSessionByCapabilityHash(capabilityHash: string): Promise<ProxySession | null>;
23
+ findSessionByMcpTokenHash(mcpTokenHash: string): Promise<ProxySession | undefined>;
23
24
  reserve(input: ReserveProxyRequestInput): Promise<ReserveProxyRequestResult>;
24
25
  complete(requestId: string, usage: ProxyUsage, actualCostUsd: number, upstreamStatus: number): Promise<ProxyRequest>;
25
26
  /**
@@ -35,8 +35,26 @@ function sessionFromRow(row) {
35
35
  budgetExhaustedAt: row.budgetExhaustedAt ?? null,
36
36
  upstreamFailure: row.upstreamFailure ?? null,
37
37
  terms: sessionTerms(row),
38
+ mcpTools: sessionMcpTools(row),
39
+ mcpTokenHash: row.mcpTokenHash ?? null,
40
+ mcpCallCount: Number(row.mcpCallCount ?? 0),
38
41
  };
39
42
  }
43
+ /** The session's pinned MCP tools; null when it has none. Anything but a list of pins refuses the session. */
44
+ function sessionMcpTools(row) {
45
+ const stored = row.mcpTools ?? null;
46
+ if (stored === null)
47
+ return null;
48
+ if (!Array.isArray(stored) ||
49
+ !stored.every((pin) => typeof pin === "object" &&
50
+ pin !== null &&
51
+ typeof pin.exposedName === "string" &&
52
+ typeof pin.toolName === "string" &&
53
+ typeof pin.serverId === "string")) {
54
+ throw new Error("invalid_proxy_session_mcp_tools");
55
+ }
56
+ return stored;
57
+ }
40
58
  /**
41
59
  * The session's stored model terms. Null only when both columns are null (a session from before
42
60
  * the catalog, which prices from the shipped fallback); terms that are partly present or cannot be
@@ -109,22 +127,32 @@ export class PrismaProxyLedger {
109
127
  await this.db.$executeRaw `
110
128
  INSERT INTO "CodingProxySession"
111
129
  ("id", "runId", "capabilityHash", "credentialRef", "protocol", "allowedModels", "deadlineAt",
112
- "budgetUsd", "status", "registryTokenHash", "pricingVersion", "catalogEntry", "createdAt", "updatedAt")
130
+ "budgetUsd", "status", "registryTokenHash", "pricingVersion", "catalogEntry", "mcpTools", "mcpTokenHash",
131
+ "createdAt", "updatedAt")
113
132
  VALUES
114
133
  (${input.id}, ${input.runId}, ${input.capabilityHash}, ${input.credentialRef}, ${input.protocol}, ${models}::jsonb,
115
134
  ${input.deadlineAt}, ${input.budgetUsd}, 'active', ${input.registryTokenHash},
116
135
  ${terms?.version ?? null}, ${terms ? JSON.stringify(terms.entry) : null}::jsonb,
136
+ ${input.mcpTools ? JSON.stringify(input.mcpTools) : null}::jsonb, ${input.mcpTokenHash ?? null},
117
137
  CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
118
138
  `;
119
139
  }
120
140
  async findSessionByCapabilityHash(capabilityHash) {
121
141
  const rows = await this.db.$queryRaw `
122
142
  SELECT "id", "runId", "capabilityHash", "credentialRef", "protocol", "allowedModels", "deadlineAt", "budgetUsd", "status", "registryTokenHash",
123
- "budgetExhaustedAt", "upstreamFailure", "pricingVersion", "catalogEntry"
143
+ "budgetExhaustedAt", "upstreamFailure", "pricingVersion", "catalogEntry", "mcpTools", "mcpTokenHash", "mcpCallCount"
124
144
  FROM "CodingProxySession" WHERE "capabilityHash" = ${capabilityHash}
125
145
  `;
126
146
  return rows[0] ? sessionFromRow(rows[0]) : null;
127
147
  }
148
+ async findSessionByMcpTokenHash(mcpTokenHash) {
149
+ const rows = await this.db.$queryRaw `
150
+ SELECT "id", "runId", "capabilityHash", "credentialRef", "protocol", "allowedModels", "deadlineAt", "budgetUsd", "status", "registryTokenHash",
151
+ "budgetExhaustedAt", "upstreamFailure", "pricingVersion", "catalogEntry", "mcpTools", "mcpTokenHash", "mcpCallCount"
152
+ FROM "CodingProxySession" WHERE "mcpTokenHash" = ${mcpTokenHash}
153
+ `;
154
+ return rows[0] ? sessionFromRow(rows[0]) : undefined;
155
+ }
128
156
  async reserve(input) {
129
157
  return this.transaction(async (tx) => {
130
158
  const sessions = await tx.$queryRaw `
@@ -1,4 +1,6 @@
1
1
  import type { ModelPricing } from "../llm/pricing-core.js";
2
+ import { capabilityHash } from "./capability-hash.js";
3
+ import type { McpToolPin } from "../../core/mcp-tools.js";
2
4
  import type { CredentialResolver, ProxyAuditSink, ProxyLedger, ProxyModelTerms, ProxyProtocol, ProxyRefusalRecord } from "./types.js";
3
5
  export declare const PROXY_MAX_BODY_BYTES: number;
4
6
  export declare const PROXY_MAX_OUTPUT_TOKENS = 1000000;
@@ -43,6 +45,10 @@ export interface CreateCodingProxySession {
43
45
  budgetUsd: number;
44
46
  /** The run's catalog entry recorded at dispatch; the session then prices and shapes requests from it. */
45
47
  terms?: ProxyModelTerms;
48
+ /** The run's pinned MCP tools (no URLs or secrets); only with mcpTokenHash, and only when there is one. */
49
+ mcpTools?: McpToolPin[];
50
+ /** SHA-256 (base64url) of the run's MCP token; the control plane mints and delivers the token itself. */
51
+ mcpTokenHash?: string;
46
52
  }
47
53
  export interface CreatedCodingProxySession {
48
54
  id: string;
@@ -70,7 +76,7 @@ export interface CodingProxyOptions {
70
76
  */
71
77
  onModelRequest?: (runId: string) => Promise<void>;
72
78
  }
73
- export declare function capabilityHash(capability: string): string;
79
+ export { capabilityHash };
74
80
  export declare class CodingProxy {
75
81
  private readonly ledger;
76
82
  private readonly credentials;
@@ -1,7 +1,9 @@
1
- import { createHash, randomBytes, randomUUID } from "node:crypto";
1
+ import { randomBytes, randomUUID } from "node:crypto";
2
2
  import { shippedCatalog } from "../llm/catalog.js";
3
3
  import { SHIPPED_CATALOG_VERSION } from "../llm/catalog-shipped.js";
4
4
  import { deriveRegistryToken } from "../../coding/registry/token.js";
5
+ import { capabilityHash } from "./capability-hash.js";
6
+ import { CLAUDE_MCP_TOOL_PREFIX } from "../../coding/mcp-token.js";
5
7
  import { AnthropicSseUsageTracker, actualCostUsd, fingerprintRequest, parseAnthropicAuthoritativeUsage, parseAuthoritativeUsage, pricingSnapshot, safeUpstreamErrorCode, terminalUsageFromSseFrame, } from "./metering.js";
6
8
  import { conversationKey, estimateHold } from "./hold.js";
7
9
  import { estimateInterrupted, previousInput } from "./interrupted.js";
@@ -48,20 +50,19 @@ export class CodingProxyError extends Error {
48
50
  this.code = code;
49
51
  }
50
52
  }
51
- export function capabilityHash(capability) {
52
- return createHash("sha256").update(capability).digest("base64url");
53
- }
53
+ export { capabilityHash };
54
54
  // OpenAI Responses allowlist. Everything a request may carry is listed here;
55
55
  // anything else is refused before a credential is resolved. It mirrors what the
56
56
  // pinned Codex CLI (src/coding-worker/package.json) actually sends, recorded in
57
57
  // fixtures/codex-<version>-responses-requests.json. Only client-side tools
58
- // (function, custom, and namespaces of them) are allowed: a hosted tool (web
59
- // search, remote MCP, code interpreter, image generation, file search, ...), a
60
- // remote image or file, a stored prompt, or a previous response would make
61
- // OpenAI fetch or run something outside the worker's egress policy and outside
62
- // the token-metered budget. Tool names vary by model and Codex version, so they
63
- // are checked for shape, not against a fixed list: a client-side tool the model
64
- // calls is executed by the worker itself, which the container boundary governs.
58
+ // (function, custom, namespaces of them, and the client-executed tool_search)
59
+ // are allowed: a hosted tool (web search, remote MCP, code interpreter, image
60
+ // generation, file search, ...), a remote image or file, a stored prompt, or a
61
+ // previous response would make OpenAI fetch or run something outside the
62
+ // worker's egress policy and outside the token-metered budget. Tool names vary
63
+ // by model and Codex version, so they are checked for shape, not against a
64
+ // fixed list: a client-side tool the model calls is executed by the worker
65
+ // itself, which the container boundary governs.
65
66
  const OPENAI_REQUEST_KEYS = new Set([
66
67
  "model",
67
68
  "instructions",
@@ -151,13 +152,32 @@ function openAiToolName(value, code = "invalid_openai_request") {
151
152
  if (typeof value !== "string" || !OPENAI_TOOL_NAME.test(value))
152
153
  throw new CodingProxyError(400, code);
153
154
  }
154
- function validateOpenAiFunctionTool(tool) {
155
- openAiOnlyKeys(tool, ["type", "name", "description", "strict", "parameters"], "invalid_openai_tool");
155
+ function validateOpenAiFunctionTool(tool, inSearchOutput) {
156
+ openAiOnlyKeys(tool, inSearchOutput
157
+ ? ["type", "name", "description", "strict", "defer_loading", "parameters"]
158
+ : ["type", "name", "description", "strict", "parameters"], "invalid_openai_tool");
156
159
  openAiToolName(tool.name, "invalid_openai_tool");
157
160
  openAiOptional(tool.description, (value) => openAiString(value, MAX_TOOL_TEXT_BYTES, "invalid_openai_tool"));
158
161
  if (tool.strict !== undefined && typeof tool.strict !== "boolean") {
159
162
  throw new CodingProxyError(400, "invalid_openai_tool");
160
163
  }
164
+ // Codex marks the tools a tool_search_output loads with defer_loading: true,
165
+ // and sends it nowhere else. It only delays when the model sees the
166
+ // definition, which is checked in full.
167
+ if (tool.defer_loading !== undefined && tool.defer_loading !== true) {
168
+ throw new CodingProxyError(400, "invalid_openai_tool");
169
+ }
170
+ openAiOptional(tool.parameters, (value) => openAiRecord(value, "invalid_openai_tool"));
171
+ }
172
+ // The tool_search tool Codex sends for models whose metadata asks for it. Only
173
+ // client execution: Codex runs the search over its own deferred tools and
174
+ // returns them in a tool_search_output item. A server-executed search would
175
+ // have OpenAI run it.
176
+ function validateOpenAiToolSearchTool(tool) {
177
+ openAiOnlyKeys(tool, ["type", "execution", "description", "parameters"], "invalid_openai_tool");
178
+ if (tool.execution !== "client")
179
+ throw new CodingProxyError(400, "openai_tool_not_allowed:tool_search");
180
+ openAiOptional(tool.description, (value) => openAiString(value, MAX_TOOL_TEXT_BYTES, "invalid_openai_tool"));
161
181
  openAiOptional(tool.parameters, (value) => openAiRecord(value, "invalid_openai_tool"));
162
182
  }
163
183
  function validateOpenAiCustomTool(tool) {
@@ -175,30 +195,57 @@ function validateOpenAiCustomTool(tool) {
175
195
  }
176
196
  openAiString(format.definition, MAX_TOOL_TEXT_BYTES, "invalid_openai_tool");
177
197
  }
178
- function validateOpenAiTools(value, count, nested = false) {
198
+ // Every tool list (the request's tools, each additional_tools item, each
199
+ // tool_search_output item) is capped at MAX_OPENAI_TOOLS entries on its own.
200
+ // Across the request, `distinct` holds the `namespace/name` of each callable
201
+ // tool and is capped too. Codex replays every earlier tool_search_output on
202
+ // each turn, so a long run repeats the same definitions many times: each copy
203
+ // is still validated, but only a tool not seen before adds to the shared cap.
204
+ function validateOpenAiTools(value, distinct, { toolSearch = false, inSearchOutput = false, namespace, listed = { value: 0 }, } = {}) {
179
205
  if (!Array.isArray(value))
180
206
  throw new CodingProxyError(400, "invalid_openai_tool");
207
+ const callable = (name) => {
208
+ distinct.add(`${namespace ?? ""}/${name}`);
209
+ if (distinct.size > MAX_OPENAI_TOOLS)
210
+ throw new CodingProxyError(400, "invalid_openai_tool");
211
+ };
181
212
  for (const candidate of value) {
182
- if (++count.value > MAX_OPENAI_TOOLS)
213
+ if (++listed.value > MAX_OPENAI_TOOLS)
183
214
  throw new CodingProxyError(400, "invalid_openai_tool");
184
215
  const tool = openAiRecord(candidate, "invalid_openai_tool");
185
216
  if (tool.type === "function") {
186
- validateOpenAiFunctionTool(tool);
217
+ validateOpenAiFunctionTool(tool, inSearchOutput);
218
+ callable(tool.name);
187
219
  }
188
220
  else if (tool.type === "custom") {
189
221
  validateOpenAiCustomTool(tool);
222
+ callable(tool.name);
190
223
  }
191
- else if (tool.type === "namespace" && !nested) {
224
+ else if (tool.type === "namespace" && namespace === undefined) {
192
225
  openAiOnlyKeys(tool, ["type", "name", "description", "tools"], "invalid_openai_tool");
193
226
  openAiToolName(tool.name, "invalid_openai_tool");
194
227
  openAiOptional(tool.description, (value) => openAiString(value, MAX_TOOL_TEXT_BYTES, "invalid_openai_tool"));
195
- validateOpenAiTools(tool.tools, count, true);
228
+ validateOpenAiTools(tool.tools, distinct, { inSearchOutput, namespace: tool.name, listed });
229
+ }
230
+ else if (tool.type === "tool_search" && toolSearch) {
231
+ validateOpenAiToolSearchTool(tool);
232
+ callable("tool_search");
196
233
  }
197
234
  else {
198
235
  throw new CodingProxyError(400, openAiCode("openai_tool_not_allowed", tool.type));
199
236
  }
200
237
  }
201
238
  }
239
+ // The arguments the model gave the client-side tool_search, as its parameters
240
+ // in the captures declare them: a query and an optional result limit.
241
+ function validateOpenAiToolSearchArguments(value) {
242
+ const args = openAiRecord(value);
243
+ openAiOnlyKeys(args, ["query", "limit"]);
244
+ openAiString(args.query, MAX_TOOL_TEXT_BYTES);
245
+ if (args.limit !== undefined && !(Number.isSafeInteger(args.limit) && args.limit > 0)) {
246
+ throw new CodingProxyError(400, "invalid_openai_request");
247
+ }
248
+ }
202
249
  function validateOpenAiContentPart(value, allowed) {
203
250
  const part = openAiRecord(value);
204
251
  if (typeof part.type !== "string" || !allowed.has(part.type)) {
@@ -241,7 +288,7 @@ function validateOpenAiTextParts(value, type) {
241
288
  openAiString(part.text, PROXY_MAX_BODY_BYTES);
242
289
  }
243
290
  }
244
- function validateOpenAiInputItem(value, toolCount) {
291
+ function validateOpenAiInputItem(value, distinctTools) {
245
292
  const item = openAiRecord(value);
246
293
  switch (item.type) {
247
294
  case undefined:
@@ -300,12 +347,32 @@ function validateOpenAiInputItem(value, toolCount) {
300
347
  openAiOptional(item.recipient, (value) => openAiString(value, MAX_OPENAI_ID_LENGTH));
301
348
  validateOpenAiContent(item.content, OPENAI_AGENT_MESSAGE_PARTS, false);
302
349
  return;
350
+ case "tool_search_call":
351
+ openAiOnlyKeys(item, ["type", "id", "call_id", "status", "execution", "arguments"]);
352
+ openAiOptional(item.id, openAiId);
353
+ openAiId(item.call_id);
354
+ openAiOptional(item.status, (value) => openAiString(value, 64));
355
+ if (item.execution !== "client")
356
+ throw new CodingProxyError(400, openAiCode("openai_input_not_allowed", item.type));
357
+ validateOpenAiToolSearchArguments(item.arguments);
358
+ return;
359
+ case "tool_search_output":
360
+ // The tools the client-side search loaded: each one passes the same checks
361
+ // as a top-level tool, so this cannot bring in a shape the proxy refuses.
362
+ openAiOnlyKeys(item, ["type", "id", "call_id", "status", "execution", "tools"]);
363
+ openAiOptional(item.id, openAiId);
364
+ openAiId(item.call_id);
365
+ openAiOptional(item.status, (value) => openAiString(value, 64));
366
+ if (item.execution !== "client")
367
+ throw new CodingProxyError(400, openAiCode("openai_input_not_allowed", item.type));
368
+ validateOpenAiTools(item.tools, distinctTools, { inSearchOutput: true });
369
+ return;
303
370
  case "additional_tools":
304
371
  openAiOnlyKeys(item, ["type", "id", "role", "tools"]);
305
372
  openAiOptional(item.id, openAiId);
306
373
  if (item.role !== undefined && item.role !== "developer")
307
374
  throw new CodingProxyError(400, "invalid_openai_request");
308
- validateOpenAiTools(item.tools, toolCount);
375
+ validateOpenAiTools(item.tools, distinctTools);
309
376
  return;
310
377
  default:
311
378
  throw new CodingProxyError(400, openAiCode("openai_input_not_allowed", item.type));
@@ -372,9 +439,9 @@ function validateOpenAiBody(body) {
372
439
  throw new CodingProxyError(400, "invalid_openai_request");
373
440
  }
374
441
  openAiOptional(body.instructions, (value) => openAiString(value, PROXY_MAX_BODY_BYTES));
375
- const toolCount = { value: 0 };
442
+ const distinctTools = new Set();
376
443
  if (body.tools !== undefined)
377
- validateOpenAiTools(body.tools, toolCount);
444
+ validateOpenAiTools(body.tools, distinctTools, { toolSearch: true });
378
445
  if (body.tool_choice !== undefined) {
379
446
  if (typeof body.tool_choice !== "string" || !OPENAI_TOOL_CHOICES.has(body.tool_choice)) {
380
447
  throw new CodingProxyError(400, "openai_tool_choice_not_allowed");
@@ -401,7 +468,7 @@ function validateOpenAiBody(body) {
401
468
  throw new CodingProxyError(400, "invalid_openai_request");
402
469
  }
403
470
  for (const item of body.input)
404
- validateOpenAiInputItem(item, toolCount);
471
+ validateOpenAiInputItem(item, distinctTools);
405
472
  }
406
473
  function parseOpenAiRequest(rawBody) {
407
474
  if (Buffer.byteLength(rawBody) > PROXY_MAX_BODY_BYTES)
@@ -516,15 +583,33 @@ function validateStructuredOutputTool(value) {
516
583
  if (tool.cache_control !== undefined)
517
584
  validateCacheControl(tool.cache_control);
518
585
  }
519
- function validateApprovedTool(value) {
586
+ /**
587
+ * One of the run's pinned MCP tools, as Claude Code advertises it: a plain client tool named
588
+ * `mcp__wardby_mcp__<exposed name>` (fixtures/claude-code-2.1.286-mcp-requests.json). Only the name
589
+ * is checked against the pins; the gateway checks the call itself again.
590
+ */
591
+ function validateMcpTool(tool) {
592
+ onlyKeys(tool, ["name", "description", "input_schema", "cache_control", "type"]);
593
+ if ((tool.description !== undefined &&
594
+ (typeof tool.description !== "string" || tool.description.length > MAX_TOOL_TEXT_BYTES)) ||
595
+ (tool.type !== undefined && tool.type !== "custom")) {
596
+ throw new CodingProxyError(400, "tools_not_allowed");
597
+ }
598
+ record(tool.input_schema, "tools_not_allowed");
599
+ if (tool.cache_control !== undefined)
600
+ validateCacheControl(tool.cache_control);
601
+ }
602
+ function validateApprovedTool(value, mcpTools) {
520
603
  const tool = record(value, "tools_not_allowed");
521
604
  if (tool.name === WARDBY_COMMAND_TOOL)
522
605
  return validateCommandTool(tool);
523
606
  if (tool.name === STRUCTURED_OUTPUT_TOOL)
524
607
  return validateStructuredOutputTool(tool);
608
+ if (typeof tool.name === "string" && mcpTools.has(tool.name))
609
+ return validateMcpTool(tool);
525
610
  throw new CodingProxyError(400, "tools_not_allowed");
526
611
  }
527
- function validateToolUseBlock(value) {
612
+ function validateToolUseBlock(value, mcpTools) {
528
613
  onlyKeys(value, ["type", "id", "name", "input"]);
529
614
  if (typeof value.id !== "string" || value.id.length < 1 || value.id.length > 512) {
530
615
  throw new CodingProxyError(400, "unsupported_anthropic_feature");
@@ -533,6 +618,9 @@ function validateToolUseBlock(value) {
533
618
  return validateCommandInput(value.input);
534
619
  if (value.name === STRUCTURED_OUTPUT_TOOL)
535
620
  return void record(value.input, "unsupported_anthropic_feature");
621
+ if (typeof value.name === "string" && mcpTools.has(value.name)) {
622
+ return void record(value.input, "unsupported_anthropic_feature");
623
+ }
536
624
  throw new CodingProxyError(400, "unsupported_anthropic_feature");
537
625
  }
538
626
  function validateToolResultBlock(value) {
@@ -559,7 +647,7 @@ function validateThinkingBlock(value) {
559
647
  throw new CodingProxyError(400, "unsupported_anthropic_feature");
560
648
  }
561
649
  }
562
- function validateMessageBlocks(value, role) {
650
+ function validateMessageBlocks(value, role, mcpTools) {
563
651
  if (!Array.isArray(value) || value.length > 10_000)
564
652
  throw new CodingProxyError(400, "invalid_anthropic_request");
565
653
  for (const candidate of value) {
@@ -568,7 +656,7 @@ function validateMessageBlocks(value, role) {
568
656
  validateTextBlock(block);
569
657
  }
570
658
  else if (block.type === "tool_use" && role === "assistant") {
571
- validateToolUseBlock(block);
659
+ validateToolUseBlock(block, mcpTools);
572
660
  }
573
661
  else if (block.type === "tool_result" && role === "user") {
574
662
  validateToolResultBlock(block);
@@ -624,7 +712,7 @@ function validateEffortConfig(value, { thinkingMode, efforts }, betas) {
624
712
  throw new CodingProxyError(400, "unsupported_anthropic_feature");
625
713
  }
626
714
  }
627
- function parseAnthropicRequest(rawBody, betaHeader, shape) {
715
+ function parseAnthropicRequest(rawBody, betaHeader, shape, mcpTools) {
628
716
  const beta = parseAnthropicBeta(betaHeader);
629
717
  if (Buffer.byteLength(rawBody) > PROXY_MAX_BODY_BYTES)
630
718
  throw new CodingProxyError(413, "payload_too_large");
@@ -688,18 +776,20 @@ function parseAnthropicRequest(rawBody, betaHeader, shape) {
688
776
  continue;
689
777
  }
690
778
  }
691
- validateMessageBlocks(message.content, message.role);
779
+ validateMessageBlocks(message.content, message.role, mcpTools);
692
780
  }
693
781
  if (body.tools !== undefined) {
694
- if (!Array.isArray(body.tools) || body.tools.length > 2)
782
+ // run_command and StructuredOutput, plus one per pinned MCP tool.
783
+ if (!Array.isArray(body.tools) || body.tools.length > 2 + mcpTools.size) {
695
784
  throw new CodingProxyError(400, "tools_not_allowed");
785
+ }
696
786
  const names = new Set();
697
787
  for (const tool of body.tools) {
698
788
  const definition = record(tool, "tools_not_allowed");
699
789
  if (typeof definition.name !== "string" || names.has(definition.name))
700
790
  throw new CodingProxyError(400, "tools_not_allowed");
701
791
  names.add(definition.name);
702
- validateApprovedTool(definition);
792
+ validateApprovedTool(definition, mcpTools);
703
793
  }
704
794
  }
705
795
  if (body.metadata !== undefined) {
@@ -760,13 +850,17 @@ function parseAnthropicRequest(rawBody, betaHeader, shape) {
760
850
  anthropicBeta: beta.header,
761
851
  };
762
852
  }
763
- function parseRequest(protocol, rawBody, anthropicBeta, shape) {
853
+ /** The Claude tool names of a session's pinned MCP tools; empty for a session without pins. */
854
+ function claudeMcpToolNames(pins) {
855
+ return new Set((pins ?? []).map((pin) => `${CLAUDE_MCP_TOOL_PREFIX}${pin.exposedName}`));
856
+ }
857
+ function parseRequest(protocol, rawBody, anthropicBeta, shape, mcpTools) {
764
858
  if (protocol !== "anthropic-messages" && anthropicBeta !== undefined) {
765
859
  throw new CodingProxyError(400, "invalid_anthropic_beta");
766
860
  }
767
861
  try {
768
862
  return protocol === "anthropic-messages"
769
- ? parseAnthropicRequest(rawBody, anthropicBeta, shape)
863
+ ? parseAnthropicRequest(rawBody, anthropicBeta, shape, mcpTools)
770
864
  : parseOpenAiRequest(rawBody);
771
865
  }
772
866
  catch (error) {
@@ -874,6 +968,11 @@ export class CodingProxy {
874
968
  throw new Error("invalid_proxy_deadline");
875
969
  if (!input.credentialRef || input.credentialRef.length > 200)
876
970
  throw new Error("invalid_proxy_credential_reference");
971
+ // Both or neither: pins without a token could never be called, a token without pins lists nothing.
972
+ const mcp = input.mcpTools !== undefined || input.mcpTokenHash !== undefined;
973
+ if (mcp && (!input.mcpTools || input.mcpTools.length === 0 || !input.mcpTokenHash)) {
974
+ throw new Error("invalid_proxy_mcp_tools");
975
+ }
877
976
  const capability = `rrp_${randomBytes(32).toString("base64url")}`;
878
977
  const id = randomUUID();
879
978
  const registryTokenHash = capabilityHash(deriveRegistryToken(capability));
@@ -888,6 +987,7 @@ export class CodingProxy {
888
987
  budgetUsd: input.budgetUsd,
889
988
  registryTokenHash,
890
989
  terms: input.terms,
990
+ ...(mcp ? { mcpTools: input.mcpTools, mcpTokenHash: input.mcpTokenHash } : {}),
891
991
  });
892
992
  this.audit({ type: "session.created", runId: input.runId });
893
993
  return {
@@ -934,7 +1034,7 @@ export class CodingProxy {
934
1034
  };
935
1035
  let parsed;
936
1036
  try {
937
- parsed = parseRequest(input.protocol, input.rawBody, input.anthropicBeta, shape);
1037
+ parsed = parseRequest(input.protocol, input.rawBody, input.anthropicBeta, shape, claudeMcpToolNames(session.mcpTools));
938
1038
  }
939
1039
  catch (error) {
940
1040
  // The session is already authenticated and no credential has been
@@ -14,6 +14,12 @@ import type { ProxyAuditSink } from "./types.js";
14
14
  * JSON). Exported so its header-mapping can be unit-tested directly,
15
15
  * without needing a live upstream. */
16
16
  export declare function createRegistryUpstream(pinned: typeof globalThis.fetch): UpstreamFetch;
17
+ /**
18
+ * WARDBY_MCP_GATEWAY_URL: the native gateway's base URL, which the /mcp route forwards to (its
19
+ * /coding-mcp route). Unset or empty: /mcp answers 503 mcp_unavailable. Anything but an http(s) URL
20
+ * without credentials, query or fragment stops start-up (the value is not echoed).
21
+ */
22
+ export declare function loadMcpGatewayUrl(env: NodeJS.ProcessEnv): string | undefined;
17
23
  export interface CodingProxyRuntimeOptions {
18
24
  db: PrismaClient;
19
25
  /** The package registry's own client, so its traffic (many concurrent
@@ -10,6 +10,7 @@ import { ModelRequestSeal } from "./dependency-cache/model-request-seal.js";
10
10
  import { DependencyCacheService } from "./dependency-cache/service.js";
11
11
  import { startDenyPortListener } from "./deny-port.js";
12
12
  import { EnvironmentCredentialResolver } from "./environment-credentials.js";
13
+ import { createMcpRoute } from "./mcp-route.js";
13
14
  import { createMockUpstream, mockUpstreamFromEnv } from "./mock-upstream.js";
14
15
  import { CodingProxy } from "./proxy.js";
15
16
  import { PrismaProxyLedger } from "./prisma-ledger.js";
@@ -44,6 +45,32 @@ export function createRegistryUpstream(pinned) {
44
45
  },
45
46
  });
46
47
  }
48
+ /**
49
+ * WARDBY_MCP_GATEWAY_URL: the native gateway's base URL, which the /mcp route forwards to (its
50
+ * /coding-mcp route). Unset or empty: /mcp answers 503 mcp_unavailable. Anything but an http(s) URL
51
+ * without credentials, query or fragment stops start-up (the value is not echoed).
52
+ */
53
+ export function loadMcpGatewayUrl(env) {
54
+ const value = env.WARDBY_MCP_GATEWAY_URL?.trim();
55
+ if (!value)
56
+ return undefined;
57
+ let url;
58
+ try {
59
+ url = new URL(value);
60
+ }
61
+ catch {
62
+ url = undefined;
63
+ }
64
+ if (!url ||
65
+ (url.protocol !== "http:" && url.protocol !== "https:") ||
66
+ url.username ||
67
+ url.password ||
68
+ url.search ||
69
+ url.hash) {
70
+ throw new Error("WARDBY_MCP_GATEWAY_URL must be an http(s) URL without credentials, query or fragment");
71
+ }
72
+ return url.href.replace(/\/+$/, "");
73
+ }
47
74
  /** Starts the trusted proxy with the fixed worker-only Docker endpoint, plus the deny port the enforcement witness probes. */
48
75
  export async function startConfiguredCodingProxy(options) {
49
76
  const env = options.env ?? process.env;
@@ -57,6 +84,8 @@ export async function startConfiguredCodingProxy(options) {
57
84
  const audit = mock
58
85
  ? (event) => options.audit?.({ ...event, mockUpstream: true })
59
86
  : options.audit;
87
+ // Throws on an invalid setting, before anything listens.
88
+ const mcpGatewayUrl = loadMcpGatewayUrl(env);
60
89
  // Throws (test_simulators_refused) on any refusal, before anything listens.
61
90
  const simulators = startupTestSimulators("coding-proxy", env);
62
91
  const buildPinnedFetch = options.createPinnedFetch ?? createPinnedProxyFetch;
@@ -123,8 +152,9 @@ export async function startConfiguredCodingProxy(options) {
123
152
  logger.warn({ module: "coding-proxy-runtime", event: "proxy.dependency_cache_no_blob_store" }, "coding proxy: WARDBY_DEPENDENCY_CACHE=true but no BLOB_STORE is configured; the dependency cache is off");
124
153
  }
125
154
  const seal = modelRequestSeal;
155
+ const ledger = new PrismaProxyLedger(options.db);
126
156
  const proxy = new CodingProxy({
127
- ledger: new PrismaProxyLedger(options.db),
157
+ ledger,
128
158
  credentials: new EnvironmentCredentialResolver(env),
129
159
  audit,
130
160
  // WARDBY_TEST_SIMULATORS (test only; refused alongside the mock upstream): the scripted model,
@@ -146,6 +176,7 @@ export async function startConfiguredCodingProxy(options) {
146
176
  expectedHost: `${CODING_PROXY_ALIAS}:${CODING_PROXY_PORT}`,
147
177
  registry,
148
178
  ...(dependencyCache ? { dependencyCache } : {}),
179
+ mcp: createMcpRoute({ ledger, gatewayUrl: mcpGatewayUrl }),
149
180
  onRequest: options.onRequest,
150
181
  });
151
182
  }
@@ -1,3 +1,4 @@
1
+ import { type IncomingMessage, type ServerResponse } from "node:http";
1
2
  import type { ProxyAuditEvent } from "./types.js";
2
3
  import { type CodingProxy } from "./proxy.js";
3
4
  import { type RegistryService } from "./registry/service.js";
@@ -19,6 +20,8 @@ export interface CodingProxyServerConfig {
19
20
  dependencyCache?: Pick<DependencyCacheService, "handle" | "uploadMs">;
20
21
  /** Time to receive a whole request (default HTTP_LIMITS.requestMs); a cache upload gets its upload window instead. */
21
22
  requestMs?: number;
23
+ /** The worker's MCP route (`/mcp`, createMcpRoute); absent, /mcp is not found. */
24
+ mcp?: (request: IncomingMessage, response: ServerResponse) => Promise<void>;
22
25
  onRequest?: (event: {
23
26
  protocol: ProxyProtocol | "other";
24
27
  status: number;
@@ -6,6 +6,7 @@ import { logger } from "../../core/logger.js";
6
6
  import { CodingProxyError, PROXY_MAX_BODY_BYTES } from "./proxy.js";
7
7
  import { RegistryError } from "../../coding/registry/types.js";
8
8
  import { MAX_PLAN_BODY_BYTES } from "./registry/service.js";
9
+ import { MCP_ROUTE_PATH } from "./mcp-route.js";
9
10
  const proxyLog = logger.child({ module: "coding-proxy" });
10
11
  /**
11
12
  * Writes a proxy audit event to the operator log. The events carry ids,
@@ -266,6 +267,15 @@ export async function startCodingProxyServer(proxy, config) {
266
267
  });
267
268
  return;
268
269
  }
270
+ if (request.url === MCP_ROUTE_PATH || request.url?.startsWith(`${MCP_ROUTE_PATH}?`)) {
271
+ if (!config.mcp) {
272
+ closeIfBodyUnread(request, response);
273
+ sendError(response, 404, "not_found");
274
+ return;
275
+ }
276
+ await config.mcp(request, response);
277
+ return;
278
+ }
269
279
  if (request.method === "HEAD" && request.url === "/api/hello") {
270
280
  response.writeHead(200, { "cache-control": "no-store" }).end();
271
281
  return;