@wardby/cli 0.5.3 → 0.6.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 +33 -0
- package/README.md +25 -14
- package/dist/claude-coding-worker/context.d.ts +22 -0
- package/dist/claude-coding-worker/context.js +92 -0
- package/dist/claude-coding-worker/driver.d.ts +7 -1
- package/dist/claude-coding-worker/driver.js +38 -5
- package/dist/claude-coding-worker/sdk.js +13 -4
- package/dist/cli-help.d.ts +1 -1
- package/dist/cli-help.js +5 -2
- package/dist/cli-native-mode.d.ts +9 -0
- package/dist/cli-native-mode.js +20 -0
- package/dist/cli.d.ts +3 -1
- package/dist/cli.js +141 -5
- package/dist/coding/claude-context.d.ts +45 -0
- package/dist/coding/claude-context.js +434 -0
- package/dist/coding/profile.d.ts +12 -0
- package/dist/coding/profile.js +6 -0
- package/dist/coding/protocol.d.ts +104 -0
- package/dist/coding/protocol.js +87 -3
- package/dist/coding-worker/artifact.d.ts +8 -0
- package/dist/coding-worker/artifact.js +2 -2
- package/dist/coding-worker/driver.js +2 -0
- package/dist/coding-worker/sdk.d.ts +6 -0
- package/dist/coding-worker/sdk.js +1 -0
- package/dist/coding-worker/skills.d.ts +23 -0
- package/dist/coding-worker/skills.js +117 -0
- package/dist/coding-worker/types.d.ts +2 -0
- package/dist/config/github-allowed-accounts.d.ts +15 -0
- package/dist/config/github-allowed-accounts.js +36 -0
- package/dist/config/low-power.d.ts +12 -0
- package/dist/config/low-power.js +18 -0
- package/dist/config/mobile.d.ts +12 -0
- package/dist/config/mobile.js +18 -0
- package/dist/config/providers.d.ts +53 -0
- package/dist/config/providers.js +92 -6
- package/dist/core/db.d.ts +16 -6
- package/dist/core/db.js +12 -10
- package/dist/core/dispatch.d.ts +11 -0
- package/dist/core/dispatch.js +36 -1
- package/dist/core/engine-native.js +9 -5
- package/dist/core/host-events.d.ts +49 -3
- package/dist/core/host-events.js +289 -15
- package/dist/core/issue-bridge.js +42 -2
- package/dist/core/issue-events.js +12 -1
- package/dist/core/issue-status.js +14 -3
- package/dist/core/merge-order-check.d.ts +129 -0
- package/dist/core/merge-order-check.js +336 -0
- package/dist/core/notification-dispatcher.d.ts +70 -0
- package/dist/core/notification-dispatcher.js +368 -0
- package/dist/core/notification-templates.d.ts +25 -0
- package/dist/core/notification-templates.js +105 -0
- package/dist/core/notifications.d.ts +10 -0
- package/dist/core/notifications.js +38 -0
- package/dist/core/pg-auth.d.ts +22 -0
- package/dist/core/pg-auth.js +152 -0
- package/dist/core/power-composition.d.ts +16 -0
- package/dist/core/power-composition.js +37 -0
- package/dist/core/power-idle.d.ts +14 -0
- package/dist/core/power-idle.js +30 -0
- package/dist/core/power.d.ts +71 -0
- package/dist/core/power.js +202 -0
- package/dist/core/reconciler.d.ts +4 -2
- package/dist/core/reconciler.js +29 -5
- package/dist/core/related-pull-requests.d.ts +48 -4
- package/dist/core/related-pull-requests.js +113 -11
- package/dist/core/repo-access.d.ts +15 -0
- package/dist/core/repo-access.js +66 -0
- package/dist/core/review-fix.d.ts +37 -0
- package/dist/core/review-fix.js +207 -6
- package/dist/core/review-host-tools.d.ts +20 -0
- package/dist/core/review-host-tools.js +58 -2
- package/dist/core/runner.d.ts +336 -3
- package/dist/core/runner.js +894 -317
- package/dist/core/secret-broker-config.d.ts +103 -0
- package/dist/core/secret-broker-config.js +112 -0
- package/dist/core/secrets.d.ts +39 -4
- package/dist/core/secrets.js +148 -40
- package/dist/core/workflow-event-recorder.d.ts +14 -0
- package/dist/core/workflow-event-recorder.js +120 -0
- package/dist/core/workflow-events.d.ts +82 -0
- package/dist/core/workflow-events.js +44 -0
- package/dist/core/workflow-run-events.d.ts +16 -0
- package/dist/core/workflow-run-events.js +40 -0
- package/dist/generated/prisma/browser.d.ts +58 -0
- package/dist/generated/prisma/client.d.ts +58 -0
- package/dist/generated/prisma/commonInputTypes.d.ts +150 -0
- package/dist/generated/prisma/enums.d.ts +27 -0
- package/dist/generated/prisma/enums.js +23 -0
- package/dist/generated/prisma/internal/class.d.ts +88 -0
- package/dist/generated/prisma/internal/class.js +4 -4
- package/dist/generated/prisma/internal/prismaNamespace.d.ts +772 -1
- package/dist/generated/prisma/internal/prismaNamespace.js +135 -4
- package/dist/generated/prisma/internal/prismaNamespaceBrowser.d.ts +139 -0
- package/dist/generated/prisma/internal/prismaNamespaceBrowser.js +135 -4
- package/dist/generated/prisma/models/Agent.d.ts +360 -1
- package/dist/generated/prisma/models/CodingAgentProfile.d.ts +73 -1
- package/dist/generated/prisma/models/CodingRun.d.ts +205 -1
- package/dist/generated/prisma/models/DeferredReview.d.ts +78 -1
- package/dist/generated/prisma/models/NativeGatewayCall.d.ts +1675 -0
- package/dist/generated/prisma/models/NativeGatewayCall.js +1 -0
- package/dist/generated/prisma/models/NativeGatewaySession.d.ts +1517 -0
- package/dist/generated/prisma/models/NativeGatewaySession.js +1 -0
- package/dist/generated/prisma/models/NativeWarmWorker.d.ts +1296 -0
- package/dist/generated/prisma/models/NativeWarmWorker.js +1 -0
- package/dist/generated/prisma/models/NotificationChannel.d.ts +1543 -0
- package/dist/generated/prisma/models/NotificationChannel.js +1 -0
- package/dist/generated/prisma/models/NotificationDelivery.d.ts +1502 -0
- package/dist/generated/prisma/models/NotificationDelivery.js +1 -0
- package/dist/generated/prisma/models/NotificationThread.d.ts +1090 -0
- package/dist/generated/prisma/models/NotificationThread.js +1 -0
- package/dist/generated/prisma/models/Run.d.ts +826 -1
- package/dist/generated/prisma/models/RunHostCheck.d.ts +36 -1
- package/dist/generated/prisma/models/Secret.d.ts +37 -1
- package/dist/generated/prisma/models/SecretBrokerChange.d.ts +1101 -0
- package/dist/generated/prisma/models/SecretBrokerChange.js +1 -0
- package/dist/generated/prisma/models/WorkflowEvent.d.ts +1422 -0
- package/dist/generated/prisma/models/WorkflowEvent.js +1 -0
- package/dist/generated/prisma/models.d.ts +8 -0
- package/dist/help/build.js +4 -2
- package/dist/help/catalog.d.ts +1 -1
- package/dist/help/catalog.js +31 -6
- package/dist/help-index.json +1363 -176
- package/dist/import/create.js +17 -0
- package/dist/import/neutral-schema.d.ts +6 -2
- package/dist/import/neutral-schema.js +2 -0
- package/dist/mcp/auth/access.d.ts +1 -0
- package/dist/mcp/auth/repo-authorization.js +14 -0
- package/dist/mcp/auth/resource-server.d.ts +2 -1
- package/dist/mcp/auth/resource-server.js +7 -2
- package/dist/mcp/auth/self-hosted/browser.d.ts +1 -1
- package/dist/mcp/auth/self-hosted/browser.js +19 -6
- package/dist/mcp/context.d.ts +6 -0
- package/dist/mcp/host-events/github-ingress.d.ts +23 -1
- package/dist/mcp/host-events/github-ingress.js +68 -0
- package/dist/mcp/index.d.ts +13 -0
- package/dist/mcp/index.js +59 -10
- package/dist/mcp/tools/agents.js +82 -11
- package/dist/mcp/tools/notification-channels.d.ts +2 -0
- package/dist/mcp/tools/notification-channels.js +293 -0
- package/dist/mcp/tools/power.d.ts +2 -0
- package/dist/mcp/tools/power.js +77 -0
- package/dist/mcp/tools/runs.d.ts +0 -6
- package/dist/mcp/tools/runs.js +7 -2
- package/dist/mcp/tools/secret-broker-compat.d.ts +12 -0
- package/dist/mcp/tools/secret-broker-compat.js +23 -0
- package/dist/mcp/tools/secret-elicitation-form.d.ts +22 -2
- package/dist/mcp/tools/secret-elicitation-form.js +200 -10
- package/dist/mcp/tools/secret-elicitation-server.js +8 -1
- package/dist/mcp/tools/secret-elicitation.d.ts +38 -3
- package/dist/mcp/tools/secret-elicitation.js +110 -12
- package/dist/mcp/tools/secrets.js +184 -7
- package/dist/mcp/tools/tools.d.ts +1 -1
- package/dist/mcp/tools/tools.js +22 -2
- package/dist/native-worker/composition.d.ts +23 -0
- package/dist/native-worker/composition.js +77 -0
- package/dist/native-worker/docker-isolation.d.ts +65 -0
- package/dist/native-worker/docker-isolation.js +183 -0
- package/dist/native-worker/docker-launcher.d.ts +74 -0
- package/dist/native-worker/docker-launcher.js +237 -0
- package/dist/native-worker/gateway-server.d.ts +39 -0
- package/dist/native-worker/gateway-server.js +315 -0
- package/dist/native-worker/gateway.d.ts +44 -0
- package/dist/native-worker/gateway.js +197 -0
- package/dist/native-worker/http-server.d.ts +22 -0
- package/dist/native-worker/http-server.js +109 -0
- package/dist/native-worker/http-transport.d.ts +17 -0
- package/dist/native-worker/http-transport.js +88 -0
- package/dist/native-worker/kubernetes-isolation.d.ts +90 -0
- package/dist/native-worker/kubernetes-isolation.js +218 -0
- package/dist/native-worker/kubernetes-launcher.d.ts +102 -0
- package/dist/native-worker/kubernetes-launcher.js +335 -0
- package/dist/native-worker/launch.d.ts +45 -0
- package/dist/native-worker/launch.js +44 -0
- package/dist/native-worker/ledger.d.ts +109 -0
- package/dist/native-worker/ledger.js +192 -0
- package/dist/native-worker/loopback.d.ts +12 -0
- package/dist/native-worker/loopback.js +49 -0
- package/dist/native-worker/main.d.ts +7 -0
- package/dist/native-worker/main.js +74 -0
- package/dist/native-worker/protocol.d.ts +744 -0
- package/dist/native-worker/protocol.js +200 -0
- package/dist/native-worker/sandbox-executor.d.ts +68 -0
- package/dist/native-worker/sandbox-executor.js +210 -0
- package/dist/native-worker/stdio.d.ts +29 -0
- package/dist/native-worker/stdio.js +188 -0
- package/dist/native-worker/warm-delivery.d.ts +16 -0
- package/dist/native-worker/warm-delivery.js +33 -0
- package/dist/native-worker/warm-pool-ledger.d.ts +39 -0
- package/dist/native-worker/warm-pool-ledger.js +73 -0
- package/dist/native-worker/warm-pool.d.ts +107 -0
- package/dist/native-worker/warm-pool.js +283 -0
- package/dist/native-worker/worker.d.ts +17 -0
- package/dist/native-worker/worker.js +115 -0
- package/dist/providers/auth/authorization-server.d.ts +3 -0
- package/dist/providers/auth/index.js +2 -0
- package/dist/providers/auth/self-hosted.d.ts +13 -1
- package/dist/providers/auth/self-hosted.js +54 -4
- package/dist/providers/chat/fake.d.ts +46 -0
- package/dist/providers/chat/fake.js +39 -0
- package/dist/providers/chat/index.d.ts +4 -0
- package/dist/providers/chat/index.js +8 -0
- package/dist/providers/chat/slack.d.ts +37 -0
- package/dist/providers/chat/slack.js +105 -0
- package/dist/providers/chat/types.d.ts +42 -0
- package/dist/providers/chat/types.js +19 -0
- package/dist/providers/engine/types.d.ts +19 -0
- package/dist/providers/engine/types.js +22 -0
- package/dist/providers/executor/composition.d.ts +24 -1
- package/dist/providers/executor/composition.js +39 -6
- package/dist/providers/executor/container.d.ts +24 -1
- package/dist/providers/executor/container.js +101 -11
- package/dist/providers/executor/dbos.d.ts +42 -0
- package/dist/providers/executor/dbos.js +118 -12
- package/dist/providers/executor/deferred.d.ts +56 -0
- package/dist/providers/executor/deferred.js +119 -0
- package/dist/providers/executor/routing.d.ts +37 -10
- package/dist/providers/executor/routing.js +73 -32
- package/dist/providers/executor/types.d.ts +12 -0
- package/dist/providers/jobs/docker.js +3 -3
- package/dist/providers/jobs/fake-kubernetes-api.d.ts +21 -2
- package/dist/providers/jobs/fake-kubernetes-api.js +42 -1
- package/dist/providers/jobs/kubernetes-api.d.ts +17 -1
- package/dist/providers/jobs/kubernetes-api.js +7 -0
- package/dist/providers/jobs/kubernetes-client.d.ts +10 -1
- package/dist/providers/jobs/kubernetes-client.js +52 -3
- package/dist/providers/jobs/kubernetes-dry-run-fixture.d.ts +6 -1
- package/dist/providers/jobs/kubernetes-dry-run-fixture.js +6 -3
- package/dist/providers/jobs/kubernetes-isolation.d.ts +3 -1
- package/dist/providers/jobs/kubernetes-isolation.js +6 -0
- package/dist/providers/jobs/kubernetes-platform.d.ts +4 -1
- package/dist/providers/jobs/kubernetes-platform.js +38 -1
- package/dist/providers/jobs/kubernetes-preflight.d.ts +2 -1
- package/dist/providers/jobs/kubernetes-preflight.js +4 -2
- package/dist/providers/jobs/kubernetes.d.ts +36 -1
- package/dist/providers/jobs/kubernetes.js +74 -4
- package/dist/providers/jobs/metadata-probes.d.ts +11 -0
- package/dist/providers/jobs/metadata-probes.js +13 -0
- package/dist/providers/jobs/types.d.ts +9 -0
- package/dist/providers/review-host/github.d.ts +11 -1
- package/dist/providers/review-host/github.js +50 -0
- package/dist/providers/review-host/types.d.ts +23 -0
- package/dist/providers/vcs/github.d.ts +32 -6
- package/dist/providers/vcs/github.js +74 -16
- package/dist/quickstart/coding-seed.d.ts +10 -3
- package/dist/quickstart/coding-seed.js +74 -3
- package/dist/quickstart/images.d.ts +2 -0
- package/dist/quickstart/images.js +7 -1
- package/dist/quickstart/reviewer-prompt.d.ts +1 -1
- package/dist/quickstart/reviewer-prompt.js +1 -1
- package/dist/quickstart-images.json +1 -1
- package/dist/sandbox/brokered-fetch.d.ts +23 -0
- package/dist/sandbox/brokered-fetch.js +37 -0
- package/dist/sandbox/host-functions.d.ts +34 -0
- package/dist/sandbox/host-functions.js +144 -80
- package/dist/sandbox/prelude.js +7 -1
- package/dist/sandbox/run-in-sandbox.d.ts +19 -0
- package/dist/sandbox/run-in-sandbox.js +28 -10
- package/dist/sandbox/safe-fetch.d.ts +2 -0
- package/dist/sandbox/safe-fetch.js +3 -1
- package/dist/sandbox/secret-broker-sigv4.d.ts +2 -0
- package/dist/sandbox/secret-broker-sigv4.js +49 -0
- package/dist/sandbox/secret-broker.d.ts +34 -0
- package/dist/sandbox/secret-broker.js +162 -0
- package/dist/sandbox/user-tool.d.ts +14 -0
- package/dist/sandbox/user-tool.js +32 -0
- package/dist/serve.js +18 -0
- package/dist/viewer/api-schema.d.ts +210 -14
- package/dist/viewer/api-schema.js +22 -0
- package/dist/viewer/build-schemas.js +4 -1
- package/dist/viewer/event-bus.d.ts +6 -0
- package/dist/viewer/event-bus.js +16 -7
- package/dist/viewer/graph.d.ts +5 -2
- package/dist/viewer/graph.js +7 -2
- package/dist/viewer/http.d.ts +0 -1
- package/dist/viewer/http.js +2 -2
- package/dist/viewer/infra.js +25 -2
- package/dist/viewer/limits.d.ts +4 -0
- package/dist/viewer/limits.js +4 -0
- package/dist/viewer/openapi.d.ts +15 -0
- package/dist/viewer/openapi.js +172 -0
- package/docs/README.md +7 -0
- package/docs/agent-recipes.md +57 -8
- package/docs/architecture-runtime.md +16 -1
- package/docs/code-review-agents.md +139 -5
- package/docs/coding-agent-setup.md +113 -5
- package/docs/coding-packages.md +4 -2
- package/docs/coding-services.md +3 -1
- package/docs/coding-worker-byo-images.md +198 -6
- package/docs/coding-worker-isolation.md +68 -14
- package/docs/getting-started-gke.md +225 -9
- package/docs/getting-started-identity-provider.md +21 -10
- package/docs/getting-started.md +5 -4
- package/docs/jira-agents.md +5 -1
- package/docs/native-sandbox.md +635 -0
- package/docs/release-verification.md +9 -0
- package/docs/security-deployment.md +113 -12
- package/docs/slack-notifications.md +252 -0
- package/docs/tool-secrets.md +252 -0
- package/docs/viewer-api.md +46 -5
- package/docs/viewer-api.openapi.yml +1404 -0
- package/help/admin-viewer.md +6 -2
- package/help/agent-recipes.md +27 -0
- package/help/architecture-agent.md +1 -1
- package/help/brokered-secrets.md +79 -0
- package/help/build-claude-code-image.md +131 -0
- package/help/build-worker-image.md +3 -6
- package/help/code-review-agents.md +68 -6
- package/help/deploy-gke.md +8 -1
- package/help/deployment-targets.md +25 -14
- package/help/errors/coding-provider-not-configured.md +2 -2
- package/help/errors/database-auth-invalid.md +28 -0
- package/help/errors/github-account-not-allowed.md +25 -0
- package/help/errors/low-power-disabled.md +29 -0
- package/help/errors/low-power-scale-forbidden.md +35 -0
- package/help/errors/low-power-wake-timeout.md +30 -0
- package/help/errors/mobile-disabled.md +31 -0
- package/help/errors/native-sandbox-capacity.md +22 -0
- package/help/errors/native-sandbox-deadline-exceeded.md +19 -0
- package/help/errors/native-sandbox-docker-failed.md +20 -0
- package/help/errors/native-sandbox-gateway-unavailable.md +21 -0
- package/help/errors/native-sandbox-image-not-pinned.md +19 -0
- package/help/errors/native-sandbox-isolation-mismatch.md +20 -0
- package/help/errors/native-sandbox-network-unenforced.md +22 -0
- package/help/errors/native-sandbox-requires-catalog.md +31 -0
- package/help/errors/native-sandbox-unavailable.md +19 -0
- package/help/errors/native-sandbox-warm-delivery-failed.md +21 -0
- package/help/errors/native-sandbox-worker-exited.md +20 -0
- package/help/errors/native-sandbox-worker-lost.md +20 -0
- package/help/errors/native-sandbox-worker-unready.md +22 -0
- package/help/errors/secret-broker-body-invalid.md +26 -0
- package/help/errors/secret-broker-changed.md +20 -0
- package/help/errors/secret-broker-config-invalid.md +30 -0
- package/help/errors/secret-broker-conflict.md +27 -0
- package/help/errors/secret-broker-destination-denied.md +28 -0
- package/help/errors/secret-broker-value-invalid.md +28 -0
- package/help/errors/secret-brokered.md +24 -0
- package/help/errors/secret-not-brokered.md +25 -0
- package/help/errors/slack-auth-failed.md +48 -0
- package/help/errors/slack-channel-unreachable.md +39 -0
- package/help/errors/slack-not-configured.md +33 -0
- package/help/getting-started.md +4 -5
- package/help/github.md +18 -1
- package/help/identity-and-access.md +5 -1
- package/help/local-repositories.md +2 -3
- package/help/low-power-mode.md +125 -0
- package/help/merge-order-check.md +117 -0
- package/help/mobile-app.md +54 -0
- package/help/native-sandbox.md +136 -0
- package/help/observability.md +8 -0
- package/help/related-pull-requests.md +70 -9
- package/help/repo-instructions-and-skills.md +91 -0
- package/help/review-fix-rounds.md +10 -0
- package/help/slack-notifications.md +62 -0
- package/help/troubleshooting/coding-workers.md +21 -0
- package/package.json +11 -4
- package/prisma/migrations/20261009000003_native_execution_mode/migration.sql +12 -0
- package/prisma/migrations/20261009000004_native_gateway_ledger/migration.sql +65 -0
- package/prisma/migrations/20261009000005_native_gateway_call_child_agent/migration.sql +5 -0
- package/prisma/migrations/20261009000006_run_deferred_start_stop/migration.sql +14 -0
- package/prisma/migrations/20261009000007_native_gateway_network_ready/migration.sql +5 -0
- package/prisma/migrations/20261010000001_workflow_notifications/migration.sql +99 -0
- package/prisma/migrations/20261011000001_native_gateway_session_status_index/migration.sql +2 -0
- package/prisma/migrations/20261012000001_native_warm_worker/migration.sql +32 -0
- package/prisma/migrations/20261013000001_coding_run_merge_order/migration.sql +2 -0
- package/prisma/migrations/20261013000001_secret_broker/migration.sql +17 -0
- package/prisma/migrations/20261013000002_deferred_review_request/migration.sql +4 -0
- package/prisma/migrations/20261014000000_repo_skills/migration.sql +15 -0
- package/prisma/migrations/20261015000001_coding_run_merge_order_index/migration.sql +2 -0
- package/prisma/migrations/20261015000002_no_change_rereview/migration.sql +2 -0
- package/prisma/schema.prisma +362 -91
|
@@ -1,25 +1,49 @@
|
|
|
1
|
-
import type { CodingImageSelector, Executor, ExecutionRecoveryResult, PersistedExecutionHandle } from "./types.js";
|
|
2
1
|
import type { CodingProvider } from "../../coding/provider.js";
|
|
2
|
+
import type { CodingImageSelector, Executor, ExecutionRecoveryResult, PersistedExecutionHandle } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Which executor owns a run: a coding run, a native run in the control plane
|
|
5
|
+
* (in-process or DBOS), or a native run in an isolated sandbox worker. Read
|
|
6
|
+
* from the run's own row (agent kind plus Run.nativeExecutionMode), never from
|
|
7
|
+
* the agent's current setting, so a setting change reaches only later runs.
|
|
8
|
+
*/
|
|
9
|
+
export type ExecutionTarget = "native" | "native-sandbox" | "coding";
|
|
3
10
|
export interface ExecutionKindResolver {
|
|
4
|
-
kindForRun(runId: string): Promise<
|
|
11
|
+
kindForRun(runId: string): Promise<ExecutionTarget | null>;
|
|
5
12
|
}
|
|
6
|
-
/**
|
|
13
|
+
/**
|
|
14
|
+
* Keeps native execution unchanged while routing coding runs to isolation and
|
|
15
|
+
* sandbox-mode native runs to the native sandbox executor.
|
|
16
|
+
*
|
|
17
|
+
* With no sandbox executor composed in, a sandbox run goes to the native
|
|
18
|
+
* executor, whose runner refuses to execute it (executeRun fails it before
|
|
19
|
+
* any spend, closing its host check and status comments): fail closed through
|
|
20
|
+
* the one path that already finishes a run properly, never a silent run in
|
|
21
|
+
* the control plane.
|
|
22
|
+
*/
|
|
7
23
|
export declare class RoutingExecutor implements Executor {
|
|
8
24
|
private readonly resolver;
|
|
9
25
|
private readonly native;
|
|
10
26
|
private readonly coding;
|
|
11
|
-
|
|
27
|
+
private readonly sandbox?;
|
|
28
|
+
constructor(resolver: ExecutionKindResolver, native: Executor, coding: Executor, sandbox?: Executor | undefined);
|
|
29
|
+
private route;
|
|
12
30
|
start(runId: string): Promise<void>;
|
|
13
31
|
stop(runId: string, reason?: string): Promise<void>;
|
|
14
32
|
/**
|
|
15
|
-
* Recovery is routed by the run's
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* handle backend would need this class to know every
|
|
33
|
+
* Recovery is routed by the run's target, exactly like start/stop: a native
|
|
34
|
+
* run's handle belongs to the native executor (e.g. a DBOS workflow handle),
|
|
35
|
+
* a sandbox run's to the sandbox executor, a coding run's to the container
|
|
36
|
+
* executor. Routing by handle backend would need this class to know every
|
|
37
|
+
* backend name.
|
|
19
38
|
*/
|
|
20
39
|
recover(handle: PersistedExecutionHandle): Promise<ExecutionRecoveryResult>;
|
|
21
|
-
/**
|
|
40
|
+
/** Each distinct executor once, in a fixed order: native, coding, sandbox. */
|
|
41
|
+
private distinct;
|
|
42
|
+
/** Lifecycle fans out to every distinct executor; each hook is optional on the seam. */
|
|
22
43
|
launch(): Promise<void>;
|
|
44
|
+
/** Fans out like `launch`. */
|
|
45
|
+
warmUp(): Promise<void>;
|
|
46
|
+
/** Reverse of `launch`. */
|
|
23
47
|
close(): Promise<void>;
|
|
24
48
|
resolveCodingWorkerImage(selector: CodingImageSelector): string;
|
|
25
49
|
resolveCodingToolImage(selector: CodingImageSelector): string | null;
|
|
@@ -33,6 +57,7 @@ export declare class RoutingExecutor implements Executor {
|
|
|
33
57
|
path: string;
|
|
34
58
|
maxBytes: number;
|
|
35
59
|
}): Promise<string | null>;
|
|
60
|
+
supportsNativeSandbox(): boolean;
|
|
36
61
|
supportsCodingServices(provider: CodingProvider): boolean;
|
|
37
62
|
}
|
|
38
63
|
export declare class PrismaExecutionKindResolver implements ExecutionKindResolver {
|
|
@@ -44,6 +69,7 @@ export declare class PrismaExecutionKindResolver implements ExecutionKindResolve
|
|
|
44
69
|
id: string;
|
|
45
70
|
};
|
|
46
71
|
select: {
|
|
72
|
+
nativeExecutionMode: true;
|
|
47
73
|
agent: {
|
|
48
74
|
select: {
|
|
49
75
|
kind: true;
|
|
@@ -51,11 +77,12 @@ export declare class PrismaExecutionKindResolver implements ExecutionKindResolve
|
|
|
51
77
|
};
|
|
52
78
|
};
|
|
53
79
|
}): Promise<{
|
|
80
|
+
nativeExecutionMode: string | null;
|
|
54
81
|
agent: {
|
|
55
82
|
kind: string;
|
|
56
83
|
};
|
|
57
84
|
} | null>;
|
|
58
85
|
};
|
|
59
86
|
});
|
|
60
|
-
kindForRun(runId: string): Promise<
|
|
87
|
+
kindForRun(runId: string): Promise<ExecutionTarget | null>;
|
|
61
88
|
}
|
|
@@ -1,53 +1,86 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* Keeps native execution unchanged while routing coding runs to isolation and
|
|
3
|
+
* sandbox-mode native runs to the native sandbox executor.
|
|
4
|
+
*
|
|
5
|
+
* With no sandbox executor composed in, a sandbox run goes to the native
|
|
6
|
+
* executor, whose runner refuses to execute it (executeRun fails it before
|
|
7
|
+
* any spend, closing its host check and status comments): fail closed through
|
|
8
|
+
* the one path that already finishes a run properly, never a silent run in
|
|
9
|
+
* the control plane.
|
|
10
|
+
*/
|
|
2
11
|
export class RoutingExecutor {
|
|
3
12
|
resolver;
|
|
4
13
|
native;
|
|
5
14
|
coding;
|
|
6
|
-
|
|
15
|
+
sandbox;
|
|
16
|
+
constructor(resolver, native, coding, sandbox) {
|
|
7
17
|
this.resolver = resolver;
|
|
8
18
|
this.native = native;
|
|
9
19
|
this.coding = coding;
|
|
20
|
+
this.sandbox = sandbox;
|
|
21
|
+
}
|
|
22
|
+
async route(runId) {
|
|
23
|
+
const target = await this.resolver.kindForRun(runId);
|
|
24
|
+
if (target === "native")
|
|
25
|
+
return { target, executor: this.native };
|
|
26
|
+
if (target === "native-sandbox")
|
|
27
|
+
return { target, executor: this.sandbox ?? this.native };
|
|
28
|
+
if (target === "coding")
|
|
29
|
+
return { target, executor: this.coding };
|
|
30
|
+
return null;
|
|
10
31
|
}
|
|
11
32
|
async start(runId) {
|
|
12
|
-
const
|
|
13
|
-
if (
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
return this.coding.start(runId);
|
|
17
|
-
throw new Error("executor_run_not_found");
|
|
33
|
+
const routed = await this.route(runId);
|
|
34
|
+
if (!routed)
|
|
35
|
+
throw new Error("executor_run_not_found");
|
|
36
|
+
return routed.executor.start(runId);
|
|
18
37
|
}
|
|
19
38
|
async stop(runId, reason) {
|
|
20
|
-
const
|
|
21
|
-
if (
|
|
22
|
-
return
|
|
23
|
-
if (kind === "coding")
|
|
24
|
-
return this.coding.stop(runId, reason);
|
|
39
|
+
const routed = await this.route(runId);
|
|
40
|
+
if (routed)
|
|
41
|
+
return routed.executor.stop(runId, reason);
|
|
25
42
|
}
|
|
26
43
|
/**
|
|
27
|
-
* Recovery is routed by the run's
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* handle backend would need this class to know every
|
|
44
|
+
* Recovery is routed by the run's target, exactly like start/stop: a native
|
|
45
|
+
* run's handle belongs to the native executor (e.g. a DBOS workflow handle),
|
|
46
|
+
* a sandbox run's to the sandbox executor, a coding run's to the container
|
|
47
|
+
* executor. Routing by handle backend would need this class to know every
|
|
48
|
+
* backend name.
|
|
31
49
|
*/
|
|
32
50
|
async recover(handle) {
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
51
|
+
const routed = await this.route(handle.runId);
|
|
52
|
+
const target = routed?.target ?? "coding";
|
|
53
|
+
const executor = routed?.executor ?? this.coding;
|
|
54
|
+
if (!executor.recover) {
|
|
55
|
+
const reason = target === "native"
|
|
56
|
+
? "native_recovery_unavailable"
|
|
57
|
+
: target === "native-sandbox" && this.sandbox
|
|
58
|
+
? "native_sandbox_recovery_unavailable"
|
|
59
|
+
: target === "native-sandbox"
|
|
60
|
+
? "native_recovery_unavailable"
|
|
61
|
+
: "coding_recovery_unavailable";
|
|
62
|
+
return { state: "lost", reason };
|
|
38
63
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
64
|
+
return executor.recover(handle);
|
|
65
|
+
}
|
|
66
|
+
/** Each distinct executor once, in a fixed order: native, coding, sandbox. */
|
|
67
|
+
distinct() {
|
|
68
|
+
return [this.native, this.coding, this.sandbox].filter((executor, index, all) => executor !== undefined && all.indexOf(executor) === index);
|
|
42
69
|
}
|
|
43
|
-
/** Lifecycle fans out to
|
|
70
|
+
/** Lifecycle fans out to every distinct executor; each hook is optional on the seam. */
|
|
44
71
|
async launch() {
|
|
45
|
-
|
|
46
|
-
|
|
72
|
+
for (const executor of this.distinct())
|
|
73
|
+
await executor.launch?.();
|
|
74
|
+
}
|
|
75
|
+
/** Fans out like `launch`. */
|
|
76
|
+
async warmUp() {
|
|
77
|
+
for (const executor of this.distinct())
|
|
78
|
+
await executor.warmUp?.();
|
|
47
79
|
}
|
|
80
|
+
/** Reverse of `launch`. */
|
|
48
81
|
async close() {
|
|
49
|
-
|
|
50
|
-
|
|
82
|
+
for (const executor of this.distinct().reverse())
|
|
83
|
+
await executor.close?.();
|
|
51
84
|
}
|
|
52
85
|
resolveCodingWorkerImage(selector) {
|
|
53
86
|
if (!this.coding.resolveCodingWorkerImage)
|
|
@@ -69,6 +102,9 @@ export class RoutingExecutor {
|
|
|
69
102
|
return null;
|
|
70
103
|
return this.coding.readCodingRepositoryFile(input);
|
|
71
104
|
}
|
|
105
|
+
supportsNativeSandbox() {
|
|
106
|
+
return this.sandbox !== undefined;
|
|
107
|
+
}
|
|
72
108
|
supportsCodingServices(provider) {
|
|
73
109
|
return this.coding.supportsCodingServices?.(provider) === true;
|
|
74
110
|
}
|
|
@@ -81,8 +117,13 @@ export class PrismaExecutionKindResolver {
|
|
|
81
117
|
async kindForRun(runId) {
|
|
82
118
|
const run = await this.db.run.findUnique({
|
|
83
119
|
where: { id: runId },
|
|
84
|
-
select: { agent: { select: { kind: true } } },
|
|
120
|
+
select: { nativeExecutionMode: true, agent: { select: { kind: true } } },
|
|
85
121
|
});
|
|
86
|
-
|
|
122
|
+
if (run?.agent.kind === "coding")
|
|
123
|
+
return "coding";
|
|
124
|
+
if (run?.agent.kind !== "native")
|
|
125
|
+
return null;
|
|
126
|
+
// Null = a run from before the snapshot existed: the control plane, as it always ran.
|
|
127
|
+
return run.nativeExecutionMode === "sandbox" ? "native-sandbox" : "native";
|
|
87
128
|
}
|
|
88
129
|
}
|
|
@@ -43,6 +43,13 @@ export interface Executor {
|
|
|
43
43
|
launch?: () => Promise<void>;
|
|
44
44
|
/** Optional graceful shutdown counterpart to `launch`. */
|
|
45
45
|
close?: () => Promise<void>;
|
|
46
|
+
/**
|
|
47
|
+
* Fire-and-forget start-up warm-up, called only from a long-running server process's own
|
|
48
|
+
* start-up (`wardby serve`, the MCP server, `wardby scheduler`) — never from a one-shot CLI
|
|
49
|
+
* command. Delegates to the coding job launcher's own `warmUp` (e.g. Kubernetes's memoized
|
|
50
|
+
* cluster preflight); never throws.
|
|
51
|
+
*/
|
|
52
|
+
warmUp?: () => Promise<void>;
|
|
46
53
|
/**
|
|
47
54
|
* Resolves a coding agent's profile selection to an immutable worker
|
|
48
55
|
* image digest, once, at dispatch time (src/core/dispatch.ts) — never
|
|
@@ -85,5 +92,10 @@ export interface Executor {
|
|
|
85
92
|
* says so (Kubernetes and Docker, for Codex and Claude Code).
|
|
86
93
|
*/
|
|
87
94
|
supportsCodingServices?(provider: CodingProvider): boolean;
|
|
95
|
+
/**
|
|
96
|
+
* Whether this executor can run sandbox-mode native runs (a native sandbox executor is composed
|
|
97
|
+
* in). MCP refuses to set an agent to sandbox mode where it cannot (docs/native-sandbox.md).
|
|
98
|
+
*/
|
|
99
|
+
supportsNativeSandbox?(): boolean;
|
|
88
100
|
}
|
|
89
101
|
import type { CodingProvider } from "../../coding/provider.js";
|
|
@@ -7,7 +7,7 @@ import { dirname, isAbsolute, join, resolve, sep } from "node:path";
|
|
|
7
7
|
import { logger } from "../../core/logger.js";
|
|
8
8
|
import { ensurePrivateDirectory } from "../../core/private-directory.js";
|
|
9
9
|
import { readBoundedRegularFile } from "../../coding-worker/artifact.js";
|
|
10
|
-
import { parseCodingAgentOutputJson, redactAndTruncate, MAX_CODING_ARTIFACT_BYTES, MAX_CODING_OUTPUT_ISSUES, SAFE_CODING_OUTPUT_ISSUE, } from "../../coding/protocol.js";
|
|
10
|
+
import { parseCodingAgentOutputJson, redactAndTruncate, MAX_CODING_ARTIFACT_BYTES, MAX_CODING_INPUT_BYTES, MAX_CODING_OUTPUT_ISSUES, SAFE_CODING_OUTPUT_ISSUE, } from "../../coding/protocol.js";
|
|
11
11
|
import { assertDockerHostSupportsIsolation, assertClaudeAgentContainerInspection, assertClaudeToolRunnerContainerInspection, assertIsolationNetworkInspection, assertKeeperContainerInspection, assertNetworkKeeperContainerInspection, assertProxyContainerInspection, assertStorageVolumeInspection, assertWorkerContainerInspection, buildDockerIsolationPlan, CLAUDE_TOOL_SETUP_ENV, dockerSupportsServicesFor, } from "./docker-isolation.js";
|
|
12
12
|
import { serviceUnreadyError } from "../../coding/services/wording.js";
|
|
13
13
|
import { assertServiceContainerInspection, buildDockerServicePlan, dockerServiceContainerNames, } from "./docker-services.js";
|
|
@@ -272,7 +272,7 @@ export class NodeDockerArtifactTransfer {
|
|
|
272
272
|
}
|
|
273
273
|
async seedInput(sourceFile, container) {
|
|
274
274
|
const metadata = await lstat(sourceFile);
|
|
275
|
-
if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.size >
|
|
275
|
+
if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.size > MAX_CODING_INPUT_BYTES) {
|
|
276
276
|
throw new Error("docker_input_artifact_invalid");
|
|
277
277
|
}
|
|
278
278
|
const docker = new NodeDockerCommandRunner({
|
|
@@ -1174,7 +1174,7 @@ export class DockerJobLauncher {
|
|
|
1174
1174
|
}
|
|
1175
1175
|
async validateInputArtifact(path) {
|
|
1176
1176
|
const metadata = await lstat(path);
|
|
1177
|
-
if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.size >
|
|
1177
|
+
if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.size > MAX_CODING_INPUT_BYTES) {
|
|
1178
1178
|
throw new Error("docker_input_artifact_invalid");
|
|
1179
1179
|
}
|
|
1180
1180
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { V1ConfigMap, V1Endpoints, V1NetworkPolicy, V1Pod, V1ResourceQuota, V1Secret, V1Service } from "@kubernetes/client-node";
|
|
1
|
+
import type { V1ConfigMap, V1Deployment, V1Endpoints, V1NetworkPolicy, V1Pod, V1ResourceQuota, V1Secret, V1Service } from "@kubernetes/client-node";
|
|
2
2
|
import type { Readable, Writable } from "node:stream";
|
|
3
3
|
import { type KubernetesApi, type KubernetesExecOptions } from "./kubernetes-api.js";
|
|
4
4
|
export interface FakeExecCall {
|
|
@@ -10,7 +10,7 @@ export interface FakeExecCall {
|
|
|
10
10
|
stdout?: Writable;
|
|
11
11
|
timeoutMs: number;
|
|
12
12
|
}
|
|
13
|
-
type Kind = "configmap" | "secret" | "pod" | "networkpolicy" | "service" | "endpoints" | "resourcequota";
|
|
13
|
+
type Kind = "configmap" | "secret" | "pod" | "networkpolicy" | "service" | "endpoints" | "resourcequota" | "deployment";
|
|
14
14
|
type Obj = {
|
|
15
15
|
metadata?: {
|
|
16
16
|
name?: string;
|
|
@@ -31,6 +31,17 @@ export declare class FakeKubernetesApi implements KubernetesApi {
|
|
|
31
31
|
onExec: (call: FakeExecCall) => Promise<number>;
|
|
32
32
|
/** The admission chain a dry-run create runs through. Default: echo the pod back unchanged. */
|
|
33
33
|
onDryRunCreatePod: (namespace: string, body: V1Pod) => Promise<V1Pod>;
|
|
34
|
+
readonly deploymentPatches: Array<{
|
|
35
|
+
name: string;
|
|
36
|
+
replicas?: number;
|
|
37
|
+
annotations?: Record<string, string>;
|
|
38
|
+
}>;
|
|
39
|
+
/** Deployment names whose patch throws KubernetesForbiddenError. */
|
|
40
|
+
forbidden: Set<string>;
|
|
41
|
+
/** "deployment/<name>" or "endpoints/<name>" keys whose read throws KubernetesForbiddenError. */
|
|
42
|
+
forbiddenReads: Set<string>;
|
|
43
|
+
/** "verb/resource/name" keys canI answers false for. */
|
|
44
|
+
accessDenied: Set<string>;
|
|
34
45
|
logs: Map<string, string>;
|
|
35
46
|
/** What readApiServerVersion reports; tests that care about provenance set it. */
|
|
36
47
|
apiServerVersion: string;
|
|
@@ -85,6 +96,8 @@ export declare class FakeKubernetesApi implements KubernetesApi {
|
|
|
85
96
|
*/
|
|
86
97
|
dryRunCreatePod(namespace: string, body: V1Pod): Promise<V1Pod>;
|
|
87
98
|
readPod(namespace: string, name: string): Promise<V1Pod | undefined>;
|
|
99
|
+
/** Matches `key=value[,key=value]` selectors against stored pods' labels. */
|
|
100
|
+
listPods(namespace: string, labelSelector: string): Promise<V1Pod[]>;
|
|
88
101
|
deletePod(namespace: string, name: string, gracePeriodSeconds: number): Promise<void>;
|
|
89
102
|
createNetworkPolicy(namespace: string, body: V1NetworkPolicy): Promise<V1NetworkPolicy>;
|
|
90
103
|
readNetworkPolicy(namespace: string, name: string): Promise<V1NetworkPolicy | undefined>;
|
|
@@ -92,6 +105,12 @@ export declare class FakeKubernetesApi implements KubernetesApi {
|
|
|
92
105
|
readService(namespace: string, name: string): Promise<V1Service | undefined>;
|
|
93
106
|
readEndpoints(namespace: string, name: string): Promise<V1Endpoints | undefined>;
|
|
94
107
|
readResourceQuota(namespace: string, name: string): Promise<V1ResourceQuota | undefined>;
|
|
108
|
+
readDeployment(namespace: string, name: string): Promise<V1Deployment | undefined>;
|
|
109
|
+
patchDeployment(namespace: string, name: string, patch: {
|
|
110
|
+
replicas?: number;
|
|
111
|
+
annotations?: Record<string, string>;
|
|
112
|
+
}): Promise<void>;
|
|
113
|
+
canI(_namespace: string, verb: string, _group: string, resource: string, name: string): Promise<boolean>;
|
|
95
114
|
readApiServerVersion(): Promise<string>;
|
|
96
115
|
readNamespace(name: string): Promise<boolean>;
|
|
97
116
|
exec(namespace: string, pod: string, container: string, command: string[], options: KubernetesExecOptions): Promise<number>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { KubernetesAlreadyExistsError, KubernetesConflictError, KubernetesNotFoundError, } from "./kubernetes-api.js";
|
|
1
|
+
import { KubernetesAlreadyExistsError, KubernetesConflictError, KubernetesForbiddenError, KubernetesNotFoundError, } from "./kubernetes-api.js";
|
|
2
2
|
/** In-memory KubernetesApi for unit tests. Tests drive pod status and exec behavior directly. */
|
|
3
3
|
export class FakeKubernetesApi {
|
|
4
4
|
objects = new Map();
|
|
@@ -13,6 +13,13 @@ export class FakeKubernetesApi {
|
|
|
13
13
|
};
|
|
14
14
|
/** The admission chain a dry-run create runs through. Default: echo the pod back unchanged. */
|
|
15
15
|
onDryRunCreatePod = async (_namespace, body) => body;
|
|
16
|
+
deploymentPatches = [];
|
|
17
|
+
/** Deployment names whose patch throws KubernetesForbiddenError. */
|
|
18
|
+
forbidden = new Set();
|
|
19
|
+
/** "deployment/<name>" or "endpoints/<name>" keys whose read throws KubernetesForbiddenError. */
|
|
20
|
+
forbiddenReads = new Set();
|
|
21
|
+
/** "verb/resource/name" keys canI answers false for. */
|
|
22
|
+
accessDenied = new Set();
|
|
16
23
|
logs = new Map();
|
|
17
24
|
/** What readApiServerVersion reports; tests that care about provenance set it. */
|
|
18
25
|
apiServerVersion = "v1.33.0";
|
|
@@ -95,6 +102,14 @@ export class FakeKubernetesApi {
|
|
|
95
102
|
async readPod(namespace, name) {
|
|
96
103
|
return this.read("pod", namespace, name);
|
|
97
104
|
}
|
|
105
|
+
/** Matches `key=value[,key=value]` selectors against stored pods' labels. */
|
|
106
|
+
async listPods(namespace, labelSelector) {
|
|
107
|
+
const wanted = labelSelector.split(",").map((pair) => pair.split("="));
|
|
108
|
+
return [...this.objects.entries()]
|
|
109
|
+
.filter(([key]) => key.startsWith(`pod/${namespace}/`))
|
|
110
|
+
.map(([, pod]) => structuredClone(pod))
|
|
111
|
+
.filter((pod) => wanted.every(([k, v]) => pod.metadata?.labels?.[k] === v));
|
|
112
|
+
}
|
|
98
113
|
async deletePod(namespace, name, gracePeriodSeconds) {
|
|
99
114
|
this.deletedPods.push({ name, gracePeriodSeconds });
|
|
100
115
|
this.delete("pod", namespace, name);
|
|
@@ -112,11 +127,37 @@ export class FakeKubernetesApi {
|
|
|
112
127
|
return this.read("service", namespace, name);
|
|
113
128
|
}
|
|
114
129
|
async readEndpoints(namespace, name) {
|
|
130
|
+
if (this.forbiddenReads.has(`endpoints/${name}`))
|
|
131
|
+
throw new KubernetesForbiddenError(this.key("endpoints", namespace, name));
|
|
115
132
|
return this.read("endpoints", namespace, name);
|
|
116
133
|
}
|
|
117
134
|
async readResourceQuota(namespace, name) {
|
|
118
135
|
return this.read("resourcequota", namespace, name);
|
|
119
136
|
}
|
|
137
|
+
async readDeployment(namespace, name) {
|
|
138
|
+
if (this.forbiddenReads.has(`deployment/${name}`))
|
|
139
|
+
throw new KubernetesForbiddenError(this.key("deployment", namespace, name));
|
|
140
|
+
return this.read("deployment", namespace, name);
|
|
141
|
+
}
|
|
142
|
+
async patchDeployment(namespace, name, patch) {
|
|
143
|
+
if (this.forbidden.has(name))
|
|
144
|
+
throw new KubernetesForbiddenError(this.key("deployment", namespace, name));
|
|
145
|
+
const key = this.key("deployment", namespace, name);
|
|
146
|
+
const current = this.objects.get(key);
|
|
147
|
+
if (!current)
|
|
148
|
+
throw new KubernetesNotFoundError(key);
|
|
149
|
+
const metadata = { ...current.metadata, resourceVersion: String(++this.version) };
|
|
150
|
+
if (patch.annotations)
|
|
151
|
+
metadata.annotations = { ...current.metadata?.annotations, ...patch.annotations };
|
|
152
|
+
const next = { ...current, metadata };
|
|
153
|
+
if (patch.replicas !== undefined)
|
|
154
|
+
next.spec = { selector: {}, template: {}, ...current.spec, replicas: patch.replicas };
|
|
155
|
+
this.objects.set(key, next);
|
|
156
|
+
this.deploymentPatches.push({ name, replicas: patch.replicas, annotations: patch.annotations });
|
|
157
|
+
}
|
|
158
|
+
async canI(_namespace, verb, _group, resource, name) {
|
|
159
|
+
return !this.accessDenied.has(`${verb}/${resource}/${name}`);
|
|
160
|
+
}
|
|
120
161
|
async readApiServerVersion() {
|
|
121
162
|
return this.apiServerVersion;
|
|
122
163
|
}
|
|
@@ -3,11 +3,15 @@
|
|
|
3
3
|
* so every launcher behavior is unit-tested against FakeKubernetesApi and
|
|
4
4
|
* re-proven against a real cluster by kubernetes.integration.test.ts.
|
|
5
5
|
*/
|
|
6
|
-
import type { V1ConfigMap, V1Endpoints, V1NetworkPolicy, V1Pod, V1ResourceQuota, V1Secret, V1Service } from "@kubernetes/client-node";
|
|
6
|
+
import type { V1ConfigMap, V1Deployment, V1Endpoints, V1NetworkPolicy, V1Pod, V1ResourceQuota, V1Secret, V1Service } from "@kubernetes/client-node";
|
|
7
7
|
import type { Readable, Writable } from "node:stream";
|
|
8
8
|
export declare class KubernetesNotFoundError extends Error {
|
|
9
9
|
constructor(message?: string);
|
|
10
10
|
}
|
|
11
|
+
/** The caller's identity lacks RBAC permission for the request (HTTP 403). */
|
|
12
|
+
export declare class KubernetesForbiddenError extends Error {
|
|
13
|
+
constructor(message?: string, options?: ErrorOptions);
|
|
14
|
+
}
|
|
11
15
|
/** Stale resourceVersion on replace. */
|
|
12
16
|
export declare class KubernetesConflictError extends Error {
|
|
13
17
|
constructor(message?: string);
|
|
@@ -36,6 +40,8 @@ export interface KubernetesApi {
|
|
|
36
40
|
*/
|
|
37
41
|
dryRunCreatePod(namespace: string, body: V1Pod): Promise<V1Pod>;
|
|
38
42
|
readPod(namespace: string, name: string): Promise<V1Pod | undefined>;
|
|
43
|
+
/** Lists pods in one namespace matching a label selector (e.g. the native sandbox janitor's). */
|
|
44
|
+
listPods(namespace: string, labelSelector: string): Promise<V1Pod[]>;
|
|
39
45
|
deletePod(namespace: string, name: string, gracePeriodSeconds: number): Promise<void>;
|
|
40
46
|
createNetworkPolicy(namespace: string, body: V1NetworkPolicy): Promise<V1NetworkPolicy>;
|
|
41
47
|
readNetworkPolicy(namespace: string, name: string): Promise<V1NetworkPolicy | undefined>;
|
|
@@ -44,11 +50,21 @@ export interface KubernetesApi {
|
|
|
44
50
|
/**
|
|
45
51
|
* Reads a Service's Endpoints by name. EndpointSlice is the durable successor to this core/v1
|
|
46
52
|
* resource (migrating to it is a follow-up); Endpoints is read by name so RBAC stays one object wide.
|
|
53
|
+
* Undefined when missing; throws KubernetesForbiddenError on 403.
|
|
47
54
|
*/
|
|
48
55
|
readEndpoints(namespace: string, name: string): Promise<V1Endpoints | undefined>;
|
|
49
56
|
/** Reads one ResourceQuota by name (its status carries hard and used), for the launcher's capacity check. */
|
|
50
57
|
readResourceQuota(namespace: string, name: string): Promise<V1ResourceQuota | undefined>;
|
|
51
58
|
readNamespace(name: string): Promise<boolean>;
|
|
59
|
+
/** Undefined when missing; throws KubernetesForbiddenError on 403. */
|
|
60
|
+
readDeployment(namespace: string, name: string): Promise<V1Deployment | undefined>;
|
|
61
|
+
/** JSON merge patch of metadata.annotations and spec.replicas only. Throws KubernetesForbiddenError on 403. */
|
|
62
|
+
patchDeployment(namespace: string, name: string, patch: {
|
|
63
|
+
replicas?: number;
|
|
64
|
+
annotations?: Record<string, string>;
|
|
65
|
+
}): Promise<void>;
|
|
66
|
+
/** SelfSubjectAccessReview: may the current identity perform `verb` on this one named resource? */
|
|
67
|
+
canI(namespace: string, verb: string, group: string, resource: string, name: string): Promise<boolean>;
|
|
52
68
|
/** Runs a command in a container; resolves with its exit code. Never uses a shell. */
|
|
53
69
|
exec(namespace: string, pod: string, container: string, command: string[], options: KubernetesExecOptions): Promise<number>;
|
|
54
70
|
/**
|
|
@@ -4,6 +4,13 @@ export class KubernetesNotFoundError extends Error {
|
|
|
4
4
|
this.name = "KubernetesNotFoundError";
|
|
5
5
|
}
|
|
6
6
|
}
|
|
7
|
+
/** The caller's identity lacks RBAC permission for the request (HTTP 403). */
|
|
8
|
+
export class KubernetesForbiddenError extends Error {
|
|
9
|
+
constructor(message, options) {
|
|
10
|
+
super(message, options);
|
|
11
|
+
this.name = "KubernetesForbiddenError";
|
|
12
|
+
}
|
|
13
|
+
}
|
|
7
14
|
/** Stale resourceVersion on replace. */
|
|
8
15
|
export class KubernetesConflictError extends Error {
|
|
9
16
|
constructor(message) {
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* method is one client call plus HTTP-status translation into the seam's
|
|
4
4
|
* error classes. Proven against a real cluster by kubernetes.integration.test.ts.
|
|
5
5
|
*/
|
|
6
|
-
import { type V1ConfigMap, type V1Endpoints, type V1ResourceQuota, type V1NetworkPolicy, type V1Pod, type V1Secret, type V1Service } from "@kubernetes/client-node";
|
|
6
|
+
import { type V1ConfigMap, type V1Deployment, type V1Endpoints, type V1ResourceQuota, type V1NetworkPolicy, type V1Pod, type V1Secret, type V1Service } from "@kubernetes/client-node";
|
|
7
7
|
import { type KubernetesApi, type KubernetesExecOptions } from "./kubernetes-api.js";
|
|
8
8
|
export interface ClientNodeKubernetesApiOptions {
|
|
9
9
|
/** kubeconfig context to use; defaults to the current context (or in-cluster credentials). */
|
|
@@ -13,6 +13,8 @@ export declare class ClientNodeKubernetesApi implements KubernetesApi {
|
|
|
13
13
|
private readonly config;
|
|
14
14
|
private readonly core;
|
|
15
15
|
private readonly networking;
|
|
16
|
+
private readonly apps;
|
|
17
|
+
private readonly authz;
|
|
16
18
|
private readonly version;
|
|
17
19
|
constructor(options?: ClientNodeKubernetesApiOptions);
|
|
18
20
|
createConfigMap(namespace: string, body: V1ConfigMap): Promise<V1ConfigMap>;
|
|
@@ -23,6 +25,7 @@ export declare class ClientNodeKubernetesApi implements KubernetesApi {
|
|
|
23
25
|
createPod(namespace: string, body: V1Pod): Promise<V1Pod>;
|
|
24
26
|
dryRunCreatePod(namespace: string, body: V1Pod): Promise<V1Pod>;
|
|
25
27
|
readPod(namespace: string, name: string): Promise<V1Pod | undefined>;
|
|
28
|
+
listPods(namespace: string, labelSelector: string): Promise<V1Pod[]>;
|
|
26
29
|
deletePod(namespace: string, name: string, gracePeriodSeconds: number): Promise<void>;
|
|
27
30
|
createNetworkPolicy(namespace: string, body: V1NetworkPolicy): Promise<V1NetworkPolicy>;
|
|
28
31
|
readNetworkPolicy(namespace: string, name: string): Promise<V1NetworkPolicy | undefined>;
|
|
@@ -32,6 +35,12 @@ export declare class ClientNodeKubernetesApi implements KubernetesApi {
|
|
|
32
35
|
readResourceQuota(namespace: string, name: string): Promise<V1ResourceQuota | undefined>;
|
|
33
36
|
readApiServerVersion(): Promise<string>;
|
|
34
37
|
readNamespace(name: string): Promise<boolean>;
|
|
38
|
+
readDeployment(namespace: string, name: string): Promise<V1Deployment | undefined>;
|
|
39
|
+
patchDeployment(namespace: string, name: string, patch: {
|
|
40
|
+
replicas?: number;
|
|
41
|
+
annotations?: Record<string, string>;
|
|
42
|
+
}): Promise<void>;
|
|
43
|
+
canI(namespace: string, verb: string, group: string, resource: string, name: string): Promise<boolean>;
|
|
35
44
|
exec(namespace: string, pod: string, container: string, command: string[], options: KubernetesExecOptions): Promise<number>;
|
|
36
45
|
readLogTail(namespace: string, pod: string, container: string, tailLines: number, limitBytes: number): Promise<string>;
|
|
37
46
|
}
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
* method is one client call plus HTTP-status translation into the seam's
|
|
4
4
|
* error classes. Proven against a real cluster by kubernetes.integration.test.ts.
|
|
5
5
|
*/
|
|
6
|
-
import { ApiException, CoreV1Api, Exec, KubeConfig, NetworkingV1Api, VersionApi, } from "@kubernetes/client-node";
|
|
6
|
+
import { ApiException, AppsV1Api, AuthorizationV1Api, CoreV1Api, Exec, KubeConfig, NetworkingV1Api, PatchStrategy, setHeaderOptions, VersionApi, } from "@kubernetes/client-node";
|
|
7
7
|
import { Writable } from "node:stream";
|
|
8
|
-
import { KubernetesAlreadyExistsError, KubernetesConflictError, KubernetesNotFoundError, } from "./kubernetes-api.js";
|
|
8
|
+
import { KubernetesAlreadyExistsError, KubernetesConflictError, KubernetesForbiddenError, KubernetesNotFoundError, } from "./kubernetes-api.js";
|
|
9
9
|
function statusCode(error) {
|
|
10
10
|
return error instanceof ApiException ? error.code : undefined;
|
|
11
11
|
}
|
|
@@ -20,6 +20,19 @@ async function readOrUndefined(read) {
|
|
|
20
20
|
throw error;
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
|
+
/** Undefined for a 404; a 403 becomes KubernetesForbiddenError (cause kept); rethrows anything else. */
|
|
24
|
+
async function readOrForbidden(what, read) {
|
|
25
|
+
try {
|
|
26
|
+
return await read();
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
if (statusCode(error) === 404)
|
|
30
|
+
return undefined;
|
|
31
|
+
if (statusCode(error) === 403)
|
|
32
|
+
throw new KubernetesForbiddenError(`forbidden: get ${what}`, { cause: error });
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
23
36
|
/** Swallows a 404 so deletes are idempotent. */
|
|
24
37
|
async function deleteIgnoringMissing(remove) {
|
|
25
38
|
try {
|
|
@@ -69,6 +82,8 @@ export class ClientNodeKubernetesApi {
|
|
|
69
82
|
config;
|
|
70
83
|
core;
|
|
71
84
|
networking;
|
|
85
|
+
apps;
|
|
86
|
+
authz;
|
|
72
87
|
version;
|
|
73
88
|
constructor(options = {}) {
|
|
74
89
|
this.config = new KubeConfig();
|
|
@@ -77,6 +92,8 @@ export class ClientNodeKubernetesApi {
|
|
|
77
92
|
this.config.setCurrentContext(options.context);
|
|
78
93
|
this.core = this.config.makeApiClient(CoreV1Api);
|
|
79
94
|
this.networking = this.config.makeApiClient(NetworkingV1Api);
|
|
95
|
+
this.apps = this.config.makeApiClient(AppsV1Api);
|
|
96
|
+
this.authz = this.config.makeApiClient(AuthorizationV1Api);
|
|
80
97
|
this.version = this.config.makeApiClient(VersionApi);
|
|
81
98
|
}
|
|
82
99
|
createConfigMap(namespace, body) {
|
|
@@ -103,6 +120,9 @@ export class ClientNodeKubernetesApi {
|
|
|
103
120
|
readPod(namespace, name) {
|
|
104
121
|
return readOrUndefined(() => this.core.readNamespacedPod({ namespace, name }));
|
|
105
122
|
}
|
|
123
|
+
async listPods(namespace, labelSelector) {
|
|
124
|
+
return (await this.core.listNamespacedPod({ namespace, labelSelector })).items;
|
|
125
|
+
}
|
|
106
126
|
deletePod(namespace, name, gracePeriodSeconds) {
|
|
107
127
|
return deleteIgnoringMissing(() => this.core.deleteNamespacedPod({ namespace, name, gracePeriodSeconds }));
|
|
108
128
|
}
|
|
@@ -119,7 +139,7 @@ export class ClientNodeKubernetesApi {
|
|
|
119
139
|
return readOrUndefined(() => this.core.readNamespacedService({ namespace, name }));
|
|
120
140
|
}
|
|
121
141
|
readEndpoints(namespace, name) {
|
|
122
|
-
return
|
|
142
|
+
return readOrForbidden(`endpoints/${namespace}/${name}`, () => this.core.readNamespacedEndpoints({ namespace, name }));
|
|
123
143
|
}
|
|
124
144
|
readResourceQuota(namespace, name) {
|
|
125
145
|
return readOrUndefined(() => this.core.readNamespacedResourceQuota({ namespace, name }));
|
|
@@ -130,6 +150,35 @@ export class ClientNodeKubernetesApi {
|
|
|
130
150
|
async readNamespace(name) {
|
|
131
151
|
return (await readOrUndefined(() => this.core.readNamespace({ name }))) !== undefined;
|
|
132
152
|
}
|
|
153
|
+
readDeployment(namespace, name) {
|
|
154
|
+
return readOrForbidden(`deployments/${namespace}/${name}`, () => this.apps.readNamespacedDeployment({ namespace, name }));
|
|
155
|
+
}
|
|
156
|
+
async patchDeployment(namespace, name, patch) {
|
|
157
|
+
const body = {
|
|
158
|
+
...(patch.annotations ? { metadata: { annotations: patch.annotations } } : {}),
|
|
159
|
+
...(patch.replicas !== undefined ? { spec: { replicas: patch.replicas } } : {}),
|
|
160
|
+
};
|
|
161
|
+
try {
|
|
162
|
+
await this.apps.patchNamespacedDeployment({ namespace, name, body }, setHeaderOptions("Content-Type", PatchStrategy.MergePatch));
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
if (statusCode(error) === 403)
|
|
166
|
+
throw new KubernetesForbiddenError(`deployments/${namespace}/${name}`, { cause: error });
|
|
167
|
+
if (statusCode(error) === 404)
|
|
168
|
+
throw new KubernetesNotFoundError(`deployments/${namespace}/${name}`);
|
|
169
|
+
throw error;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
async canI(namespace, verb, group, resource, name) {
|
|
173
|
+
const review = await this.authz.createSelfSubjectAccessReview({
|
|
174
|
+
body: {
|
|
175
|
+
apiVersion: "authorization.k8s.io/v1",
|
|
176
|
+
kind: "SelfSubjectAccessReview",
|
|
177
|
+
spec: { resourceAttributes: { namespace, verb, group, resource, name } },
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
return review.status?.allowed === true;
|
|
181
|
+
}
|
|
133
182
|
exec(namespace, pod, container, command, options) {
|
|
134
183
|
// stderr is requested but discarded so it never reaches wardby's logs.
|
|
135
184
|
const stderr = new Writable({
|
|
@@ -14,7 +14,11 @@ export interface DryRunFixture {
|
|
|
14
14
|
notes: string[];
|
|
15
15
|
mutations: PodMutation[];
|
|
16
16
|
}
|
|
17
|
-
|
|
17
|
+
declare const FIXTURE_URLS: {
|
|
18
|
+
readonly "gke-autopilot": import("url").URL;
|
|
19
|
+
readonly eks: import("url").URL;
|
|
20
|
+
};
|
|
21
|
+
export declare function loadDryRunFixture(platform?: keyof typeof FIXTURE_URLS): DryRunFixture;
|
|
18
22
|
/**
|
|
19
23
|
* Returns a copy of `document` with every mutation applied in order.
|
|
20
24
|
*
|
|
@@ -47,3 +51,4 @@ export declare function applyMutations<T>(document: T, mutations: readonly PodMu
|
|
|
47
51
|
* `/spec/Foo` and `/spec/foo` by tie-break rules that differ between hosts).
|
|
48
52
|
*/
|
|
49
53
|
export declare function diffMutations(before: unknown, after: unknown, prefix?: string): PodMutation[];
|
|
54
|
+
export {};
|
|
@@ -33,9 +33,12 @@
|
|
|
33
33
|
*/
|
|
34
34
|
import { readFileSync } from "node:fs";
|
|
35
35
|
import { canonical } from "./kubernetes-isolation.js";
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
const FIXTURE_URLS = {
|
|
37
|
+
"gke-autopilot": new URL("./fixtures/gke-autopilot-dry-run.json", import.meta.url),
|
|
38
|
+
eks: new URL("./fixtures/eks-dry-run.json", import.meta.url),
|
|
39
|
+
};
|
|
40
|
+
export function loadDryRunFixture(platform = "gke-autopilot") {
|
|
41
|
+
return JSON.parse(readFileSync(FIXTURE_URLS[platform], "utf8"));
|
|
39
42
|
}
|
|
40
43
|
function segments(path) {
|
|
41
44
|
if (path === "" || !path.startsWith("/"))
|