@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.
- package/.env.example +5 -5
- package/README.md +6 -1
- package/deploy/local/docker-compose.quickstart-coding.yml +6 -0
- package/dist/claude-coding-worker/driver.d.ts +12 -0
- package/dist/claude-coding-worker/driver.js +29 -7
- package/dist/claude-coding-worker/main.js +5 -1
- package/dist/claude-coding-worker/sdk.js +17 -1
- package/dist/claude-plugin/command.d.ts +61 -0
- package/dist/claude-plugin/command.js +376 -0
- package/dist/cli-help.d.ts +1 -1
- package/dist/cli-help.js +2 -1
- package/dist/cli.js +47 -3
- package/dist/coding/code-host.d.ts +16 -0
- package/dist/coding/code-host.js +60 -0
- package/dist/coding/mcp-token.d.ts +12 -0
- package/dist/coding/mcp-token.js +26 -0
- package/dist/coding/protocol.js +2 -0
- package/dist/coding/redact.js +6 -1
- package/dist/coding/transcript/recorder.d.ts +5 -4
- package/dist/coding/transcript/recorder.js +1 -1
- package/dist/coding-worker/debug-trace.d.ts +3 -1
- package/dist/coding-worker/debug-trace.js +4 -2
- package/dist/coding-worker/driver.js +8 -1
- package/dist/coding-worker/main.js +5 -1
- package/dist/coding-worker/mcp-config.d.ts +27 -0
- package/dist/coding-worker/mcp-config.js +32 -0
- package/dist/coding-worker/sdk.d.ts +36 -0
- package/dist/coding-worker/sdk.js +28 -1
- package/dist/coding-worker/types.d.ts +4 -0
- package/dist/config/mcp-call-limit.d.ts +8 -0
- package/dist/config/mcp-call-limit.js +18 -0
- package/dist/config/mcp-servers.d.ts +8 -0
- package/dist/config/mcp-servers.js +12 -0
- package/dist/config/test-simulators.d.ts +10 -0
- package/dist/config/test-simulators.js +28 -0
- package/dist/config/tool-timeout.d.ts +1 -0
- package/dist/config/tool-timeout.js +18 -0
- package/dist/core/coding-mcp.d.ts +36 -0
- package/dist/core/coding-mcp.js +253 -0
- package/dist/core/coding-repository.d.ts +46 -0
- package/dist/core/coding-repository.js +92 -0
- package/dist/core/delegation-slots.d.ts +27 -0
- package/dist/core/delegation-slots.js +44 -0
- package/dist/core/dispatch.d.ts +6 -2
- package/dist/core/dispatch.js +36 -11
- package/dist/core/error-code.js +1 -0
- package/dist/core/grants.d.ts +28 -3
- package/dist/core/grants.js +46 -4
- package/dist/core/host-events.js +7 -3
- package/dist/core/host-status.js +2 -2
- package/dist/core/issue-status.js +4 -3
- package/dist/core/mark-ready.d.ts +1 -1
- package/dist/core/mark-ready.js +1 -1
- package/dist/core/mcp-client/client.d.ts +47 -0
- package/dist/core/mcp-client/client.js +141 -0
- package/dist/core/mcp-client/jsonrpc.d.ts +15 -0
- package/dist/core/mcp-client/jsonrpc.js +54 -0
- package/dist/core/mcp-client/results.d.ts +13 -0
- package/dist/core/mcp-client/results.js +42 -0
- package/dist/core/mcp-servers.d.ts +90 -0
- package/dist/core/mcp-servers.js +230 -0
- package/dist/core/mcp-tools.d.ts +108 -0
- package/dist/core/mcp-tools.js +287 -0
- package/dist/core/merge-order-check.js +38 -19
- package/dist/core/notification-dispatcher.js +2 -1
- package/dist/core/related-pull-requests.js +26 -10
- package/dist/core/review-fix.js +2 -2
- package/dist/core/review-host-tools.js +3 -1
- package/dist/core/runner.d.ts +66 -16
- package/dist/core/runner.js +281 -73
- package/dist/core/secret-broker-config.d.ts +2 -0
- package/dist/core/secret-broker-config.js +1 -1
- package/dist/core/secret-guard.d.ts +5 -2
- package/dist/core/secret-guard.js +22 -7
- package/dist/core/secrets.d.ts +11 -1
- package/dist/core/secrets.js +27 -1
- package/dist/core/serializable-retry.d.ts +18 -0
- package/dist/core/serializable-retry.js +43 -0
- package/dist/core/tool-admin.d.ts +5 -1
- package/dist/core/tool-admin.js +7 -2
- package/dist/core/tool-names.d.ts +13 -0
- package/dist/core/tool-names.js +20 -0
- package/dist/core/webhooks.d.ts +4 -0
- package/dist/core/webhooks.js +68 -49
- package/dist/core/workflow-run-events.js +2 -1
- package/dist/generated/prisma/browser.d.ts +31 -0
- package/dist/generated/prisma/client.d.ts +31 -0
- package/dist/generated/prisma/commonInputTypes.d.ts +50 -50
- package/dist/generated/prisma/internal/class.d.ts +66 -0
- package/dist/generated/prisma/internal/class.js +4 -4
- package/dist/generated/prisma/internal/prismaNamespace.d.ts +530 -1
- package/dist/generated/prisma/internal/prismaNamespace.js +75 -2
- package/dist/generated/prisma/internal/prismaNamespaceBrowser.d.ts +79 -0
- package/dist/generated/prisma/internal/prismaNamespaceBrowser.js +75 -2
- package/dist/generated/prisma/models/Agent.d.ts +271 -0
- package/dist/generated/prisma/models/AgentMcpServer.d.ts +1412 -0
- package/dist/generated/prisma/models/AgentMcpServer.js +1 -0
- package/dist/generated/prisma/models/AgentRepository.d.ts +40 -1
- package/dist/generated/prisma/models/AuthUser.d.ts +1 -1
- package/dist/generated/prisma/models/CodingAgentProfile.d.ts +0 -7
- package/dist/generated/prisma/models/CodingProxySession.d.ts +119 -2
- package/dist/generated/prisma/models/CodingRun.d.ts +76 -1
- package/dist/generated/prisma/models/HostProvider.d.ts +2176 -0
- package/dist/generated/prisma/models/HostProvider.js +1 -0
- package/dist/generated/prisma/models/HostProviderCredential.d.ts +1285 -0
- package/dist/generated/prisma/models/HostProviderCredential.js +1 -0
- package/dist/generated/prisma/models/HostProviderOperation.d.ts +1470 -0
- package/dist/generated/prisma/models/HostProviderOperation.js +1 -0
- package/dist/generated/prisma/models/HostProviderTrustedAccount.d.ts +1145 -0
- package/dist/generated/prisma/models/HostProviderTrustedAccount.js +1 -0
- package/dist/generated/prisma/models/McpServer.d.ts +1500 -0
- package/dist/generated/prisma/models/McpServer.js +1 -0
- package/dist/generated/prisma/models/NativeGatewayCall.d.ts +39 -1
- package/dist/generated/prisma/models/Principal.d.ts +167 -0
- package/dist/generated/prisma/models/ResourceGrant.d.ts +1 -1
- package/dist/generated/prisma/models/Secret.d.ts +387 -0
- package/dist/generated/prisma/models/Tool.d.ts +223 -1
- package/dist/generated/prisma/models.d.ts +6 -0
- package/dist/help/search.js +12 -3
- package/dist/help-index.json +719 -47
- package/dist/import/create.js +16 -1
- package/dist/import/neutral-schema.d.ts +1 -0
- package/dist/import/neutral-schema.js +2 -0
- package/dist/mcp/auth/grants-cli.d.ts +6 -4
- package/dist/mcp/auth/grants-cli.js +42 -3
- package/dist/mcp/auth/resource-server.d.ts +3 -1
- package/dist/mcp/auth/resource-server.js +17 -2
- package/dist/mcp/context.d.ts +3 -0
- package/dist/mcp/index.js +11 -0
- package/dist/mcp/tools/agents.js +100 -15
- package/dist/mcp/tools/grants.js +66 -28
- package/dist/mcp/tools/host-providers.d.ts +2 -0
- package/dist/mcp/tools/host-providers.js +215 -0
- package/dist/mcp/tools/mcp-servers.d.ts +26 -0
- package/dist/mcp/tools/mcp-servers.js +481 -0
- package/dist/mcp/tools/repositories.js +50 -10
- package/dist/mcp/tools/runs.js +178 -18
- package/dist/mcp/tools/secret-elicitation.d.ts +2 -0
- package/dist/mcp/tools/secret-elicitation.js +25 -2
- package/dist/mcp/tools/secrets.js +38 -4
- package/dist/mcp/tools/tools.d.ts +6 -4
- package/dist/mcp/tools/tools.js +40 -8
- package/dist/mcp/tools/trigger.js +28 -6
- package/dist/mcp/transport/http-limits.d.ts +2 -0
- package/dist/mcp/transport/http-limits.js +6 -2
- package/dist/mcp/webhooks/ingress.js +10 -0
- package/dist/native-worker/gateway-server.d.ts +14 -0
- package/dist/native-worker/gateway-server.js +28 -7
- package/dist/native-worker/gateway.d.ts +2 -0
- package/dist/native-worker/gateway.js +5 -1
- package/dist/native-worker/http-server.d.ts +4 -0
- package/dist/native-worker/http-server.js +51 -0
- package/dist/native-worker/ledger.d.ts +6 -2
- package/dist/native-worker/ledger.js +5 -4
- package/dist/native-worker/protocol.d.ts +1 -0
- package/dist/native-worker/protocol.js +4 -1
- package/dist/providers/coding-proxy/capability-hash.d.ts +2 -0
- package/dist/providers/coding-proxy/capability-hash.js +5 -0
- package/dist/providers/coding-proxy/mcp-route.d.ts +34 -0
- package/dist/providers/coding-proxy/mcp-route.js +166 -0
- package/dist/providers/coding-proxy/memory-ledger.d.ts +2 -0
- package/dist/providers/coding-proxy/memory-ledger.js +15 -0
- package/dist/providers/coding-proxy/prisma-ledger.d.ts +1 -0
- package/dist/providers/coding-proxy/prisma-ledger.js +30 -2
- package/dist/providers/coding-proxy/proxy.d.ts +7 -1
- package/dist/providers/coding-proxy/proxy.js +134 -34
- package/dist/providers/coding-proxy/runtime.d.ts +6 -0
- package/dist/providers/coding-proxy/runtime.js +32 -1
- package/dist/providers/coding-proxy/server.d.ts +3 -0
- package/dist/providers/coding-proxy/server.js +10 -0
- package/dist/providers/coding-proxy/types.d.ts +13 -0
- package/dist/providers/executor/composition.js +5 -0
- package/dist/providers/executor/container.d.ts +33 -1
- package/dist/providers/executor/container.js +76 -7
- package/dist/providers/host-tools/invoke.d.ts +37 -0
- package/dist/providers/host-tools/invoke.js +117 -0
- package/dist/providers/host-tools/operations.d.ts +65 -0
- package/dist/providers/host-tools/operations.js +149 -0
- package/dist/providers/host-tools/registry.d.ts +79 -0
- package/dist/providers/host-tools/registry.js +362 -0
- package/dist/providers/host-tools/request-guard.d.ts +17 -0
- package/dist/providers/host-tools/request-guard.js +99 -0
- package/dist/providers/host-tools/self-test.d.ts +64 -0
- package/dist/providers/host-tools/self-test.js +142 -0
- package/dist/providers/jobs/docker-isolation.d.ts +2 -2
- package/dist/providers/jobs/docker-isolation.js +30 -8
- package/dist/providers/jobs/docker.d.ts +8 -0
- package/dist/providers/jobs/docker.js +33 -9
- package/dist/providers/jobs/kubernetes-isolation.d.ts +3 -1
- package/dist/providers/jobs/kubernetes-isolation.js +15 -2
- package/dist/providers/jobs/kubernetes.d.ts +5 -0
- package/dist/providers/jobs/kubernetes.js +6 -1
- package/dist/providers/jobs/types.d.ts +6 -0
- package/dist/providers/review-host/types.d.ts +6 -4
- package/dist/providers/review-host/types.js +1 -1
- package/dist/providers/vcs/git.d.ts +6 -2
- package/dist/providers/vcs/git.js +15 -4
- package/dist/providers/vcs/github.d.ts +2 -71
- package/dist/providers/vcs/github.js +8 -11
- package/dist/providers/vcs/index.d.ts +1 -1
- package/dist/providers/vcs/index.js +1 -1
- package/dist/providers/vcs/remote.d.ts +4 -0
- package/dist/providers/vcs/remote.js +7 -1
- package/dist/providers/vcs/routing.d.ts +12 -6
- package/dist/providers/vcs/routing.js +21 -6
- package/dist/providers/vcs/types.d.ts +71 -3
- package/dist/quickstart/index.d.ts +5 -0
- package/dist/quickstart/index.js +21 -0
- package/dist/quickstart-images.json +1 -1
- package/dist/sandbox/eval-core.d.ts +12 -2
- package/dist/sandbox/eval-core.js +20 -4
- package/dist/sandbox/host-functions.d.ts +4 -0
- package/dist/sandbox/host-functions.js +7 -2
- package/dist/sandbox/limits.d.ts +4 -1
- package/dist/sandbox/limits.js +4 -1
- package/dist/sandbox/run-in-sandbox.d.ts +6 -0
- package/dist/sandbox/run-in-sandbox.js +14 -2
- package/dist/sandbox/safe-fetch.d.ts +2 -0
- package/dist/sandbox/safe-fetch.js +1 -1
- package/dist/sandbox/secret-broker.d.ts +1 -0
- package/dist/sandbox/secret-broker.js +1 -1
- package/dist/sandbox/tool-timeout.d.ts +32 -0
- package/dist/sandbox/tool-timeout.js +52 -0
- package/dist/sandbox/user-tool.d.ts +8 -1
- package/dist/sandbox/user-tool.js +9 -3
- package/dist/viewer/graph.js +2 -1
- package/dist/wardby-bin.js +5 -0
- package/docs/README.md +11 -1
- package/docs/agent-recipes.md +37 -4
- package/docs/architecture-runtime.md +1 -1
- package/docs/claude-code-plugin.md +164 -0
- package/docs/code-review-agents.md +6 -0
- package/docs/coding-agent-setup.md +77 -1
- package/docs/coding-worker-byo-images.md +4 -2
- package/docs/coding-worker-isolation.md +78 -7
- package/docs/debugging-agent-runs.md +242 -0
- package/docs/getting-started-gke.md +5 -4
- package/docs/getting-started-identity-provider.md +43 -37
- package/docs/getting-started.md +8 -0
- package/docs/host-providers.md +294 -0
- package/docs/mcp-servers.md +254 -0
- package/docs/native-sandbox.md +25 -9
- package/docs/security-deployment.md +110 -31
- package/docs/tool-secrets.md +3 -0
- package/docs/viewer-api.md +6 -0
- package/docs/viewer-api.openapi.yml +1 -1
- package/help/admin-viewer.md +4 -0
- package/help/agent-recipes.md +33 -2
- package/help/brokered-secrets.md +3 -0
- package/help/build-claude-code-image.md +1 -1
- package/help/claude-code-run-pane.md +40 -0
- package/help/errors/coding-mcp-pins-unavailable.md +35 -0
- package/help/errors/database-auth-invalid.md +1 -1
- package/help/errors/host-provider-error.md +40 -0
- package/help/errors/host-tool-failed.md +36 -0
- package/help/errors/mcp-call-limit.md +22 -0
- package/help/errors/mcp-call-timeout.md +22 -0
- package/help/errors/mcp-server-auth-failed.md +22 -0
- package/help/errors/mcp-server-credentials-unavailable.md +45 -0
- package/help/errors/mcp-server-error.md +28 -0
- package/help/errors/mcp-server-unreachable.md +22 -0
- package/help/errors/mcp-servers-off.md +21 -0
- package/help/errors/mcp-tool-not-allowed.md +26 -0
- package/help/errors/mcp-tool-removed.md +22 -0
- package/help/errors/mcp-unavailable.md +25 -0
- package/help/errors/native-sandbox-capacity.md +1 -1
- package/help/errors/native-sandbox-deadline-exceeded.md +1 -1
- package/help/errors/native-sandbox-docker-failed.md +1 -1
- package/help/errors/native-sandbox-gateway-error.md +1 -1
- package/help/errors/native-sandbox-gateway-unavailable.md +1 -1
- package/help/errors/native-sandbox-image-not-pinned.md +1 -1
- package/help/errors/native-sandbox-isolation-mismatch.md +1 -1
- package/help/errors/native-sandbox-network-unenforced.md +1 -1
- package/help/errors/native-sandbox-requires-catalog.md +1 -1
- package/help/errors/native-sandbox-unavailable.md +1 -1
- package/help/errors/native-sandbox-warm-delivery-failed.md +1 -1
- package/help/errors/native-sandbox-worker-exited.md +1 -1
- package/help/errors/native-sandbox-worker-lost.md +1 -1
- package/help/errors/native-sandbox-worker-unready.md +1 -1
- package/help/errors/repository-not-listed.md +36 -0
- package/help/errors/repository-required.md +36 -0
- package/help/errors/tool-timeout.md +38 -0
- package/help/host-providers.md +40 -0
- package/help/identity-and-access.md +4 -1
- package/help/mcp-servers.md +73 -0
- package/help/mcp.md +7 -2
- package/help/native-capabilities.md +27 -1
- package/help/native-sandbox.md +1 -1
- package/help/operating-agents.md +16 -0
- package/help/related-pull-requests.md +7 -3
- package/help/repo-instructions-and-skills.md +3 -1
- package/help/resume-runs-locally.md +3 -0
- package/help/stop-run.md +1 -1
- package/help/troubleshooting/coding-workers.md +9 -1
- package/help/troubleshooting/debugging-agent-runs.md +29 -0
- package/package.json +2 -1
- package/prisma/migrations/20261102000000_host_providers/migration.sql +56 -0
- package/prisma/migrations/20261104000000_coding_agent_repository_list/migration.sql +5 -0
- package/prisma/migrations/20261106000000_tool_timeout/migration.sql +4 -0
- package/prisma/migrations/20261107000000_mcp_servers/migration.sql +33 -0
- package/prisma/migrations/20261108000000_coding_mcp/migration.sql +5 -0
- package/prisma/migrations/20261109000000_task_run_index/migration.sql +3 -0
- package/prisma/schema.prisma +159 -14
- 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", "
|
|
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
|
|
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 {
|
|
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
|
|
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,
|
|
59
|
-
// search, remote MCP, code interpreter, image
|
|
60
|
-
// remote image or file, a stored prompt, or a
|
|
61
|
-
// OpenAI fetch or run something outside the
|
|
62
|
-
// the token-metered budget. Tool names vary
|
|
63
|
-
// are checked for shape, not against a
|
|
64
|
-
//
|
|
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,
|
|
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
|
-
|
|
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 (++
|
|
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" &&
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
442
|
+
const distinctTools = new Set();
|
|
376
443
|
if (body.tools !== undefined)
|
|
377
|
-
validateOpenAiTools(body.tools,
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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;
|