@wardby/cli 0.2.0 → 0.3.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 +34 -4
- package/README.md +183 -71
- package/dist/claude-coding-worker/driver.d.ts +7 -1
- package/dist/claude-coding-worker/driver.js +31 -2
- package/dist/claude-coding-worker/main.js +5 -2
- package/dist/claude-coding-worker/tool-socket.d.ts +9 -0
- package/dist/claude-coding-worker/tool-socket.js +26 -0
- package/dist/cli-help.d.ts +1 -1
- package/dist/cli-help.js +13 -4
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +219 -29
- package/dist/coding/collect-exclude.d.ts +25 -0
- package/dist/coding/collect-exclude.js +76 -0
- package/dist/coding/profile.d.ts +67 -22
- package/dist/coding/profile.js +58 -25
- package/dist/coding/protected-path-wording.d.ts +24 -0
- package/dist/coding/protected-path-wording.js +55 -0
- package/dist/coding/protected-paths.d.ts +31 -0
- package/dist/coding/protected-paths.js +48 -0
- package/dist/coding/protocol.d.ts +99 -3
- package/dist/coding/protocol.js +155 -4
- package/dist/coding/registry/adapters.d.ts +2 -0
- package/dist/coding/registry/adapters.js +9 -0
- package/dist/coding/registry/allowlist.d.ts +9 -0
- package/dist/coding/registry/allowlist.js +28 -0
- package/dist/coding/registry/json-scan.d.ts +55 -0
- package/dist/coding/registry/json-scan.js +191 -0
- package/dist/coding/registry/lockfiles.d.ts +6 -0
- package/dist/coding/registry/lockfiles.js +89 -0
- package/dist/coding/registry/npm-lockfile.d.ts +9 -0
- package/dist/coding/registry/npm-lockfile.js +73 -0
- package/dist/coding/registry/npm-plan.d.ts +29 -0
- package/dist/coding/registry/npm-plan.js +289 -0
- package/dist/coding/registry/npm.d.ts +7 -0
- package/dist/coding/registry/npm.js +252 -0
- package/dist/coding/registry/pypi.d.ts +4 -0
- package/dist/coding/registry/pypi.js +210 -0
- package/dist/coding/registry/report.d.ts +37 -0
- package/dist/coding/registry/report.js +31 -0
- package/dist/coding/registry/token.d.ts +4 -0
- package/dist/coding/registry/token.js +7 -0
- package/dist/coding/registry/types.d.ts +286 -0
- package/dist/coding/registry/types.js +19 -0
- package/dist/coding/registry/worker-config.d.ts +14 -0
- package/dist/coding/registry/worker-config.js +31 -0
- package/dist/coding/services/builtins.d.ts +22 -0
- package/dist/coding/services/builtins.js +85 -0
- package/dist/coding/services/catalog.d.ts +443 -0
- package/dist/coding/services/catalog.js +159 -0
- package/dist/coding/services/declaration.d.ts +11 -0
- package/dist/coding/services/declaration.js +114 -0
- package/dist/coding/services/note.d.ts +8 -0
- package/dist/coding/services/note.js +15 -0
- package/dist/coding/services/resolve.d.ts +25 -0
- package/dist/coding/services/resolve.js +50 -0
- package/dist/coding/services/wording.d.ts +25 -0
- package/dist/coding/services/wording.js +70 -0
- package/dist/coding-proxy/main.js +24 -4
- package/dist/coding-worker/artifact.d.ts +18 -0
- package/dist/coding-worker/artifact.js +48 -6
- package/dist/coding-worker/debug-trace.d.ts +37 -0
- package/dist/coding-worker/debug-trace.js +117 -0
- package/dist/coding-worker/driver.d.ts +14 -1
- package/dist/coding-worker/driver.js +106 -14
- package/dist/coding-worker/errors.d.ts +7 -0
- package/dist/coding-worker/errors.js +26 -0
- package/dist/coding-worker/main.js +13 -4
- package/dist/coding-worker/sdk.d.ts +2 -2
- package/dist/coding-worker/sdk.js +7 -2
- package/dist/coding-worker/types.d.ts +3 -0
- package/dist/config/providers.d.ts +41 -0
- package/dist/config/providers.js +66 -0
- package/dist/core/budget-groups.d.ts +109 -6
- package/dist/core/budget-groups.js +125 -19
- package/dist/core/budget-wording.d.ts +22 -0
- package/dist/core/budget-wording.js +55 -0
- package/dist/core/coding-queue.d.ts +1 -1
- package/dist/core/datastores.d.ts +1 -1
- package/dist/core/datastores.js +18 -2
- package/dist/core/db.d.ts +40 -2
- package/dist/core/db.js +75 -2
- package/dist/core/dispatch.d.ts +49 -7
- package/dist/core/dispatch.js +290 -45
- package/dist/core/engine-native.js +27 -10
- package/dist/core/grants.d.ts +86 -0
- package/dist/core/grants.js +126 -0
- package/dist/core/host-events.d.ts +49 -0
- package/dist/core/host-events.js +257 -0
- package/dist/core/host-identity-links.d.ts +54 -0
- package/dist/core/host-identity-links.js +189 -0
- package/dist/core/host-status.d.ts +61 -0
- package/dist/core/host-status.js +211 -0
- package/dist/core/http-runtime.js +3 -4
- package/dist/core/in-flight-runs.d.ts +8 -0
- package/dist/core/in-flight-runs.js +56 -0
- package/dist/core/lease.d.ts +1 -1
- package/dist/core/private-directory.d.ts +20 -0
- package/dist/core/private-directory.js +36 -0
- package/dist/core/provider-wording.d.ts +12 -0
- package/dist/core/provider-wording.js +44 -0
- package/dist/core/reconciler.d.ts +39 -3
- package/dist/core/reconciler.js +86 -2
- package/dist/core/repo-access.d.ts +99 -0
- package/dist/core/repo-access.js +136 -0
- package/dist/core/review-host-checks.d.ts +11 -0
- package/dist/core/review-host-checks.js +41 -0
- package/dist/core/review-host-tools.d.ts +47 -0
- package/dist/core/review-host-tools.js +347 -0
- package/dist/core/run-heartbeat.d.ts +27 -0
- package/dist/core/run-heartbeat.js +54 -0
- package/dist/core/runner.d.ts +31 -9
- package/dist/core/runner.js +237 -40
- package/dist/core/scheduler.d.ts +1 -1
- package/dist/core/secrets.d.ts +12 -3
- package/dist/core/secrets.js +25 -6
- package/dist/core/subagent-memory-tools.d.ts +2 -2
- package/dist/core/subagent-memory-tools.js +16 -3
- package/dist/core/tool-admin.d.ts +81 -0
- package/dist/core/tool-admin.js +129 -0
- package/dist/core/tool-names.d.ts +42 -0
- package/dist/core/tool-names.js +64 -0
- package/dist/core/untrusted-content.d.ts +32 -0
- package/dist/core/untrusted-content.js +72 -0
- package/dist/core/webhooks.d.ts +9 -2
- package/dist/core/webhooks.js +23 -2
- package/dist/generated/prisma/browser.d.ts +328 -0
- package/dist/generated/prisma/browser.js +17 -0
- package/dist/generated/prisma/client.d.ts +347 -0
- package/dist/generated/prisma/client.js +34 -0
- package/dist/generated/prisma/commonInputTypes.d.ts +839 -0
- package/dist/generated/prisma/commonInputTypes.js +10 -0
- package/dist/generated/prisma/enums.d.ts +59 -0
- package/dist/generated/prisma/enums.js +59 -0
- package/dist/generated/prisma/internal/class.d.ts +605 -0
- package/dist/generated/prisma/internal/class.js +49 -0
- package/dist/generated/prisma/internal/prismaNamespace.d.ts +4494 -0
- package/dist/generated/prisma/internal/prismaNamespace.js +628 -0
- package/dist/generated/prisma/internal/prismaNamespaceBrowser.d.ts +635 -0
- package/dist/generated/prisma/internal/prismaNamespaceBrowser.js +599 -0
- package/dist/generated/prisma/models/Agent.d.ts +3560 -0
- package/dist/generated/prisma/models/Agent.js +1 -0
- package/dist/generated/prisma/models/AgentDatastore.d.ts +1210 -0
- package/dist/generated/prisma/models/AgentDatastore.js +1 -0
- package/dist/generated/prisma/models/AgentMemory.d.ts +986 -0
- package/dist/generated/prisma/models/AgentMemory.js +1 -0
- package/dist/generated/prisma/models/AgentRepository.d.ts +1425 -0
- package/dist/generated/prisma/models/AgentRepository.js +1 -0
- package/dist/generated/prisma/models/AgentSecret.d.ts +1212 -0
- package/dist/generated/prisma/models/AgentSecret.js +1 -0
- package/dist/generated/prisma/models/AgentSubAgent.d.ts +1217 -0
- package/dist/generated/prisma/models/AgentSubAgent.js +1 -0
- package/dist/generated/prisma/models/AgentTool.d.ts +1403 -0
- package/dist/generated/prisma/models/AgentTool.js +1 -0
- package/dist/generated/prisma/models/AuthFormChallenge.d.ts +1298 -0
- package/dist/generated/prisma/models/AuthFormChallenge.js +1 -0
- package/dist/generated/prisma/models/AuthLoginKey.d.ts +1245 -0
- package/dist/generated/prisma/models/AuthLoginKey.js +1 -0
- package/dist/generated/prisma/models/AuthRateLimit.d.ts +979 -0
- package/dist/generated/prisma/models/AuthRateLimit.js +1 -0
- package/dist/generated/prisma/models/AuthSession.d.ts +1376 -0
- package/dist/generated/prisma/models/AuthSession.js +1 -0
- package/dist/generated/prisma/models/AuthUser.d.ts +1700 -0
- package/dist/generated/prisma/models/AuthUser.js +1 -0
- package/dist/generated/prisma/models/BudgetGroup.d.ts +1548 -0
- package/dist/generated/prisma/models/BudgetGroup.js +1 -0
- package/dist/generated/prisma/models/CodingAgentProfile.d.ts +1681 -0
- package/dist/generated/prisma/models/CodingAgentProfile.js +1 -0
- package/dist/generated/prisma/models/CodingProxyRequest.d.ts +1749 -0
- package/dist/generated/prisma/models/CodingProxyRequest.js +1 -0
- package/dist/generated/prisma/models/CodingProxySession.d.ts +1626 -0
- package/dist/generated/prisma/models/CodingProxySession.js +1 -0
- package/dist/generated/prisma/models/CodingRun.d.ts +3583 -0
- package/dist/generated/prisma/models/CodingRun.js +1 -0
- package/dist/generated/prisma/models/CodingService.d.ts +1348 -0
- package/dist/generated/prisma/models/CodingService.js +1 -0
- package/dist/generated/prisma/models/Datastore.d.ts +1434 -0
- package/dist/generated/prisma/models/Datastore.js +1 -0
- package/dist/generated/prisma/models/DatastoreEntry.d.ts +1314 -0
- package/dist/generated/prisma/models/DatastoreEntry.js +1 -0
- package/dist/generated/prisma/models/HostEventDelivery.d.ts +946 -0
- package/dist/generated/prisma/models/HostEventDelivery.js +1 -0
- package/dist/generated/prisma/models/HostIdentity.d.ts +1232 -0
- package/dist/generated/prisma/models/HostIdentity.js +1 -0
- package/dist/generated/prisma/models/HostIdentityLinkRequest.d.ts +1473 -0
- package/dist/generated/prisma/models/HostIdentityLinkRequest.js +1 -0
- package/dist/generated/prisma/models/OAuthAuthorizationCode.d.ts +1526 -0
- package/dist/generated/prisma/models/OAuthAuthorizationCode.js +1 -0
- package/dist/generated/prisma/models/OAuthAuthorizationRequest.d.ts +1350 -0
- package/dist/generated/prisma/models/OAuthAuthorizationRequest.js +1 -0
- package/dist/generated/prisma/models/OAuthClient.d.ts +1289 -0
- package/dist/generated/prisma/models/OAuthClient.js +1 -0
- package/dist/generated/prisma/models/OAuthFamily.d.ts +1540 -0
- package/dist/generated/prisma/models/OAuthFamily.js +1 -0
- package/dist/generated/prisma/models/OAuthGrant.d.ts +1280 -0
- package/dist/generated/prisma/models/OAuthGrant.js +1 -0
- package/dist/generated/prisma/models/Principal.d.ts +2245 -0
- package/dist/generated/prisma/models/Principal.js +1 -0
- package/dist/generated/prisma/models/RegistryAllowance.d.ts +1148 -0
- package/dist/generated/prisma/models/RegistryAllowance.js +1 -0
- package/dist/generated/prisma/models/RegistryApprovedVersion.d.ts +1219 -0
- package/dist/generated/prisma/models/RegistryApprovedVersion.js +1 -0
- package/dist/generated/prisma/models/RegistryFetch.d.ts +1428 -0
- package/dist/generated/prisma/models/RegistryFetch.js +1 -0
- package/dist/generated/prisma/models/RegistryPlanRefusal.d.ts +1294 -0
- package/dist/generated/prisma/models/RegistryPlanRefusal.js +1 -0
- package/dist/generated/prisma/models/RegistryVersionFact.d.ts +1085 -0
- package/dist/generated/prisma/models/RegistryVersionFact.js +1 -0
- package/dist/generated/prisma/models/ResourceGrant.d.ts +1437 -0
- package/dist/generated/prisma/models/ResourceGrant.js +1 -0
- package/dist/generated/prisma/models/Run.d.ts +2669 -0
- package/dist/generated/prisma/models/Run.js +1 -0
- package/dist/generated/prisma/models/RunHostCheck.d.ts +1239 -0
- package/dist/generated/prisma/models/RunHostCheck.js +1 -0
- package/dist/generated/prisma/models/RunHostStatus.d.ts +1315 -0
- package/dist/generated/prisma/models/RunHostStatus.js +1 -0
- package/dist/generated/prisma/models/SchedulerLease.d.ts +970 -0
- package/dist/generated/prisma/models/SchedulerLease.js +1 -0
- package/dist/generated/prisma/models/Secret.d.ts +1399 -0
- package/dist/generated/prisma/models/Secret.js +1 -0
- package/dist/generated/prisma/models/SecretElicitationOutcome.d.ts +973 -0
- package/dist/generated/prisma/models/SecretElicitationOutcome.js +1 -0
- package/dist/generated/prisma/models/Task.d.ts +1176 -0
- package/dist/generated/prisma/models/Task.js +1 -0
- package/dist/generated/prisma/models/Tool.d.ts +1490 -0
- package/dist/generated/prisma/models/Tool.js +1 -0
- package/dist/generated/prisma/models/Webhook.d.ts +1386 -0
- package/dist/generated/prisma/models/Webhook.js +1 -0
- package/dist/generated/prisma/models.d.ts +45 -0
- package/dist/generated/prisma/models.js +1 -0
- package/dist/help/build.d.ts +1 -0
- package/dist/help/build.js +9 -0
- package/dist/help/catalog.d.ts +24 -0
- package/dist/help/catalog.js +160 -0
- package/dist/help/cli.d.ts +2 -0
- package/dist/help/cli.js +65 -0
- package/dist/help/runtime.d.ts +3 -0
- package/dist/help/runtime.js +44 -0
- package/dist/help/search.d.ts +9 -0
- package/dist/help/search.js +104 -0
- package/dist/help-index.json +660 -0
- package/dist/import/cli-args.js +3 -2
- package/dist/import/create.d.ts +6 -1
- package/dist/import/create.js +68 -14
- package/dist/import/index.d.ts +1 -1
- package/dist/import/index.js +19 -9
- package/dist/import/neutral-schema.d.ts +11 -11
- package/dist/mcp/auth/access.d.ts +70 -0
- package/dist/mcp/auth/access.js +58 -0
- package/dist/mcp/auth/grants-cli.d.ts +149 -0
- package/dist/mcp/auth/grants-cli.js +518 -0
- package/dist/mcp/auth/host-account-cli.d.ts +2 -0
- package/dist/mcp/auth/host-account-cli.js +47 -0
- package/dist/mcp/auth/ownership.d.ts +37 -63
- package/dist/mcp/auth/ownership.js +19 -14
- package/dist/mcp/auth/principal.d.ts +1 -1
- package/dist/mcp/auth/repo-authorization.d.ts +22 -0
- package/dist/mcp/auth/repo-authorization.js +51 -0
- package/dist/mcp/auth/resource-server.d.ts +32 -3
- package/dist/mcp/auth/resource-server.js +84 -3
- package/dist/mcp/auth/self-hosted/browser.js +22 -8
- package/dist/mcp/auth/self-hosted/cli.d.ts +1 -1
- package/dist/mcp/auth/self-hosted/cli.js +47 -13
- package/dist/mcp/auth/self-hosted/credentials.d.ts +21 -3
- package/dist/mcp/auth/self-hosted/credentials.js +62 -4
- package/dist/mcp/auth/self-hosted/rate-limit.d.ts +1 -1
- package/dist/mcp/auth/self-hosted/session.d.ts +3 -2
- package/dist/mcp/context.d.ts +28 -2
- package/dist/mcp/errors.d.ts +25 -6
- package/dist/mcp/errors.js +98 -0
- package/dist/mcp/host-events/github-ingress.d.ts +36 -0
- package/dist/mcp/host-events/github-ingress.js +92 -0
- package/dist/mcp/host-events/github-user-callback.d.ts +18 -0
- package/dist/mcp/host-events/github-user-callback.js +50 -0
- package/dist/mcp/index.d.ts +8 -1
- package/dist/mcp/index.js +106 -16
- package/dist/mcp/server.d.ts +1 -1
- package/dist/mcp/server.js +27 -9
- package/dist/mcp/shared/page-style.d.ts +16 -0
- package/dist/mcp/shared/page-style.js +129 -0
- package/dist/mcp/tasks/manager.d.ts +1 -1
- package/dist/mcp/tools/agents.js +447 -49
- package/dist/mcp/tools/budget-groups.js +3 -3
- package/dist/mcp/tools/datastore.js +23 -16
- package/dist/mcp/tools/grants.d.ts +2 -0
- package/dist/mcp/tools/grants.js +239 -0
- package/dist/mcp/tools/help.d.ts +5 -0
- package/dist/mcp/tools/help.js +67 -0
- package/dist/mcp/tools/host-accounts.d.ts +2 -0
- package/dist/mcp/tools/host-accounts.js +114 -0
- package/dist/mcp/tools/memory.d.ts +7 -1
- package/dist/mcp/tools/memory.js +5 -5
- package/dist/mcp/tools/repositories.d.ts +2 -0
- package/dist/mcp/tools/repositories.js +181 -0
- package/dist/mcp/tools/runs.d.ts +6 -0
- package/dist/mcp/tools/runs.js +60 -6
- package/dist/mcp/tools/scheduling.js +4 -7
- package/dist/mcp/tools/secret-elicitation-form.d.ts +1 -1
- package/dist/mcp/tools/secret-elicitation-form.js +10 -28
- package/dist/mcp/tools/secret-elicitation.d.ts +1 -1
- package/dist/mcp/tools/secrets.js +18 -6
- package/dist/mcp/tools/services.d.ts +2 -0
- package/dist/mcp/tools/services.js +222 -0
- package/dist/mcp/tools/subagents.js +53 -17
- package/dist/mcp/tools/tools.d.ts +41 -0
- package/dist/mcp/tools/tools.js +202 -41
- package/dist/mcp/tools/trigger.js +36 -6
- package/dist/mcp/tools/webhooks.js +15 -4
- package/dist/mcp/transport/streamable-http.d.ts +11 -1
- package/dist/mcp/transport/streamable-http.js +32 -2
- package/dist/mcp/unattended-schedules.d.ts +1 -1
- package/dist/mcp/webhooks/ingress.d.ts +1 -1
- package/dist/providers/auth/delegating.d.ts +19 -0
- package/dist/providers/auth/delegating.js +71 -0
- package/dist/providers/auth/index.d.ts +3 -1
- package/dist/providers/auth/index.js +11 -0
- package/dist/providers/auth/self-hosted.d.ts +4 -3
- package/dist/providers/auth/self-hosted.js +16 -4
- package/dist/providers/auth/types.d.ts +6 -0
- package/dist/providers/coding-proxy/memory-ledger.d.ts +3 -0
- package/dist/providers/coding-proxy/memory-ledger.js +21 -2
- package/dist/providers/coding-proxy/metering.d.ts +4 -0
- package/dist/providers/coding-proxy/metering.js +28 -2
- package/dist/providers/coding-proxy/prisma-ledger.d.ts +18 -1
- package/dist/providers/coding-proxy/prisma-ledger.js +50 -5
- package/dist/providers/coding-proxy/proxy.d.ts +13 -0
- package/dist/providers/coding-proxy/proxy.js +479 -18
- package/dist/providers/coding-proxy/registry/audit.d.ts +131 -0
- package/dist/providers/coding-proxy/registry/audit.js +380 -0
- package/dist/providers/coding-proxy/registry/bounded-fetch.d.ts +16 -0
- package/dist/providers/coding-proxy/registry/bounded-fetch.js +77 -0
- package/dist/providers/coding-proxy/registry/plan.d.ts +58 -0
- package/dist/providers/coding-proxy/registry/plan.js +304 -0
- package/dist/providers/coding-proxy/registry/prisma-store.d.ts +34 -0
- package/dist/providers/coding-proxy/registry/prisma-store.js +151 -0
- package/dist/providers/coding-proxy/registry/service.d.ts +213 -0
- package/dist/providers/coding-proxy/registry/service.js +1137 -0
- package/dist/providers/coding-proxy/registry/store.d.ts +127 -0
- package/dist/providers/coding-proxy/registry/store.js +70 -0
- package/dist/providers/coding-proxy/runtime.d.ts +18 -1
- package/dist/providers/coding-proxy/runtime.js +63 -0
- package/dist/providers/coding-proxy/secure-fetch.js +0 -1
- package/dist/providers/coding-proxy/server.d.ts +11 -0
- package/dist/providers/coding-proxy/server.js +163 -0
- package/dist/providers/coding-proxy/types.d.ts +15 -0
- package/dist/providers/datastore/postgres.d.ts +1 -1
- package/dist/providers/engine/types.d.ts +10 -0
- package/dist/providers/executor/build.d.ts +3 -3
- package/dist/providers/executor/composition.d.ts +4 -1
- package/dist/providers/executor/composition.js +19 -0
- package/dist/providers/executor/container.d.ts +106 -4
- package/dist/providers/executor/container.js +305 -22
- package/dist/providers/executor/dbos.d.ts +3 -4
- package/dist/providers/executor/in-process.d.ts +2 -3
- package/dist/providers/executor/routing.d.ts +7 -0
- package/dist/providers/executor/routing.js +13 -0
- package/dist/providers/executor/types.d.ts +22 -0
- package/dist/providers/jobs/claude-tool-setup.d.ts +23 -0
- package/dist/providers/jobs/claude-tool-setup.js +50 -0
- package/dist/providers/jobs/collect-prune.d.ts +7 -0
- package/dist/providers/jobs/collect-prune.js +27 -0
- package/dist/providers/jobs/docker-isolation.d.ts +48 -3
- package/dist/providers/jobs/docker-isolation.js +213 -22
- package/dist/providers/jobs/docker-services.d.ts +35 -0
- package/dist/providers/jobs/docker-services.js +191 -0
- package/dist/providers/jobs/docker.d.ts +60 -2
- package/dist/providers/jobs/docker.js +313 -50
- package/dist/providers/jobs/fake-kubernetes-api.d.ts +1 -0
- package/dist/providers/jobs/fake-kubernetes-api.js +12 -3
- package/dist/providers/jobs/kubernetes-isolation.d.ts +17 -2
- package/dist/providers/jobs/kubernetes-isolation.js +238 -57
- package/dist/providers/jobs/kubernetes-platform.d.ts +10 -4
- package/dist/providers/jobs/kubernetes-platform.js +11 -5
- package/dist/providers/jobs/kubernetes-preflight.js +3 -0
- package/dist/providers/jobs/kubernetes.d.ts +21 -2
- package/dist/providers/jobs/kubernetes.js +117 -31
- package/dist/providers/jobs/types.d.ts +20 -0
- package/dist/providers/llm/anthropic.js +6 -2
- package/dist/providers/llm/bedrock.js +2 -1
- package/dist/providers/llm/claude-messages.d.ts +5 -1
- package/dist/providers/llm/claude-messages.js +1 -0
- package/dist/providers/llm/claude-provider.d.ts +3 -1
- package/dist/providers/llm/claude-provider.js +5 -1
- package/dist/providers/llm/index.d.ts +1 -1
- package/dist/providers/llm/index.js +1 -1
- package/dist/providers/llm/pricing-anthropic.d.ts +10 -0
- package/dist/providers/llm/pricing-anthropic.js +32 -14
- package/dist/providers/llm/pricing-bedrock-claude.d.ts +8 -0
- package/dist/providers/llm/pricing-bedrock-claude.js +9 -0
- package/dist/providers/llm/routing.d.ts +10 -1
- package/dist/providers/llm/routing.js +18 -0
- package/dist/providers/llm/types.d.ts +12 -0
- package/dist/providers/llm/types.js +8 -1
- package/dist/providers/memory/postgres.d.ts +1 -1
- package/dist/providers/review-host/diff-lines.d.ts +16 -0
- package/dist/providers/review-host/diff-lines.js +59 -0
- package/dist/providers/review-host/github-events.d.ts +6 -0
- package/dist/providers/review-host/github-events.js +180 -0
- package/dist/providers/review-host/github-user-auth.d.ts +38 -0
- package/dist/providers/review-host/github-user-auth.js +128 -0
- package/dist/providers/review-host/github.d.ts +57 -0
- package/dist/providers/review-host/github.js +567 -0
- package/dist/providers/review-host/index.d.ts +12 -0
- package/dist/providers/review-host/index.js +30 -0
- package/dist/providers/review-host/review-format.d.ts +19 -0
- package/dist/providers/review-host/review-format.js +59 -0
- package/dist/providers/review-host/types.d.ts +284 -0
- package/dist/providers/review-host/types.js +24 -0
- package/dist/providers/vcs/git.d.ts +16 -6
- package/dist/providers/vcs/git.js +79 -14
- package/dist/providers/vcs/github.d.ts +74 -3
- package/dist/providers/vcs/github.js +195 -15
- package/dist/providers/vcs/types.d.ts +42 -4
- package/dist/quickstart/index.js +34 -32
- package/dist/quickstart/migrate.d.ts +23 -0
- package/dist/quickstart/migrate.js +125 -0
- package/dist/sandbox/fetch-policy.d.ts +20 -2
- package/dist/sandbox/fetch-policy.js +64 -3
- package/dist/sandbox/host-functions.d.ts +9 -1
- package/dist/sandbox/host-functions.js +12 -5
- package/dist/serve.js +1 -1
- package/dist/wardby-bin.js +6 -0
- package/docs/README.md +30 -0
- package/docs/architecture-runtime.md +90 -0
- package/docs/assets/brand/wardby-icon-512.png +0 -0
- package/docs/assets/brand/wardby-icon.svg +16 -0
- package/docs/assets/brand/wardby-mascot-profile-512.png +0 -0
- package/docs/assets/brand/wardby-mascot.png +0 -0
- package/docs/assets/brand/wardby-mascot.svg +5 -0
- package/docs/assets/wardby-workflow.svg +106 -0
- package/docs/code-review-agents.md +481 -0
- package/docs/coding-agent-setup.md +172 -0
- package/docs/coding-packages.md +455 -0
- package/docs/coding-services.md +300 -0
- package/docs/coding-worker-byo-images.md +98 -0
- package/docs/coding-worker-isolation.md +1061 -0
- package/docs/getting-started-gke.md +615 -0
- package/docs/getting-started-identity-provider.md +308 -0
- package/docs/getting-started.md +133 -0
- package/docs/observability.md +53 -0
- package/docs/release-verification.md +66 -0
- package/docs/security-deployment.md +657 -0
- package/help/code-review-agents.md +31 -0
- package/help/coding-packages.md +31 -0
- package/help/coding-services.md +71 -0
- package/help/creating-agents.md +68 -0
- package/help/deploy-gke.md +39 -0
- package/help/deployment-targets.md +39 -0
- package/help/errors/budget-group-exhausted.md +25 -0
- package/help/errors/docker-isolation-unsupported.md +26 -0
- package/help/errors/protected-path.md +45 -0
- package/help/errors/repo-access.md +27 -0
- package/help/errors/service-declaration-invalid.md +27 -0
- package/help/errors/service-declaration-unavailable.md +25 -0
- package/help/errors/service-launcher-unsupported.md +30 -0
- package/help/errors/service-not-allowed.md +27 -0
- package/help/errors/service-unknown.md +23 -0
- package/help/errors/service-unready.md +49 -0
- package/help/getting-started.md +31 -0
- package/help/github.md +30 -0
- package/help/identity-and-access.md +39 -0
- package/help/mcp.md +30 -0
- package/help/native-capabilities.md +30 -0
- package/help/observability.md +41 -0
- package/help/operating-agents.md +30 -0
- package/help/security.md +27 -0
- package/help/troubleshooting/budgets.md +26 -0
- package/help/troubleshooting/coding-workers.md +47 -0
- package/help/troubleshooting/repository-access.md +27 -0
- package/package.json +30 -11
- package/prisma/migrate.config.mjs +20 -0
- package/prisma/migrations/20260925010000_coding_collect_exclude/migration.sql +8 -0
- package/prisma/migrations/20260925015000_allowed_egress_default/migration.sql +8 -0
- package/prisma/migrations/20260925020000_coding_package_registry/migration.sql +55 -0
- package/prisma/migrations/20260925030000_tool_name_per_owner/migration.sql +11 -0
- package/prisma/migrations/20260926010000_run_trigger_host_event/migration.sql +7 -0
- package/prisma/migrations/20260926020000_code_review_hosts/migration.sql +53 -0
- package/prisma/migrations/20260926030000_auth_user_roles/migration.sql +9 -0
- package/prisma/migrations/20260926040000_agent_effort/migration.sql +6 -0
- package/prisma/migrations/20260926050000_registry_lockfile_plan/migration.sql +32 -0
- package/prisma/migrations/20260926050000_repo_access_authorization/migration.sql +82 -0
- package/prisma/migrations/20260926060000_registry_plan_refusal/migration.sql +19 -0
- package/prisma/migrations/20260926100000_registry_plan_refusal_published_at/migration.sql +5 -0
- package/prisma/migrations/20260926190000_run_host_status/migration.sql +18 -0
- package/prisma/migrations/20260926210000_run_host_status_at_dispatch/migration.sql +5 -0
- package/prisma/migrations/20260927010000_resource_grants/migration.sql +72 -0
- package/prisma/migrations/20260927020000_proxy_session_budget_exhausted/migration.sql +3 -0
- package/prisma/migrations/20260927030000_coding_debug_trace/migration.sql +4 -0
- package/prisma/migrations/20260927040000_proxy_session_upstream_failure/migration.sql +3 -0
- package/prisma/migrations/20260927050000_coding_run_services/migration.sql +74 -0
- package/prisma/migrations/20260928000000_coding_run_tool_image/migration.sql +5 -0
- package/prisma/schema.prisma +417 -23
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
2
2
|
import { getModelPricing, PRICING_VERSION } from "../llm/pricing.js";
|
|
3
3
|
import { getAnthropicPricing } from "../llm/pricing-anthropic.js";
|
|
4
|
-
import {
|
|
4
|
+
import { deriveRegistryToken } from "../../coding/registry/token.js";
|
|
5
|
+
import { AnthropicSseUsageTracker, actualCostUsd, estimateReservationUsd, fingerprintRequest, parseAnthropicAuthoritativeUsage, parseAuthoritativeUsage, pricingSnapshot, safeUpstreamErrorCode, terminalUsageFromSseFrame, } from "./metering.js";
|
|
5
6
|
import { createPinnedProxyFetch } from "./secure-fetch.js";
|
|
6
7
|
export const PROXY_MAX_BODY_BYTES = 8 * 1024 * 1024;
|
|
7
8
|
export const PROXY_MAX_OUTPUT_TOKENS = 1_000_000;
|
|
@@ -16,12 +17,20 @@ export const CLAUDE_CODE_ANTHROPIC_BETAS = [
|
|
|
16
17
|
"thinking-token-count-2026-05-13",
|
|
17
18
|
];
|
|
18
19
|
const MAX_UPSTREAM_JSON_BYTES = 16 * 1024 * 1024;
|
|
20
|
+
/** A rejected upstream's error body is read only this far, for its error code. */
|
|
21
|
+
const MAX_UPSTREAM_ERROR_BYTES = 64 * 1024;
|
|
19
22
|
const REQUEST_KEY_PATTERN = /^[\x21-\x7e]{1,200}$/;
|
|
20
23
|
const APPROVED_ANTHROPIC_BETAS = new Set(CLAUDE_CODE_ANTHROPIC_BETAS);
|
|
21
24
|
const WARDBY_COMMAND_TOOL = "mcp__wardby_tools__run_command";
|
|
22
25
|
const STRUCTURED_OUTPUT_TOOL = "StructuredOutput";
|
|
23
26
|
const MAX_COMMAND_BYTES = 16 * 1024;
|
|
24
27
|
const MAX_TOOL_TEXT_BYTES = 256 * 1024;
|
|
28
|
+
/**
|
|
29
|
+
* The longest run_command timeout the Claude tool runner offers the model (its MAX_TIMEOUT_MS,
|
|
30
|
+
* src/claude-tool-runner/command.mjs; the proxy tests keep the two equal). A lower limit here let the
|
|
31
|
+
* command run, then refused the next request, which replays that tool call, as unsupported.
|
|
32
|
+
*/
|
|
33
|
+
export const MAX_COMMAND_TIMEOUT_MS = 120_000;
|
|
25
34
|
export class CodingProxyError extends Error {
|
|
26
35
|
status;
|
|
27
36
|
code;
|
|
@@ -31,9 +40,361 @@ export class CodingProxyError extends Error {
|
|
|
31
40
|
this.code = code;
|
|
32
41
|
}
|
|
33
42
|
}
|
|
34
|
-
function capabilityHash(capability) {
|
|
43
|
+
export function capabilityHash(capability) {
|
|
35
44
|
return createHash("sha256").update(capability).digest("base64url");
|
|
36
45
|
}
|
|
46
|
+
// OpenAI Responses allowlist. Everything a request may carry is listed here;
|
|
47
|
+
// anything else is refused before a credential is resolved. It mirrors what the
|
|
48
|
+
// pinned Codex CLI (src/coding-worker/package.json) actually sends, recorded in
|
|
49
|
+
// fixtures/codex-<version>-responses-requests.json. Only client-side tools
|
|
50
|
+
// (function, custom, and namespaces of them) are allowed: a hosted tool (web
|
|
51
|
+
// search, remote MCP, code interpreter, image generation, file search, ...), a
|
|
52
|
+
// remote image or file, a stored prompt, or a previous response would make
|
|
53
|
+
// OpenAI fetch or run something outside the worker's egress policy and outside
|
|
54
|
+
// the token-metered budget. Tool names vary by model and Codex version, so they
|
|
55
|
+
// are checked for shape, not against a fixed list: a client-side tool the model
|
|
56
|
+
// calls is executed by the worker itself, which the container boundary governs.
|
|
57
|
+
const OPENAI_REQUEST_KEYS = new Set([
|
|
58
|
+
"model",
|
|
59
|
+
"instructions",
|
|
60
|
+
"input",
|
|
61
|
+
"tools",
|
|
62
|
+
"tool_choice",
|
|
63
|
+
"parallel_tool_calls",
|
|
64
|
+
"reasoning",
|
|
65
|
+
"store",
|
|
66
|
+
"stream",
|
|
67
|
+
"include",
|
|
68
|
+
"prompt_cache_key",
|
|
69
|
+
"text",
|
|
70
|
+
"client_metadata",
|
|
71
|
+
"max_output_tokens",
|
|
72
|
+
"background",
|
|
73
|
+
"service_tier",
|
|
74
|
+
]);
|
|
75
|
+
const OPENAI_REASONING_EFFORTS = new Set(["none", "minimal", "low", "medium", "high", "xhigh", "max", "ultra"]);
|
|
76
|
+
const OPENAI_REASONING_SUMMARIES = new Set(["auto", "concise", "detailed", "none"]);
|
|
77
|
+
const OPENAI_REASONING_CONTEXTS = new Set(["all_turns", "current_turn"]);
|
|
78
|
+
const OPENAI_INCLUDES = new Set(["reasoning.encrypted_content"]);
|
|
79
|
+
// Only unset (Codex never sends one) or an explicit "default". "auto" defers to
|
|
80
|
+
// the OpenAI project's own tier setting, which may be priority or scale; those
|
|
81
|
+
// and "flex" are billed at rates the pricing table does not track.
|
|
82
|
+
const OPENAI_SERVICE_TIERS = new Set(["default"]);
|
|
83
|
+
// The client_metadata keys the pinned Codex sends (recorded fixture). Several
|
|
84
|
+
// are x-codex-*/x-openai-* keys whose server-side meaning is undocumented, so a
|
|
85
|
+
// new key fails closed like every other unknown field.
|
|
86
|
+
const OPENAI_CLIENT_METADATA_KEYS = new Set([
|
|
87
|
+
"parent_turn_id",
|
|
88
|
+
"root_turn_id",
|
|
89
|
+
"session_id",
|
|
90
|
+
"thread_id",
|
|
91
|
+
"turn_id",
|
|
92
|
+
"x-codex-installation-id",
|
|
93
|
+
"x-codex-parent-thread-id",
|
|
94
|
+
"x-codex-turn-metadata",
|
|
95
|
+
"x-codex-window-id",
|
|
96
|
+
"x-openai-subagent",
|
|
97
|
+
]);
|
|
98
|
+
const OPENAI_TOOL_CHOICES = new Set(["auto", "none", "required"]);
|
|
99
|
+
const OPENAI_VERBOSITIES = new Set(["low", "medium", "high"]);
|
|
100
|
+
const OPENAI_IMAGE_DETAILS = new Set(["auto", "low", "high", "original"]);
|
|
101
|
+
const OPENAI_MESSAGE_ROLES = new Set(["user", "developer", "system", "assistant"]);
|
|
102
|
+
const OPENAI_MESSAGE_PHASES = new Set(["commentary", "final_answer"]);
|
|
103
|
+
const OPENAI_GRAMMAR_SYNTAXES = new Set(["lark", "regex"]);
|
|
104
|
+
const OPENAI_INPUT_PARTS = new Set(["input_text", "input_image"]);
|
|
105
|
+
const OPENAI_ASSISTANT_PARTS = new Set(["output_text"]);
|
|
106
|
+
const OPENAI_AGENT_MESSAGE_PARTS = new Set(["input_text", "encrypted_content"]);
|
|
107
|
+
const OPENAI_TOOL_NAME = /^[A-Za-z0-9_-]{1,128}$/;
|
|
108
|
+
const OPENAI_CODE_SUFFIX = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
109
|
+
// Only an inline base64 image: Codex's view_image and code-mode image() send
|
|
110
|
+
// local files this way. A URL (or a file_id) would make OpenAI fetch it.
|
|
111
|
+
const OPENAI_INLINE_IMAGE = /^data:image\/(?:png|jpeg|gif|webp);base64,[A-Za-z0-9+/]+={0,2}$/;
|
|
112
|
+
const MAX_OPENAI_ITEMS = 10_000;
|
|
113
|
+
const MAX_OPENAI_TOOLS = 512;
|
|
114
|
+
const MAX_OPENAI_ID_LENGTH = 512;
|
|
115
|
+
const MAX_OPENAI_METADATA_VALUE_LENGTH = 16 * 1024;
|
|
116
|
+
function openAiCode(prefix, value) {
|
|
117
|
+
return `${prefix}:${typeof value === "string" && OPENAI_CODE_SUFFIX.test(value) ? value : "other"}`;
|
|
118
|
+
}
|
|
119
|
+
function openAiRecord(value, code = "invalid_openai_request") {
|
|
120
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
121
|
+
throw new CodingProxyError(400, code);
|
|
122
|
+
return value;
|
|
123
|
+
}
|
|
124
|
+
function openAiOnlyKeys(value, keys, code = "invalid_openai_request") {
|
|
125
|
+
const allowed = new Set(keys);
|
|
126
|
+
if (Object.keys(value).some((key) => !allowed.has(key)))
|
|
127
|
+
throw new CodingProxyError(400, code);
|
|
128
|
+
}
|
|
129
|
+
function openAiString(value, maxLength, code = "invalid_openai_request") {
|
|
130
|
+
if (typeof value !== "string" || value.length > maxLength)
|
|
131
|
+
throw new CodingProxyError(400, code);
|
|
132
|
+
}
|
|
133
|
+
function openAiOptional(value, check) {
|
|
134
|
+
if (value !== undefined)
|
|
135
|
+
check(value);
|
|
136
|
+
}
|
|
137
|
+
function openAiId(value) {
|
|
138
|
+
if (typeof value !== "string" || value.length < 1 || value.length > MAX_OPENAI_ID_LENGTH) {
|
|
139
|
+
throw new CodingProxyError(400, "invalid_openai_request");
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
function openAiToolName(value, code = "invalid_openai_request") {
|
|
143
|
+
if (typeof value !== "string" || !OPENAI_TOOL_NAME.test(value))
|
|
144
|
+
throw new CodingProxyError(400, code);
|
|
145
|
+
}
|
|
146
|
+
function validateOpenAiFunctionTool(tool) {
|
|
147
|
+
openAiOnlyKeys(tool, ["type", "name", "description", "strict", "parameters"], "invalid_openai_tool");
|
|
148
|
+
openAiToolName(tool.name, "invalid_openai_tool");
|
|
149
|
+
openAiOptional(tool.description, (value) => openAiString(value, MAX_TOOL_TEXT_BYTES, "invalid_openai_tool"));
|
|
150
|
+
if (tool.strict !== undefined && typeof tool.strict !== "boolean") {
|
|
151
|
+
throw new CodingProxyError(400, "invalid_openai_tool");
|
|
152
|
+
}
|
|
153
|
+
openAiOptional(tool.parameters, (value) => openAiRecord(value, "invalid_openai_tool"));
|
|
154
|
+
}
|
|
155
|
+
function validateOpenAiCustomTool(tool) {
|
|
156
|
+
openAiOnlyKeys(tool, ["type", "name", "description", "format"], "invalid_openai_tool");
|
|
157
|
+
openAiToolName(tool.name, "invalid_openai_tool");
|
|
158
|
+
openAiOptional(tool.description, (value) => openAiString(value, MAX_TOOL_TEXT_BYTES, "invalid_openai_tool"));
|
|
159
|
+
if (tool.format === undefined)
|
|
160
|
+
return;
|
|
161
|
+
const format = openAiRecord(tool.format, "invalid_openai_tool");
|
|
162
|
+
if (format.type === "text")
|
|
163
|
+
return openAiOnlyKeys(format, ["type"], "invalid_openai_tool");
|
|
164
|
+
openAiOnlyKeys(format, ["type", "syntax", "definition"], "invalid_openai_tool");
|
|
165
|
+
if (format.type !== "grammar" || typeof format.syntax !== "string" || !OPENAI_GRAMMAR_SYNTAXES.has(format.syntax)) {
|
|
166
|
+
throw new CodingProxyError(400, "invalid_openai_tool");
|
|
167
|
+
}
|
|
168
|
+
openAiString(format.definition, MAX_TOOL_TEXT_BYTES, "invalid_openai_tool");
|
|
169
|
+
}
|
|
170
|
+
function validateOpenAiTools(value, count, nested = false) {
|
|
171
|
+
if (!Array.isArray(value))
|
|
172
|
+
throw new CodingProxyError(400, "invalid_openai_tool");
|
|
173
|
+
for (const candidate of value) {
|
|
174
|
+
if (++count.value > MAX_OPENAI_TOOLS)
|
|
175
|
+
throw new CodingProxyError(400, "invalid_openai_tool");
|
|
176
|
+
const tool = openAiRecord(candidate, "invalid_openai_tool");
|
|
177
|
+
if (tool.type === "function") {
|
|
178
|
+
validateOpenAiFunctionTool(tool);
|
|
179
|
+
}
|
|
180
|
+
else if (tool.type === "custom") {
|
|
181
|
+
validateOpenAiCustomTool(tool);
|
|
182
|
+
}
|
|
183
|
+
else if (tool.type === "namespace" && !nested) {
|
|
184
|
+
openAiOnlyKeys(tool, ["type", "name", "description", "tools"], "invalid_openai_tool");
|
|
185
|
+
openAiToolName(tool.name, "invalid_openai_tool");
|
|
186
|
+
openAiOptional(tool.description, (value) => openAiString(value, MAX_TOOL_TEXT_BYTES, "invalid_openai_tool"));
|
|
187
|
+
validateOpenAiTools(tool.tools, count, true);
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
throw new CodingProxyError(400, openAiCode("openai_tool_not_allowed", tool.type));
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
function validateOpenAiContentPart(value, allowed) {
|
|
195
|
+
const part = openAiRecord(value);
|
|
196
|
+
if (typeof part.type !== "string" || !allowed.has(part.type)) {
|
|
197
|
+
throw new CodingProxyError(400, openAiCode("openai_input_not_allowed", part.type));
|
|
198
|
+
}
|
|
199
|
+
if (part.type === "input_image") {
|
|
200
|
+
if (part.file_id !== undefined || typeof part.image_url !== "string" || !OPENAI_INLINE_IMAGE.test(part.image_url)) {
|
|
201
|
+
throw new CodingProxyError(400, "openai_remote_input_not_allowed");
|
|
202
|
+
}
|
|
203
|
+
openAiOnlyKeys(part, ["type", "image_url", "detail"]);
|
|
204
|
+
if (part.detail !== undefined && (typeof part.detail !== "string" || !OPENAI_IMAGE_DETAILS.has(part.detail))) {
|
|
205
|
+
throw new CodingProxyError(400, "invalid_openai_request");
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
else if (part.type === "encrypted_content") {
|
|
209
|
+
openAiOnlyKeys(part, ["type", "encrypted_content"]);
|
|
210
|
+
openAiString(part.encrypted_content, PROXY_MAX_BODY_BYTES);
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
openAiOnlyKeys(part, ["type", "text"]);
|
|
214
|
+
openAiString(part.text, PROXY_MAX_BODY_BYTES);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
function validateOpenAiContent(value, allowed, allowString = true) {
|
|
218
|
+
if (typeof value === "string" && allowString)
|
|
219
|
+
return;
|
|
220
|
+
if (!Array.isArray(value) || value.length > MAX_OPENAI_ITEMS)
|
|
221
|
+
throw new CodingProxyError(400, "invalid_openai_request");
|
|
222
|
+
for (const part of value)
|
|
223
|
+
validateOpenAiContentPart(part, allowed);
|
|
224
|
+
}
|
|
225
|
+
function validateOpenAiTextParts(value, type) {
|
|
226
|
+
if (!Array.isArray(value) || value.length > MAX_OPENAI_ITEMS)
|
|
227
|
+
throw new CodingProxyError(400, "invalid_openai_request");
|
|
228
|
+
for (const candidate of value) {
|
|
229
|
+
const part = openAiRecord(candidate);
|
|
230
|
+
openAiOnlyKeys(part, ["type", "text"]);
|
|
231
|
+
if (part.type !== type)
|
|
232
|
+
throw new CodingProxyError(400, openAiCode("openai_input_not_allowed", part.type));
|
|
233
|
+
openAiString(part.text, PROXY_MAX_BODY_BYTES);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
function validateOpenAiInputItem(value, toolCount) {
|
|
237
|
+
const item = openAiRecord(value);
|
|
238
|
+
switch (item.type) {
|
|
239
|
+
case undefined:
|
|
240
|
+
case "message": {
|
|
241
|
+
openAiOnlyKeys(item, ["type", "id", "role", "content", "phase"]);
|
|
242
|
+
openAiOptional(item.id, openAiId);
|
|
243
|
+
if (typeof item.role !== "string" || !OPENAI_MESSAGE_ROLES.has(item.role)) {
|
|
244
|
+
throw new CodingProxyError(400, "invalid_openai_request");
|
|
245
|
+
}
|
|
246
|
+
if (item.phase !== undefined && (typeof item.phase !== "string" || !OPENAI_MESSAGE_PHASES.has(item.phase))) {
|
|
247
|
+
throw new CodingProxyError(400, "invalid_openai_request");
|
|
248
|
+
}
|
|
249
|
+
validateOpenAiContent(item.content, item.role === "assistant" ? OPENAI_ASSISTANT_PARTS : OPENAI_INPUT_PARTS);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
case "reasoning":
|
|
253
|
+
openAiOnlyKeys(item, ["type", "id", "summary", "content", "encrypted_content"]);
|
|
254
|
+
openAiOptional(item.id, openAiId);
|
|
255
|
+
validateOpenAiTextParts(item.summary, "summary_text");
|
|
256
|
+
if (item.content !== undefined && item.content !== null)
|
|
257
|
+
validateOpenAiTextParts(item.content, "reasoning_text");
|
|
258
|
+
if (item.encrypted_content !== undefined && item.encrypted_content !== null) {
|
|
259
|
+
openAiString(item.encrypted_content, PROXY_MAX_BODY_BYTES);
|
|
260
|
+
}
|
|
261
|
+
return;
|
|
262
|
+
case "function_call":
|
|
263
|
+
openAiOnlyKeys(item, ["type", "id", "call_id", "name", "namespace", "arguments"]);
|
|
264
|
+
openAiOptional(item.id, openAiId);
|
|
265
|
+
openAiId(item.call_id);
|
|
266
|
+
openAiToolName(item.name);
|
|
267
|
+
openAiOptional(item.namespace, (value) => openAiToolName(value));
|
|
268
|
+
openAiString(item.arguments, PROXY_MAX_BODY_BYTES);
|
|
269
|
+
return;
|
|
270
|
+
case "custom_tool_call":
|
|
271
|
+
openAiOnlyKeys(item, ["type", "id", "status", "call_id", "name", "input"]);
|
|
272
|
+
openAiOptional(item.id, openAiId);
|
|
273
|
+
openAiOptional(item.status, (value) => openAiString(value, 64));
|
|
274
|
+
openAiId(item.call_id);
|
|
275
|
+
openAiToolName(item.name);
|
|
276
|
+
openAiString(item.input, PROXY_MAX_BODY_BYTES);
|
|
277
|
+
return;
|
|
278
|
+
case "function_call_output":
|
|
279
|
+
case "custom_tool_call_output":
|
|
280
|
+
openAiOnlyKeys(item, item.type === "function_call_output"
|
|
281
|
+
? ["type", "id", "call_id", "output"]
|
|
282
|
+
: ["type", "id", "call_id", "name", "output"]);
|
|
283
|
+
openAiOptional(item.id, openAiId);
|
|
284
|
+
openAiId(item.call_id);
|
|
285
|
+
openAiOptional(item.name, (value) => openAiToolName(value));
|
|
286
|
+
validateOpenAiContent(item.output, OPENAI_INPUT_PARTS);
|
|
287
|
+
return;
|
|
288
|
+
case "agent_message":
|
|
289
|
+
openAiOnlyKeys(item, ["type", "id", "author", "recipient", "content"]);
|
|
290
|
+
openAiOptional(item.id, openAiId);
|
|
291
|
+
openAiOptional(item.author, (value) => openAiString(value, MAX_OPENAI_ID_LENGTH));
|
|
292
|
+
openAiOptional(item.recipient, (value) => openAiString(value, MAX_OPENAI_ID_LENGTH));
|
|
293
|
+
validateOpenAiContent(item.content, OPENAI_AGENT_MESSAGE_PARTS, false);
|
|
294
|
+
return;
|
|
295
|
+
case "additional_tools":
|
|
296
|
+
openAiOnlyKeys(item, ["type", "id", "role", "tools"]);
|
|
297
|
+
openAiOptional(item.id, openAiId);
|
|
298
|
+
if (item.role !== undefined && item.role !== "developer")
|
|
299
|
+
throw new CodingProxyError(400, "invalid_openai_request");
|
|
300
|
+
validateOpenAiTools(item.tools, toolCount);
|
|
301
|
+
return;
|
|
302
|
+
default:
|
|
303
|
+
throw new CodingProxyError(400, openAiCode("openai_input_not_allowed", item.type));
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
function validateOpenAiReasoning(value) {
|
|
307
|
+
const reasoning = openAiRecord(value, "openai_reasoning_not_allowed");
|
|
308
|
+
openAiOnlyKeys(reasoning, ["effort", "summary", "context"], "openai_reasoning_not_allowed");
|
|
309
|
+
for (const [key, allowed] of [
|
|
310
|
+
["effort", OPENAI_REASONING_EFFORTS],
|
|
311
|
+
["summary", OPENAI_REASONING_SUMMARIES],
|
|
312
|
+
["context", OPENAI_REASONING_CONTEXTS],
|
|
313
|
+
]) {
|
|
314
|
+
const setting = reasoning[key];
|
|
315
|
+
if (setting !== undefined && (typeof setting !== "string" || !allowed.has(setting))) {
|
|
316
|
+
throw new CodingProxyError(400, "openai_reasoning_not_allowed");
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
function validateOpenAiText(value) {
|
|
321
|
+
const text = openAiRecord(value, "openai_text_not_allowed");
|
|
322
|
+
openAiOnlyKeys(text, ["verbosity", "format"], "openai_text_not_allowed");
|
|
323
|
+
if (text.verbosity !== undefined && (typeof text.verbosity !== "string" || !OPENAI_VERBOSITIES.has(text.verbosity))) {
|
|
324
|
+
throw new CodingProxyError(400, "openai_text_not_allowed");
|
|
325
|
+
}
|
|
326
|
+
if (text.format === undefined)
|
|
327
|
+
return;
|
|
328
|
+
const format = openAiRecord(text.format, "openai_text_not_allowed");
|
|
329
|
+
if (format.type === "text")
|
|
330
|
+
return openAiOnlyKeys(format, ["type"], "openai_text_not_allowed");
|
|
331
|
+
openAiOnlyKeys(format, ["type", "name", "schema", "strict", "description"], "openai_text_not_allowed");
|
|
332
|
+
if (format.type !== "json_schema" || typeof format.name !== "string" || !OPENAI_TOOL_NAME.test(format.name)) {
|
|
333
|
+
throw new CodingProxyError(400, "openai_text_not_allowed");
|
|
334
|
+
}
|
|
335
|
+
openAiRecord(format.schema, "openai_text_not_allowed");
|
|
336
|
+
if (format.strict !== undefined && typeof format.strict !== "boolean") {
|
|
337
|
+
throw new CodingProxyError(400, "openai_text_not_allowed");
|
|
338
|
+
}
|
|
339
|
+
openAiOptional(format.description, (value) => openAiString(value, MAX_TOOL_TEXT_BYTES, "openai_text_not_allowed"));
|
|
340
|
+
}
|
|
341
|
+
function validateOpenAiClientMetadata(value) {
|
|
342
|
+
const metadata = openAiRecord(value);
|
|
343
|
+
for (const [key, entry] of Object.entries(metadata)) {
|
|
344
|
+
if (!OPENAI_CLIENT_METADATA_KEYS.has(key)) {
|
|
345
|
+
throw new CodingProxyError(400, openAiCode("openai_client_metadata_key_not_allowed", key));
|
|
346
|
+
}
|
|
347
|
+
openAiString(entry, MAX_OPENAI_METADATA_VALUE_LENGTH);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
function validateOpenAiBody(body) {
|
|
351
|
+
for (const key of Object.keys(body)) {
|
|
352
|
+
if (!OPENAI_REQUEST_KEYS.has(key))
|
|
353
|
+
throw new CodingProxyError(400, openAiCode("openai_request_key_not_allowed", key));
|
|
354
|
+
}
|
|
355
|
+
if (body.background !== undefined && body.background !== false) {
|
|
356
|
+
throw new CodingProxyError(400, body.background === true ? "background_not_allowed" : "invalid_openai_request");
|
|
357
|
+
}
|
|
358
|
+
if (body.service_tier !== undefined) {
|
|
359
|
+
if (typeof body.service_tier !== "string" || !OPENAI_SERVICE_TIERS.has(body.service_tier)) {
|
|
360
|
+
throw new CodingProxyError(400, "service_tier_not_allowed");
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
if (body.store !== undefined && typeof body.store !== "boolean") {
|
|
364
|
+
throw new CodingProxyError(400, "invalid_openai_request");
|
|
365
|
+
}
|
|
366
|
+
openAiOptional(body.instructions, (value) => openAiString(value, PROXY_MAX_BODY_BYTES));
|
|
367
|
+
const toolCount = { value: 0 };
|
|
368
|
+
if (body.tools !== undefined)
|
|
369
|
+
validateOpenAiTools(body.tools, toolCount);
|
|
370
|
+
if (body.tool_choice !== undefined) {
|
|
371
|
+
if (typeof body.tool_choice !== "string" || !OPENAI_TOOL_CHOICES.has(body.tool_choice)) {
|
|
372
|
+
throw new CodingProxyError(400, "openai_tool_choice_not_allowed");
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
if (body.parallel_tool_calls !== undefined && typeof body.parallel_tool_calls !== "boolean") {
|
|
376
|
+
throw new CodingProxyError(400, "invalid_openai_request");
|
|
377
|
+
}
|
|
378
|
+
openAiOptional(body.reasoning, validateOpenAiReasoning);
|
|
379
|
+
if (body.include !== undefined) {
|
|
380
|
+
const include = body.include;
|
|
381
|
+
if (!Array.isArray(include) ||
|
|
382
|
+
new Set(include).size !== include.length ||
|
|
383
|
+
include.some((entry) => typeof entry !== "string" || !OPENAI_INCLUDES.has(entry))) {
|
|
384
|
+
throw new CodingProxyError(400, "openai_include_not_allowed");
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
openAiOptional(body.prompt_cache_key, (value) => openAiString(value, MAX_OPENAI_ID_LENGTH));
|
|
388
|
+
openAiOptional(body.text, validateOpenAiText);
|
|
389
|
+
openAiOptional(body.client_metadata, validateOpenAiClientMetadata);
|
|
390
|
+
if (typeof body.input === "string")
|
|
391
|
+
return;
|
|
392
|
+
if (!Array.isArray(body.input) || body.input.length > MAX_OPENAI_ITEMS) {
|
|
393
|
+
throw new CodingProxyError(400, "invalid_openai_request");
|
|
394
|
+
}
|
|
395
|
+
for (const item of body.input)
|
|
396
|
+
validateOpenAiInputItem(item, toolCount);
|
|
397
|
+
}
|
|
37
398
|
function parseOpenAiRequest(rawBody) {
|
|
38
399
|
if (Buffer.byteLength(rawBody) > PROXY_MAX_BODY_BYTES)
|
|
39
400
|
throw new CodingProxyError(413, "payload_too_large");
|
|
@@ -59,8 +420,7 @@ function parseOpenAiRequest(rawBody) {
|
|
|
59
420
|
}
|
|
60
421
|
if (body.stream !== true && body.stream !== false)
|
|
61
422
|
throw new CodingProxyError(400, "stream_required");
|
|
62
|
-
|
|
63
|
-
throw new CodingProxyError(400, "background_not_allowed");
|
|
423
|
+
validateOpenAiBody(body);
|
|
64
424
|
const normalized = { ...body, max_output_tokens: maxOutputTokens, store: false, background: false };
|
|
65
425
|
const encoded = JSON.stringify(normalized);
|
|
66
426
|
if (Buffer.byteLength(encoded) > PROXY_MAX_BODY_BYTES)
|
|
@@ -118,7 +478,7 @@ function validateCommandInput(value) {
|
|
|
118
478
|
(typeof input.timeout_ms !== "number" ||
|
|
119
479
|
!Number.isSafeInteger(input.timeout_ms) ||
|
|
120
480
|
input.timeout_ms < 1_000 ||
|
|
121
|
-
input.timeout_ms >
|
|
481
|
+
input.timeout_ms > MAX_COMMAND_TIMEOUT_MS)) {
|
|
122
482
|
throw new CodingProxyError(400, "unsupported_anthropic_feature");
|
|
123
483
|
}
|
|
124
484
|
}
|
|
@@ -227,6 +587,15 @@ function parseAnthropicBeta(value) {
|
|
|
227
587
|
const normalized = [...unique].sort();
|
|
228
588
|
return { header: normalized.join(","), values: new Set(normalized) };
|
|
229
589
|
}
|
|
590
|
+
/**
|
|
591
|
+
* Models that take manual extended thinking (`{type: "enabled", budget_tokens}`) instead of adaptive
|
|
592
|
+
* thinking, and no effort level: Claude Haiku 4.5, which returns a 400 for adaptive thinking. The
|
|
593
|
+
* pinned Claude Agent SDK sends it `budget_tokens` = max_tokens - 1 and no `output_config`
|
|
594
|
+
* (fixtures/anthropic-sdk-request-haiku-4-5.json). Every other model stays adaptive-only.
|
|
595
|
+
*/
|
|
596
|
+
const MANUAL_THINKING_MODELS = new Set(["claude-haiku-4-5"]);
|
|
597
|
+
/** Anthropic's minimum manual thinking budget. */
|
|
598
|
+
const MIN_THINKING_BUDGET_TOKENS = 1024;
|
|
230
599
|
function requireAnthropicBeta(values, beta) {
|
|
231
600
|
if (!values.has(beta))
|
|
232
601
|
throw new CodingProxyError(400, "anthropic_beta_required");
|
|
@@ -305,13 +674,27 @@ function parseAnthropicRequest(rawBody, betaHeader) {
|
|
|
305
674
|
if (typeof metadata.user_id !== "string")
|
|
306
675
|
throw new CodingProxyError(400, "invalid_anthropic_request");
|
|
307
676
|
}
|
|
677
|
+
const manualThinking = MANUAL_THINKING_MODELS.has(body.model);
|
|
308
678
|
if (body.thinking !== undefined) {
|
|
309
679
|
requireAnthropicBeta(beta.values, "interleaved-thinking-2025-05-14");
|
|
310
680
|
requireAnthropicBeta(beta.values, "thinking-token-count-2026-05-13");
|
|
311
681
|
const thinking = record(body.thinking);
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
682
|
+
if (manualThinking) {
|
|
683
|
+
onlyKeys(thinking, ["type", "budget_tokens"]);
|
|
684
|
+
const budget = thinking.budget_tokens;
|
|
685
|
+
if (thinking.type !== "enabled" ||
|
|
686
|
+
typeof budget !== "number" ||
|
|
687
|
+
!Number.isSafeInteger(budget) ||
|
|
688
|
+
budget < MIN_THINKING_BUDGET_TOKENS ||
|
|
689
|
+
budget >= body.max_tokens) {
|
|
690
|
+
throw new CodingProxyError(400, "unsupported_anthropic_feature");
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
else {
|
|
694
|
+
onlyKeys(thinking, ["type"]);
|
|
695
|
+
if (thinking.type !== "adaptive")
|
|
696
|
+
throw new CodingProxyError(400, "unsupported_anthropic_feature");
|
|
697
|
+
}
|
|
315
698
|
}
|
|
316
699
|
if (body.context_management !== undefined) {
|
|
317
700
|
requireAnthropicBeta(beta.values, "context-management-2025-06-27");
|
|
@@ -327,6 +710,9 @@ function parseAnthropicRequest(rawBody, betaHeader) {
|
|
|
327
710
|
}
|
|
328
711
|
}
|
|
329
712
|
if (body.output_config !== undefined) {
|
|
713
|
+
// Effort doesn't exist on a manual-thinking model; the SDK never sends it there.
|
|
714
|
+
if (manualThinking)
|
|
715
|
+
throw new CodingProxyError(400, "unsupported_anthropic_feature");
|
|
330
716
|
requireAnthropicBeta(beta.values, "effort-2025-11-24");
|
|
331
717
|
const output = record(body.output_config);
|
|
332
718
|
onlyKeys(output, ["effort"]);
|
|
@@ -352,9 +738,18 @@ function parseRequest(protocol, rawBody, anthropicBeta) {
|
|
|
352
738
|
if (protocol !== "anthropic-messages" && anthropicBeta !== undefined) {
|
|
353
739
|
throw new CodingProxyError(400, "invalid_anthropic_beta");
|
|
354
740
|
}
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
741
|
+
try {
|
|
742
|
+
return protocol === "anthropic-messages"
|
|
743
|
+
? parseAnthropicRequest(rawBody, anthropicBeta)
|
|
744
|
+
: parseOpenAiRequest(rawBody);
|
|
745
|
+
}
|
|
746
|
+
catch (error) {
|
|
747
|
+
// JSON nested deeply enough (inside a free-form tool schema, say) overflows
|
|
748
|
+
// the stack when re-encoded. It is the client's request, not a proxy fault.
|
|
749
|
+
if (error instanceof RangeError)
|
|
750
|
+
throw new CodingProxyError(400, "request_nesting_too_deep");
|
|
751
|
+
throw error;
|
|
752
|
+
}
|
|
358
753
|
}
|
|
359
754
|
function safeRequestKey(value, fingerprint) {
|
|
360
755
|
if (value === undefined)
|
|
@@ -437,6 +832,7 @@ export class CodingProxy {
|
|
|
437
832
|
throw new Error("invalid_proxy_credential_reference");
|
|
438
833
|
const capability = `rrp_${randomBytes(32).toString("base64url")}`;
|
|
439
834
|
const id = randomUUID();
|
|
835
|
+
const registryTokenHash = capabilityHash(deriveRegistryToken(capability));
|
|
440
836
|
await this.ledger.createSession({
|
|
441
837
|
id,
|
|
442
838
|
runId: input.runId,
|
|
@@ -446,6 +842,7 @@ export class CodingProxy {
|
|
|
446
842
|
allowedModels: models,
|
|
447
843
|
deadlineAt: input.deadlineAt,
|
|
448
844
|
budgetUsd: input.budgetUsd,
|
|
845
|
+
registryTokenHash,
|
|
449
846
|
});
|
|
450
847
|
this.audit({ type: "session.created", runId: input.runId });
|
|
451
848
|
return {
|
|
@@ -463,13 +860,33 @@ export class CodingProxy {
|
|
|
463
860
|
for (const controller of this.activeRequests.get(sessionId) ?? [])
|
|
464
861
|
controller.abort();
|
|
465
862
|
}
|
|
863
|
+
/** Whether this session's run was refused a model request for budget. */
|
|
864
|
+
budgetExhausted(sessionId) {
|
|
865
|
+
return this.ledger.budgetExhausted(sessionId);
|
|
866
|
+
}
|
|
867
|
+
/** The code of the first upstream failure relayed for this session's run, if any. */
|
|
868
|
+
upstreamFailure(sessionId) {
|
|
869
|
+
return this.ledger.upstreamFailure(sessionId);
|
|
870
|
+
}
|
|
466
871
|
async execute(input, sink) {
|
|
467
872
|
const session = await this.authenticate(input.bearer);
|
|
468
873
|
if (session.protocol !== input.protocol) {
|
|
469
874
|
this.audit({ type: "request.rejected", runId: session.runId, reason: "protocol_mismatch" });
|
|
470
875
|
throw new CodingProxyError(403, "protocol_mismatch");
|
|
471
876
|
}
|
|
472
|
-
|
|
877
|
+
let parsed;
|
|
878
|
+
try {
|
|
879
|
+
parsed = parseRequest(input.protocol, input.rawBody, input.anthropicBeta);
|
|
880
|
+
}
|
|
881
|
+
catch (error) {
|
|
882
|
+
// The session is already authenticated and no credential has been
|
|
883
|
+
// resolved; record the refusal against the run so a smuggling attempt
|
|
884
|
+
// is attributable, then refuse.
|
|
885
|
+
if (error instanceof CodingProxyError) {
|
|
886
|
+
this.audit({ type: "request.rejected", runId: session.runId, status: error.status, reason: error.code });
|
|
887
|
+
}
|
|
888
|
+
throw error;
|
|
889
|
+
}
|
|
473
890
|
if (!session.allowedModels.includes(parsed.model)) {
|
|
474
891
|
this.audit({ type: "request.rejected", runId: session.runId, model: parsed.model, reason: "model_not_allowed" });
|
|
475
892
|
throw new CodingProxyError(403, "model_not_allowed");
|
|
@@ -558,6 +975,8 @@ export class CodingProxy {
|
|
|
558
975
|
const headers = {
|
|
559
976
|
"content-type": "application/json",
|
|
560
977
|
accept: parsed.stream ? "text/event-stream" : "application/json",
|
|
978
|
+
// The pinned fetch never decompresses, and the response is relayed as-is.
|
|
979
|
+
"accept-encoding": "identity",
|
|
561
980
|
};
|
|
562
981
|
if (session.protocol === "anthropic-messages") {
|
|
563
982
|
headers["x-api-key"] = key;
|
|
@@ -583,7 +1002,10 @@ export class CodingProxy {
|
|
|
583
1002
|
throw new CodingProxyError(502, controller.signal.aborted ? "upstream_aborted" : "upstream_transport_error");
|
|
584
1003
|
}
|
|
585
1004
|
if (!upstream.ok) {
|
|
1005
|
+
// Read while the deadline still bounds the body; the body itself is never relayed.
|
|
1006
|
+
const code = (await rejectedUpstreamErrorCode(upstream)) ?? `http_${upstream.status}`;
|
|
586
1007
|
cleanupActive();
|
|
1008
|
+
await this.recordUpstreamFailure(session, code);
|
|
587
1009
|
await this.ledger.release(request.id, upstream.status);
|
|
588
1010
|
this.audit({
|
|
589
1011
|
type: "request.released",
|
|
@@ -604,7 +1026,7 @@ export class CodingProxy {
|
|
|
604
1026
|
await this.forwardJson(session, request, upstream, sink);
|
|
605
1027
|
}
|
|
606
1028
|
catch (error) {
|
|
607
|
-
await this.markUncertain(session, request, upstream.status,
|
|
1029
|
+
await this.markUncertain(session, request, upstream.status, failureReason(error), responseHeadersOf(upstream));
|
|
608
1030
|
sink.destroy();
|
|
609
1031
|
throw error instanceof CodingProxyError ? error : new CodingProxyError(502, "invalid_upstream_response");
|
|
610
1032
|
}
|
|
@@ -681,6 +1103,7 @@ export class CodingProxy {
|
|
|
681
1103
|
let buffer = "";
|
|
682
1104
|
let receivedBytes = 0;
|
|
683
1105
|
let completed = false;
|
|
1106
|
+
let failedWithoutUsage = false;
|
|
684
1107
|
const anthropicUsage = session.protocol === "anthropic-messages" ? new AnthropicSseUsageTracker() : undefined;
|
|
685
1108
|
const body = upstream.body;
|
|
686
1109
|
for await (const chunk of body) {
|
|
@@ -699,23 +1122,61 @@ export class CodingProxy {
|
|
|
699
1122
|
await safeWrite(sink, Buffer.from(`${frame}\n\n`), connected);
|
|
700
1123
|
continue;
|
|
701
1124
|
}
|
|
702
|
-
if (!terminal.usage)
|
|
703
|
-
|
|
1125
|
+
if (!terminal.usage) {
|
|
1126
|
+
if (!terminal.failure)
|
|
1127
|
+
throw new Error("terminal_usage_missing");
|
|
1128
|
+
// The upstream failed the response and reported no usage. Pass its failure on so the
|
|
1129
|
+
// client sees the real error instead of a cut connection; the reservation stays held.
|
|
1130
|
+
await this.recordUpstreamFailure(session, terminal.failure);
|
|
1131
|
+
if (!completed) {
|
|
1132
|
+
await this.markUncertain(session, request, upstream.status, `upstream_failed:${terminal.failure}`, responseHeadersOf(upstream));
|
|
1133
|
+
}
|
|
1134
|
+
failedWithoutUsage = true;
|
|
1135
|
+
await safeWrite(sink, Buffer.from(`${frame}\n\n`), connected);
|
|
1136
|
+
continue;
|
|
1137
|
+
}
|
|
704
1138
|
await this.complete(session, request, terminal.usage, upstream.status);
|
|
705
1139
|
completed = true;
|
|
706
1140
|
await safeWrite(sink, Buffer.from(`${frame}\n\n`), connected);
|
|
707
1141
|
}
|
|
708
1142
|
}
|
|
709
1143
|
buffer = (buffer + decoder.decode()).replaceAll("\r\n", "\n");
|
|
710
|
-
if (!completed)
|
|
1144
|
+
if (!completed && !failedWithoutUsage)
|
|
711
1145
|
throw new Error("terminal_usage_missing");
|
|
712
1146
|
if (buffer)
|
|
713
1147
|
await safeWrite(sink, Buffer.from(buffer), connected);
|
|
714
1148
|
if (connected.value)
|
|
715
1149
|
sink.end();
|
|
716
1150
|
}
|
|
717
|
-
|
|
1151
|
+
/** Best effort: the executor reads it to name the failure; it must never break the relay. */
|
|
1152
|
+
async recordUpstreamFailure(session, code) {
|
|
1153
|
+
await this.ledger.recordUpstreamFailure(session.id, code).catch(() => undefined);
|
|
1154
|
+
}
|
|
1155
|
+
async markUncertain(session, request, status, reason, headers = {}) {
|
|
718
1156
|
await this.ledger.markUncertain(request.id, status);
|
|
719
|
-
this.audit({ type: "request.uncertain", runId: session.runId, requestId: request.id, status, reason });
|
|
1157
|
+
this.audit({ type: "request.uncertain", runId: session.runId, requestId: request.id, status, reason, ...headers });
|
|
720
1158
|
}
|
|
721
1159
|
}
|
|
1160
|
+
/** The safe error code in a rejected upstream's JSON body, or undefined when it names none or cannot be read. */
|
|
1161
|
+
async function rejectedUpstreamErrorCode(upstream) {
|
|
1162
|
+
try {
|
|
1163
|
+
const bytes = await readBoundedBody(upstream.body, MAX_UPSTREAM_ERROR_BYTES);
|
|
1164
|
+
const value = JSON.parse(Buffer.from(bytes).toString("utf8"));
|
|
1165
|
+
return value && typeof value === "object" ? safeUpstreamErrorCode(value) : undefined;
|
|
1166
|
+
}
|
|
1167
|
+
catch {
|
|
1168
|
+
return undefined;
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
/** The fixed code a relay failed with, or a generic one: error messages are not recorded. */
|
|
1172
|
+
function failureReason(error) {
|
|
1173
|
+
const message = error instanceof Error ? error.message : "";
|
|
1174
|
+
return /^[a-z_]{1,64}$/.test(message) ? message : "stream_read_failed";
|
|
1175
|
+
}
|
|
1176
|
+
/** The upstream's content type and encoding, trimmed to a short printable value, for the audit record. */
|
|
1177
|
+
function responseHeadersOf(upstream) {
|
|
1178
|
+
const clean = (value) => value ? value.replace(/[^\x20-\x7e]/g, "").slice(0, 100) || undefined : undefined;
|
|
1179
|
+
const contentType = clean(upstream.headers.get("content-type"));
|
|
1180
|
+
const contentEncoding = clean(upstream.headers.get("content-encoding"));
|
|
1181
|
+
return { ...(contentType ? { contentType } : {}), ...(contentEncoding ? { contentEncoding } : {}) };
|
|
1182
|
+
}
|