@wardby/cli 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +34 -4
- package/README.md +41 -4
- package/dist/claude-coding-worker/driver.d.ts +6 -1
- package/dist/claude-coding-worker/driver.js +27 -1
- package/dist/claude-coding-worker/main.js +2 -0
- package/dist/claude-coding-worker/tool-socket.d.ts +9 -0
- package/dist/claude-coding-worker/tool-socket.js +26 -0
- package/dist/cli-help.d.ts +1 -1
- package/dist/cli-help.js +13 -4
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +219 -29
- package/dist/coding/collect-exclude.d.ts +25 -0
- package/dist/coding/collect-exclude.js +76 -0
- package/dist/coding/profile.d.ts +67 -22
- package/dist/coding/profile.js +58 -25
- package/dist/coding/protected-path-wording.d.ts +24 -0
- package/dist/coding/protected-path-wording.js +55 -0
- package/dist/coding/protected-paths.d.ts +31 -0
- package/dist/coding/protected-paths.js +48 -0
- package/dist/coding/protocol.d.ts +75 -4
- package/dist/coding/protocol.js +107 -7
- package/dist/coding/registry/adapters.d.ts +2 -0
- package/dist/coding/registry/adapters.js +9 -0
- package/dist/coding/registry/allowlist.d.ts +9 -0
- package/dist/coding/registry/allowlist.js +28 -0
- package/dist/coding/registry/json-scan.d.ts +55 -0
- package/dist/coding/registry/json-scan.js +191 -0
- package/dist/coding/registry/lockfiles.d.ts +6 -0
- package/dist/coding/registry/lockfiles.js +89 -0
- package/dist/coding/registry/npm-lockfile.d.ts +9 -0
- package/dist/coding/registry/npm-lockfile.js +73 -0
- package/dist/coding/registry/npm-plan.d.ts +29 -0
- package/dist/coding/registry/npm-plan.js +289 -0
- package/dist/coding/registry/npm.d.ts +7 -0
- package/dist/coding/registry/npm.js +252 -0
- package/dist/coding/registry/pypi.d.ts +4 -0
- package/dist/coding/registry/pypi.js +210 -0
- package/dist/coding/registry/report.d.ts +37 -0
- package/dist/coding/registry/report.js +31 -0
- package/dist/coding/registry/token.d.ts +4 -0
- package/dist/coding/registry/token.js +7 -0
- package/dist/coding/registry/types.d.ts +286 -0
- package/dist/coding/registry/types.js +19 -0
- package/dist/coding/registry/worker-config.d.ts +14 -0
- package/dist/coding/registry/worker-config.js +31 -0
- package/dist/coding/services/builtins.d.ts +22 -0
- package/dist/coding/services/builtins.js +85 -0
- package/dist/coding/services/catalog.d.ts +443 -0
- package/dist/coding/services/catalog.js +159 -0
- package/dist/coding/services/declaration.d.ts +11 -0
- package/dist/coding/services/declaration.js +114 -0
- package/dist/coding/services/note.d.ts +8 -0
- package/dist/coding/services/note.js +15 -0
- package/dist/coding/services/resolve.d.ts +25 -0
- package/dist/coding/services/resolve.js +50 -0
- package/dist/coding/services/wording.d.ts +25 -0
- package/dist/coding/services/wording.js +70 -0
- package/dist/coding-proxy/main.js +24 -4
- package/dist/coding-worker/artifact.d.ts +6 -0
- package/dist/coding-worker/debug-trace.d.ts +37 -0
- package/dist/coding-worker/debug-trace.js +117 -0
- package/dist/coding-worker/driver.d.ts +13 -1
- package/dist/coding-worker/driver.js +102 -13
- package/dist/coding-worker/errors.js +8 -0
- package/dist/coding-worker/main.js +10 -2
- package/dist/coding-worker/sdk.d.ts +2 -2
- package/dist/coding-worker/sdk.js +7 -2
- package/dist/coding-worker/types.d.ts +3 -0
- package/dist/config/providers.d.ts +41 -0
- package/dist/config/providers.js +66 -0
- package/dist/core/budget-groups.d.ts +108 -5
- package/dist/core/budget-groups.js +125 -19
- package/dist/core/budget-wording.d.ts +22 -0
- package/dist/core/budget-wording.js +55 -0
- package/dist/core/datastores.js +18 -2
- package/dist/core/db.d.ts +5 -1
- package/dist/core/db.js +8 -2
- package/dist/core/dispatch.d.ts +33 -6
- package/dist/core/dispatch.js +260 -43
- package/dist/core/engine-native.js +27 -10
- package/dist/core/grants.d.ts +86 -0
- package/dist/core/grants.js +126 -0
- package/dist/core/host-events.d.ts +49 -0
- package/dist/core/host-events.js +257 -0
- package/dist/core/host-identity-links.d.ts +54 -0
- package/dist/core/host-identity-links.js +189 -0
- package/dist/core/host-status.d.ts +61 -0
- package/dist/core/host-status.js +211 -0
- package/dist/core/in-flight-runs.d.ts +8 -0
- package/dist/core/in-flight-runs.js +56 -0
- package/dist/core/provider-wording.d.ts +12 -0
- package/dist/core/provider-wording.js +44 -0
- package/dist/core/reconciler.d.ts +38 -2
- package/dist/core/reconciler.js +86 -2
- package/dist/core/repo-access.d.ts +99 -0
- package/dist/core/repo-access.js +136 -0
- package/dist/core/review-host-checks.d.ts +11 -0
- package/dist/core/review-host-checks.js +41 -0
- package/dist/core/review-host-tools.d.ts +47 -0
- package/dist/core/review-host-tools.js +347 -0
- package/dist/core/run-heartbeat.d.ts +27 -0
- package/dist/core/run-heartbeat.js +54 -0
- package/dist/core/runner.d.ts +30 -8
- package/dist/core/runner.js +237 -40
- package/dist/core/secrets.d.ts +11 -2
- package/dist/core/secrets.js +25 -6
- package/dist/core/subagent-memory-tools.d.ts +1 -1
- package/dist/core/subagent-memory-tools.js +16 -3
- package/dist/core/tool-admin.d.ts +81 -0
- package/dist/core/tool-admin.js +129 -0
- package/dist/core/tool-names.d.ts +42 -0
- package/dist/core/tool-names.js +64 -0
- package/dist/core/untrusted-content.d.ts +32 -0
- package/dist/core/untrusted-content.js +72 -0
- package/dist/core/webhooks.d.ts +8 -1
- package/dist/core/webhooks.js +23 -2
- package/dist/generated/prisma/browser.d.ts +100 -0
- package/dist/generated/prisma/client.d.ts +100 -0
- package/dist/generated/prisma/commonInputTypes.d.ts +30 -0
- package/dist/generated/prisma/enums.d.ts +6 -0
- package/dist/generated/prisma/enums.js +6 -1
- package/dist/generated/prisma/internal/class.d.ts +143 -0
- package/dist/generated/prisma/internal/class.js +4 -4
- package/dist/generated/prisma/internal/prismaNamespace.d.ts +1746 -577
- package/dist/generated/prisma/internal/prismaNamespace.js +179 -6
- package/dist/generated/prisma/internal/prismaNamespaceBrowser.d.ts +186 -0
- package/dist/generated/prisma/internal/prismaNamespaceBrowser.js +179 -6
- package/dist/generated/prisma/models/Agent.d.ts +294 -1
- package/dist/generated/prisma/models/AgentRepository.d.ts +1425 -0
- package/dist/generated/prisma/models/AgentRepository.js +1 -0
- package/dist/generated/prisma/models/AgentTool.d.ts +95 -1
- package/dist/generated/prisma/models/AuthUser.d.ts +56 -1
- package/dist/generated/prisma/models/CodingAgentProfile.d.ts +258 -7
- package/dist/generated/prisma/models/CodingProxySession.d.ts +123 -2
- package/dist/generated/prisma/models/CodingRun.d.ts +1276 -95
- package/dist/generated/prisma/models/CodingService.d.ts +1348 -0
- package/dist/generated/prisma/models/CodingService.js +1 -0
- package/dist/generated/prisma/models/HostEventDelivery.d.ts +946 -0
- package/dist/generated/prisma/models/HostEventDelivery.js +1 -0
- package/dist/generated/prisma/models/HostIdentity.d.ts +1232 -0
- package/dist/generated/prisma/models/HostIdentity.js +1 -0
- package/dist/generated/prisma/models/HostIdentityLinkRequest.d.ts +1473 -0
- package/dist/generated/prisma/models/HostIdentityLinkRequest.js +1 -0
- package/dist/generated/prisma/models/Principal.d.ts +455 -0
- package/dist/generated/prisma/models/RegistryAllowance.d.ts +1148 -0
- package/dist/generated/prisma/models/RegistryAllowance.js +1 -0
- package/dist/generated/prisma/models/RegistryApprovedVersion.d.ts +1219 -0
- package/dist/generated/prisma/models/RegistryApprovedVersion.js +1 -0
- package/dist/generated/prisma/models/RegistryFetch.d.ts +1428 -0
- package/dist/generated/prisma/models/RegistryFetch.js +1 -0
- package/dist/generated/prisma/models/RegistryPlanRefusal.d.ts +1294 -0
- package/dist/generated/prisma/models/RegistryPlanRefusal.js +1 -0
- package/dist/generated/prisma/models/RegistryVersionFact.d.ts +1085 -0
- package/dist/generated/prisma/models/RegistryVersionFact.js +1 -0
- package/dist/generated/prisma/models/ResourceGrant.d.ts +1437 -0
- package/dist/generated/prisma/models/ResourceGrant.js +1 -0
- package/dist/generated/prisma/models/Run.d.ts +389 -1
- package/dist/generated/prisma/models/RunHostCheck.d.ts +1239 -0
- package/dist/generated/prisma/models/RunHostCheck.js +1 -0
- package/dist/generated/prisma/models/RunHostStatus.d.ts +1315 -0
- package/dist/generated/prisma/models/RunHostStatus.js +1 -0
- package/dist/generated/prisma/models/Tool.d.ts +15 -3
- package/dist/generated/prisma/models.d.ts +13 -0
- package/dist/help/build.d.ts +1 -0
- package/dist/help/build.js +9 -0
- package/dist/help/catalog.d.ts +24 -0
- package/dist/help/catalog.js +160 -0
- package/dist/help/cli.d.ts +2 -0
- package/dist/help/cli.js +65 -0
- package/dist/help/runtime.d.ts +3 -0
- package/dist/help/runtime.js +44 -0
- package/dist/help/search.d.ts +9 -0
- package/dist/help/search.js +104 -0
- package/dist/help-index.json +660 -0
- package/dist/import/cli-args.js +3 -2
- package/dist/import/create.d.ts +5 -0
- package/dist/import/create.js +67 -13
- package/dist/import/index.js +19 -9
- package/dist/import/neutral-schema.d.ts +11 -11
- package/dist/mcp/auth/access.d.ts +70 -0
- package/dist/mcp/auth/access.js +58 -0
- package/dist/mcp/auth/grants-cli.d.ts +149 -0
- package/dist/mcp/auth/grants-cli.js +518 -0
- package/dist/mcp/auth/host-account-cli.d.ts +2 -0
- package/dist/mcp/auth/host-account-cli.js +47 -0
- package/dist/mcp/auth/ownership.d.ts +26 -52
- package/dist/mcp/auth/ownership.js +19 -14
- package/dist/mcp/auth/repo-authorization.d.ts +22 -0
- package/dist/mcp/auth/repo-authorization.js +51 -0
- package/dist/mcp/auth/resource-server.d.ts +31 -2
- package/dist/mcp/auth/resource-server.js +84 -3
- package/dist/mcp/auth/self-hosted/browser.js +2 -2
- package/dist/mcp/auth/self-hosted/cli.js +47 -13
- package/dist/mcp/auth/self-hosted/credentials.d.ts +20 -2
- package/dist/mcp/auth/self-hosted/credentials.js +62 -4
- package/dist/mcp/auth/self-hosted/session.d.ts +2 -1
- package/dist/mcp/context.d.ts +27 -1
- package/dist/mcp/errors.d.ts +25 -6
- package/dist/mcp/errors.js +98 -0
- package/dist/mcp/host-events/github-ingress.d.ts +36 -0
- package/dist/mcp/host-events/github-ingress.js +92 -0
- package/dist/mcp/host-events/github-user-callback.d.ts +18 -0
- package/dist/mcp/host-events/github-user-callback.js +50 -0
- package/dist/mcp/index.d.ts +8 -1
- package/dist/mcp/index.js +106 -16
- package/dist/mcp/server.js +27 -9
- package/dist/mcp/tools/agents.js +446 -48
- package/dist/mcp/tools/budget-groups.js +3 -3
- package/dist/mcp/tools/datastore.js +22 -15
- package/dist/mcp/tools/grants.d.ts +2 -0
- package/dist/mcp/tools/grants.js +239 -0
- package/dist/mcp/tools/help.d.ts +5 -0
- package/dist/mcp/tools/help.js +67 -0
- package/dist/mcp/tools/host-accounts.d.ts +2 -0
- package/dist/mcp/tools/host-accounts.js +114 -0
- package/dist/mcp/tools/memory.d.ts +7 -1
- package/dist/mcp/tools/memory.js +5 -5
- package/dist/mcp/tools/repositories.d.ts +2 -0
- package/dist/mcp/tools/repositories.js +181 -0
- package/dist/mcp/tools/runs.d.ts +6 -0
- package/dist/mcp/tools/runs.js +60 -6
- package/dist/mcp/tools/scheduling.js +3 -6
- package/dist/mcp/tools/secrets.js +18 -6
- package/dist/mcp/tools/services.d.ts +2 -0
- package/dist/mcp/tools/services.js +222 -0
- package/dist/mcp/tools/subagents.js +52 -16
- package/dist/mcp/tools/tools.d.ts +41 -0
- package/dist/mcp/tools/tools.js +201 -40
- package/dist/mcp/tools/trigger.js +36 -6
- package/dist/mcp/tools/webhooks.js +15 -4
- package/dist/mcp/transport/streamable-http.d.ts +10 -0
- package/dist/mcp/transport/streamable-http.js +32 -2
- package/dist/providers/auth/delegating.d.ts +19 -0
- package/dist/providers/auth/delegating.js +71 -0
- package/dist/providers/auth/index.d.ts +2 -0
- package/dist/providers/auth/index.js +11 -0
- package/dist/providers/auth/self-hosted.d.ts +2 -1
- package/dist/providers/auth/self-hosted.js +16 -4
- package/dist/providers/auth/types.d.ts +6 -0
- package/dist/providers/coding-proxy/memory-ledger.d.ts +3 -0
- package/dist/providers/coding-proxy/memory-ledger.js +21 -2
- package/dist/providers/coding-proxy/metering.d.ts +4 -0
- package/dist/providers/coding-proxy/metering.js +28 -2
- package/dist/providers/coding-proxy/prisma-ledger.d.ts +17 -0
- package/dist/providers/coding-proxy/prisma-ledger.js +50 -5
- package/dist/providers/coding-proxy/proxy.d.ts +13 -0
- package/dist/providers/coding-proxy/proxy.js +479 -18
- package/dist/providers/coding-proxy/registry/audit.d.ts +131 -0
- package/dist/providers/coding-proxy/registry/audit.js +380 -0
- package/dist/providers/coding-proxy/registry/bounded-fetch.d.ts +16 -0
- package/dist/providers/coding-proxy/registry/bounded-fetch.js +77 -0
- package/dist/providers/coding-proxy/registry/plan.d.ts +58 -0
- package/dist/providers/coding-proxy/registry/plan.js +304 -0
- package/dist/providers/coding-proxy/registry/prisma-store.d.ts +34 -0
- package/dist/providers/coding-proxy/registry/prisma-store.js +151 -0
- package/dist/providers/coding-proxy/registry/service.d.ts +213 -0
- package/dist/providers/coding-proxy/registry/service.js +1137 -0
- package/dist/providers/coding-proxy/registry/store.d.ts +127 -0
- package/dist/providers/coding-proxy/registry/store.js +70 -0
- package/dist/providers/coding-proxy/runtime.d.ts +17 -0
- package/dist/providers/coding-proxy/runtime.js +63 -0
- package/dist/providers/coding-proxy/secure-fetch.js +0 -1
- package/dist/providers/coding-proxy/server.d.ts +11 -0
- package/dist/providers/coding-proxy/server.js +163 -0
- package/dist/providers/coding-proxy/types.d.ts +15 -0
- package/dist/providers/engine/types.d.ts +10 -0
- package/dist/providers/executor/build.d.ts +2 -2
- package/dist/providers/executor/composition.d.ts +3 -0
- package/dist/providers/executor/composition.js +19 -0
- package/dist/providers/executor/container.d.ts +105 -3
- package/dist/providers/executor/container.js +296 -20
- package/dist/providers/executor/dbos.d.ts +2 -3
- package/dist/providers/executor/in-process.d.ts +2 -3
- package/dist/providers/executor/routing.d.ts +7 -0
- package/dist/providers/executor/routing.js +13 -0
- package/dist/providers/executor/types.d.ts +22 -0
- package/dist/providers/jobs/claude-tool-setup.d.ts +23 -0
- package/dist/providers/jobs/claude-tool-setup.js +50 -0
- package/dist/providers/jobs/collect-prune.d.ts +7 -0
- package/dist/providers/jobs/collect-prune.js +27 -0
- package/dist/providers/jobs/docker-isolation.d.ts +48 -3
- package/dist/providers/jobs/docker-isolation.js +213 -22
- package/dist/providers/jobs/docker-services.d.ts +35 -0
- package/dist/providers/jobs/docker-services.js +191 -0
- package/dist/providers/jobs/docker.d.ts +49 -2
- package/dist/providers/jobs/docker.js +279 -32
- package/dist/providers/jobs/fake-kubernetes-api.d.ts +1 -0
- package/dist/providers/jobs/fake-kubernetes-api.js +12 -3
- package/dist/providers/jobs/kubernetes-isolation.d.ts +17 -2
- package/dist/providers/jobs/kubernetes-isolation.js +238 -57
- package/dist/providers/jobs/kubernetes-platform.d.ts +10 -4
- package/dist/providers/jobs/kubernetes-platform.js +11 -5
- package/dist/providers/jobs/kubernetes-preflight.js +3 -0
- package/dist/providers/jobs/kubernetes.d.ts +21 -2
- package/dist/providers/jobs/kubernetes.js +112 -21
- package/dist/providers/jobs/types.d.ts +18 -0
- package/dist/providers/llm/anthropic.js +6 -2
- package/dist/providers/llm/bedrock.js +2 -1
- package/dist/providers/llm/claude-messages.d.ts +5 -1
- package/dist/providers/llm/claude-messages.js +1 -0
- package/dist/providers/llm/claude-provider.d.ts +3 -1
- package/dist/providers/llm/claude-provider.js +5 -1
- package/dist/providers/llm/index.d.ts +1 -1
- package/dist/providers/llm/index.js +1 -1
- package/dist/providers/llm/pricing-anthropic.d.ts +10 -0
- package/dist/providers/llm/pricing-anthropic.js +32 -14
- package/dist/providers/llm/pricing-bedrock-claude.d.ts +8 -0
- package/dist/providers/llm/pricing-bedrock-claude.js +9 -0
- package/dist/providers/llm/routing.d.ts +10 -1
- package/dist/providers/llm/routing.js +18 -0
- package/dist/providers/llm/types.d.ts +12 -0
- package/dist/providers/llm/types.js +8 -1
- package/dist/providers/review-host/diff-lines.d.ts +16 -0
- package/dist/providers/review-host/diff-lines.js +59 -0
- package/dist/providers/review-host/github-events.d.ts +6 -0
- package/dist/providers/review-host/github-events.js +180 -0
- package/dist/providers/review-host/github-user-auth.d.ts +38 -0
- package/dist/providers/review-host/github-user-auth.js +128 -0
- package/dist/providers/review-host/github.d.ts +57 -0
- package/dist/providers/review-host/github.js +567 -0
- package/dist/providers/review-host/index.d.ts +12 -0
- package/dist/providers/review-host/index.js +30 -0
- package/dist/providers/review-host/review-format.d.ts +19 -0
- package/dist/providers/review-host/review-format.js +59 -0
- package/dist/providers/review-host/types.d.ts +284 -0
- package/dist/providers/review-host/types.js +24 -0
- package/dist/providers/vcs/git.d.ts +16 -6
- package/dist/providers/vcs/git.js +77 -13
- package/dist/providers/vcs/github.d.ts +74 -3
- package/dist/providers/vcs/github.js +195 -15
- package/dist/providers/vcs/types.d.ts +42 -4
- package/dist/quickstart/index.js +13 -2
- package/dist/sandbox/fetch-policy.d.ts +20 -2
- package/dist/sandbox/fetch-policy.js +64 -3
- package/dist/sandbox/host-functions.d.ts +9 -1
- package/dist/sandbox/host-functions.js +12 -5
- package/dist/serve.js +1 -1
- package/dist/wardby-bin.js +6 -0
- package/docs/README.md +30 -0
- package/docs/architecture-runtime.md +90 -0
- package/docs/assets/brand/wardby-icon-512.png +0 -0
- package/docs/assets/brand/wardby-icon.svg +16 -0
- package/docs/assets/brand/wardby-mascot-profile-512.png +0 -0
- package/docs/assets/brand/wardby-mascot.png +0 -0
- package/docs/assets/brand/wardby-mascot.svg +5 -0
- package/docs/assets/wardby-workflow.svg +106 -0
- package/docs/code-review-agents.md +481 -0
- package/docs/coding-agent-setup.md +172 -0
- package/docs/coding-packages.md +455 -0
- package/docs/coding-services.md +300 -0
- package/docs/coding-worker-byo-images.md +98 -0
- package/docs/coding-worker-isolation.md +1061 -0
- package/docs/getting-started-gke.md +615 -0
- package/docs/getting-started-identity-provider.md +308 -0
- package/docs/getting-started.md +133 -0
- package/docs/observability.md +53 -0
- package/docs/release-verification.md +66 -0
- package/docs/security-deployment.md +657 -0
- package/help/code-review-agents.md +31 -0
- package/help/coding-packages.md +31 -0
- package/help/coding-services.md +71 -0
- package/help/creating-agents.md +68 -0
- package/help/deploy-gke.md +39 -0
- package/help/deployment-targets.md +39 -0
- package/help/errors/budget-group-exhausted.md +25 -0
- package/help/errors/docker-isolation-unsupported.md +26 -0
- package/help/errors/protected-path.md +45 -0
- package/help/errors/repo-access.md +27 -0
- package/help/errors/service-declaration-invalid.md +27 -0
- package/help/errors/service-declaration-unavailable.md +25 -0
- package/help/errors/service-launcher-unsupported.md +30 -0
- package/help/errors/service-not-allowed.md +27 -0
- package/help/errors/service-unknown.md +23 -0
- package/help/errors/service-unready.md +49 -0
- package/help/getting-started.md +31 -0
- package/help/github.md +30 -0
- package/help/identity-and-access.md +39 -0
- package/help/mcp.md +30 -0
- package/help/native-capabilities.md +30 -0
- package/help/observability.md +41 -0
- package/help/operating-agents.md +30 -0
- package/help/security.md +27 -0
- package/help/troubleshooting/budgets.md +26 -0
- package/help/troubleshooting/coding-workers.md +47 -0
- package/help/troubleshooting/repository-access.md +27 -0
- package/package.json +12 -3
- package/prisma/migrations/20260925010000_coding_collect_exclude/migration.sql +8 -0
- package/prisma/migrations/20260925015000_allowed_egress_default/migration.sql +8 -0
- package/prisma/migrations/20260925020000_coding_package_registry/migration.sql +55 -0
- package/prisma/migrations/20260925030000_tool_name_per_owner/migration.sql +11 -0
- package/prisma/migrations/20260926010000_run_trigger_host_event/migration.sql +7 -0
- package/prisma/migrations/20260926020000_code_review_hosts/migration.sql +53 -0
- package/prisma/migrations/20260926030000_auth_user_roles/migration.sql +9 -0
- package/prisma/migrations/20260926040000_agent_effort/migration.sql +6 -0
- package/prisma/migrations/20260926050000_registry_lockfile_plan/migration.sql +32 -0
- package/prisma/migrations/20260926050000_repo_access_authorization/migration.sql +82 -0
- package/prisma/migrations/20260926060000_registry_plan_refusal/migration.sql +19 -0
- package/prisma/migrations/20260926100000_registry_plan_refusal_published_at/migration.sql +5 -0
- package/prisma/migrations/20260926190000_run_host_status/migration.sql +18 -0
- package/prisma/migrations/20260926210000_run_host_status_at_dispatch/migration.sql +5 -0
- package/prisma/migrations/20260927010000_resource_grants/migration.sql +72 -0
- package/prisma/migrations/20260927020000_proxy_session_budget_exhausted/migration.sql +3 -0
- package/prisma/migrations/20260927030000_coding_debug_trace/migration.sql +4 -0
- package/prisma/migrations/20260927040000_proxy_session_upstream_failure/migration.sql +3 -0
- package/prisma/migrations/20260927050000_coding_run_services/migration.sql +74 -0
- package/prisma/migrations/20260928000000_coding_run_tool_image/migration.sql +5 -0
- package/prisma/schema.prisma +407 -21
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `wardby grants migration-report [--json] | adopt-public --owner <subject>
|
|
3
|
+
* [--dry-run] | prune-bindings [--dry-run]`: the operator's side of the
|
|
4
|
+
* move from "owner-less = public" to explicit grants (resource-sharing
|
|
5
|
+
* grants spec §3.7). Direct database access, like `auth host-account`.
|
|
6
|
+
*
|
|
7
|
+
* - migration-report is read-only and runs before AND after `migrate
|
|
8
|
+
* deploy`: it uses raw SQL over columns that exist in both, and predicts
|
|
9
|
+
* what the grants migration will do when its table isn't there yet.
|
|
10
|
+
* - adopt-public gives every owner-less agent an owner (one Serializable
|
|
11
|
+
* transaction per agent), keeping its grants (but lowering an everyone
|
|
12
|
+
* grant to read unless --keep-everyone-execute: the agent is about to
|
|
13
|
+
* regain the adopter's secrets and capabilities), keeping and reviving the new
|
|
14
|
+
* owner's own bindings, deleting the rest, and stamping the new owner's
|
|
15
|
+
* consent on attachments of its own tools. Other attachments stay inert
|
|
16
|
+
* and are printed with the attach_tool call that re-grants them.
|
|
17
|
+
* - prune-bindings deletes the secret/datastore bindings on owned agents
|
|
18
|
+
* whose resource another principal owns (inert at run time since the
|
|
19
|
+
* grants release) and prints each one: the "removed and listed" record.
|
|
20
|
+
*/
|
|
21
|
+
import { parseArgs } from "node:util";
|
|
22
|
+
import { Prisma } from "#prisma";
|
|
23
|
+
import { canDelegate } from "../../core/grants.js";
|
|
24
|
+
import { resolvePrincipal } from "./principal.js";
|
|
25
|
+
const USAGE = "Use grants migration-report [--json] | grants adopt-public --owner <subject> [--dry-run] [--keep-everyone-execute] | grants prune-bindings [--dry-run].";
|
|
26
|
+
/** The attach_tool call (MCP) that re-grants an attachment's capabilities, for its owner to review and run. */
|
|
27
|
+
function regrantCall(agentId, toolId, caps) {
|
|
28
|
+
return `attach_tool ${JSON.stringify({
|
|
29
|
+
agentId,
|
|
30
|
+
toolId,
|
|
31
|
+
allowedSecrets: caps.allowedSecrets,
|
|
32
|
+
allowedDatastorePrefixes: caps.allowedDatastorePrefixes,
|
|
33
|
+
allowedHosts: caps.allowedHosts,
|
|
34
|
+
allowedSharedDatastorePrefixes: caps.allowedSharedDatastorePrefixes,
|
|
35
|
+
})}`;
|
|
36
|
+
}
|
|
37
|
+
function hasCapabilities(caps) {
|
|
38
|
+
const nonEmpty = (v) => Array.isArray(v) ? v.length > 0 : v !== null && typeof v === "object" && Object.keys(v).length > 0;
|
|
39
|
+
return (nonEmpty(caps.allowedSecrets) ||
|
|
40
|
+
nonEmpty(caps.allowedDatastorePrefixes) ||
|
|
41
|
+
nonEmpty(caps.allowedHosts) ||
|
|
42
|
+
nonEmpty(caps.allowedSharedDatastorePrefixes));
|
|
43
|
+
}
|
|
44
|
+
export const BEHAVIOUR_CHANGES = [
|
|
45
|
+
"Other principals can no longer edit a formerly public agent: update_agent, set/disable_schedule, delete_agent, attach/detach_tool, attach/detach_subagent, attach/detach_secret, attach/detach_datastore, datastore_set/delete, set/delete_agent_memory, create_webhook, unlink_repository.",
|
|
46
|
+
"They can no longer read its data: get/list_agent_memory, datastore_get/list, or other principals' runs.",
|
|
47
|
+
"They keep list_agents, get_agent (other owners' tool code hidden), list_subagents, list_repositories, list_tools, trigger_agent and the MCP prompts (everyone holds execute until adoption; adopt-public lowers it to read unless --keep-everyone-execute).",
|
|
48
|
+
"Until adopt-public runs, a formerly public agent runs with no owned secrets or datastores and without its tools' capabilities.",
|
|
49
|
+
"make_owner no longer accepts ownerId: null; non-owner coding task/baseRef needs the owner's allowWebhookTaskOverride.",
|
|
50
|
+
];
|
|
51
|
+
export async function migrationReport(db) {
|
|
52
|
+
const [{ present }] = await db.$queryRaw `
|
|
53
|
+
SELECT to_regclass('"ResourceGrant"') IS NOT NULL AS "present"`;
|
|
54
|
+
const ownerless = present
|
|
55
|
+
? await db.$queryRaw `
|
|
56
|
+
SELECT a."id" AS "agentId", a."name",
|
|
57
|
+
(SELECT g."level" FROM "ResourceGrant" g
|
|
58
|
+
WHERE g."resourceType" = 'agent' AND g."resourceId" = a."id" AND g."granteeKey" = 'everyone') AS "everyoneGrant"
|
|
59
|
+
FROM "Agent" a WHERE a."ownerId" IS NULL ORDER BY a."name"`
|
|
60
|
+
: (await db.$queryRaw `
|
|
61
|
+
SELECT a."id" AS "agentId", a."name" FROM "Agent" a WHERE a."ownerId" IS NULL ORDER BY a."name"`).map((row) => ({ ...row, everyoneGrant: "execute (pending migration)" }));
|
|
62
|
+
const revivedSecrets = await db.$queryRaw `
|
|
63
|
+
SELECT a."id" AS "agentId", p."id" AS "principalId", p."subject", x."boundName" AS "label"
|
|
64
|
+
FROM "AgentSecret" x JOIN "Secret" s ON s."id" = x."secretId" JOIN "Agent" a ON a."id" = x."agentId"
|
|
65
|
+
JOIN "Principal" p ON p."id" = s."ownerId"
|
|
66
|
+
WHERE a."ownerId" IS NULL ORDER BY x."boundName"`;
|
|
67
|
+
const revivedDatastores = await db.$queryRaw `
|
|
68
|
+
SELECT a."id" AS "agentId", p."id" AS "principalId", p."subject", x."boundName" AS "label"
|
|
69
|
+
FROM "AgentDatastore" x JOIN "Datastore" d ON d."id" = x."datastoreId" JOIN "Agent" a ON a."id" = x."agentId"
|
|
70
|
+
JOIN "Principal" p ON p."id" = d."ownerId"
|
|
71
|
+
WHERE a."ownerId" IS NULL ORDER BY x."boundName"`;
|
|
72
|
+
const revivedTools = await db.$queryRaw `
|
|
73
|
+
SELECT a."id" AS "agentId", p."id" AS "principalId", p."subject", t."name" AS "label",
|
|
74
|
+
x."allowedSecrets", x."allowedDatastorePrefixes", x."allowedHosts", x."allowedSharedDatastorePrefixes"
|
|
75
|
+
FROM "AgentTool" x JOIN "Tool" t ON t."id" = x."toolId" JOIN "Agent" a ON a."id" = x."agentId"
|
|
76
|
+
JOIN "Principal" p ON p."id" = t."ownerId"
|
|
77
|
+
WHERE a."ownerId" IS NULL ORDER BY t."name"`;
|
|
78
|
+
const revivedFor = (agentId) => {
|
|
79
|
+
const byPrincipal = new Map();
|
|
80
|
+
const entry = (row) => {
|
|
81
|
+
let e = byPrincipal.get(row.principalId);
|
|
82
|
+
if (!e) {
|
|
83
|
+
e = { principalId: row.principalId, subject: row.subject, secrets: [], datastores: [], toolCapabilities: [] };
|
|
84
|
+
byPrincipal.set(row.principalId, e);
|
|
85
|
+
}
|
|
86
|
+
return e;
|
|
87
|
+
};
|
|
88
|
+
for (const r of revivedSecrets)
|
|
89
|
+
if (r.agentId === agentId)
|
|
90
|
+
entry(r).secrets.push(r.label);
|
|
91
|
+
for (const r of revivedDatastores)
|
|
92
|
+
if (r.agentId === agentId)
|
|
93
|
+
entry(r).datastores.push(r.label);
|
|
94
|
+
for (const r of revivedTools)
|
|
95
|
+
if (r.agentId === agentId && hasCapabilities(r))
|
|
96
|
+
entry(r).toolCapabilities.push(r.label);
|
|
97
|
+
return [...byPrincipal.values()].sort((a, b) => a.subject.localeCompare(b.subject));
|
|
98
|
+
};
|
|
99
|
+
// A binding resolves only while the resource's owner is the agent's owner,
|
|
100
|
+
// both non-null (core/secrets.ts, core/datastores.ts).
|
|
101
|
+
const bindings = await db.$queryRaw `
|
|
102
|
+
SELECT 'secret' AS "kind", a."id" AS "agentId", a."name" AS "agentName", a."ownerId" AS "agentOwnerId",
|
|
103
|
+
x."boundName", s."id" AS "resourceId", s."ownerId" AS "resourceOwnerId"
|
|
104
|
+
FROM "AgentSecret" x JOIN "Secret" s ON s."id" = x."secretId" JOIN "Agent" a ON a."id" = x."agentId"
|
|
105
|
+
WHERE a."ownerId" IS NULL OR s."ownerId" IS NULL OR s."ownerId" <> a."ownerId"
|
|
106
|
+
UNION ALL
|
|
107
|
+
SELECT 'datastore', a."id", a."name", a."ownerId", x."boundName", d."id", d."ownerId"
|
|
108
|
+
FROM "AgentDatastore" x JOIN "Datastore" d ON d."id" = x."datastoreId" JOIN "Agent" a ON a."id" = x."agentId"
|
|
109
|
+
WHERE a."ownerId" IS NULL OR d."ownerId" IS NULL OR d."ownerId" <> a."ownerId"
|
|
110
|
+
ORDER BY "agentName", "boundName"`;
|
|
111
|
+
const [{ stamped }] = await db.$queryRaw `
|
|
112
|
+
SELECT EXISTS (
|
|
113
|
+
SELECT 1 FROM information_schema.columns
|
|
114
|
+
WHERE table_schema = current_schema() AND table_name = 'AgentTool' AND column_name = 'capabilitiesGrantedById'
|
|
115
|
+
) AS "stamped"`;
|
|
116
|
+
// After the migration: honoured only when stamped by the current owner.
|
|
117
|
+
// Before it: what the migration will leave unstamped.
|
|
118
|
+
const suspended = stamped
|
|
119
|
+
? await db.$queryRaw `
|
|
120
|
+
SELECT a."id" AS "agentId", a."name" AS "agentName", t."id" AS "toolId", t."name" AS "toolName",
|
|
121
|
+
x."allowedSecrets", x."allowedDatastorePrefixes", x."allowedHosts", x."allowedSharedDatastorePrefixes"
|
|
122
|
+
FROM "AgentTool" x JOIN "Agent" a ON a."id" = x."agentId" JOIN "Tool" t ON t."id" = x."toolId"
|
|
123
|
+
WHERE a."ownerId" IS NULL OR x."capabilitiesGrantedById" IS DISTINCT FROM a."ownerId"
|
|
124
|
+
ORDER BY a."name", t."name"`
|
|
125
|
+
: await db.$queryRaw `
|
|
126
|
+
SELECT a."id" AS "agentId", a."name" AS "agentName", t."id" AS "toolId", t."name" AS "toolName",
|
|
127
|
+
x."allowedSecrets", x."allowedDatastorePrefixes", x."allowedHosts", x."allowedSharedDatastorePrefixes"
|
|
128
|
+
FROM "AgentTool" x JOIN "Agent" a ON a."id" = x."agentId" JOIN "Tool" t ON t."id" = x."toolId"
|
|
129
|
+
WHERE a."ownerId" IS NULL OR (t."ownerId" IS NOT NULL AND t."ownerId" <> a."ownerId")
|
|
130
|
+
ORDER BY a."name", t."name"`;
|
|
131
|
+
const review = stamped
|
|
132
|
+
? await db.$queryRaw `
|
|
133
|
+
SELECT a."id" AS "agentId", a."name" AS "agentName", t."id" AS "toolId", t."name" AS "toolName",
|
|
134
|
+
x."allowedSecrets", x."allowedDatastorePrefixes", x."allowedHosts", x."allowedSharedDatastorePrefixes"
|
|
135
|
+
FROM "AgentTool" x JOIN "Agent" a ON a."id" = x."agentId" JOIN "Tool" t ON t."id" = x."toolId"
|
|
136
|
+
WHERE a."ownerId" IS NOT NULL AND t."ownerId" IS NULL AND x."capabilitiesGrantedById" = a."ownerId"
|
|
137
|
+
ORDER BY a."name", t."name"`
|
|
138
|
+
: await db.$queryRaw `
|
|
139
|
+
SELECT a."id" AS "agentId", a."name" AS "agentName", t."id" AS "toolId", t."name" AS "toolName",
|
|
140
|
+
x."allowedSecrets", x."allowedDatastorePrefixes", x."allowedHosts", x."allowedSharedDatastorePrefixes"
|
|
141
|
+
FROM "AgentTool" x JOIN "Agent" a ON a."id" = x."agentId" JOIN "Tool" t ON t."id" = x."toolId"
|
|
142
|
+
WHERE a."ownerId" IS NOT NULL AND t."ownerId" IS NULL
|
|
143
|
+
ORDER BY a."name", t."name"`;
|
|
144
|
+
// canDelegate: same owner (null = null), or the parent's owner holds
|
|
145
|
+
// execute on the child. Before the migration, an owner-less child will
|
|
146
|
+
// hold everyone-execute.
|
|
147
|
+
const failingEdges = present
|
|
148
|
+
? await db.$queryRaw `
|
|
149
|
+
SELECT e."parentAgentId", p."ownerId" AS "parentOwnerId", e."childAgentId", c."ownerId" AS "childOwnerId", e."boundName"
|
|
150
|
+
FROM "AgentSubAgent" e JOIN "Agent" p ON p."id" = e."parentAgentId" JOIN "Agent" c ON c."id" = e."childAgentId"
|
|
151
|
+
WHERE p."ownerId" IS DISTINCT FROM c."ownerId"
|
|
152
|
+
AND NOT (p."ownerId" IS NOT NULL AND EXISTS (
|
|
153
|
+
SELECT 1 FROM "ResourceGrant" g
|
|
154
|
+
WHERE g."resourceType" = 'agent' AND g."resourceId" = c."id"
|
|
155
|
+
AND g."granteeKey" IN ('principal:' || p."ownerId", 'everyone') AND g."level" IN ('execute', 'write')))
|
|
156
|
+
ORDER BY e."boundName"`
|
|
157
|
+
: await db.$queryRaw `
|
|
158
|
+
SELECT e."parentAgentId", p."ownerId" AS "parentOwnerId", e."childAgentId", c."ownerId" AS "childOwnerId", e."boundName"
|
|
159
|
+
FROM "AgentSubAgent" e JOIN "Agent" p ON p."id" = e."parentAgentId" JOIN "Agent" c ON c."id" = e."childAgentId"
|
|
160
|
+
WHERE p."ownerId" IS DISTINCT FROM c."ownerId" AND NOT (p."ownerId" IS NOT NULL AND c."ownerId" IS NULL)
|
|
161
|
+
ORDER BY e."boundName"`;
|
|
162
|
+
const onOwnerlessAgents = await db.$queryRaw `
|
|
163
|
+
SELECT w."id", w."agentId", w."ownerId" AS "createdBy"
|
|
164
|
+
FROM "Webhook" w JOIN "Agent" a ON a."id" = w."agentId" WHERE a."ownerId" IS NULL ORDER BY w."id"`;
|
|
165
|
+
// A webhook fires only while its creator owns the agent or holds execute.
|
|
166
|
+
const creatorLacksExecute = present
|
|
167
|
+
? await db.$queryRaw `
|
|
168
|
+
SELECT w."id", w."agentId", w."ownerId" AS "createdBy"
|
|
169
|
+
FROM "Webhook" w JOIN "Agent" a ON a."id" = w."agentId"
|
|
170
|
+
WHERE (a."ownerId" IS NULL OR w."ownerId" IS DISTINCT FROM a."ownerId")
|
|
171
|
+
AND NOT EXISTS (
|
|
172
|
+
SELECT 1 FROM "ResourceGrant" g
|
|
173
|
+
WHERE g."resourceType" = 'agent' AND g."resourceId" = a."id"
|
|
174
|
+
AND (g."granteeKey" = 'everyone' OR (w."ownerId" IS NOT NULL AND g."granteeKey" = 'principal:' || w."ownerId"))
|
|
175
|
+
AND g."level" IN ('execute', 'write'))
|
|
176
|
+
ORDER BY w."id"`
|
|
177
|
+
: await db.$queryRaw `
|
|
178
|
+
SELECT w."id", w."agentId", w."ownerId" AS "createdBy"
|
|
179
|
+
FROM "Webhook" w JOIN "Agent" a ON a."id" = w."agentId"
|
|
180
|
+
WHERE a."ownerId" IS NOT NULL AND w."ownerId" IS DISTINCT FROM a."ownerId"
|
|
181
|
+
ORDER BY w."id"`;
|
|
182
|
+
const orphanGrants = present
|
|
183
|
+
? await db.$queryRaw `
|
|
184
|
+
SELECT g."id", g."resourceType", g."resourceId" FROM "ResourceGrant" g
|
|
185
|
+
WHERE (g."resourceType" = 'agent' AND NOT EXISTS (SELECT 1 FROM "Agent" a WHERE a."id" = g."resourceId"))
|
|
186
|
+
OR (g."resourceType" = 'tool' AND NOT EXISTS (SELECT 1 FROM "Tool" t WHERE t."id" = g."resourceId"))
|
|
187
|
+
OR (g."resourceType" = 'budget_group'
|
|
188
|
+
AND NOT EXISTS (SELECT 1 FROM "BudgetGroup" b WHERE b."id" = g."resourceId"))
|
|
189
|
+
OR g."resourceType" NOT IN ('agent', 'tool', 'budget_group')
|
|
190
|
+
ORDER BY g."id"`
|
|
191
|
+
: null;
|
|
192
|
+
return {
|
|
193
|
+
grantsTable: present,
|
|
194
|
+
ownerlessAgents: ownerless.map((a) => ({ ...a, revivedByAdopter: revivedFor(a.agentId) })),
|
|
195
|
+
inertBindings: bindings.map((b) => ({ ...b, prunable: b.agentOwnerId !== null })),
|
|
196
|
+
suspendedCapabilities: suspended.filter(hasCapabilities).map((row) => ({
|
|
197
|
+
agentId: row.agentId,
|
|
198
|
+
agentName: row.agentName,
|
|
199
|
+
toolId: row.toolId,
|
|
200
|
+
toolName: row.toolName,
|
|
201
|
+
regrant: regrantCall(row.agentId, row.toolId, row),
|
|
202
|
+
})),
|
|
203
|
+
reviewCapabilities: review.filter(hasCapabilities).map((row) => ({
|
|
204
|
+
agentId: row.agentId,
|
|
205
|
+
agentName: row.agentName,
|
|
206
|
+
toolId: row.toolId,
|
|
207
|
+
toolName: row.toolName,
|
|
208
|
+
regrant: regrantCall(row.agentId, row.toolId, row),
|
|
209
|
+
})),
|
|
210
|
+
failingEdges,
|
|
211
|
+
webhooks: { onOwnerlessAgents, creatorLacksExecute },
|
|
212
|
+
behaviourChanges: BEHAVIOUR_CHANGES,
|
|
213
|
+
orphanGrants,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
export function formatMigrationReport(report) {
|
|
217
|
+
const lines = [];
|
|
218
|
+
const section = (title, rows, empty = "none") => {
|
|
219
|
+
lines.push("", `== ${title}`);
|
|
220
|
+
lines.push(...(rows.length > 0 ? rows.map((r) => ` - ${r}`) : [` ${empty}`]));
|
|
221
|
+
};
|
|
222
|
+
lines.push(report.grantsTable
|
|
223
|
+
? "Resource grants migration: applied."
|
|
224
|
+
: "Resource grants migration: NOT applied yet (predicting its effect).");
|
|
225
|
+
section("1. Owner-less agents (everyone grant)", report.ownerlessAgents.flatMap((a) => [
|
|
226
|
+
`${a.name} (${a.agentId}): everyone ${a.everyoneGrant ?? "none"}` +
|
|
227
|
+
(a.everyoneGrant ? " (adopt-public lowers it to read unless --keep-everyone-execute)" : ""),
|
|
228
|
+
...a.revivedByAdopter.map((r) => ` adopting as ${r.subject} would bring back: secrets [${r.secrets.join(", ")}], datastores [${r.datastores.join(", ")}], capabilities of tools [${r.toolCapabilities.join(", ")}]`),
|
|
229
|
+
]));
|
|
230
|
+
section("2. Cross-owner secret/datastore bindings (inert at run time)", report.inertBindings.map((b) => `${b.kind} "${b.boundName}" on ${b.agentName} (${b.agentId}): resource ${b.resourceId} owned by ${b.resourceOwnerId ?? "nobody"}, agent owned by ${b.agentOwnerId ?? "nobody"}` +
|
|
231
|
+
(b.prunable ? " -- removed by prune-bindings" : " -- decided by adopt-public")));
|
|
232
|
+
section("3. Attachments whose capabilities are (or will be) suspended", report.suspendedCapabilities.map((c) => `${c.agentName} / ${c.toolName}: re-grant with ${c.regrant}`));
|
|
233
|
+
section("3b. Owner-less-tool capabilities kept in force on owned agents (review: a previous owner may have set them)", report.reviewCapabilities.map((c) => `${c.agentName} / ${c.toolName}: currently ${c.regrant}`));
|
|
234
|
+
section("4. Sub-agent edges refused at run time (canDelegate)", report.failingEdges.map((e) => `${e.parentAgentId} (owner ${e.parentOwnerId ?? "nobody"}) -> ${e.childAgentId} (owner ${e.childOwnerId ?? "nobody"}) as "${e.boundName}"`));
|
|
235
|
+
section("5a. Webhooks on owner-less agents (fire through everyone-execute)", report.webhooks.onOwnerlessAgents.map((w) => `${w.id} on ${w.agentId}, created by ${w.createdBy ?? "unknown"}`));
|
|
236
|
+
section("5b. Webhooks whose creator lacks execute (will not fire)", report.webhooks.creatorLacksExecute.map((w) => `${w.id} on ${w.agentId}, created by ${w.createdBy ?? "unknown"}`));
|
|
237
|
+
section("6. Behaviour changes for formerly public agents", [
|
|
238
|
+
...(report.ownerlessAgents.length > 0
|
|
239
|
+
? [`affected: ${report.ownerlessAgents.map((a) => a.name).join(", ")}`]
|
|
240
|
+
: ["affected: none on this deployment"]),
|
|
241
|
+
...report.behaviourChanges,
|
|
242
|
+
]);
|
|
243
|
+
section("7. Orphan grants (resource gone)", report.orphanGrants === null ? [] : report.orphanGrants.map((g) => `${g.id}: ${g.resourceType} ${g.resourceId}`), report.orphanGrants === null ? "skipped: the ResourceGrant table does not exist yet" : "none");
|
|
244
|
+
return lines.join("\n").trimStart();
|
|
245
|
+
}
|
|
246
|
+
class DryRunRollback extends Error {
|
|
247
|
+
}
|
|
248
|
+
/** Gives every owner-less agent `ownerSubject` as its owner. See the module header. */
|
|
249
|
+
export async function adoptPublic(db, opts) {
|
|
250
|
+
// findUnique, never upsert: a typo must not mint a principal that then owns everything.
|
|
251
|
+
const owner = await db.principal.findUnique({ where: { subject: opts.ownerSubject } });
|
|
252
|
+
if (!owner) {
|
|
253
|
+
throw new Error(`No principal with subject "${opts.ownerSubject}" (they must have signed in at least once).`);
|
|
254
|
+
}
|
|
255
|
+
const candidates = await db.agent.findMany({
|
|
256
|
+
where: { ownerId: null },
|
|
257
|
+
select: { id: true },
|
|
258
|
+
orderBy: { name: "asc" },
|
|
259
|
+
});
|
|
260
|
+
const agents = [];
|
|
261
|
+
for (const { id } of candidates) {
|
|
262
|
+
let adopted = null;
|
|
263
|
+
try {
|
|
264
|
+
await db.$transaction(async (tx) => {
|
|
265
|
+
const agent = await tx.agent.findUnique({ where: { id } });
|
|
266
|
+
if (!agent || agent.ownerId !== null)
|
|
267
|
+
return; // adopted concurrently
|
|
268
|
+
await tx.agent.update({ where: { id }, data: { ownerId: owner.id } });
|
|
269
|
+
const secrets = await tx.agentSecret.findMany({
|
|
270
|
+
where: { agentId: id },
|
|
271
|
+
include: { secret: { select: { ownerId: true } } },
|
|
272
|
+
orderBy: { boundName: "asc" },
|
|
273
|
+
});
|
|
274
|
+
const datastores = await tx.agentDatastore.findMany({
|
|
275
|
+
where: { agentId: id },
|
|
276
|
+
include: { datastore: { select: { ownerId: true } } },
|
|
277
|
+
orderBy: { boundName: "asc" },
|
|
278
|
+
});
|
|
279
|
+
const foreignSecrets = secrets.filter((b) => b.secret.ownerId !== owner.id);
|
|
280
|
+
const foreignDatastores = datastores.filter((b) => b.datastore.ownerId !== owner.id);
|
|
281
|
+
for (const b of foreignSecrets) {
|
|
282
|
+
await tx.agentSecret.delete({ where: { agentId_secretId: { agentId: id, secretId: b.secretId } } });
|
|
283
|
+
}
|
|
284
|
+
for (const b of foreignDatastores) {
|
|
285
|
+
await tx.agentDatastore.delete({
|
|
286
|
+
where: { agentId_datastoreId: { agentId: id, datastoreId: b.datastoreId } },
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
// Only the tool's owner could attach its private tool to a public
|
|
290
|
+
// agent or change that attachment's capabilities, so those carry
|
|
291
|
+
// the new owner's consent. Everything else stays inert.
|
|
292
|
+
const attachments = await tx.agentTool.findMany({
|
|
293
|
+
where: { agentId: id },
|
|
294
|
+
include: { tool: { select: { id: true, name: true, ownerId: true } } },
|
|
295
|
+
orderBy: { toolId: "asc" },
|
|
296
|
+
});
|
|
297
|
+
const own = attachments.filter((at) => at.tool.ownerId === owner.id);
|
|
298
|
+
for (const at of own) {
|
|
299
|
+
await tx.agentTool.update({
|
|
300
|
+
where: { agentId_toolId: { agentId: id, toolId: at.toolId } },
|
|
301
|
+
data: { capabilitiesGrantedById: owner.id },
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
const edges = await tx.agentSubAgent.findMany({
|
|
305
|
+
where: { OR: [{ parentAgentId: id }, { childAgentId: id }] },
|
|
306
|
+
include: {
|
|
307
|
+
parent: { select: { id: true, ownerId: true } },
|
|
308
|
+
child: { select: { id: true, ownerId: true } },
|
|
309
|
+
},
|
|
310
|
+
orderBy: { boundName: "asc" },
|
|
311
|
+
});
|
|
312
|
+
// The agent is about to regain the adopter's secrets and tool
|
|
313
|
+
// capabilities: an everyone grant above read would let anyone run
|
|
314
|
+
// it with them (review I3). Lowered to read unless the operator
|
|
315
|
+
// explicitly keeps execute.
|
|
316
|
+
const everyone = await tx.resourceGrant.findUnique({
|
|
317
|
+
where: {
|
|
318
|
+
resourceType_resourceId_granteeKey: { resourceType: "agent", resourceId: id, granteeKey: "everyone" },
|
|
319
|
+
},
|
|
320
|
+
});
|
|
321
|
+
const everyoneGrant = { before: everyone?.level ?? null, after: everyone?.level ?? null };
|
|
322
|
+
if (everyone && everyone.level !== "read" && !opts.keepEveryoneExecute) {
|
|
323
|
+
await tx.resourceGrant.update({
|
|
324
|
+
where: { id: everyone.id },
|
|
325
|
+
data: { level: "read", source: "operator", grantedById: owner.id },
|
|
326
|
+
});
|
|
327
|
+
everyoneGrant.after = "read";
|
|
328
|
+
}
|
|
329
|
+
const failingEdges = [];
|
|
330
|
+
for (const edge of edges) {
|
|
331
|
+
if (!(await canDelegate(tx, edge.parent, edge.child))) {
|
|
332
|
+
failingEdges.push({
|
|
333
|
+
parentAgentId: edge.parentAgentId,
|
|
334
|
+
childAgentId: edge.childAgentId,
|
|
335
|
+
boundName: edge.boundName,
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
adopted = {
|
|
340
|
+
agentId: id,
|
|
341
|
+
name: agent.name,
|
|
342
|
+
bindingsRemoved: [
|
|
343
|
+
...foreignSecrets.map((b) => ({
|
|
344
|
+
kind: "secret",
|
|
345
|
+
boundName: b.boundName,
|
|
346
|
+
resourceId: b.secretId,
|
|
347
|
+
})),
|
|
348
|
+
...foreignDatastores.map((b) => ({
|
|
349
|
+
kind: "datastore",
|
|
350
|
+
boundName: b.boundName,
|
|
351
|
+
resourceId: b.datastoreId,
|
|
352
|
+
})),
|
|
353
|
+
],
|
|
354
|
+
bindingsKept: [
|
|
355
|
+
...secrets
|
|
356
|
+
.filter((b) => b.secret.ownerId === owner.id)
|
|
357
|
+
.map((b) => ({ kind: "secret", boundName: b.boundName })),
|
|
358
|
+
...datastores
|
|
359
|
+
.filter((b) => b.datastore.ownerId === owner.id)
|
|
360
|
+
.map((b) => ({ kind: "datastore", boundName: b.boundName })),
|
|
361
|
+
],
|
|
362
|
+
capabilitiesStamped: own.map((at) => at.tool.name),
|
|
363
|
+
foreignTools: attachments
|
|
364
|
+
.filter((at) => at.tool.ownerId !== owner.id)
|
|
365
|
+
.map((at) => ({
|
|
366
|
+
toolId: at.toolId,
|
|
367
|
+
toolName: at.tool.name,
|
|
368
|
+
toolOwnerId: at.tool.ownerId,
|
|
369
|
+
regrant: hasCapabilities(at) ? regrantCall(id, at.toolId, at) : null,
|
|
370
|
+
})),
|
|
371
|
+
failingEdges,
|
|
372
|
+
everyoneGrant,
|
|
373
|
+
};
|
|
374
|
+
if (opts.dryRun)
|
|
375
|
+
throw new DryRunRollback();
|
|
376
|
+
}, { isolationLevel: Prisma.TransactionIsolationLevel.Serializable });
|
|
377
|
+
}
|
|
378
|
+
catch (err) {
|
|
379
|
+
if (!(err instanceof DryRunRollback))
|
|
380
|
+
throw err;
|
|
381
|
+
}
|
|
382
|
+
if (adopted)
|
|
383
|
+
agents.push(adopted);
|
|
384
|
+
}
|
|
385
|
+
return { owner: { id: owner.id, subject: owner.subject }, dryRun: opts.dryRun, agents };
|
|
386
|
+
}
|
|
387
|
+
export function formatAdoptResult(result) {
|
|
388
|
+
const lines = [
|
|
389
|
+
`${result.dryRun ? "[dry run] would adopt" : "Adopted"} ${result.agents.length} owner-less agent(s) for ${result.owner.subject} (${result.owner.id}).`,
|
|
390
|
+
];
|
|
391
|
+
for (const a of result.agents) {
|
|
392
|
+
lines.push("", `${a.name} (${a.agentId}): owner ${result.owner.subject}; named grants are kept.`);
|
|
393
|
+
for (const b of a.bindingsKept)
|
|
394
|
+
lines.push(` kept ${b.kind} binding "${b.boundName}"`);
|
|
395
|
+
for (const b of a.bindingsRemoved)
|
|
396
|
+
lines.push(` removed ${b.kind} binding "${b.boundName}" (${b.resourceId})`);
|
|
397
|
+
for (const t of a.capabilitiesStamped)
|
|
398
|
+
lines.push(` capabilities of own tool "${t}" now in force`);
|
|
399
|
+
if (a.everyoneGrant.before !== null) {
|
|
400
|
+
lines.push(a.everyoneGrant.before === a.everyoneGrant.after
|
|
401
|
+
? ` everyone keeps ${a.everyoneGrant.after}${a.everyoneGrant.after === "execute" ? ": anyone can run it with the secrets and capabilities it regains (revoke_access to stop that)" : ""}`
|
|
402
|
+
: ` everyone lowered from ${a.everyoneGrant.before} to ${a.everyoneGrant.after} (webhooks others created stop firing; grant execute to named principals instead)`);
|
|
403
|
+
}
|
|
404
|
+
for (const t of a.foreignTools) {
|
|
405
|
+
lines.push(` tool "${t.toolName}" (${t.toolId}) is owned by ${t.toolOwnerId ?? "nobody"}: review or detach_tool it` +
|
|
406
|
+
(t.regrant ? `; its capabilities stay inert, re-grant: ${t.regrant}` : ""));
|
|
407
|
+
}
|
|
408
|
+
for (const e of a.failingEdges) {
|
|
409
|
+
lines.push(` sub-agent edge ${e.parentAgentId} -> ${e.childAgentId} as "${e.boundName}" is refused at run time (not deleted)`);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
return lines.join("\n");
|
|
413
|
+
}
|
|
414
|
+
/** Deletes cross-owner secret/datastore bindings on owned agents, in one transaction. */
|
|
415
|
+
export async function pruneBindings(db, opts) {
|
|
416
|
+
return db.$transaction(async (tx) => {
|
|
417
|
+
const secrets = await tx.agentSecret.findMany({
|
|
418
|
+
where: { agent: { ownerId: { not: null } } },
|
|
419
|
+
include: { agent: { select: { name: true, ownerId: true } }, secret: { select: { ownerId: true } } },
|
|
420
|
+
orderBy: [{ agentId: "asc" }, { boundName: "asc" }],
|
|
421
|
+
});
|
|
422
|
+
const datastores = await tx.agentDatastore.findMany({
|
|
423
|
+
where: { agent: { ownerId: { not: null } } },
|
|
424
|
+
include: { agent: { select: { name: true, ownerId: true } }, datastore: { select: { ownerId: true } } },
|
|
425
|
+
orderBy: [{ agentId: "asc" }, { boundName: "asc" }],
|
|
426
|
+
});
|
|
427
|
+
const foreignSecrets = secrets.filter((b) => b.secret.ownerId !== b.agent.ownerId);
|
|
428
|
+
const foreignDatastores = datastores.filter((b) => b.datastore.ownerId !== b.agent.ownerId);
|
|
429
|
+
if (!opts.dryRun) {
|
|
430
|
+
for (const b of foreignSecrets) {
|
|
431
|
+
await tx.agentSecret.delete({ where: { agentId_secretId: { agentId: b.agentId, secretId: b.secretId } } });
|
|
432
|
+
}
|
|
433
|
+
for (const b of foreignDatastores) {
|
|
434
|
+
await tx.agentDatastore.delete({
|
|
435
|
+
where: { agentId_datastoreId: { agentId: b.agentId, datastoreId: b.datastoreId } },
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
return {
|
|
440
|
+
dryRun: opts.dryRun,
|
|
441
|
+
removed: [
|
|
442
|
+
...foreignSecrets.map((b) => ({
|
|
443
|
+
kind: "secret",
|
|
444
|
+
agentId: b.agentId,
|
|
445
|
+
agentName: b.agent.name,
|
|
446
|
+
boundName: b.boundName,
|
|
447
|
+
resourceId: b.secretId,
|
|
448
|
+
resourceOwnerId: b.secret.ownerId,
|
|
449
|
+
})),
|
|
450
|
+
...foreignDatastores.map((b) => ({
|
|
451
|
+
kind: "datastore",
|
|
452
|
+
agentId: b.agentId,
|
|
453
|
+
agentName: b.agent.name,
|
|
454
|
+
boundName: b.boundName,
|
|
455
|
+
resourceId: b.datastoreId,
|
|
456
|
+
resourceOwnerId: b.datastore.ownerId,
|
|
457
|
+
})),
|
|
458
|
+
],
|
|
459
|
+
};
|
|
460
|
+
}, { isolationLevel: Prisma.TransactionIsolationLevel.Serializable });
|
|
461
|
+
}
|
|
462
|
+
export function formatPruneResult(result) {
|
|
463
|
+
const verb = result.dryRun ? "would remove" : "removed";
|
|
464
|
+
if (result.removed.length === 0)
|
|
465
|
+
return `No cross-owner bindings to ${result.dryRun ? "remove" : "prune"}.`;
|
|
466
|
+
return [
|
|
467
|
+
`${result.dryRun ? "[dry run] " : ""}${result.removed.length} cross-owner binding(s):`,
|
|
468
|
+
...result.removed.map((b) => ` ${verb} ${b.kind} binding "${b.boundName}" on ${b.agentName} (${b.agentId}): ${b.resourceId} owned by ${b.resourceOwnerId ?? "nobody"}`),
|
|
469
|
+
].join("\n");
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* The owner of an agent made with `wardby agent create` (resource-sharing
|
|
473
|
+
* grants spec §3.8): `--owner <subject>` must already exist (a typo never
|
|
474
|
+
* mints a principal), otherwise the local operator (LOCAL_PRINCIPAL),
|
|
475
|
+
* found or created exactly as stdio does.
|
|
476
|
+
*/
|
|
477
|
+
export async function resolveCliAgentOwner(db, ownerSubject, localPrincipal) {
|
|
478
|
+
if (ownerSubject === undefined)
|
|
479
|
+
return (await resolvePrincipal(localPrincipal, db)).id;
|
|
480
|
+
const principal = await db.principal.findUnique({ where: { subject: ownerSubject } });
|
|
481
|
+
if (!principal)
|
|
482
|
+
throw new Error(`No principal with subject "${ownerSubject}" (they must have signed in at least once).`);
|
|
483
|
+
return principal.id;
|
|
484
|
+
}
|
|
485
|
+
export async function grantsCommand(args, db, output = console.log) {
|
|
486
|
+
const [action, ...rest] = args;
|
|
487
|
+
if (action === "migration-report") {
|
|
488
|
+
const { values } = parseArgs({ args: rest, strict: true, options: { json: { type: "boolean" } } });
|
|
489
|
+
const report = await migrationReport(db);
|
|
490
|
+
output(values.json ? JSON.stringify(report, null, 2) : formatMigrationReport(report));
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
if (action === "adopt-public") {
|
|
494
|
+
const { values } = parseArgs({
|
|
495
|
+
args: rest,
|
|
496
|
+
strict: true,
|
|
497
|
+
options: {
|
|
498
|
+
owner: { type: "string" },
|
|
499
|
+
"dry-run": { type: "boolean" },
|
|
500
|
+
"keep-everyone-execute": { type: "boolean" },
|
|
501
|
+
},
|
|
502
|
+
});
|
|
503
|
+
if (!values.owner)
|
|
504
|
+
throw new Error("grants adopt-public requires --owner <subject>.");
|
|
505
|
+
output(formatAdoptResult(await adoptPublic(db, {
|
|
506
|
+
ownerSubject: values.owner,
|
|
507
|
+
dryRun: values["dry-run"] === true,
|
|
508
|
+
keepEveryoneExecute: values["keep-everyone-execute"] === true,
|
|
509
|
+
})));
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
if (action === "prune-bindings") {
|
|
513
|
+
const { values } = parseArgs({ args: rest, strict: true, options: { "dry-run": { type: "boolean" } } });
|
|
514
|
+
output(formatPruneResult(await pruneBindings(db, { dryRun: values["dry-run"] === true })));
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
throw new Error(USAGE);
|
|
518
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `wardby auth host-account list [--subject <s>] | unlink --subject <s> [--provider github]`:
|
|
3
|
+
* the operator's view of principals' linked host identities. Keyed on
|
|
4
|
+
* Principal.subject, so it works in both auth modes (self-hosted and
|
|
5
|
+
* delegating). Unlinking frees a GitHub account to be linked by another
|
|
6
|
+
* principal (link_host_account refuses one already linked elsewhere).
|
|
7
|
+
*/
|
|
8
|
+
import { parseArgs } from "node:util";
|
|
9
|
+
import { REVIEW_HOST_PROVIDERS } from "../../providers/review-host/types.js";
|
|
10
|
+
const USAGE = "Use auth host-account list [--subject <s>] | host-account unlink --subject <s> [--provider github].";
|
|
11
|
+
export async function hostAccountCommand(args, db, output = console.log) {
|
|
12
|
+
const [action, ...rest] = args;
|
|
13
|
+
if (action !== "list" && action !== "unlink")
|
|
14
|
+
throw new Error(USAGE);
|
|
15
|
+
const { values } = parseArgs({
|
|
16
|
+
args: rest,
|
|
17
|
+
strict: true,
|
|
18
|
+
allowPositionals: false,
|
|
19
|
+
options: { subject: { type: "string" }, provider: { type: "string" } },
|
|
20
|
+
});
|
|
21
|
+
const provider = values.provider ?? "github";
|
|
22
|
+
if (!REVIEW_HOST_PROVIDERS.includes(provider))
|
|
23
|
+
throw new Error(`Unknown provider "${provider}".`);
|
|
24
|
+
if (action === "list") {
|
|
25
|
+
const rows = await db.hostIdentity.findMany({
|
|
26
|
+
where: { provider, ...(values.subject ? { principal: { subject: values.subject } } : {}) },
|
|
27
|
+
include: { principal: { select: { subject: true } } },
|
|
28
|
+
orderBy: { linkedAt: "asc" },
|
|
29
|
+
});
|
|
30
|
+
output(JSON.stringify(rows.map((r) => ({
|
|
31
|
+
subject: r.principal.subject,
|
|
32
|
+
provider: r.provider,
|
|
33
|
+
login: r.login,
|
|
34
|
+
hostUserId: r.hostUserId,
|
|
35
|
+
linkedAt: r.linkedAt,
|
|
36
|
+
})), null, 2));
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (!values.subject)
|
|
40
|
+
throw new Error("auth host-account unlink requires --subject.");
|
|
41
|
+
const principal = await db.principal.findUnique({ where: { subject: values.subject } });
|
|
42
|
+
if (!principal)
|
|
43
|
+
throw new Error(`No principal with subject "${values.subject}".`);
|
|
44
|
+
const { count } = await db.hostIdentity.deleteMany({ where: { principalId: principal.id, provider } });
|
|
45
|
+
await db.hostIdentityLinkRequest.deleteMany({ where: { principalId: principal.id, provider } });
|
|
46
|
+
output(count > 0 ? "Host account unlinked." : "No host account was linked.");
|
|
47
|
+
}
|