@wardby/cli 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +5 -5
- package/README.md +6 -1
- package/deploy/local/docker-compose.quickstart-coding.yml +11 -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 +64 -0
- package/dist/claude-plugin/command.js +379 -0
- package/dist/cli-help.d.ts +1 -1
- package/dist/cli-help.js +2 -1
- package/dist/cli.js +71 -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 +3 -9
- 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 +39 -11
- package/dist/core/engine-native.d.ts +2 -0
- package/dist/core/engine-native.js +13 -0
- package/dist/core/error-code.js +2 -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/listen-address.d.ts +10 -0
- package/dist/core/listen-address.js +14 -0
- 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/power-idle.d.ts +8 -2
- package/dist/core/power-idle.js +17 -3
- package/dist/core/power.d.ts +5 -0
- package/dist/core/power.js +5 -0
- package/dist/core/related-pull-requests.js +26 -10
- package/dist/core/retention-days.d.ts +5 -0
- package/dist/core/retention-days.js +14 -0
- package/dist/core/review-fix.js +2 -2
- package/dist/core/review-host-tools.js +3 -1
- package/dist/core/run-steps/config.d.ts +25 -0
- package/dist/core/run-steps/config.js +40 -0
- package/dist/core/run-steps/control-chars.d.ts +16 -0
- package/dist/core/run-steps/control-chars.js +26 -0
- package/dist/core/run-steps/detail.d.ts +32 -0
- package/dist/core/run-steps/detail.js +240 -0
- package/dist/core/run-steps/extract.d.ts +52 -0
- package/dist/core/run-steps/extract.js +467 -0
- package/dist/core/run-steps/ingress.d.ts +51 -0
- package/dist/core/run-steps/ingress.js +160 -0
- package/dist/core/run-steps/last-step.d.ts +15 -0
- package/dist/core/run-steps/last-step.js +35 -0
- package/dist/core/run-steps/native.d.ts +48 -0
- package/dist/core/run-steps/native.js +164 -0
- package/dist/core/run-steps/seen.d.ts +16 -0
- package/dist/core/run-steps/seen.js +35 -0
- package/dist/core/run-steps/summarize.d.ts +57 -0
- package/dist/core/run-steps/summarize.js +304 -0
- package/dist/core/run-steps/sweep.d.ts +21 -0
- package/dist/core/run-steps/sweep.js +41 -0
- package/dist/core/run-steps/types.d.ts +89 -0
- package/dist/core/run-steps/types.js +39 -0
- package/dist/core/run-steps/wire.d.ts +35 -0
- package/dist/core/run-steps/wire.js +47 -0
- package/dist/core/run-steps/writer.d.ts +49 -0
- package/dist/core/run-steps/writer.js +279 -0
- package/dist/core/runner.d.ts +70 -16
- package/dist/core/runner.js +296 -74
- 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 +39 -0
- package/dist/generated/prisma/client.d.ts +39 -0
- package/dist/generated/prisma/commonInputTypes.d.ts +80 -50
- package/dist/generated/prisma/enums.d.ts +6 -0
- package/dist/generated/prisma/enums.js +5 -0
- package/dist/generated/prisma/internal/class.d.ts +77 -0
- package/dist/generated/prisma/internal/class.js +4 -4
- package/dist/generated/prisma/internal/prismaNamespace.d.ts +632 -1
- package/dist/generated/prisma/internal/prismaNamespace.js +94 -3
- package/dist/generated/prisma/internal/prismaNamespaceBrowser.d.ts +98 -0
- package/dist/generated/prisma/internal/prismaNamespaceBrowser.js +94 -3
- package/dist/generated/prisma/models/Agent.d.ts +372 -1
- 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 +159 -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/Run.d.ts +503 -1
- package/dist/generated/prisma/models/RunStep.d.ts +1453 -0
- package/dist/generated/prisma/models/RunStep.js +1 -0
- 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 +7 -0
- package/dist/help/search.js +12 -3
- package/dist/help-index.json +812 -52
- 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/access.d.ts +1 -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 +6 -0
- package/dist/mcp/index.js +21 -0
- package/dist/mcp/tools/agents.js +135 -20
- 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/power.js +2 -2
- package/dist/mcp/tools/repositories.js +50 -10
- package/dist/mcp/tools/run-access.d.ts +7 -0
- package/dist/mcp/tools/run-access.js +14 -0
- package/dist/mcp/tools/run-steps.d.ts +16 -0
- package/dist/mcp/tools/run-steps.js +85 -0
- package/dist/mcp/tools/runs.js +186 -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 +18 -1
- package/dist/native-worker/gateway-server.js +38 -9
- package/dist/native-worker/gateway.d.ts +5 -0
- package/dist/native-worker/gateway.js +13 -1
- package/dist/native-worker/http-server.d.ts +6 -4
- package/dist/native-worker/http-server.js +53 -8
- 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 +16 -0
- package/dist/providers/coding-proxy/prisma-ledger.d.ts +1 -0
- package/dist/providers/coding-proxy/prisma-ledger.js +36 -2
- package/dist/providers/coding-proxy/proxy.d.ts +33 -1
- package/dist/providers/coding-proxy/proxy.js +266 -75
- package/dist/providers/coding-proxy/run-step-forwarder.d.ts +42 -0
- package/dist/providers/coding-proxy/run-step-forwarder.js +206 -0
- package/dist/providers/coding-proxy/runtime.d.ts +13 -0
- package/dist/providers/coding-proxy/runtime.js +87 -4
- 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 +18 -0
- package/dist/providers/engine/types.d.ts +26 -0
- package/dist/providers/executor/composition.js +5 -0
- package/dist/providers/executor/container.d.ts +47 -1
- package/dist/providers/executor/container.js +84 -8
- 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 +13 -0
- package/dist/providers/jobs/kubernetes.js +28 -5
- 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/serve.js +26 -18
- package/dist/viewer/api-schema.d.ts +66 -0
- package/dist/viewer/api-schema.js +31 -0
- package/dist/viewer/graph.js +2 -1
- package/dist/viewer/openapi.js +2 -1
- package/dist/viewer/run-detail.d.ts +2 -0
- package/dist/viewer/run-detail.js +26 -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 +208 -0
- package/docs/code-review-agents.md +6 -0
- package/docs/coding-agent-setup.md +94 -1
- package/docs/coding-worker-byo-images.md +4 -2
- package/docs/coding-worker-isolation.md +94 -7
- package/docs/debugging-agent-runs.md +354 -0
- package/docs/getting-started-gke.md +33 -11
- 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 +263 -0
- package/docs/native-sandbox.md +31 -9
- package/docs/security-deployment.md +161 -31
- package/docs/tool-secrets.md +3 -0
- package/docs/viewer-api.md +27 -12
- package/docs/viewer-api.openapi.yml +78 -2
- package/help/admin-viewer.md +5 -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 +63 -0
- package/help/deploy-gke.md +5 -0
- package/help/errors/coding-mcp-gateway-unavailable.md +46 -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/low-power-mode.md +19 -8
- package/help/mcp-servers.md +73 -0
- package/help/mcp.md +9 -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 +7 -0
- package/help/run-steps.md +140 -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/migrations/20261110000000_run_steps/migration.sql +26 -0
- package/prisma/migrations/20261111000000_run_steps_viewer_notify/migration.sql +59 -0
- package/prisma/migrations/20261112000000_run_steps_detail/migration.sql +9 -0
- package/prisma/schema.prisma +211 -14
- package/scripts/git-askpass.sh +1 -1
|
@@ -1,11 +1,17 @@
|
|
|
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";
|
|
8
10
|
import { createPinnedProxyFetch } from "./secure-fetch.js";
|
|
11
|
+
import { resultsFromRequest, stepsFromJson, streamStepExtractor, } from "../../core/run-steps/extract.js";
|
|
12
|
+
import { RUN_STEPS_LEVELS } from "../../core/run-steps/config.js";
|
|
13
|
+
import { SeenIds } from "../../core/run-steps/seen.js";
|
|
14
|
+
import { CLAUDE_RESULT_TOOL } from "../../core/run-steps/summarize.js";
|
|
9
15
|
export const PROXY_MAX_BODY_BYTES = 8 * 1024 * 1024;
|
|
10
16
|
export const PROXY_MAX_OUTPUT_TOKENS = 1_000_000;
|
|
11
17
|
export const PROXY_DEFAULT_MAX_OUTPUT_TOKENS = 4_096;
|
|
@@ -30,7 +36,7 @@ const REQUEST_KEY_PATTERN = /^[\x21-\x7e]{1,200}$/;
|
|
|
30
36
|
export const OPTIONAL_ANTHROPIC_BETAS = ["per-turn-control-2026-07-01"];
|
|
31
37
|
const APPROVED_ANTHROPIC_BETAS = new Set([...CLAUDE_CODE_ANTHROPIC_BETAS, ...OPTIONAL_ANTHROPIC_BETAS]);
|
|
32
38
|
const WARDBY_COMMAND_TOOL = "mcp__wardby_tools__run_command";
|
|
33
|
-
const STRUCTURED_OUTPUT_TOOL =
|
|
39
|
+
const STRUCTURED_OUTPUT_TOOL = CLAUDE_RESULT_TOOL;
|
|
34
40
|
const MAX_COMMAND_BYTES = 16 * 1024;
|
|
35
41
|
const MAX_TOOL_TEXT_BYTES = 256 * 1024;
|
|
36
42
|
/**
|
|
@@ -48,20 +54,26 @@ export class CodingProxyError extends Error {
|
|
|
48
54
|
this.code = code;
|
|
49
55
|
}
|
|
50
56
|
}
|
|
51
|
-
|
|
52
|
-
|
|
57
|
+
/** What this session's replies and requests are read for. */
|
|
58
|
+
function stepCapture(session, ceiling) {
|
|
59
|
+
const level = session.runStepsLevel ?? "summary";
|
|
60
|
+
if (level === "off" || ceiling === "off")
|
|
61
|
+
return "none";
|
|
62
|
+
return level === "detail" && ceiling === "detail" ? "detail" : "summary";
|
|
53
63
|
}
|
|
64
|
+
export { capabilityHash };
|
|
54
65
|
// OpenAI Responses allowlist. Everything a request may carry is listed here;
|
|
55
66
|
// anything else is refused before a credential is resolved. It mirrors what the
|
|
56
67
|
// pinned Codex CLI (src/coding-worker/package.json) actually sends, recorded in
|
|
57
68
|
// 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
|
-
//
|
|
69
|
+
// (function, custom, namespaces of them, and the client-executed tool_search)
|
|
70
|
+
// are allowed: a hosted tool (web search, remote MCP, code interpreter, image
|
|
71
|
+
// generation, file search, ...), a remote image or file, a stored prompt, or a
|
|
72
|
+
// previous response would make OpenAI fetch or run something outside the
|
|
73
|
+
// worker's egress policy and outside the token-metered budget. Tool names vary
|
|
74
|
+
// by model and Codex version, so they are checked for shape, not against a
|
|
75
|
+
// fixed list: a client-side tool the model calls is executed by the worker
|
|
76
|
+
// itself, which the container boundary governs.
|
|
65
77
|
const OPENAI_REQUEST_KEYS = new Set([
|
|
66
78
|
"model",
|
|
67
79
|
"instructions",
|
|
@@ -151,13 +163,32 @@ function openAiToolName(value, code = "invalid_openai_request") {
|
|
|
151
163
|
if (typeof value !== "string" || !OPENAI_TOOL_NAME.test(value))
|
|
152
164
|
throw new CodingProxyError(400, code);
|
|
153
165
|
}
|
|
154
|
-
function validateOpenAiFunctionTool(tool) {
|
|
155
|
-
openAiOnlyKeys(tool,
|
|
166
|
+
function validateOpenAiFunctionTool(tool, inSearchOutput) {
|
|
167
|
+
openAiOnlyKeys(tool, inSearchOutput
|
|
168
|
+
? ["type", "name", "description", "strict", "defer_loading", "parameters"]
|
|
169
|
+
: ["type", "name", "description", "strict", "parameters"], "invalid_openai_tool");
|
|
156
170
|
openAiToolName(tool.name, "invalid_openai_tool");
|
|
157
171
|
openAiOptional(tool.description, (value) => openAiString(value, MAX_TOOL_TEXT_BYTES, "invalid_openai_tool"));
|
|
158
172
|
if (tool.strict !== undefined && typeof tool.strict !== "boolean") {
|
|
159
173
|
throw new CodingProxyError(400, "invalid_openai_tool");
|
|
160
174
|
}
|
|
175
|
+
// Codex marks the tools a tool_search_output loads with defer_loading: true,
|
|
176
|
+
// and sends it nowhere else. It only delays when the model sees the
|
|
177
|
+
// definition, which is checked in full.
|
|
178
|
+
if (tool.defer_loading !== undefined && tool.defer_loading !== true) {
|
|
179
|
+
throw new CodingProxyError(400, "invalid_openai_tool");
|
|
180
|
+
}
|
|
181
|
+
openAiOptional(tool.parameters, (value) => openAiRecord(value, "invalid_openai_tool"));
|
|
182
|
+
}
|
|
183
|
+
// The tool_search tool Codex sends for models whose metadata asks for it. Only
|
|
184
|
+
// client execution: Codex runs the search over its own deferred tools and
|
|
185
|
+
// returns them in a tool_search_output item. A server-executed search would
|
|
186
|
+
// have OpenAI run it.
|
|
187
|
+
function validateOpenAiToolSearchTool(tool) {
|
|
188
|
+
openAiOnlyKeys(tool, ["type", "execution", "description", "parameters"], "invalid_openai_tool");
|
|
189
|
+
if (tool.execution !== "client")
|
|
190
|
+
throw new CodingProxyError(400, "openai_tool_not_allowed:tool_search");
|
|
191
|
+
openAiOptional(tool.description, (value) => openAiString(value, MAX_TOOL_TEXT_BYTES, "invalid_openai_tool"));
|
|
161
192
|
openAiOptional(tool.parameters, (value) => openAiRecord(value, "invalid_openai_tool"));
|
|
162
193
|
}
|
|
163
194
|
function validateOpenAiCustomTool(tool) {
|
|
@@ -175,30 +206,57 @@ function validateOpenAiCustomTool(tool) {
|
|
|
175
206
|
}
|
|
176
207
|
openAiString(format.definition, MAX_TOOL_TEXT_BYTES, "invalid_openai_tool");
|
|
177
208
|
}
|
|
178
|
-
|
|
209
|
+
// Every tool list (the request's tools, each additional_tools item, each
|
|
210
|
+
// tool_search_output item) is capped at MAX_OPENAI_TOOLS entries on its own.
|
|
211
|
+
// Across the request, `distinct` holds the `namespace/name` of each callable
|
|
212
|
+
// tool and is capped too. Codex replays every earlier tool_search_output on
|
|
213
|
+
// each turn, so a long run repeats the same definitions many times: each copy
|
|
214
|
+
// is still validated, but only a tool not seen before adds to the shared cap.
|
|
215
|
+
function validateOpenAiTools(value, distinct, { toolSearch = false, inSearchOutput = false, namespace, listed = { value: 0 }, } = {}) {
|
|
179
216
|
if (!Array.isArray(value))
|
|
180
217
|
throw new CodingProxyError(400, "invalid_openai_tool");
|
|
218
|
+
const callable = (name) => {
|
|
219
|
+
distinct.add(`${namespace ?? ""}/${name}`);
|
|
220
|
+
if (distinct.size > MAX_OPENAI_TOOLS)
|
|
221
|
+
throw new CodingProxyError(400, "invalid_openai_tool");
|
|
222
|
+
};
|
|
181
223
|
for (const candidate of value) {
|
|
182
|
-
if (++
|
|
224
|
+
if (++listed.value > MAX_OPENAI_TOOLS)
|
|
183
225
|
throw new CodingProxyError(400, "invalid_openai_tool");
|
|
184
226
|
const tool = openAiRecord(candidate, "invalid_openai_tool");
|
|
185
227
|
if (tool.type === "function") {
|
|
186
|
-
validateOpenAiFunctionTool(tool);
|
|
228
|
+
validateOpenAiFunctionTool(tool, inSearchOutput);
|
|
229
|
+
callable(tool.name);
|
|
187
230
|
}
|
|
188
231
|
else if (tool.type === "custom") {
|
|
189
232
|
validateOpenAiCustomTool(tool);
|
|
233
|
+
callable(tool.name);
|
|
190
234
|
}
|
|
191
|
-
else if (tool.type === "namespace" &&
|
|
235
|
+
else if (tool.type === "namespace" && namespace === undefined) {
|
|
192
236
|
openAiOnlyKeys(tool, ["type", "name", "description", "tools"], "invalid_openai_tool");
|
|
193
237
|
openAiToolName(tool.name, "invalid_openai_tool");
|
|
194
238
|
openAiOptional(tool.description, (value) => openAiString(value, MAX_TOOL_TEXT_BYTES, "invalid_openai_tool"));
|
|
195
|
-
validateOpenAiTools(tool.tools,
|
|
239
|
+
validateOpenAiTools(tool.tools, distinct, { inSearchOutput, namespace: tool.name, listed });
|
|
240
|
+
}
|
|
241
|
+
else if (tool.type === "tool_search" && toolSearch) {
|
|
242
|
+
validateOpenAiToolSearchTool(tool);
|
|
243
|
+
callable("tool_search");
|
|
196
244
|
}
|
|
197
245
|
else {
|
|
198
246
|
throw new CodingProxyError(400, openAiCode("openai_tool_not_allowed", tool.type));
|
|
199
247
|
}
|
|
200
248
|
}
|
|
201
249
|
}
|
|
250
|
+
// The arguments the model gave the client-side tool_search, as its parameters
|
|
251
|
+
// in the captures declare them: a query and an optional result limit.
|
|
252
|
+
function validateOpenAiToolSearchArguments(value) {
|
|
253
|
+
const args = openAiRecord(value);
|
|
254
|
+
openAiOnlyKeys(args, ["query", "limit"]);
|
|
255
|
+
openAiString(args.query, MAX_TOOL_TEXT_BYTES);
|
|
256
|
+
if (args.limit !== undefined && !(Number.isSafeInteger(args.limit) && args.limit > 0)) {
|
|
257
|
+
throw new CodingProxyError(400, "invalid_openai_request");
|
|
258
|
+
}
|
|
259
|
+
}
|
|
202
260
|
function validateOpenAiContentPart(value, allowed) {
|
|
203
261
|
const part = openAiRecord(value);
|
|
204
262
|
if (typeof part.type !== "string" || !allowed.has(part.type)) {
|
|
@@ -241,7 +299,7 @@ function validateOpenAiTextParts(value, type) {
|
|
|
241
299
|
openAiString(part.text, PROXY_MAX_BODY_BYTES);
|
|
242
300
|
}
|
|
243
301
|
}
|
|
244
|
-
function validateOpenAiInputItem(value,
|
|
302
|
+
function validateOpenAiInputItem(value, distinctTools) {
|
|
245
303
|
const item = openAiRecord(value);
|
|
246
304
|
switch (item.type) {
|
|
247
305
|
case undefined:
|
|
@@ -300,12 +358,32 @@ function validateOpenAiInputItem(value, toolCount) {
|
|
|
300
358
|
openAiOptional(item.recipient, (value) => openAiString(value, MAX_OPENAI_ID_LENGTH));
|
|
301
359
|
validateOpenAiContent(item.content, OPENAI_AGENT_MESSAGE_PARTS, false);
|
|
302
360
|
return;
|
|
361
|
+
case "tool_search_call":
|
|
362
|
+
openAiOnlyKeys(item, ["type", "id", "call_id", "status", "execution", "arguments"]);
|
|
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
|
+
validateOpenAiToolSearchArguments(item.arguments);
|
|
369
|
+
return;
|
|
370
|
+
case "tool_search_output":
|
|
371
|
+
// The tools the client-side search loaded: each one passes the same checks
|
|
372
|
+
// as a top-level tool, so this cannot bring in a shape the proxy refuses.
|
|
373
|
+
openAiOnlyKeys(item, ["type", "id", "call_id", "status", "execution", "tools"]);
|
|
374
|
+
openAiOptional(item.id, openAiId);
|
|
375
|
+
openAiId(item.call_id);
|
|
376
|
+
openAiOptional(item.status, (value) => openAiString(value, 64));
|
|
377
|
+
if (item.execution !== "client")
|
|
378
|
+
throw new CodingProxyError(400, openAiCode("openai_input_not_allowed", item.type));
|
|
379
|
+
validateOpenAiTools(item.tools, distinctTools, { inSearchOutput: true });
|
|
380
|
+
return;
|
|
303
381
|
case "additional_tools":
|
|
304
382
|
openAiOnlyKeys(item, ["type", "id", "role", "tools"]);
|
|
305
383
|
openAiOptional(item.id, openAiId);
|
|
306
384
|
if (item.role !== undefined && item.role !== "developer")
|
|
307
385
|
throw new CodingProxyError(400, "invalid_openai_request");
|
|
308
|
-
validateOpenAiTools(item.tools,
|
|
386
|
+
validateOpenAiTools(item.tools, distinctTools);
|
|
309
387
|
return;
|
|
310
388
|
default:
|
|
311
389
|
throw new CodingProxyError(400, openAiCode("openai_input_not_allowed", item.type));
|
|
@@ -372,9 +450,9 @@ function validateOpenAiBody(body) {
|
|
|
372
450
|
throw new CodingProxyError(400, "invalid_openai_request");
|
|
373
451
|
}
|
|
374
452
|
openAiOptional(body.instructions, (value) => openAiString(value, PROXY_MAX_BODY_BYTES));
|
|
375
|
-
const
|
|
453
|
+
const distinctTools = new Set();
|
|
376
454
|
if (body.tools !== undefined)
|
|
377
|
-
validateOpenAiTools(body.tools,
|
|
455
|
+
validateOpenAiTools(body.tools, distinctTools, { toolSearch: true });
|
|
378
456
|
if (body.tool_choice !== undefined) {
|
|
379
457
|
if (typeof body.tool_choice !== "string" || !OPENAI_TOOL_CHOICES.has(body.tool_choice)) {
|
|
380
458
|
throw new CodingProxyError(400, "openai_tool_choice_not_allowed");
|
|
@@ -401,7 +479,7 @@ function validateOpenAiBody(body) {
|
|
|
401
479
|
throw new CodingProxyError(400, "invalid_openai_request");
|
|
402
480
|
}
|
|
403
481
|
for (const item of body.input)
|
|
404
|
-
validateOpenAiInputItem(item,
|
|
482
|
+
validateOpenAiInputItem(item, distinctTools);
|
|
405
483
|
}
|
|
406
484
|
function parseOpenAiRequest(rawBody) {
|
|
407
485
|
if (Buffer.byteLength(rawBody) > PROXY_MAX_BODY_BYTES)
|
|
@@ -516,15 +594,33 @@ function validateStructuredOutputTool(value) {
|
|
|
516
594
|
if (tool.cache_control !== undefined)
|
|
517
595
|
validateCacheControl(tool.cache_control);
|
|
518
596
|
}
|
|
519
|
-
|
|
597
|
+
/**
|
|
598
|
+
* One of the run's pinned MCP tools, as Claude Code advertises it: a plain client tool named
|
|
599
|
+
* `mcp__wardby_mcp__<exposed name>` (fixtures/claude-code-2.1.286-mcp-requests.json). Only the name
|
|
600
|
+
* is checked against the pins; the gateway checks the call itself again.
|
|
601
|
+
*/
|
|
602
|
+
function validateMcpTool(tool) {
|
|
603
|
+
onlyKeys(tool, ["name", "description", "input_schema", "cache_control", "type"]);
|
|
604
|
+
if ((tool.description !== undefined &&
|
|
605
|
+
(typeof tool.description !== "string" || tool.description.length > MAX_TOOL_TEXT_BYTES)) ||
|
|
606
|
+
(tool.type !== undefined && tool.type !== "custom")) {
|
|
607
|
+
throw new CodingProxyError(400, "tools_not_allowed");
|
|
608
|
+
}
|
|
609
|
+
record(tool.input_schema, "tools_not_allowed");
|
|
610
|
+
if (tool.cache_control !== undefined)
|
|
611
|
+
validateCacheControl(tool.cache_control);
|
|
612
|
+
}
|
|
613
|
+
function validateApprovedTool(value, mcpTools) {
|
|
520
614
|
const tool = record(value, "tools_not_allowed");
|
|
521
615
|
if (tool.name === WARDBY_COMMAND_TOOL)
|
|
522
616
|
return validateCommandTool(tool);
|
|
523
617
|
if (tool.name === STRUCTURED_OUTPUT_TOOL)
|
|
524
618
|
return validateStructuredOutputTool(tool);
|
|
619
|
+
if (typeof tool.name === "string" && mcpTools.has(tool.name))
|
|
620
|
+
return validateMcpTool(tool);
|
|
525
621
|
throw new CodingProxyError(400, "tools_not_allowed");
|
|
526
622
|
}
|
|
527
|
-
function validateToolUseBlock(value) {
|
|
623
|
+
function validateToolUseBlock(value, mcpTools) {
|
|
528
624
|
onlyKeys(value, ["type", "id", "name", "input"]);
|
|
529
625
|
if (typeof value.id !== "string" || value.id.length < 1 || value.id.length > 512) {
|
|
530
626
|
throw new CodingProxyError(400, "unsupported_anthropic_feature");
|
|
@@ -533,6 +629,9 @@ function validateToolUseBlock(value) {
|
|
|
533
629
|
return validateCommandInput(value.input);
|
|
534
630
|
if (value.name === STRUCTURED_OUTPUT_TOOL)
|
|
535
631
|
return void record(value.input, "unsupported_anthropic_feature");
|
|
632
|
+
if (typeof value.name === "string" && mcpTools.has(value.name)) {
|
|
633
|
+
return void record(value.input, "unsupported_anthropic_feature");
|
|
634
|
+
}
|
|
536
635
|
throw new CodingProxyError(400, "unsupported_anthropic_feature");
|
|
537
636
|
}
|
|
538
637
|
function validateToolResultBlock(value) {
|
|
@@ -559,7 +658,7 @@ function validateThinkingBlock(value) {
|
|
|
559
658
|
throw new CodingProxyError(400, "unsupported_anthropic_feature");
|
|
560
659
|
}
|
|
561
660
|
}
|
|
562
|
-
function validateMessageBlocks(value, role) {
|
|
661
|
+
function validateMessageBlocks(value, role, mcpTools) {
|
|
563
662
|
if (!Array.isArray(value) || value.length > 10_000)
|
|
564
663
|
throw new CodingProxyError(400, "invalid_anthropic_request");
|
|
565
664
|
for (const candidate of value) {
|
|
@@ -568,7 +667,7 @@ function validateMessageBlocks(value, role) {
|
|
|
568
667
|
validateTextBlock(block);
|
|
569
668
|
}
|
|
570
669
|
else if (block.type === "tool_use" && role === "assistant") {
|
|
571
|
-
validateToolUseBlock(block);
|
|
670
|
+
validateToolUseBlock(block, mcpTools);
|
|
572
671
|
}
|
|
573
672
|
else if (block.type === "tool_result" && role === "user") {
|
|
574
673
|
validateToolResultBlock(block);
|
|
@@ -624,7 +723,7 @@ function validateEffortConfig(value, { thinkingMode, efforts }, betas) {
|
|
|
624
723
|
throw new CodingProxyError(400, "unsupported_anthropic_feature");
|
|
625
724
|
}
|
|
626
725
|
}
|
|
627
|
-
function parseAnthropicRequest(rawBody, betaHeader, shape) {
|
|
726
|
+
function parseAnthropicRequest(rawBody, betaHeader, shape, mcpTools) {
|
|
628
727
|
const beta = parseAnthropicBeta(betaHeader);
|
|
629
728
|
if (Buffer.byteLength(rawBody) > PROXY_MAX_BODY_BYTES)
|
|
630
729
|
throw new CodingProxyError(413, "payload_too_large");
|
|
@@ -688,18 +787,20 @@ function parseAnthropicRequest(rawBody, betaHeader, shape) {
|
|
|
688
787
|
continue;
|
|
689
788
|
}
|
|
690
789
|
}
|
|
691
|
-
validateMessageBlocks(message.content, message.role);
|
|
790
|
+
validateMessageBlocks(message.content, message.role, mcpTools);
|
|
692
791
|
}
|
|
693
792
|
if (body.tools !== undefined) {
|
|
694
|
-
|
|
793
|
+
// run_command and StructuredOutput, plus one per pinned MCP tool.
|
|
794
|
+
if (!Array.isArray(body.tools) || body.tools.length > 2 + mcpTools.size) {
|
|
695
795
|
throw new CodingProxyError(400, "tools_not_allowed");
|
|
796
|
+
}
|
|
696
797
|
const names = new Set();
|
|
697
798
|
for (const tool of body.tools) {
|
|
698
799
|
const definition = record(tool, "tools_not_allowed");
|
|
699
800
|
if (typeof definition.name !== "string" || names.has(definition.name))
|
|
700
801
|
throw new CodingProxyError(400, "tools_not_allowed");
|
|
701
802
|
names.add(definition.name);
|
|
702
|
-
validateApprovedTool(definition);
|
|
803
|
+
validateApprovedTool(definition, mcpTools);
|
|
703
804
|
}
|
|
704
805
|
}
|
|
705
806
|
if (body.metadata !== undefined) {
|
|
@@ -760,13 +861,17 @@ function parseAnthropicRequest(rawBody, betaHeader, shape) {
|
|
|
760
861
|
anthropicBeta: beta.header,
|
|
761
862
|
};
|
|
762
863
|
}
|
|
763
|
-
|
|
864
|
+
/** The Claude tool names of a session's pinned MCP tools; empty for a session without pins. */
|
|
865
|
+
function claudeMcpToolNames(pins) {
|
|
866
|
+
return new Set((pins ?? []).map((pin) => `${CLAUDE_MCP_TOOL_PREFIX}${pin.exposedName}`));
|
|
867
|
+
}
|
|
868
|
+
function parseRequest(protocol, rawBody, anthropicBeta, shape, mcpTools) {
|
|
764
869
|
if (protocol !== "anthropic-messages" && anthropicBeta !== undefined) {
|
|
765
870
|
throw new CodingProxyError(400, "invalid_anthropic_beta");
|
|
766
871
|
}
|
|
767
872
|
try {
|
|
768
873
|
return protocol === "anthropic-messages"
|
|
769
|
-
? parseAnthropicRequest(rawBody, anthropicBeta, shape)
|
|
874
|
+
? parseAnthropicRequest(rawBody, anthropicBeta, shape, mcpTools)
|
|
770
875
|
: parseOpenAiRequest(rawBody);
|
|
771
876
|
}
|
|
772
877
|
catch (error) {
|
|
@@ -821,9 +926,17 @@ export class CodingProxy {
|
|
|
821
926
|
priceVersion;
|
|
822
927
|
activeRequests = new Map();
|
|
823
928
|
onModelRequest;
|
|
929
|
+
steps;
|
|
930
|
+
createStepExtractor;
|
|
931
|
+
runStepsCeiling;
|
|
932
|
+
/** Results already handed over, per session: each request resends the history. */
|
|
933
|
+
seenResults = new SeenIds();
|
|
824
934
|
constructor(options) {
|
|
825
935
|
this.ledger = options.ledger;
|
|
826
936
|
this.onModelRequest = options.onModelRequest;
|
|
937
|
+
this.steps = options.steps;
|
|
938
|
+
this.createStepExtractor = options.createStepExtractor;
|
|
939
|
+
this.runStepsCeiling = options.runStepsCeiling ?? "summary";
|
|
827
940
|
this.credentials = options.credentials;
|
|
828
941
|
this.upstreamUrls = {
|
|
829
942
|
"openai-responses": options.upstreamUrl ?? "https://api.openai.com/v1/responses",
|
|
@@ -874,6 +987,14 @@ export class CodingProxy {
|
|
|
874
987
|
throw new Error("invalid_proxy_deadline");
|
|
875
988
|
if (!input.credentialRef || input.credentialRef.length > 200)
|
|
876
989
|
throw new Error("invalid_proxy_credential_reference");
|
|
990
|
+
// Both or neither: pins without a token could never be called, a token without pins lists nothing.
|
|
991
|
+
const mcp = input.mcpTools !== undefined || input.mcpTokenHash !== undefined;
|
|
992
|
+
if (mcp && (!input.mcpTools || input.mcpTools.length === 0 || !input.mcpTokenHash)) {
|
|
993
|
+
throw new Error("invalid_proxy_mcp_tools");
|
|
994
|
+
}
|
|
995
|
+
if (input.runStepsLevel !== undefined && !RUN_STEPS_LEVELS.includes(input.runStepsLevel)) {
|
|
996
|
+
throw new Error("invalid_proxy_run_steps_level");
|
|
997
|
+
}
|
|
877
998
|
const capability = `rrp_${randomBytes(32).toString("base64url")}`;
|
|
878
999
|
const id = randomUUID();
|
|
879
1000
|
const registryTokenHash = capabilityHash(deriveRegistryToken(capability));
|
|
@@ -888,6 +1009,8 @@ export class CodingProxy {
|
|
|
888
1009
|
budgetUsd: input.budgetUsd,
|
|
889
1010
|
registryTokenHash,
|
|
890
1011
|
terms: input.terms,
|
|
1012
|
+
...(mcp ? { mcpTools: input.mcpTools, mcpTokenHash: input.mcpTokenHash } : {}),
|
|
1013
|
+
...(input.runStepsLevel ? { runStepsLevel: input.runStepsLevel } : {}),
|
|
891
1014
|
});
|
|
892
1015
|
this.audit({ type: "session.created", runId: input.runId });
|
|
893
1016
|
return {
|
|
@@ -901,6 +1024,7 @@ export class CodingProxy {
|
|
|
901
1024
|
};
|
|
902
1025
|
}
|
|
903
1026
|
async cancelSession(sessionId) {
|
|
1027
|
+
this.seenResults.forget(sessionId);
|
|
904
1028
|
await this.ledger.cancelSession(sessionId);
|
|
905
1029
|
for (const controller of this.activeRequests.get(sessionId) ?? [])
|
|
906
1030
|
controller.abort();
|
|
@@ -934,7 +1058,7 @@ export class CodingProxy {
|
|
|
934
1058
|
};
|
|
935
1059
|
let parsed;
|
|
936
1060
|
try {
|
|
937
|
-
parsed = parseRequest(input.protocol, input.rawBody, input.anthropicBeta, shape);
|
|
1061
|
+
parsed = parseRequest(input.protocol, input.rawBody, input.anthropicBeta, shape, claudeMcpToolNames(session.mcpTools));
|
|
938
1062
|
}
|
|
939
1063
|
catch (error) {
|
|
940
1064
|
// The session is already authenticated and no credential has been
|
|
@@ -1000,6 +1124,8 @@ export class CodingProxy {
|
|
|
1000
1124
|
now: this.now(),
|
|
1001
1125
|
});
|
|
1002
1126
|
if (reservation.outcome === "inactive") {
|
|
1127
|
+
// The session has ended (cancelled, expired or closed): another replica may have ended it.
|
|
1128
|
+
this.seenResults.forget(session.id);
|
|
1003
1129
|
this.audit({ type: "request.rejected", runId: session.runId, reason: reservation.reason });
|
|
1004
1130
|
throw new CodingProxyError(403, `session_${reservation.reason}`);
|
|
1005
1131
|
}
|
|
@@ -1029,6 +1155,14 @@ export class CodingProxy {
|
|
|
1029
1155
|
model: request.model,
|
|
1030
1156
|
...holdAudit,
|
|
1031
1157
|
});
|
|
1158
|
+
// Run steps: the results this (validated) request carries for the previous turn, once per call id.
|
|
1159
|
+
const capture = stepCapture(session, this.runStepsCeiling);
|
|
1160
|
+
if (capture !== "none") {
|
|
1161
|
+
this.submitSteps(input.bearer, session.runId, () => ({
|
|
1162
|
+
steps: [],
|
|
1163
|
+
results: this.seenResults.fresh(session.id, resultsFromRequest(session.protocol, parsed.body, { detail: capture === "detail" })),
|
|
1164
|
+
}));
|
|
1165
|
+
}
|
|
1032
1166
|
let key;
|
|
1033
1167
|
try {
|
|
1034
1168
|
key = await this.credentials.resolve(session.credentialRef);
|
|
@@ -1048,6 +1182,7 @@ export class CodingProxy {
|
|
|
1048
1182
|
}
|
|
1049
1183
|
const remainingMs = session.deadlineAt.getTime() - this.now().getTime();
|
|
1050
1184
|
if (remainingMs <= 0) {
|
|
1185
|
+
this.seenResults.forget(session.id);
|
|
1051
1186
|
await this.ledger.release(request.id, 403);
|
|
1052
1187
|
throw new CodingProxyError(403, "session_expired");
|
|
1053
1188
|
}
|
|
@@ -1121,9 +1256,9 @@ export class CodingProxy {
|
|
|
1121
1256
|
}
|
|
1122
1257
|
try {
|
|
1123
1258
|
if (parsed.stream)
|
|
1124
|
-
await this.forwardStream(session, request, upstream, sink, progress);
|
|
1259
|
+
await this.forwardStream(session, request, upstream, sink, progress, input.bearer);
|
|
1125
1260
|
else
|
|
1126
|
-
await this.forwardJson(session, request, upstream, sink);
|
|
1261
|
+
await this.forwardJson(session, request, upstream, sink, input.bearer);
|
|
1127
1262
|
}
|
|
1128
1263
|
catch (error) {
|
|
1129
1264
|
await this.markUncertain(session, request, upstream.status, failureReason(error), responseHeadersOf(upstream), progress);
|
|
@@ -1193,7 +1328,7 @@ export class CodingProxy {
|
|
|
1193
1328
|
throw new CodingProxyError(502, "reservation_invariant_violated");
|
|
1194
1329
|
}
|
|
1195
1330
|
}
|
|
1196
|
-
async forwardJson(session, request, upstream, sink) {
|
|
1331
|
+
async forwardJson(session, request, upstream, sink, capability) {
|
|
1197
1332
|
const declaredLength = Number(upstream.headers.get("content-length"));
|
|
1198
1333
|
if (Number.isFinite(declaredLength) && declaredLength > MAX_UPSTREAM_JSON_BYTES) {
|
|
1199
1334
|
throw new Error("upstream_response_too_large");
|
|
@@ -1213,8 +1348,15 @@ export class CodingProxy {
|
|
|
1213
1348
|
sink.start(upstream.status, { "content-type": "application/json", "cache-control": "no-store" });
|
|
1214
1349
|
await safeWrite(sink, bytes, { value: true });
|
|
1215
1350
|
sink.end();
|
|
1351
|
+
const capture = stepCapture(session, this.runStepsCeiling);
|
|
1352
|
+
if (capture !== "none") {
|
|
1353
|
+
this.submitSteps(capability, session.runId, () => ({
|
|
1354
|
+
steps: stepsFromJson(session.protocol, value, { detail: capture === "detail" }),
|
|
1355
|
+
results: [],
|
|
1356
|
+
}));
|
|
1357
|
+
}
|
|
1216
1358
|
}
|
|
1217
|
-
async forwardStream(session, request, upstream, sink, progress) {
|
|
1359
|
+
async forwardStream(session, request, upstream, sink, progress, capability) {
|
|
1218
1360
|
if (!upstream.body)
|
|
1219
1361
|
throw new Error("missing_upstream_body");
|
|
1220
1362
|
sink.start(upstream.status, { "content-type": "text/event-stream", "cache-control": "no-store" });
|
|
@@ -1225,49 +1367,98 @@ export class CodingProxy {
|
|
|
1225
1367
|
let completed = false;
|
|
1226
1368
|
let failedWithoutUsage = false;
|
|
1227
1369
|
const anthropicUsage = session.protocol === "anthropic-messages" ? (progress.anthropic = new AnthropicSseUsageTracker()) : undefined;
|
|
1370
|
+
// Run steps: each frame is read after it was relayed. An extractor that throws is dropped, never the relay.
|
|
1371
|
+
let steps = this.newStepExtractor(session);
|
|
1372
|
+
const observe = (frame) => {
|
|
1373
|
+
if (!steps)
|
|
1374
|
+
return;
|
|
1375
|
+
try {
|
|
1376
|
+
steps.consume(frame);
|
|
1377
|
+
}
|
|
1378
|
+
catch {
|
|
1379
|
+
steps = undefined;
|
|
1380
|
+
}
|
|
1381
|
+
};
|
|
1228
1382
|
const body = upstream.body;
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
await this.
|
|
1383
|
+
try {
|
|
1384
|
+
for await (const chunk of body) {
|
|
1385
|
+
receivedBytes += chunk.byteLength;
|
|
1386
|
+
if (receivedBytes > MAX_UPSTREAM_JSON_BYTES)
|
|
1387
|
+
throw new Error("upstream_response_too_large");
|
|
1388
|
+
buffer = (buffer + decoder.decode(chunk, { stream: true })).replaceAll("\r\n", "\n");
|
|
1389
|
+
for (;;) {
|
|
1390
|
+
const boundary = buffer.indexOf("\n\n");
|
|
1391
|
+
if (boundary < 0)
|
|
1392
|
+
break;
|
|
1393
|
+
const frame = buffer.slice(0, boundary);
|
|
1394
|
+
buffer = buffer.slice(boundary + 2);
|
|
1395
|
+
const terminal = anthropicUsage ? anthropicUsage.consume(frame) : terminalUsageFromSseFrame(frame);
|
|
1396
|
+
if (!terminal.terminal) {
|
|
1397
|
+
progress.relayedOutputBytes += relayedTextBytes(frame, session.protocol);
|
|
1398
|
+
await safeWrite(sink, Buffer.from(`${frame}\n\n`), connected);
|
|
1399
|
+
observe(frame);
|
|
1400
|
+
continue;
|
|
1401
|
+
}
|
|
1402
|
+
if (!terminal.usage) {
|
|
1403
|
+
if (!terminal.failure)
|
|
1404
|
+
throw new Error("terminal_usage_missing");
|
|
1405
|
+
// The upstream failed the response and reported no usage. Pass its failure on so the
|
|
1406
|
+
// client sees the real error instead of a cut connection; the reservation stays held.
|
|
1407
|
+
await this.recordUpstreamFailure(session, terminal.failure);
|
|
1408
|
+
if (!completed) {
|
|
1409
|
+
await this.markUncertain(session, request, upstream.status, `upstream_failed:${terminal.failure}`, responseHeadersOf(upstream), progress);
|
|
1410
|
+
}
|
|
1411
|
+
failedWithoutUsage = true;
|
|
1412
|
+
await safeWrite(sink, Buffer.from(`${frame}\n\n`), connected);
|
|
1413
|
+
observe(frame);
|
|
1414
|
+
continue;
|
|
1254
1415
|
}
|
|
1255
|
-
|
|
1416
|
+
await this.complete(session, request, terminal.usage, upstream.status);
|
|
1417
|
+
completed = true;
|
|
1256
1418
|
await safeWrite(sink, Buffer.from(`${frame}\n\n`), connected);
|
|
1257
|
-
|
|
1419
|
+
observe(frame);
|
|
1258
1420
|
}
|
|
1259
|
-
await this.complete(session, request, terminal.usage, upstream.status);
|
|
1260
|
-
completed = true;
|
|
1261
|
-
await safeWrite(sink, Buffer.from(`${frame}\n\n`), connected);
|
|
1262
1421
|
}
|
|
1422
|
+
buffer = (buffer + decoder.decode()).replaceAll("\r\n", "\n");
|
|
1423
|
+
if (!completed && !failedWithoutUsage)
|
|
1424
|
+
throw new Error("terminal_usage_missing");
|
|
1425
|
+
if (buffer)
|
|
1426
|
+
await safeWrite(sink, Buffer.from(buffer), connected);
|
|
1427
|
+
if (connected.value)
|
|
1428
|
+
sink.end();
|
|
1429
|
+
}
|
|
1430
|
+
finally {
|
|
1431
|
+
// The reply's steps, also when the stream broke (then its open text is marked cut).
|
|
1432
|
+
const extractor = steps;
|
|
1433
|
+
if (extractor)
|
|
1434
|
+
this.submitSteps(capability, session.runId, () => ({ steps: extractor.finish(), results: [] }));
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
/** Queues a step batch for the control plane. Best effort: any failure drops the steps, never the request. */
|
|
1438
|
+
submitSteps(capability, runId, build) {
|
|
1439
|
+
if (!this.steps)
|
|
1440
|
+
return;
|
|
1441
|
+
try {
|
|
1442
|
+
const batch = build();
|
|
1443
|
+
if (batch.steps.length > 0 || batch.results.length > 0)
|
|
1444
|
+
this.steps.submit(capability, runId, batch);
|
|
1445
|
+
}
|
|
1446
|
+
catch {
|
|
1447
|
+
// Dropped: steps are never a reason to fail a model request.
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
newStepExtractor(session) {
|
|
1451
|
+
if (!this.steps)
|
|
1452
|
+
return undefined;
|
|
1453
|
+
const capture = stepCapture(session, this.runStepsCeiling);
|
|
1454
|
+
if (capture === "none")
|
|
1455
|
+
return undefined;
|
|
1456
|
+
try {
|
|
1457
|
+
return (this.createStepExtractor ?? streamStepExtractor)(session.protocol, { detail: capture === "detail" });
|
|
1458
|
+
}
|
|
1459
|
+
catch {
|
|
1460
|
+
return undefined;
|
|
1263
1461
|
}
|
|
1264
|
-
buffer = (buffer + decoder.decode()).replaceAll("\r\n", "\n");
|
|
1265
|
-
if (!completed && !failedWithoutUsage)
|
|
1266
|
-
throw new Error("terminal_usage_missing");
|
|
1267
|
-
if (buffer)
|
|
1268
|
-
await safeWrite(sink, Buffer.from(buffer), connected);
|
|
1269
|
-
if (connected.value)
|
|
1270
|
-
sink.end();
|
|
1271
1462
|
}
|
|
1272
1463
|
/** Best effort: the executor reads it to name the failure; it must never break the relay. */
|
|
1273
1464
|
async recordUpstreamFailure(session, code) {
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { type RunStepBatch, type RunStepSubmitter } from "../../core/run-steps/types.js";
|
|
2
|
+
export declare const MAX_FORWARD_QUEUE = 256;
|
|
3
|
+
/** A queue holds at most this many bytes of (estimated) serialized submissions. */
|
|
4
|
+
export declare const MAX_FORWARD_QUEUE_BYTES: number;
|
|
5
|
+
export interface RunStepForwarderOptions {
|
|
6
|
+
/** WARDBY_RUN_STEPS_URL, without a trailing slash. */
|
|
7
|
+
url: string;
|
|
8
|
+
fetchImpl?: typeof fetch;
|
|
9
|
+
maxQueue?: number;
|
|
10
|
+
maxQueueBytes?: number;
|
|
11
|
+
timeoutMs?: number;
|
|
12
|
+
/** How long flush() and close() wait for the queue to settle before giving up. */
|
|
13
|
+
flushTimeoutMs?: number;
|
|
14
|
+
log?: {
|
|
15
|
+
info(fields: Record<string, unknown>, message: string): void;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export declare class RunStepForwarder implements RunStepSubmitter {
|
|
19
|
+
private readonly options;
|
|
20
|
+
private queue;
|
|
21
|
+
private queueBytes;
|
|
22
|
+
private draining;
|
|
23
|
+
private scheduled;
|
|
24
|
+
private closed;
|
|
25
|
+
private inflight;
|
|
26
|
+
private dropped;
|
|
27
|
+
private readonly loggedReasons;
|
|
28
|
+
constructor(options: RunStepForwarderOptions);
|
|
29
|
+
submit(capability: string, runId: string, batch: RunStepBatch): void;
|
|
30
|
+
/** Resolves once everything submitted so far was posted or dropped, or after a bound. Never rejects. */
|
|
31
|
+
flush(): Promise<void>;
|
|
32
|
+
/** Stops accepting, waits at most the flush bound, then drops what is left and aborts the in-flight POST. */
|
|
33
|
+
close(): Promise<void>;
|
|
34
|
+
private kick;
|
|
35
|
+
private drain;
|
|
36
|
+
/**
|
|
37
|
+
* The capability goes in the Authorization header only. The response body is never read, and a
|
|
38
|
+
* failure is logged by a fixed reason: an error's or a response's text never reaches a log line.
|
|
39
|
+
*/
|
|
40
|
+
private post;
|
|
41
|
+
private drop;
|
|
42
|
+
}
|