@wardby/cli 0.2.1 → 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 +41 -4
- package/dist/claude-coding-worker/driver.d.ts +6 -1
- package/dist/claude-coding-worker/driver.js +27 -1
- package/dist/claude-coding-worker/main.js +2 -0
- 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 +75 -4
- package/dist/coding/protocol.js +107 -7
- 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 +6 -0
- 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 +13 -1
- package/dist/coding-worker/driver.js +102 -13
- package/dist/coding-worker/errors.js +8 -0
- package/dist/coding-worker/main.js +10 -2
- 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 +108 -5
- 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/datastores.js +18 -2
- package/dist/core/db.d.ts +5 -1
- package/dist/core/db.js +8 -2
- package/dist/core/dispatch.d.ts +33 -6
- package/dist/core/dispatch.js +260 -43
- 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/in-flight-runs.d.ts +8 -0
- package/dist/core/in-flight-runs.js +56 -0
- package/dist/core/provider-wording.d.ts +12 -0
- package/dist/core/provider-wording.js +44 -0
- package/dist/core/reconciler.d.ts +38 -2
- 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 +30 -8
- package/dist/core/runner.js +237 -40
- package/dist/core/secrets.d.ts +11 -2
- package/dist/core/secrets.js +25 -6
- package/dist/core/subagent-memory-tools.d.ts +1 -1
- 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 +8 -1
- package/dist/core/webhooks.js +23 -2
- package/dist/generated/prisma/browser.d.ts +100 -0
- package/dist/generated/prisma/client.d.ts +100 -0
- package/dist/generated/prisma/commonInputTypes.d.ts +30 -0
- package/dist/generated/prisma/enums.d.ts +6 -0
- package/dist/generated/prisma/enums.js +6 -1
- package/dist/generated/prisma/internal/class.d.ts +143 -0
- package/dist/generated/prisma/internal/class.js +4 -4
- package/dist/generated/prisma/internal/prismaNamespace.d.ts +1746 -577
- package/dist/generated/prisma/internal/prismaNamespace.js +179 -6
- package/dist/generated/prisma/internal/prismaNamespaceBrowser.d.ts +186 -0
- package/dist/generated/prisma/internal/prismaNamespaceBrowser.js +179 -6
- package/dist/generated/prisma/models/Agent.d.ts +294 -1
- package/dist/generated/prisma/models/AgentRepository.d.ts +1425 -0
- package/dist/generated/prisma/models/AgentRepository.js +1 -0
- package/dist/generated/prisma/models/AgentTool.d.ts +95 -1
- package/dist/generated/prisma/models/AuthUser.d.ts +56 -1
- package/dist/generated/prisma/models/CodingAgentProfile.d.ts +258 -7
- package/dist/generated/prisma/models/CodingProxySession.d.ts +123 -2
- package/dist/generated/prisma/models/CodingRun.d.ts +1276 -95
- package/dist/generated/prisma/models/CodingService.d.ts +1348 -0
- package/dist/generated/prisma/models/CodingService.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/Principal.d.ts +455 -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 +389 -1
- 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/Tool.d.ts +15 -3
- package/dist/generated/prisma/models.d.ts +13 -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 +5 -0
- package/dist/import/create.js +67 -13
- 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 +26 -52
- package/dist/mcp/auth/ownership.js +19 -14
- 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 +31 -2
- package/dist/mcp/auth/resource-server.js +84 -3
- package/dist/mcp/auth/self-hosted/browser.js +2 -2
- package/dist/mcp/auth/self-hosted/cli.js +47 -13
- package/dist/mcp/auth/self-hosted/credentials.d.ts +20 -2
- package/dist/mcp/auth/self-hosted/credentials.js +62 -4
- package/dist/mcp/auth/self-hosted/session.d.ts +2 -1
- package/dist/mcp/context.d.ts +27 -1
- 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.js +27 -9
- package/dist/mcp/tools/agents.js +446 -48
- package/dist/mcp/tools/budget-groups.js +3 -3
- package/dist/mcp/tools/datastore.js +22 -15
- 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 +3 -6
- 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 +52 -16
- package/dist/mcp/tools/tools.d.ts +41 -0
- package/dist/mcp/tools/tools.js +201 -40
- package/dist/mcp/tools/trigger.js +36 -6
- package/dist/mcp/tools/webhooks.js +15 -4
- package/dist/mcp/transport/streamable-http.d.ts +10 -0
- package/dist/mcp/transport/streamable-http.js +32 -2
- package/dist/providers/auth/delegating.d.ts +19 -0
- package/dist/providers/auth/delegating.js +71 -0
- package/dist/providers/auth/index.d.ts +2 -0
- package/dist/providers/auth/index.js +11 -0
- package/dist/providers/auth/self-hosted.d.ts +2 -1
- 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 +17 -0
- 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 +17 -0
- 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/engine/types.d.ts +10 -0
- package/dist/providers/executor/build.d.ts +2 -2
- package/dist/providers/executor/composition.d.ts +3 -0
- package/dist/providers/executor/composition.js +19 -0
- package/dist/providers/executor/container.d.ts +105 -3
- package/dist/providers/executor/container.js +296 -20
- package/dist/providers/executor/dbos.d.ts +2 -3
- 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 +49 -2
- package/dist/providers/jobs/docker.js +279 -32
- 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 +112 -21
- package/dist/providers/jobs/types.d.ts +18 -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/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 +77 -13
- 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 +13 -2
- 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 +12 -3
- 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 +407 -21
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { storedServiceNames } from "../coding/services/catalog.js";
|
|
2
|
+
import { SERVICE_UNREADY_CATEGORY, serviceRefusalSentence, serviceUnreadySentence, } from "../coding/services/wording.js";
|
|
3
|
+
import { PROTECTED_PATH_CATEGORY, PROTECTED_PATH_HOST_LINE } from "../coding/protected-path-wording.js";
|
|
4
|
+
import { loadBudgetSentence } from "./budget-wording.js";
|
|
5
|
+
import { logger } from "./logger.js";
|
|
6
|
+
import { providerClassOfCategory, providerSentence } from "./provider-wording.js";
|
|
7
|
+
const log = logger.child({ module: "host-status" });
|
|
8
|
+
/** Keeps the edited comment readable; the full reply stays on the run. */
|
|
9
|
+
const MAX_REPLY_CHARS = 2000;
|
|
10
|
+
const TERMINAL = new Set(["succeeded", "failed", "refused", "lost", "budget_exhausted", "cancelled"]);
|
|
11
|
+
const runLine = (runId) => `<sub>wardby run \`${runId}\`</sub>`;
|
|
12
|
+
export function workingBody(runId) {
|
|
13
|
+
return `👀 Working on it.\n\n${runLine(runId)}`;
|
|
14
|
+
}
|
|
15
|
+
/** The status row for a mention run, created in the same transaction as the run. */
|
|
16
|
+
export function mentionStatusRow(provider, event, runId) {
|
|
17
|
+
return {
|
|
18
|
+
runId,
|
|
19
|
+
provider,
|
|
20
|
+
repository: event.repository,
|
|
21
|
+
number: event.number,
|
|
22
|
+
commentKind: event.replyToReviewCommentId ? "inline" : "conversation",
|
|
23
|
+
replyToReviewCommentId: event.replyToReviewCommentId ?? null,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function pullRequestOutcome(result) {
|
|
27
|
+
if (!result || typeof result !== "object")
|
|
28
|
+
return null;
|
|
29
|
+
const r = result;
|
|
30
|
+
if (r.outcome !== "pull_request_opened" && r.outcome !== "pull_request_updated")
|
|
31
|
+
return null;
|
|
32
|
+
if (typeof r.repository !== "string" || typeof r.pullRequestNumber !== "number")
|
|
33
|
+
return null;
|
|
34
|
+
if (!Number.isInteger(r.pullRequestNumber) || r.pullRequestNumber <= 0)
|
|
35
|
+
return null;
|
|
36
|
+
return { outcome: r.outcome, repository: r.repository, pullRequestNumber: r.pullRequestNumber };
|
|
37
|
+
}
|
|
38
|
+
/** The agent's reply as a quote, cut to MAX_REPLY_CHARS, with @-mentions defused so nobody is pinged. */
|
|
39
|
+
function quoteReply(text) {
|
|
40
|
+
const cut = text.length > MAX_REPLY_CHARS ? `${text.slice(0, MAX_REPLY_CHARS)}…` : text;
|
|
41
|
+
return cut
|
|
42
|
+
.replace(/@(?=[\w-])/g, "@")
|
|
43
|
+
.split("\n")
|
|
44
|
+
.map((line) => `> ${line}`)
|
|
45
|
+
.join("\n");
|
|
46
|
+
}
|
|
47
|
+
export function outcomeBody(run, repository, pullRequests, failedChildren = [], opts = {}) {
|
|
48
|
+
const links = pullRequests.map((pr) => {
|
|
49
|
+
const ref = pr.repository.toLowerCase() === repository.toLowerCase()
|
|
50
|
+
? `#${pr.pullRequestNumber}`
|
|
51
|
+
: `${pr.repository}#${pr.pullRequestNumber}`;
|
|
52
|
+
return pr.outcome === "pull_request_opened" ? `Opened ${ref}` : `Pushed changes to ${ref}`;
|
|
53
|
+
});
|
|
54
|
+
const partial = links.length > 0 ? `\n\n${links.join(", ")}.` : "";
|
|
55
|
+
const footer = runLine(run.id);
|
|
56
|
+
if (run.status === "lost") {
|
|
57
|
+
return `❌ Interrupted before it finished (for example, wardby restarted). Repeat your request to retry.${partial}\n\n${footer}`;
|
|
58
|
+
}
|
|
59
|
+
if (run.status === "budget_exhausted" || run.status === "refused") {
|
|
60
|
+
// A refused run could not start for lack of budget; see budgetSentence.
|
|
61
|
+
return `❌ ${opts.budgetSentence ?? "Out of budget."}${partial}\n\n${footer}`;
|
|
62
|
+
}
|
|
63
|
+
if (run.status !== "succeeded") {
|
|
64
|
+
// Only the status: a run's error text can carry internal detail that does not belong on the host.
|
|
65
|
+
return `❌ Stopped: the run ended with status \`${run.status}\`.${partial}\n\n${footer}`;
|
|
66
|
+
}
|
|
67
|
+
const reply = run.finalText?.trim();
|
|
68
|
+
const quoted = reply ? `\n\n${quoteReply(reply)}` : "";
|
|
69
|
+
if (failedChildren.length > 0) {
|
|
70
|
+
// The agent itself finished, but the work it handed off did not.
|
|
71
|
+
const outOfBudget = failedChildren.filter((c) => c.status === "budget_exhausted");
|
|
72
|
+
const providerClassOf = (c) => c.status === "failed" ? providerClassOfCategory(c.failureCategory) : null;
|
|
73
|
+
// A coding sub-run its services stopped: refused at dispatch, or a sidecar never became ready.
|
|
74
|
+
const serviceSentenceOf = (c) => c.status === "refused"
|
|
75
|
+
? serviceRefusalSentence(c.error)
|
|
76
|
+
: c.status === "failed" && c.failureCategory === SERVICE_UNREADY_CATEGORY
|
|
77
|
+
? serviceUnreadySentence(c.services ?? [])
|
|
78
|
+
: null;
|
|
79
|
+
// No path reaches this comment (see coding/protected-path-wording.ts), only the category.
|
|
80
|
+
const protectedPathOf = (c) => c.status === "failed" && c.failureCategory === PROTECTED_PATH_CATEGORY;
|
|
81
|
+
const providerClasses = new Set(failedChildren.map(providerClassOf).filter((c) => c !== null));
|
|
82
|
+
const serviceSentences = new Set(failedChildren.map(serviceSentenceOf).filter((s) => s !== null));
|
|
83
|
+
const protectedPathFailed = failedChildren.some(protectedPathOf);
|
|
84
|
+
const other = failedChildren.filter((c) => c.status !== "budget_exhausted" &&
|
|
85
|
+
providerClassOf(c) === null &&
|
|
86
|
+
serviceSentenceOf(c) === null &&
|
|
87
|
+
!protectedPathOf(c));
|
|
88
|
+
const lines = [];
|
|
89
|
+
if (outOfBudget.length > 0) {
|
|
90
|
+
lines.push(`A sub-run ran out of budget: ${outOfBudget.map((c) => `\`${c.id}\``).join(", ")}.`);
|
|
91
|
+
}
|
|
92
|
+
// The class only, never the provider's code: the sentence says who has to act.
|
|
93
|
+
for (const providerClass of providerClasses) {
|
|
94
|
+
lines.push(`A sub-run could not reach the model: ${providerSentence(providerClass)}`);
|
|
95
|
+
}
|
|
96
|
+
// The fixed sentence only, never the Run.error code in front of it.
|
|
97
|
+
for (const sentence of serviceSentences) {
|
|
98
|
+
lines.push(`A sub-run could not start: ${sentence}`);
|
|
99
|
+
}
|
|
100
|
+
if (protectedPathFailed)
|
|
101
|
+
lines.push(PROTECTED_PATH_HOST_LINE);
|
|
102
|
+
if (other.length > 0) {
|
|
103
|
+
lines.push(`A sub-run did not succeed: ${other.map((c) => `\`${c.id}\` (\`${c.status}\`)`).join(", ")}.`);
|
|
104
|
+
}
|
|
105
|
+
return `❌ ${lines.join(" ")}${partial}${quoted}\n\n${footer}`;
|
|
106
|
+
}
|
|
107
|
+
if (links.length > 0)
|
|
108
|
+
return `✅ ${links.join(", ")}.\n\n${footer}`;
|
|
109
|
+
return `✅ Finished without opening a pull request.${quoted}\n\n${footer}`;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Writes a finished run's outcome to its status comment and marks it
|
|
113
|
+
* complete: edits the comment when it exists; when it does not yet, posts the
|
|
114
|
+
* outcome as a new comment only if `postIfMissing` (the reconciler, well after
|
|
115
|
+
* the run ended), and otherwise leaves the row for postMentionStatus, which
|
|
116
|
+
* is about to post it. A failed host call leaves the row open for the
|
|
117
|
+
* reconciler to retry.
|
|
118
|
+
*/
|
|
119
|
+
export async function completeHostStatus(db, run, hosts, opts = {}) {
|
|
120
|
+
if (!hosts)
|
|
121
|
+
return;
|
|
122
|
+
try {
|
|
123
|
+
const status = await db.runHostStatus.findUnique({ where: { runId: run.id } });
|
|
124
|
+
if (!status || status.completedAt)
|
|
125
|
+
return;
|
|
126
|
+
if (!status.commentId && !opts.postIfMissing)
|
|
127
|
+
return;
|
|
128
|
+
const host = hosts[status.provider];
|
|
129
|
+
if (!host)
|
|
130
|
+
return;
|
|
131
|
+
const children = await db.run.findMany({
|
|
132
|
+
where: { parentRunId: run.id },
|
|
133
|
+
select: {
|
|
134
|
+
id: true,
|
|
135
|
+
status: true,
|
|
136
|
+
error: true,
|
|
137
|
+
codingRun: { select: { result: true, failureCategory: true, services: true } },
|
|
138
|
+
},
|
|
139
|
+
orderBy: { startedAt: "asc" },
|
|
140
|
+
});
|
|
141
|
+
const pullRequests = children
|
|
142
|
+
.map((c) => pullRequestOutcome(c.codingRun?.result))
|
|
143
|
+
.filter((pr) => pr !== null);
|
|
144
|
+
const failedChildren = children
|
|
145
|
+
.filter((c) => TERMINAL.has(c.status) && c.status !== "succeeded")
|
|
146
|
+
.map((c) => ({
|
|
147
|
+
id: c.id,
|
|
148
|
+
status: c.status,
|
|
149
|
+
failureCategory: c.codingRun?.failureCategory ?? null,
|
|
150
|
+
error: c.error ?? null,
|
|
151
|
+
services: storedServiceNames(c.codingRun?.services),
|
|
152
|
+
}));
|
|
153
|
+
const budgetSentence = run.status === "budget_exhausted" || run.status === "refused"
|
|
154
|
+
? await loadBudgetSentence(db, run.id, run.status)
|
|
155
|
+
: undefined;
|
|
156
|
+
const body = outcomeBody(run, status.repository, pullRequests, failedChildren, { budgetSentence });
|
|
157
|
+
let commentId = status.commentId;
|
|
158
|
+
if (commentId) {
|
|
159
|
+
await host.editComment(status.repository, {
|
|
160
|
+
kind: status.commentKind === "inline" ? "inline" : "conversation",
|
|
161
|
+
id: commentId,
|
|
162
|
+
body,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
commentId = (await host.comment(status.repository, {
|
|
167
|
+
number: status.number,
|
|
168
|
+
body,
|
|
169
|
+
...(status.replyToReviewCommentId ? { replyToReviewCommentId: status.replyToReviewCommentId } : {}),
|
|
170
|
+
})).id;
|
|
171
|
+
}
|
|
172
|
+
await db.runHostStatus.update({ where: { runId: run.id }, data: { commentId, completedAt: new Date() } });
|
|
173
|
+
}
|
|
174
|
+
catch (err) {
|
|
175
|
+
log.warn({ err, runId: run.id }, "could not complete the run's status comment");
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Posts the "working on it" comment for a mention run whose status row was
|
|
180
|
+
* written at dispatch, and records the comment on the row. When the run
|
|
181
|
+
* already ended (a fast failure can beat this follow-up), completes it at
|
|
182
|
+
* once. Does nothing when the row is gone, already has a comment, or is
|
|
183
|
+
* already complete. Never throws.
|
|
184
|
+
*/
|
|
185
|
+
export async function postMentionStatus(db, host, runId, hosts) {
|
|
186
|
+
try {
|
|
187
|
+
const status = await db.runHostStatus.findUnique({ where: { runId } });
|
|
188
|
+
if (!status || status.commentId || status.completedAt)
|
|
189
|
+
return;
|
|
190
|
+
const posted = await host.comment(status.repository, {
|
|
191
|
+
number: status.number,
|
|
192
|
+
body: workingBody(runId),
|
|
193
|
+
...(status.replyToReviewCommentId ? { replyToReviewCommentId: status.replyToReviewCommentId } : {}),
|
|
194
|
+
});
|
|
195
|
+
const claimed = await db.runHostStatus.updateMany({
|
|
196
|
+
where: { runId, commentId: null, completedAt: null },
|
|
197
|
+
data: { commentId: posted.id },
|
|
198
|
+
});
|
|
199
|
+
if (claimed.count === 0) {
|
|
200
|
+
// The reconciler posted the outcome meanwhile; this comment is surplus.
|
|
201
|
+
log.warn({ runId }, "status comment posted after the outcome; leaving both");
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
const run = await db.run.findUnique({ where: { id: runId }, select: { id: true, status: true, finalText: true } });
|
|
205
|
+
if (run && TERMINAL.has(run.status))
|
|
206
|
+
await completeHostStatus(db, run, hosts);
|
|
207
|
+
}
|
|
208
|
+
catch (err) {
|
|
209
|
+
log.warn({ err, runId }, "could not post the status comment");
|
|
210
|
+
}
|
|
211
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Registers a run for the duration of `work`. */
|
|
2
|
+
export declare function trackRun<T>(runId: string, work: () => Promise<T>): Promise<T>;
|
|
3
|
+
export declare function inFlightRunIds(): string[];
|
|
4
|
+
/**
|
|
5
|
+
* Resolves once no run is executing, or after `timeoutMs`, whichever is
|
|
6
|
+
* first. Returns the ids still executing at that point (empty when drained).
|
|
7
|
+
*/
|
|
8
|
+
export declare function waitForInFlightRuns(timeoutMs: number): Promise<string[]>;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Counts the native runs executing in this process, so a shutdown can wait
|
|
3
|
+
* for them instead of abandoning them. Every native run, however it was
|
|
4
|
+
* started (a direct start, a DBOS workflow, a workflow adopted from a dead
|
|
5
|
+
* instance), goes through executeRun, which registers here. Coding jobs are
|
|
6
|
+
* not counted: they run in their own pods and a new instance collects them.
|
|
7
|
+
*/
|
|
8
|
+
import { logger } from "./logger.js";
|
|
9
|
+
const log = logger.child({ module: "in-flight-runs" });
|
|
10
|
+
const active = new Set();
|
|
11
|
+
let waiters = [];
|
|
12
|
+
/** Registers a run for the duration of `work`. */
|
|
13
|
+
export async function trackRun(runId, work) {
|
|
14
|
+
active.add(runId);
|
|
15
|
+
try {
|
|
16
|
+
return await work();
|
|
17
|
+
}
|
|
18
|
+
finally {
|
|
19
|
+
active.delete(runId);
|
|
20
|
+
if (active.size === 0) {
|
|
21
|
+
const done = waiters;
|
|
22
|
+
waiters = [];
|
|
23
|
+
for (const wake of done)
|
|
24
|
+
wake();
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export function inFlightRunIds() {
|
|
29
|
+
return [...active];
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Resolves once no run is executing, or after `timeoutMs`, whichever is
|
|
33
|
+
* first. Returns the ids still executing at that point (empty when drained).
|
|
34
|
+
*/
|
|
35
|
+
export async function waitForInFlightRuns(timeoutMs) {
|
|
36
|
+
if (active.size === 0 || timeoutMs <= 0)
|
|
37
|
+
return inFlightRunIds();
|
|
38
|
+
log.info({ runIds: inFlightRunIds(), timeoutMs }, "waiting for in-flight runs before shutdown");
|
|
39
|
+
await new Promise((resolve) => {
|
|
40
|
+
const timer = setTimeout(() => {
|
|
41
|
+
waiters = waiters.filter((w) => w !== wake);
|
|
42
|
+
resolve();
|
|
43
|
+
}, timeoutMs);
|
|
44
|
+
const wake = () => {
|
|
45
|
+
clearTimeout(timer);
|
|
46
|
+
resolve();
|
|
47
|
+
};
|
|
48
|
+
waiters.push(wake);
|
|
49
|
+
});
|
|
50
|
+
const remaining = inFlightRunIds();
|
|
51
|
+
if (remaining.length > 0)
|
|
52
|
+
log.warn({ runIds: remaining }, "shutdown drain timed out; these runs are abandoned");
|
|
53
|
+
else
|
|
54
|
+
log.info("in-flight runs finished; shutting down");
|
|
55
|
+
return remaining;
|
|
56
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a host (a PR comment, a check summary) is told when the model provider
|
|
3
|
+
* itself refused a run's requests: the class of problem and who has to act.
|
|
4
|
+
* Public repositories read these, so they never carry the provider's own
|
|
5
|
+
* error code or message; the code stays in the operator log.
|
|
6
|
+
*/
|
|
7
|
+
export type ProviderFailureClass = "quota" | "rate_limited" | "unavailable" | "rejected";
|
|
8
|
+
/** Files an upstream error code (or `http_<status>`) under the class of problem it names. */
|
|
9
|
+
export declare function classifyProviderFailure(code: string): ProviderFailureClass;
|
|
10
|
+
export declare function providerSentence(providerClass: ProviderFailureClass): string;
|
|
11
|
+
/** The class a run's `provider_<class>` failure category names, or null for any other category. */
|
|
12
|
+
export declare function providerClassOfCategory(category: string | null | undefined): ProviderFailureClass | null;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a host (a PR comment, a check summary) is told when the model provider
|
|
3
|
+
* itself refused a run's requests: the class of problem and who has to act.
|
|
4
|
+
* Public repositories read these, so they never carry the provider's own
|
|
5
|
+
* error code or message; the code stays in the operator log.
|
|
6
|
+
*/
|
|
7
|
+
const CLASSES = ["quota", "rate_limited", "unavailable", "rejected"];
|
|
8
|
+
const QUOTA_MARKERS = ["quota", "spend_limit", "billing", "insufficient_funds", "credit"];
|
|
9
|
+
const UNAVAILABLE_CODES = new Set([
|
|
10
|
+
"server_error",
|
|
11
|
+
"overloaded",
|
|
12
|
+
"overloaded_error",
|
|
13
|
+
"service_unavailable",
|
|
14
|
+
// Anthropic's name for an internal server error.
|
|
15
|
+
"api_error",
|
|
16
|
+
]);
|
|
17
|
+
/** Files an upstream error code (or `http_<status>`) under the class of problem it names. */
|
|
18
|
+
export function classifyProviderFailure(code) {
|
|
19
|
+
const lower = code.toLowerCase();
|
|
20
|
+
// Checked first: an exhausted account is the one case only an operator can fix.
|
|
21
|
+
if (QUOTA_MARKERS.some((marker) => lower.includes(marker)))
|
|
22
|
+
return "quota";
|
|
23
|
+
if (lower.includes("rate_limit"))
|
|
24
|
+
return "rate_limited";
|
|
25
|
+
if (UNAVAILABLE_CODES.has(lower) || /^http_5\d\d$/.test(lower))
|
|
26
|
+
return "unavailable";
|
|
27
|
+
return "rejected";
|
|
28
|
+
}
|
|
29
|
+
const SENTENCES = {
|
|
30
|
+
quota: "The model provider refused the request: its account has reached a spending or quota limit. An operator needs to raise the limit with the provider, then retry.",
|
|
31
|
+
rate_limited: "The model provider is rate-limiting requests. Try again later.",
|
|
32
|
+
unavailable: "The model provider reported an outage or overload. Try again later.",
|
|
33
|
+
rejected: "The model provider rejected the request.",
|
|
34
|
+
};
|
|
35
|
+
export function providerSentence(providerClass) {
|
|
36
|
+
return SENTENCES[providerClass];
|
|
37
|
+
}
|
|
38
|
+
/** The class a run's `provider_<class>` failure category names, or null for any other category. */
|
|
39
|
+
export function providerClassOfCategory(category) {
|
|
40
|
+
if (!category?.startsWith("provider_"))
|
|
41
|
+
return null;
|
|
42
|
+
const value = category.slice("provider_".length);
|
|
43
|
+
return CLASSES.includes(value) ? value : null;
|
|
44
|
+
}
|
|
@@ -27,17 +27,53 @@
|
|
|
27
27
|
* UPDATE commits first flips the row out of the matched status, so the
|
|
28
28
|
* second one's WHERE simply matches zero rows — double-reconcile is a
|
|
29
29
|
* no-op, not a race.
|
|
30
|
+
*
|
|
31
|
+
* Each pass also completes review-host checks left "in progress" by a run
|
|
32
|
+
* that ended without reaching `executeRun`'s own finalizer (reaped here as
|
|
33
|
+
* `lost`, failed to start, failed while loading, ...). One sweep covers all
|
|
34
|
+
* of those paths instead of patching each. Mention status comments get the
|
|
35
|
+
* same sweep: it also catches a run that ended before its comment was posted.
|
|
30
36
|
*/
|
|
31
37
|
import type { PrismaClient } from "#prisma";
|
|
32
38
|
import type { Executor } from "../providers/executor/types.js";
|
|
33
|
-
|
|
39
|
+
import type { ReviewHostRegistry } from "../providers/review-host/types.js";
|
|
40
|
+
export type ReconcilerDb = Pick<PrismaClient, "run" | "runHostCheck" | "runHostStatus">;
|
|
41
|
+
/**
|
|
42
|
+
* How long after a run finishes before its still-open check counts as
|
|
43
|
+
* orphaned: long enough that the sweep never races `executeRun`'s own
|
|
44
|
+
* `closeOpenHostCheck` for a run that just ended.
|
|
45
|
+
*/
|
|
46
|
+
export declare const ORPHANED_CHECK_GRACE_MS = 60000;
|
|
47
|
+
/** Upper bound on checks completed per pass, so a backlog drains over several passes. */
|
|
48
|
+
export declare const ORPHANED_CHECK_BATCH = 50;
|
|
49
|
+
/** How long after a run finishes the sweep keeps trying to complete its check. */
|
|
50
|
+
export declare const ORPHANED_CHECK_MAX_AGE_MS: number;
|
|
51
|
+
/**
|
|
52
|
+
* Completes (as a failure) open host checks whose run reached a terminal status
|
|
53
|
+
* at least ORPHANED_CHECK_GRACE_MS and at most ORPHANED_CHECK_MAX_AGE_MS
|
|
54
|
+
* ago. Newest-finished first, so old checks the host keeps refusing cannot
|
|
55
|
+
* starve newer ones. A check the host still refuses after the max age (the
|
|
56
|
+
* check was deleted, the App was uninstalled, ...) is left as it is rather
|
|
57
|
+
* than retried forever; its Re-run button still works. Never queries when
|
|
58
|
+
* no host is configured. `closeOpenHostCheck` is best-effort and never throws.
|
|
59
|
+
*/
|
|
60
|
+
export declare function closeOrphanedHostChecks(db: Pick<PrismaClient, "runHostCheck" | "run">, hosts: ReviewHostRegistry | undefined, now?: Date): Promise<void>;
|
|
61
|
+
/**
|
|
62
|
+
* Completes open mention status comments on the same terms as
|
|
63
|
+
* closeOrphanedHostChecks: the run ended between ORPHANED_CHECK_GRACE_MS and
|
|
64
|
+
* ORPHANED_CHECK_MAX_AGE_MS ago, newest first, at most ORPHANED_CHECK_BATCH per
|
|
65
|
+
* pass. `completeHostStatus` is best-effort and never throws.
|
|
66
|
+
*/
|
|
67
|
+
export declare function closeOrphanedHostStatuses(db: Pick<PrismaClient, "runHostStatus" | "run">, hosts: ReviewHostRegistry | undefined, now?: Date): Promise<void>;
|
|
34
68
|
/** Runs one reconciliation pass. Returns the number of runs marked `lost`. */
|
|
35
|
-
export declare function reconcileOnce(db: ReconcilerDb, now?: Date, heartbeatTimeoutMs?: number, executor?: Executor): Promise<number>;
|
|
69
|
+
export declare function reconcileOnce(db: ReconcilerDb, now?: Date, heartbeatTimeoutMs?: number, executor?: Executor, reviewHosts?: ReviewHostRegistry): Promise<number>;
|
|
36
70
|
export interface ReconcilerOptions {
|
|
37
71
|
db?: ReconcilerDb;
|
|
38
72
|
intervalMs?: number;
|
|
39
73
|
heartbeatTimeoutMs?: number;
|
|
40
74
|
executor?: Executor;
|
|
75
|
+
/** Hosts used to complete checks orphaned by runs that ended abnormally; none configured = no sweep. */
|
|
76
|
+
reviewHosts?: ReviewHostRegistry;
|
|
41
77
|
}
|
|
42
78
|
export interface ReconcilerHandle {
|
|
43
79
|
stop(): void;
|
package/dist/core/reconciler.js
CHANGED
|
@@ -27,13 +27,95 @@
|
|
|
27
27
|
* UPDATE commits first flips the row out of the matched status, so the
|
|
28
28
|
* second one's WHERE simply matches zero rows — double-reconcile is a
|
|
29
29
|
* no-op, not a race.
|
|
30
|
+
*
|
|
31
|
+
* Each pass also completes review-host checks left "in progress" by a run
|
|
32
|
+
* that ended without reaching `executeRun`'s own finalizer (reaped here as
|
|
33
|
+
* `lost`, failed to start, failed while loading, ...). One sweep covers all
|
|
34
|
+
* of those paths instead of patching each. Mention status comments get the
|
|
35
|
+
* same sweep: it also catches a run that ended before its comment was posted.
|
|
30
36
|
*/
|
|
31
37
|
import { HEARTBEAT_TIMEOUT_MS, RECONCILE_INTERVAL_MS } from "./timing.js";
|
|
32
38
|
import { prisma as defaultDb } from "./db.js";
|
|
33
39
|
import { logger } from "./logger.js";
|
|
40
|
+
import { closeOpenHostCheck } from "./review-host-checks.js";
|
|
41
|
+
import { completeHostStatus } from "./host-status.js";
|
|
34
42
|
const reconcilerLog = logger.child({ module: "reconciler" });
|
|
43
|
+
/**
|
|
44
|
+
* How long after a run finishes before its still-open check counts as
|
|
45
|
+
* orphaned: long enough that the sweep never races `executeRun`'s own
|
|
46
|
+
* `closeOpenHostCheck` for a run that just ended.
|
|
47
|
+
*/
|
|
48
|
+
export const ORPHANED_CHECK_GRACE_MS = 60_000;
|
|
49
|
+
/** Upper bound on checks completed per pass, so a backlog drains over several passes. */
|
|
50
|
+
export const ORPHANED_CHECK_BATCH = 50;
|
|
51
|
+
/** How long after a run finishes the sweep keeps trying to complete its check. */
|
|
52
|
+
export const ORPHANED_CHECK_MAX_AGE_MS = 24 * 60 * 60 * 1000;
|
|
53
|
+
/**
|
|
54
|
+
* Completes (as a failure) open host checks whose run reached a terminal status
|
|
55
|
+
* at least ORPHANED_CHECK_GRACE_MS and at most ORPHANED_CHECK_MAX_AGE_MS
|
|
56
|
+
* ago. Newest-finished first, so old checks the host keeps refusing cannot
|
|
57
|
+
* starve newer ones. A check the host still refuses after the max age (the
|
|
58
|
+
* check was deleted, the App was uninstalled, ...) is left as it is rather
|
|
59
|
+
* than retried forever; its Re-run button still works. Never queries when
|
|
60
|
+
* no host is configured. `closeOpenHostCheck` is best-effort and never throws.
|
|
61
|
+
*/
|
|
62
|
+
export async function closeOrphanedHostChecks(db, hosts, now = new Date()) {
|
|
63
|
+
const providers = Object.keys(hosts ?? {});
|
|
64
|
+
if (!hosts || providers.length === 0)
|
|
65
|
+
return;
|
|
66
|
+
const orphans = await db.runHostCheck.findMany({
|
|
67
|
+
where: {
|
|
68
|
+
completedAt: null,
|
|
69
|
+
// A check for a provider that isn't configured could never be completed; keep it out of the batch.
|
|
70
|
+
provider: { in: providers },
|
|
71
|
+
run: {
|
|
72
|
+
status: { notIn: ["pending", "running"] },
|
|
73
|
+
finishedAt: {
|
|
74
|
+
lte: new Date(now.getTime() - ORPHANED_CHECK_GRACE_MS),
|
|
75
|
+
gte: new Date(now.getTime() - ORPHANED_CHECK_MAX_AGE_MS),
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
select: { run: { select: { id: true, status: true } } },
|
|
80
|
+
orderBy: { run: { finishedAt: "desc" } },
|
|
81
|
+
take: ORPHANED_CHECK_BATCH,
|
|
82
|
+
});
|
|
83
|
+
for (const { run } of orphans)
|
|
84
|
+
await closeOpenHostCheck(db, run, hosts);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Completes open mention status comments on the same terms as
|
|
88
|
+
* closeOrphanedHostChecks: the run ended between ORPHANED_CHECK_GRACE_MS and
|
|
89
|
+
* ORPHANED_CHECK_MAX_AGE_MS ago, newest first, at most ORPHANED_CHECK_BATCH per
|
|
90
|
+
* pass. `completeHostStatus` is best-effort and never throws.
|
|
91
|
+
*/
|
|
92
|
+
export async function closeOrphanedHostStatuses(db, hosts, now = new Date()) {
|
|
93
|
+
const providers = Object.keys(hosts ?? {});
|
|
94
|
+
if (!hosts || providers.length === 0)
|
|
95
|
+
return;
|
|
96
|
+
const orphans = await db.runHostStatus.findMany({
|
|
97
|
+
where: {
|
|
98
|
+
completedAt: null,
|
|
99
|
+
provider: { in: providers },
|
|
100
|
+
run: {
|
|
101
|
+
status: { notIn: ["pending", "running"] },
|
|
102
|
+
finishedAt: {
|
|
103
|
+
lte: new Date(now.getTime() - ORPHANED_CHECK_GRACE_MS),
|
|
104
|
+
gte: new Date(now.getTime() - ORPHANED_CHECK_MAX_AGE_MS),
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
select: { run: { select: { id: true, status: true, finalText: true } } },
|
|
109
|
+
orderBy: { run: { finishedAt: "desc" } },
|
|
110
|
+
take: ORPHANED_CHECK_BATCH,
|
|
111
|
+
});
|
|
112
|
+
// postIfMissing: by now the dispatching instance's follow-up has had its
|
|
113
|
+
// chance; a run that ended with no comment (it died first) gets one here.
|
|
114
|
+
for (const { run } of orphans)
|
|
115
|
+
await completeHostStatus(db, run, hosts, { postIfMissing: true });
|
|
116
|
+
}
|
|
35
117
|
/** Runs one reconciliation pass. Returns the number of runs marked `lost`. */
|
|
36
|
-
export async function reconcileOnce(db, now = new Date(), heartbeatTimeoutMs = HEARTBEAT_TIMEOUT_MS, executor) {
|
|
118
|
+
export async function reconcileOnce(db, now = new Date(), heartbeatTimeoutMs = HEARTBEAT_TIMEOUT_MS, executor, reviewHosts) {
|
|
37
119
|
const cutoff = new Date(now.getTime() - heartbeatTimeoutMs);
|
|
38
120
|
const stale = {
|
|
39
121
|
executionManaged: true,
|
|
@@ -133,6 +215,8 @@ export async function reconcileOnce(db, now = new Date(), heartbeatTimeoutMs = H
|
|
|
133
215
|
});
|
|
134
216
|
lost += result.count;
|
|
135
217
|
}
|
|
218
|
+
await closeOrphanedHostChecks(db, reviewHosts, now);
|
|
219
|
+
await closeOrphanedHostStatuses(db, reviewHosts, now);
|
|
136
220
|
return lost;
|
|
137
221
|
}
|
|
138
222
|
export function startReconciler(options = {}) {
|
|
@@ -140,7 +224,7 @@ export function startReconciler(options = {}) {
|
|
|
140
224
|
const intervalMs = options.intervalMs ?? RECONCILE_INTERVAL_MS;
|
|
141
225
|
const heartbeatTimeoutMs = options.heartbeatTimeoutMs ?? HEARTBEAT_TIMEOUT_MS;
|
|
142
226
|
const timer = setInterval(() => {
|
|
143
|
-
reconcileOnce(db, new Date(), heartbeatTimeoutMs, options.executor).catch((err) => {
|
|
227
|
+
reconcileOnce(db, new Date(), heartbeatTimeoutMs, options.executor, options.reviewHosts).catch((err) => {
|
|
144
228
|
reconcilerLog.error({ err }, "reconcile pass failed");
|
|
145
229
|
});
|
|
146
230
|
}, intervalMs);
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Repository access authorization, host-neutral (security findings
|
|
3
|
+
* H5-1/A3/C3-2 and H5-3). An agent may use a repository only when its current
|
|
4
|
+
* owner's verified host identity has enough permission on it, or when the
|
|
5
|
+
* authorization was an explicit, recorded admin approval (or predates
|
|
6
|
+
* enforcement: "grandfathered"). Owner-less (public) agents never hold
|
|
7
|
+
* repository authority: anyone can edit them.
|
|
8
|
+
*
|
|
9
|
+
* One gate serves every point of use (a coding run before its workspace is
|
|
10
|
+
* prepared, each repo_* tool call, each host-event dispatch and mention
|
|
11
|
+
* author) and every set-time check (link_repository, create/update_agent).
|
|
12
|
+
* Host answers are cached per (provider, repository, host user id) for a
|
|
13
|
+
* short TTL, positive and negative alike; set-time checks bypass the cache
|
|
14
|
+
* and refresh it. A host error always denies: fail closed.
|
|
15
|
+
* See docs/private/2026-09-26-repo-access-authorization-spec-and-plan.md.
|
|
16
|
+
*/
|
|
17
|
+
import type { PrismaClient } from "#prisma";
|
|
18
|
+
import { type HostPermission, type HostUser, type ReviewHostRegistry } from "../providers/review-host/types.js";
|
|
19
|
+
export type { HostPermission };
|
|
20
|
+
/** How an authorization was granted; stored on the row that grants the authority. */
|
|
21
|
+
export type AuthorizedVia = "host_permission" | "admin" | "grandfathered";
|
|
22
|
+
export declare const AUTHORIZED_VIA: readonly AuthorizedVia[];
|
|
23
|
+
export type RepoAccessDenial = "owner_required" | "not_authorized" | "identity_not_linked" | "insufficient_permission" | "check_failed"
|
|
24
|
+
/** In-flight use only: the host was unreachable or rate-limited, even after one retry. */
|
|
25
|
+
| "check_unavailable";
|
|
26
|
+
export type RepoAccessDecision = {
|
|
27
|
+
ok: true;
|
|
28
|
+
level?: HostPermission;
|
|
29
|
+
} | {
|
|
30
|
+
ok: false;
|
|
31
|
+
reason: RepoAccessDenial;
|
|
32
|
+
level?: HostPermission;
|
|
33
|
+
};
|
|
34
|
+
/** What a use of the repository needs. */
|
|
35
|
+
export type RepoAccessKind = "coding" | "write" | "read" | "mention";
|
|
36
|
+
/**
|
|
37
|
+
* Coding runs and write links push, comment, and publish checks; a mention
|
|
38
|
+
* drives a write-capable agent holding its owner's tools and secrets, so
|
|
39
|
+
* triage is not enough for it either.
|
|
40
|
+
*/
|
|
41
|
+
export declare function requiredLevel(kind: RepoAccessKind): HostPermission;
|
|
42
|
+
export declare function atLeast(level: HostPermission, required: HostPermission): boolean;
|
|
43
|
+
export interface AuthorizeUseInput {
|
|
44
|
+
/** The agent's CURRENT owner — never whoever authorized the link. */
|
|
45
|
+
ownerId: string | null;
|
|
46
|
+
provider: string;
|
|
47
|
+
repository: string;
|
|
48
|
+
required: HostPermission;
|
|
49
|
+
/** The stamp on the row granting the authority (null = never authorized). */
|
|
50
|
+
authorizedVia: string | null;
|
|
51
|
+
/**
|
|
52
|
+
* A use by a run already under way (a launched coding run, its final push,
|
|
53
|
+
* a repo_* call): a transient host error (5xx, timeout, rate limit) is
|
|
54
|
+
* retried once, then denied as `check_unavailable`. Starting something new
|
|
55
|
+
* and set-time checks stay strict: any error is `check_failed`, no retry.
|
|
56
|
+
*/
|
|
57
|
+
retryTransient?: boolean;
|
|
58
|
+
}
|
|
59
|
+
export interface RepoAccessGate {
|
|
60
|
+
/** Point of use: may this agent (by its owner and stamp) use the repository now? */
|
|
61
|
+
authorizeUse(input: AuthorizeUseInput): Promise<RepoAccessDecision>;
|
|
62
|
+
/** Does this principal's linked identity have `required` on the repository? */
|
|
63
|
+
authorizePrincipal(input: {
|
|
64
|
+
principalId: string;
|
|
65
|
+
provider: string;
|
|
66
|
+
repository: string;
|
|
67
|
+
required: HostPermission;
|
|
68
|
+
/** Bypass (and refresh) the cache: set-time checks. */
|
|
69
|
+
fresh?: boolean;
|
|
70
|
+
}): Promise<RepoAccessDecision>;
|
|
71
|
+
/** Does this host user (e.g. a mention's author) have `required` on the repository? */
|
|
72
|
+
authorizeHostUser(input: {
|
|
73
|
+
provider: string;
|
|
74
|
+
repository: string;
|
|
75
|
+
user: HostUser;
|
|
76
|
+
required: HostPermission;
|
|
77
|
+
fresh?: boolean;
|
|
78
|
+
}): Promise<RepoAccessDecision>;
|
|
79
|
+
}
|
|
80
|
+
export interface RepoAccessGateOptions {
|
|
81
|
+
db: Pick<PrismaClient, "hostIdentity">;
|
|
82
|
+
hosts: ReviewHostRegistry;
|
|
83
|
+
/** Default 5 minutes. */
|
|
84
|
+
ttlMs?: number;
|
|
85
|
+
/** Default 1000; the oldest entry is evicted first. */
|
|
86
|
+
maxEntries?: number;
|
|
87
|
+
now?: () => number;
|
|
88
|
+
/** Delay before the one retry of a transient error (default 1s). */
|
|
89
|
+
retryDelayMs?: number;
|
|
90
|
+
sleep?: (ms: number) => Promise<void>;
|
|
91
|
+
}
|
|
92
|
+
/** 5xx, rate limits (403/429), and transport failures: worth one retry for a run already under way. */
|
|
93
|
+
export declare function isTransientHostError(err: unknown): boolean;
|
|
94
|
+
export declare const DEFAULT_REPO_ACCESS_TTL_MS: number;
|
|
95
|
+
export declare function createRepoAccessGate(options: RepoAccessGateOptions): RepoAccessGate;
|
|
96
|
+
/** Actionable, non-leaking text for a denial, for tool results and run errors. */
|
|
97
|
+
export declare function describeDenial(decision: Extract<RepoAccessDecision, {
|
|
98
|
+
ok: false;
|
|
99
|
+
}>, repository: string): string;
|