@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,710 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { lstat, mkdir, opendir, readFile, readlink, realpath, rm } from "node:fs/promises";
|
|
3
|
+
import { isAbsolute, resolve, sep } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { MAX_REDACTED_SPAN, redactTokenShapedValues, normalizeGitHubRepository, normalizeGitRef, } from "../../coding/protocol.js";
|
|
6
|
+
import { isSafeGitHubInstallationToken } from "./github.js";
|
|
7
|
+
export const DEFAULT_MAX_CHANGED_FILES = 1_000;
|
|
8
|
+
export const DEFAULT_MAX_DIFF_BYTES = 1024 * 1024;
|
|
9
|
+
const MAX_GIT_OUTPUT_BYTES = 2 * 1024 * 1024;
|
|
10
|
+
const MAX_WORKSPACE_ENTRIES = 100_000;
|
|
11
|
+
const SAFE_COMMIT_SHA = /^[0-9a-f]{40}$/;
|
|
12
|
+
const SAFE_RUN_ID = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
|
|
13
|
+
const HARDENED_GIT_CONFIG = [
|
|
14
|
+
"-c",
|
|
15
|
+
"core.hooksPath=/dev/null",
|
|
16
|
+
"-c",
|
|
17
|
+
"commit.gpgSign=false",
|
|
18
|
+
"-c",
|
|
19
|
+
"tag.gpgSign=false",
|
|
20
|
+
"-c",
|
|
21
|
+
"credential.helper=",
|
|
22
|
+
"-c",
|
|
23
|
+
"protocol.allow=never",
|
|
24
|
+
"-c",
|
|
25
|
+
"protocol.https.allow=always",
|
|
26
|
+
];
|
|
27
|
+
export class GitCommandError extends Error {
|
|
28
|
+
exitCode;
|
|
29
|
+
constructor(exitCode, detail) {
|
|
30
|
+
super(`git_command_failed:${exitCode ?? "spawn"}${detail ? `:${detail}` : ""}`);
|
|
31
|
+
this.exitCode = exitCode;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const GIT_OUTPUT_DISPLAY_BYTES = 8 * 1024;
|
|
35
|
+
export function redactGitOutput(value, secrets = []) {
|
|
36
|
+
// Only the first 8 KiB is ever kept, so redaction works on that plus one
|
|
37
|
+
// MAX_REDACTED_SPAN of margin rather than on all 2 MiB git may have produced
|
|
38
|
+
// (see redactAndTruncate: the margin is what keeps truncation safe). The rest
|
|
39
|
+
// is discarded here and never reaches a log or the database.
|
|
40
|
+
let output = value.slice(0, GIT_OUTPUT_DISPLAY_BYTES + MAX_REDACTED_SPAN);
|
|
41
|
+
for (const secret of secrets) {
|
|
42
|
+
if (secret)
|
|
43
|
+
output = output.split(secret).join("[REDACTED]");
|
|
44
|
+
}
|
|
45
|
+
return redactTokenShapedValues(output).slice(0, GIT_OUTPUT_DISPLAY_BYTES);
|
|
46
|
+
}
|
|
47
|
+
/** Runs Git without a shell or inherited application secrets. */
|
|
48
|
+
export class NodeGitCommandRunner {
|
|
49
|
+
options;
|
|
50
|
+
gitBinary;
|
|
51
|
+
path;
|
|
52
|
+
askPassPath;
|
|
53
|
+
constructor(options) {
|
|
54
|
+
this.options = options;
|
|
55
|
+
this.gitBinary = options.gitBinary ?? "git";
|
|
56
|
+
this.path = options.path ?? "/usr/bin:/bin";
|
|
57
|
+
this.askPassPath =
|
|
58
|
+
options.askPassPath ?? fileURLToPath(new URL("../../../scripts/git-askpass.sh", import.meta.url));
|
|
59
|
+
}
|
|
60
|
+
async run(args, options = {}) {
|
|
61
|
+
if (options.authToken !== undefined && !isSafeGitHubInstallationToken(options.authToken)) {
|
|
62
|
+
throw new Error("git_auth_token_invalid");
|
|
63
|
+
}
|
|
64
|
+
const env = {
|
|
65
|
+
PATH: this.path,
|
|
66
|
+
HOME: this.options.homeDir,
|
|
67
|
+
XDG_CONFIG_HOME: this.options.homeDir,
|
|
68
|
+
LANG: "C",
|
|
69
|
+
LC_ALL: "C",
|
|
70
|
+
GIT_CONFIG_NOSYSTEM: "1",
|
|
71
|
+
GIT_CONFIG_GLOBAL: "/dev/null",
|
|
72
|
+
GIT_CONFIG_SYSTEM: "/dev/null",
|
|
73
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
74
|
+
GCM_INTERACTIVE: "never",
|
|
75
|
+
GIT_ASKPASS: options.authToken ? this.askPassPath : "/bin/false",
|
|
76
|
+
SSH_ASKPASS: "/bin/false",
|
|
77
|
+
GIT_LFS_SKIP_SMUDGE: "1",
|
|
78
|
+
};
|
|
79
|
+
const maxBytes = options.maxOutputBytes ?? MAX_GIT_OUTPUT_BYTES;
|
|
80
|
+
const secrets = options.authToken ? [options.authToken] : [];
|
|
81
|
+
return new Promise((resolvePromise, rejectPromise) => {
|
|
82
|
+
const child = spawn(this.gitBinary, [...args], {
|
|
83
|
+
cwd: options.cwd,
|
|
84
|
+
env,
|
|
85
|
+
shell: false,
|
|
86
|
+
stdio: ["ignore", "pipe", "pipe", options.authToken ? "pipe" : "ignore"],
|
|
87
|
+
});
|
|
88
|
+
const childStdout = child.stdout;
|
|
89
|
+
const childStderr = child.stderr;
|
|
90
|
+
if (!childStdout || !childStderr) {
|
|
91
|
+
child.kill("SIGKILL");
|
|
92
|
+
rejectPromise(new GitCommandError(null, "git_stdio_unavailable"));
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (options.authToken) {
|
|
96
|
+
const authPipe = child.stdio[3];
|
|
97
|
+
authPipe.on("error", () => undefined);
|
|
98
|
+
authPipe.end(`${options.authToken}\n`);
|
|
99
|
+
}
|
|
100
|
+
const stdout = [];
|
|
101
|
+
const stderr = [];
|
|
102
|
+
let outputBytes = 0;
|
|
103
|
+
let outputExceeded = false;
|
|
104
|
+
let spawnError = false;
|
|
105
|
+
const capture = (chunks) => (chunk) => {
|
|
106
|
+
outputBytes += chunk.length;
|
|
107
|
+
if (outputBytes > maxBytes) {
|
|
108
|
+
outputExceeded = true;
|
|
109
|
+
child.kill("SIGKILL");
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
chunks.push(chunk);
|
|
113
|
+
};
|
|
114
|
+
childStdout.on("data", capture(stdout));
|
|
115
|
+
childStderr.on("data", capture(stderr));
|
|
116
|
+
child.once("error", () => {
|
|
117
|
+
spawnError = true;
|
|
118
|
+
});
|
|
119
|
+
child.once("close", (code) => {
|
|
120
|
+
if (outputExceeded) {
|
|
121
|
+
rejectPromise(new GitCommandError(code, "git_output_limit"));
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (spawnError) {
|
|
125
|
+
rejectPromise(new GitCommandError(null, "git_spawn_failed"));
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const result = {
|
|
129
|
+
stdout: Buffer.concat(stdout).toString("utf8"),
|
|
130
|
+
stderr: redactGitOutput(Buffer.concat(stderr).toString("utf8"), secrets),
|
|
131
|
+
};
|
|
132
|
+
if (code !== 0) {
|
|
133
|
+
rejectPromise(new GitCommandError(code, result.stderr.trim()));
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
resolvePromise(result);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
function validateProtectedPath(value) {
|
|
142
|
+
const path = value.trim();
|
|
143
|
+
if (!path ||
|
|
144
|
+
Buffer.byteLength(path, "utf8") > 512 ||
|
|
145
|
+
path.startsWith("/") ||
|
|
146
|
+
path.startsWith("./") ||
|
|
147
|
+
path.includes("\\") ||
|
|
148
|
+
path.split("/").some((part) => !part || part === "." || part === "..")) {
|
|
149
|
+
throw new Error("vcs_protected_path_invalid");
|
|
150
|
+
}
|
|
151
|
+
return path;
|
|
152
|
+
}
|
|
153
|
+
function globRegex(pattern) {
|
|
154
|
+
let source = "^";
|
|
155
|
+
for (let index = 0; index < pattern.length; index += 1) {
|
|
156
|
+
const character = pattern[index];
|
|
157
|
+
if (character === "*") {
|
|
158
|
+
if (pattern[index + 1] === "*") {
|
|
159
|
+
source += ".*";
|
|
160
|
+
index += 1;
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
source += "[^/]*";
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
else if (character === "?") {
|
|
167
|
+
source += "[^/]";
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
source += character.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return new RegExp(`${source}$`);
|
|
174
|
+
}
|
|
175
|
+
function validateChangedPath(path) {
|
|
176
|
+
if (!path ||
|
|
177
|
+
isAbsolute(path) ||
|
|
178
|
+
path.includes("\\") ||
|
|
179
|
+
path.split("/").some((part) => !part || part === "." || part === "..")) {
|
|
180
|
+
throw new Error("vcs_changed_path_invalid");
|
|
181
|
+
}
|
|
182
|
+
return path;
|
|
183
|
+
}
|
|
184
|
+
function nullSeparated(value) {
|
|
185
|
+
const values = value.split("\0");
|
|
186
|
+
if (values.at(-1) === "")
|
|
187
|
+
values.pop();
|
|
188
|
+
return values.map(validateChangedPath);
|
|
189
|
+
}
|
|
190
|
+
function directChild(root, child) {
|
|
191
|
+
return child.startsWith(`${root}${sep}`) && !child.slice(root.length + 1).includes(sep);
|
|
192
|
+
}
|
|
193
|
+
/** Human-readable label for continuation status notifications -- falls back to the opaque run id alone when no agent name is known. */
|
|
194
|
+
function runLabel(agentName, runId) {
|
|
195
|
+
return agentName ? `${agentName} (wardby run ${runId})` : `wardby run ${runId}`;
|
|
196
|
+
}
|
|
197
|
+
export class GitVcsProvider {
|
|
198
|
+
options;
|
|
199
|
+
rootDir;
|
|
200
|
+
git;
|
|
201
|
+
maxChangedFiles;
|
|
202
|
+
maxDiffBytes;
|
|
203
|
+
cloneUrlForRepository;
|
|
204
|
+
constructor(options) {
|
|
205
|
+
this.options = options;
|
|
206
|
+
this.rootDir = resolve(options.rootDir);
|
|
207
|
+
if (this.rootDir === resolve("/"))
|
|
208
|
+
throw new Error("vcs_root_invalid");
|
|
209
|
+
this.git = options.git ?? new NodeGitCommandRunner({ homeDir: resolve(this.rootDir, ".home") });
|
|
210
|
+
this.maxChangedFiles = options.maxChangedFiles ?? DEFAULT_MAX_CHANGED_FILES;
|
|
211
|
+
this.maxDiffBytes = options.maxDiffBytes ?? DEFAULT_MAX_DIFF_BYTES;
|
|
212
|
+
if (!Number.isSafeInteger(this.maxChangedFiles) ||
|
|
213
|
+
this.maxChangedFiles <= 0 ||
|
|
214
|
+
!Number.isSafeInteger(this.maxDiffBytes) ||
|
|
215
|
+
this.maxDiffBytes <= 0) {
|
|
216
|
+
throw new Error("vcs_limits_invalid");
|
|
217
|
+
}
|
|
218
|
+
this.cloneUrlForRepository =
|
|
219
|
+
options.cloneUrlForRepository ?? ((repository) => `https://github.com/${repository}.git`);
|
|
220
|
+
}
|
|
221
|
+
async prepareWorkspace(input) {
|
|
222
|
+
const normalized = this.validateInput(input);
|
|
223
|
+
await mkdir(this.rootDir, { recursive: true, mode: 0o700 });
|
|
224
|
+
await mkdir(resolve(this.rootDir, ".home"), { recursive: true, mode: 0o700 });
|
|
225
|
+
const runRoot = resolve(this.rootDir, normalized.runId);
|
|
226
|
+
if (!directChild(this.rootDir, runRoot))
|
|
227
|
+
throw new Error("vcs_workspace_path_invalid");
|
|
228
|
+
try {
|
|
229
|
+
await mkdir(runRoot, { mode: 0o700 });
|
|
230
|
+
}
|
|
231
|
+
catch (error) {
|
|
232
|
+
if (error.code === "EEXIST")
|
|
233
|
+
throw new Error("vcs_workspace_exists", { cause: error });
|
|
234
|
+
throw error;
|
|
235
|
+
}
|
|
236
|
+
const workspacePath = resolve(runRoot, "workspace");
|
|
237
|
+
const gitMetadataPath = resolve(runRoot, "git");
|
|
238
|
+
const cloneUrl = this.cloneUrlForRepository(normalized.repository);
|
|
239
|
+
if (!this.options.cloneUrlForRepository && cloneUrl !== `https://github.com/${normalized.repository}.git`) {
|
|
240
|
+
throw new Error("vcs_remote_invalid");
|
|
241
|
+
}
|
|
242
|
+
// Revision-in-place: a continuation clones the EXISTING headRef branch
|
|
243
|
+
// directly (its current tip becomes baseCommit below) instead of
|
|
244
|
+
// baseRef -- everything after this line (branch/checkout/reset, and
|
|
245
|
+
// every check in finalizeChanges) is unchanged either way, since
|
|
246
|
+
// baseCommit already means exactly what it needs to: "where this
|
|
247
|
+
// round started."
|
|
248
|
+
const cloneBranch = normalized.continuation ? normalized.headRef : normalized.baseRef;
|
|
249
|
+
try {
|
|
250
|
+
await this.options.github.withRepositoryToken(normalized.repository, async (token) => {
|
|
251
|
+
await this.git.run([
|
|
252
|
+
...HARDENED_GIT_CONFIG,
|
|
253
|
+
"clone",
|
|
254
|
+
"--no-checkout",
|
|
255
|
+
"--single-branch",
|
|
256
|
+
"--no-tags",
|
|
257
|
+
// History is never needed: the worker gets no Git metadata, the
|
|
258
|
+
// finalizer compares only against baseCommit and the new commit's
|
|
259
|
+
// parent, and pushOnce compares SHAs. Depth 1 keeps large
|
|
260
|
+
// repositories' history off the control plane's disk.
|
|
261
|
+
"--depth",
|
|
262
|
+
"1",
|
|
263
|
+
"--branch",
|
|
264
|
+
cloneBranch,
|
|
265
|
+
"--separate-git-dir",
|
|
266
|
+
gitMetadataPath,
|
|
267
|
+
cloneUrl,
|
|
268
|
+
workspacePath,
|
|
269
|
+
], { cwd: runRoot, authToken: token });
|
|
270
|
+
});
|
|
271
|
+
await rm(resolve(workspacePath, ".git"), { force: true });
|
|
272
|
+
const baseCommit = await this.revParseRaw(gitMetadataPath, workspacePath, `refs/remotes/origin/${cloneBranch}^{commit}`);
|
|
273
|
+
await this.gitForPaths(gitMetadataPath, workspacePath, ["config", "--local", "core.hooksPath", "/dev/null"]);
|
|
274
|
+
await this.gitForPaths(gitMetadataPath, workspacePath, ["config", "--local", "commit.gpgSign", "false"]);
|
|
275
|
+
await this.gitForPaths(gitMetadataPath, workspacePath, ["config", "--local", "tag.gpgSign", "false"]);
|
|
276
|
+
await this.gitForPaths(gitMetadataPath, workspacePath, ["config", "--local", "user.name", "Wardby"]);
|
|
277
|
+
await this.gitForPaths(gitMetadataPath, workspacePath, [
|
|
278
|
+
"config",
|
|
279
|
+
"--local",
|
|
280
|
+
"user.email",
|
|
281
|
+
"wardby@users.noreply.github.com",
|
|
282
|
+
]);
|
|
283
|
+
// Revision-in-place: a continuation's clone (--branch === headRef, above)
|
|
284
|
+
// already leaves headRef checked out locally, pointing at exactly
|
|
285
|
+
// baseCommit -- `branch --force` on an already-checked-out branch is
|
|
286
|
+
// both unnecessary here and something Git refuses outright ("cannot
|
|
287
|
+
// force update the branch ... checked out"). Only a fresh run (cloned
|
|
288
|
+
// baseRef, a DIFFERENT name from headRef) needs this to create headRef.
|
|
289
|
+
if (!normalized.continuation) {
|
|
290
|
+
await this.gitForPaths(gitMetadataPath, workspacePath, ["branch", "--force", normalized.headRef, baseCommit]);
|
|
291
|
+
}
|
|
292
|
+
await this.gitForPaths(gitMetadataPath, workspacePath, [
|
|
293
|
+
"symbolic-ref",
|
|
294
|
+
"HEAD",
|
|
295
|
+
`refs/heads/${normalized.headRef}`,
|
|
296
|
+
]);
|
|
297
|
+
await this.gitForPaths(gitMetadataPath, workspacePath, ["read-tree", "--reset", "-u", baseCommit]);
|
|
298
|
+
await this.inspectWorkspace(workspacePath);
|
|
299
|
+
return {
|
|
300
|
+
id: `vcs-${normalized.runId}`,
|
|
301
|
+
...normalized,
|
|
302
|
+
baseCommit,
|
|
303
|
+
workspacePath,
|
|
304
|
+
gitMetadataPath,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
catch (error) {
|
|
308
|
+
await rm(runRoot, { recursive: true, force: true });
|
|
309
|
+
throw error;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
async recoverWorkspace(input) {
|
|
313
|
+
const normalized = this.validateInput(input);
|
|
314
|
+
const expected = this.expectedPaths(normalized.runId);
|
|
315
|
+
try {
|
|
316
|
+
const metadata = await lstat(expected.runRoot);
|
|
317
|
+
if (!metadata.isDirectory() || metadata.isSymbolicLink())
|
|
318
|
+
throw new Error("vcs_workspace_path_invalid");
|
|
319
|
+
}
|
|
320
|
+
catch (error) {
|
|
321
|
+
if (error.code === "ENOENT")
|
|
322
|
+
return null;
|
|
323
|
+
throw error;
|
|
324
|
+
}
|
|
325
|
+
const cloneBranch = normalized.continuation ? normalized.headRef : normalized.baseRef;
|
|
326
|
+
const baseCommit = await this.revParseRaw(expected.gitMetadataPath, expected.workspacePath, `refs/remotes/origin/${cloneBranch}^{commit}`);
|
|
327
|
+
const recovered = {
|
|
328
|
+
id: `vcs-${normalized.runId}`,
|
|
329
|
+
...normalized,
|
|
330
|
+
baseCommit,
|
|
331
|
+
workspacePath: expected.workspacePath,
|
|
332
|
+
gitMetadataPath: expected.gitMetadataPath,
|
|
333
|
+
};
|
|
334
|
+
const prepared = await this.validatePrepared(recovered);
|
|
335
|
+
await this.inspectWorkspace(prepared.workspacePath);
|
|
336
|
+
await this.assertRemote(prepared);
|
|
337
|
+
await this.assertSafeLocalConfig(prepared.gitMetadataPath);
|
|
338
|
+
const symbolicHead = (await this.gitFor(prepared, ["symbolic-ref", "--short", "HEAD"])).stdout.trim();
|
|
339
|
+
if (symbolicHead !== prepared.headRef)
|
|
340
|
+
throw new Error("vcs_head_ref_mismatch");
|
|
341
|
+
const currentHead = await this.revParse(prepared, "HEAD");
|
|
342
|
+
if (currentHead !== prepared.baseCommit) {
|
|
343
|
+
const parent = await this.revParse(prepared, "HEAD^");
|
|
344
|
+
if (parent !== prepared.baseCommit)
|
|
345
|
+
throw new Error("vcs_commit_history_invalid");
|
|
346
|
+
}
|
|
347
|
+
return prepared;
|
|
348
|
+
}
|
|
349
|
+
async finalizeChanges(workspace, details) {
|
|
350
|
+
const prepared = await this.validatePrepared(workspace);
|
|
351
|
+
await this.inspectWorkspace(prepared.workspacePath);
|
|
352
|
+
await this.assertRemote(prepared);
|
|
353
|
+
await this.assertSafeLocalConfig(prepared.gitMetadataPath);
|
|
354
|
+
await this.gitFor(prepared, ["add", "--all", "--", ":/"]);
|
|
355
|
+
const changed = nullSeparated((await this.gitFor(prepared, [
|
|
356
|
+
"diff",
|
|
357
|
+
"--cached",
|
|
358
|
+
"--name-only",
|
|
359
|
+
"-z",
|
|
360
|
+
"--no-renames",
|
|
361
|
+
prepared.baseCommit,
|
|
362
|
+
"--",
|
|
363
|
+
])).stdout);
|
|
364
|
+
if (changed.length > this.maxChangedFiles)
|
|
365
|
+
throw new Error("vcs_changed_file_limit");
|
|
366
|
+
const protectedMatchers = prepared.protectedPaths.map((path) => ({ path, matcher: globRegex(path) }));
|
|
367
|
+
const protectedChange = changed.find((path) => protectedMatchers.some(({ matcher }) => matcher.test(path)));
|
|
368
|
+
if (protectedChange)
|
|
369
|
+
throw new Error(`vcs_protected_path:${protectedChange}`);
|
|
370
|
+
let diff;
|
|
371
|
+
try {
|
|
372
|
+
diff = (await this.gitFor(prepared, ["diff", "--cached", "--binary", "--no-ext-diff", "--no-textconv", prepared.baseCommit, "--"], { maxOutputBytes: this.maxDiffBytes })).stdout;
|
|
373
|
+
}
|
|
374
|
+
catch (error) {
|
|
375
|
+
if (error instanceof GitCommandError && error.message.includes("git_output_limit")) {
|
|
376
|
+
throw new Error("vcs_diff_size_limit", { cause: error });
|
|
377
|
+
}
|
|
378
|
+
throw error;
|
|
379
|
+
}
|
|
380
|
+
if (Buffer.byteLength(diff, "utf8") > this.maxDiffBytes)
|
|
381
|
+
throw new Error("vcs_diff_size_limit");
|
|
382
|
+
const currentHead = await this.revParse(prepared, "HEAD");
|
|
383
|
+
if (changed.length === 0 && currentHead === prepared.baseCommit) {
|
|
384
|
+
return {
|
|
385
|
+
outcome: "no_changes",
|
|
386
|
+
repository: prepared.repository,
|
|
387
|
+
baseRef: prepared.baseRef,
|
|
388
|
+
baseCommit: prepared.baseCommit,
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
const symbolicHead = (await this.gitFor(prepared, ["symbolic-ref", "--short", "HEAD"])).stdout.trim();
|
|
392
|
+
if (symbolicHead !== prepared.headRef)
|
|
393
|
+
throw new Error("vcs_head_ref_mismatch");
|
|
394
|
+
let commitSha;
|
|
395
|
+
if (currentHead === prepared.baseCommit) {
|
|
396
|
+
await this.gitFor(prepared, ["commit", "--no-verify", "--no-gpg-sign", "-m", `Wardby run ${prepared.runId}`]);
|
|
397
|
+
commitSha = await this.revParse(prepared, "HEAD");
|
|
398
|
+
}
|
|
399
|
+
else {
|
|
400
|
+
const parent = await this.revParse(prepared, "HEAD^");
|
|
401
|
+
if (parent !== prepared.baseCommit)
|
|
402
|
+
throw new Error("vcs_commit_history_invalid");
|
|
403
|
+
const afterCommitChanges = nullSeparated((await this.gitFor(prepared, ["diff", "--cached", "--name-only", "-z", "HEAD", "--"])).stdout);
|
|
404
|
+
if (afterCommitChanges.length > 0)
|
|
405
|
+
throw new Error("vcs_finalized_workspace_changed");
|
|
406
|
+
commitSha = currentHead;
|
|
407
|
+
}
|
|
408
|
+
await this.pushOnce(prepared, commitSha);
|
|
409
|
+
// Revision-in-place: identify the PR by the run that originally opened
|
|
410
|
+
// it (createOrFindDraftPullRequest's marker-based lookup keys on that
|
|
411
|
+
// run's id), not this run's own -- this finds the existing open PR and
|
|
412
|
+
// returns it rather than creating a new one, since headRef/baseRef
|
|
413
|
+
// already match it exactly. No other change needed here: pushing a new
|
|
414
|
+
// commit onto that branch already updates the PR natively.
|
|
415
|
+
const pullRequest = await this.options.github.createOrFindDraftPullRequest({
|
|
416
|
+
runId: prepared.continuation?.rootRunId ?? prepared.runId,
|
|
417
|
+
repository: prepared.repository,
|
|
418
|
+
baseRef: prepared.baseRef,
|
|
419
|
+
headRef: prepared.headRef,
|
|
420
|
+
summary: details?.summary,
|
|
421
|
+
tests: details?.tests,
|
|
422
|
+
tag: details?.tag,
|
|
423
|
+
});
|
|
424
|
+
return {
|
|
425
|
+
outcome: prepared.continuation ? "pull_request_updated" : "pull_request_opened",
|
|
426
|
+
repository: prepared.repository,
|
|
427
|
+
baseRef: prepared.baseRef,
|
|
428
|
+
baseCommit: prepared.baseCommit,
|
|
429
|
+
headRef: prepared.headRef,
|
|
430
|
+
commitSha,
|
|
431
|
+
pullRequestNumber: pullRequest.number,
|
|
432
|
+
pullRequestUrl: pullRequest.url,
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
async cleanup(workspace) {
|
|
436
|
+
const expected = this.expectedPaths(workspace.runId);
|
|
437
|
+
if (workspace.id !== `vcs-${workspace.runId}` ||
|
|
438
|
+
workspace.workspacePath !== expected.workspacePath ||
|
|
439
|
+
workspace.gitMetadataPath !== expected.gitMetadataPath) {
|
|
440
|
+
throw new Error("vcs_workspace_handle_invalid");
|
|
441
|
+
}
|
|
442
|
+
await rm(expected.runRoot, { recursive: true, force: true });
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* Best-effort "wardby is working on this PR" signal (see VcsProvider) --
|
|
446
|
+
* whole body wrapped so this can NEVER throw or otherwise affect the
|
|
447
|
+
* real coding run, mirroring docker.ts's readWorkerFailureDiagnostic
|
|
448
|
+
* ("diagnostics are optional and must never affect terminal cleanup").
|
|
449
|
+
* No-op for a fresh (non-continuation) workspace: there's no PR to
|
|
450
|
+
* attach anything to until its one commit lands.
|
|
451
|
+
*/
|
|
452
|
+
async notifyContinuationStarted(workspace, details) {
|
|
453
|
+
if (!workspace.continuation)
|
|
454
|
+
return;
|
|
455
|
+
try {
|
|
456
|
+
const identity = {
|
|
457
|
+
runId: workspace.runId,
|
|
458
|
+
rootRunId: workspace.continuation.rootRunId,
|
|
459
|
+
repository: workspace.repository,
|
|
460
|
+
baseRef: workspace.baseRef,
|
|
461
|
+
headRef: workspace.headRef,
|
|
462
|
+
};
|
|
463
|
+
await Promise.allSettled([
|
|
464
|
+
this.options.github.upsertContinuationStatusComment({
|
|
465
|
+
...identity,
|
|
466
|
+
body: `🔄 ${runLabel(details?.agentName, workspace.runId)} is working on this PR...`,
|
|
467
|
+
}),
|
|
468
|
+
this.options.github.createContinuationCheckRun({
|
|
469
|
+
repository: workspace.repository,
|
|
470
|
+
headSha: workspace.baseCommit,
|
|
471
|
+
runId: workspace.runId,
|
|
472
|
+
}),
|
|
473
|
+
]);
|
|
474
|
+
}
|
|
475
|
+
catch {
|
|
476
|
+
// Best-effort observability only -- must never affect the real run.
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* Companion to notifyContinuationStarted -- finds and updates whatever
|
|
481
|
+
* that call created, never creates fresh state itself (see
|
|
482
|
+
* upsertContinuationStatusComment vs updateContinuationStatusComment,
|
|
483
|
+
* and createContinuationCheckRun vs completeContinuationCheckRun in
|
|
484
|
+
* github.ts). Safe to call more than once for the same run. Same
|
|
485
|
+
* never-throw contract as notifyContinuationStarted.
|
|
486
|
+
*/
|
|
487
|
+
async notifyContinuationFinished(workspace, outcome, details) {
|
|
488
|
+
if (!workspace.continuation)
|
|
489
|
+
return;
|
|
490
|
+
try {
|
|
491
|
+
const identity = {
|
|
492
|
+
runId: workspace.runId,
|
|
493
|
+
rootRunId: workspace.continuation.rootRunId,
|
|
494
|
+
repository: workspace.repository,
|
|
495
|
+
baseRef: workspace.baseRef,
|
|
496
|
+
headRef: workspace.headRef,
|
|
497
|
+
};
|
|
498
|
+
const label = runLabel(details?.agentName, workspace.runId);
|
|
499
|
+
const summarySuffix = details?.summary ? `\n\n${details.summary}` : "";
|
|
500
|
+
const body = outcome === "succeeded" ? `✅ ${label} finished.${summarySuffix}` : `❌ ${label} failed.${summarySuffix}`;
|
|
501
|
+
await Promise.allSettled([
|
|
502
|
+
this.options.github.updateContinuationStatusComment({ ...identity, body }),
|
|
503
|
+
this.options.github.completeContinuationCheckRun({
|
|
504
|
+
repository: workspace.repository,
|
|
505
|
+
headSha: workspace.baseCommit,
|
|
506
|
+
runId: workspace.runId,
|
|
507
|
+
outcome,
|
|
508
|
+
}),
|
|
509
|
+
]);
|
|
510
|
+
}
|
|
511
|
+
catch {
|
|
512
|
+
// Best-effort observability only -- must never affect the real run.
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
validateInput(input) {
|
|
516
|
+
if (!SAFE_RUN_ID.test(input.runId))
|
|
517
|
+
throw new Error("vcs_run_id_invalid");
|
|
518
|
+
const repository = normalizeGitHubRepository(input.repository);
|
|
519
|
+
const baseRef = normalizeGitRef(input.baseRef);
|
|
520
|
+
const headRef = normalizeGitRef(input.headRef);
|
|
521
|
+
// Revision-in-place: a continuation's headRef legitimately belongs to a
|
|
522
|
+
// DIFFERENT run (the one that originally opened the PR), so it can't
|
|
523
|
+
// match this run's own derived pattern -- it must instead match that
|
|
524
|
+
// run's. The caller (dispatch.ts) is responsible for having verified,
|
|
525
|
+
// against the database, that this run is actually allowed to continue
|
|
526
|
+
// that branch before ever setting `continuation`; this is a structural
|
|
527
|
+
// check only, not the security boundary.
|
|
528
|
+
let continuation;
|
|
529
|
+
if (input.continuation) {
|
|
530
|
+
if (!SAFE_RUN_ID.test(input.continuation.rootRunId))
|
|
531
|
+
throw new Error("vcs_root_run_id_invalid");
|
|
532
|
+
if (headRef !== `wardby/run-${input.continuation.rootRunId}`)
|
|
533
|
+
throw new Error("vcs_head_ref_invalid");
|
|
534
|
+
continuation = { rootRunId: input.continuation.rootRunId };
|
|
535
|
+
}
|
|
536
|
+
else if (headRef !== `wardby/run-${input.runId}`) {
|
|
537
|
+
throw new Error("vcs_head_ref_invalid");
|
|
538
|
+
}
|
|
539
|
+
const protectedPaths = [...new Set(input.protectedPaths.map(validateProtectedPath))];
|
|
540
|
+
if (protectedPaths.length === 0 || protectedPaths.length > 128)
|
|
541
|
+
throw new Error("vcs_protected_paths_invalid");
|
|
542
|
+
return { runId: input.runId, repository, baseRef, headRef, protectedPaths, continuation };
|
|
543
|
+
}
|
|
544
|
+
expectedPaths(runId) {
|
|
545
|
+
if (!SAFE_RUN_ID.test(runId))
|
|
546
|
+
throw new Error("vcs_workspace_handle_invalid");
|
|
547
|
+
const runRoot = resolve(this.rootDir, runId);
|
|
548
|
+
if (!directChild(this.rootDir, runRoot))
|
|
549
|
+
throw new Error("vcs_workspace_handle_invalid");
|
|
550
|
+
return {
|
|
551
|
+
runRoot,
|
|
552
|
+
workspacePath: resolve(runRoot, "workspace"),
|
|
553
|
+
gitMetadataPath: resolve(runRoot, "git"),
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
async validatePrepared(workspace) {
|
|
557
|
+
const normalized = this.validateInput(workspace);
|
|
558
|
+
const expected = this.expectedPaths(normalized.runId);
|
|
559
|
+
if (workspace.id !== `vcs-${normalized.runId}` ||
|
|
560
|
+
!SAFE_COMMIT_SHA.test(workspace.baseCommit) ||
|
|
561
|
+
workspace.repository !== normalized.repository ||
|
|
562
|
+
workspace.baseRef !== normalized.baseRef ||
|
|
563
|
+
workspace.headRef !== normalized.headRef ||
|
|
564
|
+
workspace.workspacePath !== expected.workspacePath ||
|
|
565
|
+
workspace.gitMetadataPath !== expected.gitMetadataPath ||
|
|
566
|
+
workspace.protectedPaths.length !== normalized.protectedPaths.length ||
|
|
567
|
+
workspace.protectedPaths.some((path, index) => path !== normalized.protectedPaths[index]) ||
|
|
568
|
+
workspace.continuation?.rootRunId !== normalized.continuation?.rootRunId) {
|
|
569
|
+
throw new Error("vcs_workspace_handle_invalid");
|
|
570
|
+
}
|
|
571
|
+
const [rootReal, runRootReal, workspaceReal, metadataReal] = await Promise.all([
|
|
572
|
+
realpath(this.rootDir),
|
|
573
|
+
realpath(expected.runRoot),
|
|
574
|
+
realpath(expected.workspacePath),
|
|
575
|
+
realpath(expected.gitMetadataPath),
|
|
576
|
+
]);
|
|
577
|
+
if (runRootReal !== resolve(rootReal, workspace.runId) ||
|
|
578
|
+
workspaceReal !== resolve(runRootReal, "workspace") ||
|
|
579
|
+
metadataReal !== resolve(runRootReal, "git")) {
|
|
580
|
+
throw new Error("vcs_workspace_path_invalid");
|
|
581
|
+
}
|
|
582
|
+
return { ...workspace, ...normalized };
|
|
583
|
+
}
|
|
584
|
+
async inspectWorkspace(workspacePath) {
|
|
585
|
+
const rootStat = await lstat(workspacePath);
|
|
586
|
+
if (!rootStat.isDirectory() || rootStat.isSymbolicLink())
|
|
587
|
+
throw new Error("vcs_workspace_path_invalid");
|
|
588
|
+
let entries = 0;
|
|
589
|
+
const visit = async (directory) => {
|
|
590
|
+
const handle = await opendir(directory);
|
|
591
|
+
for await (const entry of handle) {
|
|
592
|
+
entries += 1;
|
|
593
|
+
if (entries > MAX_WORKSPACE_ENTRIES)
|
|
594
|
+
throw new Error("vcs_workspace_entry_limit");
|
|
595
|
+
if (entry.name.toLowerCase() === ".git")
|
|
596
|
+
throw new Error("vcs_nested_repository");
|
|
597
|
+
const fullPath = resolve(directory, entry.name);
|
|
598
|
+
const stat = await lstat(fullPath);
|
|
599
|
+
if (stat.isSymbolicLink()) {
|
|
600
|
+
const target = await readlink(fullPath);
|
|
601
|
+
const resolvedTarget = resolve(directory, target);
|
|
602
|
+
if (isAbsolute(target) ||
|
|
603
|
+
(resolvedTarget !== workspacePath && !resolvedTarget.startsWith(`${workspacePath}${sep}`))) {
|
|
604
|
+
throw new Error("vcs_symlink_escape");
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
else if (stat.isDirectory()) {
|
|
608
|
+
await visit(fullPath);
|
|
609
|
+
}
|
|
610
|
+
else if (!stat.isFile()) {
|
|
611
|
+
throw new Error("vcs_special_file");
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
};
|
|
615
|
+
await visit(workspacePath);
|
|
616
|
+
}
|
|
617
|
+
async assertRemote(workspace) {
|
|
618
|
+
const expectedUrl = this.cloneUrlForRepository(workspace.repository);
|
|
619
|
+
const remotes = (await this.gitFor(workspace, ["remote"])).stdout.trim().split("\n").filter(Boolean);
|
|
620
|
+
const fetchUrls = (await this.gitFor(workspace, ["remote", "get-url", "--all", "origin"])).stdout
|
|
621
|
+
.trim()
|
|
622
|
+
.split("\n")
|
|
623
|
+
.filter(Boolean);
|
|
624
|
+
const pushUrls = (await this.gitFor(workspace, ["remote", "get-url", "--push", "--all", "origin"])).stdout
|
|
625
|
+
.trim()
|
|
626
|
+
.split("\n")
|
|
627
|
+
.filter(Boolean);
|
|
628
|
+
if (remotes.length !== 1 ||
|
|
629
|
+
remotes[0] !== "origin" ||
|
|
630
|
+
fetchUrls.length !== 1 ||
|
|
631
|
+
fetchUrls[0] !== expectedUrl ||
|
|
632
|
+
pushUrls.length !== 1 ||
|
|
633
|
+
pushUrls[0] !== expectedUrl) {
|
|
634
|
+
throw new Error("vcs_remote_invalid");
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
/**
|
|
638
|
+
* Deliberately NOT windowed, unlike every other redaction caller: here
|
|
639
|
+
* `redactTokenShapedValues` is a DETECTOR, not a formatter — nothing from the
|
|
640
|
+
* config is ever emitted, only the fixed `vcs_git_config_unsafe` — so
|
|
641
|
+
* redacting a prefix would narrow a security predicate over an
|
|
642
|
+
* attacker-controlled file (a cloned repo's `.git/config`) to buy speed. The
|
|
643
|
+
* input is already bounded: the size check short-circuits above it, so
|
|
644
|
+
* redaction never sees more than 64 KiB, and with every pattern linear the
|
|
645
|
+
* worst adversarial 64 KiB shape measures 22 ms (it was 1.5 s while the JWT
|
|
646
|
+
* pattern was quadratic — that bug, not the breadth of this scan, was the
|
|
647
|
+
* problem).
|
|
648
|
+
*/
|
|
649
|
+
async assertSafeLocalConfig(gitMetadataPath) {
|
|
650
|
+
const config = await readFile(resolve(gitMetadataPath, "config"), "utf8");
|
|
651
|
+
if (Buffer.byteLength(config, "utf8") > 64 * 1024 ||
|
|
652
|
+
/^\s*\[(?:credential|filter\b|http\b|url\b)/im.test(config) ||
|
|
653
|
+
/^\s*(?:extraheader|helper|insteadof|sshcommand|textconv|clean|smudge)\s*=/im.test(config) ||
|
|
654
|
+
redactTokenShapedValues(config) !== config) {
|
|
655
|
+
throw new Error("vcs_git_config_unsafe");
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
async pushOnce(workspace, commitSha) {
|
|
659
|
+
await this.options.github.withRepositoryToken(workspace.repository, async (token) => {
|
|
660
|
+
const remote = await this.remoteHead(workspace, token);
|
|
661
|
+
if (remote === commitSha)
|
|
662
|
+
return;
|
|
663
|
+
// A continuation's remote branch legitimately already sits at
|
|
664
|
+
// baseCommit (that's the tip we cloned and committed on top of) --
|
|
665
|
+
// that's the expected fast-forward pre-push state, not a conflict.
|
|
666
|
+
// Any OTHER non-null value means something else moved the branch
|
|
667
|
+
// since we cloned (a human push, a race), which is a real conflict.
|
|
668
|
+
if (remote && remote !== workspace.baseCommit)
|
|
669
|
+
throw new Error("vcs_head_ref_conflict");
|
|
670
|
+
try {
|
|
671
|
+
await this.gitFor(workspace, ["push", "origin", `${commitSha}:refs/heads/${workspace.headRef}`], {
|
|
672
|
+
authToken: token,
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
catch (error) {
|
|
676
|
+
if ((await this.remoteHead(workspace, token)) === commitSha)
|
|
677
|
+
return;
|
|
678
|
+
throw error;
|
|
679
|
+
}
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
async remoteHead(workspace, token) {
|
|
683
|
+
const result = await this.gitFor(workspace, ["ls-remote", "--heads", "origin", `refs/heads/${workspace.headRef}`], {
|
|
684
|
+
authToken: token,
|
|
685
|
+
});
|
|
686
|
+
const line = result.stdout.trim();
|
|
687
|
+
if (!line)
|
|
688
|
+
return null;
|
|
689
|
+
const [sha, ref, ...rest] = line.split(/\s+/);
|
|
690
|
+
if (rest.length > 0 || !SAFE_COMMIT_SHA.test(sha) || ref !== `refs/heads/${workspace.headRef}`) {
|
|
691
|
+
throw new Error("vcs_remote_response_invalid");
|
|
692
|
+
}
|
|
693
|
+
return sha;
|
|
694
|
+
}
|
|
695
|
+
async revParse(workspace, revision) {
|
|
696
|
+
return this.revParseRaw(workspace.gitMetadataPath, workspace.workspacePath, revision);
|
|
697
|
+
}
|
|
698
|
+
async revParseRaw(gitMetadataPath, workspacePath, revision) {
|
|
699
|
+
const sha = (await this.gitForPaths(gitMetadataPath, workspacePath, ["rev-parse", "--verify", revision])).stdout.trim();
|
|
700
|
+
if (!SAFE_COMMIT_SHA.test(sha))
|
|
701
|
+
throw new Error("vcs_commit_sha_invalid");
|
|
702
|
+
return sha;
|
|
703
|
+
}
|
|
704
|
+
gitFor(workspace, args, options = {}) {
|
|
705
|
+
return this.gitForPaths(workspace.gitMetadataPath, workspace.workspacePath, args, options);
|
|
706
|
+
}
|
|
707
|
+
gitForPaths(gitMetadataPath, workspacePath, args, options = {}) {
|
|
708
|
+
return this.git.run([`--git-dir=${gitMetadataPath}`, `--work-tree=${workspacePath}`, ...HARDENED_GIT_CONFIG, ...args], { ...options, cwd: workspacePath });
|
|
709
|
+
}
|
|
710
|
+
}
|