@wardby/cli 0.1.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 +131 -0
- package/LICENSE +202 -0
- package/NOTICE +2 -0
- package/README.md +282 -0
- package/bin/wardby.js +3 -0
- package/deploy/local/docker-compose.yml +19 -0
- package/dist/claude-coding-worker/driver.d.ts +70 -0
- package/dist/claude-coding-worker/driver.js +128 -0
- package/dist/claude-coding-worker/main.d.ts +2 -0
- package/dist/claude-coding-worker/main.js +41 -0
- package/dist/claude-coding-worker/sdk.d.ts +4 -0
- package/dist/claude-coding-worker/sdk.js +43 -0
- package/dist/claude-coding-worker/tool-relay.d.ts +1 -0
- package/dist/claude-coding-worker/tool-relay.js +9 -0
- package/dist/cli-help.d.ts +1 -0
- package/dist/cli-help.js +20 -0
- package/dist/cli.d.ts +18 -0
- package/dist/cli.js +629 -0
- package/dist/coding/observability.d.ts +59 -0
- package/dist/coding/observability.js +77 -0
- package/dist/coding/profile.d.ts +88 -0
- package/dist/coding/profile.js +137 -0
- package/dist/coding/protocol.d.ts +345 -0
- package/dist/coding/protocol.js +445 -0
- package/dist/coding/provider.d.ts +4 -0
- package/dist/coding/provider.js +18 -0
- package/dist/coding-proxy/main.d.ts +1 -0
- package/dist/coding-proxy/main.js +51 -0
- package/dist/coding-worker/artifact.d.ts +16 -0
- package/dist/coding-worker/artifact.js +37 -0
- package/dist/coding-worker/driver.d.ts +46 -0
- package/dist/coding-worker/driver.js +158 -0
- package/dist/coding-worker/errors.d.ts +1 -0
- package/dist/coding-worker/errors.js +23 -0
- package/dist/coding-worker/isolation-probe.d.ts +1 -0
- package/dist/coding-worker/isolation-probe.js +146 -0
- package/dist/coding-worker/keeper.d.ts +4 -0
- package/dist/coding-worker/keeper.js +29 -0
- package/dist/coding-worker/main.d.ts +2 -0
- package/dist/coding-worker/main.js +43 -0
- package/dist/coding-worker/sdk.d.ts +26 -0
- package/dist/coding-worker/sdk.js +28 -0
- package/dist/coding-worker/types.d.ts +64 -0
- package/dist/coding-worker/types.js +1 -0
- package/dist/config/providers.d.ts +158 -0
- package/dist/config/providers.js +162 -0
- package/dist/core/budget-groups.d.ts +72 -0
- package/dist/core/budget-groups.js +149 -0
- package/dist/core/budget.d.ts +63 -0
- package/dist/core/budget.js +69 -0
- package/dist/core/coding-queue.d.ts +32 -0
- package/dist/core/coding-queue.js +54 -0
- package/dist/core/cron.d.ts +31 -0
- package/dist/core/cron.js +43 -0
- package/dist/core/datastores.d.ts +41 -0
- package/dist/core/datastores.js +105 -0
- package/dist/core/db.d.ts +2 -0
- package/dist/core/db.js +2 -0
- package/dist/core/dispatch.d.ts +81 -0
- package/dist/core/dispatch.js +184 -0
- package/dist/core/engine-native.d.ts +29 -0
- package/dist/core/engine-native.js +259 -0
- package/dist/core/http-runtime.d.ts +7 -0
- package/dist/core/http-runtime.js +101 -0
- package/dist/core/lease.d.ts +13 -0
- package/dist/core/lease.js +22 -0
- package/dist/core/logger.d.ts +13 -0
- package/dist/core/logger.js +45 -0
- package/dist/core/memory-tools.d.ts +11 -0
- package/dist/core/memory-tools.js +122 -0
- package/dist/core/reconciler.d.ts +45 -0
- package/dist/core/reconciler.js +152 -0
- package/dist/core/runner.d.ts +87 -0
- package/dist/core/runner.js +529 -0
- package/dist/core/scheduler.d.ts +47 -0
- package/dist/core/scheduler.js +121 -0
- package/dist/core/secrets.d.ts +46 -0
- package/dist/core/secrets.js +100 -0
- package/dist/core/subagent-memory-tools.d.ts +18 -0
- package/dist/core/subagent-memory-tools.js +121 -0
- package/dist/core/timing.d.ts +7 -0
- package/dist/core/timing.js +7 -0
- package/dist/core/webhooks.d.ts +23 -0
- package/dist/core/webhooks.js +113 -0
- package/dist/env.d.ts +11 -0
- package/dist/env.js +13 -0
- package/dist/import/budgets.d.ts +16 -0
- package/dist/import/budgets.js +21 -0
- package/dist/import/bundle.d.ts +16 -0
- package/dist/import/bundle.js +61 -0
- package/dist/import/cli-args.d.ts +2 -0
- package/dist/import/cli-args.js +44 -0
- package/dist/import/create.d.ts +34 -0
- package/dist/import/create.js +363 -0
- package/dist/import/index.d.ts +25 -0
- package/dist/import/index.js +130 -0
- package/dist/import/model-gate.d.ts +1 -0
- package/dist/import/model-gate.js +3 -0
- package/dist/import/neutral-schema.d.ts +369 -0
- package/dist/import/neutral-schema.js +109 -0
- package/dist/import/preflight.d.ts +41 -0
- package/dist/import/preflight.js +71 -0
- package/dist/import/report.d.ts +5 -0
- package/dist/import/report.js +98 -0
- package/dist/import/tool-scan.d.ts +8 -0
- package/dist/import/tool-scan.js +20 -0
- package/dist/mcp/auth/ownership.d.ts +113 -0
- package/dist/mcp/auth/ownership.js +87 -0
- package/dist/mcp/auth/principal.d.ts +9 -0
- package/dist/mcp/auth/principal.js +9 -0
- package/dist/mcp/auth/resource-server.d.ts +47 -0
- package/dist/mcp/auth/resource-server.js +82 -0
- package/dist/mcp/auth/self-hosted/browser.d.ts +3 -0
- package/dist/mcp/auth/self-hosted/browser.js +273 -0
- package/dist/mcp/auth/self-hosted/cli.d.ts +2 -0
- package/dist/mcp/auth/self-hosted/cli.js +39 -0
- package/dist/mcp/auth/self-hosted/credentials.d.ts +58 -0
- package/dist/mcp/auth/self-hosted/credentials.js +106 -0
- package/dist/mcp/auth/self-hosted/rate-limit.d.ts +13 -0
- package/dist/mcp/auth/self-hosted/rate-limit.js +20 -0
- package/dist/mcp/auth/self-hosted/session.d.ts +36 -0
- package/dist/mcp/auth/self-hosted/session.js +110 -0
- package/dist/mcp/capabilities.d.ts +11 -0
- package/dist/mcp/capabilities.js +10 -0
- package/dist/mcp/context.d.ts +46 -0
- package/dist/mcp/context.js +1 -0
- package/dist/mcp/errors.d.ts +13 -0
- package/dist/mcp/errors.js +22 -0
- package/dist/mcp/index.d.ts +32 -0
- package/dist/mcp/index.js +187 -0
- package/dist/mcp/jsonrpc.d.ts +8 -0
- package/dist/mcp/jsonrpc.js +1 -0
- package/dist/mcp/server.d.ts +107 -0
- package/dist/mcp/server.js +203 -0
- package/dist/mcp/tasks/manager.d.ts +63 -0
- package/dist/mcp/tasks/manager.js +103 -0
- package/dist/mcp/tools/agents.d.ts +2 -0
- package/dist/mcp/tools/agents.js +375 -0
- package/dist/mcp/tools/budget-groups.d.ts +2 -0
- package/dist/mcp/tools/budget-groups.js +136 -0
- package/dist/mcp/tools/datastore.d.ts +2 -0
- package/dist/mcp/tools/datastore.js +217 -0
- package/dist/mcp/tools/memory.d.ts +3 -0
- package/dist/mcp/tools/memory.js +60 -0
- package/dist/mcp/tools/models.d.ts +8 -0
- package/dist/mcp/tools/models.js +15 -0
- package/dist/mcp/tools/runs.d.ts +2 -0
- package/dist/mcp/tools/runs.js +61 -0
- package/dist/mcp/tools/scheduling.d.ts +2 -0
- package/dist/mcp/tools/scheduling.js +48 -0
- package/dist/mcp/tools/secret-elicitation-form.d.ts +22 -0
- package/dist/mcp/tools/secret-elicitation-form.js +93 -0
- package/dist/mcp/tools/secret-elicitation-server.d.ts +5 -0
- package/dist/mcp/tools/secret-elicitation-server.js +57 -0
- package/dist/mcp/tools/secret-elicitation.d.ts +38 -0
- package/dist/mcp/tools/secret-elicitation.js +52 -0
- package/dist/mcp/tools/secrets.d.ts +13 -0
- package/dist/mcp/tools/secrets.js +124 -0
- package/dist/mcp/tools/subagents.d.ts +2 -0
- package/dist/mcp/tools/subagents.js +133 -0
- package/dist/mcp/tools/text-result.d.ts +6 -0
- package/dist/mcp/tools/text-result.js +3 -0
- package/dist/mcp/tools/tools.d.ts +2 -0
- package/dist/mcp/tools/tools.js +211 -0
- package/dist/mcp/tools/trigger.d.ts +2 -0
- package/dist/mcp/tools/trigger.js +120 -0
- package/dist/mcp/tools/webhooks.d.ts +2 -0
- package/dist/mcp/tools/webhooks.js +34 -0
- package/dist/mcp/transport/http-limits.d.ts +17 -0
- package/dist/mcp/transport/http-limits.js +92 -0
- package/dist/mcp/transport/stdio.d.ts +23 -0
- package/dist/mcp/transport/stdio.js +6 -0
- package/dist/mcp/transport/streamable-http.d.ts +34 -0
- package/dist/mcp/transport/streamable-http.js +158 -0
- package/dist/mcp/unattended-schedules.d.ts +15 -0
- package/dist/mcp/unattended-schedules.js +21 -0
- package/dist/mcp/webhooks/ingress.d.ts +17 -0
- package/dist/mcp/webhooks/ingress.js +33 -0
- package/dist/observability/config.d.ts +6 -0
- package/dist/observability/config.js +20 -0
- package/dist/observability/metrics-server.d.ts +11 -0
- package/dist/observability/metrics-server.js +47 -0
- package/dist/observability/metrics.d.ts +36 -0
- package/dist/observability/metrics.js +158 -0
- package/dist/providers/auth/authorization-server.d.ts +41 -0
- package/dist/providers/auth/authorization-server.js +1 -0
- package/dist/providers/auth/delegating.d.ts +44 -0
- package/dist/providers/auth/delegating.js +105 -0
- package/dist/providers/auth/index.d.ts +11 -0
- package/dist/providers/auth/index.js +20 -0
- package/dist/providers/auth/release-gate.d.ts +1 -0
- package/dist/providers/auth/release-gate.js +4 -0
- package/dist/providers/auth/self-hosted.d.ts +78 -0
- package/dist/providers/auth/self-hosted.js +377 -0
- package/dist/providers/auth/subject.d.ts +1 -0
- package/dist/providers/auth/subject.js +6 -0
- package/dist/providers/auth/types.d.ts +63 -0
- package/dist/providers/auth/types.js +22 -0
- package/dist/providers/coding-proxy/deny-port.d.ts +5 -0
- package/dist/providers/coding-proxy/deny-port.js +45 -0
- package/dist/providers/coding-proxy/environment-credentials.d.ts +7 -0
- package/dist/providers/coding-proxy/environment-credentials.js +16 -0
- package/dist/providers/coding-proxy/index.d.ts +7 -0
- package/dist/providers/coding-proxy/index.js +7 -0
- package/dist/providers/coding-proxy/memory-ledger.d.ts +17 -0
- package/dist/providers/coding-proxy/memory-ledger.js +103 -0
- package/dist/providers/coding-proxy/metering.d.ts +20 -0
- package/dist/providers/coding-proxy/metering.js +161 -0
- package/dist/providers/coding-proxy/prisma-ledger.d.ts +15 -0
- package/dist/providers/coding-proxy/prisma-ledger.js +213 -0
- package/dist/providers/coding-proxy/proxy.d.ts +75 -0
- package/dist/providers/coding-proxy/proxy.js +721 -0
- package/dist/providers/coding-proxy/runtime.d.ts +18 -0
- package/dist/providers/coding-proxy/runtime.js +39 -0
- package/dist/providers/coding-proxy/secure-fetch.d.ts +9 -0
- package/dist/providers/coding-proxy/secure-fetch.js +72 -0
- package/dist/providers/coding-proxy/server.d.ts +17 -0
- package/dist/providers/coding-proxy/server.js +139 -0
- package/dist/providers/coding-proxy/types.d.ts +99 -0
- package/dist/providers/coding-proxy/types.js +1 -0
- package/dist/providers/datastore/index.d.ts +2 -0
- package/dist/providers/datastore/index.js +2 -0
- package/dist/providers/datastore/postgres.d.ts +20 -0
- package/dist/providers/datastore/postgres.js +130 -0
- package/dist/providers/datastore/scoped.d.ts +10 -0
- package/dist/providers/datastore/scoped.js +46 -0
- package/dist/providers/datastore/types.d.ts +31 -0
- package/dist/providers/datastore/types.js +7 -0
- package/dist/providers/email/types.d.ts +46 -0
- package/dist/providers/email/types.js +8 -0
- package/dist/providers/engine/index.d.ts +1 -0
- package/dist/providers/engine/index.js +1 -0
- package/dist/providers/engine/types.d.ts +67 -0
- package/dist/providers/engine/types.js +16 -0
- package/dist/providers/executor/build.d.ts +6 -0
- package/dist/providers/executor/build.js +15 -0
- package/dist/providers/executor/composition.d.ts +14 -0
- package/dist/providers/executor/composition.js +123 -0
- package/dist/providers/executor/container.d.ts +184 -0
- package/dist/providers/executor/container.js +936 -0
- package/dist/providers/executor/dbos-status.d.ts +50 -0
- package/dist/providers/executor/dbos-status.js +64 -0
- package/dist/providers/executor/dbos.d.ts +53 -0
- package/dist/providers/executor/dbos.js +269 -0
- package/dist/providers/executor/in-process.d.ts +18 -0
- package/dist/providers/executor/in-process.js +38 -0
- package/dist/providers/executor/index.d.ts +8 -0
- package/dist/providers/executor/index.js +8 -0
- package/dist/providers/executor/routing.d.ts +48 -0
- package/dist/providers/executor/routing.js +70 -0
- package/dist/providers/executor/types.d.ts +55 -0
- package/dist/providers/executor/types.js +1 -0
- package/dist/providers/index.d.ts +43 -0
- package/dist/providers/index.js +18 -0
- package/dist/providers/jobs/docker-isolation.d.ts +166 -0
- package/dist/providers/jobs/docker-isolation.js +724 -0
- package/dist/providers/jobs/docker.d.ts +127 -0
- package/dist/providers/jobs/docker.js +1027 -0
- package/dist/providers/jobs/fake-kubernetes-api.d.ts +98 -0
- package/dist/providers/jobs/fake-kubernetes-api.js +142 -0
- package/dist/providers/jobs/fake.d.ts +23 -0
- package/dist/providers/jobs/fake.js +132 -0
- package/dist/providers/jobs/kubernetes-api.d.ts +60 -0
- package/dist/providers/jobs/kubernetes-api.js +20 -0
- package/dist/providers/jobs/kubernetes-client.d.ts +36 -0
- package/dist/providers/jobs/kubernetes-client.js +171 -0
- package/dist/providers/jobs/kubernetes-dry-run-fixture.d.ts +49 -0
- package/dist/providers/jobs/kubernetes-dry-run-fixture.js +141 -0
- package/dist/providers/jobs/kubernetes-isolation.d.ts +147 -0
- package/dist/providers/jobs/kubernetes-isolation.js +607 -0
- package/dist/providers/jobs/kubernetes-platform.d.ts +177 -0
- package/dist/providers/jobs/kubernetes-platform.js +280 -0
- package/dist/providers/jobs/kubernetes-preflight.d.ts +59 -0
- package/dist/providers/jobs/kubernetes-preflight.js +339 -0
- package/dist/providers/jobs/kubernetes-witness.d.ts +10 -0
- package/dist/providers/jobs/kubernetes-witness.js +193 -0
- package/dist/providers/jobs/kubernetes.d.ts +127 -0
- package/dist/providers/jobs/kubernetes.js +874 -0
- package/dist/providers/jobs/safe-extract.d.ts +9 -0
- package/dist/providers/jobs/safe-extract.js +325 -0
- package/dist/providers/jobs/types.d.ts +78 -0
- package/dist/providers/jobs/types.js +6 -0
- package/dist/providers/jobs/workspace-swap.d.ts +2 -0
- package/dist/providers/jobs/workspace-swap.js +47 -0
- package/dist/providers/llm/anthropic.d.ts +13 -0
- package/dist/providers/llm/anthropic.js +21 -0
- package/dist/providers/llm/bedrock.d.ts +19 -0
- package/dist/providers/llm/bedrock.js +33 -0
- package/dist/providers/llm/claude-messages.d.ts +106 -0
- package/dist/providers/llm/claude-messages.js +157 -0
- package/dist/providers/llm/claude-provider.d.ts +40 -0
- package/dist/providers/llm/claude-provider.js +31 -0
- package/dist/providers/llm/index.d.ts +10 -0
- package/dist/providers/llm/index.js +10 -0
- package/dist/providers/llm/openai.d.ts +23 -0
- package/dist/providers/llm/openai.js +156 -0
- package/dist/providers/llm/pricing-anthropic.d.ts +4 -0
- package/dist/providers/llm/pricing-anthropic.js +30 -0
- package/dist/providers/llm/pricing-bedrock-claude.d.ts +12 -0
- package/dist/providers/llm/pricing-bedrock-claude.js +37 -0
- package/dist/providers/llm/pricing-core.d.ts +26 -0
- package/dist/providers/llm/pricing-core.js +17 -0
- package/dist/providers/llm/pricing.d.ts +30 -0
- package/dist/providers/llm/pricing.js +74 -0
- package/dist/providers/llm/registration.d.ts +8 -0
- package/dist/providers/llm/registration.js +39 -0
- package/dist/providers/llm/routing.d.ts +25 -0
- package/dist/providers/llm/routing.js +32 -0
- package/dist/providers/llm/types.d.ts +85 -0
- package/dist/providers/llm/types.js +12 -0
- package/dist/providers/memory/index.d.ts +2 -0
- package/dist/providers/memory/index.js +2 -0
- package/dist/providers/memory/postgres.d.ts +20 -0
- package/dist/providers/memory/postgres.js +72 -0
- package/dist/providers/memory/types.d.ts +32 -0
- package/dist/providers/memory/types.js +17 -0
- package/dist/providers/secrets/app-key.d.ts +8 -0
- package/dist/providers/secrets/app-key.js +50 -0
- package/dist/providers/secrets/index.d.ts +10 -0
- package/dist/providers/secrets/index.js +13 -0
- package/dist/providers/secrets/transfer-envelope.d.ts +12 -0
- package/dist/providers/secrets/transfer-envelope.js +35 -0
- package/dist/providers/secrets/types.d.ts +18 -0
- package/dist/providers/secrets/types.js +12 -0
- package/dist/providers/storage/types.d.ts +18 -0
- package/dist/providers/storage/types.js +7 -0
- package/dist/providers/vcs/git.d.ts +105 -0
- package/dist/providers/vcs/git.js +710 -0
- package/dist/providers/vcs/github.d.ts +105 -0
- package/dist/providers/vcs/github.js +394 -0
- package/dist/providers/vcs/index.d.ts +6 -0
- package/dist/providers/vcs/index.js +26 -0
- package/dist/providers/vcs/types.d.ts +118 -0
- package/dist/providers/vcs/types.js +1 -0
- package/dist/sandbox/bounded-json.d.ts +2 -0
- package/dist/sandbox/bounded-json.js +36 -0
- package/dist/sandbox/bridge.d.ts +15 -0
- package/dist/sandbox/bridge.js +78 -0
- package/dist/sandbox/eval-core.d.ts +37 -0
- package/dist/sandbox/eval-core.js +159 -0
- package/dist/sandbox/fetch-policy.d.ts +24 -0
- package/dist/sandbox/fetch-policy.js +96 -0
- package/dist/sandbox/generated/zod-to-json-schema.bundle.js +5 -0
- package/dist/sandbox/generated/zod.bundle.js +1 -0
- package/dist/sandbox/host-functions.d.ts +29 -0
- package/dist/sandbox/host-functions.js +168 -0
- package/dist/sandbox/limits.d.ts +39 -0
- package/dist/sandbox/limits.js +39 -0
- package/dist/sandbox/parser-worker/__fixtures__/crash.d.ts +1 -0
- package/dist/sandbox/parser-worker/__fixtures__/crash.js +6 -0
- package/dist/sandbox/parser-worker/__fixtures__/echo.d.ts +1 -0
- package/dist/sandbox/parser-worker/__fixtures__/echo.js +5 -0
- package/dist/sandbox/parser-worker/__fixtures__/reject.d.ts +1 -0
- package/dist/sandbox/parser-worker/__fixtures__/reject.js +5 -0
- package/dist/sandbox/parser-worker/__fixtures__/spin-forever.d.ts +1 -0
- package/dist/sandbox/parser-worker/__fixtures__/spin-forever.js +11 -0
- package/dist/sandbox/parser-worker/pool.d.ts +13 -0
- package/dist/sandbox/parser-worker/pool.js +111 -0
- package/dist/sandbox/parser-worker/worker.d.ts +14 -0
- package/dist/sandbox/parser-worker/worker.js +63 -0
- package/dist/sandbox/pii-redaction.d.ts +1 -0
- package/dist/sandbox/pii-redaction.js +29 -0
- package/dist/sandbox/prelude.d.ts +26 -0
- package/dist/sandbox/prelude.js +408 -0
- package/dist/sandbox/quickjs-module.d.ts +13 -0
- package/dist/sandbox/quickjs-module.js +19 -0
- package/dist/sandbox/run-in-sandbox.d.ts +34 -0
- package/dist/sandbox/run-in-sandbox.js +53 -0
- package/dist/sandbox/safe-fetch.d.ts +26 -0
- package/dist/sandbox/safe-fetch.js +140 -0
- package/dist/sandbox/tool-capabilities.d.ts +66 -0
- package/dist/sandbox/tool-capabilities.js +103 -0
- package/dist/sandbox/zod-params.d.ts +22 -0
- package/dist/sandbox/zod-params.js +60 -0
- package/dist/serve.d.ts +14 -0
- package/dist/serve.js +54 -0
- package/dist/wardby-bin.d.ts +2 -0
- package/dist/wardby-bin.js +20 -0
- package/package.json +123 -0
- package/prisma/migrations/20260905000000_init/migration.sql +34 -0
- package/prisma/migrations/20260905010000_scheduler_durability/migration.sql +31 -0
- package/prisma/migrations/20260905020000_tools_multiturn/migration.sql +47 -0
- package/prisma/migrations/20260905030000_cache_tool_json_schema/migration.sql +13 -0
- package/prisma/migrations/20260906000000_mcp_phase4/migration.sql +115 -0
- package/prisma/migrations/20260906010000_run_final_text/migration.sql +7 -0
- package/prisma/migrations/20260906020000_run_turns/migration.sql +6 -0
- package/prisma/migrations/20260906030000_task_principal_id/migration.sql +5 -0
- package/prisma/migrations/20260906040000_secure_self_hosted_oauth/migration.sql +67 -0
- package/prisma/migrations/20260906050000_phase5_coding_agents/migration.sql +52 -0
- package/prisma/migrations/20260906060000_tool_capability_scoping/migration.sql +22 -0
- package/prisma/migrations/20260906070000_datastore_pii_flag/migration.sql +6 -0
- package/prisma/migrations/20260906080000_secret_elicitation_outcomes/migration.sql +13 -0
- package/prisma/migrations/20260907010000_coding_proxy_ledger/migration.sql +57 -0
- package/prisma/migrations/20260907020000_budget_groups/migration.sql +24 -0
- package/prisma/migrations/20260907030000_coding_webhook_task_override/migration.sql +4 -0
- package/prisma/migrations/20260908010000_run_execution_backend/migration.sql +2 -0
- package/prisma/migrations/20260908020000_coding_run_audit_metadata/migration.sql +7 -0
- package/prisma/migrations/20260910010000_agent_secret_bound_name/migration.sql +16 -0
- package/prisma/migrations/20260911010000_coding_worker_toolchains/migration.sql +5 -0
- package/prisma/migrations/20260912010000_coding_provider_contract/migration.sql +24 -0
- package/prisma/migrations/20260912020000_coding_proxy_protocol/migration.sql +10 -0
- package/prisma/migrations/20260912030000_agent_memory/migration.sql +18 -0
- package/prisma/migrations/20260912040000_named_shared_datastores/migration.sql +68 -0
- package/prisma/migrations/20260913000000_run_trigger_webhook/migration.sql +2 -0
- package/prisma/migrations/20260913010000_agent_sub_agent/migration.sql +32 -0
- package/prisma/migrations/20260913020000_run_task_override/migration.sql +5 -0
- package/prisma/migrations/20260913030000_coding_run_root/migration.sql +11 -0
- package/prisma/migrations/20260922010000_coding_run_queued_at/migration.sql +8 -0
- package/prisma/migrations/20260922020000_coding_workspace_disk/migration.sql +8 -0
- package/prisma/migrations/migration_lock.toml +3 -0
- package/prisma/schema.prisma +723 -0
- package/scripts/git-askpass.sh +11 -0
|
@@ -0,0 +1,936 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { mkdir, realpath, rename, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { join, resolve, sep } from "node:path";
|
|
4
|
+
import { CodingProfileSchema } from "../../coding/profile.js";
|
|
5
|
+
import { logger } from "../../core/logger.js";
|
|
6
|
+
import { codingRunObserver } from "../../coding/observability.js";
|
|
7
|
+
import { CODING_PROTOCOL_VERSION, CodingRunResultSchema, CodingTaskInputSchema, parseCodingAgentOutputJson, redactAndTruncate, } from "../../coding/protocol.js";
|
|
8
|
+
import { getModelPricing } from "../llm/pricing.js";
|
|
9
|
+
import { getAnthropicPricing } from "../llm/pricing-anthropic.js";
|
|
10
|
+
import { isImmutableDockerImage } from "../jobs/docker-isolation.js";
|
|
11
|
+
import { HEARTBEAT_TIMEOUT_MS } from "../../core/timing.js";
|
|
12
|
+
const containerLog = logger.child({ module: "container-executor" });
|
|
13
|
+
const TERMINAL_STATUSES = new Set(["succeeded", "failed", "refused", "lost", "budget_exhausted", "cancelled"]);
|
|
14
|
+
const PROVISIONING_BACKEND = "provisioning";
|
|
15
|
+
/** Serializes concurrency-slot claims across replicas. Distinct from the OAuth client lock (7412901). */
|
|
16
|
+
const CODING_SLOT_LOCK_SQL = "SELECT 1 AS locked FROM pg_advisory_xact_lock(7412902)";
|
|
17
|
+
/**
|
|
18
|
+
* Private sentinel thrown inside claimProvisioning's $transaction callback
|
|
19
|
+
* when the Run has already left pending/running (e.g. drainCodingQueue
|
|
20
|
+
* committed a coding_queue_timeout failure between the CodingRun claim
|
|
21
|
+
* update and the Run status flip). Throwing aborts the whole transaction --
|
|
22
|
+
* including the CodingRun claim -- so the claim is never left behind on a
|
|
23
|
+
* run that just got revived as "running"; the catch outside the
|
|
24
|
+
* transaction turns it into a plain "unavailable" result.
|
|
25
|
+
*/
|
|
26
|
+
class RunNoLongerActiveError extends Error {
|
|
27
|
+
}
|
|
28
|
+
function proxyProtocol(provider) {
|
|
29
|
+
if (provider === "codex")
|
|
30
|
+
return "openai-responses";
|
|
31
|
+
if (provider === "claude-code")
|
|
32
|
+
return "anthropic-messages";
|
|
33
|
+
throw new Error("coding_provider_unsupported");
|
|
34
|
+
}
|
|
35
|
+
export class PrismaContainerExecutionStore {
|
|
36
|
+
db;
|
|
37
|
+
options;
|
|
38
|
+
constructor(db, options = {}) {
|
|
39
|
+
this.db = db;
|
|
40
|
+
this.options = options;
|
|
41
|
+
}
|
|
42
|
+
async load(runId) {
|
|
43
|
+
const row = await this.db.run.findUnique({
|
|
44
|
+
where: { id: runId },
|
|
45
|
+
include: { agent: true, codingRun: { include: { proxySession: true } } },
|
|
46
|
+
});
|
|
47
|
+
if (!row?.codingRun)
|
|
48
|
+
return null;
|
|
49
|
+
return {
|
|
50
|
+
runId: row.id,
|
|
51
|
+
status: row.status,
|
|
52
|
+
agentKind: row.agent.kind,
|
|
53
|
+
agentName: row.agent.name,
|
|
54
|
+
ownerId: row.agent.ownerId,
|
|
55
|
+
task: row.codingRun.task,
|
|
56
|
+
repository: row.codingRun.repository,
|
|
57
|
+
baseRef: row.codingRun.baseRef,
|
|
58
|
+
headRef: row.codingRun.headRef,
|
|
59
|
+
provider: row.codingRun.provider,
|
|
60
|
+
model: row.codingRun.model,
|
|
61
|
+
timeoutSec: row.codingRun.timeoutSec,
|
|
62
|
+
allowedEgress: row.codingRun.allowedEgress,
|
|
63
|
+
protectedPaths: row.codingRun.protectedPaths,
|
|
64
|
+
rootCodingRunId: row.codingRun.rootCodingRunId,
|
|
65
|
+
budgetUsd: Number(row.codingRun.budgetReservedUsd),
|
|
66
|
+
tokensIn: row.tokensIn,
|
|
67
|
+
tokensOut: row.tokensOut,
|
|
68
|
+
costUsd: Number(row.costUsd),
|
|
69
|
+
jobHandle: row.codingRun.jobBackend && row.codingRun.jobBackend !== PROVISIONING_BACKEND && row.codingRun.jobHandle
|
|
70
|
+
? { backend: row.codingRun.jobBackend, id: row.codingRun.jobHandle }
|
|
71
|
+
: null,
|
|
72
|
+
provisioningClaim: row.codingRun.jobBackend === PROVISIONING_BACKEND ? (row.codingRun.jobHandle ?? null) : null,
|
|
73
|
+
proxySessionId: row.codingRun.proxySession?.id ?? null,
|
|
74
|
+
result: row.codingRun.result,
|
|
75
|
+
workerImage: row.codingRun.workerImage,
|
|
76
|
+
workspaceDiskMb: row.codingRun.workspaceDiskMb,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
async claimProvisioning(runId, claimId) {
|
|
80
|
+
try {
|
|
81
|
+
return await this.db.$transaction(async (tx) => {
|
|
82
|
+
const { maxConcurrent } = this.options;
|
|
83
|
+
if (maxConcurrent !== undefined) {
|
|
84
|
+
// Slot usage is derived from run state, never a separate counter: a
|
|
85
|
+
// run that finishes, fails, is stopped, or is reconciled to lost stops
|
|
86
|
+
// counting, so a crashed replica cannot leak a slot.
|
|
87
|
+
await tx.$queryRawUnsafe(CODING_SLOT_LOCK_SQL);
|
|
88
|
+
const active = await tx.codingRun.count({
|
|
89
|
+
where: { jobBackend: { not: null }, run: { status: { in: ["pending", "running"] } } },
|
|
90
|
+
});
|
|
91
|
+
// Oldest first (spec §6): a free slot belongs to the queued runs
|
|
92
|
+
// ahead of this one, not to whichever claim reaches the lock first.
|
|
93
|
+
// Without this a fresh dispatch could take a slot freed with no
|
|
94
|
+
// immediate drain (stopped from another replica, say) and starve
|
|
95
|
+
// older queued runs into coding_queue_timeout. A run not yet queued
|
|
96
|
+
// is behind every queued run; queued runs order by (queuedAt,
|
|
97
|
+
// runId), the same order drainCodingQueue starts them in.
|
|
98
|
+
const self = await tx.codingRun.findUnique({ where: { runId }, select: { queuedAt: true } });
|
|
99
|
+
const selfQueuedAt = self?.queuedAt ?? null;
|
|
100
|
+
const queuedAhead = await tx.codingRun.count({
|
|
101
|
+
where: {
|
|
102
|
+
runId: { not: runId },
|
|
103
|
+
queuedAt: { not: null },
|
|
104
|
+
jobBackend: null,
|
|
105
|
+
run: { status: "pending" },
|
|
106
|
+
...(selfQueuedAt
|
|
107
|
+
? { OR: [{ queuedAt: { lt: selfQueuedAt } }, { queuedAt: selfQueuedAt, runId: { lt: runId } }] }
|
|
108
|
+
: {}),
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
if (active + queuedAhead >= maxConcurrent) {
|
|
112
|
+
const queued = await tx.codingRun.updateMany({
|
|
113
|
+
where: { runId, jobBackend: null, queuedAt: null, run: { status: "pending" } },
|
|
114
|
+
data: { queuedAt: new Date() },
|
|
115
|
+
});
|
|
116
|
+
if (queued.count === 1)
|
|
117
|
+
return "queued";
|
|
118
|
+
const alreadyQueued = await tx.codingRun.count({
|
|
119
|
+
where: { runId, jobBackend: null, queuedAt: { not: null }, run: { status: "pending" } },
|
|
120
|
+
});
|
|
121
|
+
return alreadyQueued === 1 ? "queued" : "unavailable";
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
const claimed = await tx.codingRun.updateMany({
|
|
125
|
+
where: {
|
|
126
|
+
runId,
|
|
127
|
+
jobBackend: null,
|
|
128
|
+
jobHandle: null,
|
|
129
|
+
run: { status: { in: ["pending", "running"] } },
|
|
130
|
+
},
|
|
131
|
+
data: { jobBackend: PROVISIONING_BACKEND, jobHandle: claimId, queuedAt: null },
|
|
132
|
+
});
|
|
133
|
+
if (claimed.count === 0)
|
|
134
|
+
return "unavailable";
|
|
135
|
+
const started = await tx.run.updateMany({
|
|
136
|
+
where: { id: runId, status: { in: ["pending", "running"] } },
|
|
137
|
+
data: { status: "running", heartbeatAt: new Date() },
|
|
138
|
+
});
|
|
139
|
+
if (started.count === 0) {
|
|
140
|
+
// The Run left pending/running between the CodingRun claim above and
|
|
141
|
+
// here (e.g. drainCodingQueue's coding_queue_timeout failure landed
|
|
142
|
+
// mid-claim). Abort the whole transaction so the CodingRun claim
|
|
143
|
+
// rolls back too, rather than reviving a run that just failed.
|
|
144
|
+
throw new RunNoLongerActiveError();
|
|
145
|
+
}
|
|
146
|
+
return "claimed";
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
catch (err) {
|
|
150
|
+
if (err instanceof RunNoLongerActiveError)
|
|
151
|
+
return "unavailable";
|
|
152
|
+
throw err;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
async persistHandle(runId, claimId, handle) {
|
|
156
|
+
await this.db.$transaction(async (tx) => {
|
|
157
|
+
const coding = await tx.codingRun.findUnique({ where: { runId }, include: { run: true } });
|
|
158
|
+
if (!coding)
|
|
159
|
+
throw new Error("coding_run_not_found");
|
|
160
|
+
if (coding.run.status !== "pending" && coding.run.status !== "running") {
|
|
161
|
+
throw new Error("coding_run_not_active");
|
|
162
|
+
}
|
|
163
|
+
if (coding.jobBackend || coding.jobHandle) {
|
|
164
|
+
if (coding.jobBackend === handle.backend && coding.jobHandle === handle.id)
|
|
165
|
+
return;
|
|
166
|
+
if (coding.jobBackend !== PROVISIONING_BACKEND || coding.jobHandle !== claimId) {
|
|
167
|
+
throw new Error("coding_job_handle_conflict");
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
const persisted = await tx.codingRun.updateMany({
|
|
171
|
+
where: { runId, jobBackend: PROVISIONING_BACKEND, jobHandle: claimId },
|
|
172
|
+
data: { jobBackend: handle.backend, jobHandle: handle.id },
|
|
173
|
+
});
|
|
174
|
+
if (persisted.count !== 1)
|
|
175
|
+
throw new Error("coding_job_handle_conflict");
|
|
176
|
+
await tx.run.update({
|
|
177
|
+
where: { id: runId },
|
|
178
|
+
data: { status: "running", heartbeatAt: new Date() },
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
async heartbeat(runId) {
|
|
183
|
+
await this.db.run.updateMany({
|
|
184
|
+
where: { id: runId, status: "running" },
|
|
185
|
+
data: { heartbeatAt: new Date() },
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
async complete(runId, status, result) {
|
|
189
|
+
await this.db.$transaction(async (tx) => {
|
|
190
|
+
const run = await tx.run.findUnique({ where: { id: runId }, include: { codingRun: true } });
|
|
191
|
+
if (!run?.codingRun)
|
|
192
|
+
throw new Error("coding_run_not_found");
|
|
193
|
+
if (TERMINAL_STATUSES.has(run.status)) {
|
|
194
|
+
if (run.codingRun.result && stableJson(run.codingRun.result) === stableJson(result))
|
|
195
|
+
return;
|
|
196
|
+
throw new Error("coding_run_terminal_conflict");
|
|
197
|
+
}
|
|
198
|
+
await tx.codingRun.update({
|
|
199
|
+
where: { runId },
|
|
200
|
+
data: { result, resultSchema: CODING_PROTOCOL_VERSION },
|
|
201
|
+
});
|
|
202
|
+
await tx.run.update({
|
|
203
|
+
where: { id: runId },
|
|
204
|
+
data: {
|
|
205
|
+
status,
|
|
206
|
+
finalText: result.summary,
|
|
207
|
+
finishedAt: new Date(),
|
|
208
|
+
heartbeatAt: new Date(),
|
|
209
|
+
error: null,
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
async terminate(runId, status, error, audit) {
|
|
215
|
+
await this.db.$transaction(async (tx) => {
|
|
216
|
+
const updated = await tx.run.updateMany({
|
|
217
|
+
where: { id: runId, status: { in: ["pending", "running"] } },
|
|
218
|
+
data: { status, error, finishedAt: new Date(), heartbeatAt: new Date() },
|
|
219
|
+
});
|
|
220
|
+
if (updated.count && audit) {
|
|
221
|
+
await tx.codingRun.update({
|
|
222
|
+
where: { runId },
|
|
223
|
+
data: { failureCategory: audit.failureCategory, diagnosticId: audit.diagnosticId },
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
/** Plain capabilities live only for the short provisioning window. */
|
|
230
|
+
export class RunCapabilityVault {
|
|
231
|
+
values = new Map();
|
|
232
|
+
set(runId, capability) {
|
|
233
|
+
if (this.values.has(runId))
|
|
234
|
+
throw new Error("coding_capability_exists");
|
|
235
|
+
this.values.set(runId, capability);
|
|
236
|
+
}
|
|
237
|
+
async get(runId) {
|
|
238
|
+
const capability = this.values.get(runId);
|
|
239
|
+
if (!capability)
|
|
240
|
+
throw new Error("coding_capability_unavailable");
|
|
241
|
+
return capability;
|
|
242
|
+
}
|
|
243
|
+
delete(runId) {
|
|
244
|
+
this.values.delete(runId);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
class PreflightError extends Error {
|
|
248
|
+
}
|
|
249
|
+
export class ContainerExecutor {
|
|
250
|
+
options;
|
|
251
|
+
artifactRoot;
|
|
252
|
+
active = new Map();
|
|
253
|
+
sleep;
|
|
254
|
+
now;
|
|
255
|
+
observer;
|
|
256
|
+
startedAt = new Map();
|
|
257
|
+
constructor(options) {
|
|
258
|
+
this.options = options;
|
|
259
|
+
this.artifactRoot = resolve(options.artifactRoot);
|
|
260
|
+
if (this.artifactRoot === resolve("/"))
|
|
261
|
+
throw new Error("coding_artifact_root_invalid");
|
|
262
|
+
if (!isImmutableDockerImage(options.workerImage)) {
|
|
263
|
+
throw new Error("coding_worker_image_invalid");
|
|
264
|
+
}
|
|
265
|
+
for (const versions of Object.values(options.additionalWorkerImages ?? {})) {
|
|
266
|
+
for (const image of Object.values(versions)) {
|
|
267
|
+
if (!isImmutableDockerImage(image))
|
|
268
|
+
throw new Error("coding_worker_image_invalid");
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
for (const image of [options.claudeWorkerImage, options.claudeToolRunnerImage]) {
|
|
272
|
+
if (image !== undefined && !isImmutableDockerImage(image))
|
|
273
|
+
throw new Error("coding_worker_image_invalid");
|
|
274
|
+
}
|
|
275
|
+
if (!/^[A-Za-z][A-Za-z0-9_.:-]{0,199}$/.test(options.credentialRef)) {
|
|
276
|
+
throw new Error("coding_credential_ref_invalid");
|
|
277
|
+
}
|
|
278
|
+
if (options.anthropicCredentialRef !== undefined &&
|
|
279
|
+
!/^[A-Za-z][A-Za-z0-9_.:-]{0,199}$/.test(options.anthropicCredentialRef)) {
|
|
280
|
+
throw new Error("coding_credential_ref_invalid");
|
|
281
|
+
}
|
|
282
|
+
const { cpus, memoryMb, pids, diskMb } = options.limits;
|
|
283
|
+
if (!Number.isFinite(cpus) ||
|
|
284
|
+
cpus <= 0 ||
|
|
285
|
+
!Number.isSafeInteger(memoryMb) ||
|
|
286
|
+
memoryMb <= 0 ||
|
|
287
|
+
!Number.isSafeInteger(pids) ||
|
|
288
|
+
pids <= 0 ||
|
|
289
|
+
!Number.isSafeInteger(diskMb) ||
|
|
290
|
+
diskMb <= 0) {
|
|
291
|
+
throw new Error("coding_resource_limits_invalid");
|
|
292
|
+
}
|
|
293
|
+
this.sleep =
|
|
294
|
+
options.sleep ?? ((milliseconds) => new Promise((resolvePromise) => setTimeout(resolvePromise, milliseconds)));
|
|
295
|
+
this.now = options.now ?? (() => new Date());
|
|
296
|
+
this.observer = options.observer ?? codingRunObserver;
|
|
297
|
+
}
|
|
298
|
+
async start(runId) {
|
|
299
|
+
const current = this.active.get(runId);
|
|
300
|
+
if (current)
|
|
301
|
+
return current;
|
|
302
|
+
this.startedAt.set(runId, this.now().getTime());
|
|
303
|
+
this.emit({ stage: "queued", runId });
|
|
304
|
+
const execution = this.execute(runId).finally(async () => {
|
|
305
|
+
this.active.delete(runId);
|
|
306
|
+
if (!this.options.onSlotReleased)
|
|
307
|
+
return;
|
|
308
|
+
const after = await this.options.store.load(runId).catch(() => null);
|
|
309
|
+
if (after && TERMINAL_STATUSES.has(after.status))
|
|
310
|
+
this.options.onSlotReleased();
|
|
311
|
+
});
|
|
312
|
+
this.active.set(runId, execution);
|
|
313
|
+
return execution;
|
|
314
|
+
}
|
|
315
|
+
async stop(runId, reason = "cancelled") {
|
|
316
|
+
const run = await this.options.store.load(runId);
|
|
317
|
+
if (!run)
|
|
318
|
+
return;
|
|
319
|
+
if (TERMINAL_STATUSES.has(run.status))
|
|
320
|
+
return this.cleanupTerminal(run);
|
|
321
|
+
this.emit({ stage: "stopping", runId, jobId: run.jobHandle?.id });
|
|
322
|
+
// Persist the cancellation fence first so a concurrent finisher cannot publish.
|
|
323
|
+
const failure = this.failure("cancelled", { cancelled: true });
|
|
324
|
+
await this.options.store.terminate(runId, "cancelled", failure.error, failure.audit);
|
|
325
|
+
this.terminal(run, "cancelled", failure.audit);
|
|
326
|
+
if (run.proxySessionId)
|
|
327
|
+
await this.options.sessions.cancelSession(run.proxySessionId).catch(() => undefined);
|
|
328
|
+
if (run.jobHandle) {
|
|
329
|
+
await this.options.jobs.stop(run.jobHandle, reason).catch(() => undefined);
|
|
330
|
+
await this.options.jobs.remove(run.jobHandle).catch(() => undefined);
|
|
331
|
+
}
|
|
332
|
+
this.options.capabilities.delete(runId);
|
|
333
|
+
const input = this.preflightForCleanup(run);
|
|
334
|
+
const workspace = input ? await this.options.vcs.recoverWorkspace(input).catch(() => null) : null;
|
|
335
|
+
if (workspace) {
|
|
336
|
+
await this.options.vcs.cleanup(workspace).catch(() => undefined);
|
|
337
|
+
await this.options.vcs.notifyContinuationFinished?.(workspace, "failed", { agentName: run.agentName });
|
|
338
|
+
}
|
|
339
|
+
await rm(this.artifactPath(runId), { recursive: true, force: true }).catch(() => undefined);
|
|
340
|
+
this.emit({ stage: "cleanup", runId, jobId: run.jobHandle?.id, cleanupSucceeded: true });
|
|
341
|
+
}
|
|
342
|
+
async recover(handle) {
|
|
343
|
+
const run = await this.options.store.load(handle.runId);
|
|
344
|
+
if (!run)
|
|
345
|
+
return { state: "lost", reason: "coding_run_not_found" };
|
|
346
|
+
if (TERMINAL_STATUSES.has(run.status)) {
|
|
347
|
+
await this.cleanupTerminal(run);
|
|
348
|
+
return { state: "terminal" };
|
|
349
|
+
}
|
|
350
|
+
if (handle.backend === PROVISIONING_BACKEND || run.provisioningClaim) {
|
|
351
|
+
await this.abandon(run, "coding_ambiguous_provisioning");
|
|
352
|
+
return { state: "lost", reason: "coding_ambiguous_provisioning" };
|
|
353
|
+
}
|
|
354
|
+
if (!run.jobHandle || run.jobHandle.backend !== handle.backend || run.jobHandle.id !== handle.id) {
|
|
355
|
+
await this.abandon(run, "coding_job_handle_mismatch");
|
|
356
|
+
return { state: "lost", reason: "coding_job_handle_mismatch" };
|
|
357
|
+
}
|
|
358
|
+
let status;
|
|
359
|
+
try {
|
|
360
|
+
status = await this.options.jobs.status(run.jobHandle);
|
|
361
|
+
}
|
|
362
|
+
catch {
|
|
363
|
+
await this.abandon(run, "coding_job_status_unavailable");
|
|
364
|
+
return { state: "lost", reason: "coding_job_status_unavailable" };
|
|
365
|
+
}
|
|
366
|
+
if (status.state === "pending" || status.state === "running") {
|
|
367
|
+
await this.options.store.heartbeat(run.runId);
|
|
368
|
+
return { state: "active" };
|
|
369
|
+
}
|
|
370
|
+
await this.finishTerminal(run, run.jobHandle, status.state);
|
|
371
|
+
const finished = await this.options.store.load(run.runId);
|
|
372
|
+
return finished && TERMINAL_STATUSES.has(finished.status)
|
|
373
|
+
? { state: "terminal" }
|
|
374
|
+
: { state: "lost", reason: "coding_recovery_incomplete" };
|
|
375
|
+
}
|
|
376
|
+
async execute(runId) {
|
|
377
|
+
let run = await this.options.store.load(runId);
|
|
378
|
+
if (!run)
|
|
379
|
+
throw new Error("coding_run_not_found");
|
|
380
|
+
if (TERMINAL_STATUSES.has(run.status))
|
|
381
|
+
return this.cleanupTerminal(run);
|
|
382
|
+
let workspace = null;
|
|
383
|
+
let sessionId = run.proxySessionId;
|
|
384
|
+
let handle = run.jobHandle;
|
|
385
|
+
let claimId = null;
|
|
386
|
+
let spendEnabled = Boolean(sessionId);
|
|
387
|
+
try {
|
|
388
|
+
const prepared = this.preflight(run);
|
|
389
|
+
if (!handle) {
|
|
390
|
+
if (run.provisioningClaim)
|
|
391
|
+
return;
|
|
392
|
+
claimId = randomUUID();
|
|
393
|
+
// "queued": every slot is taken; the run stays pending and
|
|
394
|
+
// drainCodingQueue starts it when one frees. "unavailable": another
|
|
395
|
+
// process owns provisioning, or the run is no longer active.
|
|
396
|
+
if ((await this.options.store.claimProvisioning(runId, claimId)) !== "claimed")
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
try {
|
|
400
|
+
workspace = await this.options.vcs.recoverWorkspace(prepared);
|
|
401
|
+
if (!workspace && handle)
|
|
402
|
+
throw new Error("coding_workspace_lost");
|
|
403
|
+
if (!workspace)
|
|
404
|
+
workspace = await this.options.vcs.prepareWorkspace(prepared);
|
|
405
|
+
this.emit({ stage: "prepared", runId });
|
|
406
|
+
}
|
|
407
|
+
catch (error) {
|
|
408
|
+
if (!spendEnabled && !handle)
|
|
409
|
+
throw new PreflightError(safeError(error), { cause: error });
|
|
410
|
+
throw error;
|
|
411
|
+
}
|
|
412
|
+
await this.options.vcs.notifyContinuationStarted?.(workspace, { agentName: run.agentName });
|
|
413
|
+
if (!handle) {
|
|
414
|
+
if (sessionId)
|
|
415
|
+
throw new Error("coding_ambiguous_provisioning");
|
|
416
|
+
const beforeSession = await this.requireCurrent(runId);
|
|
417
|
+
if (TERMINAL_STATUSES.has(beforeSession.status))
|
|
418
|
+
throw new Error("coding_run_no_longer_active");
|
|
419
|
+
const deadlineAt = new Date(this.now().getTime() + run.timeoutSec * 1_000);
|
|
420
|
+
const session = await this.options.sessions.createSession({
|
|
421
|
+
runId,
|
|
422
|
+
credentialRef: this.credentialRef(run.provider),
|
|
423
|
+
protocol: proxyProtocol(run.provider),
|
|
424
|
+
allowedModels: [run.model],
|
|
425
|
+
deadlineAt,
|
|
426
|
+
budgetUsd: run.budgetUsd,
|
|
427
|
+
});
|
|
428
|
+
spendEnabled = true;
|
|
429
|
+
sessionId = session.id;
|
|
430
|
+
this.options.capabilities.set(runId, session.capability);
|
|
431
|
+
const inputArtifact = await this.writeInput(run, deadlineAt);
|
|
432
|
+
const beforeLaunch = await this.requireCurrent(runId);
|
|
433
|
+
if (TERMINAL_STATUSES.has(beforeLaunch.status))
|
|
434
|
+
throw new Error("coding_run_no_longer_active");
|
|
435
|
+
const spec = this.jobSpec(run, inputArtifact);
|
|
436
|
+
// Backends whose handle is derivable (Kubernetes) persist it first, so a crash between the
|
|
437
|
+
// launch and the write still leaves a handle `abandon()` can stop and remove the run with.
|
|
438
|
+
const planned = this.options.jobs.plannedHandle?.(spec);
|
|
439
|
+
if (planned)
|
|
440
|
+
await this.options.store.persistHandle(runId, claimId, planned);
|
|
441
|
+
handle = await this.whileHeartbeating(runId, () => this.options.jobs.launch(spec));
|
|
442
|
+
if (planned && (planned.backend !== handle.backend || planned.id !== handle.id)) {
|
|
443
|
+
throw new Error("coding_job_handle_mismatch");
|
|
444
|
+
}
|
|
445
|
+
await this.options.store.persistHandle(runId, claimId, handle);
|
|
446
|
+
this.options.capabilities.delete(runId);
|
|
447
|
+
this.emit({ stage: "launched", runId, jobId: handle.id, budgetReservedUsd: run.budgetUsd });
|
|
448
|
+
}
|
|
449
|
+
let delay = this.options.pollMinMs ?? 250;
|
|
450
|
+
const maximumDelay = this.options.pollMaxMs ?? 5_000;
|
|
451
|
+
let observedRunning = false;
|
|
452
|
+
for (;;) {
|
|
453
|
+
const status = await this.options.jobs.status(handle);
|
|
454
|
+
if (status.state !== "pending" && status.state !== "running") {
|
|
455
|
+
await this.finishTerminal(run, handle, status.state, workspace, sessionId);
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
if (status.state === "running" && !observedRunning) {
|
|
459
|
+
observedRunning = true;
|
|
460
|
+
this.emit({ stage: "running", runId, jobId: handle.id });
|
|
461
|
+
}
|
|
462
|
+
await this.options.store.heartbeat(runId);
|
|
463
|
+
await this.sleep(delay);
|
|
464
|
+
delay = Math.min(maximumDelay, Math.max(delay + 1, Math.ceil(delay * 1.5)));
|
|
465
|
+
run = (await this.options.store.load(runId)) ?? run;
|
|
466
|
+
if (TERMINAL_STATUSES.has(run.status))
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
catch (error) {
|
|
471
|
+
this.options.capabilities.delete(runId);
|
|
472
|
+
if (sessionId)
|
|
473
|
+
await this.options.sessions.cancelSession(sessionId).catch(() => undefined);
|
|
474
|
+
if (handle)
|
|
475
|
+
await this.options.jobs.stop(handle, "executor_failure").catch(() => undefined);
|
|
476
|
+
const status = !spendEnabled && error instanceof PreflightError ? "refused" : "failed";
|
|
477
|
+
const failure = this.failure(error);
|
|
478
|
+
this.emit({
|
|
479
|
+
stage: "stopping",
|
|
480
|
+
runId,
|
|
481
|
+
jobId: handle?.id,
|
|
482
|
+
failureCategory: failure.audit.failureCategory,
|
|
483
|
+
diagnosticId: failure.audit.diagnosticId,
|
|
484
|
+
});
|
|
485
|
+
await this.options.store.terminate(runId, status, failure.error, failure.audit);
|
|
486
|
+
this.terminal(run, status, failure.audit);
|
|
487
|
+
if (handle)
|
|
488
|
+
await this.options.jobs.remove(handle).catch(() => undefined);
|
|
489
|
+
if (workspace) {
|
|
490
|
+
await this.options.vcs.cleanup(workspace).catch(() => undefined);
|
|
491
|
+
await this.options.vcs.notifyContinuationFinished?.(workspace, "failed", { agentName: run.agentName });
|
|
492
|
+
}
|
|
493
|
+
this.emit({ stage: "cleanup", runId, jobId: handle?.id, cleanupSucceeded: true });
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* Runs `work` while keeping the run's heartbeat fresh.
|
|
498
|
+
*
|
|
499
|
+
* `jobs.launch` is one long await that legitimately takes minutes on a real
|
|
500
|
+
* cluster — the pod cannot be scheduled until an autoscaler has created a node
|
|
501
|
+
* for it — and nothing inside it beats the heartbeat. Meanwhile the reconciler
|
|
502
|
+
* sweeps any run whose heartbeat is older than HEARTBEAT_TIMEOUT_MS and
|
|
503
|
+
* declares it lost, which deletes the job out from under the launch that is
|
|
504
|
+
* still running. The launch then fails on its own pod having vanished, so a
|
|
505
|
+
* single healthy run produces two failures and neither names the real cause.
|
|
506
|
+
*
|
|
507
|
+
* Measured on GKE Autopilot (2026-09-23): a scheduled run whose pod was
|
|
508
|
+
* waiting on a cold gVisor node pool was declared lost at exactly 60s, and the
|
|
509
|
+
* launch it had interrupted failed 32s later with
|
|
510
|
+
* `kubernetes_isolation_unsupported:timeout`. This never appeared on kind,
|
|
511
|
+
* where a pod is scheduled in seconds and the heartbeat gap stays well inside
|
|
512
|
+
* the timeout. The gap is what is wrong, not the timeout: a run that is
|
|
513
|
+
* provisioning IS alive, and the executor is the only thing that knows it.
|
|
514
|
+
*
|
|
515
|
+
* Failures to beat are swallowed on purpose. A missed beat costs at worst the
|
|
516
|
+
* recovery this exists to prevent, while throwing here would fail a launch
|
|
517
|
+
* that is otherwise healthy.
|
|
518
|
+
*/
|
|
519
|
+
async whileHeartbeating(runId, work) {
|
|
520
|
+
const interval = this.options.heartbeatIntervalMs ?? Math.floor(HEARTBEAT_TIMEOUT_MS / 3);
|
|
521
|
+
const timer = setInterval(() => {
|
|
522
|
+
void this.options.store.heartbeat(runId).catch(() => undefined);
|
|
523
|
+
}, interval);
|
|
524
|
+
// Never hold the process open for a heartbeat: shutdown should not wait on one.
|
|
525
|
+
timer.unref?.();
|
|
526
|
+
try {
|
|
527
|
+
return await work();
|
|
528
|
+
}
|
|
529
|
+
finally {
|
|
530
|
+
clearInterval(timer);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
preflight(run) {
|
|
534
|
+
try {
|
|
535
|
+
if (run.status !== "pending" && run.status !== "running")
|
|
536
|
+
throw new Error("coding_run_status_invalid");
|
|
537
|
+
if (run.agentKind !== "coding" || !run.ownerId)
|
|
538
|
+
throw new Error("coding_run_ownership_invalid");
|
|
539
|
+
if (run.provider === "codex")
|
|
540
|
+
getModelPricing(run.model);
|
|
541
|
+
else if (run.provider === "claude-code")
|
|
542
|
+
getAnthropicPricing(run.model);
|
|
543
|
+
else
|
|
544
|
+
throw new Error("coding_provider_unsupported");
|
|
545
|
+
if (!Number.isFinite(run.budgetUsd) || run.budgetUsd <= 0 || run.costUsd > run.budgetUsd) {
|
|
546
|
+
throw new Error("coding_run_budget_invalid");
|
|
547
|
+
}
|
|
548
|
+
const profile = CodingProfileSchema.parse({
|
|
549
|
+
provider: run.provider,
|
|
550
|
+
repository: run.repository,
|
|
551
|
+
baseRef: run.baseRef,
|
|
552
|
+
defaultTask: null,
|
|
553
|
+
timeoutSec: run.timeoutSec,
|
|
554
|
+
allowedEgress: run.allowedEgress,
|
|
555
|
+
protectedPaths: run.protectedPaths,
|
|
556
|
+
});
|
|
557
|
+
const expectedHeadRunId = run.rootCodingRunId ?? run.runId;
|
|
558
|
+
if (run.headRef !== `wardby/run-${expectedHeadRunId}`)
|
|
559
|
+
throw new Error("coding_head_ref_invalid");
|
|
560
|
+
const continuationOf = run.rootCodingRunId ? { runId: run.rootCodingRunId } : undefined;
|
|
561
|
+
CodingTaskInputSchema.parse({
|
|
562
|
+
schemaVersion: CODING_PROTOCOL_VERSION,
|
|
563
|
+
runId: run.runId,
|
|
564
|
+
repository: profile.repository,
|
|
565
|
+
baseRef: profile.baseRef,
|
|
566
|
+
headRef: run.headRef,
|
|
567
|
+
task: run.task,
|
|
568
|
+
model: run.model,
|
|
569
|
+
budgetUsd: run.budgetUsd,
|
|
570
|
+
deadlineAt: new Date(this.now().getTime() + run.timeoutSec * 1_000).toISOString(),
|
|
571
|
+
continuationOf,
|
|
572
|
+
});
|
|
573
|
+
return {
|
|
574
|
+
runId: run.runId,
|
|
575
|
+
repository: profile.repository,
|
|
576
|
+
baseRef: profile.baseRef,
|
|
577
|
+
headRef: run.headRef,
|
|
578
|
+
protectedPaths: profile.protectedPaths,
|
|
579
|
+
continuation: run.rootCodingRunId ? { rootRunId: run.rootCodingRunId } : undefined,
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
catch (error) {
|
|
583
|
+
throw new PreflightError(safeError(error), { cause: error });
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
async finishTerminal(run, handle, jobState, existingWorkspace, existingSessionId) {
|
|
587
|
+
const sessionId = existingSessionId ?? run.proxySessionId;
|
|
588
|
+
if (sessionId)
|
|
589
|
+
await this.options.sessions.cancelSession(sessionId).catch(() => undefined);
|
|
590
|
+
// For notifyContinuationFinished in the `finally` below -- defaults to
|
|
591
|
+
// "failed" and is only flipped right before an actual success return.
|
|
592
|
+
let outcome = "failed";
|
|
593
|
+
let finishedSummary;
|
|
594
|
+
try {
|
|
595
|
+
if (jobState !== "succeeded") {
|
|
596
|
+
const collected = await this.options.jobs.collect(handle).catch(() => null);
|
|
597
|
+
const reason = collected?.diagnostic ?? collected?.reason ?? jobState;
|
|
598
|
+
this.emit({ stage: "collected", runId: run.runId, jobId: handle.id });
|
|
599
|
+
const status = jobState === "lost" ? "lost" : "failed";
|
|
600
|
+
const failure = this.failure(`job_${reason}`);
|
|
601
|
+
await this.options.store.terminate(run.runId, status, failure.error, failure.audit);
|
|
602
|
+
this.terminal(run, status, failure.audit);
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
const collected = await this.options.jobs.collect(handle);
|
|
606
|
+
this.emit({ stage: "collected", runId: run.runId, jobId: handle.id });
|
|
607
|
+
if (collected.reason !== "completed" || !collected.resultArtifact)
|
|
608
|
+
throw new Error("coding_result_missing");
|
|
609
|
+
const output = parseCodingAgentOutputJson(collected.resultArtifact);
|
|
610
|
+
if (output.runId !== run.runId)
|
|
611
|
+
throw new Error("coding_result_run_mismatch");
|
|
612
|
+
let current = await this.requireCurrent(run.runId);
|
|
613
|
+
if (current.costUsd > current.budgetUsd || output.outcome === "budget_exhausted") {
|
|
614
|
+
this.emit({ stage: "budget_cutoff", runId: run.runId, jobId: handle.id });
|
|
615
|
+
await this.options.store.complete(run.runId, "budget_exhausted", this.resultFor(output, current, "budget_exhausted"));
|
|
616
|
+
this.terminal(current, "budget_exhausted");
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
if (output.outcome === "no_changes") {
|
|
620
|
+
await this.options.store.complete(run.runId, "succeeded", this.resultFor(output, current));
|
|
621
|
+
this.terminal(current, "succeeded");
|
|
622
|
+
outcome = "succeeded";
|
|
623
|
+
finishedSummary = output.summary;
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
const preparedInput = this.preflight(current);
|
|
627
|
+
const workspace = existingWorkspace ?? (await this.options.vcs.recoverWorkspace(preparedInput));
|
|
628
|
+
if (!workspace)
|
|
629
|
+
throw new Error("coding_workspace_lost");
|
|
630
|
+
await this.options.jobs.materializeWorkspace(handle, workspace.workspacePath);
|
|
631
|
+
current = await this.requireCurrent(run.runId);
|
|
632
|
+
if (current.costUsd > current.budgetUsd) {
|
|
633
|
+
this.emit({ stage: "budget_cutoff", runId: run.runId, jobId: handle.id });
|
|
634
|
+
await this.options.store.complete(run.runId, "budget_exhausted", this.resultFor(output, current, "budget_exhausted"));
|
|
635
|
+
this.terminal(current, "budget_exhausted");
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
const finalized = await this.options.vcs.finalizeChanges(workspace, {
|
|
639
|
+
summary: output.summary,
|
|
640
|
+
tests: output.tests,
|
|
641
|
+
tag: output.tag,
|
|
642
|
+
});
|
|
643
|
+
const result = this.resultFor(output, current, finalized.outcome, finalized);
|
|
644
|
+
await this.options.store.complete(run.runId, "succeeded", result);
|
|
645
|
+
if (finalized.outcome === "pull_request_opened" || finalized.outcome === "pull_request_updated") {
|
|
646
|
+
this.emit({ stage: finalized.outcome, runId: run.runId, jobId: handle.id });
|
|
647
|
+
}
|
|
648
|
+
this.terminal(current, "succeeded");
|
|
649
|
+
outcome = "succeeded";
|
|
650
|
+
finishedSummary = output.summary;
|
|
651
|
+
}
|
|
652
|
+
catch (error) {
|
|
653
|
+
const failure = this.failure(error);
|
|
654
|
+
await this.options.store.terminate(run.runId, "failed", failure.error, failure.audit);
|
|
655
|
+
this.terminal(run, "failed", failure.audit);
|
|
656
|
+
}
|
|
657
|
+
finally {
|
|
658
|
+
await this.options.jobs.remove(handle).catch(() => undefined);
|
|
659
|
+
const input = this.preflightForCleanup(run);
|
|
660
|
+
const workspace = existingWorkspace ?? (input ? await this.options.vcs.recoverWorkspace(input).catch(() => null) : null);
|
|
661
|
+
if (workspace) {
|
|
662
|
+
await this.options.vcs.cleanup(workspace).catch(() => undefined);
|
|
663
|
+
await this.options.vcs.notifyContinuationFinished?.(workspace, outcome, {
|
|
664
|
+
summary: finishedSummary,
|
|
665
|
+
agentName: run.agentName,
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
await rm(this.artifactPath(run.runId), { recursive: true, force: true }).catch(() => undefined);
|
|
669
|
+
this.emit({ stage: "cleanup", runId: run.runId, jobId: handle.id, cleanupSucceeded: true });
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
resultFor(output, run, forcedOutcome, finalized) {
|
|
673
|
+
const outcome = forcedOutcome ?? (output.outcome === "budget_exhausted" ? "budget_exhausted" : "no_changes");
|
|
674
|
+
return CodingRunResultSchema.parse({
|
|
675
|
+
schemaVersion: CODING_PROTOCOL_VERSION,
|
|
676
|
+
outcome,
|
|
677
|
+
repository: run.repository,
|
|
678
|
+
baseRef: run.baseRef,
|
|
679
|
+
...((outcome === "pull_request_opened" || outcome === "pull_request_updated") &&
|
|
680
|
+
(finalized?.outcome === "pull_request_opened" || finalized?.outcome === "pull_request_updated")
|
|
681
|
+
? {
|
|
682
|
+
headRef: finalized.headRef,
|
|
683
|
+
commitSha: finalized.commitSha,
|
|
684
|
+
pullRequestUrl: finalized.pullRequestUrl,
|
|
685
|
+
pullRequestNumber: finalized.pullRequestNumber,
|
|
686
|
+
}
|
|
687
|
+
: {}),
|
|
688
|
+
summary: output.summary,
|
|
689
|
+
tests: output.tests,
|
|
690
|
+
tag: output.tag,
|
|
691
|
+
usage: { tokensIn: run.tokensIn, tokensOut: run.tokensOut, costUsd: run.costUsd },
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
resolveCodingWorkerImage(selector) {
|
|
695
|
+
if (selector.provider === "claude-code") {
|
|
696
|
+
if (selector.toolchain !== "node" || selector.toolchainVersion !== null) {
|
|
697
|
+
throw new Error("coding_toolchain_unsupported:claude-code");
|
|
698
|
+
}
|
|
699
|
+
const image = selector.workerImageRef ?? this.options.claudeWorkerImage;
|
|
700
|
+
if (!image || !this.options.claudeToolRunnerImage)
|
|
701
|
+
throw new Error("coding_provider_not_configured:claude-code");
|
|
702
|
+
if (!isImmutableDockerImage(image) || !isImmutableDockerImage(this.options.claudeToolRunnerImage)) {
|
|
703
|
+
throw new Error("coding_worker_image_invalid");
|
|
704
|
+
}
|
|
705
|
+
return image;
|
|
706
|
+
}
|
|
707
|
+
if (selector.provider !== "codex") {
|
|
708
|
+
throw new Error(`coding_provider_unsupported:${String(selector.provider)}`);
|
|
709
|
+
}
|
|
710
|
+
if (selector.workerImageRef) {
|
|
711
|
+
if (!isImmutableDockerImage(selector.workerImageRef))
|
|
712
|
+
throw new Error("coding_worker_image_invalid");
|
|
713
|
+
return selector.workerImageRef;
|
|
714
|
+
}
|
|
715
|
+
if (selector.toolchain === "node")
|
|
716
|
+
return this.options.workerImage;
|
|
717
|
+
const versions = this.options.additionalWorkerImages?.[selector.toolchain];
|
|
718
|
+
const image = selector.toolchainVersion ? versions?.[selector.toolchainVersion] : undefined;
|
|
719
|
+
if (!image) {
|
|
720
|
+
throw new Error(`No worker image for toolchain "${selector.toolchain}" version "${selector.toolchainVersion ?? "(none)"}" — refusing to guess. Add it to additionalWorkerImages.`);
|
|
721
|
+
}
|
|
722
|
+
return image;
|
|
723
|
+
}
|
|
724
|
+
async requireCurrent(runId) {
|
|
725
|
+
const current = await this.options.store.load(runId);
|
|
726
|
+
if (!current)
|
|
727
|
+
throw new Error("coding_run_not_found");
|
|
728
|
+
return current;
|
|
729
|
+
}
|
|
730
|
+
jobSpec(run, inputArtifact) {
|
|
731
|
+
const provider = run.provider === "claude-code" ? "claude-code" : "codex";
|
|
732
|
+
if (provider === "claude-code" && (!run.workerImage || !this.options.claudeToolRunnerImage)) {
|
|
733
|
+
throw new Error("coding_provider_not_configured:claude-code");
|
|
734
|
+
}
|
|
735
|
+
if (run.workspaceDiskMb && run.workspaceDiskMb > this.options.maxDiskMb) {
|
|
736
|
+
throw new Error("coding_workspace_disk_exceeds_limit");
|
|
737
|
+
}
|
|
738
|
+
return {
|
|
739
|
+
kind: "coding-agent",
|
|
740
|
+
runId: run.runId,
|
|
741
|
+
provider,
|
|
742
|
+
image: run.workerImage ?? this.options.workerImage,
|
|
743
|
+
...(provider === "claude-code" ? { toolImage: this.options.claudeToolRunnerImage } : {}),
|
|
744
|
+
inputArtifact,
|
|
745
|
+
timeoutSec: run.timeoutSec,
|
|
746
|
+
limits: { ...this.options.limits, ...(run.workspaceDiskMb ? { diskMb: run.workspaceDiskMb } : {}) },
|
|
747
|
+
labels: {},
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
credentialRef(provider) {
|
|
751
|
+
if (provider === "codex")
|
|
752
|
+
return this.options.credentialRef;
|
|
753
|
+
if (provider === "claude-code" && this.options.anthropicCredentialRef)
|
|
754
|
+
return this.options.anthropicCredentialRef;
|
|
755
|
+
throw new Error("coding_provider_not_configured:claude-code");
|
|
756
|
+
}
|
|
757
|
+
async writeInput(run, deadlineAt) {
|
|
758
|
+
await mkdir(this.artifactRoot, { recursive: true, mode: 0o700 });
|
|
759
|
+
const rootReal = await realpath(this.artifactRoot);
|
|
760
|
+
const directory = resolve(rootReal, run.runId);
|
|
761
|
+
if (!directory.startsWith(`${rootReal}${sep}`))
|
|
762
|
+
throw new Error("coding_artifact_path_invalid");
|
|
763
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
764
|
+
if ((await realpath(directory)) !== directory)
|
|
765
|
+
throw new Error("coding_artifact_path_invalid");
|
|
766
|
+
const destination = join(directory, "input.json");
|
|
767
|
+
const temporary = join(directory, `.input-${randomUUID()}.tmp`);
|
|
768
|
+
const input = CodingTaskInputSchema.parse({
|
|
769
|
+
schemaVersion: CODING_PROTOCOL_VERSION,
|
|
770
|
+
runId: run.runId,
|
|
771
|
+
repository: run.repository,
|
|
772
|
+
baseRef: run.baseRef,
|
|
773
|
+
headRef: run.headRef,
|
|
774
|
+
task: run.task,
|
|
775
|
+
model: run.model,
|
|
776
|
+
budgetUsd: run.budgetUsd,
|
|
777
|
+
deadlineAt: deadlineAt.toISOString(),
|
|
778
|
+
continuationOf: run.rootCodingRunId ? { runId: run.rootCodingRunId } : undefined,
|
|
779
|
+
});
|
|
780
|
+
// The run directory is 0700; world-readable mode only crosses Docker's UID boundary.
|
|
781
|
+
await writeFile(temporary, JSON.stringify(input), { flag: "wx", mode: 0o444 });
|
|
782
|
+
await rename(temporary, destination);
|
|
783
|
+
return destination;
|
|
784
|
+
}
|
|
785
|
+
artifactPath(runId) {
|
|
786
|
+
return resolve(this.artifactRoot, runId);
|
|
787
|
+
}
|
|
788
|
+
preflightForCleanup(run) {
|
|
789
|
+
if (!Array.isArray(run.protectedPaths) || !run.protectedPaths.every((path) => typeof path === "string"))
|
|
790
|
+
return null;
|
|
791
|
+
return {
|
|
792
|
+
runId: run.runId,
|
|
793
|
+
repository: run.repository,
|
|
794
|
+
baseRef: run.baseRef,
|
|
795
|
+
headRef: run.headRef,
|
|
796
|
+
protectedPaths: run.protectedPaths,
|
|
797
|
+
continuation: run.rootCodingRunId ? { rootRunId: run.rootCodingRunId } : undefined,
|
|
798
|
+
};
|
|
799
|
+
}
|
|
800
|
+
async abandon(run, reason) {
|
|
801
|
+
const failure = this.failure(reason);
|
|
802
|
+
await this.options.store.terminate(run.runId, "lost", failure.error, failure.audit);
|
|
803
|
+
this.terminal(run, "lost", failure.audit);
|
|
804
|
+
if (run.proxySessionId)
|
|
805
|
+
await this.options.sessions.cancelSession(run.proxySessionId).catch(() => undefined);
|
|
806
|
+
if (run.jobHandle) {
|
|
807
|
+
await this.options.jobs.stop(run.jobHandle, reason).catch(() => undefined);
|
|
808
|
+
await this.options.jobs.collect(run.jobHandle).catch(() => undefined);
|
|
809
|
+
await this.options.jobs.remove(run.jobHandle).catch(() => undefined);
|
|
810
|
+
}
|
|
811
|
+
const input = this.preflightForCleanup(run);
|
|
812
|
+
const workspace = input ? await this.options.vcs.recoverWorkspace(input).catch(() => null) : null;
|
|
813
|
+
if (workspace) {
|
|
814
|
+
await this.options.vcs.cleanup(workspace).catch(() => undefined);
|
|
815
|
+
await this.options.vcs.notifyContinuationFinished?.(workspace, "failed", { agentName: run.agentName });
|
|
816
|
+
}
|
|
817
|
+
await rm(this.artifactPath(run.runId), { recursive: true, force: true }).catch(() => undefined);
|
|
818
|
+
this.emit({ stage: "cleanup", runId: run.runId, jobId: run.jobHandle?.id, cleanupSucceeded: true });
|
|
819
|
+
}
|
|
820
|
+
async cleanupTerminal(run) {
|
|
821
|
+
if (run.proxySessionId)
|
|
822
|
+
await this.options.sessions.cancelSession(run.proxySessionId).catch(() => undefined);
|
|
823
|
+
if (run.jobHandle) {
|
|
824
|
+
const status = await this.options.jobs.status(run.jobHandle).catch(() => null);
|
|
825
|
+
if (status?.state === "pending" || status?.state === "running") {
|
|
826
|
+
await this.options.jobs.stop(run.jobHandle, "terminal_cleanup").catch(() => undefined);
|
|
827
|
+
}
|
|
828
|
+
await this.options.jobs.collect(run.jobHandle).catch(() => undefined);
|
|
829
|
+
await this.options.jobs.remove(run.jobHandle).catch(() => undefined);
|
|
830
|
+
}
|
|
831
|
+
const input = this.preflightForCleanup(run);
|
|
832
|
+
const workspace = input ? await this.options.vcs.recoverWorkspace(input).catch(() => null) : null;
|
|
833
|
+
if (workspace) {
|
|
834
|
+
await this.options.vcs.cleanup(workspace).catch(() => undefined);
|
|
835
|
+
// Safety-net retry for an already-terminal run recovered later (e.g.
|
|
836
|
+
// a crash between an earlier store.complete/terminate and this
|
|
837
|
+
// notification actually firing) -- safe to call again because
|
|
838
|
+
// notifyContinuationFinished is find-and-update-or-no-op, never
|
|
839
|
+
// find-or-create.
|
|
840
|
+
await this.options.vcs.notifyContinuationFinished?.(workspace, run.status === "succeeded" ? "succeeded" : "failed", { agentName: run.agentName });
|
|
841
|
+
}
|
|
842
|
+
await rm(this.artifactPath(run.runId), { recursive: true, force: true }).catch(() => undefined);
|
|
843
|
+
this.emit({ stage: "cleanup", runId: run.runId, jobId: run.jobHandle?.id, cleanupSucceeded: true });
|
|
844
|
+
}
|
|
845
|
+
emit(event) {
|
|
846
|
+
try {
|
|
847
|
+
this.observer.emit(event);
|
|
848
|
+
}
|
|
849
|
+
catch {
|
|
850
|
+
// Telemetry cannot change a run's security or terminal behavior.
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
terminal(run, outcome, audit) {
|
|
854
|
+
this.emit({
|
|
855
|
+
stage: "terminal",
|
|
856
|
+
runId: run.runId,
|
|
857
|
+
jobId: run.jobHandle?.id,
|
|
858
|
+
outcome,
|
|
859
|
+
failureCategory: audit?.failureCategory,
|
|
860
|
+
diagnosticId: audit?.diagnosticId,
|
|
861
|
+
durationMs: this.duration(run.runId),
|
|
862
|
+
budgetActualUsd: run.costUsd,
|
|
863
|
+
...(run.provider === "codex" || run.provider === "claude-code"
|
|
864
|
+
? { workerProvider: run.provider, proxyProtocol: proxyProtocol(run.provider) }
|
|
865
|
+
: {}),
|
|
866
|
+
});
|
|
867
|
+
}
|
|
868
|
+
/**
|
|
869
|
+
* `cancelled` is passed by the one caller that knows — stop(). It is never
|
|
870
|
+
* inferred from the message: `failureCategory` substring-matches, and
|
|
871
|
+
* "cancel"/"Canceled" is routine phrasing in gRPC, Kubernetes, Docker and
|
|
872
|
+
* aborted-HTTP errors ("rpc error: code = Canceled desc = context canceled"),
|
|
873
|
+
* so inferring it would silence exactly the genuine failures this log exists
|
|
874
|
+
* to surface.
|
|
875
|
+
*/
|
|
876
|
+
failure(error, options = {}) {
|
|
877
|
+
const category = failureCategory(error);
|
|
878
|
+
const diagnosticId = `coding_diag_${randomUUID()}`;
|
|
879
|
+
// The persisted error is deliberately opaque (it reaches the agent owner),
|
|
880
|
+
// so the operator needs the real reason somewhere: the control-plane log,
|
|
881
|
+
// keyed by the same diagnostic id. Token-shaped values are redacted, and a
|
|
882
|
+
// cause chain is kept because the outer message is often just a wrapper.
|
|
883
|
+
// A user asking to stop is not a failure, so it gets the same id at info.
|
|
884
|
+
const line = { diagnosticId, category, reason: describeFailure(error) };
|
|
885
|
+
if (options.cancelled === true)
|
|
886
|
+
containerLog.info(line, "coding run cancelled; the persisted error is its id only");
|
|
887
|
+
else
|
|
888
|
+
containerLog.warn(line, "coding run failed; the persisted error is the diagnostic id only");
|
|
889
|
+
return { error: `coding_failure_${category}:${diagnosticId}`, audit: { failureCategory: category, diagnosticId } };
|
|
890
|
+
}
|
|
891
|
+
duration(runId) {
|
|
892
|
+
const startedAt = this.startedAt.get(runId);
|
|
893
|
+
return startedAt === undefined ? undefined : Math.max(0, this.now().getTime() - startedAt);
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
/** Operator-facing failure text: the message plus its cause chain, redacted. */
|
|
897
|
+
export function describeFailure(error, depth = 0) {
|
|
898
|
+
if (depth > 4)
|
|
899
|
+
return "…";
|
|
900
|
+
const message = error instanceof Error ? error.message : typeof error === "string" ? error : "unknown";
|
|
901
|
+
const redacted = redactAndTruncate(message, 500);
|
|
902
|
+
const cause = error instanceof Error ? error.cause : undefined;
|
|
903
|
+
return cause === undefined || cause === null ? redacted : `${redacted} <- ${describeFailure(cause, depth + 1)}`;
|
|
904
|
+
}
|
|
905
|
+
function safeError(error) {
|
|
906
|
+
const message = error instanceof Error ? error.message : "unknown";
|
|
907
|
+
return `coding_executor:${redactAndTruncate(message, 200).replace(/[^A-Za-z0-9_.:-]/g, "_")}`;
|
|
908
|
+
}
|
|
909
|
+
function failureCategory(error) {
|
|
910
|
+
const message = safeError(error);
|
|
911
|
+
if (message.includes("preflight") || message.includes("ownership") || message.includes("image"))
|
|
912
|
+
return "preflight";
|
|
913
|
+
if (message.includes("budget"))
|
|
914
|
+
return "budget";
|
|
915
|
+
if (message.includes("artifact") || message.includes("result"))
|
|
916
|
+
return "artifact";
|
|
917
|
+
if (message.includes("workspace") || message.includes("git") || message.includes("vcs"))
|
|
918
|
+
return "workspace";
|
|
919
|
+
if (message.includes("job") || message.includes("container"))
|
|
920
|
+
return "job";
|
|
921
|
+
if (message.includes("cancel"))
|
|
922
|
+
return "cancelled";
|
|
923
|
+
return "executor";
|
|
924
|
+
}
|
|
925
|
+
function stableJson(value) {
|
|
926
|
+
if (Array.isArray(value))
|
|
927
|
+
return `[${value.map(stableJson).join(",")}]`;
|
|
928
|
+
if (value && typeof value === "object") {
|
|
929
|
+
const record = value;
|
|
930
|
+
return `{${Object.keys(record)
|
|
931
|
+
.sort()
|
|
932
|
+
.map((key) => `${JSON.stringify(key)}:${stableJson(record[key])}`)
|
|
933
|
+
.join(",")}}`;
|
|
934
|
+
}
|
|
935
|
+
return JSON.stringify(value) ?? "null";
|
|
936
|
+
}
|