@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
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
+
import { normalizeCollectExclusions } from "../../coding/collect-exclude.js";
|
|
3
|
+
import { pruneCollectExcluded } from "./collect-prune.js";
|
|
2
4
|
import { spawn } from "node:child_process";
|
|
3
5
|
import { lstat, mkdir, mkdtemp, open, opendir, readdir, readFile, readlink, realpath, rename, rm, unlink, } from "node:fs/promises";
|
|
4
6
|
import { dirname, isAbsolute, join, resolve, sep } from "node:path";
|
|
@@ -6,13 +8,24 @@ import { logger } from "../../core/logger.js";
|
|
|
6
8
|
import { ensurePrivateDirectory } from "../../core/private-directory.js";
|
|
7
9
|
import { readBoundedRegularFile } from "../../coding-worker/artifact.js";
|
|
8
10
|
import { parseCodingAgentOutputJson, redactAndTruncate, MAX_CODING_ARTIFACT_BYTES, MAX_CODING_OUTPUT_ISSUES, SAFE_CODING_OUTPUT_ISSUE, } from "../../coding/protocol.js";
|
|
9
|
-
import { assertDockerHostSupportsIsolation, assertClaudeAgentContainerInspection, assertClaudeToolRunnerContainerInspection, assertIsolationNetworkInspection, assertKeeperContainerInspection, assertProxyContainerInspection, assertStorageVolumeInspection, assertWorkerContainerInspection, buildDockerIsolationPlan, } from "./docker-isolation.js";
|
|
11
|
+
import { assertDockerHostSupportsIsolation, assertClaudeAgentContainerInspection, assertClaudeToolRunnerContainerInspection, assertIsolationNetworkInspection, assertKeeperContainerInspection, assertNetworkKeeperContainerInspection, assertProxyContainerInspection, assertStorageVolumeInspection, assertWorkerContainerInspection, buildDockerIsolationPlan, CLAUDE_TOOL_SETUP_ENV, dockerSupportsServicesFor, } from "./docker-isolation.js";
|
|
12
|
+
import { serviceUnreadyError } from "../../coding/services/wording.js";
|
|
13
|
+
import { assertServiceContainerInspection, buildDockerServicePlan, dockerServiceContainerNames, } from "./docker-services.js";
|
|
14
|
+
import { claudeToolSetup } from "./claude-tool-setup.js";
|
|
10
15
|
import { replaceDirectoryFromStaging } from "./workspace-swap.js";
|
|
11
16
|
const BACKEND = "docker";
|
|
12
17
|
const STATE_SCHEMA_VERSION = 1;
|
|
13
18
|
const MAX_DOCKER_OUTPUT_BYTES = 1024 * 1024;
|
|
14
19
|
const MAX_DOCKER_SEED_DIAGNOSTIC_BYTES = 4 * 1024;
|
|
15
20
|
const MAX_WORKSPACE_ENTRIES = 100_000;
|
|
21
|
+
/** How long one service image pull may take before the run fails as service_unready. */
|
|
22
|
+
export const SERVICE_IMAGE_PULL_TIMEOUT_MS = 300_000;
|
|
23
|
+
/**
|
|
24
|
+
* The default bound on a run's whole service start-up (every service's create, start, inspection and
|
|
25
|
+
* readiness probing; image pulls excluded), matching the Kubernetes launcher's pod-start bound.
|
|
26
|
+
*/
|
|
27
|
+
export const DEFAULT_SERVICE_READY_TIMEOUT_MS = 120_000;
|
|
28
|
+
const MAX_SERVICE_PROBE_OUTPUT_BYTES = 64 * 1024;
|
|
16
29
|
const TERMINAL_PHASES = new Set(["succeeded", "failed", "stopped", "lost", "removed"]);
|
|
17
30
|
export const SAFE_WORKER_DIAGNOSTIC = /^(?:worker_[a-z_]+|coding_[a-z_]+|wardby_[a-z_]+)$/;
|
|
18
31
|
/**
|
|
@@ -81,7 +94,7 @@ export class NodeDockerCommandRunner {
|
|
|
81
94
|
if (args.some((argument) => argument.includes("\0")))
|
|
82
95
|
throw new Error("docker_argument_invalid");
|
|
83
96
|
const extraEnv = options.env ?? {};
|
|
84
|
-
if (Object.keys(extraEnv).some((key) => key !== "WARDBY_RUN_CAPABILITY"))
|
|
97
|
+
if (Object.keys(extraEnv).some((key) => key !== "WARDBY_RUN_CAPABILITY" && key !== CLAUDE_TOOL_SETUP_ENV))
|
|
85
98
|
throw new Error("docker_environment_invalid");
|
|
86
99
|
const env = {
|
|
87
100
|
PATH: this.path,
|
|
@@ -100,6 +113,13 @@ export class NodeDockerCommandRunner {
|
|
|
100
113
|
rejectPromise(new DockerCommandError(null));
|
|
101
114
|
return;
|
|
102
115
|
}
|
|
116
|
+
let timedOut = false;
|
|
117
|
+
const timer = options.timeoutMs === undefined
|
|
118
|
+
? undefined
|
|
119
|
+
: setTimeout(() => {
|
|
120
|
+
timedOut = true;
|
|
121
|
+
child.kill("SIGKILL");
|
|
122
|
+
}, options.timeoutMs);
|
|
103
123
|
const stdout = [];
|
|
104
124
|
const stderr = [];
|
|
105
125
|
let bytes = 0;
|
|
@@ -120,6 +140,11 @@ export class NodeDockerCommandRunner {
|
|
|
120
140
|
spawnFailed = true;
|
|
121
141
|
});
|
|
122
142
|
child.once("close", (code) => {
|
|
143
|
+
if (timer)
|
|
144
|
+
clearTimeout(timer);
|
|
145
|
+
if (timedOut) {
|
|
146
|
+
return rejectPromise(new DockerCommandError(code, false, false, { subcommand: args[0], stderr: "timed out" }));
|
|
147
|
+
}
|
|
123
148
|
if (exceeded)
|
|
124
149
|
return rejectPromise(new DockerCommandError(code, false, true));
|
|
125
150
|
if (spawnFailed)
|
|
@@ -256,7 +281,7 @@ export class NodeDockerArtifactTransfer {
|
|
|
256
281
|
});
|
|
257
282
|
await docker.run(["container", "cp", sourceFile, `${container}:/run/wardby/storage/input/input.json`]);
|
|
258
283
|
}
|
|
259
|
-
async materializeDirectory(container, source, destination, maxBytes) {
|
|
284
|
+
async materializeDirectory(container, source, destination, maxBytes, exclusions) {
|
|
260
285
|
await replaceDirectoryFromStaging(destination, maxBytes, "docker_workspace_destination_invalid", async (staging) => {
|
|
261
286
|
await new NodeDockerCommandRunner({ dockerBinary: this.dockerBinary, homeDir: "/tmp", path: this.path }).run([
|
|
262
287
|
"container",
|
|
@@ -264,6 +289,8 @@ export class NodeDockerArtifactTransfer {
|
|
|
264
289
|
`${container}:${source}/.`,
|
|
265
290
|
staging,
|
|
266
291
|
]);
|
|
292
|
+
// Before replaceDirectoryFromStaging validates: excluded folders never count.
|
|
293
|
+
await pruneCollectExcluded(staging, exclusions);
|
|
267
294
|
});
|
|
268
295
|
}
|
|
269
296
|
}
|
|
@@ -380,6 +407,42 @@ function resourceLabels(record) {
|
|
|
380
407
|
function labelsMatch(actual, expected) {
|
|
381
408
|
return Object.entries(expected).every(([key, value]) => actual?.[key] === value);
|
|
382
409
|
}
|
|
410
|
+
/**
|
|
411
|
+
* A run's service start-up budget: `budgetMs` from the start, extended by the time image pulls took,
|
|
412
|
+
* and never past the run's deadline.
|
|
413
|
+
*/
|
|
414
|
+
class ServiceStartBudget {
|
|
415
|
+
now;
|
|
416
|
+
budgetMs;
|
|
417
|
+
deadlineAt;
|
|
418
|
+
startedAt;
|
|
419
|
+
excludedMs = 0;
|
|
420
|
+
constructor(now, budgetMs, deadlineAt) {
|
|
421
|
+
this.now = now;
|
|
422
|
+
this.budgetMs = budgetMs;
|
|
423
|
+
this.deadlineAt = deadlineAt;
|
|
424
|
+
this.startedAt = now();
|
|
425
|
+
}
|
|
426
|
+
/** What is left of the budget, capped by the run's deadline; zero once either runs out. */
|
|
427
|
+
remainingMs() {
|
|
428
|
+
const now = this.now();
|
|
429
|
+
return Math.max(0, Math.min(this.startedAt + this.excludedMs + this.budgetMs - now, this.deadlineAt - now));
|
|
430
|
+
}
|
|
431
|
+
/** What is left before the run's deadline: the only bound (besides its own) on an image pull. */
|
|
432
|
+
untilDeadlineMs() {
|
|
433
|
+
return Math.max(0, this.deadlineAt - this.now());
|
|
434
|
+
}
|
|
435
|
+
exclude(milliseconds) {
|
|
436
|
+
this.excludedMs += Math.max(0, milliseconds);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
/** `coding_service_unready:<name>` (the executor's service_unready), keeping the Docker failure as the operator's cause. */
|
|
440
|
+
function serviceUnready(name, cause) {
|
|
441
|
+
const error = serviceUnreadyError(name);
|
|
442
|
+
if (cause !== undefined)
|
|
443
|
+
error.cause = cause;
|
|
444
|
+
return error;
|
|
445
|
+
}
|
|
383
446
|
function capabilityFromInspection(container) {
|
|
384
447
|
return container.Config?.Env?.find((value) => value.startsWith("WARDBY_RUN_CAPABILITY="))?.slice("WARDBY_RUN_CAPABILITY=".length);
|
|
385
448
|
}
|
|
@@ -417,6 +480,8 @@ export class DockerJobLauncher {
|
|
|
417
480
|
docker;
|
|
418
481
|
transfer;
|
|
419
482
|
now;
|
|
483
|
+
sleep;
|
|
484
|
+
serviceReadyTimeoutMs;
|
|
420
485
|
locks = new Map();
|
|
421
486
|
timers = new Map();
|
|
422
487
|
ready;
|
|
@@ -429,8 +494,17 @@ export class DockerJobLauncher {
|
|
|
429
494
|
this.docker = options.docker ?? new NodeDockerCommandRunner({ homeDir: join(this.stateRoot, ".home") });
|
|
430
495
|
this.transfer = options.transfer ?? new NodeDockerArtifactTransfer();
|
|
431
496
|
this.now = options.now ?? Date.now;
|
|
497
|
+
this.sleep =
|
|
498
|
+
options.sleep ?? ((milliseconds) => new Promise((resolvePromise) => setTimeout(resolvePromise, milliseconds)));
|
|
499
|
+
this.serviceReadyTimeoutMs = options.serviceReadyTimeoutMs ?? DEFAULT_SERVICE_READY_TIMEOUT_MS;
|
|
500
|
+
if (!Number.isFinite(this.serviceReadyTimeoutMs) || this.serviceReadyTimeoutMs <= 0)
|
|
501
|
+
throw new Error("docker_launcher_service_timeout_invalid");
|
|
432
502
|
this.ready = this.initialize();
|
|
433
503
|
}
|
|
504
|
+
/** Starts JobSpec.services (a network keeper plus one hardened container per service) for Codex and Claude Code runs. */
|
|
505
|
+
supportsServicesFor(provider) {
|
|
506
|
+
return dockerSupportsServicesFor(provider);
|
|
507
|
+
}
|
|
434
508
|
async launch(spec) {
|
|
435
509
|
await this.ready;
|
|
436
510
|
return this.withRun(spec.runId, async () => this.launchLocked(spec));
|
|
@@ -493,7 +567,9 @@ export class DockerJobLauncher {
|
|
|
493
567
|
if (resolve(destination) !== expected || !expected.startsWith(`${this.workspaceRoot}${sep}`)) {
|
|
494
568
|
throw new Error("docker_workspace_destination_invalid");
|
|
495
569
|
}
|
|
496
|
-
await this.transfer.materializeDirectory(this.keeperName(current), "/run/wardby/storage/workspace", expected, current.spec.limits.diskMb * 1024 * 1024
|
|
570
|
+
await this.transfer.materializeDirectory(this.keeperName(current), "/run/wardby/storage/workspace", expected, current.spec.limits.diskMb * 1024 * 1024,
|
|
571
|
+
// Names are always the current built-in list; only the paths come from the spec.
|
|
572
|
+
normalizeCollectExclusions(current.spec.collectExclude?.paths));
|
|
497
573
|
});
|
|
498
574
|
}
|
|
499
575
|
async stop(handle, _reason) {
|
|
@@ -557,6 +633,7 @@ export class DockerJobLauncher {
|
|
|
557
633
|
}
|
|
558
634
|
async launchLocked(spec) {
|
|
559
635
|
const plan = buildDockerIsolationPlan(spec, this.options.proxyContainer);
|
|
636
|
+
const services = buildDockerServicePlan(spec);
|
|
560
637
|
const specHash = stableSpecHash(spec);
|
|
561
638
|
const existing = await this.readRecord(spec.runId);
|
|
562
639
|
if (existing) {
|
|
@@ -581,7 +658,7 @@ export class DockerJobLauncher {
|
|
|
581
658
|
await this.refresh(existing);
|
|
582
659
|
return clone(existing.handle);
|
|
583
660
|
}
|
|
584
|
-
return this.provision(existing, plan, capability);
|
|
661
|
+
return this.provision(existing, plan, services, capability);
|
|
585
662
|
}
|
|
586
663
|
await this.validateInputArtifact(spec.inputArtifact);
|
|
587
664
|
const capability = ensureCapability(await this.options.resolveCapability(spec.runId));
|
|
@@ -600,7 +677,7 @@ export class DockerJobLauncher {
|
|
|
600
677
|
// This durable record is the idempotency fence before any Docker side effect.
|
|
601
678
|
await this.writeRecord(record);
|
|
602
679
|
try {
|
|
603
|
-
return await this.provision(record, plan, capability);
|
|
680
|
+
return await this.provision(record, plan, services, capability);
|
|
604
681
|
}
|
|
605
682
|
catch (error) {
|
|
606
683
|
await this.options
|
|
@@ -613,7 +690,7 @@ export class DockerJobLauncher {
|
|
|
613
690
|
throw error;
|
|
614
691
|
}
|
|
615
692
|
}
|
|
616
|
-
async provision(record, plan, capability) {
|
|
693
|
+
async provision(record, plan, services, capability) {
|
|
617
694
|
await this.assertHost();
|
|
618
695
|
const labels = resourceLabels(record);
|
|
619
696
|
await this.createAndAssert(appendLabels(plan.networkCreateArgs, labels), ["network", "inspect", plan.names.network], (value) => {
|
|
@@ -640,13 +717,21 @@ export class DockerJobLauncher {
|
|
|
640
717
|
await this.run(plan.proxyNetworkConnectArgs);
|
|
641
718
|
const proxy = await this.inspect(["container", "inspect", this.options.proxyContainer]);
|
|
642
719
|
assertProxyContainerInspection(proxy, record.runId);
|
|
720
|
+
// Built before anything starts, so a setup past the tool runner's bounds fails the launch early.
|
|
721
|
+
const toolSetup = record.spec.provider === "claude-code" ? claudeToolSetup(record.spec, capability) : undefined;
|
|
722
|
+
// Services first (a run with services only): the Codex worker and Claude Code's tool runner join
|
|
723
|
+
// the network keeper's namespace, so it must exist, and every service must pass readiness before
|
|
724
|
+
// anything that runs the run's commands starts.
|
|
725
|
+
const networkKeeperId = plan.networkKeeperCreateArgs
|
|
726
|
+
? await this.startServices(record, plan.networkKeeperCreateArgs, plan.names.networkKeeperContainer, services, labels)
|
|
727
|
+
: undefined;
|
|
643
728
|
if (plan.toolCreateArgs) {
|
|
644
729
|
await this.createAndAssert(appendLabels(plan.toolCreateArgs, labels), ["container", "inspect", plan.names.toolContainer], (value) => {
|
|
645
|
-
assertClaudeToolRunnerContainerInspection(value, record.spec);
|
|
730
|
+
assertClaudeToolRunnerContainerInspection(value, record.spec, toolSetup, networkKeeperId);
|
|
646
731
|
if (!labelsMatch(value.Config?.Labels, labels)) {
|
|
647
732
|
throw new Error("docker_resource_attestation_failed");
|
|
648
733
|
}
|
|
649
|
-
});
|
|
734
|
+
}, { env: { WARDBY_TOOL_SETUP: toolSetup } });
|
|
650
735
|
await this.startContainer(plan.names.toolContainer);
|
|
651
736
|
await this.waitForToolRunner(plan.names.toolContainer);
|
|
652
737
|
}
|
|
@@ -657,7 +742,7 @@ export class DockerJobLauncher {
|
|
|
657
742
|
assertClaudeAgentContainerInspection(inspected, record.spec, capability);
|
|
658
743
|
}
|
|
659
744
|
else {
|
|
660
|
-
assertWorkerContainerInspection(inspected, record.spec, capability);
|
|
745
|
+
assertWorkerContainerInspection(inspected, record.spec, capability, networkKeeperId);
|
|
661
746
|
}
|
|
662
747
|
if (!labelsMatch(inspected.Config?.Labels, labels))
|
|
663
748
|
throw new Error("docker_resource_attestation_failed");
|
|
@@ -668,6 +753,138 @@ export class DockerJobLauncher {
|
|
|
668
753
|
this.scheduleDeadline(record);
|
|
669
754
|
return clone(record.handle);
|
|
670
755
|
}
|
|
756
|
+
/**
|
|
757
|
+
* A run with services (docs/coding-services.md). The network keeper joins the run's internal
|
|
758
|
+
* network and owns the namespace; each service joins it and must pass its catalog readiness
|
|
759
|
+
* command before the next one starts, as Kubernetes starts native sidecars. The whole start-up is
|
|
760
|
+
* bounded (ServiceStartBudget): running out fails as coding_service_unready for the service being
|
|
761
|
+
* waited on. Returns the keeper's container ID, which Docker may report as the services' and
|
|
762
|
+
* worker's network mode.
|
|
763
|
+
*/
|
|
764
|
+
async startServices(record, keeperArgs, keeperName, services, labels) {
|
|
765
|
+
const budget = new ServiceStartBudget(this.now, this.serviceReadyTimeoutMs, record.deadlineAt);
|
|
766
|
+
const keeper = (await this.createAndAssert(appendLabels(keeperArgs, labels), ["container", "inspect", keeperName], (value) => {
|
|
767
|
+
const inspected = value;
|
|
768
|
+
assertNetworkKeeperContainerInspection(inspected, record.spec);
|
|
769
|
+
if (!labelsMatch(inspected.Config?.Labels, labels))
|
|
770
|
+
throw new Error("docker_resource_attestation_failed");
|
|
771
|
+
}));
|
|
772
|
+
const keeperId = keeper.Id;
|
|
773
|
+
if (!keeperId)
|
|
774
|
+
throw new Error("docker_resource_attestation_failed");
|
|
775
|
+
await this.startContainer(keeperName);
|
|
776
|
+
for (const planned of services) {
|
|
777
|
+
const name = planned.service.name;
|
|
778
|
+
if (budget.remainingMs() <= 0)
|
|
779
|
+
throw serviceUnready(name);
|
|
780
|
+
await this.ensureServiceImage(planned, budget);
|
|
781
|
+
if (budget.remainingMs() <= 0)
|
|
782
|
+
throw serviceUnready(name);
|
|
783
|
+
let createFailure;
|
|
784
|
+
try {
|
|
785
|
+
await this.createAndAssert(appendLabels(planned.createArgs, labels), ["container", "inspect", planned.container], (value) => {
|
|
786
|
+
const inspected = value;
|
|
787
|
+
assertServiceContainerInspection(inspected, record.spec, planned.service, keeperId);
|
|
788
|
+
if (!labelsMatch(inspected.Config?.Labels, labels))
|
|
789
|
+
throw new Error("docker_resource_attestation_failed");
|
|
790
|
+
}, undefined, (error) => {
|
|
791
|
+
createFailure = error;
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
catch (error) {
|
|
795
|
+
// createAndAssert tolerates a failed create (a crash retry may find it made), so a service
|
|
796
|
+
// Docker refused to create shows up as a failed inspection; the create's own failure is the
|
|
797
|
+
// useful cause, not that generic "not found". Attestation failures rethrow as is.
|
|
798
|
+
if (error instanceof DockerCommandError)
|
|
799
|
+
throw serviceUnready(name, createFailure ?? error);
|
|
800
|
+
throw error;
|
|
801
|
+
}
|
|
802
|
+
if (budget.remainingMs() <= 0)
|
|
803
|
+
throw serviceUnready(name);
|
|
804
|
+
try {
|
|
805
|
+
await this.startContainer(planned.container);
|
|
806
|
+
}
|
|
807
|
+
catch (error) {
|
|
808
|
+
throw serviceUnready(name, error);
|
|
809
|
+
}
|
|
810
|
+
await this.waitForServiceReady(planned, budget);
|
|
811
|
+
}
|
|
812
|
+
return keeperId;
|
|
813
|
+
}
|
|
814
|
+
/**
|
|
815
|
+
* A local copy of the digest-pinned image is used as is; otherwise it is pulled, bounded by its
|
|
816
|
+
* own limit and the run's deadline. Pull time does not count toward the start-up budget.
|
|
817
|
+
*/
|
|
818
|
+
async ensureServiceImage(planned, budget) {
|
|
819
|
+
try {
|
|
820
|
+
await this.run(["image", "inspect", "--format", "{{.Id}}", planned.service.image]);
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
823
|
+
catch {
|
|
824
|
+
// Missing (or unreadable): pull it by its digest below.
|
|
825
|
+
}
|
|
826
|
+
const untilDeadline = budget.untilDeadlineMs();
|
|
827
|
+
if (untilDeadline <= 0)
|
|
828
|
+
throw serviceUnready(planned.service.name);
|
|
829
|
+
const startedAt = this.now();
|
|
830
|
+
try {
|
|
831
|
+
await this.run(["image", "pull", "--quiet", planned.service.image], {
|
|
832
|
+
timeoutMs: Math.min(SERVICE_IMAGE_PULL_TIMEOUT_MS, untilDeadline),
|
|
833
|
+
});
|
|
834
|
+
}
|
|
835
|
+
catch (error) {
|
|
836
|
+
throw serviceUnready(planned.service.name, error);
|
|
837
|
+
}
|
|
838
|
+
finally {
|
|
839
|
+
budget.exclude(this.now() - startedAt);
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
/**
|
|
843
|
+
* The catalog readiness command, like a Kubernetes startup probe: every periodSeconds, each
|
|
844
|
+
* attempt bounded by timeoutSeconds, failing after failureThreshold consecutive failures. A
|
|
845
|
+
* service that exited or vanished fails at once, and so does one still unready when the start-up
|
|
846
|
+
* budget runs out (each probe and wait is clipped to what is left of it). Probe output is never
|
|
847
|
+
* logged or kept, so these errors carry no cause.
|
|
848
|
+
*/
|
|
849
|
+
async waitForServiceReady(planned, budget) {
|
|
850
|
+
const { command, periodSeconds, timeoutSeconds, failureThreshold } = planned.service.readiness;
|
|
851
|
+
const name = planned.service.name;
|
|
852
|
+
let failures = 0;
|
|
853
|
+
for (;;) {
|
|
854
|
+
if (budget.remainingMs() <= 0)
|
|
855
|
+
throw serviceUnready(name);
|
|
856
|
+
let state;
|
|
857
|
+
try {
|
|
858
|
+
state = await this.inspect(["container", "inspect", planned.container]);
|
|
859
|
+
}
|
|
860
|
+
catch (error) {
|
|
861
|
+
if (error instanceof DockerCommandError)
|
|
862
|
+
throw serviceUnready(name);
|
|
863
|
+
throw error;
|
|
864
|
+
}
|
|
865
|
+
if (state.State?.Running !== true)
|
|
866
|
+
throw serviceUnready(name);
|
|
867
|
+
const remaining = budget.remainingMs();
|
|
868
|
+
if (remaining <= 0)
|
|
869
|
+
throw serviceUnready(name);
|
|
870
|
+
try {
|
|
871
|
+
await this.run(["container", "exec", planned.container, ...command], {
|
|
872
|
+
timeoutMs: Math.min(timeoutSeconds * 1_000, remaining),
|
|
873
|
+
maxOutputBytes: MAX_SERVICE_PROBE_OUTPUT_BYTES,
|
|
874
|
+
});
|
|
875
|
+
return;
|
|
876
|
+
}
|
|
877
|
+
catch {
|
|
878
|
+
failures += 1;
|
|
879
|
+
if (failures >= failureThreshold)
|
|
880
|
+
throw serviceUnready(name);
|
|
881
|
+
}
|
|
882
|
+
const wait = Math.min(periodSeconds * 1_000, budget.remainingMs());
|
|
883
|
+
if (wait <= 0)
|
|
884
|
+
throw serviceUnready(name);
|
|
885
|
+
await this.sleep(wait);
|
|
886
|
+
}
|
|
887
|
+
}
|
|
671
888
|
async refresh(record) {
|
|
672
889
|
if (isTerminal(record))
|
|
673
890
|
return;
|
|
@@ -701,7 +918,7 @@ export class DockerJobLauncher {
|
|
|
701
918
|
"inspect",
|
|
702
919
|
buildDockerIsolationPlan(record.spec, this.options.proxyContainer).names.toolContainer,
|
|
703
920
|
]);
|
|
704
|
-
assertClaudeToolRunnerContainerInspection(tool, record.spec);
|
|
921
|
+
assertClaudeToolRunnerContainerInspection(tool, record.spec, claudeToolSetup(record.spec, capability), await this.attestedNetworkKeeperId(record));
|
|
705
922
|
if (tool.State?.Running !== true || tool.State?.Status !== "running") {
|
|
706
923
|
record.phase = "failed";
|
|
707
924
|
record.result = { exitCode: 1, reason: "failed", diagnostic: "worker_tool_runner_failed" };
|
|
@@ -711,7 +928,7 @@ export class DockerJobLauncher {
|
|
|
711
928
|
}
|
|
712
929
|
}
|
|
713
930
|
else {
|
|
714
|
-
assertWorkerContainerInspection(inspection, record.spec, capability);
|
|
931
|
+
assertWorkerContainerInspection(inspection, record.spec, capability, await this.attestedNetworkKeeperId(record));
|
|
715
932
|
}
|
|
716
933
|
const phase = dockerState(inspection);
|
|
717
934
|
if (!phase || phase === "provisioning" || phase === "active")
|
|
@@ -724,28 +941,27 @@ export class DockerJobLauncher {
|
|
|
724
941
|
async stopRecord(record, timedOut) {
|
|
725
942
|
if (isTerminal(record))
|
|
726
943
|
return;
|
|
727
|
-
|
|
728
|
-
await this.run(["container", "stop", "--time", "10", record.handle.id]);
|
|
729
|
-
}
|
|
730
|
-
catch (error) {
|
|
731
|
-
if (!(error instanceof DockerCommandError && error.notFound))
|
|
732
|
-
throw new Error("docker_stop_failed", { cause: error });
|
|
733
|
-
}
|
|
944
|
+
await this.stopContainer(record.handle.id);
|
|
734
945
|
if (record.spec.provider === "claude-code") {
|
|
735
|
-
|
|
736
|
-
try {
|
|
737
|
-
await this.run(["container", "stop", "--time", "10", tool]);
|
|
738
|
-
}
|
|
739
|
-
catch (error) {
|
|
740
|
-
if (!(error instanceof DockerCommandError && error.notFound))
|
|
741
|
-
throw new Error("docker_stop_failed", { cause: error });
|
|
742
|
-
}
|
|
946
|
+
await this.stopContainer(buildDockerIsolationPlan(record.spec, this.options.proxyContainer).names.toolContainer);
|
|
743
947
|
}
|
|
948
|
+
// Services stop with the run; they (and the network keeper) are removed with its other resources.
|
|
949
|
+
for (const service of dockerServiceContainerNames(record.spec))
|
|
950
|
+
await this.stopContainer(service);
|
|
744
951
|
record.phase = timedOut ? "failed" : "stopped";
|
|
745
952
|
record.result = timedOut ? { exitCode: 124, reason: "timed_out" } : resultFor("stopped");
|
|
746
953
|
await this.writeRecord(record);
|
|
747
954
|
this.clearDeadline(record);
|
|
748
955
|
}
|
|
956
|
+
async stopContainer(name) {
|
|
957
|
+
try {
|
|
958
|
+
await this.run(["container", "stop", "--time", "10", name]);
|
|
959
|
+
}
|
|
960
|
+
catch (error) {
|
|
961
|
+
if (!(error instanceof DockerCommandError && error.notFound))
|
|
962
|
+
throw new Error("docker_stop_failed", { cause: error });
|
|
963
|
+
}
|
|
964
|
+
}
|
|
749
965
|
async sweepExpired(record) {
|
|
750
966
|
// The DB verifier ran immediately before this call; never sweep by label alone.
|
|
751
967
|
try {
|
|
@@ -765,17 +981,26 @@ export class DockerJobLauncher {
|
|
|
765
981
|
await this.removeContainerIfAttested(plan.names.workerContainer, labels);
|
|
766
982
|
if (plan.toolCreateArgs)
|
|
767
983
|
await this.removeContainerIfAttested(plan.names.toolContainer, labels);
|
|
984
|
+
// Services, then the network keeper: nothing (the Codex worker, Claude's tool runner, a service)
|
|
985
|
+
// may still share its namespace when it goes.
|
|
986
|
+
// --volumes: an image VOLUME the attestation rejected must not outlive its container.
|
|
987
|
+
for (const service of dockerServiceContainerNames(record.spec)) {
|
|
988
|
+
await this.removeContainerIfAttested(service, labels, true);
|
|
989
|
+
}
|
|
990
|
+
if (plan.networkKeeperCreateArgs) {
|
|
991
|
+
await this.removeContainerIfAttested(plan.names.networkKeeperContainer, labels);
|
|
992
|
+
}
|
|
768
993
|
await this.runIgnoreMissing(["network", "disconnect", plan.names.network, this.options.proxyContainer]);
|
|
769
994
|
await this.removeContainerIfAttested(plan.names.keeperContainer, labels);
|
|
770
995
|
await this.removeNetworkIfAttested(plan.names.network, labels);
|
|
771
996
|
await this.removeVolumeIfAttested(plan.names.storageVolume, labels);
|
|
772
997
|
}
|
|
773
|
-
async removeContainerIfAttested(name, labels) {
|
|
998
|
+
async removeContainerIfAttested(name, labels, anonymousVolumes = false) {
|
|
774
999
|
try {
|
|
775
1000
|
const inspected = await this.inspect(["container", "inspect", name]);
|
|
776
1001
|
if (!labelsMatch(inspected.Config?.Labels, labels))
|
|
777
1002
|
throw new Error("docker_resource_attestation_failed");
|
|
778
|
-
await this.runIgnoreMissing(["container", "rm", "--force", name]);
|
|
1003
|
+
await this.runIgnoreMissing(["container", "rm", "--force", ...(anonymousVolumes ? ["--volumes"] : []), name]);
|
|
779
1004
|
}
|
|
780
1005
|
catch (error) {
|
|
781
1006
|
if (!(error instanceof DockerCommandError && error.notFound))
|
|
@@ -852,18 +1077,40 @@ export class DockerJobLauncher {
|
|
|
852
1077
|
keeperName(record) {
|
|
853
1078
|
return buildDockerIsolationPlan(record.spec, this.options.proxyContainer).names.keeperContainer;
|
|
854
1079
|
}
|
|
1080
|
+
/** For a run with services: the attested network keeper's ID, which the worker's network mode may name. */
|
|
1081
|
+
async attestedNetworkKeeperId(record) {
|
|
1082
|
+
if (record.spec.services === undefined)
|
|
1083
|
+
return undefined;
|
|
1084
|
+
const name = buildDockerIsolationPlan(record.spec, this.options.proxyContainer).names.networkKeeperContainer;
|
|
1085
|
+
let keeper;
|
|
1086
|
+
try {
|
|
1087
|
+
keeper = await this.inspect(["container", "inspect", name]);
|
|
1088
|
+
}
|
|
1089
|
+
catch (error) {
|
|
1090
|
+
throw new Error("docker_status_unavailable", { cause: error });
|
|
1091
|
+
}
|
|
1092
|
+
assertNetworkKeeperContainerInspection(keeper, record.spec);
|
|
1093
|
+
if (!labelsMatch(keeper.Config?.Labels, resourceLabels(record)) || !keeper.Id) {
|
|
1094
|
+
throw new Error("docker_resource_attestation_failed");
|
|
1095
|
+
}
|
|
1096
|
+
return keeper.Id;
|
|
1097
|
+
}
|
|
855
1098
|
async assertHost() {
|
|
856
1099
|
const info = await this.inspect(["info", "--format", "{{json .}}"]);
|
|
857
1100
|
assertDockerHostSupportsIsolation(info);
|
|
858
1101
|
}
|
|
859
|
-
async createAndAssert(createArgs, inspectArgs, assert, options) {
|
|
1102
|
+
async createAndAssert(createArgs, inspectArgs, assert, options, onCreateFailure) {
|
|
860
1103
|
try {
|
|
861
1104
|
await this.run(createArgs, options);
|
|
862
1105
|
}
|
|
863
|
-
catch {
|
|
1106
|
+
catch (error) {
|
|
864
1107
|
// A retry may find a resource created before the previous process crashed.
|
|
1108
|
+
if (error instanceof DockerCommandError)
|
|
1109
|
+
onCreateFailure?.(error);
|
|
865
1110
|
}
|
|
866
|
-
|
|
1111
|
+
const inspected = await this.inspect(inspectArgs);
|
|
1112
|
+
assert(inspected);
|
|
1113
|
+
return inspected;
|
|
867
1114
|
}
|
|
868
1115
|
async waitForStorage(keeper) {
|
|
869
1116
|
// The keeper emits this fixed marker only after creating every storage
|
|
@@ -122,8 +122,9 @@ export class FakeKubernetesApi {
|
|
|
122
122
|
}
|
|
123
123
|
async exec(namespace, pod, container, command, options) {
|
|
124
124
|
// Like the real API: exec into a missing pod or an unknown container fails instead of succeeding
|
|
125
|
-
// silently, so no test can pass on behaviour a cluster would reject. (The adapter
|
|
126
|
-
//
|
|
125
|
+
// silently, so no test can pass on behaviour a cluster would reject. (The adapter does not
|
|
126
|
+
// actually enforce timeoutMs itself: tests drive slow execs through onExec directly. The value
|
|
127
|
+
// is still recorded on the call, so a test can assert which timeout the caller configured.)
|
|
127
128
|
const target = this.read("pod", namespace, pod);
|
|
128
129
|
if (!target)
|
|
129
130
|
throw new KubernetesNotFoundError(this.key("pod", namespace, pod));
|
|
@@ -131,7 +132,15 @@ export class FakeKubernetesApi {
|
|
|
131
132
|
if (!containers.some((entry) => entry.name === container)) {
|
|
132
133
|
throw new KubernetesNotFoundError(`${this.key("pod", namespace, pod)}/${container}`);
|
|
133
134
|
}
|
|
134
|
-
const call = {
|
|
135
|
+
const call = {
|
|
136
|
+
namespace,
|
|
137
|
+
pod,
|
|
138
|
+
container,
|
|
139
|
+
command,
|
|
140
|
+
stdin: options.stdin,
|
|
141
|
+
stdout: options.stdout,
|
|
142
|
+
timeoutMs: options.timeoutMs,
|
|
143
|
+
};
|
|
135
144
|
this.execCalls.push(call);
|
|
136
145
|
return this.onExec(call);
|
|
137
146
|
}
|
|
@@ -1,13 +1,26 @@
|
|
|
1
1
|
import type { V1NetworkPolicy, V1Pod, V1Secret } from "@kubernetes/client-node";
|
|
2
|
+
import { type ResolvedCodingService } from "../../coding/services/catalog.js";
|
|
2
3
|
import type { JobSpec } from "./types.js";
|
|
3
4
|
import { type KubernetesPlatform } from "./kubernetes-platform.js";
|
|
4
5
|
export declare const KUBERNETES_ISOLATION_ERROR = "kubernetes_isolation_unsupported";
|
|
5
|
-
export declare const KUBERNETES_PROVIDER_UNSUPPORTED = "kubernetes_provider_unsupported";
|
|
6
6
|
/** Extra seconds past timeoutSec before Kubernetes kills the pod (keeper included). */
|
|
7
7
|
export declare const POD_DEADLINE_GRACE_SECONDS = 300;
|
|
8
8
|
export declare const KEEPER_CONTAINER = "keeper";
|
|
9
9
|
export declare const STORAGE_INIT_CONTAINER = "storage-init";
|
|
10
10
|
export declare const WORKER_CONTAINER = "worker";
|
|
11
|
+
/** Claude's credential-free command runner: a native sidecar in the run pod. */
|
|
12
|
+
export declare const TOOL_RUNNER_CONTAINER = "tool-runner";
|
|
13
|
+
/** The run Secret's key holding the tool runner's setup (claude-tool-setup.ts). */
|
|
14
|
+
export declare const TOOL_SETUP_SECRET_KEY = "tool-setup";
|
|
15
|
+
/** Where the tool runner listens and the agent's relay connects (claude-tool-runner/main.mjs). */
|
|
16
|
+
export declare const TOOL_SOCKET_PATH = "/run/wardby/tool/runner.sock";
|
|
17
|
+
/** Every service sidecar's container name starts with this (service-postgres, service-redis, ...). */
|
|
18
|
+
export declare const SERVICE_CONTAINER_PREFIX = "service-";
|
|
19
|
+
/** Disk-backed scratch for each of a service's writablePaths (a socket directory, /tmp). */
|
|
20
|
+
export declare const SERVICE_SCRATCH_MIB = 64;
|
|
21
|
+
export declare function serviceContainerName(name: string): string;
|
|
22
|
+
/** What a service sidecar reserves of the pod's ephemeral storage: its data volume plus its scratch volumes. */
|
|
23
|
+
export declare function serviceEphemeralStorageMib(service: ResolvedCodingService): number;
|
|
11
24
|
export declare const STORAGE_ROOT = "/run/wardby/storage";
|
|
12
25
|
export declare const KEEPER_SEEDED_MARKER = "/run/wardby/storage/input/.seeded";
|
|
13
26
|
export declare const PROXY_POD_LABEL: {
|
|
@@ -53,12 +66,14 @@ export interface RunPodOptions {
|
|
|
53
66
|
namespace: string;
|
|
54
67
|
proxyIp: string;
|
|
55
68
|
runtimeClassName?: string;
|
|
69
|
+
/** Emitted only when set, so an unconfigured launcher submits exactly the pod it always has. */
|
|
70
|
+
priorityClassName?: string;
|
|
56
71
|
/** Which platform's admission rules the emitted resources must already satisfy. Default: "generic". */
|
|
57
72
|
platform?: KubernetesPlatform;
|
|
58
73
|
}
|
|
59
74
|
export declare function buildRunPod(spec: JobSpec, options: RunPodOptions): V1Pod;
|
|
60
75
|
export declare function buildRunNetworkPolicy(spec: JobSpec, namespace: string): V1NetworkPolicy;
|
|
61
|
-
export declare function buildCapabilitySecret(spec: JobSpec, namespace: string, capability: string): V1Secret;
|
|
76
|
+
export declare function buildCapabilitySecret(spec: JobSpec, namespace: string, capability: string, toolSetup?: string): V1Secret;
|
|
62
77
|
/**
|
|
63
78
|
* A copy of `pod` with ONLY the API server's own defaulting and aliasing undone
|
|
64
79
|
* — the same `normalizeSpec` pass `assertRunPodMatches` runs, and nothing else.
|