@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,4 +1,4 @@
|
|
|
1
|
-
import type { V1NetworkPolicy, V1Pod, V1Secret } from "@kubernetes/client-node";
|
|
1
|
+
import type { V1Container, V1NetworkPolicy, V1Pod, V1Secret } from "@kubernetes/client-node";
|
|
2
2
|
import { type ResolvedCodingService } from "../../coding/services/catalog.js";
|
|
3
3
|
import type { JobSpec } from "./types.js";
|
|
4
4
|
import { type KubernetesPlatform } from "./kubernetes-platform.js";
|
|
@@ -81,6 +81,8 @@ export interface RunPodOptions {
|
|
|
81
81
|
export declare function buildRunPod(spec: JobSpec, options: RunPodOptions): V1Pod;
|
|
82
82
|
export declare function buildRunNetworkPolicy(spec: JobSpec, namespace: string): V1NetworkPolicy;
|
|
83
83
|
export declare function buildCapabilitySecret(spec: JobSpec, namespace: string, capability: string, toolSetup?: string): V1Secret;
|
|
84
|
+
/** A copy of a container's resources with every quantity in one rendering ("1" and "1000m" alike); see normalizeResources. */
|
|
85
|
+
export declare function canonicalResources(r?: V1Container["resources"]): V1Container["resources"];
|
|
84
86
|
/**
|
|
85
87
|
* A copy of `pod` with ONLY the API server's own defaulting and aliasing undone
|
|
86
88
|
* — the same `normalizeSpec` pass `assertRunPodMatches` runs, and nothing else.
|
|
@@ -561,6 +561,12 @@ function normalizeResources(r) {
|
|
|
561
561
|
bag["ephemeral-storage"] = memoryBytes(bag["ephemeral-storage"]);
|
|
562
562
|
}
|
|
563
563
|
}
|
|
564
|
+
/** A copy of a container's resources with every quantity in one rendering ("1" and "1000m" alike); see normalizeResources. */
|
|
565
|
+
export function canonicalResources(r) {
|
|
566
|
+
const copy = r ? JSON.parse(JSON.stringify(r)) : undefined;
|
|
567
|
+
normalizeResources(copy);
|
|
568
|
+
return copy;
|
|
569
|
+
}
|
|
564
570
|
function normalizeProbe(p) {
|
|
565
571
|
if (!p)
|
|
566
572
|
return;
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
* regresses to a documentation-only fixture again.
|
|
30
30
|
*/
|
|
31
31
|
import type { V1PodSpec, V1Toleration } from "@kubernetes/client-node";
|
|
32
|
-
export type KubernetesPlatform = "generic" | "gke-autopilot";
|
|
32
|
+
export type KubernetesPlatform = "generic" | "gke-autopilot" | "eks";
|
|
33
33
|
export declare const KUBERNETES_PLATFORMS: readonly KubernetesPlatform[];
|
|
34
34
|
export declare const KUBERNETES_PLATFORM_ERROR = "kubernetes_platform_unconformable";
|
|
35
35
|
/** A request the platform cannot legally run, refused here instead of being silently adjusted by the platform. */
|
|
@@ -84,6 +84,9 @@ export interface KubernetesPlatformProfile {
|
|
|
84
84
|
/** gVisor is mandatory: an unset or different runtime class is refused at startup, not warned about. */
|
|
85
85
|
requiresGvisor: boolean;
|
|
86
86
|
}
|
|
87
|
+
/** Node label and taint key of the EKS sandbox node group; deploy/aws and overlays/eks use the same strings. */
|
|
88
|
+
export declare const EKS_SANDBOX_NODE_LABEL = "wardby.dev/sandbox";
|
|
89
|
+
export declare const EKS_SANDBOX_TAINT_KEY = "sandbox";
|
|
87
90
|
export declare function platformProfile(name: KubernetesPlatform): KubernetesPlatformProfile;
|
|
88
91
|
export interface ContainerResourceRequest {
|
|
89
92
|
cpuMillicores: number;
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
export const KUBERNETES_PLATFORMS = Object.freeze([
|
|
1
|
+
export const KUBERNETES_PLATFORMS = Object.freeze([
|
|
2
|
+
"generic",
|
|
3
|
+
"gke-autopilot",
|
|
4
|
+
"eks",
|
|
5
|
+
]);
|
|
2
6
|
export const KUBERNETES_PLATFORM_ERROR = "kubernetes_platform_unconformable";
|
|
3
7
|
/** A request the platform cannot legally run, refused here instead of being silently adjusted by the platform. */
|
|
4
8
|
export class KubernetesPlatformError extends Error {
|
|
@@ -109,9 +113,42 @@ const GKE_AUTOPILOT = {
|
|
|
109
113
|
},
|
|
110
114
|
requiresGvisor: true,
|
|
111
115
|
};
|
|
116
|
+
/** Node label and taint key of the EKS sandbox node group; deploy/aws and overlays/eks use the same strings. */
|
|
117
|
+
export const EKS_SANDBOX_NODE_LABEL = "wardby.dev/sandbox";
|
|
118
|
+
export const EKS_SANDBOX_TAINT_KEY = "sandbox";
|
|
119
|
+
/**
|
|
120
|
+
* Amazon EKS with a dedicated gVisor node group (deploy/aws). EKS has no managed sandbox runtime, so
|
|
121
|
+
* the node group's launch template installs runsc and the overlay's `gvisor` RuntimeClass schedules
|
|
122
|
+
* onto it: the RuntimeClass admission plugin merges its `scheduling` block into the pod, which is
|
|
123
|
+
* the toleration and nodeSelector below — wardby never sets either itself (see SYMMETRY CAVEAT).
|
|
124
|
+
*
|
|
125
|
+
* PROVISIONAL: written from the RuntimeClass spec, not yet a server-side dry-run capture. EKS has no
|
|
126
|
+
* Autopilot-style resource admission, so resources are the generic rules plus explicit ephemeral
|
|
127
|
+
* storage (kubelet eviction accounting on a shared sandbox node). Replace the fixture with a real
|
|
128
|
+
* capture before relying on the allowance list (fixtures/eks-dry-run.json).
|
|
129
|
+
*/
|
|
130
|
+
const EKS = {
|
|
131
|
+
name: "eks",
|
|
132
|
+
resources: {
|
|
133
|
+
cpuFloorMillicores: 0,
|
|
134
|
+
cpuIncrementMillicores: 1,
|
|
135
|
+
memoryPerCpuMinMib: 0,
|
|
136
|
+
memoryPerCpuMaxMib: Number.POSITIVE_INFINITY,
|
|
137
|
+
explicitEphemeralStorage: true,
|
|
138
|
+
},
|
|
139
|
+
metadata: {
|
|
140
|
+
podAnnotationKeyPrefixes: [],
|
|
141
|
+
podLabelKeyPrefixes: [],
|
|
142
|
+
tolerations: [{ key: EKS_SANDBOX_TAINT_KEY, operator: "Equal", value: GVISOR_RUNTIME_CLASS, effect: "NoSchedule" }],
|
|
143
|
+
nodeSelector: { [EKS_SANDBOX_NODE_LABEL]: GVISOR_RUNTIME_CLASS },
|
|
144
|
+
dropsPodSeccompProfile: false,
|
|
145
|
+
},
|
|
146
|
+
requiresGvisor: true,
|
|
147
|
+
};
|
|
112
148
|
const PROFILES = Object.freeze({
|
|
113
149
|
generic: freezeProfile(GENERIC),
|
|
114
150
|
"gke-autopilot": freezeProfile(GKE_AUTOPILOT),
|
|
151
|
+
eks: freezeProfile(EKS),
|
|
115
152
|
});
|
|
116
153
|
export function platformProfile(name) {
|
|
117
154
|
const profile = PROFILES[name];
|
|
@@ -40,7 +40,8 @@ export interface CanaryResult {
|
|
|
40
40
|
* probe (`enforcementProbeScript`), which had to make the same distinction after a live cluster
|
|
41
41
|
* with no NetworkPolicy at all passed a boolean version of this check; preflight and gate must
|
|
42
42
|
* agree on what counts as evidence, or one of them is vacuous. `internet`, `metadata` and `proxy`
|
|
43
|
-
* stay plain reachability, where "connected" is the only thing that matters
|
|
43
|
+
* stay plain reachability, where "connected" is the only thing that matters; `metadata` is true if
|
|
44
|
+
* **any** cloud metadata endpoint connects.
|
|
44
45
|
*
|
|
45
46
|
* CNIs program a new pod's policy a few seconds after it starts, so the script first waits (up to
|
|
46
47
|
* 20 s) for the deny-port connect to be *dropped*; if it never is, `proxyDeny` reports true.
|
|
@@ -11,6 +11,7 @@ import { randomBytes } from "node:crypto";
|
|
|
11
11
|
import { CODING_PROXY_DENY_PORT, CODING_PROXY_PORT } from "./docker-isolation.js";
|
|
12
12
|
import { KUBERNETES_ISOLATION_ERROR, WORKER_CONTAINER, buildRunNetworkPolicy, buildRunPod, isRegistryDigest, } from "./kubernetes-isolation.js";
|
|
13
13
|
import { KubernetesPlatformError, assertPlatformConfig, platformProfile } from "./kubernetes-platform.js";
|
|
14
|
+
import { CLOUD_METADATA_PROBES } from "./metadata-probes.js";
|
|
14
15
|
import { readProxyWitness } from "./kubernetes-witness.js";
|
|
15
16
|
/**
|
|
16
17
|
* The image the preflight canary runs (it only needs node): the Codex worker when configured,
|
|
@@ -40,7 +41,8 @@ const CANARY_KEYS = Object.keys(EXPECTED).sort();
|
|
|
40
41
|
* probe (`enforcementProbeScript`), which had to make the same distinction after a live cluster
|
|
41
42
|
* with no NetworkPolicy at all passed a boolean version of this check; preflight and gate must
|
|
42
43
|
* agree on what counts as evidence, or one of them is vacuous. `internet`, `metadata` and `proxy`
|
|
43
|
-
* stay plain reachability, where "connected" is the only thing that matters
|
|
44
|
+
* stay plain reachability, where "connected" is the only thing that matters; `metadata` is true if
|
|
45
|
+
* **any** cloud metadata endpoint connects.
|
|
44
46
|
*
|
|
45
47
|
* CNIs program a new pod's policy a few seconds after it starts, so the script first waits (up to
|
|
46
48
|
* 20 s) for the deny-port connect to be *dropped*; if it never is, `proxyDeny` reports true.
|
|
@@ -81,7 +83,7 @@ export const CANARY_SCRIPT = [
|
|
|
81
83
|
" ),",
|
|
82
84
|
` proxyDeny: !(await dropped(process.env.WARDBY_CANARY_PROXY_IP, ${CODING_PROXY_DENY_PORT})),`,
|
|
83
85
|
' internet: await reachable("1.1.1.1", 443),',
|
|
84
|
-
|
|
86
|
+
` metadata: (await Promise.all(${JSON.stringify(CLOUD_METADATA_PROBES.map((p) => [p.host, p.port]))}.map(([host, port]) => reachable(host, port)))).some(Boolean),`,
|
|
85
87
|
` proxy: await reachable(process.env.WARDBY_CANARY_PROXY_IP, ${CODING_PROXY_PORT}),`,
|
|
86
88
|
" };",
|
|
87
89
|
" console.log(JSON.stringify({ wardbyCanary }));",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type Readable } from "node:stream";
|
|
2
2
|
import type { KubernetesJobConfig } from "../../config/providers.js";
|
|
3
|
+
import { type Power } from "../../core/power.js";
|
|
3
4
|
import { type KubernetesApi } from "./kubernetes-api.js";
|
|
4
5
|
import { type ServiceStateReporter } from "./service-state.js";
|
|
5
6
|
import type { CodingProvider } from "../../coding/provider.js";
|
|
@@ -19,6 +20,11 @@ export interface KubernetesJobLauncherOptions {
|
|
|
19
20
|
* the gate probes the address `provision` re-reads for that launch, not this memoized one.
|
|
20
21
|
*/
|
|
21
22
|
preflight?: () => Promise<KubernetesClusterInfo | void>;
|
|
23
|
+
/**
|
|
24
|
+
* Low-power mode: `launch()` wakes the (possibly scaled-to-zero) coding proxy before its preflight,
|
|
25
|
+
* and `warmUp()` defers the preflight while the proxy is asleep. Default `DISABLED_POWER` (no-op).
|
|
26
|
+
*/
|
|
27
|
+
power?: Power;
|
|
22
28
|
now?: () => number;
|
|
23
29
|
sleep?: (milliseconds: number) => Promise<void>;
|
|
24
30
|
readyTimeoutMs?: number;
|
|
@@ -67,6 +73,7 @@ export declare class KubernetesJobLauncher implements WorkspaceJobLauncher {
|
|
|
67
73
|
/** Timeout of one streak exec: ENFORCEMENT_BLOCKED_STREAK probes plus the gaps between them. */
|
|
68
74
|
private readonly streakExecTimeoutMs;
|
|
69
75
|
private preflightResult?;
|
|
76
|
+
private readonly power;
|
|
70
77
|
constructor(options: KubernetesJobLauncherOptions);
|
|
71
78
|
/**
|
|
72
79
|
* Services run as native sidecars in the run pod (kubernetes-isolation.ts), for either provider:
|
|
@@ -94,7 +101,35 @@ export declare class KubernetesJobLauncher implements WorkspaceJobLauncher {
|
|
|
94
101
|
materializeWorkspace(handle: JobHandle, destination: string): Promise<void>;
|
|
95
102
|
stop(handle: JobHandle, _reason?: string): Promise<void>;
|
|
96
103
|
remove(handle: JobHandle): Promise<void>;
|
|
97
|
-
/**
|
|
104
|
+
/**
|
|
105
|
+
* Low-power mode: whether the proxy is scaled to zero. A failed check is logged by code only (the
|
|
106
|
+
* leading reason code, else the error's class name -- never the raw message, which could carry
|
|
107
|
+
* cluster detail) and counts as awake, so warm-up runs the preflight as it would without the mode.
|
|
108
|
+
*/
|
|
109
|
+
private proxyAsleep;
|
|
110
|
+
/**
|
|
111
|
+
* Starts the memoized cluster preflight (`runPreflight`) right away, instead of waiting for the
|
|
112
|
+
* first `launch()`, so a long-running server process's own start-up pays the preflight's cost (a
|
|
113
|
+
* canary pod on `kind`: ~20s) rather than that process's first coding run. Never throws: it only
|
|
114
|
+
* reports the outcome (an info line on success, a warning naming the failure code otherwise) so
|
|
115
|
+
* an operator sees a misconfigured cluster at boot. Any `launch()` already awaiting this same
|
|
116
|
+
* attempt (concurrent with this call) still fails exactly as it does today when the preflight
|
|
117
|
+
* failed — it shares the same rejected promise. But unlike a preflight `launch()` starts on its
|
|
118
|
+
* own, a start-up failure here is **not** left stuck memoized: a transient cluster problem at
|
|
119
|
+
* process start must not fail every coding run until restart, so the failed attempt is cleared
|
|
120
|
+
* (only if nothing has already started a newer one — compared by identity, never by state) and
|
|
121
|
+
* the *next* `launch()` runs the preflight fresh. A successful warm-up stays memoized as always.
|
|
122
|
+
* In low-power mode, while the proxy is asleep, warm-up is skipped (the memo stays unset) rather
|
|
123
|
+
* than waking the proxy only for the preflight; the first `launch()` wakes it and runs the preflight.
|
|
124
|
+
*/
|
|
125
|
+
warmUp(): Promise<void>;
|
|
126
|
+
/**
|
|
127
|
+
* Memoized: the preflight (or, without one, a single proxy-witness read) runs once per launcher —
|
|
128
|
+
* including its failure, which stays memoized and fails every subsequent `launch()` immediately,
|
|
129
|
+
* for a preflight `launch()` itself started (there was no `warmUp()`, or `warmUp()`'s own failed
|
|
130
|
+
* attempt was already cleared). `warmUp()` is the one caller that clears a failed attempt instead
|
|
131
|
+
* of leaving it stuck; see its own doc comment.
|
|
132
|
+
*/
|
|
98
133
|
private runPreflight;
|
|
99
134
|
/**
|
|
100
135
|
* Waits until the run's NetworkPolicy is enforced on this pod: the keeper (same network namespace
|
|
@@ -29,7 +29,8 @@ import { finished } from "node:stream/promises";
|
|
|
29
29
|
import { createHash } from "node:crypto";
|
|
30
30
|
import { isIP } from "node:net";
|
|
31
31
|
import { logger } from "../../core/logger.js";
|
|
32
|
-
import {
|
|
32
|
+
import { DISABLED_POWER, wakeTimeoutMs } from "../../core/power.js";
|
|
33
|
+
import { MAX_CODING_ARTIFACT_BYTES, MAX_CODING_INPUT_BYTES, MAX_DEBUG_TRACE_LINE_BYTES, parseCodingAgentOutputJson, } from "../../coding/protocol.js";
|
|
33
34
|
import { CODING_PROXY_DENY_PORT, CODING_PROXY_PORT } from "./docker-isolation.js";
|
|
34
35
|
import { parseWorkerDiagnosticLine } from "./docker.js";
|
|
35
36
|
import { KubernetesAlreadyExistsError, KubernetesConflictError } from "./kubernetes-api.js";
|
|
@@ -386,9 +387,11 @@ export class KubernetesJobLauncher {
|
|
|
386
387
|
/** Timeout of one streak exec: ENFORCEMENT_BLOCKED_STREAK probes plus the gaps between them. */
|
|
387
388
|
streakExecTimeoutMs;
|
|
388
389
|
preflightResult;
|
|
390
|
+
power;
|
|
389
391
|
constructor(options) {
|
|
390
392
|
this.options = options;
|
|
391
393
|
this.api = options.api;
|
|
394
|
+
this.power = options.power ?? DISABLED_POWER;
|
|
392
395
|
this.namespace = options.config.namespace;
|
|
393
396
|
this.workspaceRoot = resolve(options.workspaceRoot);
|
|
394
397
|
if (this.workspaceRoot === resolve("/"))
|
|
@@ -495,6 +498,11 @@ export class KubernetesJobLauncher {
|
|
|
495
498
|
this.warn(`kubernetes_spec_rejected: ${error.message}`);
|
|
496
499
|
throw error;
|
|
497
500
|
}
|
|
501
|
+
// Low-power mode: the proxy may be scaled to zero; wake it before the preflight reads it. Guarded
|
|
502
|
+
// (not just a no-op await) so mode off keeps launch() joining an in-flight warm-up synchronously.
|
|
503
|
+
if (this.power.enabled) {
|
|
504
|
+
await this.power.ensureAwake("proxy", { timeoutMs: wakeTimeoutMs(this.readyTimeoutMs), runId: spec.runId });
|
|
505
|
+
}
|
|
498
506
|
await this.runPreflight();
|
|
499
507
|
const names = kubernetesRunNames(spec.runId);
|
|
500
508
|
const handle = { backend: BACKEND, id: `${this.namespace}/${names.token}` };
|
|
@@ -618,7 +626,69 @@ export class KubernetesJobLauncher {
|
|
|
618
626
|
}
|
|
619
627
|
// -------------------------------------------------------------------------
|
|
620
628
|
// Launch
|
|
621
|
-
/**
|
|
629
|
+
/**
|
|
630
|
+
* Low-power mode: whether the proxy is scaled to zero. A failed check is logged by code only (the
|
|
631
|
+
* leading reason code, else the error's class name -- never the raw message, which could carry
|
|
632
|
+
* cluster detail) and counts as awake, so warm-up runs the preflight as it would without the mode.
|
|
633
|
+
*/
|
|
634
|
+
async proxyAsleep() {
|
|
635
|
+
try {
|
|
636
|
+
return await this.power.isAsleep("proxy");
|
|
637
|
+
}
|
|
638
|
+
catch (error) {
|
|
639
|
+
const code = (error instanceof Error && /^[a-z][a-z0-9_]*(?=:)/.exec(error.message)?.[0]) ||
|
|
640
|
+
(error instanceof Error ? error.name : "unknown");
|
|
641
|
+
this.warn(`kubernetes_preflight_power_check_failed: ${code}`);
|
|
642
|
+
return false;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* Starts the memoized cluster preflight (`runPreflight`) right away, instead of waiting for the
|
|
647
|
+
* first `launch()`, so a long-running server process's own start-up pays the preflight's cost (a
|
|
648
|
+
* canary pod on `kind`: ~20s) rather than that process's first coding run. Never throws: it only
|
|
649
|
+
* reports the outcome (an info line on success, a warning naming the failure code otherwise) so
|
|
650
|
+
* an operator sees a misconfigured cluster at boot. Any `launch()` already awaiting this same
|
|
651
|
+
* attempt (concurrent with this call) still fails exactly as it does today when the preflight
|
|
652
|
+
* failed — it shares the same rejected promise. But unlike a preflight `launch()` starts on its
|
|
653
|
+
* own, a start-up failure here is **not** left stuck memoized: a transient cluster problem at
|
|
654
|
+
* process start must not fail every coding run until restart, so the failed attempt is cleared
|
|
655
|
+
* (only if nothing has already started a newer one — compared by identity, never by state) and
|
|
656
|
+
* the *next* `launch()` runs the preflight fresh. A successful warm-up stays memoized as always.
|
|
657
|
+
* In low-power mode, while the proxy is asleep, warm-up is skipped (the memo stays unset) rather
|
|
658
|
+
* than waking the proxy only for the preflight; the first `launch()` wakes it and runs the preflight.
|
|
659
|
+
*/
|
|
660
|
+
async warmUp() {
|
|
661
|
+
if (this.power.enabled && (await this.proxyAsleep())) {
|
|
662
|
+
kubernetesLog.info("kubernetes_preflight_deferred: proxy asleep (low-power mode); the first coding run runs the preflight");
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
const attempt = this.runPreflight();
|
|
666
|
+
try {
|
|
667
|
+
await attempt;
|
|
668
|
+
kubernetesLog.info("kubernetes_preflight_warm_up_succeeded: cluster preflight passed at start-up");
|
|
669
|
+
}
|
|
670
|
+
catch (error) {
|
|
671
|
+
// Only clear the memo if it still holds this exact attempt: a concurrent launch() or warmUp()
|
|
672
|
+
// may have already seen this same rejection and started a fresh one (which must survive), and
|
|
673
|
+
// nothing else could have replaced this attempt with anything but a newer one, since
|
|
674
|
+
// runPreflight() never overwrites an already-set, still-pending-or-resolved promise.
|
|
675
|
+
if (this.preflightResult === attempt)
|
|
676
|
+
this.preflightResult = undefined;
|
|
677
|
+
// Same code-only convention as every other failure this launcher logs (e.g. launch()'s
|
|
678
|
+
// kubernetes_spec_rejected above): the error's own message is the fixed reason code set by
|
|
679
|
+
// runPreflight's errorWithCode (KUBERNETES_ISOLATION_ERROR) — never the raw cause, which could
|
|
680
|
+
// carry cluster detail.
|
|
681
|
+
const code = error instanceof Error ? error.message : String(error);
|
|
682
|
+
this.warn(`kubernetes_preflight_warm_up_failed: ${code}`);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
/**
|
|
686
|
+
* Memoized: the preflight (or, without one, a single proxy-witness read) runs once per launcher —
|
|
687
|
+
* including its failure, which stays memoized and fails every subsequent `launch()` immediately,
|
|
688
|
+
* for a preflight `launch()` itself started (there was no `warmUp()`, or `warmUp()`'s own failed
|
|
689
|
+
* attempt was already cleared). `warmUp()` is the one caller that clears a failed attempt instead
|
|
690
|
+
* of leaving it stuck; see its own doc comment.
|
|
691
|
+
*/
|
|
622
692
|
runPreflight() {
|
|
623
693
|
this.preflightResult ??= (async () => {
|
|
624
694
|
try {
|
|
@@ -862,14 +932,14 @@ export class KubernetesJobLauncher {
|
|
|
862
932
|
const staging = await mkdtemp(join(tmpdir(), "wardby-k8s-input-"));
|
|
863
933
|
try {
|
|
864
934
|
const source = await lstat(inputArtifact);
|
|
865
|
-
if (!source.isFile() || source.size >
|
|
935
|
+
if (!source.isFile() || source.size > MAX_CODING_INPUT_BYTES) {
|
|
866
936
|
throw new Error("kubernetes_input_artifact_invalid");
|
|
867
937
|
}
|
|
868
938
|
const copy = join(staging, "input.json");
|
|
869
939
|
await copyFile(inputArtifact, copy);
|
|
870
940
|
// Re-check the copy: the source could have changed between lstat and copy.
|
|
871
941
|
const copied = await lstat(copy);
|
|
872
|
-
if (!copied.isFile() || copied.size >
|
|
942
|
+
if (!copied.isFile() || copied.size > MAX_CODING_INPUT_BYTES) {
|
|
873
943
|
throw new Error("kubernetes_input_artifact_invalid");
|
|
874
944
|
}
|
|
875
945
|
await this.seedDirectory(names, staging, INPUT_STORAGE, timeoutMs);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloud metadata endpoints a coding or native worker pod must never reach: they hand out node or
|
|
3
|
+
* workload credentials. The instance metadata service (GCE, EC2 IPv4), the EKS Pod Identity agent (IPv4
|
|
4
|
+
* and IPv6), and EC2's IPv6 IMDS endpoint. Probed on port 80, where every one of them serves. A pod without
|
|
5
|
+
* IPv6 fails the last connect immediately, which counts as unreachable — correct either way.
|
|
6
|
+
* src/sandbox/fetch-policy.ts blocks the same addresses for in-process fetches.
|
|
7
|
+
*/
|
|
8
|
+
export declare const CLOUD_METADATA_PROBES: readonly {
|
|
9
|
+
host: string;
|
|
10
|
+
port: number;
|
|
11
|
+
}[];
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloud metadata endpoints a coding or native worker pod must never reach: they hand out node or
|
|
3
|
+
* workload credentials. The instance metadata service (GCE, EC2 IPv4), the EKS Pod Identity agent (IPv4
|
|
4
|
+
* and IPv6), and EC2's IPv6 IMDS endpoint. Probed on port 80, where every one of them serves. A pod without
|
|
5
|
+
* IPv6 fails the last connect immediately, which counts as unreachable — correct either way.
|
|
6
|
+
* src/sandbox/fetch-policy.ts blocks the same addresses for in-process fetches.
|
|
7
|
+
*/
|
|
8
|
+
export const CLOUD_METADATA_PROBES = Object.freeze([
|
|
9
|
+
Object.freeze({ host: "169.254.169.254", port: 80 }),
|
|
10
|
+
Object.freeze({ host: "169.254.170.23", port: 80 }),
|
|
11
|
+
Object.freeze({ host: "fd00:ec2::254", port: 80 }),
|
|
12
|
+
Object.freeze({ host: "fd00:ec2::23", port: 80 }),
|
|
13
|
+
]);
|
|
@@ -88,6 +88,15 @@ export interface JobLauncher {
|
|
|
88
88
|
* itself stays the authority.
|
|
89
89
|
*/
|
|
90
90
|
hasCapacityFor?: (spec: JobSpec) => Promise<boolean>;
|
|
91
|
+
/**
|
|
92
|
+
* Starts any one-time start-up check (e.g. Kubernetes's memoized cluster preflight) right away,
|
|
93
|
+
* instead of waiting for the first `launch()`. Never throws — a failure is only logged — and
|
|
94
|
+
* `launch()` still runs and reports the same check itself, memoized exactly as if this had never
|
|
95
|
+
* been called. Composition roots call this, fire-and-forget, only from a long-running server
|
|
96
|
+
* process's own start-up (`wardby serve`, the MCP server, `wardby scheduler`); never from a
|
|
97
|
+
* one-shot CLI command. Absent on a launcher with nothing to warm up (e.g. Docker).
|
|
98
|
+
*/
|
|
99
|
+
warmUp?: () => Promise<void>;
|
|
91
100
|
launch: (spec: JobSpec) => Promise<JobHandle>;
|
|
92
101
|
status: (handle: JobHandle) => Promise<JobStatus>;
|
|
93
102
|
collect: (handle: JobHandle) => Promise<JobResult>;
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* and revokes it afterwards. See docs/private/2026-09-25-code-review-host-design.md §5.1.
|
|
6
6
|
*/
|
|
7
7
|
import { type GitHubAppClient } from "../vcs/github.js";
|
|
8
|
-
import { ReviewHostError, type CiView, type CodeReviewHost, type CommentInput, type CommentRef, type CompleteCheckInput, type EditCommentInput, type FileListView, type FileReadResult, type HostPermission, type HostUser, type PublishReviewInput, type PublishReviewResult, type PullRequestHead, type PullRequestOrigin, type PullRequestView, type StartCheckInput } from "./types.js";
|
|
8
|
+
import { ReviewHostError, type CiView, type CodeReviewHost, type CommentInput, type CommentRef, type CompleteCheckInput, type EditCommentInput, type FileListView, type FileReadResult, type HostPermission, type HostUser, type PublishReviewInput, type PublishReviewResult, type PullRequestHead, type PullRequestOrigin, type PullRequestView, type StartCheckInput, type UpsertNamedCheckInput } from "./types.js";
|
|
9
9
|
/** listFiles leaves out vendored trees and lockfiles; every host applies the same filter. */
|
|
10
10
|
export declare function skippedInListing(path: string): boolean;
|
|
11
11
|
type Json = Record<string, unknown>;
|
|
@@ -16,6 +16,7 @@ export declare class GitHubReviewHost implements CodeReviewHost {
|
|
|
16
16
|
private readonly client;
|
|
17
17
|
readonly provider: "github";
|
|
18
18
|
constructor(client: GitHubAppClient);
|
|
19
|
+
appOwnerLogin(): Promise<string>;
|
|
19
20
|
/** Runs `action` with a token for exactly `permissions`, translating failures to ReviewHostError. */
|
|
20
21
|
protected withToken<T>(repository: string, permissions: Record<string, "read" | "write">, action: (get: (path: string, init?: RequestInit, expected?: number[]) => Promise<Response>, graphql: GraphQl) => Promise<T>): Promise<T>;
|
|
21
22
|
repositoryPermission(repository: string, user: HostUser): Promise<{
|
|
@@ -70,5 +71,14 @@ export declare class GitHubReviewHost implements CodeReviewHost {
|
|
|
70
71
|
checkId: string;
|
|
71
72
|
}>;
|
|
72
73
|
completeCheck(repository: string, input: CompleteCheckInput): Promise<void>;
|
|
74
|
+
/**
|
|
75
|
+
* Creates or updates, by fixed name, a check run on `input.headSha` that
|
|
76
|
+
* this App owns: lists the commit's check runs filtered to that name and
|
|
77
|
+
* patches the newest one `fromApp` owns, else creates one. Writes nothing
|
|
78
|
+
* when that run already carries the same status, conclusion, and output.
|
|
79
|
+
* A completed run is never moved back to in_progress (GitHub does not
|
|
80
|
+
* allow reopening a completed check run): a fresh one is created instead.
|
|
81
|
+
*/
|
|
82
|
+
upsertNamedCheck(repository: string, input: UpsertNamedCheckInput): Promise<void>;
|
|
73
83
|
}
|
|
74
84
|
export {};
|
|
@@ -22,6 +22,12 @@ export function skippedInListing(path) {
|
|
|
22
22
|
return VENDORED.test(path) || path.endsWith("package-lock.json");
|
|
23
23
|
}
|
|
24
24
|
const CHECK_TEXT_LIMIT = 65_535;
|
|
25
|
+
/**
|
|
26
|
+
* Cap on a named check's output.title. Not a documented GitHub limit (none
|
|
27
|
+
* is published for the title); a conservative length that keeps the title
|
|
28
|
+
* a one-line heading in the checks UI.
|
|
29
|
+
*/
|
|
30
|
+
const CHECK_TITLE_LIMIT = 255;
|
|
25
31
|
const COMMENT_WRITE = { issues: "write", pull_requests: "write" };
|
|
26
32
|
const REVIEW_WRITE = { pull_requests: "write", checks: "write" };
|
|
27
33
|
/** Always implicitly granted to an installation token; enough for the collaborator-permission endpoint. */
|
|
@@ -201,6 +207,9 @@ export class GitHubReviewHost {
|
|
|
201
207
|
constructor(client) {
|
|
202
208
|
this.client = client;
|
|
203
209
|
}
|
|
210
|
+
async appOwnerLogin() {
|
|
211
|
+
return (await this.client.appIdentity()).ownerLogin;
|
|
212
|
+
}
|
|
204
213
|
/** Runs `action` with a token for exactly `permissions`, translating failures to ReviewHostError. */
|
|
205
214
|
async withToken(repository, permissions, action) {
|
|
206
215
|
try {
|
|
@@ -708,4 +717,45 @@ export class GitHubReviewHost {
|
|
|
708
717
|
const base = repoPath(repository);
|
|
709
718
|
await this.withToken(repository, { checks: "write" }, (get) => this.patchCheck(get, base, input));
|
|
710
719
|
}
|
|
720
|
+
/**
|
|
721
|
+
* Creates or updates, by fixed name, a check run on `input.headSha` that
|
|
722
|
+
* this App owns: lists the commit's check runs filtered to that name and
|
|
723
|
+
* patches the newest one `fromApp` owns, else creates one. Writes nothing
|
|
724
|
+
* when that run already carries the same status, conclusion, and output.
|
|
725
|
+
* A completed run is never moved back to in_progress (GitHub does not
|
|
726
|
+
* allow reopening a completed check run): a fresh one is created instead.
|
|
727
|
+
*/
|
|
728
|
+
async upsertNamedCheck(repository, input) {
|
|
729
|
+
// A caller's bad input, like path_invalid: never interpolated into the URL.
|
|
730
|
+
if (!SAFE_SHA.test(input.headSha))
|
|
731
|
+
throw new ReviewHostError("host_api_error", "head_sha_invalid");
|
|
732
|
+
const base = repoPath(repository);
|
|
733
|
+
const output = {
|
|
734
|
+
title: input.title.slice(0, CHECK_TITLE_LIMIT),
|
|
735
|
+
summary: input.summary.slice(0, CHECK_TEXT_LIMIT),
|
|
736
|
+
};
|
|
737
|
+
const conclusion = input.status === "completed" ? input.conclusion : undefined;
|
|
738
|
+
await this.withToken(repository, { checks: "write" }, async (get) => {
|
|
739
|
+
const { id: appId } = await this.client.appIdentity();
|
|
740
|
+
const payload = record(await (await get(`${base}/commits/${input.headSha}/check-runs?check_name=${encodeURIComponent(input.name)}`)).json());
|
|
741
|
+
const existing = list(payload.check_runs ?? [])
|
|
742
|
+
.filter((r) => fromApp(r, appId))
|
|
743
|
+
.sort((a, b) => num(b.id) - num(a.id))[0];
|
|
744
|
+
const body = { status: input.status, ...(conclusion ? { conclusion } : {}), output };
|
|
745
|
+
if (existing) {
|
|
746
|
+
const current = record(existing.output ?? {});
|
|
747
|
+
const unchanged = existing.status === input.status &&
|
|
748
|
+
(existing.conclusion ?? undefined) === conclusion &&
|
|
749
|
+
current.title === output.title &&
|
|
750
|
+
current.summary === output.summary;
|
|
751
|
+
if (unchanged)
|
|
752
|
+
return;
|
|
753
|
+
if (!(existing.status === "completed" && input.status !== "completed")) {
|
|
754
|
+
await get(`${base}/check-runs/${encodeURIComponent(String(num(existing.id)))}`, { method: "PATCH", body: JSON.stringify(body) }, [200]);
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
await get(`${base}/check-runs`, { method: "POST", body: JSON.stringify({ name: input.name, head_sha: input.headSha, ...body }) }, [201]);
|
|
759
|
+
});
|
|
760
|
+
}
|
|
711
761
|
}
|
|
@@ -207,6 +207,18 @@ export interface CompleteCheckInput {
|
|
|
207
207
|
text?: string;
|
|
208
208
|
detailsUrl?: string;
|
|
209
209
|
}
|
|
210
|
+
export type UpsertNamedCheckInput = {
|
|
211
|
+
headSha: string;
|
|
212
|
+
/** The check's fixed name, e.g. MERGE_ORDER_CHECK_NAME ("wardby merge order"); identifies which run to update. */
|
|
213
|
+
name: string;
|
|
214
|
+
title: string;
|
|
215
|
+
summary: string;
|
|
216
|
+
} & ({
|
|
217
|
+
status: "in_progress";
|
|
218
|
+
} | {
|
|
219
|
+
status: "completed";
|
|
220
|
+
conclusion: "success" | "failure";
|
|
221
|
+
});
|
|
210
222
|
/**
|
|
211
223
|
* A user's effective permission on one repository, host-neutral and ranked
|
|
212
224
|
* none < read < triage < write < maintain < admin. GitHub maps its
|
|
@@ -222,6 +234,8 @@ export interface HostUser {
|
|
|
222
234
|
}
|
|
223
235
|
export interface CodeReviewHost {
|
|
224
236
|
readonly provider: ReviewHostProvider;
|
|
237
|
+
/** The GitHub App's owner account (always served; see WARDBY_GITHUB_ALLOWED_ACCOUNTS). Hosts without an App omit it. */
|
|
238
|
+
appOwnerLogin?(): Promise<string>;
|
|
225
239
|
/**
|
|
226
240
|
* The user's effective permission on `repository`, asked with the App's own
|
|
227
241
|
* installation credential (never a user token). Answers only for the user
|
|
@@ -274,6 +288,15 @@ export interface CodeReviewHost {
|
|
|
274
288
|
checkId: string;
|
|
275
289
|
}>;
|
|
276
290
|
completeCheck(repository: string, input: CompleteCheckInput): Promise<void>;
|
|
291
|
+
/**
|
|
292
|
+
* Creates or updates, by fixed name, a check run on one commit that isn't
|
|
293
|
+
* tied to a run's own startCheck/completeCheck lifecycle (e.g. the
|
|
294
|
+
* `wardby merge order` check, recomputed fresh and reposted on demand).
|
|
295
|
+
* Finds the one this App owns among the commit's check runs with that
|
|
296
|
+
* name and patches it; creates one when none exists. Hosts that can't
|
|
297
|
+
* post checks (e.g. a local repository) leave this out.
|
|
298
|
+
*/
|
|
299
|
+
upsertNamedCheck?(repository: string, input: UpsertNamedCheckInput): Promise<void>;
|
|
277
300
|
}
|
|
278
301
|
export type ReviewHostErrorCode = "host_not_installed" | "host_permission_missing" | "host_api_error" | "host_invalid_response";
|
|
279
302
|
export declare class ReviewHostError extends Error {
|
|
@@ -149,9 +149,16 @@ export interface RelatedPullRequestEntry {
|
|
|
149
149
|
state?: RelatedPullRequestState;
|
|
150
150
|
/** The pull request whose description this section is in. */
|
|
151
151
|
self?: boolean;
|
|
152
|
+
/**
|
|
153
|
+
* Step in the delegating agent's declared merge order (1-99), from
|
|
154
|
+
* CodingRun.mergeOrder; absent = the delegating agent set no order
|
|
155
|
+
* (an open entry is listed under "Not ordered:" when any other entry,
|
|
156
|
+
* open or done, has one).
|
|
157
|
+
*/
|
|
158
|
+
mergeOrder?: number;
|
|
152
159
|
}
|
|
153
160
|
export interface RelatedPullRequestsInput {
|
|
154
|
-
/** In
|
|
161
|
+
/** In merge order: by mergeOrder when set, else the order the request's coding runs were dispatched. */
|
|
155
162
|
entries: readonly RelatedPullRequestEntry[];
|
|
156
163
|
issue?: {
|
|
157
164
|
key: string;
|
|
@@ -162,9 +169,18 @@ export interface RelatedPullRequestsInput {
|
|
|
162
169
|
/**
|
|
163
170
|
* The "Related pull requests" block, markers included, built only from
|
|
164
171
|
* stored rows (never agent text). Open (and draft, and not-yet-known) pull
|
|
165
|
-
* requests form the
|
|
166
|
-
*
|
|
167
|
-
*
|
|
172
|
+
* requests form the merge order; merged and closed ones follow as context.
|
|
173
|
+
* Undefined when no other valid pull request is left after validation: a
|
|
174
|
+
* set of one is not a set.
|
|
175
|
+
*
|
|
176
|
+
* When any valid entry (open, merged or closed) carries a `mergeOrder` (set
|
|
177
|
+
* by the delegating agent), steps are counted over all of them, so a
|
|
178
|
+
* label never changes as pull requests merge: the open entries that have
|
|
179
|
+
* one are numbered under "Merge order" and labelled with their step (equal
|
|
180
|
+
* `mergeOrder` values share a step label), any open entry without one is
|
|
181
|
+
* listed after under "Not ordered:", and a merged or closed entry that has
|
|
182
|
+
* one carries its step label in the context list too. Otherwise every open
|
|
183
|
+
* entry is numbered in the unordered "Suggested merge order" list.
|
|
168
184
|
*/
|
|
169
185
|
export declare function renderRelatedSection(input: RelatedPullRequestsInput): string | undefined;
|
|
170
186
|
/**
|
|
@@ -228,11 +244,21 @@ export declare class GitHubAppClient implements GitHubRepositoryAccess {
|
|
|
228
244
|
*/
|
|
229
245
|
readFileAtRef(input: RepositoryFileInput): Promise<string | null>;
|
|
230
246
|
private identity?;
|
|
231
|
-
/**
|
|
232
|
-
|
|
247
|
+
/**
|
|
248
|
+
* The App's id, slug (its @-mention handle), and owner account, read once
|
|
249
|
+
* with the App JWT and memoized. `fresh` re-reads GET /app and replaces the
|
|
250
|
+
* memo (used before a destructive action, since the owner can change).
|
|
251
|
+
* A rejection is never cached.
|
|
252
|
+
*/
|
|
253
|
+
appIdentity(opts?: {
|
|
254
|
+
fresh?: boolean;
|
|
255
|
+
}): Promise<{
|
|
233
256
|
id: number;
|
|
234
257
|
slug: string;
|
|
258
|
+
ownerLogin: string;
|
|
235
259
|
}>;
|
|
260
|
+
/** Removes this App from an installation (an account outside the trusted set). */
|
|
261
|
+
deleteInstallation(installationId: number): Promise<void>;
|
|
236
262
|
upsertContinuationStatusComment(input: ContinuationStatusCommentInput): Promise<void>;
|
|
237
263
|
updateContinuationStatusComment(input: ContinuationStatusCommentInput): Promise<void>;
|
|
238
264
|
createContinuationCheckRun(input: ContinuationCheckRunInput): Promise<void>;
|