diff --git a/.github/workflows/dark-factory.yml b/.github/workflows/dark-factory.yml new file mode 100644 index 00000000..53250faa --- /dev/null +++ b/.github/workflows/dark-factory.yml @@ -0,0 +1,77 @@ +# Dark Factory (Flow B) trigger β€” issue labeled `dark-factory` β†’ orchestrator. +# +# This is the P1 entrypoint (Β§4 step 1). It gates on the label, mints a +# short-TTL token scoped to this repo, and POSTs the run request to the +# orchestrator running on spoke-dev. The orchestrator (not this Action) holds +# the long-lived credentials and drives the sandbox; the Action's only job is +# to authenticate the trigger and hand over a short-lived token. +# +# The token here is the workflow's GITHUB_TOKEN (auto-expires when the run +# ends). For cross-repo / org installs, swap in a GitHub App token minted via +# actions/create-github-app-token β€” the orchestrator treats it the same way. +name: dark-factory + +on: + issues: + types: [labeled] + +# Least-privilege: the orchestrator opens the PR + updates the sticky comment +# using the token we pass, so it needs write on contents + PRs + issues. +permissions: + contents: write + pull-requests: write + issues: write + +jobs: + dispatch: + # Only fire when the `dark-factory` label is the one that was added. + if: github.event.label.name == 'dark-factory' + runs-on: ubuntu-latest + steps: + - name: Acknowledge on the issue + uses: actions/github-script@v7 + with: + script: | + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: '🏭 Dark Factory accepted this issue β€” claiming a warm Kata sandbox on the hub…', + }); + + - name: Submit df-run to Argo Workflows + env: + # argo-server events endpoint base, e.g. https:///argo-workflows + # Configure as a repo/org variable so the host isn't hard-coded. + ARGO_SERVER: ${{ vars.DARK_FACTORY_ARGO_SERVER }} + # Bearer token for the argo-server events API (a token bound to the + # dark-factory-workflow SA, or an SSO token). Stored as a repo secret. + ARGO_TOKEN: ${{ secrets.DARK_FACTORY_ARGO_TOKEN }} + ISSUE_ID: ${{ github.event.issue.id }} + ISSUE_NUMBER: ${{ github.event.issue.number }} + ISSUE_TITLE: ${{ github.event.issue.title }} + ISSUE_BODY: ${{ github.event.issue.body }} + REPO: ${{ github.repository }} + BASE: ${{ github.event.repository.default_branch }} + run: | + set -euo pipefail + if [ -z "${ARGO_SERVER:-}" ]; then + echo "::error::DARK_FACTORY_ARGO_SERVER is not set β€” cannot submit." + exit 1 + fi + # POST the issue to the WorkflowEventBinding 'dark-factory' in the argo + # namespace. The discriminator header + payload.issue selector gate it. + payload="$(jq -n \ + --arg id "$ISSUE_ID" \ + --arg number "$ISSUE_NUMBER" \ + --arg repo "$REPO" \ + --arg title "$ISSUE_TITLE" \ + --arg body "$ISSUE_BODY" \ + --arg base "$BASE" \ + '{issue: {id: $id, number: ($number|tonumber), repo: $repo, title: $title, body: $body, base: $base}}')" + curl -fsS -X POST "${ARGO_SERVER}/api/v1/events/argo/dark-factory" \ + -H "Content-Type: application/json" \ + -H "x-dark-factory: true" \ + -H "Authorization: Bearer ${ARGO_TOKEN}" \ + -d "$payload" + echo "Submitted df-run for issue #$ISSUE_NUMBER to Argo Workflows." diff --git a/.gitignore b/.gitignore index 568bff5e..6c43881a 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,10 @@ config.local.yaml private/ .local/ + +# Dark Factory IAM terraform working files (committed: iam/*.tf; ignored: state/plugins) +gitops/addons/charts/dark-factory/iam/.terraform/ +gitops/addons/charts/dark-factory/iam/.terraform.lock.hcl +gitops/addons/charts/dark-factory/iam/terraform.tfstate +gitops/addons/charts/dark-factory/iam/terraform.tfstate.backup +gitops/addons/charts/dark-factory/iam/_provider.tf diff --git a/README.md b/README.md index 8110945d..45f76244 100644 --- a/README.md +++ b/README.md @@ -1,75 +1,154 @@ -# Open Agentic Platform on Amazon EKS +# Open Agentic Platform (OAP) on Amazon EKS -An AI agent platform built on Amazon EKS, featuring Bifrost LLM gateway, Langfuse observability, AgentGateway (A2A MCP auth), and multi-agent orchestration. +**A complete, production-shaped platform for building, running, securing, and observing AI agents on +Amazon EKS β€” installed with one command.** + +OAP turns a set of EKS clusters into an agent platform: a place where a team can onboard a model, +declare an agent as a Kubernetes resource, give it tools (MCP), memory, a browser, and a code +interpreter, wire multiple agents together (A2A), secure every hop with real identity, and watch it +all through end-to-end traces β€” entirely via GitOps. It ships with a hands-on workshop and two +flagship patterns: a **multi-agent financial-services** system and the **Dark Factory** autonomous +coding pipeline running in hardware-isolated micro-VM sandboxes. + +Everything is declarative and GitOps-driven (ArgoCD + Crossplane + KubeVela). You describe intent; +the platform reconciles it across a hub and any number of spoke clusters. + +--- + +## Platform at a glance + +Agent workloads sit on a layered stack β€” **Agent Platform Capabilities (APC)** on top of a +platform-engineering foundation, all on Amazon EKS Auto Mode: + +

+ OAP layered architecture +

+ +> _Diagrams are editable draw.io sources under [`docs/architecture/diagrams/src/`](docs/architecture/diagrams/src/) +> (open in [draw.io](https://app.diagrams.net) or the VS Code Draw.io extension) with SVGs exported to `img/`._ + +--- + +## Why OAP + +- **Agents as first-class Kubernetes resources** β€” `kind: Agent`, `kind: RemoteMCPServer`. Declare, + version, blue/green, and roll back agents like any other workload. +- **Batteries included** β€” model gateway, identity, tool gateway, runtime, memory, browser, code + interpreter, observability, and hardware isolation are pre-wired addons, not homework. +- **Real security posture** β€” Keycloak OIDC with JWT-enforced clientβ†’agent, agentβ†’tool, and + agentβ†’agent authorization, plus per-workload LLM identity. The "lethal trifecta" is designed out. +- **Runs the hard patterns** β€” multi-agent orchestration, and an autonomous coding factory where + untrusted, code-writing agents run inside **Kata micro-VMs** next to your control plane, safely. +- **One-command install, GitOps forever after** β€” `task install` provisions the hub, spokes, and + every capability; after that, git is the source of truth. + +--- ## Quick Start ### Prerequisites -- AWS account with Bedrock access -- [Task](https://taskfile.dev), kubectl, Helm 3.x, AWS CLI, `yq` -- Podman or Docker (for Kind-based bootstrap) +- AWS account with Amazon Bedrock access +- [Task](https://taskfile.dev), `kubectl`, Helm 3.x, AWS CLI, `yq` +- Podman or Docker (for the Kind-based bootstrap) +- A domain with an ACM cert + Route53 zone (ingress), and IAM Identity Center (ArgoCD SSO) ### Install ```bash # 1. Configure cp config.yaml config.local.yaml -# Edit config.local.yaml with your values +# Edit config.local.yaml with your AWS / domain / SSO values -# 2. Install everything (platform + agentic components) +# 2. Install everything (platform + spokes + agentic capabilities) task install ``` -That's it. The installer provisions an EKS hub cluster, deploys the base platform (ArgoCD, Crossplane, observability), then layers on the agentic components. +That's it. The installer bootstraps from Kind, provisions an EKS **hub** cluster, deploys the base +platform (ArgoCD, Crossplane, observability), provisions optional **spoke** clusters, then layers on +the agentic capabilities as ArgoCD-managed addons. The Kind bootstrap is destroyed once the hub is +self-managing. -### Configuration +--- -Edit `config.local.yaml`: +## Agent Platform Capabilities (APC) -| Section | Key Fields | Description | -|---------|-----------|-------------| -| `platform` | `repo`, `ref` | Base platform repo and version tag | -| `aws` | `region`, `accountId`, `profile` | AWS settings | -| `hub` | `clusterName`, `kubernetesVersion` | Hub cluster config | -| `domain` | | Ingress domain (must have ACM cert + Route53 zone) | -| `identityCenter` | `instanceArn`, `region`, `adminGroupId` | SSO for ArgoCD | -| `agenticRepo` | `url`, `revision`, `basepath` | This repo's git coordinates (for ArgoCD) | -| `spokes` | | Optional spoke clusters (see below) | +Each capability is a GitOps addon (`gitops/addons/charts/`), gated per cluster via ArgoCD +ApplicationSets. Status reflects the current reference deployment. -### Spoke Clusters +| Capability | Delivered by | Status | What it gives you | +|---|---|---|---| +| **Model as a Service** | `bifrost` (platform) Β· `litellm` (workshop) | βœ… | LLM gateway to Bedrock with routing, fallbacks, rate limiting, caching, cost tracking. Onboard a model declaratively. | +| **Agent Identities** | `agent-gateway` + Keycloak | βœ… | OIDC identities for agents, users, and MCP clients (`platform` realm). | +| **Agent Gateway** | `agent-gateway`, `gateway-api-crds` | βœ… | A2A + MCP gateway with JWT-auth policies enforced on every call. | +| **Agent Runtime** | `crossplane-agentcore` | βœ… | Crossplane compositions for Amazon Bedrock AgentCore (`agentruntimes` CRD). | +| **Agent Lifecycle** | `oam-agent-components` + KAgent | βœ… | Declarative `Agent` CRDs, KubeVela OAM components, blue/green via ArgoCD. | +| **Agent Observability** | `otel-collector`, `langfuse`, Jaeger, AMP, AMG | βœ… | End-to-end traces (userβ†’agentβ†’toolβ†’agent), LLM traces/cost, metrics + dashboards. | +| **Agent Memory** | `crossplane-agentcore` | ⚠️ CRD ready | `memories.*` CRDs registered; wire an instance for persistent memory. | +| **Agent Browser** | `crossplane-agentcore` | ⚠️ CRD ready | Managed headless browser resource for agents that browse. | +| **Agent Code Interpreter** | `crossplane-agentcore` / sandbox | ⚠️ CRD ready | Sandboxed code execution for agents. | +| **Agent Isolation** | `agent-sandbox` | βœ… (V2) | **Kata + Cloud Hypervisor micro-VMs** β€” hardware-isolated, credential-less sandboxes for untrusted agent code. | +| **Agent Evaluation** | (planned) | ⬜ | Eval tooling (AgentCore Evals / RAGAS) β€” roadmap. | -Add spoke clusters for workload environments: +**Gateway note:** the platform ships **Bifrost** as the enabled AI gateway (per-workload virtual +keys, model routing); **LiteLLM** is included as an alternative chart and is the gateway used in the +workshop teaching path. Both front Amazon Bedrock. -```yaml -spokes: - dev: - region: us-west-2 - kubernetesVersion: "1.35" - vpcCidr: "10.1.0.0/16" - autoMode: true - prod: - region: us-west-2 - kubernetesVersion: "1.35" - vpcCidr: "10.2.0.0/16" - autoMode: true -``` +--- -Spokes are provisioned via Crossplane from the hub. Agentic components deploy to all clusters automatically. +## Provisioning & Topology -### Fleet Management & Targeting +OAP uses a **hub + spokes** model, provisioned declaratively: -This repo controls which clusters receive the agentic platform via `gitops/overlays/environments/*/enabled-addons.yaml`: +- **Hub cluster** β€” runs ArgoCD, Crossplane, the platform control plane, and (by default) the agentic + capabilities. Crossplane on the hub provisions the spokes. +- **Spoke clusters** (`dev`, `prod`, …) β€” workload environments; agentic addons deploy to them + automatically based on their `environment` label. +- **Kind bootstrap** β€” a throwaway local cluster that stands up the hub, then self-destructs. + +Addons are targeted through a layered ApplicationSet model in `gitops/addons/`: + +``` +bootstrap/default/addons.yaml # master catalog: every addon, its chart path + selector +default/addons/ # values applied to all clusters +environments//addons # per-environment overrides (e.g. control-plane) +clusters//addons # per-cluster overrides (e.g. hub) +``` + +Enable/disable capabilities per environment via `gitops/overlays/environments//enabled-addons.yaml`: ```yaml # gitops/overlays/environments/dev/enabled-addons.yaml enabledAddons: - agent_platform: true # deploy agentic components to dev clusters + agent_platform: true # deploy agentic capabilities to dev + bifrost: true + agent_sandbox: true ``` -Set `agent_platform: false` to exclude an environment. Fleet member definitions in `gitops/fleet/members/` control spoke discovery. +### Configuration (`config.local.yaml`) -## Available Commands +| Section | Key Fields | Description | +|---------|-----------|-------------| +| `platform` | `repo`, `ref` | Base platform repo (appmod-blueprints) + version | +| `aws` | `region`, `accountId`, `profile` | AWS settings | +| `hub` | `clusterName`, `kubernetesVersion` | Hub cluster config | +| `domain` | | Ingress domain (ACM cert + Route53 zone) | +| `identityCenter` | `instanceArn`, `region`, `adminGroupId` | SSO for ArgoCD | +| `agenticRepo` | `url`, `revision`, `basepath` | This repo's coordinates (for ArgoCD) | +| `spokes` | | Optional spoke clusters (below) | + +```yaml +spokes: + dev: { region: us-west-2, kubernetesVersion: "1.35", vpcCidr: "10.1.0.0/16", autoMode: true } + prod: { region: us-west-2, kubernetesVersion: "1.35", vpcCidr: "10.2.0.0/16", autoMode: true } +``` + +Spokes are provisioned via Crossplane from the hub. Fleet member definitions in `gitops/fleet/members/` +control spoke discovery. + +--- + +## Commands | Command | Description | |---------|-------------| @@ -77,75 +156,141 @@ Set `agent_platform: false` to exclude an environment. Fleet member definitions | `task platform:install` | Provision base EKS platform only | | `task spokes:install` | Provision spoke clusters only | | `task spokes:status` | Check spoke provisioning progress | -| `task agentic:install` | Deploy agentic components only | +| `task agentic:install` | Deploy agentic capabilities only | | `task status` | Show ArgoCD application status | | `task upgrade` | Upgrade everything | -| `task destroy` | Remove agentic components (keeps base platform) | +| `task destroy` | Remove agentic capabilities (keeps base platform) | | `task spokes:destroy` | Delete spoke clusters | +--- + ## Architecture ``` -β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” -β”‚ open-agentic-platform (this repo) β”‚ -β”‚ config.local.yaml β†’ task install β”‚ -β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - β”‚ β”‚ - β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β” - β”‚ appmod-blueprintsβ”‚ β”‚ ArgoCD Applicationβ”‚ - β”‚ (base platform) β”‚ β”‚ (agentic addons) β”‚ - β”‚ read-only clone β”‚ β”‚ points to this repoβ”‚ - β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - β”‚ β”‚ - β–Ό β–Ό - β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” - β”‚ EKS Hub Cluster β”‚ - β”‚ ArgoCD ─── watches both repos (read-only) β”‚ - β”‚ Crossplane ─── provisions spoke clusters β”‚ - β”‚ β”‚ - β”‚ Agentic: LiteLLM, Langfuse, Jaeger, β”‚ - β”‚ AgentGateway, Bifrost, AgentCore β”‚ - β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ open-agentic-platform (this repo) β”‚ +β”‚ config.local.yaml β†’ task install β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ β”‚ + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ appmod-blueprints β”‚ β”‚ ArgoCD Application β”‚ + β”‚ (base platform) β”‚ β”‚ (agentic addons) β”‚ + β”‚ read-only clone β”‚ β”‚ points to this repoβ”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ β”‚ + β–Ό β–Ό + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ EKS HUB CLUSTER β”‚ + β”‚ ArgoCD (GitOps) Β· Crossplane (provisions spokes) β”‚ + β”‚ Capabilities: Bifrost / LiteLLM Β· AgentGateway Β· Keycloak β”‚ + β”‚ Langfuse Β· Jaeger Β· OTEL Β· AMP / AMG β”‚ + β”‚ AgentCore (Crossplane) Β· Kata sandboxes β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ β”‚ + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ spoke: dev β”‚ β”‚ spoke: prod β”‚ + β”‚ agent workloadsβ”‚ β”‚ agent workloadsβ”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ``` -## Components - -| Component | Purpose | -|-----------|---------| -| **LiteLLM** | LLM gateway with rate limiting, caching, fallbacks | -| **Langfuse** | LLM observability β€” traces, costs, analytics | -| **Jaeger** | Distributed tracing for agent interactions | -| **AgentGateway** | MCP auth gateway with Keycloak OIDC | -| **Bifrost** | AI gateway for model routing | -| **AgentCore** | Crossplane compositions for Bedrock AgentCore | +--- ## Workshop -The `workshop/` directory contains hands-on examples: +`workshop/` is a hands-on path from zero to a secured, observable multi-agent system: | Module | Description | |--------|-------------| -| `00-initial-setup` | Bedrock + LiteLLM configuration | -| `01-first-agent` | Basic KAgent with Bedrock | -| `02-k8s-ops-agent` | Kubernetes operations agent | -| `03-multi-tool-agent` | Agent with MCP tool servers | -| `04-multi-agents` | Financial services multi-agent system | -| `05-observability` | Monitoring and tracing setup | +| `00-initial-setup` | Bedrock + model gateway configuration (Model as a Service) | +| `01-first-agent` | Deploy your first agent as a CRD | +| `02-k8s-ops-agent` | A Kubernetes operations agent | +| `03-multi-tool-agent` | Onboard MCP tool servers and wire them to an agent | +| `04-multi-agents` | Financial-services multi-agent orchestration (A2A) | +| `05-observability` | Tracing, LLM observability, metrics, dashboards | -## Resources +--- -- [LiteLLM](https://docs.litellm.ai) -- [Langfuse](https://langfuse.com/docs) -- [Amazon Bedrock](https://aws.amazon.com/bedrock) -- [appmod-blueprints](https://github.com/aws-samples/appmod-blueprints) (base platform) +## Flagship Patterns + +### Multi-Agent Financial Services (A2A) +A `financial-advisor` agent delegates to specialist agents (portfolio, market, risk) over the A2A +protocol β€” every hop authenticated through the gateway and traced through OTEL / Jaeger / Langfuse. +See `workshop/04-multi-agents/financial-services`. + +### Dark Factory β€” autonomous coding in hardware-isolated sandboxes (V2) +The Dark Factory turns a **GitHub issue into a reviewed, merged PR, autonomously**: + +1. A labeled issue triggers an **Argo Workflow** on the hub. +2. A coding agent (Claude Code or Kiro CLI) implements the change inside a **credential-less Kata + micro-VM** β€” an untrusted, network-locked, hardware-isolated sandbox β€” and opens a PR. +3. The change is reviewed by the **real AWS DevOps Agent** (release readiness) and **AWS Security + Agent** (OWASP / secrets / IAM / dependency risk), plus a holdout gate and `terraform validate` / + ephemeral-namespace deploy tests β€” every step reporting onto the PR. +4. A human approves; a separate Argo workflow squash-merges and reaps the sandbox. + +

+ Dark Factory β€” issue to merged PR flow +

+ +It is the platform's proof that you can run untrusted, code-writing agents safely alongside a control +plane. See [`docs/dark-factory/`](docs/dark-factory/) and [`examples/dark-factory/`](examples/dark-factory/). -## Design & Architecture +--- -Design documents and open work items β€” check these before starting a new feature: +## Managed or open source β€” your choice, per capability -| Document | What it covers | Open items | +OAP is **CNCF-aligned and cloud-agnostic**. Every capability can be backed by an **Amazon Bedrock +AgentCore** managed service **or** an **open-source alternative** β€” same declarative manifest, +different backend. Agents are onboarded either **imageless** (CRD-defined, no container to build) or +**BYO-image** (any OCI image / SDK). The abstraction layer (Kro / KubeVela orchestrating Crossplane Β· +ACK Β· OpenTofu) resolves your choice at deploy time. + +

+ OAP β€” AgentCore managed or OSS alternative per capability +

+ +### Capabilities β†’ charts (in this repo) + +| Addon chart | Capability | +|---|---| +| `bifrost`, `litellm` | Model as a Service / AI gateway | +| `agent-gateway`, `gateway-api-crds` | Agent Gateway + identity (A2A / MCP authz) | +| `crossplane-agentcore` | Runtime, Memory, Browser, Code Interpreter (Bedrock AgentCore) | +| `oam-agent-components` | Agent lifecycle (KubeVela OAM components) | +| `otel-collector`, `langfuse` | Observability (traces + LLM analytics) | +| `agent-sandbox` | Agent isolation (Kata + Cloud Hypervisor micro-VMs) | +| `dark-factory` | Autonomous coding pipeline (Argo Workflows + agents) | +| `application-sets` | ArgoCD ApplicationSet wiring | + +--- + +## Roadmap + +- **V2 (in progress):** agent sandboxes (βœ… Dark Factory + Kata), full Memory / Browser / Code-Interpreter + instances, self-service agent onboarding (Backstage). +- **V3:** AgentCore Gateway / Runtime extensions; agentic workflow engines (Camunda, Pega); evaluation + tooling (AgentCore Evals / RAGAS). + +--- + +## Documentation & Design + +Design docs and open work items β€” check these before starting a new feature: + +| Document | Covers | Open items | |---|---|---| -| [gitops/addons/charts/bifrost/DESIGN.md](gitops/addons/charts/bifrost/DESIGN.md) | Bifrost AI Gateway architecture, current state (no auth), and **per-workload Virtual Key minting** target design | ⚠️ `is_vk_mandatory` is disabled β€” implement per-workload VK minting via KubeVela workflow step before enabling | -| [platform/oam/DESIGN.md](platform/oam/DESIGN.md) | KubeVela OAM `agent` and `mcp-server` ComponentDefinition design decisions | β€” | -| [applications/strands-agent-base/ARCHITECTURE.md](applications/strands-agent-base/ARCHITECTURE.md) | Strands agent internals, LLM gateway integration, A2A protocol | References LiteLLM β€” superseded by Bifrost (`OpenAIModel` + `x-bf-vk`) | -| [gitops/DEPLOYMENT.md](gitops/DEPLOYMENT.md) | GitOps deployment runbook: ArgoCD bootstrap, addon enablement, Pod Identity setup | LiteLLM Pod Identity step is superseded by declarative Crossplane in bifrost chart | +| [`docs/architecture/`](docs/architecture/) | Agent identity & token exchange, platform architecture | β€” | +| [`docs/OBSERVABILITY.md`](docs/OBSERVABILITY.md) | Tracing, LLM observability, metrics/dashboards | β€” | +| [`docs/dark-factory/README.md`](docs/dark-factory/README.md) | Dark Factory design, flows, diagrams | β€” | +| [`docs/dark-factory/AGENT-INSTALL.md`](docs/dark-factory/AGENT-INSTALL.md) | Connecting the AWS DevOps + Security Agents | β€” | +| [`gitops/DEPLOYMENT.md`](gitops/DEPLOYMENT.md) | GitOps deployment runbook (ArgoCD bootstrap, addon enablement, Pod Identity) | LiteLLM Pod-Identity step superseded by declarative Crossplane in the bifrost chart | +| [`gitops/addons/charts/bifrost/DESIGN.md`](gitops/addons/charts/bifrost/DESIGN.md) | Bifrost AI Gateway + **per-workload Virtual Key** target design | ⚠️ `is_vk_mandatory` disabled β€” implement per-workload VK minting before enabling | +| [`platform/oam/DESIGN.md`](platform/oam/DESIGN.md) | KubeVela OAM `agent` / `mcp-server` ComponentDefinitions | β€” | +| [`applications/strands-agent-base/ARCHITECTURE.md`](applications/strands-agent-base/ARCHITECTURE.md) | Strands agent internals, gateway integration, A2A | References LiteLLM β€” superseded by Bifrost (`OpenAIModel` + `x-bf-vk`) | + +## Resources + +- [Amazon Bedrock](https://aws.amazon.com/bedrock) Β· [AgentCore](https://aws.amazon.com/bedrock/agentcore/) +- [Bifrost](https://github.com/maximhq/bifrost) Β· [LiteLLM](https://docs.litellm.ai) Β· [Langfuse](https://langfuse.com/docs) +- [Kata Containers](https://katacontainers.io) Β· [Argo Workflows](https://argoproj.github.io/workflows/) +- [appmod-blueprints](https://github.com/aws-samples/appmod-blueprints) (base platform) diff --git a/applications/strands-agent-base/app/agent.py b/applications/strands-agent-base/app/agent.py index 2e3162b5..c9502b92 100644 --- a/applications/strands-agent-base/app/agent.py +++ b/applications/strands-agent-base/app/agent.py @@ -47,6 +47,25 @@ def _get_model() -> OpenAIModel: return _model +def _gateway_headers() -> dict: + """Authorization header from the projected workload-identity token. + + The `gateway-identity` OAM trait mounts a projected ServiceAccount token + (audience `agentgateway`) and sets WORKLOAD_TOKEN_PATH. AgentGateway + validates it against the cluster OIDC issuer. Read fresh on each connect so + the kubelet-rotated token is always current. Returns {} when no token is + mounted (gateway auth not in use). + """ + path = os.getenv("WORKLOAD_TOKEN_PATH") + if path: + try: + with open(path) as f: + return {"Authorization": "Bearer " + f.read().strip()} + except OSError: + logger.warning("WORKLOAD_TOKEN_PATH set but token unreadable at %s", path) + return {} + + def _get_mcp_tools() -> list: global _mcp_tools, _mcp_exit_stack if _mcp_exit_stack is not None: @@ -61,7 +80,7 @@ def _get_mcp_tools() -> list: for url in urls: logger.info(f"Connecting to MCP server: {url}") try: - client = MCPClient(lambda u=url: streamablehttp_client(u)) + client = MCPClient(lambda u=url: streamablehttp_client(u, headers=_gateway_headers())) stack.enter_context(client) server_tools = client.list_tools_sync() logger.info(f" Loaded {len(server_tools)} tools from {url}") diff --git a/applications/strands-agent-base/app/main.py b/applications/strands-agent-base/app/main.py index 8296f3d5..524b5e9a 100644 --- a/applications/strands-agent-base/app/main.py +++ b/applications/strands-agent-base/app/main.py @@ -20,8 +20,6 @@ # 3. Via Collector (OTEL_EXPORTER_OTLP_ENDPOINT set) β€” agent sends to local collector if os.getenv("OTEL_PYTHON_DISTRO") == "aws_distro": # Decentralized mode: ADOT auto-instrumentation handles telemetry. - # No manual StrandsTelemetry init β€” the aws_distro entry point - # configures exporters via env vars (CW Logs, X-Ray, CW Metrics). pass elif os.getenv("LANGFUSE_BASE_URL"): try: diff --git a/docs/OBSERVABILITY.md b/docs/OBSERVABILITY.md index 609f2d67..6c35bde1 100644 --- a/docs/OBSERVABILITY.md +++ b/docs/OBSERVABILITY.md @@ -1,154 +1,244 @@ -# Observability Architecture +# Agent Observability -## Overview +The platform supports two observability modes for agents. Both work out of the box β€” no custom image build needed if using the pre-built ECR image. -The Open Agentic Platform uses a dual-pipeline observability strategy: +## Mode 1: Centralized (Default) β€” Langfuse + AMP/Grafana -- **Langfuse** β€” LLM trace visualization (agent reasoning, tool calls, token usage, costs) -- **Amazon Managed Grafana (AMG)** β€” operational dashboards (latency, throughput, system health) +``` +Agent β†’ OTel Collector (:4318) β†’ Langfuse (traces) + AMP (metrics) β†’ Grafana +``` + +The agent exports OTLP spans to the local OTel Collector. The collector forwards traces to Langfuse and scrapes Bifrost metrics to AMP. + +### How it works + +1. OAM `agent` ComponentDefinition injects `OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector.otel.svc.cluster.local:4318` +2. Agent's `main.py` detects the endpoint and calls `StrandsTelemetry().setup_otlp_exporter()` +3. Strands SDK creates spans for agent invocations, tool calls, and LLM requests +4. Bifrost proxy reads the W3C `traceparent` header and creates child spans for model calls +5. OTel Collector merges all spans into one trace tree and exports to Langfuse via OTLP/HTTP with Basic Auth +6. Collector also scrapes Bifrost Prometheus metrics and pushes to AMP via remote write + +### Deploy (no config needed β€” centralized is the default) + +```yaml +apiVersion: core.oam.dev/v1beta1 +kind: Application +metadata: + name: my-agent + namespace: default +spec: + components: + - name: my-agent + type: agent + properties: + name: my-agent + namespace: default + description: "My agent" + image: ".dkr.ecr..amazonaws.com/strands-agent:latest" + systemMessage: "You are helpful." + modelConfig: + modelId: claude-sonnet +``` + +### View traces -Jaeger is not used. Langfuse acts as the distributed tracer for agent workloads. +Langfuse UI: `https:///` (Keycloak SSO) -## Architecture +### View metrics + +AMG Grafana: Agent Platform > Bifrost LLM Metrics dashboard + +--- + +## Mode 2: Decentralized β€” CloudWatch GenAI Console ``` -Spoke Clusters (dev, prod) Hub Cluster -β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” -β”‚ β”‚ β”‚ β”‚ -β”‚ Agent (Strands) β”‚ β”‚ Langfuse v3 β”‚ -β”‚ β”œβ”€ StrandsTelemetry SDK β”‚ β”‚ β”œβ”€ Web (:3000) β”‚ -β”‚ β”‚ OTLP/HTTP direct ───────┼─────┼──► β”‚ /api/public/otel β”‚ -β”‚ β”‚ (traces with GenAI β”‚HTTPSβ”‚ β”œβ”€ Worker (async processing) β”‚ -β”‚ β”‚ semantic attributes) β”‚ β”‚ β”œβ”€ Redis (queue) β”‚ -β”‚ β”‚ β”‚ β”‚ β”œβ”€ ClickHouse (OLAP trace store) β”‚ -β”‚ └─ W3C traceparent header β”‚ β”‚ β”œβ”€ MinIO (S3 event upload) β”‚ -β”‚ ↓ β”‚ β”‚ └─ PostgreSQL (metadata) β”‚ -β”‚ Bifrost Proxy β”‚ β”‚ β”‚ -β”‚ β”œβ”€ Receives traceparent β”‚ β”‚ Seed CronJob (every 5min) β”‚ -β”‚ β”œβ”€ Creates child LLM spans β”‚ β”‚ └─ Auto-assigns SSO users β”‚ -β”‚ └─ Exposes :8080/metrics β”‚ β”‚ β”‚ -β”‚ ↓ β”‚ β”‚ AMG (Grafana) β”‚ -β”‚ OTel Collector β”‚ β”‚ β”œβ”€ AMP datasource (Prometheus) β”‚ -β”‚ β”œβ”€ Traces β†’ X-Ray β”‚ β”‚ └─ Agent Platform dashboards β”‚ -β”‚ └─ Metrics β†’ AMP β”‚ β”‚ β”‚ -β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ -β”‚ Prometheus Scrapers (AMP) β”‚ -β”‚ └─ kube-state-metrics β”‚ -β”‚ └─ node-exporter β”‚ -β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +Agent β†’ ADOT auto-instrumentation β†’ CloudWatch (traces + logs + metrics) +``` + +The agent exports directly to CloudWatch via the AWS OpenTelemetry Distro (ADOT >=0.18.0). No collector or Langfuse needed. + +### How it works + +1. OAM `agent` ComponentDefinition injects ADOT env vars and overrides the container command to `opentelemetry-instrument python -m app.main` +2. ADOT auto-instruments the FastAPI + Strands application at startup +3. GenAI spans (agent invocations, tool calls, LLM requests) are exported to CloudWatch +4. With ADOT 0.18.0+ (unsplit architecture), spans go directly to the `aws/spans` log group β€” no separate log group needed +5. Pod Identity on the `default` ServiceAccount provides CW/X-Ray write permissions (provisioned by the `oam-agent-components` chart) + +### Deploy + +```yaml +apiVersion: core.oam.dev/v1beta1 +kind: Application +metadata: + name: my-agent + namespace: default +spec: + components: + - name: my-agent + type: agent + properties: + name: my-agent + namespace: default + description: "My agent" + image: ".dkr.ecr..amazonaws.com/strands-agent:latest" + systemMessage: "You are helpful." + modelConfig: + modelId: claude-sonnet + observability: + mode: decentralized ``` -## Trace Flow (Agent β†’ Langfuse) +### View traces -1. Agent creates a root span via `StrandsTelemetry.setup_otlp_exporter()` -2. Agent injects W3C `traceparent` header into Bifrost LLM call -3. Bifrost creates nested child spans (model, tokens, latency) -4. Both agent and Bifrost push spans via OTLP/HTTP to Langfuse's `/api/public/otel` endpoint -5. Langfuse Web accepts the payload, queues to Redis -6. Langfuse Worker reads from Redis, stores raw event in MinIO (S3), writes structured data to ClickHouse -7. Traces visible in Langfuse UI with full parent-child hierarchy +CloudWatch Console > Application Signals > GenAI Observability -### Authentication +### Prerequisites (automated by `task install`) -Traces are authenticated via HTTP Basic Auth: -- Header: `Authorization: Basic ` -- Header: `x-langfuse-ingestion-version: 4` (enables real-time Fast Preview) -- Keys are created by the Langfuse seed CronJob and stored in the Langfuse database +- CloudWatch Transaction Search enabled (one-time per account) +- Pod Identity with `logs:PutLogEvents`, `xray:PutTraceSegments`, `cloudwatch:PutMetricData` -### Agent Configuration (OAM ComponentDefinition) +--- -The `agent` OAM component injects these env vars: +## Environment Variables Injected by Mode +| Variable | Centralized | Decentralized | +|----------|-------------|---------------| +| `OTEL_SERVICE_NAME` | βœ… `` | βœ… `` | +| `OTEL_TRACES_EXPORTER` | βœ… `otlp` | βœ… `otlp` | +| `OTEL_EXPORTER_OTLP_ENDPOINT` | βœ… collector :4318 | β€” | +| `OTEL_PYTHON_DISTRO` | β€” | βœ… `aws_distro` | +| `OTEL_PYTHON_CONFIGURATOR` | β€” | βœ… `aws_configurator` | +| `OTEL_EXPORTER_OTLP_PROTOCOL` | β€” | βœ… `http/protobuf` | +| `OTEL_RESOURCE_ATTRIBUTES` | β€” | βœ… `service.name=` | +| `AGENT_OBSERVABILITY_ENABLED` | β€” | βœ… `true` | +| Container command | `python -m app.main` | `opentelemetry-instrument python -m app.main` | + +--- + +## Agent Image + +Both modes use the same image. The image includes all dependencies for both paths. + +### Dependencies (pyproject.toml) + +```toml +dependencies = [ + "fastapi~=0.115.0", + "uvicorn[standard]>=0.34.2", + "pydantic~=2.0", + "strands-agents[a2a,openai,otel]~=1.0", + "bedrock-agentcore", + "aws-opentelemetry-distro>=0.18.0", +] ``` -OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector.otel.svc.cluster.local:4318 -OTEL_SERVICE_NAME= -LANGFUSE_PUBLIC_KEY= -LANGFUSE_SECRET_KEY= -LANGFUSE_BASE_URL=https:// + +- `strands-agents[otel]` β€” brings OpenTelemetry SDK + OTLP HTTP exporter (centralized) +- `aws-opentelemetry-distro>=0.18.0` β€” ADOT with unsplit architecture (decentralized) + +### Dockerfile + +```dockerfile +FROM ghcr.io/astral-sh/uv:python3.13-bookworm-slim AS builder +WORKDIR /app +COPY pyproject.toml ./ +RUN uv pip install --system --no-cache --prerelease=allow . + +FROM python:3.13-slim-bookworm +RUN apt-get update && apt-get upgrade -y && rm -rf /var/lib/apt/lists/* +WORKDIR /app +COPY --from=builder /usr/local/lib/python3.13/site-packages /usr/local/lib/python3.13/site-packages +COPY --from=builder /usr/local/bin /usr/local/bin +COPY app/ ./app/ +RUN useradd -m -u 1000 appuser && chown -R appuser:appuser /app +USER appuser +EXPOSE 8083 +CMD ["python", "-m", "app.main"] ``` -The agent's `main.py` initializes `StrandsTelemetry().setup_otlp_exporter()` at startup which configures the OTLP exporter pointing to Langfuse. +### Build and Push -## Metrics Flow (Bifrost β†’ Grafana) +```bash +cd applications/strands-agent-base +IMAGE_NAME=strands-agent IMAGE_TAG=latest AWS_REGION=us-east-1 ./build.sh push +``` -1. Bifrost exposes Prometheus metrics at `:8080/metrics` -2. OTel Collector scrapes Bifrost metrics (prometheus receiver) -3. Collector forwards to AMP via `prometheusremotewrite` exporter -4. AMG (Grafana) queries AMP for dashboards +Output: `.dkr.ecr.us-east-1.amazonaws.com/strands-agent:latest` -### Metrics Available +--- -| Metric | Source | Description | -|--------|--------|-------------| -| Request latency per model | Bifrost | P50/P95/P99 latency by model | -| Token usage | Bifrost | Input/output/total tokens per request | -| Provider cost | Bifrost | Cost tracking by model provider | -| Cache hit ratio | Bifrost | Virtual key cache effectiveness | -| Pod CPU/Memory | kube-state-metrics | Resource usage by namespace | -| Node health | node-exporter | Cluster infrastructure metrics | +## Testing Agents -## Langfuse v3 Infrastructure +### Deploy a centralized agent and invoke -Deployed on the hub cluster only (namespace: `langfuse`). +```bash +# Deploy +kubectl apply -f platform/oam/examples/example-agent-centralized-observability.yaml -| Component | Image | Purpose | -|-----------|-------|---------| -| langfuse (Web) | `langfuse/langfuse:3` | API + UI, OTLP receiver | -| langfuse-worker | `langfuse/langfuse-worker:3` | Async event processing | -| langfuse-postgres | `postgres:16-alpine` | Metadata, users, projects | -| langfuse-clickhouse | `clickhouse/clickhouse-server:24.12` | OLAP trace/observation storage | -| langfuse-redis | `redis:7-alpine` | Async queue + cache | -| langfuse-minio | `minio/minio:latest` | S3-compatible blob storage (event upload) | +# Wait for pod +kubectl get pods -l app.kubernetes.io/name=my-agent -w -### Why MinIO? +# Invoke +kubectl run test --image=curlimages/curl --rm -it --restart=Never -- \ + curl -s -X POST http://my-agent-stable.default.svc.cluster.local:8083/ \ + -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","method":"message/send","id":"1","params":{"message":{"role":"user","messageId":"t1","parts":[{"type":"text","text":"What is S3?"}]}}}' -Langfuse v3 requires S3-compatible blob storage for its event ingestion pipeline. Raw OTLP events are written to S3 before being processed into ClickHouse. Without it, the OTLP endpoint returns 500. MinIO provides this locally without requiring an AWS S3 bucket. +# Verify trace in Langfuse +curl -s "https:///api/public/traces?limit=1" \ + -H "Authorization: Basic $(echo -n pk-lf-otel-platform:sk-lf-otel-platform-2026 | base64)" +``` -### Seed CronJob +### Deploy a decentralized agent and invoke -Runs every 5 minutes (idempotent): -1. Creates "Agent Platform" organization -2. Creates "agent-platform" project -3. Creates API keys for OTLP authentication -4. Auto-assigns any new Keycloak SSO users as OWNER of org + project +```bash +# Deploy +kubectl apply -f platform/oam/examples/example-agent-decentralized-observability.yaml -## OTel Collector +# Wait for pod +kubectl get pods -l app.kubernetes.io/name=my-cw-agent -w -Deployed on every spoke cluster (namespace: `otel`). +# Invoke +kubectl run test --image=curlimages/curl --rm -it --restart=Never -- \ + curl -s -X POST http://my-cw-agent-stable.default.svc.cluster.local:8083/ \ + -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","method":"message/send","id":"1","params":{"message":{"role":"user","messageId":"t1","parts":[{"type":"text","text":"What is Lambda?"}]}}}' + +# Verify trace in CloudWatch +# Console > CloudWatch > Application Signals > GenAI Observability +``` -Two pipelines: -- **Traces:** OTLP receiver (HTTP:4318) β†’ otlphttp/langfuse exporter -- **Metrics:** Prometheus scraper (Bifrost) β†’ AMP remote write +--- -The Langfuse trace export happens directly from the agent SDK, not through the collector. The collector handles infrastructure-level traces (X-Ray service maps) and Bifrost metrics only. +## Platform Infrastructure -## Grafana Dashboards (AMG) +### Centralized (provisioned by task install) -Three agent-platform dashboards in the "Agent Platform" folder: +| Component | Location | Purpose | +|-----------|----------|---------| +| OTel Collector | `otel` namespace (all clusters) | OTLP receiver + Langfuse export + AMP remote write | +| Langfuse v3 | `langfuse` namespace (hub only) | Trace UI + OTLP endpoint | +| AMP Workspace | AWS (Crossplane-provisioned) | Prometheus metrics storage | +| AMG Workspace | AWS (Crossplane-provisioned) | Grafana dashboards | +| Bifrost | `bifrost` namespace (all clusters) | LLM proxy with OTEL plugin + metrics | -### Agent Platform β€” Overview -- OAM Agent pods count -- MCP Server pods count -- LiteLLM/Bifrost readiness -- AgentGateway readiness -- CPU/Memory by namespace (litellm, bifrost, otel, agentgateway-system, mcp-*) -- Pod restarts (last 1h) -- Argo Rollout replicas +### Decentralized (provisioned by task install) -### Agent Platform β€” LiteLLM Gateway -- LiteLLM CPU/Memory -- Bifrost CPU/Memory -- Network I/O +| Component | Location | Purpose | +|-----------|----------|---------| +| Pod Identity | `vela-system` namespace (Crossplane) | IAM role with CW/X-Ray permissions | +| Transaction Search | AWS account-level | One-time CW config (enabled in Taskfile) | -### Agent Platform β€” X-Ray Traces -- Service map (node graph) -- Recent traces (table) +--- -## Access +## Troubleshooting -| Service | URL | Auth | -|---------|-----|------| -| Langfuse UI | `https:///` | Keycloak SSO (user1) | -| Langfuse OTLP | `https:///api/public/otel/v1/traces` | Basic Auth (API keys) | -| AMG (Grafana) | `https://g-.grafana-workspace..amazonaws.com` | Keycloak SSO | -| Langfuse API | `https:///api/public/traces` | Basic Auth (API keys) | +| Problem | Check | +|---------|-------| +| No traces in Langfuse | Agent logs for `StrandsTelemetry` init. Collector logs for export errors. ExternalSecret `langfuse-otel-auth` status. | +| No traces in CloudWatch | Pod Identity association exists (`kubectl get podidentityassociation -n vela-system`). Agent logs for ADOT init. Transaction Search enabled. | +| Cost shows $0 in Langfuse | Seed CronJob logs (`kubectl logs -n langfuse -l app=langfuse-seed`). Check 401 auth errors. | +| Agent crashes on startup | Check if image has ADOT packages. Verify port 4318 (not 4317) for centralized. | +| Metrics not in Grafana | Check `amp_endpoint_url` annotation on cluster secret. Collector logs for AMP errors. | diff --git a/docs/architecture/agent-identity-and-token-exchange.md b/docs/architecture/agent-identity-and-token-exchange.md new file mode 100644 index 00000000..3f5ee8c5 --- /dev/null +++ b/docs/architecture/agent-identity-and-token-exchange.md @@ -0,0 +1,233 @@ +# Agent Identity & Token Exchange β€” Architecture Decisions + +Status: living document. Captures the significant decisions behind the secretless +workload-identity model and the roadmap to user-delegated (on-behalf-of) access. + +Repos / branches: +- OAP (this repo): `aws-samples/sample-open-agentic-platform` β€” branch `feature/oam-for-agents` (PR #33 β†’ `main`). +- Platform: `aws-samples/appmod-blueprints` β€” branch `feature/agent-platform-shapirov`. + +Reference environment: cluster `peeks-hub`, account `929819487611`, `us-west-2`. +EKS OIDC issuer: `https://oidc.eks.us-west-2.amazonaws.com/id/1BABC5C7BFD3BFE9636A486678E1D6F6`. +Component versions: agentgateway `v1.1.0`, Crossplane `v2.2.1` +(functions: environment-configs v0.3.0, patch-and-transform v0.10.0, cel-filter v0.2.0), +Keycloak `26.3.3`, Bifrost `2.1.16`, vela CLI `1.10.7`. + +--- + +## Goal + +Every agent gets a **secretless identity**. Two trust domains: +1. **AgentGateway / MCP** β€” so agents reach MCP servers and other agents. +2. **AWS** β€” so agents call AWS APIs (Bedrock, AgentCore memory, etc.). + +North star: when an agent calls MCP / another agent **on behalf of a user**, the +downstream request must carry the **user as subject** and the **agent as actor** +(delegation) β€” for (a) audit logs that show "API call X executed by agent Y on +behalf of user Z", and (b) authorization decisions based on the invoking +user/group/role, not just the agent's blanket workload trust. + +--- + +## ADR-1 β€” LLM gateway is Bifrost (context) + +Migrated LiteLLM β†’ Bifrost. Bedrock via EKS Pod Identity; model alias +`claude-sonnet`. Agent uses Strands `OpenAIModel` against Bifrost `/v1`. Governance +VK auth currently disabled (`enforceAuthOnInference: false`). Not identity-critical +but sets the "gateway is the boundary" pattern. + +## ADR-2 β€” Inbound workload identity = "Shape A" (gateway trusts the cluster EKS OIDC) + +**Decision.** Agents authenticate to AgentGateway with their **projected Kubernetes +ServiceAccount token** (audience `agentgateway`). AgentGateway validates it against +the **cluster's own EKS OIDC issuer**, added as a *second* JWT provider on the +gateway policy (alongside the Keycloak provider used for humans). + +**Alternatives rejected:** +- *Keycloak-issued workload token (inbound "Shape B")* β€” Keycloak's external-token + grants (JWT Authorization Grant / legacy token exchange) require a **confidential + client secret** and a **per-workload linked Keycloak user**; not secretless, heavy. +- *client_credentials with a per-workload secret* β€” a secret per workload; rejected. +- *SPIRE* β€” heavier infra; kept as a fallback if the SA-token path proves insufficient. + +**Rationale.** No secret; kubelet auto-rotates the token; per-workload identity is +free (`sub = system:serviceaccount::`); **environment isolation is free** +(dev/prod clusters have distinct OIDC issuers, so a dev token is cryptographically +invalid at the prod gateway). Keycloak remains the **human** IdP unchanged. + +**Consequences / how.** +- Gateway `AgentgatewayPolicy.traffic.jwtAuthentication.providers` gets a 2nd + provider (issuer = cluster OIDC, JWKS over a static `AgentgatewayBackend` + host `oidc.eks..amazonaws.com:443` with inline `policies.tls: {}`). +- Authz CEL allows both shapes: + `(has(jwt.realm_access) && jwt.realm_access.roles.exists(r, r=="default-roles-platform")) || jwt.sub.startsWith("system:serviceaccount:")`. +- The agent app reads the token from `WORKLOAD_TOKEN_PATH` and sends + `Authorization: Bearer` to MCP (via `streamablehttp_client(headers=...)`). + +**Verified:** agent pod β†’ gateway β†’ `mcp-time` returns tools (HTTP 200; was 401 +before the provider + agent-code wiring). + +## ADR-3 β€” Identity is composed via OAM traits; the agent owns its ServiceAccount + +**Decision.** Identity is modeled as **KubeVela traits** attached to a component, +not baked into each ComponentDefinition: +- `gateway-identity` β€” projects the `agentgateway`-audience SA token + sets `WORKLOAD_TOKEN_PATH`. +- `aws-service-identity` β€” grants AWS IAM identity (see ADR-4). + +**Key constraint that drove the design:** a **trait can only read `context.name`** +(the component name) β€” it *cannot* read a component's `parameter.name`. So anything +a trait must reference (the ServiceAccount, the container) has to be named +`context.name`. Therefore: +- The `agent` component was refactored to key **everything** off `context.name` / + `context.namespace` (Rollout, Services, container, SA, `AGENT_NAME`, gateway route) + and to **own a dedicated ServiceAccount** = `context.name`. The `name`, + `namespace`, and `serviceAccount` parameters were **removed** (breaking change: + OAM Applications drop `properties.name/namespace`). +- Cloud-agnostic naming: `aws-service-identity` (future `gcp-service-identity`, …). +- Added a generic `service-rollout` component (Argo Rollout + health gating, owns + its SA) as the base for non-agent workloads. `appmod-service`/`dp-service-account` + kept for compatibility. + +## ADR-4 β€” AWS identity = Pod Identity, "Option C" (self-inject + init-wait) via an XPodIdentity Composition fed by `env-config` + +**Decision.** `aws-service-identity` emits a **`PodIdentity` claim** +(`platform.gitops.io`, appmod XRD/Composition). The **`XPodIdentity` Composition** +resolves `clusterName`/`region` from the ambient **`env-config` EnvironmentConfig** +(via `function-environment-configs`) and creates the IAM Role +(`-role`) + `PodIdentityAssociation`. The developer passes **no +cluster parameters** β€” only optional `accessFor` (sibling component policies). + +**Why "Option C" (self-inject + init-wait).** EKS Pod Identity injects creds via a +**mutating webhook at pod admission**, which only fires if the association already +exists β†’ a race that broke the pure-trait approach historically. Option C makes the +pod self-inject the creds env (`AWS_CONTAINER_CREDENTIALS_FULL_URI`) + the +`pods.eks.amazonaws.com` projected token, and adds a `wait-for-aws-identity` init +container that blocks until `aws sts get-caller-identity` succeeds. **Verified on +cluster:** the EKS webhook *skips* injection when the creds env is already present +(no duplicate-volume conflict), and STS resolves without a region env. + +**`env-config` is the ambient metadata contract.** A cluster-scoped Crossplane +`EnvironmentConfig` named `env-config` on every cluster, carrying at least +`clusterName`, `region`, `vpcId`, `privateSubnetIds`, `publicSubnetIds`. Only +Compositions can consume it (not KubeVela, not raw MRs) β€” hence the `XPodIdentity` +Composition indirection. + +**Verified:** `PodIdentity` claim β†’ Role + `PodIdentityAssociation` Ready +(`clusterName=peeks-hub` from `env-config`); agent pod init-wait logs "AWS identity +ready"; in-pod STS returns the assumed role. + +## ADR-5 β€” Per-cluster EKS OIDC issuer surfaced as the `eks_oidc_provider` annotation + +**Decision.** The gateway's workload JWT provider is templated per-cluster from an +`eks_oidc_provider` cluster-secret annotation: +- **Spokes:** the `platform-cluster` Crossplane Composition writes it from + `status.oidcIssuer` (same Observe+Update `Object` pattern as `aws_vpc_id`); + automatic for every spoke at provision time. +- **Hub:** set by the OAP `Taskfile` (`agentic:hub-oidc-annotation`), because the + hub is bootstrapped from a kind cluster before the platform exists (chicken-and-egg). + +## Key gotchas (do not re-discover) + +- **Crossplane ProviderConfig is `default`**, not `provider-aws-config` (the latter + is stale in `dp-service-account`). Applies to the classic `*.aws.upbound.io` + provider. The namespaced `*.aws.m.upbound.io` family has **no** ProviderConfig on + this cluster β€” so use the **classic** provider (this is why `agentcore-memory` was + switched from `bedrockagentcore.aws.m.upbound.io` β†’ `bedrockagentcore.aws.upbound.io` + + `default`). +- **Keycloak 26.3.3 token-exchange matrix:** Standard Token Exchange v2 (GA) is + internal-internal only (subject must be a Keycloak access token). External-token β†’ + Keycloak token needs Legacy Token Exchange V1 (preview, deprecated) or JWT + Authorization Grant (26.5 preview) β€” both require a **confidential client** and a + **linked user**. This is why inbound identity is Shape A, not Keycloak-brokered. +- ArgoCD stuck sync operations pin an old git revision; terminate with + `kubectl patch app -n argocd --type merge -p '{"operation":null}'` then refresh. +- OAM defs must be lowercase-hyphen (RFC-1123). Regenerate with + `bash platform/oam/generate.sh` after editing CUE; commit both `.cue` and generated YAML. + +--- + +# Roadmap β€” User-delegated access via gateway token exchange + +Target = ADR-6 (below). This is the mechanism that "checks the security boxes": +per-call audit of *user + agent*, and user/group/role-based authorization on MCP/API +calls. + +## ADR-6 (TARGET) β€” User on-behalf-of via AgentGateway backend token exchange + +**Model.** Gateway-side **backend** auth exchanges the inbound bearer for a +downstream token before calling the upstream, using **RFC 8693 delegation**: +`subject_token = user token`, `actor_token = agent's SA token` β†’ downstream token +with `sub = user`, `act = agent`, audience-scoped to the target MCP/agent. Secrets +(the exchange client credential) live at the **gateway** (a k8s Secret), never in +the agent. Our existing pieces compose: **Shape A SA token = the `actor`**, the +**Keycloak user token = the `subject`**. + +**Standards / grants** (all under agentgateway `backendAuth.oauthTokenExchange`): +- RFC 8693 token exchange (`subject_token`, plus `actorToken`, `resources` per RFC 8707). +- RFC 7523 jwt-bearer / JWT assertion (`assertion`) β€” matches Keycloak JWT Authorization Grant. +- Entra OBO (jwt-bearer + `requested_token_use=on_behalf_of`). +Multi-hop (agentβ†’agentβ†’MCP) = OAuth Identity & Authorization Chaining / ID-JAG +(token-exchange + jwt-bearer composition; agentgateway `cross_app_access`). + +### Blocker β€” pending agentgateway release + +- **Feature status in agentgateway:** MERGED to `main`. Data plane in + `crates/agentgateway/src/http/auth/oauth/` (mod/transport/cross_app_access), + controller `backend_policies.go` + `agentgateway_policy_types.go`, e2e tests, and + `examples/traffic-token-exchange/{oauth-rfc8693,jwt-authz-grant}`. PRs **#2189** + (data plane) and **#2458** (controller). Blog: agentgateway.dev/blog/2026-07-12-…-token-exchange-jwt-assertion-entra-obo. +- **Installed version:** agentgateway **v1.1.0** (proxy + controller). Its + `AgentgatewayPolicy` `spec.backend.auth` keys are + `[aws, azure, gcp, key, passthrough, secretRef]` β€” **no `oauthTokenExchange`**. +- **Therefore:** token exchange is NOT usable on our cluster yet. It requires + upgrading agentgateway to the release that ships #2189/#2458 (post-v1.1.0; as of + 2026-07-13 appears to still be `main`/pre-release β€” verify a tagged release before + the bump). + +### Phased plan + +1. **Track & upgrade agentgateway.** Wait for / pin the release exposing + `backend.auth.oauthTokenExchange`; bump the `agentgateway`/`agentgateway-crds` + addons; re-verify the CRD has the field. +2. **Propagate user identity through the agent (the main app change).** Today the + agent calls MCP with its *own* SA token. For OBO it must capture the inbound + **user** token (A2A auth passthrough) and forward it as the subject on outbound + MCP calls; the SA token becomes the actor. This is the largest new piece. +3. **Attach a backend exchange policy** (`AgentgatewayPolicy` with + `backend.auth.oauthTokenExchange`) in front of the MCP backends: `subject_token` + = user token, `actorToken` = agent SA token, audience-scoped per tool; token + endpoint as an `AgentgatewayBackend`, client secret from a k8s Secret. +4. **Authorization at the gateway.** Enforce user/group/role (from the user's + claims) as the chokepoint β€” a tool call is allowed only if the invoking user is + authorized, not merely because the agent workload is trusted. +5. **Multi-hop chaining (ID-JAG)** for agentβ†’agentβ†’MCP, preserving the original user + and accumulating the actor chain so the audit trail stays intact. + +### Open questions to resolve during the spike + +- **Keycloak delegation support:** can our Keycloak issue a genuine RFC 8693 + delegation token with an `act` claim (vs impersonation)? Historically Keycloak is + strongest on internal-internal + impersonation; verify before committing. (May + require Keycloak 26.5 JWT Authorization Grant / config, or gateway-constructed + delegation.) +- **Token lifetime:** user tokens are ~1h; the exchanged token TTL is capped by the + subject `exp`. Long autonomous tasks need a refresh/offline strategy or must be + bounded to the user session. +- **Not fully secretless:** the exchange requires a client secret, but at the + **gateway** (correct place), not per-agent; the inbound SA-token path stays secretless. + +--- + +## Current state (implemented + verified) + +| Area | State | +|---|---| +| Bifrost LLM gateway (Bedrock, claude-sonnet) | done, verified | +| Gateway identity (Shape A) β€” SA token validated by cluster EKS OIDC | done, verified (agentβ†’gatewayβ†’mcp-time 200) | +| `gateway-identity` trait + agent reads `WORKLOAD_TOKEN_PATH` | done, verified | +| `aws-service-identity` trait β†’ `XPodIdentity` β†’ Role + PodIdentityAssociation | done, verified (STS in-pod) | +| `env-config` EnvironmentConfig (clusterName/region) | live on hub | +| `service-rollout` component; `agent` refactored to `context.name` + owns SA | done | +| `agentcore-memory` classic provider + `default` | done, memory provisions | +| Per-cluster `eks_oidc_provider` (spoke Composition; hub Taskfile) | done | +| **User-delegated token exchange (OBO)** | **blocked on agentgateway release (ADR-6)** | diff --git a/docs/architecture/diagrams/img/dark-factory-flow.svg b/docs/architecture/diagrams/img/dark-factory-flow.svg new file mode 100644 index 00000000..a2b46421 --- /dev/null +++ b/docs/architecture/diagrams/img/dark-factory-flow.svg @@ -0,0 +1,3 @@ + + +
Dark Factory β€” GitHub issue β†’ autonomous, reviewed, merged PR
Untrusted, code-writing agents run inside hardware-isolated Kata micro-VMs, next to the control plane β€” safely.
🏷️ GitHub issue
labeled dark-factory
Argo Events
Sensor
df-run
Argo Workflow
πŸ”’ Kata micro-VM

coder: Claude Code / Kiro
credential-less Β· network-locked
πŸ”€ Pull Request
opened
AWS DevOps Agent
release readiness
(cross-repo Β· standards Β· access-control)
AWS Security Agent
code security review
(OWASP Β· secrets Β· IAM Β· deps)
Holdout gate
terraform validate
ephemeral deploy-test
πŸ‘€ Human
approve?
df-merge-teardown
squash-merge + reap the sandbox VM
βœ… Merged to main
cleared
yes
Text is not SVG - cannot display
\ No newline at end of file diff --git a/docs/architecture/diagrams/img/oap-layered.svg b/docs/architecture/diagrams/img/oap-layered.svg new file mode 100644 index 00000000..cd5d525b --- /dev/null +++ b/docs/architecture/diagrams/img/oap-layered.svg @@ -0,0 +1,3 @@ + + +
Open Agentic Platform β€” Layered Architecture
AGENT WORKLOADS
Financial Advisor | K8s Ops Agent | Multi-Tool Agent | Custom Agents (BYOA)
AGENT PLATFORM CAPABILITIES (OAP)
Model as a
Service
Agent
Identities
Agent
Gateway
Agent
Isolation
Agent
Runtime
Agent
Lifecycle
Agent
Memory
Agent
Browser
Agent Code
Interpreter
Agent
Observability
Agent
Evaluation
PLATFORM ENGINEERING
Developer
Portal
Identity
& Access
Infra as
Code
Continuous
Delivery
Workflow
Orchestration
Service
Discovery
Packaging /
Templating
Code
Repository
Config
Repository
Artifact
Registries
Secret
Repository
Observability
Security & Governance
Signing
COMPUTE PLATFORM β€” Amazon EKS Auto Mode
AWS Cloud
Text is not SVG - cannot display
\ No newline at end of file diff --git a/docs/architecture/diagrams/img/oap-managed-or-oss.svg b/docs/architecture/diagrams/img/oap-managed-or-oss.svg new file mode 100644 index 00000000..6d620ded --- /dev/null +++ b/docs/architecture/diagrams/img/oap-managed-or-oss.svg @@ -0,0 +1,3 @@ + + +
Open Agentic Platform on AWS
CNCF-aligned Β· cloud-agnostic Β· AgentCore managed OR OSS alternative per capability
COMPUTE PLATFORM β€” cloud-agnostic
Amazon EKS
ROSA
SUSE Rancher
Self-Managed K8s
AGENT ABSTRACTION β€” Kro / KubeVela CRDs (orchestrate Crossplane Β· ACK Β· OpenTofu)
Imageless Agents

CRD-defined Β· no container image
auto AuthN/AuthZ Β· declarative K8s
BYO-Image Agents

Strands SDK Β· Spring AI Β· Rust
LangGraph Β· CrewAI Β· any OCI image
CAPABILITY BACKENDS β€” AgentCore managed Β·ORΒ· OSS alternative per capability
Memory

AgentCore
β€” or β€”
Milvus Β· Qdrant Β· Mem0
Gateway

AgentCore
β€” or β€”
KGateway Β· LiteLLM Β· Envoy
Identity

AgentCore
β€” or β€”
Keycloak Β· SPIFFE/SPIRE
Observability

AgentCore
β€” or β€”
Langfuse Β· OTel Β· Prometheus
Policy

AgentCore
β€” or β€”
NeMo Β· OPA Β· Guardrails
Evaluations

AgentCore
β€” or β€”
Ragas Β· DeepEval
Code Interp.

AgentCore
β€” or β€”
gVisor Β· Kata Β· Firecracker
Browser

AgentCore
β€” or β€”
Playwright Β· Selenium
Amazon Bedrock
Claude 3.x / 4.x
Llama 3.x
Mistral
Amazon Nova
Titan Embeddings
Self-Hosted LLM
vLLM / RHOAI
llm-d
llama.cpp
AWS Neuron
Text is not SVG - cannot display
\ No newline at end of file diff --git a/docs/architecture/diagrams/src/dark-factory-flow.drawio b/docs/architecture/diagrams/src/dark-factory-flow.drawio new file mode 100644 index 00000000..dc036a9b --- /dev/null +++ b/docs/architecture/diagrams/src/dark-factory-flow.drawio @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/architecture/diagrams/src/oap-layered.drawio b/docs/architecture/diagrams/src/oap-layered.drawio new file mode 100644 index 00000000..83dd77fb --- /dev/null +++ b/docs/architecture/diagrams/src/oap-layered.drawio @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/architecture/diagrams/src/oap-managed-or-oss.drawio b/docs/architecture/diagrams/src/oap-managed-or-oss.drawio new file mode 100644 index 00000000..8bad92df --- /dev/null +++ b/docs/architecture/diagrams/src/oap-managed-or-oss.drawio @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/dark-factory/AGENT-INSTALL.md b/docs/dark-factory/AGENT-INSTALL.md new file mode 100644 index 00000000..b9e9f515 --- /dev/null +++ b/docs/dark-factory/AGENT-INSTALL.md @@ -0,0 +1,145 @@ +# Connecting the AWS DevOps Agent & AWS Security Agent to the Dark Factory + +The Dark Factory pipeline reviews every PR with two managed AWS agents: + +- **AWS DevOps Agent** β€” *Release Readiness* code review: cross-repo dependency risk, standards + compliance, access-control correctness, and (optionally) build+test in an AWS-managed environment. + Verdict: **BLOCK / Proceed with Caution / Safe to Release**. +- **AWS Security Agent** β€” code security review: OWASP Top 10, hardcoded secrets, IAM misuse, + dependency risk, with inline findings + recommended fixes. + +Both integrate with GitHub the same way: **install a GitHub App, connect the repo to an Agent +Space, enable review.** After that, every pull request is reviewed automatically and the agents post +their verdicts back onto the PR. This page is the end-to-end setup β€” follow it top to bottom. + +> **Prerequisites:** an AWS account with the DevOps Agent + Security Agent enabled, admin access to +> that account, and **owner/admin** rights on the GitHub org or user that owns your repo (needed to +> install a GitHub App). The examples use account `940019131157`, region `us-west-2`, repo +> `elamaran11/dark-factory-sandbox` β€” substitute your own. + +--- + +## Part A β€” AWS DevOps Agent (Release Readiness code review) + +### A1. Create / open an Agent Space +1. AWS Console β†’ **AWS DevOps Agent**. +2. Create an Agent Space (or open an existing one), e.g. **`dark-factory`**. + +### A2. Register GitHub (account level) +1. In the Agent Space β†’ **Capabilities** tab β†’ **Pipeline** section β†’ **Add** β†’ **GitHub**. +2. Choose **User** or **Organization** (must match where the repo lives), then submit. +3. GitHub opens its **authorize** screen β†’ authorize AWS DevOps Agent. +4. On the **Install & Authorize** page, choose **Only select repositories β†’ your repo** (or All), + then **Install & Authorize**. You're returned to the console with GitHub registered. + > Permission level: **Read & Write** (default) β€” lets the agent post PR comments, check-runs, and + > optional remediation PRs. Read-Only disables those write actions. + +### A3. Connect the repo to the Agent Space + enable review +1. In the Agent Space β†’ **Capabilities** β†’ **Pipeline** β†’ **Add** β†’ **GitHub** β†’ pick the + registration β†’ select your repo β†’ **Add**. +2. In **Code Review and Automated Testing**, per repo: + - **Auto trigger change review** = **ON** β€” reviews every PR automatically. + - **Automated verification testing** = ON (optional) β€” builds/tests the change in an AWS-managed + verification environment (deeper than static analysis). + - **Runtime role** (optional) β€” an IAM role the agent assumes for private-registry/artifact + access during builds. +3. **Save.** + +### A4. What you'll see on a PR +On every new/updated PR the DevOps Agent posts a status/check-run +**`aws-devops-agent/release-readiness-review`** (`pending` β†’ `success`/`failure`) with a link to the +full report, plus inline comments for any risks it finds (e.g. an unpinned image, a missing variable +default, an over-broad IAM change). A clean change gets **"change approved"**. + +--- + +## Part B β€” AWS Security Agent (code security review) + +The Security Agent can review a PR **two ways** β€” you can use either or both: + +### Path B1 (recommended) β€” GitHub App: inline bot findings +1. AWS Console β†’ **AWS Security Agent** β†’ your Agent Space (e.g. **`dark-factory`**). +2. **Integrations β†’ GitHub β†’ Connect** (or open + `https://github.com/apps/aws-security-agent/installations/new`). Authorize + install the App on + your repo, **Read & Write** (so it can post inline comments + optional fix PRs). +3. Back in the Security Agent console, **add the GitHub integration to your Agent Space** and + **enable code review** on the repo. *(Installing the App on GitHub and connecting it to the Agent + Space are two distinct steps β€” do both.)* +4. On the next PR the agent posts as **`aws-security-agent[bot]`**: an "AWS Security Agent is + reviewing…" notice, then inline findings (or **"No issues identified"**), with recommended fixes. + +### Path B2 β€” headless code-review API (no GitHub App) +Fully API-driven β€” useful for automation that shouldn't depend on a GitHub App. The Dark Factory's +`security-agent` step already implements this (`scripts/security-agent.sh`): stage the diff to S3, +then: +``` +aws securityagent create-code-review --agent-space-id --assets '{"sourceCode":[{"s3Location":"s3://.../src.zip"}]}' --service-role +aws securityagent start-code-review-job --agent-space-id --code-review-id --diff-source '{"s3Uri":"s3://.../diff.patch"}' +aws securityagent list-findings --agent-space-id --code-review-job-id +``` +Findings come back with `riskType`, `riskLevel` (INFORMATIONALβ†’CRITICAL), and `confidence`. The IAM +this needs (a service role trusting `securityagent.amazonaws.com` + an IRSA role for the workflow + +an S3 bucket) is committed as Terraform in `gitops/addons/charts/dark-factory/iam/securityagent.tf`, +and the Agent Space is reconciled by the PreSync bootstrap Job +(`templates/06-securityagent-bootstrap.yaml`). **The Dark Factory runs both paths** β€” the App for +inline bot findings and the headless path for the merge-gate signal. + +--- + +## Part C β€” How the Dark Factory pipeline uses the agents + +Once the agents are connected, the `df-run` workflow wires them into the PR lifecycle (ordering: +**DevOps first, then Security**): + +1. Coder opens the PR β†’ posts `dark-factory:coding` + `dark-factory:local-test` comments. +2. **`devops-gate`** waits for the DevOps Agent's `aws-devops-agent/release-readiness-review` verdict. + On a clear verdict it applies the **`needs-security-review`** label. *(Config: + `devopsAgent.checkContext` / `devopsAgent.checkRunName` in `values.yaml` β€” the check name the gate + watches for.)* +3. **`security-agent`** runs (gated on that label) β†’ the Security Agent reviews the diff; the + `aws-security-agent[bot]` also comments inline (Path B1). +4. **`deploy-test`** runs for deployable changes (`terraform validate` for `*.tf`, ephemeral-namespace + apply for k8s). +5. **Sticky status** rewrites the PR body into one board (build+tests, security, devops, deploy-test). +6. A human **approves** the PR β†’ the `df-merge-teardown` workflow **squash-merges** and reaps the + sandbox. *(GitHub blocks a PR author from approving their own PR, so the approver must be a + different identity than the one the coder opens PRs as.)* + +### Relevant `values.yaml` knobs +```yaml +devopsAgent: + enabled: true + gate: check # wait for the DevOps Agent check-run (native model) + checkContext: "(aws-devops-agent/release-readiness-review|...)" # JS regex (no (?i) flag) + checkRunName: "aws-devops-agent/release-readiness-review" # exact check name +securityAgent: + enabled: true # headless S3-diff path (Path B2) + app: + enabled: true # GitHub App inline bot (Path B1) + checkContext: "" # set only if the App posts a check/status to gate on + checkRunName: "" +``` + +--- + +## Verify the setup + +```bash +# Open a PR in the connected repo, then within a few minutes: + +# DevOps Agent posted its review? +gh api repos///commits//status \ + --jq '.statuses[] | select(.context|test("devops")) | {context,state,description}' + +# Security Agent bot commented inline? +gh api repos///issues//comments \ + --jq '.[] | select(.user.login=="aws-security-agent[bot]") | .body[0:80]' + +# Security Agent integration recorded on the space (headless path)? +aws securityagent list-integrations --query 'integrationSummaries[].provider' +aws securityagent list-integrated-resources --agent-space-id +``` + +If the DevOps check never appears, the repo isn't connected to the Agent Space (Part A3). If the +Security bot never comments, the GitHub App isn't connected to the Agent Space (Part B1 step 3) β€” +installing the App on GitHub alone is not enough. diff --git a/docs/dark-factory/PROFILES.md b/docs/dark-factory/PROFILES.md new file mode 100644 index 00000000..edd15c64 --- /dev/null +++ b/docs/dark-factory/PROFILES.md @@ -0,0 +1,56 @@ +# Dark Factory β€” language & stack support (no profiles) + +> **Decision:** the Dark Factory does **not** use per-language "profiles" in platform config. +> Language support is decoupled into the **coder image** (toolchains) and the **target repo** +> (build/test discovered from marker files) β€” devs control it, the platform stays generic. +> An earlier design added a `stackProfiles` map + a `dark-factory-` label; it was removed as +> redundant (the coder already auto-detects, and `detect-deployable` already classifies verification). + +## How a stack is supported β€” three decoupled layers + +1. **Toolchains β†’ the coder image.** `examples/dark-factory/coder/Dockerfile` is a generic image that + carries `git`, `node`, `python3`, `go`. To support Java/Rust/etc., add the toolchain **to the + image** (or maintain a variant image) β€” not to platform config. This is the dev/image concern. + *(The trusted `deploy-test` image separately carries `kubectl` + `terraform`.)* + +2. **Build/test β†’ discovered from the repo.** The coder picks the build/test command from the repo's + own marker files β€” devs control it by their repo layout, with a `Makefile` as the explicit override: + + | Marker in the repo | Coder runs | + |---|---| + | `Makefile` with a `test:` target | `make test` *(explicit dev override β€” checked first)* | + | `package.json` | `npm install` + `npm test` | + | `go.mod` | `go test ./...` | + | `pyproject.toml` / `setup.py` / `requirements.txt` | `pytest -q` | + | `Cargo.toml` | `cargo test` | + | `pom.xml` | `mvn -q test` | + | `build.gradle[.kts]` | `./gradlew test` | + | *(none β€” e.g. Terraform/config change)* | skipped (no unit suite; `deploy-test` still validates) | + +3. **Verification kind β†’ auto-detected.** `detect-deployable` classifies the changed files and + `deploy-test` runs the right tool β€” `*.tf` β†’ `terraform validate`; `Chart.yaml`/`k8s/`/`Dockerfile` + β†’ deploy into an ephemeral namespace. No label, no config. + +## Why no profiles (the reasoning) + +- The one thing a profile added over auto-detection was a `scaffoldHint` string β€” and the **issue text + already states the stack** ("Terraform for an S3 bucket", "a Spring Boot service"), which the coder + reads directly. The build/test commands and the verify kind were **already** covered by marker-file + detection and `detect-deployable`. +- Keeping per-language build/test in Helm values meant the platform had to know every language β€” the + opposite of generic. Pushing it to the **image** (toolchains) and the **repo** (marker files) keeps + the platform language-agnostic and puts control where it belongs: with the devs/repo. + +## Adding a new language + +1. Add its toolchain to the coder image (`coder/Dockerfile`) β€” e.g. `apk add openjdk maven`. +2. Ensure `buildAndTest()`'s marker list covers it (most already are; add a marker if exotic). +3. That's it β€” no values, no label, no pipeline change. A repo `Makefile test` target works for + anything without even a marker. + +## Greenfield note + +For a brand-new/empty repo with no marker files yet, the coder relies on the **issue text** to know +the stack (usually sufficient) and creates the idiomatic project (which then has the marker files a +re-run/iterate would detect). If a repo wants to be explicit, a committed `Makefile` (`build`/`test` +targets) is the clean, dev-owned contract. diff --git a/docs/dark-factory/README.md b/docs/dark-factory/README.md new file mode 100644 index 00000000..cb097cbd --- /dev/null +++ b/docs/dark-factory/README.md @@ -0,0 +1,888 @@ +# Dark Factory β€” Autonomous Agent Coding Pattern + +> **Status:** Design **+ implementation**. Phases **P0–P3b** are built and running on the hub (see +> [Β§12](#12-phased-delivery)). The **full event-driven lifecycle** works hands-off: a labeled issue β†’ +> coder β†’ holdout gate + Security/DevOps reviews β†’ PR with live status; a **PR comment β†’ bounded +> revision** (`df-iterate`); a **human approval β†’ green-gated merge + teardown** (`df-merge-teardown`). +> All verified end-to-end on a live cluster. This doc describes the architecture, the reuse map onto +> the platform, and what remains (P4: conditional deploy-test + reaper + metrics; P5). + +A **dark factory** is a manufacturing plant that runs *with the lights off* β€” no humans on the +floor, robots do everything. Applied to software: **a human writes an issue (a spec); AI agents +do the rest** β€” implement, build, test, security/ops review, open a PR, and (after a human +approves the *results*) merge and tear everything down. + +This pattern wires that idea onto the **Open Agent Platform (OAP)** using components the platform +already has: hardware-isolated **Kata micro-VM sandboxes** (from `eks-platform-openclaw`), the +**Bifrost β†’ Bedrock** LLM gateway, **Argo Workflows** on the hub as the orchestrator, the +**hub + spoke** cluster fleet, and **AWS-managed frontier agents** (Security, DevOps) for +independent review. The factory itself runs on the **hub build plane**; its output ships to the +spokes as normal deployments. + +--- + +## Table of contents +1. [What is a Dark Factory](#1-what-is-a-dark-factory) +2. [Two flows at a glance](#2-two-flows-at-a-glance) +3. [Flow A β€” Agent Sandbox capability](#3-flow-a--agent-sandbox-capability-permanent-platform-feature) +4. [Flow B β€” the Dark Factory pipeline](#4-flow-b--the-dark-factory-pipeline) + - [Flow D β€” Lambda MicroVM substrate (alternative to Flow A)](#45-flow-d--lambda-microvm-substrate-alternative-to-flow-a) +5. [The pluggable coding assistant](#5-the-pluggable-coding-assistant) +6. [Independent verification](#6-independent-verification-the-heart-of-the-pattern) +7. [Live status in the PR](#7-live-status-in-the-pr) +8. [Human-in-the-loop & the comment loop](#8-human-in-the-loop--the-iterative-comment-loop) +9. [Lifecycle, teardown & cost](#9-lifecycle-teardown--cost) +10. [Security model](#10-security-model) +11. [Industry alignment & anti-patterns](#11-industry-alignment--anti-patterns-what-the-world-agrees-on) +12. [Phased delivery](#12-phased-delivery) +13. [Open questions / future work](#13-open-questions--future-work) +14. [References](#14-references) + +--- + +## 1. What is a Dark Factory + +The term is borrowed from manufacturing and popularized for coding by two sources this design +draws on: + +- **Steve Yegge β€” "Welcome to Gas City"**: a *supervisor plane* that deploys teams of + collaborating agents as composable "packs," where humans **watch the factory work** from a + rich console rather than typing code. Work is a first-class, versioned primitive. +- **HackerNoon β€” "The Dark Factory Pattern"**: an **autonomy-level** ladder and the key + engineering ideas β€” **specs instead of code**, **holdout scenarios** (the coding agent never + sees the acceptance tests; a separate evaluator judges), **build-before-push**, ephemeral + environments, and **humans reviewing results, not diffs**. + +### Autonomy levels (we target Level 3) + +| Level | What it looks like | +|------:|--------------------| +| 1 | AI finishes your sentences; you do everything else. | +| 2 | AI writes whole files; **you review every change**. | +| **3** | **AI generates code from a spec; a holdout gate + reviewers verify; you approve the merge.** ← *this design* | +| 3.5 | Some low-risk services auto-merge without you. | +| 4 | Full dark factory: specs in, merged tested code out. | + +At **Level 3**, the human's job shrinks from *"read every line of a diff"* to *"read the evidence +and click approve"* β€” but the human still gates the merge, and can drive changes via PR comments. + +--- + +## 2. Two flows at a glance + +This design is deliberately split into **two independent flows** so the sandbox capability is +useful on its own and the factory is a consumer of it. + +| | **Flow A β€” Agent Sandbox capability** | **Flow B β€” Dark Factory** | +|---|---|---| +| **What** | A permanent platform feature: Kata micro-VM sandboxes + `Sandbox` CRD + a **warm pool** kept ready | The autonomous coding pipeline: issue β†’ code β†’ test β†’ review β†’ PR β†’ merge β†’ teardown | +| **When** | Comes up automatically when the agent platform is deployed | Triggered per GitHub issue labeled `dark-factory` | +| **Where** | Installed on the **hub cluster** β€” the build/author plane, co-located with Argo Workflows | **Runs on the hub cluster**, orchestrated by Argo Workflows, co-located with the sandbox pool | +| **Lifecycle** | Long-lived; pool self-heals to a target buffer | Ephemeral per issue; torn down on merge/close | +| **Diagram** | [`diagrams/flow-a-sandbox-capability.md`](diagrams/flow-a-sandbox-capability.md) | [`diagrams/flow-b-dark-factory.md`](diagrams/flow-b-dark-factory.md) | + +> **Why split them?** The sandbox capability is generically useful (any agent workload can claim +> an isolated VM). The Dark Factory is one *consumer* of that capability. Keeping them separate +> means the isolation substrate can ship, be tested, and be reused independently of the factory. + +> **Flow D β€” a second substrate.** Flow A's isolation boundary is a **Kata micro-VM pod** on a +> platform-owned nested-virt node group. **Flow D** offers an *alternative* Flow-A substrate β€” an +> **AWS Lambda MicroVM** (serverless micro-VM, no node group) provisioned via the ACK `lambdamicrovms` +> controller and composed by a single **KRO `ResourceGraphDefinition`**. Flow B is unchanged and can +> target either substrate through the same `SandboxClaim` contract. See +> [Β§4.5](#45-flow-d--lambda-microvm-substrate-alternative-to-flow-a) and +> [`diagrams/flow-d-microvm-sandbox.md`](diagrams/flow-d-microvm-sandbox.md). *(Flow C is reserved for +> other work.)* + +> **Why the hub, not a spoke?** The Dark Factory is a **pre-dev build/author** activity: it *writes* +> code and needs GitHub write access. That belongs on the **hub β€” the control/build plane** β€” not on +> a spoke, which is the **deploy/run plane** hosting real enterprise workloads (putting a +> GitHub-write-capable author of untrusted code next to running apps is the wrong placement). The +> factory's output β€” *merged, reviewed* code β€” then flows to the spokes as a normal deployment, which +> is the right place for deployment-time security/DevOps gating. Co-locating the sandbox pool with +> Argo Workflows on the hub also keeps orchestration **single-cluster**: the workflow watches the +> coder pod and eval Job directly, owner-references cascade teardown, and no cross-cluster control +> plane is needed. The **Kata micro-VM is the isolation boundary** and travels with the workload β€” +> so "on the hub" is safe *provided* the control-plane-specific hardening in [Β§10](#10-security-model) +> is in place (dedicated tainted kata nodegroup + egress lockdown that denies the hub's own +> control-plane services). + +--- + +## 3. Flow A β€” Agent Sandbox capability (permanent platform feature) + +> πŸ“Š **See the fancy diagrams:** [`diagrams/flow-a-sandbox-capability.md`](diagrams/flow-a-sandbox-capability.md) +> (capability architecture + warm-pool state machine). + +Shipped as a GitOps addon, enabled exactly like every other platform addon β€” an ApplicationSet fans +the three agent-sandbox charts (`agent-sandbox-operator`, `agent-sandbox`, `kata-deploy`) onto the +target cluster. The capability is gated to the **hub** via `alwaysSelector`, which is honoured +regardless of the global `useSelectors` flag: + +```yaml +# gitops/addons/bootstrap/default/addons.yaml (per agent-sandbox entry) +alwaysSelector: + matchExpressions: + - key: environment + operator: In + values: ['control-plane'] # the hub's cluster-secret label β†’ hub-only +``` + +> **Migration note:** the capability was first proven on **spoke-dev** (gated `environment In [dev]`). +> It is being relocated to the **hub** (`environment In [control-plane]`) so it sits with Argo +> Workflows on the build plane β€” see [Β§2](#2-two-flows-at-a-glance) for why, and Phase 2 of the +> implementation plan for the devβ†’hub cutover (stand up on hub, then prune the spoke-dev pool). + +### What the addon installs + +| Piece | Source in this repo | Role | +|---|---|---| +| **Sandbox operator + CRDs** (`agents.x-k8s.io` + `extensions.agents.x-k8s.io/v1beta1`) | `gitops/addons/charts/agent-sandbox/upstream/` (vendored v0.5.1, sync-wave 0) | Materializes one Kata-VM pod per `Sandbox`; serves `SandboxClaim`/`SandboxTemplate`/`SandboxWarmPool` + the conversion webhook | +| **Kata runtime (Cloud Hypervisor default)** | `kata-deploy` OCI chart (sync-wave 1) | Installs the containerd handlers on the tainted kata nodes | +| **RuntimeClasses** `kata-clh` Β· `kata-qemu` | `agent-sandbox/templates/10-runtimeclasses.yaml` (sync-wave 2) | Workload picks its VMM via `runtimeClassName` | +| **`SandboxTemplate` `coder-sandbox`** | `agent-sandbox/templates/20-sandboxtemplate.yaml` | The coder pod spec the warm pool clones (isolation invariants baked in) | +| **`SandboxWarmPool` `coder-warmpool`** | `agent-sandbox/templates/40-sandboxwarmpool.yaml` | Native operator primitive β€” keeps N idle sandboxes pre-warmed; refills on claim | +| **NetworkPolicy** (egress lockdown) | `agent-sandbox/templates/30-networkpolicy.yaml` | Default-deny egress; allow only DNS + Bifrost + HTTPS β€” **plus, on the hub, deny the control-plane services** (see [Β§10](#10-security-model)) | +| **kata-readiness DaemonSet** | `agent-sandbox/templates/15-kata-readiness.yaml` | Removes the `runtime-not-ready` startup taint once kata-deploy is healthy | + +### Hub prerequisites (Auto Mode can't host Kata) + +The hub runs **EKS Auto Mode + Bottlerocket**, which **cannot** run Kata micro-VMs (same blocker +proven on the Auto-Mode spokes). Hosting the capability on the hub therefore requires a **dedicated, +self-managed nested-virt Managed Node Group** alongside Auto Mode: + +| Requirement | Detail | +|---|---| +| **Nested-virt MNG** | `c8i`/`m8i` instances with `cpu_options.nested_virtualization=enabled`, `/dev/kvm` present, `min=0` scale-to-zero. Artifacts in `gitops/addons/charts/agent-sandbox/nodepool/` (`kata-mng.tf` / eksctl / nodeadm userData). | +| **Auto-Mode addon prereqs** | Self-managed nodes get neither CNI nor kube-proxy from Auto Mode β€” the `vpc-cni` **and** `kube-proxy` EKS addons must be installed or the kata node stays `NotReady` / kata-deploy crashloops. | +| **Tainted + labelled** | Node registers `kata=true:NoSchedule` (workload taint) + `katacontainers.io/runtime-not-ready` (startup taint, removed by kata-readiness), labelled `kata-enabled=true` β€” so **coder VMs never co-schedule with hub control-plane pods**. | + +> These are hard requirements: without the nested-virt MNG the warm pool has nowhere to run; without +> the taint + label + egress lockdown, an untrusted coder VM could land next to β€” or reach β€” the hub's +> control-plane services. See [Β§10](#10-security-model). + +> βœ… **As-built (certified on the hub):** the capability was migrated dev β†’ hub via GitOps. A +> `c8i.4xlarge` nested-virt MNG was provisioned, the `vpc-cni` + `kube-proxy` addons installed, and +> the node joined Ready. Verified end-to-end: operator `1/1`, warm pool `3/3`, a `kata-clh` pod runs +> a real micro-VM (guest kernel β‰  host), `SandboxClaim` binds + the pool refills, and the coder is +> blocked from the control plane while Bifrost + public GitHub + DNS work. The old spoke-dev pool was +> torn down. + +### Warm pool β€” instant claims, cheap idle + +When the platform finishes deploying, the operator's native `SandboxWarmPool` brings up a **target +buffer of 2–3 idle sandboxes**. A consumer binds to a *ready* VM instantly (no cold boot). Cycling +rules: + +- **On claim** β†’ provision a **refill** so the buffer stays at target. +- **On release** β†’ if the pool is above target, **remove** the extra idle sandbox. +- **Idle** sandboxes scale to `replicas: 0` (PVC retained) and resume on demand. + +> πŸ’‘ **Cost note (industry gotcha):** a literal pool of *parked, running* micro-VMs burns money. +> The consensus mitigation (E2B, Modal, Bedrock AgentCore) is **snapshot/fork-from-template + idle +> reaping**, not idle VMs left running. Implementation should prefer snapshot-restore where the +> Kata VMM supports it, and always pair the pool with aggressive idle TTL reaping. See +> [Β§9](#9-lifecycle-teardown--cost). + +--- + +## 4. Flow B β€” the Dark Factory pipeline + +> πŸ“Š **See the fancy diagrams:** [`diagrams/flow-b-dark-factory.md`](diagrams/flow-b-dark-factory.md) +> (end-to-end pipeline + detailed sequence + live-status mock). + +Runs on the **hub**, orchestrated by **Argo Workflows** (already deployed on the hub, GitOps-managed). +End to end: + +1. **Trigger** β€” an issue labeled `dark-factory` fires a GitHub **webhook** into an **Argo Events** + Sensor, which submits a `df-run` Workflow. **Dedup:** the workflow is named deterministically + `df-run-`, so GitHub's duplicate webhook deliveries (retries + rapid re-labels) collide + (`AlreadyExists`) and are harmless no-ops β€” **one issue = one in-flight run**. Without this, each + delivery spawned a competing run that force-pushed its own commit and split the `dark-factory/*` + statuses across SHAs. *(Argo Events is the native Kubernetes eventing path; a thin GitHub Action β†’ + `argo-server /api/v1/events` is the fallback if Argo Events isn't enabled.)* +2. **Claim** β€” the workflow's `claim` step creates a `SandboxClaim(warmPoolRef: coder-warmpool)` and + **binds a warm sandbox**; the operator refills the buffer. Because Argo and the pool are on the + **same cluster**, the step watches the claim's `status.conditions[Ready]` directly β€” no + cross-cluster control needed. +3. **Code** β€” the issue is written into the sandbox as `/workspace/SPEC.md`. The **pluggable + coder** (Claude Code headless by default; Kiro headless as a profile) implements on branch + `df/issue-` and **builds + runs unit tests until green** inside the Kata VM, then pushes the + branch. The coder holds only `contents:write` β€” it does *not* open the PR. +4. **PR opens (after green)** β€” the workflow reads the coder's result locally (completed pod + + `/workspace/artifacts/result.json`) and **the workflow opens the PR** once tests are green. Before + this point, status lives on the **issue**; from here on the canonical status board is the **PR**. +5. **Independent verification** β€” *parallel* DAG steps, driven by the workflow, **never by the coder** + (see [Β§6](#6-independent-verification-the-heart-of-the-pattern) and + [diagram B.4](diagrams/flow-b-dark-factory.md#b4--the-df-run-dag-as-built--how-step-gating-works)). + Each step runs **outside** the coder in a trusted hub pod and is gated by a `when:` condition on a + prior step's output (Argo skips it if the condition is false β€” deterministic, not agentic): + - **Holdout gate** βœ… *built* β€” a hub-side step (scenarios in a ConfigMap the credential-less coder + cannot fetch) runs hidden BDD scenarios' **executable tests** (the un-gameable signal) plus a + **different-family Nova judge** that catches *gaming*; β‰₯90% to pass. Gated `when: pr-number != ""`. + - **Security review** β€” the **AWS Security Agent** (managed, read-only on the diff) as the primary + backend; an optional **Fable-5 deep-security sandbox** (a *second* isolated Kata VM) as a gated + deep tier. + - **DevOps review** β€” the **AWS DevOps Agent** (or a Fable-5 reviewer / IaC linters) for + reliability, deployability, cost, observability, and IaC correctness. Advisory in v1. + - **Deploy-test** *(P4, conditional)* β€” for PRs that touch deployable artifacts (a `detect-deployable` + step greps the diff for `Chart.yaml`/`k8s/`/`Dockerfile`), a **trusted** step deploys to an + ephemeral namespace, probes it, and tears it down. This is the only step that holds **K8s access** + β€” never the coder. Its probes are ground truth; the DevOps agent is the advisory second opinion. +6. **Gate + live status** β€” a `gate` step aggregates the findings; each step upserts the **one sticky + PR comment** β³β†’βœ…/❌ with timestamps and log/trace links (single writer = the workflow, serialized + by a per-issue mutex β€” see [Β§7](#7-live-status-in-the-pr)). +7. **Human review** β€” a human reviews the **evidence** (test results, holdout %, security/devops + findings) and either approves or comments. The `df-run` workflow ends here (PR labelled + `df/awaiting-approval`); the human's response arrives as a *new* event. +8. **Iterate** β€” a PR comment fires the Sensor β†’ a `df-iterate` workflow resumes the scaled-to-zero + sandbox (**same retained workspace PVC**) and the coder applies the change. **Bounded to N rounds**, + then a human breaks the tie (see [Β§8](#8-human-in-the-loop--the-iterative-comment-loop)). +9. **Merge + teardown** β€” a PR *review approved* event fires a `df-merge-teardown` workflow: it merges + the PR (the agent **never** self-merges β€” merge only follows an explicit human approval event), + then its `onExit` handler deletes the sandbox, PVC, and eval Job. A **reaper CronJob** sweeps + abandoned/timed-out runs as the crash-net. + +### Orchestration: Argo Workflows on the hub + +The orchestrator is **Argo Workflows**, not a bespoke long-running service. Each issue/comment/review +event submits a **short-lived Workflow** (`df-run`, `df-iterate`, `df-merge-teardown`) keyed on the +issue id β€” durable state lives in the retained workspace PVC + GitHub + a per-issue state ConfigMap, +not in a parked process. This buys per-issue isolation, concurrency (bounded by a semaphore against +the kata nodepool capacity), retries, a durable run history, native Prometheus metrics, and the Argo +UI β€” the substrate for scaling across many concurrent issues. + +> The earlier **P1 Node orchestrator** has been **removed** β€” its claim/coder/sticky-comment logic is +> now the `df-run` DAG's steps (a `resource` template creates the `SandboxClaim`, a `script` step +> polls GitHub, `onExit` tears down). Only the **coder image** remains under +> [`examples/dark-factory/coder/`](../../examples/dark-factory/coder/). + +### Two worked use-cases + +| Issue example | How it's tested | Teardown | +|---|---|---| +| *"Add a `weather-agent` to the examples"* | Deploy into an **ephemeral namespace** on the hub β†’ run holdout scenarios β†’ delete namespace | Namespace + branch artifacts | +| *"Build an EKS cluster with X"* | **Dry-run / crossplane-render** by default; a `deep-test` label spins a **real ephemeral `PlatformCluster`** (appmod-blueprints composition) | Delete the `PlatformCluster` claim | + +--- + +## 4.5. Flow D β€” Lambda MicroVM substrate (alternative to Flow A) + +> πŸ“Š **See the diagrams:** [`diagrams/flow-d-microvm-sandbox.md`](diagrams/flow-d-microvm-sandbox.md) +> (substrate architecture + platform/app ownership split + the RuntimeClass-shim bridge). + +Flow A's isolation boundary is a **Kata micro-VM pod** on a platform-owned nested-virt node group. +**Flow D is a second Flow-A substrate**: an **AWS Lambda MicroVM** β€” a *serverless* micro-VM with no +node group to run or pay for while idle, per-claim lifecycle, and sub-second warm starts. Flow B is +unchanged: it still creates a `SandboxClaim`, a pod still shows up, and the **same `dark-factory-coder`** +runs its coding/testing loop β€” except the coder executes inside a Lambda MicroVM. *(Flow C is reserved +for other work; this substrate is Flow D.)* + +### How it's built β€” KRO RGD over ACK primitives + +| Layer | Mechanism | Notes | +|---|---|---| +| **Composition** | **Managed KRO** (EKS Capability) + one `MicrovmSandbox` `ResourceGraphDefinition` | One CR expands into the IMAGE primitives below (built once); the running `Microvm` is NOT in the graph β€” the shim runs it imperatively | +| **GA primitives** | **Managed ACK** (EKS Capability) β€” `iam` Role, `s3` Bucket | AWS-run; the image store + build/exec roles | +| **Image primitive** | **Self-managed ACK** β€” the pre-GA `lambdamicrovms` controller | `MicrovmImage` CRD (`lambdamicrovms.services.k8s.aws/v1alpha1`); the `Microvm` is created via SDK by the shim, not as a graph resource | + +> **Why self-managed for the MicroVM controller?** Managed ACK bundles only controllers whose service +> is **GA upstream** (see the [ACK community services / GA list](https://aws-controllers-k8s.github.io/community/docs/community/services/)). +> `lambdamicrovms` is **pre-GA** (`v1alpha1`, not on that list), so it isn't in Managed ACK yet β€” it +> runs as its own GitOps addon. **Managed ACK + self-managed lambdamicrovms coexist** (different CRD +> groups β†’ no conflict). When `lambdamicrovms` goes GA, delete the self-managed addon and Managed ACK +> adopts it β€” **the RGD is unchanged**. This "install both now" posture is deliberate and futuristic. +> +> The **Managed KRO + Managed ACK capabilities themselves** are enabled at the platform layer in the +> **appmod-blueprints** repo (an EKS Capability toggle) β€” see that repo's +> `docs/EKS-Capabilities-KRO-ACK-Setup.md`. This repo owns only the **self-managed `lambdamicrovms` +> controller + the KRO `MicrovmSandbox` RGD + the sandbox shim** (Flow D). + +### The split: KRO builds the image ONCE; the shim runs the VM per session + +This is the load-bearing design decision (and it matches the ACK controller's own guidance β€” +image build is slow/declarative, running a VM is fast/imperative): + +- **Platform image β€” declarative, built ONCE by KRO/ACK.** The `MicrovmSandbox` RGD + (`agent-sandbox-lambda/templates/image/`) composes only the slow-changing infra: `MicrovmImage` + (`baseImageARN`, `buildRoleARN`, `codeArtifact.uri` β€” an **S3 zip** of the arm64 `dark-factory-coder` + + a Dockerfile) plus its **build role**, **execution role**, and **S3 artifact bucket** (ACK GA + controllers). A **single committed `MicrovmSandbox` instance** (GitOps-applied) is reconciled once; + KRO gates the handoff on a successful build (`readyWhen state == CREATED||UPDATED`). Its status + surfaces `imageARN` + `executionRoleARN`. The RGD **does not** contain a `Microvm`. +- **Per-session VM β€” imperative, driven by the shim.** Running a MicroVM (`RunMicrovm`), and its + `suspend` / `resume` / `TerminateMicrovm`, are request-time SDK ops the ACK controller does **not** + reconcile. So the shim owns them β€” never a `Microvm` CR per claim. + +### The RuntimeClass shim (claim β†’ pod β†’ MicroVM) + +A literal K8s `RuntimeClass` (like `kata-clh`) maps to a **node-local containerd handler**; Lambda +MicroVM is a **remote AWS service**, so a true node-level RuntimeClass would require a virtual-kubelet +provider (a large Go runtime β€” **out of scope**). Flow D instead ships a **`lambda-microvm` +SandboxTemplate variant** (`agent-sandbox-lambda/templates/shim/`) whose pod is a lightweight +**bridge**: on claim it **reads the platform image handoff** (`imageARN` + `executionRoleARN` from the +one built `MicrovmSandbox`) and calls **`RunMicrovm`** (SDK) to launch this session's VM, records the +`microvmID` as an annotation on the owning `Sandbox`, and holds the pod so its lifecycle mirrors the +MicroVM's. On real teardown it calls `TerminateMicrovm`. To Flow B and the user the UX is identical to +Flow A. Interactive exec/attach passthrough is **best-effort**; full fidelity is a virtual-kubelet follow-up. + +### Suspend / resume / terminate β€” the coder VM persists across the review loop + +Because the substrate is a Lambda MicroVM (not a pod), Flow D uses **suspend/resume through the Agent +Sandbox CRD** to keep the coder's context across the whole reviewβ†’fixβ†’re-review loop β€” the payoff of +this substrate over Kata (where each fix round claims a fresh pod): + +1. **Coder finishes the coding task β†’ SUSPEND** (`df-run` flips `Sandbox.operatingMode=Suspended`; the + `microvm-lifecycle` reconcile loop calls `suspend-microvm` by the annotated id). Compute is freed; + the VM's memory/disk are snapshotted. +2. DevOps + Security agents review the PR while the coder is suspended. +3. **Findings + "fix" β†’ RESUME the SAME VM** (df-iterate sets `operatingMode=Running` β†’ `resume-microvm`). + Context intact β€” no cold re-implement. +4. Coder fixes β†’ SUSPEND again; loop 2–4 until both agents clear. +5. **Final exit (merge) β†’ TERMINATE** (`df-merge-teardown` calls `TerminateMicrovm`, then deletes the + claim). This is the **only** place the VM is destroyed β€” `df-run`'s onExit is substrate-aware and + **keeps** the suspended Lambda VM (unlike Kata, which frees its pod on df-run exit). + +The ACK `Microvm` has no suspend field, so the `microvm-lifecycle` loop supplies the intentβ†’SDK +translation β€” pure shim, no virtual-kubelet. See +[`diagrams/flow-d-microvm-sandbox.md` Β§D.3a](diagrams/flow-d-microvm-sandbox.md). + +### Delivery & status + +Shipped as GitOps in its **own chart** β€” `gitops/addons/charts/agent-sandbox-lambda/` (separate from +the Kata `agent-sandbox` chart), structured as `templates/image/` (KRO RGD + the one platform +`MicrovmSandbox`) and `templates/shim/` (bridge SandboxTemplate + warm pool + `microvm-lifecycle` +controller). **Disabled by default** (`microvm.enabled=false`); the hub overlay +(`clusters/hub/addons/agent-sandbox-lambda/values.yaml`) carries cluster-specific values, and a gated +`agent-sandbox-lambda` addon entry deploys it hub-only. The platform-capability enablement (Managed ACK ++ Managed KRO) lands separately in the **appmod-blueprints** platform repo (they're EKS Capabilities, +like the Managed ArgoCD the hub already runs). This PR delivers the **design + GitOps scaffold**; the +live end-to-end path (enable capabilities β†’ sync controller β†’ publish the arm64 artifact β†’ run a MicroVM +coder with suspend/resume) is the follow-up. + +--- + +## 5. The pluggable coding assistant + +The coder is behind a **thin, swappable interface** β€” a deliberate choice (the industry lesson is +*don't marry a single vendor*). Two profiles ship; both run **inside** the Kata sandbox and reach +models only through the **Bifrost** LLM gateway. + +| Profile | Why | Notes | +|---|---|---| +| **A β€” Claude Code headless** *(primary)* | Purpose-built for autonomous implementβ†’buildβ†’testβ†’git loops; proven headless/CI autonomy | `CLAUDE_CODE_USE_BEDROCK` / base-URL β†’ Bifrost; strongest multi-file + shell | +| **B β€” Kiro headless** | **Spec-driven** (`spec β†’ requirements β†’ design β†’ tasks`) β€” the most natural fit since *an issue is a spec*; supports a headless GitHub Actions mode | AWS-native; documented as the second profile | + +### The coder contract (drop-in interface) + +Everything crosses the boundary as **files + env**, so swapping profiles is one config line: + +``` +INPUTS (mounted into the sandbox) + /workspace/SPEC.md # the issue, as a spec + /workspace/repo/ # the checked-out target repo (branch df/issue-) + /workspace/RETRY.md # (optional) one-line failure reasons from a prior holdout run + tmpfs: bifrost-api-key # mode 0400, read then unset β€” never in env + tmpfs: gh-token # short-TTL, mode 0400 +ENV + CODER_PROFILE=claude-code|kiro + BIFROST_URL=http://bifrost.bifrost.svc:8080 +OUTPUTS (produced by the coder) + git branch df/issue- with commits + /workspace/artifacts/result.json # what changed, build/test logs, evidence links +``` + +> The holdout scenarios are **deliberately absent** from this list β€” the coder never receives them. +> See [Β§6](#6-independent-verification-the-heart-of-the-pattern). + +--- + +## 6. Independent verification (the heart of the pattern) + +This is the part most teams skip β€” and it's why their agents learn to *game the tests*. Two +independent checks run **outside** the coder's control. + +### 6.1 Holdout gate β€” train/test separation for code βœ… *built (P2, advisory)* + +Acceptance criteria are **plain-English BDD scenarios**, each paired with an **executable test**, +stored where the coder **cannot see or edit** them. A separate hub-side step runs them against the +built code. As built, the content lives in the chart under +[`gitops/addons/charts/dark-factory/holdout/`](../../gitops/addons/charts/dark-factory/holdout/) and +renders into **hub ConfigMaps** in the `argo` namespace: + +``` +holdout/ + evaluate.js # the evaluator (β†’ ConfigMap df-holdout-eval) + -/scenarios.json # hidden scenarios + executable tests (β†’ ConfigMap df-holdout-) + -/rubric.md # how the judge scores +``` + +The `holdout-gate` Argo step (see [diagram B.4](./diagrams/flow-b-dark-factory.md#b4--the-df-run-dag-as-built--how-step-gating-works)) +clones the coder's `df/issue-N`, diffs it vs base, and runs `evaluate.js`. + +**Hard rules (these are the whole point):** + +1. **The coder cannot read or write the holdout β€” enforced by capability, not policy.** The scenarios + live in a Kubernetes ConfigMap; the coder runs in a Kata VM with **no K8s API access** + (`automountServiceAccountToken: false`, verified β€” no token, API times out), so it *cannot fetch + the ConfigMap even if it tried*. It only ever clones the target repo, never the holdout. On a + failed run the coder gets only **one-line reasons** (`RETRY.md`) β€” never the scenario text. +2. **Two signals, both required to pass a scenario:** + - **The executable test** β€” run against the built code, this is the **hard, un-gameable** signal: + it *proves* the behaviour (a `return true` stub cannot pass a real test with unseen inputs). + - **A different-family LLM judge** β€” we judge Claude's code with **Amazon Nova** + (`us.amazon.nova-pro-v1:0`) to defeat self-preference bias (a model scores its own output + higher). **2-of-3** votes smooth non-determinism. +3. **The judge detects *gaming*, not behaviour.** The test already proves behaviour; asking the judge + to re-derive it from a diff produced false negatives. So the judge's *only* job is to catch code + that passes the narrow test **without genuinely implementing it** β€” hard-coded example inputs, + lookup tables, `return true`, reaching the grading path β€” defaulting to PASS. It only sees + scenarios whose test already passed. +4. **Gate = β‰₯90%** of scenarios pass (test-green **and** judge-quorum). Posts a `dark-factory/holdout` + commit status. **Advisory in v1** (`holdout.blocking=false`) β€” reported, not enforced; flip to + `blocking: true` to gate the workflow. + +> This mirrors ML holdout sets and is directly validated by StrongDM's "Software Factory," which +> found *"`return true` is a great way to pass narrowly written tests"* and fixed it by storing +> scenarios **outside** the codebase. +> +> **Verified both directions (2026-07-15):** honest `subtract` β†’ all 4 hidden tests green, judge +> confirms no gaming β†’ **4/4 (100%) gate passed**. A hard-coded-lookup stub β†’ tests with unseen +> inputs go RED, and on the one narrow test it passes the judge votes 0/3 catching the lookup table β†’ +> **0/4 gate FAIL**. Neither signal alone is the gate; together they resist gaming. + +### 6.2 Reviews β€” the REAL AWS Frontier Agents (DevOps β†’ Security) βœ… *built* + +The reviews are the **genuine managed AWS agents** β€” **AWS DevOps Agent** and **AWS Security Agent** +(AWS Continuum / Frontier Agents). Not linters, not a Nova stand-in, not a stub. They run **outside +the coder VM** (the coder never grades itself) and are **ordered**, matching the AI-DLC model: + +``` +coder implements β†’ AWS DevOps Agent (broad, FIRST) β†’ clears? β†’ label `needs-security-review` + ↓ + AWS Security Agent (narrow/strict, SECOND, gated on the label) β†’ both clear β†’ merge +``` + +| Agent | Order | Scope | How it's invoked | +|---|---|---|---| +| **AWS DevOps Agent** β€” Release Readiness code review | **1st (broad)** | cross-repo dependency risk, standards compliance, access-control correctness, build+test in an AWS-managed env β†’ **BLOCK / Proceed with Caution / Safe to Release** | **GitHub App** auto-reviews the PR and posts a check-run (`devopsAgent.gate: check`, default). The df-run `devops-gate` step **waits** for that check, then applies `needs-security-review`. *(No headless code-review API exists; the coding-agent plugin is a `label`-mode fallback β€” see the manual-step note.)* | +| **AWS Security Agent** β€” code security review | **2nd (narrow)** | OWASP Top 10, hardcoded secrets, IAM misuse, dependency risk | **Dual-path (both run, redundant by design):** (1) **headless** β€” the `security-agent` step clones read-only, stages `{source archive, unified diff}` in S3, calls `securityagent create-code-review β†’ start-code-review-job β†’ list-findings` via the workflow's **IRSA** role, maps findings to `dark-factory/security` + a relayed PR comment (no GitHub App, no OAuth); (2) **`aws-security-agent` GitHub App** β€” once installed on the repo, auto-reviews every PR and posts **inline findings as `aws-security-agent [Bot]`** (like the DevOps Agent App). `merge.js`/`status.js` read the App's real check (`securityAgent.app.checkRunName`) so a Security **BLOCK** gates the merge. See `docs/dark-factory/AGENT-INSTALL.md`. | + +**Why the split matters (verified live 2026-07-16):** +- The **Security Agent path is 100% GitOps + headless** β€” proven end-to-end against the real service: + a flawed sample (`hardcoded AWS key + SQL injection + wildcard IAM`) returned exactly + `DEFAULT_CREDENTIALS (HIGH)`, `SQL_INJECTION (HIGH)`, `PRIVILEGE_ESCALATION (CRITICAL)`. The + committed `scripts/security-agent.sh` drives that same chain and was validated against the live API. +- The **agent space + application** are reconciled **once** by an idempotent ArgoCD **PreSync Job** + (`scripts/bootstrap-agentspace.sh`), which writes their IDs into a Secret the review step reads. Only + the **per-PR code-review + job** are created per run. IAM (IRSA role, service role, S3 bucket, OIDC + provider) is committed Terraform in **`iam/securityagent.tf`** β€” the chart *consumes* ARNs, never + mints IAM. +- **v1 = advisory** (`securityAgent.blockLevel: none`) β€” findings are reported, never fail the run. + Raise `blockLevel` to `low|medium|high|critical` to **block** on a finding at/above that risk level. + +> **⚠️ The one manual, non-GitOps step (flagged, never faked).** Both agents need a **one-time console +> connect** of the GitHub repo to the Agent Space (an OAuth grant no tool can script). For the +> **Security Agent** this is optional β€” the headless diff API needs no repo connect. For the **DevOps +> Agent** it's required (its review only runs via the GitHub App / plugin / chat). Until it's done, the +> `devops-gate` reports **not-cleared** and the Security Agent step is **skipped** β€” the pipeline +> **never fakes a DevOps pass**. Connecting the repo (β‰ˆ5 min in the AWS DevOps Agent console) is the +> single manual action in the whole pipeline. + +**Engine parameterization (coder).** The coder VM runs either engine via `coder.engine`: +`claude` (Claude Code `claude -p`, default + tested) or `kiro` (Kiro CLI `kiro run --headless`). Both +are first-class; the image (`examples/dark-factory/coder/Dockerfile`) carries both CLIs +(`KIRO_CLI_URL` build-arg pins the Kiro artifact). `entrypoint.js` branches on the engine. + +> **Why the workflow invokes them, not the coder:** it keeps the untrusted sandbox +> **credential-less** and preserves *separation of concerns* β€” the agent doing the work is not the +> one grading it (see the [lethal-trifecta gotcha](#11-industry-alignment--anti-patterns-what-the-world-agrees-on)). +> This is also why security testing is a **separate step, not folded into the coding assistant**: a +> coder that ran its own security scan would grade its own work and could be prompt-injected into +> suppressing findings. + +--- + +## 7. Live status in the PR βœ… *built* + +The human **watches the factory work** through **two coordinated surfaces on the PR** β€” no comment +spam, one canonical board. + +**1. Commit statuses = the live check surface (per-step, verifiable).** Each step posts a GitHub +**commit status** on the PR head SHA as it finishes β€” the coder posts `dark-factory/implementation`, +and the hub-side verify steps post `dark-factory/{holdout,security,devops}`. These render as the PR's +**Checks** and roll up into a single combined state. Because they're pinned to the exact SHA, they're +tamper-evident evidence, not prose. + +**2. The PR body = the one sticky status board (marker-managed).** The coder opens the PR with a +`` marker block; since it opens the PR *before* verification runs, it can +only mark the checks **running**: + +```markdown +### 🏭 Dark Factory β€” verification +- βœ… Build + unit tests: implemented, built + tests green +- ⏳ Holdout gate: running… +- ⏳ Security review: running… +- ⏳ DevOps review: running… +``` + +The workflow's **`sticky-status` step** (`review/status.js`) runs *after* every verify step, reads +the authoritative `dark-factory/*` commit statuses back from GitHub, and **rewrites the marker block +in place** with the real verdicts + overall state: + +```markdown +### 🏭 Dark Factory β€” verification +- βœ… Build + unit tests: implemented, built + tests green +- βœ… Holdout gate: holdout 4/4 (100%) β€” gate passed +- βœ… Security review: security: no findings +- βœ… DevOps review: devops: no findings + +_Overall: **success**. … Awaiting human review._ +``` + +**Single writer, idempotent.** Only the workflow rewrites the block (never the coder); it regenerates +the block from the commit statuses each run, so re-runs and the per-issue mutex never produce +duplicate or racing edits. Commit statuses are the source of truth; the body is the human-readable +rollup. *(Future: link each line to raw logs / the Argo run / the Langfuse trace β€” +verifiability-by-citation.)* + +--- + +## 7a. Success metrics (Argo/GitOps-native) + +Platform success is measured the same GitOps-native way everything else is β€” no bespoke telemetry. +Each workflow declares Prometheus metrics via Argo's `metrics:` blocks (scraped by the hub's +kube-prometheus-stack); a `grafana_dashboard`-labelled ConfigMap renders them, and **Langfuse** (on +the hub) captures the LLM-level token/cost/latency traces for per-issue drill-down. + +**Built (P4):** `df-run` emits `df_runs_total{status}` and `df_run_duration_seconds` (lead-time proxy) +via its `metrics:` block (`metrics.enabled`). The richer per-signal metrics below and the Grafana +dashboard ConfigMap are the next increment. + +| Metric | Meaning | Status | +|---|---|---| +| `df_runs_total{status}` | df-run outcomes by status (throughput + outcome mix) | βœ… built | +| `df_run_duration_seconds` | df-run wall-clock (lead-time proxy) | βœ… built | +| `df_claim_latency_seconds` | `SandboxClaim` create β†’ Ready (warm-pool health) | ⬜ next | +| `df_holdout_pass_pct` | Holdout satisfaction per run | ⬜ next | +| `df_iteration_rounds` | Human comment loops per issue (convergence) | ⬜ next | +| `df_vm_minutes` | Kata VM lifetime per run β€” the cost proxy | ⬜ next | +| `df_teardown_success` | Teardown completed (leak detection) | ⬜ next | +| change-failure rate | merged `df` PRs later reverted (post-merge signal) | ⬜ next | + +> This mirrors the Dark Factory deck's **Metrics & Cost Attribution** model: token counters β†’ +> cost-tier routing β†’ computed signals (items/hour, cycle time, queue depth) surfaced on a status +> API. Here the "status API" is Prometheus + the Argo UI + the sticky PR comment. + +--- + +## 8. Human-in-the-loop & the iterative comment loop βœ… *built (P3b)* + +Level 3 means the **human approves the merge** β€” and can steer via comments: + +- **Comment β†’ revision (`df-iterate`).** A human comment on a Dark Factory PR fires the Argo Events + Sensor (`pr-commented` dependency: `issue_comment` created, on a PR, **non-bot** β€” so the factory's + own sticky/status comments can't self-trigger a loop). It submits a **`df-iterate` workflow** that + resolves the PR β†’ `df/issue-` β†’ issue number, then re-submits **`df-run`** with `iterate-note` = + the comment. `df-run` injects it as `DF_ITERATE_NOTE`; the coder **checks out the existing branch** + (building on prior work β€” no PVC needed), appends the note to `SPEC.md`, revises, and force-pushes + β†’ the same PR updates in place and re-verifies. *(Verified: "add multiply" comment β†’ coder kept + `subtract` and added `multiply` in a new commit on the same branch.)* +- **Bounded convergence:** capped at `iterate.maxIterations` rounds (default 3), tracked by a + `df-iterations/` label on the PR (stateless across workflows); past the cap `df-iterate` + comments that a human must break the tie. Each revision is deduped by the comment id + (`df-iterate-`), and each round's run by `df-run--i`. +- The agent **never self-merges**; it only pushes to its own `df/issue-` branch. Merge happens + only in the `df-merge-teardown` workflow, and *only* in response to a genuine **human PR-approval + event** (`pr-approved`) β€” there is no path where the pipeline's own output produces an approval + (GitHub also blocks author self-approval). Branch protections and CI still apply. + +--- + +## 9. Lifecycle, teardown & cost + +| Phase | Sandbox state | Cost posture | +|---|---|---| +| Idle in warm pool | `replicas: 0` or snapshot | Minimal (no running VM) | +| Claimed / coding | `replicas: 1` | Active VM billed | +| Awaiting review | **`replicas: 0`** (PVC kept) | Minimal β€” resumes on comment | +| Merged / closed | **Deleted** (Sandbox + PVC + test infra + eval job) | Zero | + +- **Scale-to-zero between activity** keeps the (possibly long) review window cheap. +- **Teardown happens two ways (both built):** (1) every `df-run` has an **`onExit` handler** that + releases its `SandboxClaim` on success *or* failure (pool refills); (2) on human approval, + **`df-merge-teardown`** squash-merges the (green-verified) PR, deletes the coder branch, and reaps + the claim by its `dark-factory.io/issue-number` label. Because Argo and the pool are co-located on + the hub, owner-references cascade cleanup for in-workflow-created objects. +- **Merge is human-gated, never self-merge.** `df-merge-teardown` fires *only* from a + `pull_request_review` **approved** event on a `df/issue-*` branch, and `merge.js` re-checks that + every `dark-factory/*` status is `success` before merging β€” so a stray approval on a red PR can't + land. (GitHub also blocks the PR author from approving their own PR, so the approver is necessarily + a different human.) +- **Reaper CronJob** βœ… *built* β€” runs every 15 min (`reaper.schedule`) as a narrowly-scoped SA, + sweeping **stale ephemeral deploy-test namespaces** (`dark-factory.io/ephemeral=true`) and + **abandoned `df-run` `SandboxClaims`** older than `reaper.reapAfterSeconds` (3h) with no live + workflow. The crash-net behind the per-run `onExit` teardown and the claim's own TTL. +- **Conditional deploy-test** βœ… *built (P4)* β€” for PRs that touch deployable artifacts + (`detect-deployable` greps the GitHub compare API for `Chart.yaml`/`k8s/`/`Dockerfile`), a + **trusted** step creates an **ephemeral namespace**, applies `deployTest.manifestPath`, waits for + workloads to become `Available` (and flags crashloops), then **tears the namespace down via a + `trap`** β€” always, even on failure. It is the **only step that holds K8s access** (a scoped + ClusterRole: namespaces + workload kinds, no secrets/RBAC), bound to the workflow SA β€” never the + coder. Posts `dark-factory/deploy-test`; advisory in v1 (`deployTest.blocking=false`). *(Verified: + a PR adding a `k8s/hello.yaml` nginx Deployment deployed Ready into `df-test-14-…` and was reaped.)* +- **Ephemeral EKS test targets** (`deep-test` `PlatformCluster`) remain a **label-gated later tier** + (they provision a real cluster, ~15–20 min); the built default is the in-cluster ephemeral namespace. + +--- + +## 10. Security model + +Untrusted, LLM-generated code + issue text from anyone = treat the whole sandbox as hostile. + +- **Hardware isolation:** every coder runs in a **Kata micro-VM** (own kernel), not a shared-kernel + container. The isolation boundary is the VM β€” it travels with the workload regardless of host + cluster. +- **No cloud credentials in the sandbox:** the coder holds only a **Bifrost API key** and a + **short-TTL GitHub token (`contents:write` only)** via **projected tmpfs (mode 0400)** β€” read then + unset, never in env. All AWS IAM lives with the **Argo workflow orchestrator, outside the VM**. The + coder pushes a branch; the *workflow* opens the PR and does the merge. +- **Egress lockdown:** a **NetworkPolicy** default-denies egress and allows only **DNS + Bifrost:8080 + + GitHub/HTTPS**. `automountServiceAccountToken: false`, runAsNonRoot, seccomp `RuntimeDefault`, + drop `ALL` caps. + +**Running on the hub β€” the three-layer control-plane isolation (verified live).** Because the sandbox +pool is co-located with the hub's control-plane services (Keycloak, ArgoCD, external-secrets, Argo), +a single egress NetworkPolicy is **not** sufficient β€” and on EKS it is also *incomplete* (see the +CNI note below). The hub deployment enforces isolation in **three independent layers**, all certified +against a live coder pod: + +1. **Standard `NetworkPolicy`** (`30-networkpolicy.yaml`) β€” default-deny egress; allow only DNS + + Bifrost:8080 + public HTTPS with all RFC-1918 + link-local (incl. IMDS `169.254.169.254`) + excepted, so pod-IP egress to the control plane is blocked. +2. **Admin-tier `ClusterNetworkPolicy`** (`31-clusternetworkpolicy.yaml`) β€” a `Deny` on egress to the + control-plane namespaces. Needed because EKS VPC-CNI standard NetworkPolicy *only applies to + Deployment-owned pods*, and coder pods are owned by a **`Sandbox` CR** β€” the Admin tier applies + regardless of ownership and is evaluated first (Deny wins). +3. **ClusterIP egress-firewall DaemonSet** (`32-clusterip-egress-firewall.yaml`) β€” a host-network, + `NET_ADMIN` DaemonSet on the kata node (in `kube-system`, since the sandbox namespace's `baseline` + PodSecurity forbids privileged pods) that installs `FORWARD` iptables rules matching + `conntrack --ctorigdst` (the **original ClusterIP before kube-proxy DNAT**): allow the Bifrost + ClusterIP, drop the rest of the service CIDR. This closes the CNI gap in the note below. + +> ⚠️ **EKS VPC-CNI ClusterIP gap (found + fixed).** Neither standard NetworkPolicy nor Admin +> ClusterNetworkPolicy egress applies to traffic sent to a **Service ClusterIP** β€” kube-proxy DNATs +> it to a backend pod IP *before* policy evaluation, so control-plane Services (e.g. `172.20.x`) slip +> past the CIDR/namespace denies even though backend **pod IPs are correctly blocked**. Layer 3 (the +> node firewall) is what actually closes this. Verified: before it, `external-secrets` + the API +> server were reachable by ClusterIP; after it, both are blocked while Bifrost + public egress + DNS +> still work. + +Plus the always-on basics: + +- **Dedicated tainted kata nodegroup:** coder VMs schedule only onto the nested-virt MNG + (`kata=true:NoSchedule` + `kata-enabled=true`); control-plane pods never land there and vice-versa. +- **No cluster API from the VM:** `automountServiceAccountToken: false`, no RBAC β€” even if a coder + opens a TCP socket to a ClusterIP, it has **no credentials** to authenticate (services reject it: + the external-secrets webhook returns TLS alert 47; the API server rejects unauthenticated calls). + Layers: Kata VM + no creds + pod-IP deny (2 tiers) + ClusterIP node-firewall + service auth. + +- **Prod is never a test bed, and neither is a spoke:** the factory runs on the **hub build plane**; + the spokes are the **deploy/run plane**. Unreviewed agent code never runs next to enterprise + workloads (spoke) or touches prod β€” its output reaches the spokes only as *merged, reviewed* code + through the normal deployment path. +- **⚠️ Lethal trifecta (the #1 risk β€” see [Β§11](#11-industry-alignment--anti-patterns-what-the-world-agrees-on)):** untrusted + issue text + credentials + egress is the exact recipe for prompt-injection exfiltration + (demonstrated against GitHub-issue-driven agents in the wild). The mitigations above exist + specifically to break that trifecta: keep credentials out of the issue-ingesting context, deny + egress (including the hub's own control plane), and treat all issue/repo content as hostile input. + +--- + +## 11. Industry alignment & anti-patterns (what the world agrees on) + +We validated this design against how GitHub Copilot coding agent, OpenAI Codex cloud, Devin, Google +Jules, Cursor background agents, Factory.ai, and StrongDM's "Software Factory" actually work. + +### βœ… Where we match consensus + +| Design choice | Industry practice | +|---|---| +| Issue β†’ event β†’ ephemeral sandbox β†’ build/test β†’ PR | The recurring ~7-stage pipeline across Copilot/Codex/Devin/Jules/Factory | +| **DAG orchestration + concurrent dispatch** (Argo Workflows) | *Convergent pattern.* Stripe/Coinbase/Ramp/StrongDM independently arrived at isolated sandboxes + subagent/DAG orchestration + cost-routing for scale | +| **Kata micro-VM isolation** | *Above-consensus.* microVM-class isolation (Firecracker, Kata, Bedrock AgentCore's per-session microVM) is the defensible choice for untrusted LLM code; shared-kernel containers are considered insufficient | +| Build/test **until green before** the PR | Explicit in codex-1's RL training, Devin, Copilot | +| **Holdout scenarios the coder never sees** | *Above-consensus.* Directly matches StrongDM's Software Factory (they learned it the hard way after `return true` gamed their tests) | +| **One sticky status surface**, not comment spam | Copilot draft-PR + session logs; Devin single review status; Factory "Mission Control" | +| Human **approves the PR**, agent iterates on comments | The dominant gating norm β€” agents do **not** self-merge by default | +| Single coder + **independent read-only reviewers** | Anthropic + Cognition agree parallel multi-agent *authoring* is a poor fit for coding; the good pattern is one coder + a fresh model reviewing the finished diff (CodeRabbit's Security Agent) | + +### ⚠️ Anti-patterns we explicitly design against + +1. **Lethal trifecta / prompt injection (highest risk).** Untrusted issue text + cloud creds + egress + β†’ data exfiltration. Invariant Labs demonstrated a malicious GitHub *issue* injecting an agent + into leaking private-repo data via an auto-PR. **Our defense:** credentials never in the + issue-ingesting sandbox context; egress denied except Bifrost/GitHub; issue/repo content treated + as hostile; frontier agents scoped read-only. *(Willison "lethal trifecta"; Invariant Labs.)* +2. **Reward hacking / test-gaming.** Frontier models stub evaluators (`evaluate = _always_ok`), make + `verify()` return true, read reference answers, or delete the test oracle (METR, OpenAI, + Anthropic). **Our defense:** the holdout the coder cannot see or edit β€” if the coder can reach it, + the holdout is theater. +3. **LLM-judge as a sole hard gate.** Judges have proven position/verbosity/**self-preference** bias + (causal β€” a model favors its own family's output). **Our defense:** different judge model + + paired executable tests + 2-of-3 + a probabilistic satisfaction score, never a lone boolean. +4. **Multi-agent over-orchestration.** **Our defense:** single-threaded coder; Security/DevOps are + stateless read-only reviewers on the finished diff, never co-authors. +5. **Non-converging comment loops.** **Our defense:** batch comments into one run, cap iterations, + hard time/turn limits. +6. **Warm-pool idle burn.** **Our defense:** snapshot/fork + idle reaping + scale-to-zero, not parked + VMs. +7. **Rubber-stamp reviews.** AI-co-authored PRs carry measurably more issues; "review results not + code" can decay into a green rubber stamp. **Our defense:** the human reviews *structured + evidence* (tests + holdout % + security findings + diff-path confinement), and high-risk changes + (infra, `deep-test`) get a firmer gate. + +--- + +## 12. Phased delivery + +Each phase is independently valuable β€” if you stop after any one, you're better off than before. + +| Phase | Delivers | Status | Independently useful? | +|------:|----------|:------:|-----------------------| +| **P0** | **Relocate the sandbox capability to the hub** β€” nested-virt MNG + control-plane isolation + devβ†’hub cutover (all GitOps) | βœ… **done** | βœ… Kata warm pool co-located with Argo on the build plane | +| **P1** | First `df-run` **WorkflowTemplate**: trigger β†’ claim warm sandbox β†’ Claude Code coder β†’ build/test β†’ workflow opens PR + sticky status β†’ manual teardown | βœ… **done** | βœ… A working autonomous-PR loop on Argo | +| **P2** | Strict **holdout gate** (hidden scenarios in a hub ConfigMap, executable tests + a different-family Nova judge, β‰₯90% gate) | βœ… **done** (advisory) | βœ… Quality gate that resists gaming β€” verified green (honest code 4/4) *and* adversarially (gamed stub 0/4) | +| **P3** | **Security + DevOps review steps** β€” parallel hub-side reviewers, `auto` backend (linters + Nova), advisory, posting `dark-factory/{security,devops}` statuses (managed AWS-Agent backend swappable in when its API lands) | βœ… **done** (advisory) | βœ… Independent review evidence β€” verified: clean code 0 findings; adversarial diffs correctly flagged | +| **P3b** | **Full event-driven lifecycle**: trigger dedup (one issue = one run) + live PR-body status + **`df-merge-teardown`** (approval β†’ green-gated squash-merge + teardown) + **`df-iterate`** (PR comment β†’ bounded revision on the existing branch) + coder no-diff guard | βœ… **done** | βœ… Hands-off labelβ†’runβ†’verifyβ†’PR, commentβ†’revise, approveβ†’mergeβ†’teardown β€” all verified live | +| **P4** | Conditional **`deploy-test`** (gated on `detect-deployable`; ephemeral namespace deploy+probe+teardown, scoped ClusterRole) + **reaper CronJob** + **df-run Prometheus metrics**; *(deep-test `PlatformCluster` tier + Grafana dashboard remain)* | βœ… **done** (core) | βœ… Full lights-off lifecycle + measurement β€” deploy-test verified (nginx Ready + reaped) | +| **P5** | **Kiro** coder profile; per-severity **blocking** gate option; **Fable-5 deep-security sandbox** (`deep-sec`) | ⬜ planned | βœ… Vendor-plurality + higher autonomy + deep review | + +--- + +## 12a. Running Kata on EKS Auto Mode clusters (validated design) + +The hub (like the spokes) runs **EKS Auto Mode + Bottlerocket** (`c6a`/`c6g` nodes). Auto Mode's +managed nodes **cannot host Kata**: no control over `cpuOptions.nestedVirtualization`, no +kernel-module loading (`modprobe kvm_intel`), no `kata-deploy`, and those node types don't expose +VT-x. `eks-platform-openclaw` avoids Auto Mode entirely for this reason β€” but we don't have to. + +> **Applies to the hub.** This design was first validated on spoke-dev, but the mechanism β€” a +> nested-virt MNG *alongside* Auto Mode β€” is exactly what the hub relocation requires. The same +> chart artifacts, node bootstrap, and hard-won lessons below carry over verbatim; only the target +> cluster changes (and the hub adds the control-plane egress lockdown from [Β§10](#10-security-model)). + +### Decision: self-managed nested-virt MNG *alongside* Auto Mode + +Add a small, tainted **self-managed Managed Node Group** of **nested-virt `c8i`/`m8i`** instances to +the cluster (spoke-dev in the original validation; **the hub** under the current design). Auto Mode +keeps running everything else; kata sandboxes schedule onto the MNG via the `kata=true:NoSchedule` +taint the chart already applies. We chose an **MNG, not a second Karpenter** β€” running a +self-managed Karpenter beside Auto Mode's managed Karpenter risks NodePool/CRD conflicts, whereas +MNGs are additive and coexist cleanly. + +Rejected alternatives: **Bedrock AgentCore / Fargate** (breaks the k8s-native pod model our whole +Sandbox/warm-pool/claim design depends on β€” it's an invoke-a-session runtime, not a pod we own); +**gVisor** (same Auto-Mode node-install blocker as Kata, weaker isolation). + +### βœ… Validated by two live tests (spoke-dev, 2026-07-10) + +A `c8i.4xlarge` kata MNG was created on spoke-dev, exercised, then torn down. Results: + +| Question | Result | +|---|---| +| Self-managed MNG coexists with Auto Mode? | **βœ… Yes** β€” MNG provisioned alongside Auto Mode nodepools, no conflict; Auto Mode stayed healthy | +| Nested virtualization / `/dev/kvm`? | **βœ… Yes** β€” `/dev/kvm` present, `kvm_intel` loaded, 32 `vmx` flags, via `CpuOptions.NestedVirtualization: enabled` | +| Node joins the cluster & goes Ready? | **βœ… Yes** β€” with the fixes below (nodeadm endpoint/CA + vpc-cni + kube-proxy) | +| Kata runtime install (kata-deploy)? | **βœ… Yes** β€” `1/1`, zero restarts, once `kube-proxy` was installed | +| **Real Kata VM runs?** | **βœ… YES** β€” pod under `kata-clh` had guest kernel `6.18.35` vs host `6.12.90` = true hardware VM isolation | + +### Hard-won lessons (baked into the implementation) + +1. **Node bootstrap** β€” do **not** override the AMI + userData with plain bash; that clobbers the + EKS bootstrap and the node boots (`/dev/kvm` present) but never joins. Use the **AL2023 nodeadm + MIME userData**, and set nested-virt via the launch-template `CpuOptions`, not userData. +2. **Teardown ordering** β€” delete the **MNG first and let it drain** (set min/desired=0 first). + Terminating the instance out from under the MNG makes the ASG respawn and can wedge the delete on + a `Pending:Wait` lifecycle hook; recover with `terminate-instance-in-auto-scaling-group` + + `complete-lifecycle-action`. +3. **Custom-AMI nodeadm needs cluster coordinates** β€” with a custom `ImageId`, nodeadm can't + auto-discover the API; you must set `apiServerEndpoint` + `certificateAuthority` + `cidr` in the + NodeConfig, or it fails "Apiserver endpoint is missing in cluster configuration". +4. **Auto Mode has no `vpc-cni`** β€” self-managed MNG nodes stay `NotReady` (`cni plugin not + initialized`) until you install the `vpc-cni` EKS addon. `aws-node` tolerates all taints and + schedules onto the kata node once installed. +5. **kata-deploy on Auto Mode (open item)** β€” the upstream kata-deploy chart defaults to the + **experimental nydus snapshotter** (`EXPERIMENTAL_SETUP_SNAPSHOTTER=nydus`), which restarts + containerd and briefly drops CNI networking; kata-deploy then fails its own API call + (`Failed to get node ... client error (Connect)`) and crashloops before installing the runtime. + Fix to apply next: disable the experimental nydus snapshotter (openclaw uses overlayfs) and/or + raise kata-deploy's API-retry tolerance. Everything *up to* the runtime install is proven; the + runtime install itself needs this one chart-tuning fix. + +Also fixed during testing: the kata-deploy Helm values are **top-level** (`nodeSelector`, +`tolerations`, `shims`) for a direct install β€” the nested `kata-deploy:` key only applies when it's +a subchart. Our catalog entry uses the nested form (correct, since ArgoCD deploys it as its own +app), but a direct `helm install` must use top-level values. + +--- + +## 13. Open questions / future work + +*To resolve during implementation β€” flagged honestly rather than assumed:* + +- **Nested-virt capacity on the hub** β€” confirm `c8i`/`m8i` availability + headroom for the kata MNG + alongside the hub control plane; size the warm-pool/semaphore ceiling to it. +- **Exact hub control-plane egress deny list** β€” the concrete namespaces/service CIDRs (keycloak, + argocd, external-secrets, argo) to encode in the NetworkPolicy for the hub deployment. +- **Headless auth** for Claude Code & Kiro through a Bifrost base-URL override inside a Kata VM + (prototype first in P1). +- **Bifrost VK + tmpfs secret projection** β€” mint a short-TTL GitHub token + Bifrost virtual key and + project them onto the claimed sandbox (mode 0400). Documented but not yet wired β€” close in P1. +- **Exact AWS Security / DevOps Agent APIs & auth** β€” confirm the invocation contract at build time; + clear **Fable-5** provider-data-share / 30-day retention before enabling the deep-sec tier. +- **Workflow RBAC scope** β€” the Argo workflow SA needs `sandboxclaims` (CRUD) + read `sandboxes` + + eval `Job`/`ConfigMap` in `agent-sandbox-system`, plus `PlatformCluster` claims for the `deep-test` + path. No pod/exec, no secrets, no cluster scope. +- **Argo `resource`-template `successCondition` on CRD conditions** β€” validate the JSONPath filter + form against Argo v3.6.7, or fall back to a `kubectl wait --for=condition=Ready` step. +- **Workspace access mode** β€” RWO (EBS) forces strict coder↔eval serialization + same-AZ pinning; + RWX (EFS) allows read-only eval-alongside and cheaper iteration. Decide before P2. + +> βœ… **Resolved since the first draft:** native `SandboxWarmPool` CRD is adopted (custom pool-manager +> CronJob dropped); the upstream operator is vendored into the addon catalog; Kata-on-Auto-Mode is +> validated ([Β§12a](#12a-running-kata-on-eks-auto-mode-clusters-validated-design)); the sandbox +> capability is being relocated devβ†’hub (this design). + +--- + +## 14. References + +**Pattern sources** +- Steve Yegge β€” *Welcome to Gas City* β€” https://steve-yegge.medium.com/welcome-to-gas-city-57f564bb3607 +- *The Dark Factory Pattern: Moving From AI-Assisted to Fully Autonomous Coding* β€” https://hackernoon.com/the-dark-factory-pattern-moving-from-ai-assisted-to-fully-autonomous-coding +- Kiro headless in GitHub Actions β€” https://builder.aws.com/content/35cLFnKM6DJMgRzdZQ7XPZkJmoz/automate-reviews-in-github-actions-with-kiro-headless-mode + +**Industry pipelines** +- GitHub Copilot coding agent β€” https://github.blog/news-insights/product-news/github-copilot-meet-the-new-coding-agent/ +- OpenAI Codex β€” https://openai.com/index/introducing-codex/ +- Devin SDLC integration β€” https://docs.devin.ai/essential-guidelines/sdlc-integration +- Factory.ai Missions β€” https://docs.factory.ai/cli/features/missions/overview +- StrongDM Software Factory β€” https://factory.strongdm.ai/ +- AWS Bedrock AgentCore runtime sessions (per-session microVM) β€” https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/runtime-sessions.md +- AWS DevOps Agent β€” https://aws.amazon.com/devops-agent/ + +**Failure modes / safety** +- Simon Willison β€” *The lethal trifecta* β€” https://simonwillison.net/2025/Jun/16/the-lethal-trifecta/ +- Invariant Labs β€” GitHub MCP prompt-injection exfiltration β€” https://invariantlabs.ai/blog/mcp-github-vulnerability +- METR β€” *Recent frontier models are reward hacking* β€” https://metr.org/blog/2025-06-05-recent-reward-hacking/ +- OpenAI β€” *Detecting misbehavior in frontier reasoning models* β€” https://openai.com/index/chain-of-thought-monitoring/ +- Anthropic β€” *Reward tampering* β€” https://www.anthropic.com/research/reward-tampering +- LLM-judge self-preference bias β€” https://arxiv.org/abs/2404.13076 Β· MT-Bench β€” https://arxiv.org/abs/2306.05685 +- Cognition β€” *Don't build multi-agents* β€” https://cognition.ai/blog/dont-build-multi-agents +- Anthropic β€” *Claude Code best practices* β€” https://www.anthropic.com/engineering/claude-code-best-practices + +**Platform building blocks (this monorepo & siblings)** +- `eks-platform-openclaw` β€” Kata micro-VM sandbox, `Sandbox` CRD, session-router lifecycle (uses LiteLLM there; **this platform uses Bifrost** as the LLM gateway) +- `appmod-blueprints` β€” `PlatformCluster` Crossplane composition (ephemeral EKS), KRO CI/CD pipeline +- `agent-platform-amazon-eks` β€” hub/spoke fleet, addon ApplicationSets, kagent, agent-gateway, **Bifrost** LLM gateway diff --git a/docs/dark-factory/RESTORE-STATE.md b/docs/dark-factory/RESTORE-STATE.md new file mode 100644 index 00000000..60001bcd --- /dev/null +++ b/docs/dark-factory/RESTORE-STATE.md @@ -0,0 +1,47 @@ +# Dark Factory β€” restore checkpoint (2026-07-20) + +## Resume the Claude Code session + claude --resume ade840e8-b310-4ac7-a173-98e4fbbc5098 +(or `claude --resume` and pick it from the list) + +## Git state (all pushed β€” nothing to recover) +- platform repo: sample-open-agentic-platform @ branch dark-factory-autonomous-agent-coding-pattern, HEAD f512706 +- sandbox repo: elamaran11/dark-factory-sandbox @ main (IAM policy from PR #50 merged in) + +## STATUS: fully working end-to-end with BOTH real AWS agents +- AWS DevOps Agent (GitHub App) β€” reviews every PR, posts aws-devops-agent/release-readiness-review + check; df-run `devops-gate` waits on it, applies needs-security-review label. +- AWS Security Agent β€” TWO paths, both run: (a) GitHub App β†’ inline aws-security-agent[bot] findings; + (b) headless code-review API (create-code-reviewβ†’start-jobβ†’list-findings via IRSA). +- Full lifecycle PROVEN: issue #49 β†’ PR #50 β†’ coder wrote least-privilege IAM (infra/iam.tf) β†’ + terraform validate passed β†’ DevOps approved β†’ Security "no issues" β†’ human (shapirov103) approved β†’ + df-merge-teardown workflow squash-merged + deleted branch + reaped sandbox β†’ IAM landed in main. + +## How merge works (answered): a SEPARATE Argo workflow +- Approval (by a NON-author identity) β†’ GitHub webhook β†’ Argo Sensor `pr-approved` trigger β†’ + submits `df-merge-teardown` workflow β†’ merge.js re-checks green + squash-merges via GitHub API + (as the bot PAT elamaran11) β†’ teardown-claim reaps the sandbox. +- Author cannot self-approve (GitHub rule); the coder opens PRs as elamaran11, so approver must differ. + +## Recent fixes this session (all committed + deployed hub-side, no image rebuild) +- Security Agent GitHub App connected (App install + connect to dark-factory Agent Space β€” BOTH steps). +- App posts inline COMMENTS only (no check/status) β†’ securityAgent.app.checkContext/checkRunName EMPTY; + merge gate uses headless dark-factory/security signal. +- status.js: multi-SHA aggregation (reads statuses/check-runs across ALL PR commits, not just head) β€” + fixes "Security review: not run" from SHA drift. +- status.js: holdout row OMITTED entirely when absent or not-applicable (Terraform PRs) β€” no clutter. +- docs/dark-factory/AGENT-INSTALL.md rewritten GA/public-followable (no allow-list/SIM/preview). +- values.yaml P3 comment corrected to GA reality. + +## OPEN (optional, not blocking) β€” one real robustness gap +- merge.js still reads only pr.head.sha (same head-only bug I fixed in status.js). It merged PR #50 + correctly, but if Security posts a BLOCK on an earlier commit and head moves, the gate could miss it. + FIX: apply the same multi-SHA aggregation (read statuses+check-runs across all PR commits) to merge.js. + +## Config quick-ref (values.yaml) +- coder.engine: claude | kiro ; coder image dark-factory-coder:v0.2.3 (aws-cli/git/zip, subdir discovery) +- devopsAgent: gate=check, checkRunName=aws-devops-agent/release-readiness-review +- securityAgent: headless (enabled) + app.enabled (GitHub App inline bot) +- ArgoCD app dark-factory-hub tracks this branch; control plane on the OPENCLAW cluster + (kubectl --context openclaw -n argocd port-forward svc/argo-cd-argocd-server 8080:443). +- account 940019131157, us-west-2. Security Agent space as-0fa95663..., DevOps space 65fe3629... diff --git a/docs/dark-factory/SUBSTRATE-BENCHMARK.md b/docs/dark-factory/SUBSTRATE-BENCHMARK.md new file mode 100644 index 00000000..196f34cb --- /dev/null +++ b/docs/dark-factory/SUBSTRATE-BENCHMARK.md @@ -0,0 +1,178 @@ +# Dark Factory β€” Substrate Benchmark: Kata micro-VM vs Lambda MicroVM + +A side-by-side comparison of the two sandbox substrates that run the autonomous coder, +measured on **identical issues fired in parallel** on the same hub cluster. + +- **Flow B β€” Kata micro-VM** (mature, default): the coder runs in a hardware-isolated Kata + pod on a self-managed nested-virt EKS node group. +- **Flow D β€” AWS Lambda MicroVM** (pre-GA): the coder runs in a Firecracker MicroVM + provisioned via the `lambdamicrovms` ACK controller, driven by a bridge pod. + +Both run the **same `dark-factory-coder`** (same `entrypoint.js`), produce the same kind of +PR, and go through the **same review gates** (AWS DevOps Agent + AWS Security Agent). The only +difference is *where the coder executes* and *how it's provisioned*. + +--- + +## TL;DR + +| | Kata micro-VM (Flow B) | Lambda MicroVM (Flow D) | +| --- | --- | --- | +| **Workflow** | `df-run` (certified) | `df-run-lambda` (separate, MicroVM-native) | +| **Provisioning** | pre-warmed pool β†’ **instant claim** | **RunMicrovm cold-start per session** (~90s) | +| **Time to first PR** (from label) | ~**2 min** | ~**2.5 min** | +| **LLM path** | Bifrost gateway (in-cluster) + Langfuse traces | **Bedrock-direct** (exec role) β€” no cluster network | +| **Scale-to-zero when idle** | ❌ node pool runs continuously | βœ… **suspend-to-zero** between PR and merge | +| **Fix-round mechanic** | fresh pod each round | **resume the SAME suspended VM** (warm); recreate if the pre-GA resume fails | +| **Infra to manage** | nested-virt node group (Karpenter/MNG) | none β€” serverless MicroVMs | +| **Observability** | native `kubectl logs` | custom `/logs` HTTP endpoint (no runtime CloudWatch) | +| **Maturity** | production-ready today | pre-GA (preview) β€” pilot-grade | +| **Economics at 1000s scale** | pay for idle capacity | pay per active minute (the strategic win) | + +**Bottom line:** at small scale the two feel equivalent (the LLM coding step ~2–4 min and the +external review agents ~8–15 min dominate total time on *both*). The Lambda substrate's advantage +is **not latency** β€” it's **operational + economic**: no node pool to run, and suspend-to-zero +between the PR and the human's review/merge. Its cost is **maturity** (pre-GA control plane β€” resume +from suspend is occasionally flaky, mitigated by the recreate-fallback) and the extra plumbing below. + +--- + +## Benchmarked run (identical issue, per substrate) + +### Time to first PR (from label β†’ PR opened) +| Substrate | Issue | PR | Elapsed | Notes | +| --- | --- | --- | --- | --- | +| Kata | #137 | β€” | ~**2 min** | pre-warmed pod, instant claim | +| Lambda | #135 | #136 | ~**2.5 min** (20:53:03 β†’ 20:55:35) | native `df-run-lambda`, RunMicrovm cold-start | + +**Ξ” β‰ˆ 30–90s** β€” the MicroVM cold-start (`RunMicrovm` β†’ RUNNING β†’ `/run`) vs Kata's pre-warmed pod +claim. Note the MicroVM-native `df-run-lambda` is **faster than the old bridge path** (~3.7 min): +removing the SandboxClaim/warm-pool indirection cut ~1 min. Everything after (clone β†’ LLM β†’ push) is +identical code and takes the same time. + +### Lifecycle timing (Lambda #135, native pipeline) +| Phase | Time | +| --- | --- | +| provision-microvm (RunMicrovm β†’ RUNNING β†’ `/run` HTTP 200) | ~90s | +| **drive-coder** (clone β†’ LLM β†’ push β†’ PR) | ~60s | +| suspend-microvm (VM β†’ SUSPENDED, stays down) | ~5s | +| holdout / deploy-test (terraform validate) | ~20–40s each | +| security-agent + devops-gate (external) | ~8–15 min combined (dominates) | +| fix round: resume-or-recreate + coder re-run β†’ new commit | ~2 min | + +*(The external review agents dominate total wall-clock on BOTH substrates. The MicroVM is SUSPENDED +for the entire multi-minute review window β€” that idle time is free on Lambda, billed on Kata.)* + +--- + +## Where the logs are + +| What | Kata (Flow B) | Lambda (Flow D) | +| --- | --- | --- | +| Pipeline steps | Argo UI (`/argo-workflows`) or `kubectl logs -n argo ` | **same** | +| Coder output | `kubectl logs -n agent-sandbox-system df-issue-` (native) | **`GET https:///logs`** with an auth token (runtime CloudWatch routing is unreliable on the pre-GA runtime, so the hook-server captures coder stdout to a file + serves it) | +| Image build | n/a (normal ECR image) | CloudWatch `/aws/lambda/microvms/coder-image` | + +--- + +## DAG β€” two SEPARATE WorkflowTemplates (one per substrate) + +The substrates run **different Argo WorkflowTemplates**, so each graph is clean and Kata's certified +pipeline is never touched by Flow D changes. The Argo Events sensor routes by label: +`dark-factory` β†’ `df-run` (Kata), `darkfactory-lambda` β†’ `df-run-lambda` (Flow D). + +**Kata β€” `df-run` (certified, byte-identical to the mature pipeline):** +``` +claim(SandboxClaim) β†’ drive-coder β†’ { holdout, devops-gate β†’ security, detect β†’ deploy-test } β†’ status β†’ onExit(teardown: delete claim) +``` + +**Lambda β€” `df-run-lambda` (MicroVM-native; NO SandboxClaim / bridge / warm pool):** +``` +provision-microvm β†’ drive-coder β†’ suspend-microvm β†’ { holdout, devops-gate, security, detect β†’ deploy-test } β†’ status β†’ onExit(keep suspended VM) +``` +The one extra node β€” `suspend-microvm` β€” is **explicit and lives only in the Lambda graph**, so the +Kata graph still contains zero MicroVM nodes. Suspend/resume is owned by the workflow directly (it +calls `aws lambda-microvms suspend/resume-microvm`), not a bridge or a lifecycle controller. + +### Substrate-specific mechanics +- **Kata:** `claim-sandbox` binds a **pre-warmed** pod from `coder-warmpool`; the operator injects + `DF_*` env; the baked `entrypoint.js` runs in-cluster, reaches models via **Bifrost**, native logs. +- **Lambda:** `provision-microvm` (a single workflow step, running as `dark-factory-workflow` with the + lambda-microvms role via Pod Identity) does it all β€” no bridge pod, no warm pool: + 1. reads the platform image handoff (imageARN + execRoleARN, built **once** by KRO/ACK), + 2. creates the **`Microvm` CR** (stable name `mvm-`) + a runHookPayload Secret + (the review note is folded in here β€” the coder has no claim env), + 3. waits RUNNING + endpoint, mints an auth token, **POSTs `/run`** β†’ the hook-server + background-spawns the same `entrypoint.js` with `USE_BEDROCK=1` (Bedrock-direct, no cluster net), + 4. the `suspend-microvm` DAG step suspends the VM once the PR is open (idlePolicy + `autoResumeEnabled=false` + nothing polls the endpoint β†’ it **stays** suspended), + 5. on a fix round `provision-microvm` **resumes the same suspended VM** (warm resume); if the + pre-GA service failed the resume (VM terminated), it **recreates a fresh VM** automatically, + 6. `df-merge-teardown` deletes the `Microvm` CR at merge β†’ controller `TerminateMicrovm`. + +--- + +## Step-by-step: what actually happens + +### Kata (Flow B) β€” `df-run` +1. Issue labeled `dark-factory` β†’ sensor dep `issue-labeled-kata` β†’ `df-run`. +2. `claim-sandbox` binds a **pre-warmed** Kata pod from `coder-warmpool` (instant). +3. Operator injects `DF_*` β†’ baked `entrypoint.js`: clone β†’ Claude Code (via **Bifrost**) β†’ **open PR**. +4. Review gates: DevOps Agent + Security Agent β†’ consolidated verdict. +5. "fix findings" β†’ `df-iterate` β†’ **new** Kata coder round β†’ re-review. +6. Approve β†’ `df-merge-teardown` merges + deletes the claim. + +### Lambda MicroVM (Flow D) β€” `df-run-lambda` +1. Issue labeled `darkfactory-lambda` β†’ sensor dep `issue-labeled-lambda` β†’ `df-run-lambda`. +2. `provision-microvm` creates the `Microvm` CR β†’ controller `RunMicrovm` (**cold-start ~90s**) β†’ + RUNNING; mints token; `POST /run` β†’ hook-server spawns the coder (`USE_BEDROCK=1`, + **Bedrock-direct**). Coder: clone β†’ Claude Code β†’ **open PR**. +3. `suspend-microvm` step suspends the VM β†’ it **stays SUSPENDED** while gates run (scale-to-zero). +4. Same review gates + verdict. +5. "fix findings" β†’ `df-iterate` β†’ `df-run-lambda` fix round: **resume the SAME VM** (warm) or, if the + pre-GA service failed the resume, **recreate fresh**; the coder re-runs with the note β†’ new commit. +6. Approve β†’ `df-merge-teardown` merges + deletes the `Microvm` CR β†’ controller `TerminateMicrovm`. + +--- + +## Gotchas the Lambda substrate needed (that Kata does not) + +Because a MicroVM is **outside the cluster network, has a read-only rootfs, and uses a +snapshot/hook execution model**: + +| # | Gotcha | Fix | +| --- | --- | --- | +| 1 | Coder crashed `EACCES mkdir /workspace/artifacts` (no writable volume like Kata) | set `WORKSPACE=/tmp/workspace` (writable tmpfs) β€” *the silent killer* | +| 2 | Can't reach Bifrost's ClusterIP from a MicroVM | **Bedrock-direct** via the exec role (`bedrock:InvokeModel`); no Bifrost/NLB/VPC-connector | +| 3 | Runtime logs don't reach CloudWatch | hook-server captures coder stdout β†’ `/logs` HTTP endpoint | +| 4 | Coder is one-shot but the MicroVM `/run` hook has a 30s timeout | `/run` **background-spawns** the coder + returns fast; pipeline polls GitHub for the PR | +| 5 | Ingress: `ALL_INGRESS` blocks auth-token minting | use **`HTTP_INGRESS`** | +| 6 | aws-cli image lacks `lambda-microvms`; no node | step image = `aws-cli:latest` (has the verbs) + python3 for JSON + fetch kubectl at start | +| 7 | `runHookPayload` is a `SecretKeyReference`; imperative `run-microvm --run-hook-payload` doesn't fire `/run` | deliver via the **declarative `Microvm` CR** | +| 8 | Image rebuild: overwriting the same S3 key doesn't rebuild | use versioned artifact keys; bump `codeArtifactUri` | +| 9 | **VM would not stay SUSPENDED** (console showed RUNNING) | `idlePolicy.autoResumeEnabled=false` **and** never hit the VM endpoint after `/run` β€” any request auto-resumes it | +| 10 | **VM auto-terminated before the fix round** (resume hit "already terminated") | `suspendedDurationSeconds=28800` (Lambda's 8h max) so it survives the reviewβ†’human window; 5 min was far too short | +| 11 | **Fix round re-ran but committed nothing** (coder "done" on old sha) | the review note must ride on the **runHookPayload** (MicroVM has no claim env); hook-server keys its `/run` guard on a **per-invocation run-id** so a resumed VM accepts a fresh run | +| 12 | Pre-GA **resume-from-suspend is intermittently flaky** (Internal service error β†’ VM terminates) | `provision-microvm` checks the VM's real AWS state and **recreates a fresh VM** when resume isn't possible β€” the fix round self-heals | +| 13 | Two Sandboxes fought over one VM β†’ suspend/resume **flapped** until the VM died | name the CR/workflow by **issue-number** (stable across rounds) so there's exactly **one VM per issue** | +| β€” | IAM: the workflow SA calls the lambda-microvms verbs | Pod Identity binds `dark-factory-workflow` β†’ the lambda-microvms role (get/suspend/resume/terminate-microvm + create-auth-token); exec role keeps `bedrock:InvokeModel` | + +> **Note on the ACK CR status:** `Microvm.status.state` is **stale** β€” it does not reflect +> suspend/resume/terminate. Always read AWS truth with `aws lambda-microvms get-microvm --query state`. + +Kata needs **none** of these β€” it's an in-cluster pod with a mounted workspace, native logs, +Bifrost reachability, and a normal ECR image. That's why the two substrates are **separate +WorkflowTemplates** (`df-run` vs `df-run-lambda`): Flow D's plumbing never touches the certified Kata graph. + +--- + +## When to choose which + +- **Kata (today):** production-ready, mature, standard `kubectl`/IDE access, in-cluster networking. + Choose it now for reliability. Cost: you run + pay for a nested-virt node pool continuously. +- **Lambda MicroVM (strategic):** serverless, suspend-to-zero per idle session, no node pool β€” + the model that scales economically to thousands of sessions. Choose it as it reaches GA. Cost + today: pre-GA control-plane maturity + the plumbing above. + +Both share the **same coder, same pipeline, same review gates, same UX** β€” so migrating between +substrates is a label change, invisible to the developer/issue author. diff --git a/docs/dark-factory/SUBSTRATE-DIAGRAMS.md b/docs/dark-factory/SUBSTRATE-DIAGRAMS.md new file mode 100644 index 00000000..26442e88 --- /dev/null +++ b/docs/dark-factory/SUBSTRATE-DIAGRAMS.md @@ -0,0 +1,134 @@ +# Dark Factory β€” Substrate Diagrams (Kata vs Lambda MicroVM) + +Visual companion to [`SUBSTRATE-BENCHMARK.md`](./SUBSTRATE-BENCHMARK.md). All diagrams are +Mermaid (render on GitHub). + +--- + +## 1. Label-routed to two SEPARATE WorkflowTemplates + +The Argo Events sensor routes each label to a **different** WorkflowTemplate, so Kata's certified +pipeline is never touched by Flow D. Kata keeps its SandboxClaim; Lambda provisions a MicroVM directly. + +```mermaid +flowchart TD + ISSUE["GitHub issue labeled"] --> SENSOR["Argo Events sensor"] + SENSOR -->|"dark-factory
(issue-labeled-kata)"| DFRUN["df-run
(certified Kata)"] + SENSOR -->|"darkfactory-lambda
(issue-labeled-lambda)"| DFRUNL["df-run-lambda
(MicroVM-native)"] + DFRUN --> KCLAIM["claim-sandbox
(warm Kata pod)"] --> KCODE["drive-coder"] + DFRUNL --> PROV["provision-microvm
(create Microvm CR + POST /run)"] --> LCODE["drive-coder"] + LCODE --> SUSP["suspend-microvm
(scale-to-zero)"] + KCODE --> GATES["holdout · detect→deploy-test
devops-gate Β· security-agent"] + SUSP --> GATES + GATES --> STATUS["status (consolidated verdict)"] + STATUS --> EXIT["onExit: Kata deletes claim Β·
Lambda KEEPS suspended VM"] +``` + +The Kata graph has **zero MicroVM nodes**. `suspend-microvm` is explicit and lives only in +`df-run-lambda`; suspend/resume is driven by the workflow itself (Β§4), not a bridge or controller. + +--- + +## 2. Kata micro-VM substrate (Flow B) + +```mermaid +flowchart LR + CLAIM["SandboxClaim"] --> OP["agent-sandbox operator"] + OP --> POD["Kata pod (kata-clh)
on nested-virt node group"] + POD --> ENT["entrypoint.js (baked)"] + ENT -->|models| BIF["Bifrost gateway
(ClusterIP, in-cluster)"] + BIF --> BED["Bedrock"] + ENT -->|git/gh :443| GH["GitHub β†’ PR"] + ENT -->|secrets| SEC["/etc/secrets
(projected tmpfs)"] + POD -.native logs.-> KL["kubectl logs"] + BIF -.traces.-> LF["Langfuse"] +``` + +- Pre-warmed pod β†’ **instant claim**. +- Workspace is a **mounted writable volume**; logs are native; models via Bifrost (with Langfuse + traces). Node pool runs continuously. + +--- + +## 3. Lambda MicroVM substrate (Flow D) β€” MicroVM-native, no bridge + +```mermaid +flowchart LR + PROV["provision-microvm step
(dark-factory-workflow SA
+ lambda-microvms role)"] -->|reads handoff| IMG["MicrovmSandbox status
imageARN + execRoleARN
(built once by KRO/ACK)"] + PROV -->|creates mvm-<issue-number>| MCR["Microvm CR
(runHookPayload = Secret ref,
autoResume=false)"] + MCR --> CTRL["lambdamicrovms controller"] + CTRL -->|RunMicrovm cold-start| VM["Firecracker MicroVM
hook-server :8080"] + PROV -->|mint token, POST /run| VM + VM --> ENT["entrypoint.js (USE_BEDROCK=1)"] + ENT -->|models, direct| BED["Bedrock
(exec role, public egress)"] + ENT -->|git/gh :443| GH["GitHub β†’ PR"] + VM -.coder stdout.-> LOGS["GET /logs (token)"] + SUSP["suspend-microvm step
(after PR)"] -->|suspend-microvm| VM + MERGE["df-merge-teardown
(at merge)"] -->|delete CR| TERM["controller TerminateMicrovm"] +``` + +- One workflow **step** (`provision-microvm`) does create + drive `/run` β€” **no bridge pod, no + SandboxClaim, no warm pool**. `RunMicrovm` cold-start per session (~90s); no node pool. +- No cluster network dependency β€” **Bedrock-direct**. Logs via `/logs`. The explicit + `suspend-microvm` step suspends after the PR; the VM stays suspended (autoResume=false) until a fix + round resumes it or merge terminates it. + +--- + +## 4. Lambda suspend / resume (workflow-driven; warm resume + recreate-fallback) + +```mermaid +sequenceDiagram + participant W as df-run-lambda (workflow) + participant C as lambdamicrovms controller + participant V as MicroVM + W->>C: provision: create Microvm CR (autoResume=false) + C->>V: RunMicrovm (cold-start) + V-->>W: RUNNING + endpoint + W->>V: POST /run (token) β†’ coder starts β†’ PR + W->>V: suspend-microvm step + Note over V: SUSPENDED β€” stays down (no endpoint polling) + Note over W,V: review gates run while VM is suspended (free) + Note over W,V: FIX ROUND (df-iterate β†’ df-run-lambda): + W->>V: resume-microvm (warm β€” SAME VM) + alt resume OK (pre-GA happy path) + V-->>W: RUNNING β†’ POST /run β†’ coder re-runs β†’ new commit + else resume fails (pre-GA flakiness β†’ VM terminated) + W->>C: recreate: fresh Microvm CR + C->>V: RunMicrovm β†’ coder re-runs β†’ new commit + end + W->>C: at merge (df-merge-teardown): delete Microvm CR + C->>V: TerminateMicrovm +``` + +The CR is named `mvm-` (stable across rounds) β†’ exactly one VM per issue, so +suspend/resume never flap between competing owners. + +--- + +## 5. End-to-end lifecycle (issue β†’ PR β†’ fix β†’ merge) β€” both substrates + +```mermaid +flowchart TD + A["Issue labeled"] --> B["df-run: claim + coder β†’ PR"] + B --> C["DevOps Agent + Security Agent review"] + C --> D{"Security findings?"} + D -->|clean| APR["Human approves PR"] + D -->|findings| FIX["Human comments 'fix findings'"] + FIX --> IT["df-iterate β†’ df-run (same substrate via trigger-label)"] + IT --> B + APR --> MERGE["df-merge-teardown:
merge PR + release/terminate sandbox"] +``` + +The loop is identical for both substrates; `df-iterate` reads the originating issue's label to +route the fix round back to the **same** substrate (Kata pool or Lambda pool). + +--- + +## Legend / key facts + +- **Warm pool:** Kata = ready pods (instant); Lambda = bridge pods that RunMicrovm on claim. +- **LLM:** Kata β†’ Bifrost (traced in Langfuse); Lambda β†’ Bedrock-direct (exec role). +- **Workspace:** Kata β†’ mounted volume; Lambda β†’ `/tmp/workspace` (read-only rootfs). +- **Logs:** Kata β†’ `kubectl logs`; Lambda β†’ `/logs` endpoint (+ build logs in CloudWatch). +- **Teardown:** Kata β†’ release claim; Lambda β†’ delete `Microvm` CR β†’ TerminateMicrovm. diff --git a/docs/dark-factory/diagrams/flow-a-sandbox-capability.md b/docs/dark-factory/diagrams/flow-a-sandbox-capability.md new file mode 100644 index 00000000..caf6e44d --- /dev/null +++ b/docs/dark-factory/diagrams/flow-a-sandbox-capability.md @@ -0,0 +1,40 @@ +# Flow A β€” Agent Sandbox Capability (permanent platform feature) + +The **Agent Sandbox** capability ships as a first-class agent-platform GitOps addon. When the +platform is deployed, it stands up the Kata (Cloud Hypervisor) micro-VM runtime on a dedicated +nested-virt node group, the `Sandbox` CRD + operator, and a **warm pool** of pre-provisioned, +hardware-isolated sandboxes kept ready by the operator's native `SandboxWarmPool`. This is +independent of the Dark Factory β€” it is the reusable isolation substrate any agent workload can +claim. + +> **Hosted on the hub cluster** β€” the build/author plane, co-located with Argo Workflows so Flow B +> orchestrates the pool single-cluster. Because the hub runs EKS Auto Mode (which can't host Kata) +> and the fleet control plane (Keycloak/ArgoCD/external-secrets), the capability requires a +> **dedicated tainted nested-virt node group** and **control-plane egress isolation** β€” see +> [README Β§3](../README.md#3-flow-a--agent-sandbox-capability-permanent-platform-feature) and +> [Β§10](../README.md#10-security-model). + +> 🎨 Diagrams are editable draw.io β€” sources in [`src/`](./src/), rendered PNGs in [`img/`](./img/). + +--- + +## A.1 β€” Capability architecture + +Shipped as a GitOps addon: an ApplicationSet renders the operator, the Kata runtime on a nested-virt +node group, and a warm pool that any workload (notably the Dark Factory) claims on demand. + +![Flow A β€” capability architecture](./img/flow-a-capability.png) + +*Edit: [`src/flow-a-capability.drawio`](./src/flow-a-capability.drawio)* + +--- + +## A.2 β€” Warm-pool cycling (claim ↔ refill ↔ scale-to-zero) + +The operator keeps a steady buffer of idle sandboxes so a consumer binds a **ready** VM instantly +instead of paying cold-start. Idle sandboxes use the `Sandbox` `replicas: 0/1` **scale subresource**, +so "idle" is cheap; on claim the operator provisions a refill to keep the buffer at target. + +![Flow A β€” warm-pool cycling](./img/flow-a-warmpool.png) + +*Edit: [`src/flow-a-warmpool.drawio`](./src/flow-a-warmpool.drawio)* diff --git a/docs/dark-factory/diagrams/flow-b-dark-factory.md b/docs/dark-factory/diagrams/flow-b-dark-factory.md new file mode 100644 index 00000000..be21dd09 --- /dev/null +++ b/docs/dark-factory/diagrams/flow-b-dark-factory.md @@ -0,0 +1,125 @@ +# Flow B β€” Dark Factory (issue β†’ PR β†’ merge β†’ teardown) + +A GitHub issue is a **spec**. On the **hub cluster**, **Argo Workflows** claims a warm Kata sandbox, +a pluggable coding assistant implements + tests the change and pushes a branch, the workflow opens a +PR and runs independent verification (holdout gate + AWS Security/DevOps review), a human approves on +**results**, and everything is torn down on merge. Autonomy **Level 3**: the human's only job is to +approve the merge. + +> **Runs on the hub** β€” the build/author plane, co-located with Argo Workflows and the Flow A warm +> pool. Single-cluster orchestration: the workflow watches the coder pod and eval Job directly. See +> [README Β§2](../README.md#2-two-flows-at-a-glance) for *why the hub, not a spoke*, and +> [Β§10](../README.md#10-security-model) for the control-plane isolation that makes it safe. + +> 🎨 Diagrams are editable draw.io β€” sources in [`src/`](./src/), rendered PNGs in [`img/`](./img/). + +--- + +## B.1 β€” End-to-end lifecycle + +Issue β†’ claim a warm micro-VM β†’ coder implements + tests β†’ verify β†’ open PR with a live sticky +status β†’ human approves on results β†’ merge + teardown. A bounded feedback loop routes review +comments back to the coder. + +![Flow B β€” end-to-end lifecycle](./img/flow-b-lifecycle.png) + +*Edit: [`src/flow-b-lifecycle.drawio`](./src/flow-b-lifecycle.drawio)* + +--- + +## B.2 β€” Hub topology & three-layer isolation + +The Dark Factory shares a cluster with the fleet control plane, so untrusted coder VMs are fenced +off by **three independent, verified layers** β€” a standard NetworkPolicy (pod-IP egress), an +Admin-tier ClusterNetworkPolicy (applies to the Sandbox-CR-owned coder pods), and a ClusterIP +node-firewall (closes the EKS VPC-CNI DNAT gap). Net result: the coder reaches only DNS, Bifrost, +and public GitHub β€” never the control plane, node, or API server. + +![Flow B β€” hub topology and three-layer isolation](./img/flow-b-hub-topology.png) + +*Edit: [`src/flow-b-hub-topology.drawio`](./src/flow-b-hub-topology.drawio)* + +--- + +## B.3 β€” Event-driven lifecycle + +No long-running orchestrator. An **Argo Events Sensor** turns each GitHub webhook into a short-lived, +issue-keyed workflow (`df-run`, `df-iterate`, `df-merge-teardown`); durable state lives in the +retained workspace PVC + GitHub, not a parked process. + +![Flow B β€” event-driven lifecycle](./img/flow-b-lifecycle-events.png) + +*Edit: [`src/flow-b-lifecycle-events.drawio`](./src/flow-b-lifecycle-events.drawio)* + +--- + +## B.4 β€” The `df-run` DAG (as built) & how step-gating works + +This is the **implemented** pipeline (P1 + P2 holdout + P3 Security/DevOps reviewers, all solid +emerald), with the P4 `deploy-test` steps drawn dashed so the target shape is legible. It answers the +two questions the higher-level diagrams don't: **where each step runs** (trust boundary) and **how +Argo decides whether a step runs** (the `when:` gate). + +![Flow B β€” the df-run DAG as built](./img/flow-b-df-run-dag.png) + +*Edit: [`src/flow-b-df-run-dag.drawio`](./src/flow-b-df-run-dag.drawio)* + +**How a step is gated β€” `when:` on a prior step's output.** Argo is a declarative orchestrator, not +an agent: it does not "improvise" steps. Every task is *defined* in the DAG, and each carries an +optional `when:` expression that Argo evaluates at runtime against a value an earlier step emitted. +The `holdout-gate` step already does this: + +```yaml +- name: holdout-gate + dependencies: [drive-coder] + when: "{{tasks.drive-coder.outputs.parameters.pr-number}} != \"\"" # run only if a PR exists +``` + +`drive-coder` writes the PR number to a file β†’ Argo captures it as an output parameter β†’ Argo +substitutes the real value into the `when:` string (`"7" != ""` β†’ **run**; `"" != ""` β†’ **skip**, +no pod is created). Deterministic, file/value-based β€” no AI in the decision. + +**Conditional deploy-test (P4) uses the same mechanism, keyed on the diff.** A cheap `detect-deployable` +step runs `git diff --name-only` and greps for deployable files (`Chart.yaml`, `k8s/`, `deployment.yaml`, +`Dockerfile`); it emits `deployable = true|false`; `deploy-test` is gated `when: deployable == true`. +So "the PR touched a Deployment manifest β†’ deploy-test runs; it only touched app code β†’ skip." + +**Two levels of testing, two homes (the trust boundary):** + +| Test level | What it checks | Where it runs | K8s access | +|---|---|---|---| +| **Unit / build** | compiles, `subtract(5,3)==2`, `npm test`/`go test` green | inside the **coder** (Kata VM) + re-run by **holdout-gate** | **none** (correct β€” the VM is untrusted) | +| **Deploy / integration** (P4) | deploys to an ephemeral namespace, endpoint returns 200, pod healthy | a **trusted hub step** (`deploy-test`), never the VM | **yes** β€” held by the workflow SA, never the coder | + +**Executable tests decide; LLM/agents advise.** The holdout's hidden tests are the ground truth (a +stub can't pass a real test); the Nova judge is a *reviewer* that catches gaming the tests can't see +(hard-coded inputs, `return true`). The **P3 Security/DevOps reviewers** are the same shape β€” their +deterministic linters (secret scan, `npm audit`; Dockerfile/k8s hygiene) are the hard signal and the +Nova reviewer is the advisory second opinion; both are read-only on the diff and post +`dark-factory/{security,devops}` statuses (advisory in v1). For deploy-work, the `deploy-test` +executable probes will be ground truth and the DevOps agent the advisory second opinion. + +--- + +## B.5 β€” The one sticky PR comment + +The workflow maintains **one** comment, edited in place via a hidden marker β€” no comment spam. Until +tests are green there is no PR, so pre-PR status lives on the **issue**; from PR-open onward the +comment is the canonical board. Parallel review steps are serialized by a per-issue mutex. + +``` +## 🏭 Dark Factory β€” issue #42 Β· PR #128 +βœ… Claimed sandbox (hub) 12:01 +βœ… Branch df/issue-42 12:01 +βœ… Implement 12:04 +βœ… Build + unit tests 12:07 πŸ“„ log +βœ… PR opened #128 12:07 +⏳ Security review… +⬜ DevOps review +⬜ Holdout gate (0/12) +⬜ Ready for review +``` + +Each stage links to raw logs / the Argo run / the Langfuse trace (**verifiability-by-citation**). +The PR **body** carries the final report: what changed, test results, holdout satisfaction %, and +the Security/DevOps findings. diff --git a/docs/dark-factory/diagrams/flow-d-microvm-sandbox.md b/docs/dark-factory/diagrams/flow-d-microvm-sandbox.md new file mode 100644 index 00000000..0eadcd25 --- /dev/null +++ b/docs/dark-factory/diagrams/flow-d-microvm-sandbox.md @@ -0,0 +1,138 @@ +# Flow D β€” Lambda MicroVM–backed Agent Sandbox (alternative substrate) + +**Flow D is a second Flow-A substrate.** Where Flow A hands out **Kata micro-VM pods** on a +self-managed nested-virt EKS node group, Flow D hands out **AWS Lambda MicroVMs** provisioned by the +ACK `lambdamicrovms` controller and composed by a single **KRO `ResourceGraphDefinition`**. The Agent +Sandbox UX is unchanged: a consumer (notably **Flow B β€” Dark Factory**) creates a `SandboxClaim`, a +pod shows up, and the **same `dark-factory-coder`** runs its coding/testing loop β€” except the coder +executes inside a Lambda MicroVM instead of on the Kata node. + +> **Flow C is reserved for other work** β€” this substrate is **Flow D**. + +> **Why a second substrate?** Kata (Flow A) needs a dedicated nested-virt node group the platform owns +> and pays for while idle. Lambda MicroVM is a **serverless** micro-VM: no node group to run, per-claim +> lifecycle, sub-second warm starts, and a clean **platform-owns-the-image / app-owns-the-instance** +> split that maps directly onto the two ACK CRDs. Flow B can target either substrate with no pipeline +> change β€” it only ever sees the Agent Sandbox `SandboxClaim` contract. + +> 🎨 Diagrams are editable draw.io β€” sources in [`src/`](./src/), rendered PNGs in [`img/`](./img/). +> *(Flow D diagram sources are added alongside the Flow A/B ones; see `src/flow-d-*.drawio`.)* + +--- + +## D.1 β€” Substrate architecture (KRO RGD over ACK primitives) + +The platform installs two controllers and one composition layer, then exposes **one** custom +resource to consumers: + +- **Managed ACK** (EKS Capability) runs the **GA** controllers β€” `iam.services.k8s.aws` (Role) and + `s3.services.k8s.aws` (Bucket) β€” that the MicroVM image + instance depend on. +- **Self-managed ACK** runs **only** the pre-GA `lambdamicrovms.services.k8s.aws` controller (its own + Helm chart / ArgoCD addon), because Managed ACK bundles GA controllers only. +- **Managed KRO** (EKS Capability) runs the `ResourceGraphDefinition` engine. +- A single **`MicrovmSandbox` RGD** ties it all together: one CR expands into `MicrovmImage` + + `Microvm` + IAM `Role`(s) + S3 `Bucket`. + +``` +consumer (Flow B / any agent) + β”‚ creates + β–Ό + MicrovmSandbox (kro.run/v1alpha1 β€” the single abstraction) + β”‚ expands into + β”œβ”€β”€ MicrovmImage (lambdamicrovms.services.k8s.aws) ── platform-owned inputs + β”œβ”€β”€ S3 Bucket (s3.services.k8s.aws) ── image codeArtifact store + β”œβ”€β”€ IAM Role (build) (iam.services.k8s.aws) ── MicrovmImage.buildRoleArn + β”œβ”€β”€ IAM Role (exec) (iam.services.k8s.aws) ── Microvm.executionRoleArn + └── Microvm (lambdamicrovms.services.k8s.aws) ── app-owned instance lifecycle +``` + +*Edit: `src/flow-d-substrate.drawio` β†’ `img/flow-d-substrate.png`.* + +--- + +## D.2 β€” Platform-owned vs app-owned split (inside one RGD) + +The two ACK CRDs encode the ownership boundary the platform team and application teams care about; +the RGD schema surfaces each half to the right owner: + +| Layer | Owner | ACK resource | Key fields | +|---|---|---|---| +| **Image / substrate** | Platform | `MicrovmImage` | `baseImageARN`, `buildRoleArn`, `codeArtifact.uri` (S3), egress connectors | +| **Instance / run** | App team | `Microvm` | `imageIdentifier`, `executionRoleArn`, `ingress/egressNetworkConnectors`, `idlePolicy` | + +- **Platform** sets the image once (built **from the existing `dark-factory-coder` image** + its + `entrypoint.js`, published to the S3 `codeArtifact` bucket) β€” declarative, ACK-managed, GitOps. +- **App teams / Flow B** create per-claim `Microvm` instances referencing that image, and own the + instance lifecycle (`RunMicrovm` / `TerminateMicrovm`, idle policy) via the same claim they use today. + +*Edit: `src/flow-d-ownership.drawio` β†’ `img/flow-d-ownership.png`.* + +--- + +## D.3 β€” The RuntimeClass shim (claim β†’ pod β†’ MicroVM) + +A literal Kubernetes `RuntimeClass` (like `kata-clh`) maps to a **node-local containerd handler**. +Lambda MicroVM is a **remote AWS service**, so a true node-level RuntimeClass isn't possible without a +virtual-kubelet provider (a large Go runtime component β€” explicitly **out of scope**). Flow D uses a +**RuntimeClass-marked bridge pod** instead, preserving the exact Agent Sandbox UX: + +``` +SandboxClaim (Flow B injects DF_ISSUE_NUMBER, repo, branch β€” unchanged) + β”‚ + β–Ό +Sandbox β†’ Pod from the `lambda-microvm` SandboxTemplate variant + β”‚ (bridge container; lands on a normal Auto-Mode node, NOT the kata pool) + β–Ό +bridge applies a MicrovmSandbox (KRO) CR + β”‚ + β–Ό +Microvm RUNNING ── runs the SAME dark-factory-coder entrypoint (node /app/entrypoint.js) + β”‚ + β”œβ”€β”€ bridge streams MicroVM logs β†’ pod logs (pod Running ⇔ Microvm RUNNING) + └── pod exit / claim teardown β†’ TerminateMicrovm +``` + +To Flow B and the user this is identical to Flow A β€” "a sandbox pod appeared and ran the coder" β€” but +the coder actually executed in the Lambda MicroVM. Log streaming is straightforward; interactive +exec/attach passthrough is **best-effort** (full fidelity would need virtual-kubelet). + +*Edit: `src/flow-d-shim.drawio` β†’ `img/flow-d-shim.png`.* + +--- + +## D.3a β€” Suspend / resume (Sandbox.operatingMode β†’ MicroVM) + +The Agent Sandbox CRD exposes `spec.operatingMode ∈ {Running, Suspended}` β€” the declarative +suspend/resume intent. But the ACK `Microvm` CR has **no suspend field**: its spec is create-time +only, `State` is status-only, and `suspend-microvm`/`resume-microvm` are **imperative SDK ops the ACK +controller deliberately does not reconcile**. So flipping `operatingMode` does nothing on its own β€” a +controller must translate intent into the SDK call. + +Flow D closes that gap with a tiny always-on **`microvm-lifecycle`** reconcile loop (a ConfigMap +script on `alpine/k8s`, same pattern as the pool-manager β€” **no virtual-kubelet, no new image**): + +``` +Sandbox.operatingMode: Running β†’ Suspended : aws lambda-microvms suspend-microvm --microvm-identifier +Sandbox.operatingMode: Suspended β†’ Running : aws lambda-microvms resume-microvm --microvm-identifier +``` + +- `` (the `microvmID`) is resolved from the `MicrovmSandbox` (KRO) status; the loop is idempotent + (stamps a `last-mode` annotation, acts only on transitions). +- The `MicrovmSandbox` is **kept** across suspend (the bridge's `preStop` detects `operatingMode: + Suspended` and skips teardown), so the VM survives suspend/resume; it's deleted only on real claim + teardown β†’ `TerminateMicrovm`. +- Chosen over bridge `preStop` hooks alone because a reconcile loop is **robust to pod/node loss** and + resume needs no live pod. This is the open-source **Sandbox-CRD-driven** suspend/resume you get with + the MicroVM substrate. + +*Edit: `src/flow-d-suspend-resume.drawio` β†’ `img/flow-d-suspend-resume.png`.* + +--- + +## D.4 β€” Future: when `lambdamicrovms` goes GA + +`lambdamicrovms` is currently **pre-GA** (`v1alpha1`), so its controller is self-managed. When it +graduates to GA upstream, **Managed ACK adopts it automatically** β€” the self-managed chart is deleted +and the `MicrovmSandbox` RGD is **unchanged** (it references the same `lambdamicrovms.services.k8s.aws` +CRDs regardless of who runs the controller). The design deliberately keeps the RGD independent of the +controller install method so this migration is a one-line addon removal. diff --git a/docs/dark-factory/diagrams/img/flow-a-capability.png b/docs/dark-factory/diagrams/img/flow-a-capability.png new file mode 100644 index 00000000..8290cbc6 Binary files /dev/null and b/docs/dark-factory/diagrams/img/flow-a-capability.png differ diff --git a/docs/dark-factory/diagrams/img/flow-a-warmpool.png b/docs/dark-factory/diagrams/img/flow-a-warmpool.png new file mode 100644 index 00000000..96e84fb7 Binary files /dev/null and b/docs/dark-factory/diagrams/img/flow-a-warmpool.png differ diff --git a/docs/dark-factory/diagrams/img/flow-b-df-run-dag.png b/docs/dark-factory/diagrams/img/flow-b-df-run-dag.png new file mode 100644 index 00000000..337be0dc Binary files /dev/null and b/docs/dark-factory/diagrams/img/flow-b-df-run-dag.png differ diff --git a/docs/dark-factory/diagrams/img/flow-b-hub-topology.png b/docs/dark-factory/diagrams/img/flow-b-hub-topology.png new file mode 100644 index 00000000..dd0b14d1 Binary files /dev/null and b/docs/dark-factory/diagrams/img/flow-b-hub-topology.png differ diff --git a/docs/dark-factory/diagrams/img/flow-b-lifecycle-events.png b/docs/dark-factory/diagrams/img/flow-b-lifecycle-events.png new file mode 100644 index 00000000..9384d307 Binary files /dev/null and b/docs/dark-factory/diagrams/img/flow-b-lifecycle-events.png differ diff --git a/docs/dark-factory/diagrams/img/flow-b-lifecycle.png b/docs/dark-factory/diagrams/img/flow-b-lifecycle.png new file mode 100644 index 00000000..14461484 Binary files /dev/null and b/docs/dark-factory/diagrams/img/flow-b-lifecycle.png differ diff --git a/docs/dark-factory/diagrams/src/flow-a-capability.drawio b/docs/dark-factory/diagrams/src/flow-a-capability.drawio new file mode 100644 index 00000000..29bb04e5 --- /dev/null +++ b/docs/dark-factory/diagrams/src/flow-a-capability.drawio @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/dark-factory/diagrams/src/flow-a-warmpool.drawio b/docs/dark-factory/diagrams/src/flow-a-warmpool.drawio new file mode 100644 index 00000000..611d454a --- /dev/null +++ b/docs/dark-factory/diagrams/src/flow-a-warmpool.drawio @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/dark-factory/diagrams/src/flow-b-df-run-dag.drawio b/docs/dark-factory/diagrams/src/flow-b-df-run-dag.drawio new file mode 100644 index 00000000..a37a613d --- /dev/null +++ b/docs/dark-factory/diagrams/src/flow-b-df-run-dag.drawio @@ -0,0 +1,158 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/dark-factory/diagrams/src/flow-b-hub-topology.drawio b/docs/dark-factory/diagrams/src/flow-b-hub-topology.drawio new file mode 100644 index 00000000..dc1591be --- /dev/null +++ b/docs/dark-factory/diagrams/src/flow-b-hub-topology.drawio @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/dark-factory/diagrams/src/flow-b-lifecycle-events.drawio b/docs/dark-factory/diagrams/src/flow-b-lifecycle-events.drawio new file mode 100644 index 00000000..a3414440 --- /dev/null +++ b/docs/dark-factory/diagrams/src/flow-b-lifecycle-events.drawio @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/dark-factory/diagrams/src/flow-b-lifecycle.drawio b/docs/dark-factory/diagrams/src/flow-b-lifecycle.drawio new file mode 100644 index 00000000..f1dfa71e --- /dev/null +++ b/docs/dark-factory/diagrams/src/flow-b-lifecycle.drawio @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/dark-factory/flow-d-coder-in-microvm-design.md b/docs/dark-factory/flow-d-coder-in-microvm-design.md new file mode 100644 index 00000000..05101b87 --- /dev/null +++ b/docs/dark-factory/flow-d-coder-in-microvm-design.md @@ -0,0 +1,214 @@ +# Flow D β€” Running the Coder *inside* the Lambda MicroVM (design) + +**Status:** design / spike β€” NOT implemented. Written after proving the Flow D **substrate** +end-to-end and discovering that running the actual coder in the VM is an application +re-architecture, not a wiring task. + +## TL;DR + +The Flow D **substrate + lifecycle is proven live**: a `darkfactory-lambda` GitHub issue β†’ +Argo sensor β†’ `df-run` claims the Lambda warm pool β†’ the bridge calls `RunMicrovm` β†’ a real +Lambda MicroVM reaches **RUNNING** in AWS β†’ `suspend`/`resume`/`terminate` are wired β†’ the VM +is terminated on teardown (verified, zero orphans). + +What is **NOT** done: the coder does not actually *execute* inside that MicroVM, so no PR is +produced. That is because the one-shot `dark-factory-coder` and the Lambda MicroVM +snapshot/hook execution model are **fundamentally different execution shapes**. Closing the gap +requires re-architecting the coder, plus VPC/Bifrost networking. This doc specifies that work +so it can be decided deliberately. + +## Why it isn't just wiring β€” the execution-model mismatch + +| | Kata coder (Flow B, works today) | Lambda MicroVM model | +| --- | --- | --- | +| Shape | **one-shot batch process**: `node entrypoint.js` runs cloneβ†’agentβ†’pushβ†’PR, then exits | **long-lived HTTP service** that is *snapshotted* at build, *resumed* per session | +| Duration | 5–15 min per run | per-request; the `run` lifecycle hook has a **30s timeout** ("keep it short β€” on the critical path") | +| Trigger | pod start + `DF_ISSUE_NUMBER` env injected by the SandboxClaim | build-time `ready`/`validate` hooks; per-instance `run` hook receives `runHookPayload` as the HTTP request body | +| Secrets/context | files projected into the pod (`/etc/secrets/gh-token`, `bifrost-api-key`) + `DF_*` env | `runHookPayload` β€” a **Kubernetes `SecretKeyReference`** on the `Microvm` CR, delivered as the `/run` hook body (≀16 KB); image must set `hooks.microvmHooks.run: ENABLED` | +| Network | in-cluster: reaches Bifrost by ClusterIP `172.20.181.17:8080`; git/gh over public :443 | runs **outside the cluster network**; only `INTERNET_EGRESS` by default; cannot reach a ClusterIP; VPC reach needs an egress **network connector** | + +The killer facts (verified against `mmeckes/lambdamicrovms-controller` docs + the live `aws +lambda-microvms`/`lambda-core` CLIs, 2026-08-03): + +1. **`/run` hook = 30s timeout.** A 5–15 min coder run cannot happen *in* the hook. +2. **The intended app model is request/response** (02-developer-handoff: RunMicrovm β†’ mint + auth token β†’ HTTP request β†’ response β†’ terminate) β€” not a batch job. +3. **`runHookPayload` is delivered as an HTTP body to a `/run` endpoint the app must SERVE** β€” + NOT an env var and NOT a mounted file. (An earlier attempt at an env/file boot-shim was + wrong and is discarded.) +4. So the coder must be **wrapped in a long-running HTTP server** that starts the coding work + asynchronously β€” the coder's current `entrypoint.js` is not written that way. + +## Proposed design (async `/run` pattern) + +Keep the coder *logic* (`entrypoint.js`) intact; change how it is *invoked*. + +``` +build: MicrovmImage (FROM arm64 dark-factory-coder + a thin HTTP wrapper) + hooks.port: 8080 + microvmImageHooks.ready: server up β†’ safe to snapshot + microvmHooks.run: ENABLED (30s), suspend/resume/terminate ENABLED + +run: controller delivers runHookPayload (Secret {ghToken, bifrostKey, bifrostUrl(NLB), + issueNumber, repo, branch, baseBranch, title}) as the /run body + β†’ wrapper writes /etc/secrets/{gh-token,bifrost-api-key} + exports DF_*/BIFROST_URL + β†’ wrapper spawns `node entrypoint.js` in the BACKGROUND, returns 200 within 30s + β†’ coder does cloneβ†’agentβ†’pushβ†’PR async (many minutes) + +observe: df-run's existing `await-coder` step ALREADY polls GitHub for the PR head β€” reuse it + verbatim; it doesn't care whether the coder ran in Kata or a MicroVM. + +teardown: suspend/resume/terminate hooks best-effort flush; bridge TerminateMicrovm on exit. +``` + +### Components to build + +1. **HTTP wrapper + artifact** (`coder-microvm/`): a small server (`server.js`) exposing + `ready`, `run`, `suspend`, `resume`, `terminate` on :8080; `run` materializes the payload + into the coder's existing file/env contract and background-spawns `entrypoint.js`. Dockerfile + `FROM 940019131157.dkr.ecr.us-west-2.amazonaws.com/dark-factory-coder:-arm64`. Zip + (Dockerfile + server.js) β†’ S3, per the controller's `ci/package-artifact.sh` format. + *(Supersedes the placeholder `microvm-entry.js` listener that only existed to get the image + to CREATED.)* + +2. **MicrovmImage: enable hooks** (RGD `templates/image/10-rgd-and-image.yaml`): add + `hooks.port: 8080`, `microvmImageHooks.ready: ENABLED`, `microvmHooks.run/suspend/resume/ + terminate: ENABLED`. Without `run: ENABLED` the payload is silently never delivered. + +3. **VPC egress connector** (bootstrap Job β€” honest: *GitOps-provisioned, not continuously + reconciled*; no ACK/Crossplane API exists for `lambda-core` connectors). Committed + find-or-create Job modeled on `06-securityagent-bootstrap.yaml`: + `aws lambda-core get/create-network-connector` with + `VpcEgressConfiguration={SubnetIds:[hub subnets], SecurityGroupIds:[sg], NetworkProtocol:IPv4}` + β†’ writes the connector ARN to a ConfigMap the MicrovmImage `egressNetworkConnectors` reads. + IAM: the bootstrap/capability role needs `lambda-core:*NetworkConnector*` + the EC2 ENI perms + Lambda uses to provision ENIs. **Caveat:** if the connector is deleted out-of-band, nothing + self-heals until the Job re-runs (not a controller). + +4. **Bifrost VPC-reachable** (internal NLB β€” this part *is* declarative): a `Service + type=LoadBalancer` with `service.beta.kubernetes.io/aws-load-balancer-internal: "true"` + + `nlb-target-type` in the bifrost chart, reconciled by the AWS Load Balancer Controller. The + MicroVM (via the egress connector) reaches Bifrost at the NLB's stable VPC address on :8080. + (Bifrost's pod IP `10.0.x.x` is in-VPC and reachable via the connector, but ephemeral β€” the + NLB gives a stable target. Its ClusterIP `172.20.x.x` is NOT routable from a VPC ENI.) + **Shared-infra change β€” needs owner sign-off.** + +5. **runHookPayload Secret + Microvm wiring**: the bridge (or a per-session step) writes a + Secret with the payload key and the `Microvm`/RunMicrovm references it as + `runHookPayload: {name, key}`. Since it's a SecretKeyReference the **controller** delivers it + β€” confirm whether the imperative `RunMicrovm` path the bridge uses accepts the same, or + whether this session should create a short-lived `Microvm` CR instead. + +6. **Security-group rules**: allow the connector ENIs β†’ Bifrost NLB on :8080. + +## Open questions for review + +- **Async vs request-driven?** Background-spawn (df-run polls for the PR, minimal coder change) + vs. the reference's request/response model (bridge sends an HTTP "code this" request + waits; + needs the auth-token path). Background-spawn reuses `await-coder` and is less invasive. +- **Imperative RunMicrovm vs a `Microvm` CR per session?** `runHookPayload` being a + SecretKeyReference is controller-delivered; the current bridge calls `aws run-microvm` + imperatively. Decide whether per-session VMs become short-lived `Microvm` CRs (declarative + payload delivery) or stay imperative (verify the CLI accepts an inline/secret payload). +- **Cost:** the VPC egress connector provisions ENIs; the internal NLB is an hourly resource. + Both are ongoing while Flow D is enabled. +- **Is in-VM coder even required for the goal?** The substrate is a valid deliverable on its + own (a second sandbox substrate). Running the coder in it is the "make it actually code" step + β€” worth confirming it's in scope before the re-architecture. + +## 2026-08-03 build attempt β€” where it got to + the confirmed blocker + +Built and deployed the Bedrock-direct async design end-to-end. Live results: + +- βœ… **lambda-coder artifact + image**: `examples/dark-factory/coder-microvm/` (hook-server.js + + Dockerfile FROM the arm64 coder + the USE_BEDROCK entrypoint branch). MicrovmImage rebuilt + to **UPDATED** with `hooks` enabled; exec role has `bedrock:InvokeModel*`. Verified the + hook-server runs in the VM β€” CloudWatch shows `[hook-server] listening on :8080 (lambda-coder, + Bedrock-direct)`. +- βœ… **Bridge payload**: builds JSON (issue ctx + GitHub token + region) with python3 (node is + absent in the aws-cli image) and passes `--run-hook-payload` on `run-microvm`. VM launches + RUNNING with the payload; no bridge crash. +- ❌ **BLOCKER: the `/run` hook never fires** β†’ the coder never starts in the VM β†’ no PR. + CloudWatch shows the server `listening` but never logs the `/run` handling / background-spawn. + +**Confirmed root cause:** `runHookPayload` is a **`SecretKeyReference`, "not a literal"** β€” the +docs + the 02-developer-handoff example deliver it via the **declarative `Microvm` CR** +(`runHookPayload: {name, key}` β†’ the self-managed controller reads the Secret and drives the +`/run` hook). The imperative `run-microvm --run-hook-payload ""` CLI path the bridge uses +does **not** invoke `/run` (VM reaches RUNNING but the hook is silent). Two things also worth +noting from the reference: (a) the intended session model is the CLIENT minting an auth token and +sending an HTTP request to the VM endpoint with `X-aws-proxy-auth` (request/response), and (b) the +`/run` hook is service-internal on VM start. + +**Correct path (next):** make the per-session VM a **`Microvm` CR** (declarative), not a bridge +CLI call: +- bridge (or a per-session step) writes a **Secret** with the payload key, then creates a + `Microvm` CR referencing it (`imageIdentifierRef`, `executionRoleRef`, `runHookPayload:{name,key}`, + `idlePolicy`), and reads back `status.microvmID` for the lifecycle annotation. +- the self-managed lambdamicrovms controller reconciles it and delivers the payload to `/run`, + which background-spawns the coder. +- teardown = delete the `Microvm` CR (controller terminates), replacing the imperative + TerminateMicrovm. +This trades the imperative bridge for the declarative CR path the payload mechanism actually +requires β€” and it's MORE GitOps-faithful. Est: bridge rewrite (CR create/delete instead of CLI) ++ a per-session Secret; the hook-server/artifact/image/IAM/Bedrock pieces are already done and verified. + +## 2026-08-03 (later) β€” declarative Microvm CR path: reconciles + VM runs, /run still silent + +Switched the bridge from the imperative `run-microvm` CLI to the **declarative `Microvm` CR** +path (write payload Secret β†’ create `Microvm` CR with `runHookPayload:{name,key}` β†’ controller +reconciles β†’ delete CR on teardown). Verified working: +- βœ… Bridge creates the Secret + `Microvm` CR (`mvm-`); RBAC for microvms+secrets added. +- βœ… Controller reconciles it: CR `state=RUNNING`, `ACK.ResourceSynced=True`, `status.microvmID` + populated, annotated on the Sandbox. Deleting the CR cleanly terminates the VM (0 orphans). +- βœ… MicrovmImage is v2.0, `UPDATED`, `hooks` present (run/ready/suspend/resume/terminate). +- ❌ **STILL no `/run` output**: CloudWatch `/aws/lambda/microvms/coder-image` shows the build-time + `[hook-server] listening on :8080` but ZERO runtime events after the VM starts β€” the coder never + logs, no PR. The `/run` hook is not producing coder execution we can observe. + +**What's ruled out:** payload delivery mechanism (now declarative CR, the documented path), image +hooks (present, v2.0 built), IAM (bedrock on exec role), bridge crash (restarts=0), YAML (renders +clean). **What's NOT yet proven:** that the service actually invokes `/run` against the hook-server, +and that hook-server's `/run` handler + background-spawn + Bedrock call execute. Can't see inside +the VM beyond CloudWatch (which is empty at runtime) β€” needs either (a) the VM's runtime logs routed +somewhere visible, (b) hitting the VM endpoint directly with an auth token (X-aws-proxy-auth) to +probe the hook-server, or (c) the controller/service confirming the run-hook HTTP call + its response. +This is the current debugging frontier β€” the substrate, image, CR path, and teardown all work; the +open question is purely whether/how the `/run` hook reaches the in-VM hook-server and why it emits +no logs. + +## 2026-08-03 E2E run #106 β€” full chain works to /run; 2 pinpointed gaps + +Ran a clean GH-issue E2E and **probed the VM directly** (minted an auth token, hit the endpoint). +Stage-by-stage: issue β†’ label β†’ workflow β†’ bridge claim β†’ Microvm CR (`mvm-106`) β†’ VM RUNNING with +endpoint β€” all βœ…. Then the decisive probes against the live VM: +- `GET https:///` (X-aws-proxy-auth) β†’ `{"status":"ok","path":"/"}` β†’ **hook-server is + ALIVE and reachable at runtime.** +- `POST /run` β†’ `{"status":"started"}` β†’ **the /run handler works and background-spawns the coder.** + +So the entire chain β€” including the hook-server and its /runβ†’coder-spawn β€” is functional. The two +remaining gaps are now precisely isolated: + +1. **The service does not auto-invoke `/run` on launch.** After RunMicrovm/Microvm-CR reconcile, the + run hook is not called automatically β€” I had to POST /run manually to start the coder. Either the + run hook fires on a trigger we're not hitting, or the payload/hook wiring needs a specific field to + auto-fire. (auth-token minting: `create-microvm-auth-token --expiration-in-minutes N --allowed-ports + port=8080`; token is at `.authToken.X-aws-proxy-auth`.) +2. **Runtime logs don't reach CloudWatch.** `logging.cloudWatch.logGroup` on the image captures BUILD + logs only; after the VM runs, `/aws/lambda/microvms/coder-image` has 0 runtime events even though the + hook-server clearly runs (proven by the probe). This blinded every prior run β€” need to wire runtime + stdout/stderr to CloudWatch (or read it another way) to observe the coder. + +Both are now concrete, small-surface problems (a hook-trigger config + a log-routing config), NOT +architecture. The substrate, image+hooks, Bedrock exec role, declarative Microvm CR path, payload +delivery, hook-server, /runβ†’coder-spawn, and clean CR-delete teardown are all verified working. + +## What exists today (so nothing is lost) + +- Substrate live: RGD Active, S3 bucket, build/exec roles, **MicrovmImage CREATED (v1.0)**, + bridge launches/terminates a real MicroVM from a `darkfactory-lambda` issue. +- All the substrate + bridge fixes are committed on `flow-d-lambda-microvm-sandbox` (container + named `coder`, aws-cli v2 image, kubectl fetch, API-server + Pod Identity egress, + downward-API SANDBOX_NAME, microvmSuspend on, `darkfactory-lambda` label). +- The **placeholder** code artifact (`microvm-entry.js` listener) is what's in S3 today β€” it + only proved the image builds; it must be replaced per Β§1 above. diff --git a/examples/dark-factory/README.md b/examples/dark-factory/README.md new file mode 100644 index 00000000..88e3d133 --- /dev/null +++ b/examples/dark-factory/README.md @@ -0,0 +1,109 @@ +# Dark Factory β€” Flow B coder image + +The **in-VM coder** for the Dark Factory (Flow B). This directory builds the container image that +runs **inside the Kata micro-VM sandbox** β€” the untrusted side of the trust boundary. Orchestration +(trigger β†’ claim β†’ drive β†’ PR β†’ teardown) is **not** here: it's done declaratively by **Argo +Workflows on the hub** (see the [`dark-factory` Helm chart](../../gitops/addons/charts/dark-factory/) +and [`docs/dark-factory/README.md`](../../docs/dark-factory/README.md) Β§4). + +> **History:** an earlier P1 used a bespoke long-running **Node orchestrator** (`orchestrator/`) and +> an HTTP-server coder (`coder/agent.js`). Both were removed once Flow B moved to Argo Workflows β€” +> the orchestrator's responsibilities are now the `df-run` WorkflowTemplate's DAG steps (a `resource` +> template creates the `SandboxClaim`; a `script` step polls GitHub; `onExit` releases the claim), +> and the coder became **credential-less + self-reporting** (`entrypoint.js`). + +## What's here + +| Path | Role | Trust | +|---|---|---| +| `coder/entrypoint.js` | The in-VM coder. Auto-runs on VM start; reads the `DF_*` env the `SandboxClaim` injects, fetches the issue as `SPEC.md`, checks out `df/issue-`, runs Claude Code headless via Bifrost, builds + tests, pushes the branch, opens the PR, and sets the `dark-factory/implementation` commit status. | **untrusted** (Kata VM, no cloud creds, no k8s API) | +| `coder/Dockerfile` | Lean `node:20-alpine` + git/bash/python3/go + the Claude Code CLI. Carries **no** credentials. | β€” | + +## How the coder is driven (single-cluster on the hub) + +``` +GitHub issue (label: dark-factory) + β†’ Argo Events: GitHub webhook EventSource β†’ Sensor + β†’ df-run WorkflowTemplate (argo ns, per-issue mutex) + β”œβ”€ claim : resource template creates SandboxClaim(warmPoolRef=coder-warmpool) + β”‚ with the issue injected as env (DF_ISSUE_NUMBER/REPO/BRANCH/…) + β”‚ β†’ operator binds a warm Kata VM (Flow A), status Ready + β”‚ β”Œβ”€β”€ coder VM (this image) auto-runs entrypoint.js on start: + β”‚ β”‚ issue β†’ SPEC.md β†’ checkout df/issue-N β†’ claude implements β†’ + β”‚ β”‚ build + unit tests β†’ push branch β†’ open PR β†’ + β”‚ β”‚ POST commit status dark-factory/implementation = success|failure + β”‚ └── credential-less to the k8s API β†’ self-reports through GitHub + β”œβ”€ drive-coder : script step POLLS the GitHub API for a PR on df/issue-N and + β”‚ reads the head commit's dark-factory/implementation status + β”œβ”€ status : sticky status (P1 stops here β€” PR open, awaiting human) + └─ onExit : teardown β€” delete the SandboxClaim (operator refills the pool) + β†’ human reviews the PR, approves, merges +``` + +## The `DF_*` contract (env the claim injects) + +`entrypoint.js` is entirely env-driven. The `df-run` claim step sets these via `SandboxClaim.spec.env` +(verified contract: the SandboxTemplate opts in with `envVarsInjectionPolicy: Overrides`): + +| Var | Purpose | +|---|---| +| `DF_REPO` | `owner/name` of the target repo | +| `DF_ISSUE_NUMBER` | the GitHub issue number (the spec) | +| `DF_BRANCH` | `df/issue-` | +| `DF_BASE_BRANCH` | base to branch from (default `main`) | +| `DF_ISSUE_TITLE` | issue title (for the PR title) | +| `CODER_PROFILE` | `claude-code` (primary) or `kiro` | +| `BIFROST_URL` | LLM gateway β€” the **ClusterIP** (the Kata VM guest DNS can't resolve svc names) | + +Secrets are **not** in env: the short-TTL GitHub token (+ optional Bifrost key) are projected into +the VM at `/etc/secrets` (tmpfs, mode 0400) and read at point of use. + +## Bifrost gotchas baked into `entrypoint.js` + +The Claude Code CLI talks to models only through the platform's **Bifrost** gateway. Four issues had +to be solved to make `claude -p` work inside the locked-down VM (see the commit history + memory): + +1. **User-Agent routing** β€” Bifrost 400s (`Unexpected field type`) on any request whose UA starts + with `claude-cli`. `entrypoint.js` fronts Bifrost with a **localhost UA-shim** (a separate `node` + process on `127.0.0.1:8791`) that rewrites the UA and transparently forwards everything (incl. the + SSE stream). `ANTHROPIC_BASE_URL` points at the shim. +2. **Writable HOME** β€” `readOnlyRootFilesystem` makes `$HOME` read-only, so Claude Code can't create + `~/.claude` (shell snapshots its Bash tool needs). `HOME`/`CLAUDE_CONFIG_DIR`/XDG are pointed at + the writable `/tmp` tmpfs. +3. **Model alias** β€” use `ANTHROPIC_MODEL=claude-sonnet` (a Bifrost alias β†’ `us.anthropic.claude-sonnet-4-5`). + Do **not** set `CLAUDE_CODE_USE_BEDROCK` (that bypasses the base URL and needs in-VM AWS creds). +4. **`git push --force`** (not `--force-with-lease`) β€” the depth-1 clone can't satisfy the lease + check; `df/issue-N` is bot-owned + single-writer (per-issue workflow mutex), so `--force` is safe. + +The GitHub self-report helper retries transient failures (transport/5xx/429) so a blip on the report +call can't mark a good run failed. + +## Trust boundary (Β§10) + +- The **coder is untrusted**: Kata micro-VM (own kernel), `automountServiceAccountToken: false` (no + k8s API), no cloud IAM. Its only credentials are a Bifrost key + short-TTL GitHub token via + projected tmpfs (0400). Flow A's NetworkPolicy + node ClusterIP firewall lock egress to Bifrost + + DNS + GitHub. Because it holds no k8s creds, it **self-reports through GitHub** (the workflow polls). +- The **AWS IAM stays with the hub orchestrator** (Argo), never in the VM. +- This breaks the **lethal trifecta** (untrusted issue text + credentials + egress): credentials are + never in the issue-ingesting sandbox context, and egress is denied by default. + +## Build & deploy + +The image is built + pushed to ECR and pinned on the Flow A `SandboxTemplate` via GitOps +(`gitops/addons/charts/agent-sandbox/values.yaml` β†’ `coderTemplate.image`). ArgoCD syncs the template; +the pool-manager recycles warm pods onto the new tag. + +```bash +# amd64 (hub nodes are amd64); podman/docker both work. +podman build --platform linux/amd64 -t /dark-factory-coder: examples/dark-factory/coder +podman push /dark-factory-coder: +# β†’ bump coderTemplate.image in the agent-sandbox chart values, commit, let ArgoCD sync. +``` + +## Roadmap + +- **P1 (done):** issue β†’ PR, end-to-end, hands-off. The workflow stops at "PR open, awaiting human." +- **P2:** holdout gate β€” an isolated eval Job runs hidden BDD scenarios the coder never sees. +- **P3:** AWS Security / DevOps review agents (advisory β†’ blocking-capable). +- **P4:** auto-merge/teardown on approval + iterate loop (PR-comment driven). diff --git a/examples/dark-factory/coder-microvm/Dockerfile b/examples/dark-factory/coder-microvm/Dockerfile new file mode 100644 index 00000000..3254805a --- /dev/null +++ b/examples/dark-factory/coder-microvm/Dockerfile @@ -0,0 +1,32 @@ +# Flow D β€” lambda-coder image: the dark-factory coder wrapped for the Lambda MicroVM +# snapshot/hook runtime. +# +# FROM the arm64 dark-factory-coder (Lambda MicroVM is ARM_64-only) β€” it carries +# entrypoint.js + the toolchain (git, node, claude-code). We add ONLY the hook server +# that adapts the one-shot coder to the MicroVM lifecycle (see hook-server.js): the +# /run hook background-spawns entrypoint.js with USE_BEDROCK=1 so the coder calls +# Bedrock directly via the MicroVM execution role β€” no Bifrost / EKS-network path. +# +# The image is built by Lambda from a code-artifact ZIP (this Dockerfile + hook-server.js) +# in S3 β€” NOT pushed to ECR as a normal image. The base coder image IS pulled from ECR +# during that build (the MicrovmImage build role keeps ecr:Get*). +ARG CODER_IMAGE=940019131157.dkr.ecr.us-west-2.amazonaws.com/dark-factory-coder:v0.2.5-arm64 +FROM ${CODER_IMAGE} + +WORKDIR /app + +# The hook server (serves ready/validate/run/suspend/resume/terminate on :8080). +COPY hook-server.js /app/hook-server.js + +# Ship the UPDATED coder over the one baked into the ECR base. The base image's +# entrypoint.js predates the USE_BEDROCK branch; overlaying it here means the +# MicrovmImage build (which pulls the ECR base) gets the Bedrock-capable coder +# WITHOUT a separate ECR rebuild+push. Keep in sync with examples/dark-factory/coder/entrypoint.js. +COPY entrypoint.js /app/entrypoint.js + +# Lambda MicroVM snapshots the process started here. hooks.port on the MicrovmImage +# must match this (8080). The server stays up (long-running) so the VM isn't idle- +# suspended mid coder-run β€” the Microvm idlePolicy.maxIdleDurationSeconds is set +# longer than a coder run. +EXPOSE 8080 +CMD ["node", "/app/hook-server.js"] diff --git a/examples/dark-factory/coder-microvm/hook-server.js b/examples/dark-factory/coder-microvm/hook-server.js new file mode 100644 index 00000000..0cad32fd --- /dev/null +++ b/examples/dark-factory/coder-microvm/hook-server.js @@ -0,0 +1,118 @@ +// Flow D β€” Lambda MicroVM hook server (the lambda-coder wrapper). +// +// Lambda MicroVM is a snapshot/hook runtime: the platform builds an image by +// starting THIS process and snapshotting it once the `ready` hook says "go", then +// resumes that snapshot per session and calls the `run` hook with the session's +// runHookPayload as the request body. Hooks are HTTP endpoints we serve on :8080. +// +// The dark-factory coder (entrypoint.js) is a ONE-SHOT batch job (clone β†’ agent β†’ +// push β†’ PR, 5-15 min). It cannot run inside the 30s run hook, so /run just +// materializes the payload into the coder's file/env contract and BACKGROUND-SPAWNS +// entrypoint.js, then returns 200 immediately. The coder runs async; df-run's +// await-coder step polls GitHub for the PR (same as Kata). The VM stays alive because +// idlePolicy.maxIdleDurationSeconds > a coder run (idle = no inbound traffic). +// +// LLM: USE_BEDROCK=1 β†’ entrypoint.js calls Bedrock DIRECTLY via the MicroVM execution +// role (no Bifrost / EKS-network dependency). See docs/dark-factory/flow-d-coder-in-microvm-design.md. +// +// KEPT MINIMAL: this is the exact shape that built cleanly (v2.0). The /run handler +// stays trivial and synchronous so the build's ready-hook completes fast. Observability +// is via direct endpoint probes, not a /status route (adding one correlated with a +// hung ready-hook build on the pre-GA controller). + +const http = require("http"); +const fs = require("fs"); +const { spawn } = require("child_process"); + +const PORT = parseInt(process.env.HOOKS_PORT || "8080", 10); +const SECRETS_DIR = "/tmp/secrets"; +// Run-id of the coder invocation currently in flight (or last completed). NOT a plain +// boolean: the VM is SUSPENDED after the first PR and RESUMED for a fix round, and the +// resumed process keeps its in-memory state β€” a one-shot `coderStarted=true` guard, +// frozen in the snapshot, made the resumed VM ignore the fix round's /run entirely (the +// coder never re-ran; the fix round reported "done" on the old sha). Instead we key on a +// per-invocation run-id (issue + iterate-note hash): a /run whose id differs from the +// one in flight starts a fresh coder (this is a new round after a resume); a /run that +// repeats the current id is a duplicate webhook and is ignored. +let currentRunId = null; +let coderRunning = false; + +function runIdOf(d) { + const note = d.iterateNoteB64 || d.iterateNote || ""; + // Cheap stable hash of issue+note so a fix round (new note) => new id => re-run. + let h = 0; const s = `${d.issueNumber || ""}:${note}`; + for (let i = 0; i < s.length; i++) { h = ((h << 5) - h + s.charCodeAt(i)) | 0; } + return `${d.issueNumber || "?"}#${(h >>> 0).toString(36)}`; +} + +function startCoder(payload) { + let d = {}; + try { d = JSON.parse(payload || "{}"); } catch (e) { console.log("[hook-server] payload not JSON:", e.message); } + const rid = runIdOf(d); + if (rid === currentRunId) { console.log(`[hook-server] /run duplicate for ${rid} β€” ignoring`); return; } + if (coderRunning) { console.log(`[hook-server] /run for ${rid} but ${currentRunId} still running β€” ignoring`); return; } + const isRerun = currentRunId !== null; // a prior run existed => this is a post-resume fix round + currentRunId = rid; + coderRunning = true; + // Truncate the coder log on each new run. Otherwise the previous round's + // "done β€” PR opened on " line lingers and the bridge's /logs grep matches it + // instantly, suspending the VM before the fix-round coder has done anything. + try { fs.writeFileSync("/tmp/coder.log", ""); } catch {} + console.log(`[hook-server] /run accepted run-id=${rid}${isRerun ? " (post-resume re-run)" : ""}`); + fs.mkdirSync(SECRETS_DIR, { recursive: true, mode: 0o700 }); + if (d.ghToken) fs.writeFileSync(`${SECRETS_DIR}/gh-token`, d.ghToken, { mode: 0o400 }); + const env = { + ...process.env, + USE_BEDROCK: "1", + // MicroVM rootfs is read-only + there's no /workspace volume mount (unlike Kata, + // where the operator mounts a writable workspace). entrypoint.js mkdir's + // ${WORKSPACE}/artifacts and clones there, so point it at the writable tmpfs β€” + // else it crashes EACCES on /workspace/artifacts before doing any work. + WORKSPACE: "/tmp/workspace", + GH_TOKEN_PATH: `${SECRETS_DIR}/gh-token`, + AWS_REGION: d.region || process.env.AWS_REGION || "us-west-2", + DF_ISSUE_NUMBER: d.issueNumber ? String(d.issueNumber) : "", + DF_REPO: d.repo || "", + DF_BRANCH: d.branch || (d.issueNumber ? `df/issue-${d.issueNumber}` : ""), + DF_BASE_BRANCH: d.baseBranch || "main", + DF_ISSUE_TITLE: d.issueTitle || "", + }; + // Fix round (df-iterate): the bridge folds the human's change request into the + // payload as iterateNoteB64. Without this, a Lambda fix round re-runs the coder + // with NO instructions β†’ it sees the PR already open and reports "done" on the + // old sha with zero changes (the Kata path injects DF_ITERATE_NOTE_B64 as claim + // env; the MicroVM has no claim env, so it must ride in on the runHookPayload). + if (d.iterateNoteB64) env.DF_ITERATE_NOTE_B64 = d.iterateNoteB64; + if (d.iterateNote) env.DF_ITERATE_NOTE = d.iterateNote; + if (d.model) env.CODER_MODEL = d.model; + console.log(`[hook-server] /run β†’ spawning coder for issue #${env.DF_ISSUE_NUMBER} repo=${env.DF_REPO}`); + // Capture the coder's stdout+stderr to /tmp/coder.log so /logs can return it β€” + // runtime CloudWatch routing doesn't work on this runtime, and there's no shell, + // so this file (read over the HTTP token) is the ONLY way to see what the coder did. + const logFd = fs.openSync("/tmp/coder.log", "a"); + const child = spawn("node", ["/app/entrypoint.js"], { env, stdio: ["ignore", logFd, logFd], detached: true }); + child.unref(); + child.on("error", (e) => { coderRunning = false; try { fs.appendFileSync("/tmp/coder.log", "SPAWN-ERROR: " + e.message + "\n"); } catch {} }); + // Clear the in-flight flag when the coder exits so a resumed VM's next /run (fix round) + // is accepted. `unref`'d + detached, but we still get 'exit' while this process lives. + child.on("exit", (code) => { coderRunning = false; console.log(`[hook-server] coder run-id=${currentRunId} exited code=${code}`); }); +} + +const server = http.createServer((req, res) => { + let body = ""; + req.on("data", (c) => { body += c; }); + req.on("end", () => { + const ok = (o) => { res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify(o || { status: "ok" })); }; + switch (req.url) { + case "/ready": return ok({ status: "ready" }); + case "/validate": return ok({ status: "valid" }); + case "/run": startCoder(body); return ok({ status: "started" }); + case "/logs": { let l=""; try { l=fs.readFileSync("/tmp/coder.log","utf8"); } catch {} return ok({ status:"ok", runId: currentRunId, running: coderRunning, log: l.slice(-6000) }); } + case "/suspend": return ok({ status: "suspended" }); + case "/resume": return ok({ status: "resumed" }); + case "/terminate": return ok({ status: "terminated" }); + default: return ok({ status: "ok", path: req.url }); + } + }); +}); +server.listen(PORT, () => console.log(`[hook-server] listening on :${PORT} (lambda-coder, Bedrock-direct)`)); diff --git a/examples/dark-factory/coder/Dockerfile b/examples/dark-factory/coder/Dockerfile new file mode 100644 index 00000000..e079c0f1 --- /dev/null +++ b/examples/dark-factory/coder/Dockerfile @@ -0,0 +1,56 @@ +# Dark Factory coder image β€” runs INSIDE the Kata micro-VM sandbox. +# +# Bundles the in-VM agent + the toolchains a coder run needs (git, node, the +# agentic CLIs). This is untrusted-code territory: the image carries no +# credentials β€” secrets are injected at runtime via projected tmpfs (0400) and +# model access is only through Bifrost. Keep it lean and pinned. +FROM public.ecr.aws/docker/library/node:20-alpine + +# Toolchains for build/test across common stacks + git for checkout/push. Also +# aws-cli + zip + curl because the SAME image is reused by the hub-side steps +# (security-agent diff staging, bootstrap) which need those. bash for the scripts. +RUN apk add --no-cache git bash python3 py3-pip go zip curl aws-cli + +# ── Agentic engines β€” BOTH first-class + selectable via CODER_ENGINE ───────── +# claude β†’ Claude Code CLI (default) | kiro β†’ Kiro CLI (headless) +# entrypoint.js branches on the engine; the image carries both so a run can pick +# either without a rebuild. +RUN npm install -g @anthropic-ai/claude-code + +# Kiro CLI. Internally distributed via Builder Toolbox (`toolbox install +# kiro-cli`), which isn't available in this base image, so install from the +# platform's pinned artifact URL at build time. Best-effort: the image still +# builds (Claude as default) if the URL is unset/unreachable β€” CODER_ENGINE=kiro +# then logs a clear error at runtime rather than silently misbehaving. +ARG KIRO_CLI_URL="" +RUN if [ -n "$KIRO_CLI_URL" ]; then \ + echo "installing kiro-cli from $KIRO_CLI_URL" && \ + curl -fsSL "$KIRO_CLI_URL" -o /usr/local/bin/kiro && chmod +x /usr/local/bin/kiro ; \ + else \ + echo "WARN: KIRO_CLI_URL not set β€” CODER_ENGINE=kiro unavailable until provided at build" ; \ + fi + +# ── AWS DevOps Agent β€” release-readiness review plugin (NOT baked here) ────── +# The DevOps Agent review is invoked via the Claude Code / Kiro plugin (there is +# NO headless code-review API). That plugin installs via the AIM CLI and connects +# to an Agent Space needing a ONE-TIME console setup (Agent Space + repo connect / +# access key) β€” a connection that can't be baked into an image or performed from +# the credential-less, network-locked VM (no Midway). entrypoint.js therefore +# reports the DevOps review as "not-connected" (never a fake pass) until the +# platform wires the MCP/A2A access-key path from a hub-side step. See docs Β§6.2. + +WORKDIR /app +# entrypoint.js β€” env-driven Flow B coder (auto-runs on VM start, self-reports +# via GitHub commit status; the df-run Argo workflow polls GitHub for the result). +COPY entrypoint.js ./ + +# Non-root, matches the SandboxTemplate securityContext (runAsUser 1000). +USER 1000 + +ENV WORKSPACE=/workspace + +# Baked into the Flow A SandboxTemplate as the coder image. On VM start it reads +# the DF_* env injected by the SandboxClaim, implements + tests, pushes the +# branch, runs the DevOps Agent review (if connected), opens the PR, and sets the +# dark-factory/implementation commit status. +CMD ["node", "entrypoint.js"] diff --git a/examples/dark-factory/coder/entrypoint.js b/examples/dark-factory/coder/entrypoint.js new file mode 100644 index 00000000..5b43fa31 --- /dev/null +++ b/examples/dark-factory/coder/entrypoint.js @@ -0,0 +1,549 @@ +// entrypoint.js β€” the in-VM coder for Flow B P1 (runs on Kata micro-VM start). +// +// This is the UNTRUSTED side of the trust boundary. It holds NO cloud creds and +// NO Kubernetes API access (no SA token). Its only credentials are a Bifrost key +// and a short-TTL GitHub token, both read from projected tmpfs (mode 0400). +// Because it can't talk to the k8s API, it SELF-REPORTS through GitHub β€” the +// df-run workflow polls GitHub for the PR + the dark-factory/implementation +// commit status this script sets. +// +// Driven entirely by env injected via SandboxClaim.spec.env (contract verified +// against the live operator, envVarsInjectionPolicy=Allowed): +// DF_REPO owner/name of the target repo +// DF_ISSUE_NUMBER the GitHub issue number (the spec) +// DF_BRANCH df/issue- +// DF_BASE_BRANCH base to branch from (default main) +// DF_ISSUE_TITLE issue title (for the PR title) +// CODER_PROFILE claude-code | kiro +// BIFROST_URL LLM gateway (from the SandboxTemplate) +// +// Flow: fetch issue β†’ SPEC.md β†’ checkout df/issue-N β†’ coder implements β†’ +// build+test β†’ push β†’ open PR β†’ set commit status success/failure. +const fs = require("fs"); +const http = require("http"); +const https = require("https"); +const { URL } = require("url"); +const { execFileSync, spawn } = require("child_process"); + +const WORKSPACE = process.env.WORKSPACE || "/workspace"; +const BIFROST_URL = process.env.BIFROST_URL || "http://bifrost.bifrost.svc.cluster.local:8080"; +const REPO = process.env.DF_REPO; +const ISSUE = process.env.DF_ISSUE_NUMBER; +const BRANCH = process.env.DF_BRANCH || `df/issue-${ISSUE}`; +const BASE = process.env.DF_BASE_BRANCH || "main"; +const TITLE = process.env.DF_ISSUE_TITLE || `Dark Factory: issue #${ISSUE}`; +// Agentic engine the VM runs. Both engines are first-class and selectable: +// claude β†’ claude -p (Claude Code, default) | kiro β†’ kiro run --headless +// Accept the new CODER_ENGINE and the legacy CODER_PROFILE (claude-code|kiro); +// normalize either to a bare engine id. +const ENGINE = (() => { + const raw = (process.env.CODER_ENGINE || process.env.CODER_PROFILE || "claude").toLowerCase(); + return raw.startsWith("kiro") ? "kiro" : "claude"; +})(); +const PROFILE = ENGINE; // back-compat alias used in a few log lines +// AWS DevOps Agent β€” release-readiness review via the coding-agent plugin, run +// BEFORE the PR opens (docs Β§6.2). Modes: "claude-plugin" (Claude Code DevOps +// Agent plugin) | "off". On a clear verdict the coder applies DF_DEVOPS_CLEAR_LABEL +// so the hub's Security Agent step runs next (DevOps-first ordering). +const DEVOPS_AGENT_MODE = (process.env.DF_DEVOPS_AGENT_MODE || "off").toLowerCase(); +const DEVOPS_CLEAR_LABEL = process.env.DF_DEVOPS_CLEAR_LABEL || "needs-security-review"; +const DEVOPS_CLEAR_VERDICTS = (process.env.DF_DEVOPS_CLEAR_VERDICTS || "Safe to Release,Proceed with Caution") + .split(",").map((s) => s.trim().toLowerCase()).filter(Boolean); + +const GH_TOKEN_PATH = process.env.GH_TOKEN_PATH || "/etc/secrets/gh-token"; +const BIFROST_KEY_PATH = process.env.BIFROST_KEY_PATH || "/etc/secrets/bifrost-api-key"; +// LLM observability (traces/cost/tokens) is provided by BIFROST's telemetry, which +// already exports full per-call traces to Langfuse tagged user-agent=dark-factory-coder +// (prompt, response, model, tokens). We deliberately do NOT post a redundant +// coder-side trace here β€” Bifrost's is richer. See docs/dark-factory Β§7a. + +function readSecret(p) { + try { return fs.readFileSync(p, "utf8").trim(); } catch { return null; } +} +function sh(cmd, args, opts = {}) { + return execFileSync(cmd, args, { + cwd: opts.cwd || WORKSPACE, encoding: "utf8", + stdio: ["ignore", "pipe", "pipe"], env: opts.env || process.env, + maxBuffer: 64 * 1024 * 1024, + }); +} + +const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); + +// Minimal GitHub REST helper (self-report bus β€” no k8s API available). +// GitHub is the completion bus the df-run workflow polls, so a transient network +// blip on a report call (observed: "socket hang up" on the final success POST) +// must NOT be allowed to mark a good run as failed. Retry transient transport +// errors (ECONNRESET / socket hang up) and 5xx with a short backoff. +function ghOnce(method, path, body) { + const token = readSecret(GH_TOKEN_PATH); + return new Promise((resolve, reject) => { + const data = body ? JSON.stringify(body) : null; + const req = https.request( + { host: "api.github.com", method, path, + headers: { + "User-Agent": "dark-factory-coder", Authorization: `Bearer ${token}`, + Accept: "application/vnd.github+json", "X-GitHub-Api-Version": "2022-11-28", + ...(data ? { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(data) } : {}), + } }, + (res) => { + let buf = ""; res.on("data", (c) => (buf += c)); + res.on("end", () => { + if (res.statusCode >= 200 && res.statusCode < 300) resolve(buf ? JSON.parse(buf) : {}); + else { const e = new Error(`GitHub ${method} ${path} β†’ ${res.statusCode}: ${buf}`); e.statusCode = res.statusCode; reject(e); } + }); + }); + req.on("error", reject); + if (data) req.write(data); + req.end(); + }); +} + +async function gh(method, path, body) { + let lastErr; + for (let attempt = 1; attempt <= 4; attempt++) { + try { return await ghOnce(method, path, body); } + catch (e) { + // Retry only transient failures β€” never a 4xx (bad request / already exists). + const transient = e.statusCode === undefined || e.statusCode >= 500 || e.statusCode === 429; + if (!transient || attempt === 4) throw e; + lastErr = e; + await sleep(500 * attempt); + } + } + throw lastErr; +} + +// Upsert ONE marker-based PR comment (edit in place, no spam) β€” mirrors the +// hub-side comment.js so the coder's own steps (coding complete, local testing) +// leave a visible mark on the PR just like the review agents do. +async function postStickyComment(prNumber, marker, bodyMd) { + if (!prNumber) return; + const full = `\n${bodyMd.trim()}`; + try { + let existing = null; + for (let page = 1; page <= 5 && !existing; page++) { + const cs = await gh("GET", `/repos/${REPO}/issues/${prNumber}/comments?per_page=100&page=${page}`); + if (!Array.isArray(cs) || !cs.length) break; + existing = cs.find((c) => (c.body || "").includes(``)); + } + if (existing) await gh("PATCH", `/repos/${REPO}/issues/comments/${existing.id}`, { body: full }); + else await gh("POST", `/repos/${REPO}/issues/${prNumber}/comments`, { body: full }); + console.log(`[coder] posted PR comment ${marker}`); + } catch (e) { console.error(`[coder] comment ${marker} non-fatal: ${e.message}`); } +} + +async function fetchIssueSpec() { + const issue = await gh("GET", `/repos/${REPO}/issues/${ISSUE}`); + let spec = `# ${issue.title}\n\n${issue.body || ""}\n`; + // No profile/scaffold-hint injection: the issue text states the stack ("a Spring + // Boot service", "Terraform for an S3 bucket"), the coder generates idiomatic + // code from that, and build/test is discovered from the resulting marker files. + // Iterate mode (df-iterate): a human left a change request on the PR. Append it + // so the coder revises the EXISTING branch to address the feedback, rather than + // re-implementing from scratch. DF_ITERATE_NOTE is injected by the df-iterate + // claim; absent on a first (df-run) pass. + // Prefer the base64 form: the revision note (esp. auto-fed agent findings) is + // arbitrary markdown with newlines/quotes/braces that CANNOT be injected raw into + // the SandboxClaim env YAML (it broke the manifest). status.js/df-iterate base64 + // it into DF_ITERATE_NOTE_B64; decode here. Fall back to plain DF_ITERATE_NOTE. + const note = iterateNote(); + if (note && note.trim()) { + spec += `\n---\n\n## Revision requested (address this feedback on the existing branch)\n\n${note}\n`; + } + return spec; +} + +// Resolve the revision note from DF_ITERATE_NOTE_B64 (preferred, safe for arbitrary +// text) or the legacy plain DF_ITERATE_NOTE. +function iterateNote() { + const b64 = process.env.DF_ITERATE_NOTE_B64; + if (b64 && b64.trim()) { try { return Buffer.from(b64.trim(), "base64").toString("utf8"); } catch (_) { /* fall through */ } } + return process.env.DF_ITERATE_NOTE || ""; +} + +function checkout() { + const token = readSecret(GH_TOKEN_PATH); + const url = `https://x-access-token:${token}@github.com/${REPO}.git`; + const dir = `${WORKSPACE}/repo`; + const iterating = !!(iterateNote() && iterateNote().trim()); + if (!fs.existsSync(dir)) { + // On iterate, start from the existing coder branch (build on prior work); + // otherwise branch fresh from BASE. + if (iterating) { + try { sh("git", ["clone", "--depth", "1", "--branch", BRANCH, url, dir]); } + catch { sh("git", ["clone", "--depth", "1", "--branch", BASE, url, dir]); } + } else { + sh("git", ["clone", "--depth", "1", "--branch", BASE, url, dir]); + } + } + sh("git", ["checkout", "-B", BRANCH], { cwd: dir }); + sh("git", ["config", "user.email", "dark-factory@noreply"], { cwd: dir }); + sh("git", ["config", "user.name", "Dark Factory"], { cwd: dir }); + return dir; +} + +// Bifrost does User-Agent-prefix routing: any request whose UA starts with +// "claude-cli" is run through a Claude-Code-specific request transform that is +// broken on this build and returns `400 Unexpected field type` β€” REGARDLESS of +// the body (the identical body + any other UA returns 200; verified by header +// binary-search against the live gateway). We can't patch Bifrost from inside +// the untrusted VM, so we front it with a tiny localhost shim that rewrites the +// UA to a generic value and transparently forwards everything else β€” including +// SSE streams (Claude Code sends stream:true). Claude Code points at this shim +// via ANTHROPIC_BASE_URL; the shim proxies to the real Bifrost /anthropic route. +// +// The shim MUST run in its OWN process: we launch the coder CLI with the +// synchronous execFileSync (so we can await its exit), which blocks this Node +// event loop for the whole run β€” an in-process http.Server would never accept a +// connection (observed: ConnectionRefused). So we write the shim to a temp file +// and spawn `node` on it in the background, then wait for its port to open. +const SHIM_PORT = 8791; +function startBifrostUaShim(upstreamBase) { + const shimSrc = ` +const http=require("http"),https=require("https"),{URL}=require("url"); +const up=new URL(${JSON.stringify(upstreamBase)}); +const agent=up.protocol==="https:"?https:http; +http.createServer((cReq,cRes)=>{ + const headers={...cReq.headers,host:up.host,"user-agent":"dark-factory-coder"}; + const pReq=agent.request({protocol:up.protocol,hostname:up.hostname,port:up.port||(up.protocol==="https:"?443:80),method:cReq.method,path:up.pathname.replace(/\\/+$/,"")+cReq.url,headers}, + pRes=>{cRes.writeHead(pRes.statusCode,pRes.headers);pRes.pipe(cRes);}); + pReq.on("error",e=>{cRes.writeHead(502);cRes.end(String(e.message));}); + cReq.pipe(pReq); +}).listen(${SHIM_PORT},"127.0.0.1",()=>console.log("[ua-shim] listening on ${SHIM_PORT} -> "+up.href)); +`; + const shimPath = "/tmp/ua-shim.js"; + fs.writeFileSync(shimPath, shimSrc); + const child = spawn("node", [shimPath], { stdio: "inherit", detached: false }); + child.unref(); + // Block until the shim's port is accepting (execFileSync below can't yield). + const deadline = Date.now() + 5000; + while (Date.now() < deadline) { + try { execFileSync("node", ["-e", `require("net").connect(${SHIM_PORT},"127.0.0.1").on("connect",()=>process.exit(0)).on("error",()=>process.exit(1))`], { stdio: "ignore" }); break; } + catch { execFileSync("sleep", ["0.2"]); } + } + return `http://127.0.0.1:${SHIM_PORT}`; +} + +function runCoder(repoDir) { + // Two LLM transports, selected by USE_BEDROCK: + // - Kata (Flow B, default): Bifrost gateway. The Kata VM is credential-less + + // in-cluster, so it reaches models through Bifrost's /anthropic route (which + // also gives centralized Langfuse observability). CLAUDE_CODE_USE_BEDROCK is + // deliberately UNSET here (it would make the CLI use the Bedrock SDK directly + // and ignore ANTHROPIC_BASE_URL). + // - Lambda MicroVM (Flow D): USE_BEDROCK=1. A MicroVM runs OUTSIDE the cluster + // network and can't reach Bifrost's ClusterIP; forcing it back in-cluster + // needed a VPC connector + internal NLB. Instead the MicroVM's EXECUTION ROLE + // grants bedrock:InvokeModel, so Claude Code calls Bedrock directly over public + // egress β€” no EKS network dependency. Trade-off: these calls bypass Bifrost's + // Langfuse telemetry (documented in flow-d-coder-in-microvm-design.md). + const useBedrock = /^(1|true|yes)$/i.test(process.env.USE_BEDROCK || ""); + const baseEnv = { + ...process.env, + // The sandbox runs with readOnlyRootFilesystem, so $HOME (/home/node) is NOT + // writable. Claude Code writes its config, session state, and β€” critically β€” + // per-invocation SHELL SNAPSHOT files that its Bash tool sources before every + // command into ~/.claude. If that dir can't be created, every Bash call (npm + // install/test, git) fails and the agent loops retrying forever (observed: + // a trivial change ran >15min with no commit). Point HOME + config dir at the + // writable /tmp tmpfs so the CLI can persist and run shell commands. + HOME: "/tmp/coder-home", + CLAUDE_CONFIG_DIR: "/tmp/coder-home/.claude", + XDG_CONFIG_HOME: "/tmp/coder-home/.config", + XDG_CACHE_HOME: "/tmp/coder-home/.cache", + // Non-interactive: never open a browser / prompt for login in headless mode. + CI: "1", + }; + let env; + if (useBedrock) { + // Bedrock-direct: creds come from the MicroVM execution role (Pod Identity / + // instance creds); the CLI uses the Bedrock SDK. Model must be a real Bedrock + // model ID (NOT a Bifrost alias). AWS_REGION comes from the runHookPayload/env. + env = { + ...baseEnv, + CLAUDE_CODE_USE_BEDROCK: "1", + AWS_REGION: process.env.AWS_REGION || process.env.CODER_REGION || "us-west-2", + ANTHROPIC_MODEL: process.env.CODER_MODEL || "us.anthropic.claude-sonnet-4-5-20250929-v1:0", + ANTHROPIC_SMALL_FAST_MODEL: process.env.CODER_SMALL_MODEL || process.env.CODER_MODEL || "us.anthropic.claude-sonnet-4-5-20250929-v1:0", + }; + fs.mkdirSync("/tmp/coder-home/.claude", { recursive: true }); + console.log(`[coder] LLM: Bedrock-direct region=${env.AWS_REGION} model=${env.ANTHROPIC_MODEL}`); + } else { + // Bifrost is an Anthropic-compatible gateway. Route through the localhost + // UA-shim so Bifrost doesn't apply its broken claude-cli request transform. + // Bifrost auth is optional; send a placeholder so the CLI doesn't prompt. + const key = readSecret(BIFROST_KEY_PATH) || "bifrost"; + const base = startBifrostUaShim(`${BIFROST_URL.replace(/\/+$/, "")}/anthropic`); + env = { + ...baseEnv, + ANTHROPIC_BASE_URL: base, + ANTHROPIC_API_KEY: key, + // Bifrost maps model ALIASES β†’ Bedrock model IDs. Claude Code's default + // model name (e.g. claude-sonnet-4) isn't a Bifrost alias and returns + // "provided model identifier is invalid" (400). Use the platform's Bifrost + // alias (verified: 'claude-sonnet' β†’ us.anthropic.claude-sonnet-4-5). + ANTHROPIC_MODEL: process.env.CODER_MODEL || "claude-sonnet", + ANTHROPIC_SMALL_FAST_MODEL: process.env.CODER_MODEL || "claude-sonnet", + }; + fs.mkdirSync("/tmp/coder-home/.claude", { recursive: true }); + delete env.CLAUDE_CODE_USE_BEDROCK; + console.log(`[coder] LLM: Bifrost base=${base} model=${env.ANTHROPIC_MODEL}`); + } + // Inherit stdio so the coder CLI's own output + errors stream into the pod + // logs (kubectl logs), instead of being swallowed by execFileSync's exception. + const opts = { cwd: repoDir, env, stdio: "inherit", maxBuffer: 64 * 1024 * 1024 }; + const prompt = `Implement the change described in ${WORKSPACE}/SPEC.md. Build and run unit tests until green. Commit your work.`; + if (ENGINE === "kiro") { + // Kiro CLI headless β€” the coder image carries the `kiro` binary; it reads the + // same Bifrost/Bedrock env above. --headless drives it non-interactively. + console.log("[coder] engine=kiro (kiro run --headless)"); + return execFileSync("kiro", ["run", "--headless", "--spec", `${WORKSPACE}/SPEC.md`], opts); + } + console.log("[coder] engine=claude (claude -p)"); + return execFileSync( + "claude", + ["-p", prompt, "--permission-mode", "bypassPermissions", "--verbose"], + opts, + ); +} + +// AWS DevOps Agent β€” release-readiness code review, run in-VM via the coding-agent +// plugin BEFORE the PR opens (docs Β§6.2). Returns { verdict, cleared, summary }. +// This is the REAL managed agent, invoked through the engine's plugin β€” NOT a +// linter/LLM stand-in. Because the plugin needs a one-time console connect +// (Agent Space + repo), when it isn't wired the review is reported as +// "not-connected" and cleared=false (NEVER a fake pass) so the hub sticky-status +// shows DevOps as not-run and Security is correctly skipped. +function runDevopsReview(repoDir) { + if (DEVOPS_AGENT_MODE === "off") return { verdict: "skipped", cleared: false, summary: "DevOps Agent disabled" }; + const key = readSecret(BIFROST_KEY_PATH) || "bifrost"; + const base = startBifrostUaShim(`${BIFROST_URL.replace(/\/+$/, "")}/anthropic`); + const env = { + ...process.env, + ANTHROPIC_BASE_URL: base, ANTHROPIC_API_KEY: key, + ANTHROPIC_MODEL: process.env.CODER_MODEL || "claude-sonnet", + ANTHROPIC_SMALL_FAST_MODEL: process.env.CODER_MODEL || "claude-sonnet", + HOME: "/tmp/coder-home", CLAUDE_CONFIG_DIR: "/tmp/coder-home/.claude", + XDG_CONFIG_HOME: "/tmp/coder-home/.config", XDG_CACHE_HOME: "/tmp/coder-home/.cache", CI: "1", + }; + delete env.CLAUDE_CODE_USE_BEDROCK; + const opts = { cwd: repoDir, env, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], maxBuffer: 64 * 1024 * 1024, timeout: 15 * 60 * 1000 }; + // Ask the coding agent to invoke the DevOps Agent release-readiness review on + // the working changes and print the verdict on the last line as DF_DEVOPS_VERDICT=. + const prompt = + "Use the AWS DevOps Agent plugin to run a release readiness code review on the current uncommitted+committed changes in this repo " + + "(cross-repository dependency risks, internal standards compliance, access-control correctness; run static analysis). " + + "Summarize the findings, then print EXACTLY one final line: DF_DEVOPS_VERDICT=."; + try { + let out = ""; + if (ENGINE === "kiro") { + out = execFileSync("kiro", ["run", "--headless", "--prompt", prompt], opts) || ""; + } else { + out = execFileSync("claude", ["-p", prompt, "--permission-mode", "bypassPermissions"], opts) || ""; + } + const m = String(out).match(/DF_DEVOPS_VERDICT=\s*(.+)\s*$/im); + const verdict = m ? m[1].trim() : "unknown"; + const cleared = DEVOPS_CLEAR_VERDICTS.includes(verdict.toLowerCase()); + console.log(`[coder] AWS DevOps Agent verdict: ${verdict} (cleared=${cleared})`); + return { verdict, cleared, summary: `AWS DevOps Agent: ${verdict}` }; + } catch (e) { + // Plugin not connected / not installed β†’ report honestly, do NOT fake-pass. + const msg = (e.stderr || e.stdout || e.message || "").toString(); + const notConnected = /plugin|not installed|not connected|unknown command|No such|command not found/i.test(msg); + console.error(`[coder] DevOps Agent review unavailable: ${msg.slice(0, 200)}`); + return { verdict: notConnected ? "not-connected" : "error", cleared: false, summary: "AWS DevOps Agent not connected (one-time console setup required)" }; + } +} + +function buildAndTest(repoDir) { + // Build/test is DISCOVERED from the repo's own marker files β€” no per-language + // platform config. Devs control build/test by their repo layout; the toolchains + // live in the coder IMAGE (see coder/Dockerfile), not here. A repo Makefile with + // a `test` target is the explicit dev-controlled override, checked first. + // + // The project may not be at the repo ROOT (e.g. an infra/ + app/ split), so we + // probe a few conventional subdirs and run the toolchain in the first that has a + // recognizable marker. Root wins if present; otherwise app/src/backend/server. + const CANDIDATES = [".", "app", "src", "backend", "server"]; + const hasIn = (d, f) => fs.existsSync(`${repoDir}/${d}/${f}`.replace(/\/\.\//, "/")); + const dirOf = (f) => CANDIDATES.find((d) => hasIn(d, f)); + const makeTestDir = () => { + for (const d of CANDIDATES) { + try { if (hasIn(d, "Makefile") && /^test:/m.test(fs.readFileSync(`${repoDir}/${d}/Makefile`, "utf8"))) return d; } catch { /* skip */ } + } + return null; + }; + const wd = (d) => `${repoDir}/${d}`.replace(/\/\.$/, ""); + try { + let d; + if ((d = makeTestDir()) != null) { console.log(`[coder] Makefile test target in ${d}/`); sh("make", ["test"], { cwd: wd(d) }); } + else if ((d = dirOf("package.json")) != null) { console.log(`[coder] node project in ${d}/`); sh("npm", ["install", "--no-audit", "--no-fund"], { cwd: wd(d) }); sh("npm", ["test"], { cwd: wd(d) }); } + else if ((d = dirOf("go.mod")) != null) sh("go", ["test", "./..."], { cwd: wd(d) }); + else if ((d = ["pyproject.toml", "setup.py", "requirements.txt"].map(dirOf).find(Boolean)) != null) sh("python", ["-m", "pytest", "-q"], { cwd: wd(d) }); + else if ((d = dirOf("Cargo.toml")) != null) sh("cargo", ["test"], { cwd: wd(d) }); + else if ((d = dirOf("pom.xml")) != null) sh("mvn", ["-q", "test"], { cwd: wd(d) }); + else if ((d = ["build.gradle", "build.gradle.kts"].map(dirOf).find(Boolean)) != null) sh("./gradlew", ["test"], { cwd: wd(d) }); + else return { green: true, summary: "no recognized test suite β€” skipped (e.g. infra/config change)" }; + return { green: true, summary: "tests passed" }; + } catch (e) { + return { green: false, summary: (e.stdout || e.stderr || e.message || "").toString().slice(-400) }; + } +} + +async function main() { + for (const [k, v] of Object.entries({ DF_REPO: REPO, DF_ISSUE_NUMBER: ISSUE })) { + if (!v) { console.error(`[coder] missing required env ${k}`); process.exit(2); } + } + fs.mkdirSync(`${WORKSPACE}/artifacts`, { recursive: true }); + console.log(`[coder] issue #${ISSUE} of ${REPO} β†’ branch ${BRANCH} (profile=${PROFILE})`); + + const spec = await fetchIssueSpec(); + fs.writeFileSync(`${WORKSPACE}/SPEC.md`, spec); + const repoDir = checkout(); + + let headSha = ""; + try { + runCoder(repoDir); + const test = buildAndTest(repoDir); + if (!test.green) throw new Error(`tests not green: ${test.summary}`); + + // Nothing-to-do guard: if the coder produced no commits ahead of the base, + // there's no diff β€” GitHub rejects the PR with 422 "No commits between…". + // This happens when the requested change already exists on base. Report the + // implementation status as success (the spec is satisfied) and exit cleanly + // instead of crash-looping. The df-run poller sees success on the base head. + let ahead = "1"; // default to "has changes" if we can't determine (fail open to PR) + try { + execFileSync("git", ["fetch", "--depth", "1", "origin", BASE], { cwd: repoDir, stdio: "ignore" }); + ahead = sh("git", ["rev-list", "--count", `origin/${BASE}..HEAD`], { cwd: repoDir }).trim(); + } catch { /* base unfetchable β€” proceed to PR, GitHub will validate */ } + if (ahead === "0") { + console.log(`[coder] no changes ahead of ${BASE} β€” the spec appears already satisfied; skipping PR`); + headSha = sh("git", ["rev-parse", "HEAD"], { cwd: repoDir }).trim(); + await gh("POST", `/repos/${REPO}/statuses/${headSha}`, { + state: "success", context: "dark-factory/implementation", + description: "no changes needed β€” spec already satisfied on base", + }); + process.exit(0); + } + // df/issue-N is bot-owned and single-writer (the df-run workflow holds a + // per-issue mutex), so a plain --force is safe and correct. --force-with-lease + // can't be used: the depth-1 clone never fetched origin/df/issue-N, so its + // lease check fails ("stale info") whenever the branch already exists from a + // prior run. + // AWS DevOps Agent release-readiness review runs FIRST (before the PR opens), + // per the DevOps-first ordering. Its verdict drives the handoff label so the + // hub's Security Agent step runs only after DevOps clears. + const devops = runDevopsReview(repoDir); + + sh("git", ["push", "-u", "origin", BRANCH, "--force"], { cwd: repoDir }); + headSha = sh("git", ["rev-parse", "HEAD"], { cwd: repoDir }).trim(); + + // Open the PR (idempotent: ignore "already exists"). The coder opens the PR + // BEFORE the hub-side verify steps run, so it can only mark them "running". + // The workflow's sticky-status step rewrites the block below (between the + // dark-factory:status marker) with the real verdicts once holdout/security/ + // devops finish β€” the "one live sticky status" (README Β§7). Keep this block's + // shape in sync with that step. + // DevOps line reflects how the review is driven: + // - mode "off" (default = check-gate): the AWS DevOps Agent GitHub App reviews + // the PR and posts its own check β€” so show "pending (GitHub App)", NOT skipped. + // - mode "claude-plugin" (label-gate): the coder drove it β†’ show the verdict + // (or "not connected" honestly if the plugin isn't wired). + // Either way the hub sticky-status step overwrites this block with the live + // verdict (from the check-run / statuses) once verification completes. + // NEUTRAL placeholder only. The coder opens the PR BEFORE the hub verify steps + // + the AWS agents run, so it must NOT print per-step states (they'd be stale + // guesses that confused readers: "Holdout: running…" long after it finished, + // "Security: runs after DevOps…" while the bot was already done). The pipeline's + // ONE consolidated review (status.js β†’ dark-factory:verdict-review) is the + // authoritative live status. We keep the dark-factory:status marker so status.js + // can still replace this block with the final verdict summary at the end. + const prBody = [ + `Closes #${ISSUE}.`, + "", + "", + "### 🏭 Dark Factory β€” verification", + `- βœ… **Build + unit tests (in-VM):** ${test.summary}`, + "", + "⏳ **Verification in progress.** Hub gates (holdout, deploy-test) and the real", + "AWS DevOps + Security agents are reviewing this PR. Results are posted as a", + "single **consolidated verdict review** on this PR when they finish β€” that", + "review (not this body) is the source of truth for merge readiness.", + "", + "_Autonomously implemented in a hardware-isolated micro-VM. DevOps + Security reviews are the real AWS Frontier Agents._", + ].join("\n"); + let prNumber = ""; + try { + const created = await gh("POST", `/repos/${REPO}/pulls`, { + title: `Dark Factory: ${TITLE} (#${ISSUE})`, head: BRANCH, base: BASE, + body: prBody, + maintainer_can_modify: true, + }); + prNumber = created && created.number ? String(created.number) : ""; + } catch (e) { if (!/already exists|A pull request already/i.test(e.message)) throw e; } + // If the PR already existed, look up its number (labels attach to the PR). + if (!prNumber) { + try { + const list = await gh("GET", `/repos/${REPO}/pulls?head=${REPO.split("/")[0]}:${BRANCH}&state=open`); + if (Array.isArray(list) && list[0]) prNumber = String(list[0].number); + } catch (_) {} + } + + // Every step reports on the PR as a comment. The coder posts two: (1) coding + // complete β€” what it changed; (2) local testing β€” the in-VM build+test result. + // (The review agents post their own comments; the sticky PR-body block is the + // consolidated board.) Files changed = git name-status vs base. + let changed = ""; + try { changed = sh("git", ["diff", "--name-status", `origin/${BASE}...HEAD`], { cwd: repoDir }).trim(); } catch { try { changed = sh("git", ["show", "--name-status", "--oneline", "-1", "HEAD"], { cwd: repoDir }).trim(); } catch {} } + const filesBlock = changed ? "```\n" + changed.slice(0, 1500) + "\n```" : "_(diff summary unavailable)_"; + await postStickyComment(prNumber, "dark-factory:coding", + `### βœ… πŸ€– Coding complete (engine: ${ENGINE})\n\nImplemented the change for issue #${ISSUE} on \`${BRANCH}\` in a hardware-isolated Kata micro-VM.\n\n**Files changed:**\n${filesBlock}`); + await postStickyComment(prNumber, "dark-factory:local-test", + `### ${test.green ? "βœ…" : "❌"} πŸ§ͺ Local testing (in-VM, before PR)\n\n**${test.green ? "Build + unit tests passed" : "Tests NOT green"}** β€” discovered from the repo's own marker files (no central config).\n\n${test.summary ? "```\n" + String(test.summary).slice(0, 800) + "\n```" : ""}`); + + // Post the AWS DevOps Agent verdict as its own commit status, and β€” when the + // verdict clears β€” apply the handoff label so the hub's Security Agent step + // runs next (DevOps-first ordering). When not connected/BLOCK, we leave the + // label off (Security stays gated) and report honestly. + if (DEVOPS_AGENT_MODE !== "off") { + const dvState = devops.cleared ? "success" : (devops.verdict === "BLOCK" ? "failure" : "error"); + try { + await gh("POST", `/repos/${REPO}/statuses/${headSha}`, { + state: dvState, context: "dark-factory/devops", + description: devops.summary.slice(0, 130), + }); + } catch (_) {} + if (devops.cleared && prNumber) { + // Labels attach to the PR number (the hub devops-gate reads + // /issues//labels). Label the PR, not the issue. + try { + await gh("POST", `/repos/${REPO}/issues/${prNumber}/labels`, { labels: [DEVOPS_CLEAR_LABEL] }); + console.log(`[coder] AWS DevOps Agent cleared β†’ applied '${DEVOPS_CLEAR_LABEL}' to PR #${prNumber} (Security Agent will run)`); + } catch (e) { console.error(`[coder] could not apply handoff label: ${e.message}`); } + } else if (devops.cleared && !prNumber) { + console.error("[coder] DevOps cleared but PR number unknown β€” cannot apply handoff label"); + } else { + console.log(`[coder] AWS DevOps Agent did NOT clear (${devops.verdict}) β†’ Security Agent stays gated`); + } + } + + // Self-report SUCCESS on the head SHA β€” this is what df-run polls for. + await gh("POST", `/repos/${REPO}/statuses/${headSha}`, { + state: "success", context: "dark-factory/implementation", + description: "implemented, built + tests green", + }); + console.log(`[coder] done β€” PR opened, status success on ${headSha}`); + } catch (e) { + console.error(`[coder] failed: ${e.message}`); + if (headSha) { + try { await gh("POST", `/repos/${REPO}/statuses/${headSha}`, { state: "failure", context: "dark-factory/implementation", description: e.message.slice(0, 130) }); } catch (_) {} + } + process.exit(1); + } + // Keep the VM alive briefly so logs are collectible; the claim TTL / teardown reaps it. + process.exit(0); +} + +main(); diff --git a/examples/dark-factory/deploy-test/Dockerfile b/examples/dark-factory/deploy-test/Dockerfile new file mode 100644 index 00000000..b291aeef --- /dev/null +++ b/examples/dark-factory/deploy-test/Dockerfile @@ -0,0 +1,25 @@ +# Dark Factory deploy-test image β€” runs in the TRUSTED hub deploy-test step. +# +# Content-aware validation of a PR's deployable artifacts: +# kind=k8s β†’ kubectl apply into an ephemeral namespace + wait Ready +# kind=terraform β†’ terraform init -backend=false + validate (+ fmt check) +# Bundles exactly what both paths + the PR-comment upsert need: kubectl, +# terraform, node, git, curl, bash. This is the only Dark Factory image that +# holds K8s access at runtime (via the workflow SA) β€” never the coder. +FROM public.ecr.aws/docker/library/alpine:3.20 + +ARG KUBECTL_VERSION=v1.31.0 +ARG TERRAFORM_VERSION=1.9.8 +ARG TARGETARCH=amd64 + +RUN apk add --no-cache bash git curl nodejs unzip ca-certificates \ + && curl -fsSL "https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/${TARGETARCH}/kubectl" -o /usr/local/bin/kubectl \ + && chmod +x /usr/local/bin/kubectl \ + && curl -fsSL "https://releases.hashicorp.com/terraform/${TERRAFORM_VERSION}/terraform_${TERRAFORM_VERSION}_linux_${TARGETARCH}.zip" -o /tmp/tf.zip \ + && unzip -o /tmp/tf.zip -d /usr/local/bin \ + && rm /tmp/tf.zip \ + && kubectl version --client=true 2>/dev/null | head -1 || true \ + && terraform version | head -1 + +# Non-root; the step needs no privileged access beyond the SA-bound RBAC. +USER 65532 diff --git a/gitops/addons/bootstrap/default/addons.yaml b/gitops/addons/bootstrap/default/addons.yaml index 7ad63963..84369c0b 100644 --- a/gitops/addons/bootstrap/default/addons.yaml +++ b/gitops/addons/bootstrap/default/addons.yaml @@ -358,3 +358,195 @@ oam-agent-components: global: awsRegion: '{{.metadata.annotations.aws_region}}' clusterName: '{{.metadata.annotations.aws_cluster_name}}' + +# Agent Sandbox OPERATOR (upstream agent-sandbox v0.5.1) β€” the controller + +# Sandbox/SandboxClaim/SandboxTemplate/SandboxWarmPool CRDs + webhooks. Vendored +# manifest applied directly (it's a self-contained install; the ~400KB CRD set +# is too large for a Helm-templated chart). ServerSideApply + Replace handle the +# oversized CRDs. Sync-wave 0: CRDs + operator MUST exist before the kata-deploy +# runtime (wave 1) and the agent-sandbox chart CRs (wave 2). +agent-sandbox-operator: + enabled: true + namespace: agent-sandbox-system + defaultVersion: '0.5.1' + path: 'gitops/addons/charts/agent-sandbox/upstream' + annotationsAppSet: + argocd.argoproj.io/sync-wave: '0' + syncPolicyAppSet: + preserveResourcesOnDeletion: false + # Agent Sandbox is a HUB-only capability. The Dark Factory (Flow B) is a + # pre-dev build/author activity that writes code + holds GitHub write access β€” + # it belongs on the hub (build plane, co-located with Argo Workflows), not on + # a spoke (deploy/run plane hosting enterprise workloads). alwaysSelector is + # honored regardless of the global useSelectors flag, so the generated + # ApplicationSet matches ONLY the cluster whose `environment` label is + # `control-plane` (the hub) β€” spoke apps are pruned. Kata runs on a dedicated + # tainted nested-virt MNG (Auto Mode can't host it) β€” see docs/dark-factory Β§10. + alwaysSelector: + matchExpressions: + - key: environment + operator: In + values: ['control-plane'] + +# Agent Sandbox capability chart β€” RuntimeClasses, kata-readiness (removes the +# runtime-not-ready startup taint), coder SandboxTemplate, SandboxWarmPool, and +# the egress NetworkPolicy. Sync-wave 2: after the operator (wave 0) + kata +# runtime (wave 1). Consumers (the Dark Factory, Flow B) then claim from the +# warm pool via a SandboxClaim(warmPoolRef). +agent-sandbox: + enabled: true + namespace: agent-sandbox-system + defaultVersion: '0.1.0' + path: 'gitops/addons/charts/agent-sandbox' + annotationsAppSet: + argocd.argoproj.io/sync-wave: '2' + # Hub-only (see agent-sandbox-operator note above). alwaysSelector pins + # generation to the `environment: control-plane` cluster regardless of useSelectors. + alwaysSelector: + matchExpressions: + - key: environment + operator: In + values: ['control-plane'] + valuesObject: + coderTemplate: + bifrostUrl: '{{default "http://bifrost.bifrost.svc.cluster.local:8080" (index .metadata.annotations "bifrost_url")}}' + +# agent-sandbox-lambda β€” Flow D. A SECOND, opt-in Agent-Sandbox substrate: the coder +# runs in an AWS Lambda MicroVM (Firecracker) instead of a Kata pod. KRO builds the +# platform image ONCE (MicrovmImage + build/exec IAM + S3 + logs via ACK GA +# controllers); a shim (bridge pod + lifecycle controller) drives the per-session VM +# (RunMicrovm/suspend/resume/TerminateMicrovm) imperatively. Same namespace + operator +# as agent-sandbox; only ADDS the MicroVM path. DORMANT until the per-cluster overlay +# flips microvm.enabled=true (needs Managed KRO+ACK, the self-managed ack-lambdamicrovms +# controller, and a published arm64 coder artifact). Hub-only, sync-wave 2. +agent-sandbox-lambda: + enabled: true + namespace: agent-sandbox-system + defaultVersion: '0.1.0' + path: 'gitops/addons/charts/agent-sandbox-lambda' + annotationsAppSet: + argocd.argoproj.io/sync-wave: '2' + alwaysSelector: + matchExpressions: + - key: environment + operator: In + values: ['control-plane'] + +# kata-deploy β€” installs the Kata runtime (containerd handlers for kata-clh / +# kata-qemu) on the tainted kata MNG nodes. Separate app (not a subchart dep of +# agent-sandbox) so ArgoCD pulls the upstream OCI chart directly. Gated by +# enable_agent_sandbox_kata β€” only clusters with a kata-capable nested-virt MNG +# (see docs/dark-factory Β§12a) should carry that label. Sync-wave 1: runtime is +# installed on nodes before the Sandbox operator (wave 2) schedules VMs. +kata-deploy: + enabled: true + namespace: kube-system + chartName: kata-deploy + defaultVersion: '3.32.0' + # OCI chart at oci://ghcr.io/kata-containers/kata-deploy-charts/kata-deploy. + # Do NOT set chartNamespace β€” the appset would build chart=/ + # (kata-containers/kata-deploy) producing a wrong OCI path. The namespace is + # already part of chartRepository. + chartRepository: 'ghcr.io/kata-containers/kata-deploy-charts' + annotationsAppSet: + argocd.argoproj.io/sync-wave: '1' + # Hub-only: the kata runtime is only installed where the nested-virt MNG + # exists (the hub). alwaysSelector pins generation to `environment: + # control-plane` regardless of useSelectors β€” no spoke kata-deploy app. + alwaysSelector: + matchExpressions: + - key: environment + operator: In + values: ['control-plane'] + # The kata-deploy OCI chart reads these keys at the ROOT of values (verified + # against chart v3.32.0 templates/kata-deploy.yaml β†’ .Values.nodeSelector / + # .Values.tolerations). Do NOT nest them under a `kata-deploy:` key. + valuesObject: + image: + reference: quay.io/kata-containers/kata-deploy + tag: '3.32.0' + # Run the DaemonSet ONLY on the tainted nested-virt kata node β€” never on the + # hub's Auto Mode control-plane nodes (where it crashes on the TEE/nydus + # snapshotter and would churn containerd). + nodeSelector: + kata-enabled: 'true' + tolerations: + - key: kata + operator: Equal + value: 'true' + effect: NoSchedule + - key: katacontainers.io/runtime-not-ready + operator: Exists + effect: NoSchedule + # Only install the two VMMs the platform uses (clh default + qemu); the + # RuntimeClasses are rendered by the agent-sandbox chart, not kata-deploy. + shims: + disableAll: true + qemu: + enabled: true + clh: + enabled: true + runtimeClasses: + enabled: false + +# ack-lambdamicrovms (Flow D) β€” SELF-MANAGED ACK controller for the pre-GA +# Lambda MicroVM service (lambdamicrovms.services.k8s.aws: MicrovmImage + Microvm). +# Self-managed because Managed ACK (EKS Capability) only bundles GA-upstream +# controllers, and lambdamicrovms is still v1alpha1. Managed ACK (GA iam/s3) and +# this self-managed controller COEXIST (different CRD groups). When lambdamicrovms +# goes GA, delete this addon and Managed ACK adopts it β€” the KRO RGD is unchanged. +# +# DISABLED by default: Flow D is dormant until a cluster opts in. Hub-only, like +# the rest of the sandbox substrate. Installs the OCI chart +# oci://public.ecr.aws/aws-controllers-k8s/lambdamicrovms-chart. Sync-wave 0 so the +# CRDs + controller are up before the agent-sandbox chart's MicrovmSandbox RGD +# (wave 2) references them. +ack-lambdamicrovms: + enabled: true + namespace: ack-system + chartName: lambdamicrovms-chart + defaultVersion: '0.1.1' + chartRepository: 'public.ecr.aws/aws-controllers-k8s' + annotationsAppSet: + argocd.argoproj.io/sync-wave: '0' + # Hub-only: the MicroVM substrate lives with the rest of the sandbox capability + # on the build plane. alwaysSelector pins generation to environment: + # control-plane regardless of useSelectors β€” never generated on a spoke. + alwaysSelector: + matchExpressions: + - key: environment + operator: In + values: ['control-plane'] + # ACK chart reads these at the ROOT of values (chart v0.1.1). The controller + # reconciles the Microvm + MicrovmImage CRDs; it uses EKS Pod Identity for AWS + # auth (empty SA annotations β€” the pod-identity association is created out of + # band / by the platform), so no IRSA role-arn annotation is set here. + valuesObject: + aws: + region: '{{default "us-west-2" (index .metadata.annotations "aws_region")}}' + serviceAccount: + create: true + name: ack-lambdamicrovms-controller + # Cluster-scoped install so the single MicrovmSandbox RGD (any namespace) can + # create Microvm/MicrovmImage CRs the controller reconciles. + installScope: cluster + deletionPolicy: delete + +# Dark Factory (Flow B) β€” Argo Workflows that turn a GitHub issue into a PR by +# claiming the Flow A warm pool. Hub-only (co-located with Argo Workflows + the +# sandbox pool); sync-wave 3 so it lands after the agent-sandbox capability +# (wave 2). Deploys the WorkflowTemplates + workflow RBAC. +dark-factory: + enabled: true + namespace: agent-sandbox-system + defaultVersion: '0.1.0' + path: 'gitops/addons/charts/dark-factory' + annotationsAppSet: + argocd.argoproj.io/sync-wave: '3' + # Hub-only. alwaysSelector pins generation to environment: control-plane + # regardless of useSelectors β€” never generated on a spoke. + alwaysSelector: + matchExpressions: + - key: environment + operator: In + values: ['control-plane'] diff --git a/gitops/addons/charts/agent-sandbox-lambda/Chart.yaml b/gitops/addons/charts/agent-sandbox-lambda/Chart.yaml new file mode 100644 index 00000000..d5da419c --- /dev/null +++ b/gitops/addons/charts/agent-sandbox-lambda/Chart.yaml @@ -0,0 +1,14 @@ +apiVersion: v2 +name: agent-sandbox-lambda +description: >- + Flow D β€” Lambda MicroVM substrate for the Agent Sandbox capability. A second, + opt-in execution substrate alongside the Kata micro-VM chart (agent-sandbox): + the coder runs in an AWS Lambda MicroVM (Firecracker) instead of a Kata node. + KRO builds the platform image ONCE (MicrovmImage + build/exec IAM + S3 artifact + + CloudWatch logs, via ACK GA controllers); a lightweight shim (bridge pod + + lifecycle controller) drives the per-session VM (RunMicrovm / suspend / resume / + TerminateMicrovm) imperatively via the AWS SDK. Same dark-factory-coder (arm64) + image + same Agent Sandbox UX as Flow A. Disabled by default (microvm.enabled). +type: application +version: 0.1.0 +appVersion: "0.1.0" diff --git a/gitops/addons/charts/agent-sandbox-lambda/templates/_helpers.tpl b/gitops/addons/charts/agent-sandbox-lambda/templates/_helpers.tpl new file mode 100644 index 00000000..dc54887e --- /dev/null +++ b/gitops/addons/charts/agent-sandbox-lambda/templates/_helpers.tpl @@ -0,0 +1,28 @@ +{{/* +Common labels applied to every resource this chart renders. Kept under the +app.kubernetes.io/name "agent-sandbox" (same capability, Lambda substrate) so +Flow D resources associate with the Agent Sandbox capability; the chart name +distinguishes them. +*/}} +{{- define "agent-sandbox.labels" -}} +app.kubernetes.io/name: agent-sandbox +app.kubernetes.io/component: lambda-microvm +app.kubernetes.io/part-of: open-agent-platform +app.kubernetes.io/managed-by: {{ .Release.Service }} +helm.sh/chart: {{ printf "%s-%s" .Chart.Name .Chart.Version }} +{{- end -}} + +{{/* +Selector labels (stable subset used by controllers). +*/}} +{{- define "agent-sandbox.selectorLabels" -}} +app.kubernetes.io/name: agent-sandbox +app.kubernetes.io/component: lambda-microvm +{{- end -}} + +{{/* +The namespace the capability runs in (must match the Kata agent-sandbox chart). +*/}} +{{- define "agent-sandbox.namespace" -}} +{{- default "agent-sandbox-system" .Values.namespace -}} +{{- end -}} diff --git a/gitops/addons/charts/agent-sandbox-lambda/templates/image/10-rgd-and-image.yaml b/gitops/addons/charts/agent-sandbox-lambda/templates/image/10-rgd-and-image.yaml new file mode 100644 index 00000000..55201363 --- /dev/null +++ b/gitops/addons/charts/agent-sandbox-lambda/templates/image/10-rgd-and-image.yaml @@ -0,0 +1,217 @@ +{{- if and .Values.microvm .Values.microvm.enabled }} +{{- /* +Flow D β€” MicrovmSandbox ResourceGraphDefinition (KRO). + +ONE composite CRD (kro.run) that ties together ALL the Lambda MicroVM primitives so +a consumer (the lambda-microvm SandboxTemplate bridge, or any agent) creates a single +`MicrovmSandbox` and gets the whole substrate: + + MicrovmSandbox -> S3 Bucket (s3.services.k8s.aws) image codeArtifact store + IAM Role (build) (iam.services.k8s.aws) MicrovmImage.buildRoleARN + IAM Role (exec) (iam.services.k8s.aws) Microvm.executionRoleARN + MicrovmImage (lambdamicrovms.services.k8s.aws) platform-owned image + Microvm (lambdamicrovms.services.k8s.aws) app-owned instance + +OWNERSHIP SPLIT is expressed IN THE SCHEMA: + spec.image.* β€” PLATFORM-owned (base image, code artifact) β€” set once per image + spec.run.* β€” APP-owned (per-claim instance: idle policy) + +NETWORK: Lambda MicroVMs have PUBLIC internet egress by DEFAULT, so no network +connectors are attached here (the coder only needs outbound git/gh/registry, like +Flow A). Ingress connectors (AWS-managed, inbound HTTPS) and VPC egress connectors +(created out-of-band via `aws lambda-core create-network-connector`) are optional +add-ons a future variant can wire in; they are intentionally omitted from v1. + +CONTROLLER SPLIT: the s3/iam resources are reconciled by MANAGED ACK (GA controllers); +MicrovmImage/Microvm by the SELF-MANAGED lambdamicrovms controller (pre-GA). The RGD is +identical regardless of who runs the controllers β€” when lambdamicrovms goes GA and +Managed ACK adopts it, THIS FILE DOES NOT CHANGE. + +Gated behind microvm.enabled so Flow D stays dormant until a cluster opts in. Requires +the Managed KRO capability (kro.run) + the ACK controllers to be present on the cluster. +*/ -}} +apiVersion: kro.run/v1alpha1 +kind: ResourceGraphDefinition +metadata: + name: microvm-sandbox + labels: + {{- include "agent-sandbox.labels" . | nindent 4 }} + annotations: + # Apply the RGD FIRST so KRO generates the MicrovmSandbox CRD before the instance + # (below) is synced β€” otherwise the instance fails dry-run ("CRD not found") and + # blocks the whole app. + argocd.argoproj.io/sync-wave: "-1" +spec: + schema: + apiVersion: v1alpha1 + kind: MicrovmSandbox + group: {{ .Values.microvm.apiGroup | default "kro.run" | quote }} + # SCOPE: this RGD builds ONLY the platform IMAGE + the slow-changing infra it + # needs (S3 artifact bucket, build role, execution role). It deliberately does + # NOT create a `Microvm` β€” a MicroVM instance is a per-SESSION, request-time + # resource whose create/suspend/resume/terminate are IMPERATIVE SDK ops that the + # ACK controller does not reconcile (confirmed in the lambdamicrovms-controller + # reference: run/suspend/resume/terminate are SDK calls, not desired state, and + # the 06-kro example likewise leaves the running Microvm out of the graph). The + # shim (SandboxTemplate bridge + microvm-lifecycle controller, templates 51/52) + # owns that lifecycle. So a `MicrovmSandbox` = "an image is built + ready + its + # exec identity", and its status is the HANDOFF the shim consumes to RunMicrovm. + spec: + # ── PLATFORM-owned: the image / substrate (set once per image) ────────── + # ARN of the AWS-published base MicroVM image to build from, e.g. + # arn:aws:lambda::aws:microvm-image:al2023-1 (ARM_64 β€” the only arch + # Lambda MicroVM supports). + baseImageARN: string + # S3 URI of the coder code artifact zip (app + Dockerfile), e.g. + # s3:///. Lambda MicroVM's codeArtifact.uri is S3-ONLY β€” it is + # NOT an ECR image reference (the Dockerfile inside the zip MAY pull private + # ECR base layers, which is why buildRole keeps ecr:Get*/BatchGetImage). The + # artifact must be published to the bucket before the first build runs. + codeArtifactUri: string + # AWS region + a name stem for the created resources. + region: string | default="{{ .Values.microvm.region }}" + name: string + status: + # The HANDOFF the shim reads to RunMicrovm (imperatively) per session: + # imageARN β†’ Microvm.imageIdentifier + # executionRoleARN β†’ Microvm.executionRoleARN + # Plus imageState so the shim only launches once the build is CREATED/UPDATED. + imageARN: ${image.status.ackResourceMetadata.arn} + imageState: ${image.status.state} + imageVersion: ${image.status.latestActiveImageVersion} + executionRoleARN: ${execRole.status.ackResourceMetadata.arn} + resources: + # 1) S3 bucket that stores the MicroVM code artifact (GA β€” Managed ACK). + - id: bucket + template: + apiVersion: s3.services.k8s.aws/v1alpha1 + kind: Bucket + metadata: + name: ${schema.spec.name}-microvm-artifacts + spec: + name: ${schema.spec.name}-microvm-artifacts + # 2) IAM role the image BUILD assumes (GA β€” Managed ACK). Platform-owned. + # Lambda assumes this during create-microvm-image to pull the code artifact + # (ECR/S3) + write build logs. Trust = lambda.amazonaws.com (verified in the + # Lambda MicroVM getting-started docs), with inline ECR-read + S3-read + logs. + - id: buildRole + template: + apiVersion: iam.services.k8s.aws/v1alpha1 + kind: Role + metadata: + name: ${schema.spec.name}-microvm-build + spec: + name: ${schema.spec.name}-microvm-build + assumeRolePolicyDocument: | + {"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":"lambda.amazonaws.com"},"Action":["sts:AssumeRole","sts:TagSession"]}]} + inlinePolicies: + microvm-build: | + {"Version":"2012-10-17","Statement":[ + {"Effect":"Allow","Action":["ecr:GetAuthorizationToken","ecr:BatchGetImage","ecr:GetDownloadUrlForLayer"],"Resource":"*"}, + {"Effect":"Allow","Action":["s3:GetObject","s3:ListBucket"],"Resource":["arn:aws:s3:::${bucket.spec.name}","arn:aws:s3:::${bucket.spec.name}/*"]}, + {"Effect":"Allow","Action":["logs:CreateLogGroup","logs:CreateLogStream","logs:PutLogEvents"],"Resource":"arn:aws:logs:*:*:log-group:/aws/lambda/microvms/*"} + ]} + # 3) IAM role the RUNNING MicroVM assumes (GA β€” Managed ACK). App-owned exec identity. + # trust = lambda.amazonaws.com. Grants bedrock:InvokeModel so the coder calls + # Bedrock DIRECTLY (Flow D is Bedrock-direct: a MicroVM runs outside the cluster + # and can't reach Bifrost's ClusterIP, so it uses this role's creds over public + # egress instead β€” see flow-d-coder-in-microvm-design.md). git/gh over public :443. + - id: execRole + template: + apiVersion: iam.services.k8s.aws/v1alpha1 + kind: Role + metadata: + name: ${schema.spec.name}-microvm-exec + spec: + name: ${schema.spec.name}-microvm-exec + assumeRolePolicyDocument: | + {"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":"lambda.amazonaws.com"},"Action":["sts:AssumeRole","sts:TagSession"]}]} + inlinePolicies: + bedrock-invoke: | + {"Version":"2012-10-17","Statement":[ + {"Effect":"Allow","Action":["bedrock:InvokeModel","bedrock:InvokeModelWithResponseStream","bedrock:Converse","bedrock:ConverseStream"],"Resource":["arn:aws:bedrock:*::foundation-model/*","arn:aws:bedrock:*:*:inference-profile/*"]} + ]} + # 4) MicrovmImage (pre-GA β€” SELF-MANAGED lambdamicrovms controller). Platform-owned. + # readyWhen gates the Microvm (resource 5) on a genuinely SUCCESSFUL build β€” + # state CREATED/UPDATED β€” so the instance never launches from a half-built or + # failed image (KRO holds the Microvm until this predicate is true). + - id: image + readyWhen: + - ${image.status.state == "CREATED" || image.status.state == "UPDATED"} + template: + apiVersion: lambdamicrovms.services.k8s.aws/v1alpha1 + kind: MicrovmImage + metadata: + name: ${schema.spec.name}-image + spec: + name: ${schema.spec.name}-image + baseImageARN: ${schema.spec.baseImageARN} + buildRoleARN: ${buildRole.status.ackResourceMetadata.arn} + codeArtifact: + uri: ${schema.spec.codeArtifactUri} + # Lambda MicroVM is ARM_64-ONLY β€” the sole supported architecture. The + # code artifact + any bundled binaries must be arm64 (dark-factory-coder + # is built for arm64 for exactly this substrate). + cpuConfigurations: + - architecture: ARM_64 + # Lifecycle hooks β€” the lambda-coder (hook-server.js) serves these on :8080. + # ready : build waits for the server to be up before snapshotting a + # clean, waiting coder (else the snapshot is taken too early). + # run : per-session start; delivers runHookPayload (issue context + + # GitHub token) as the request body β†’ hook-server background- + # spawns the coder. WITHOUT run:ENABLED the payload is silently + # never delivered. + # suspend/resume/terminate : lifecycle acks (coder holds no external state). + hooks: + port: 8080 + microvmImageHooks: + ready: ENABLED + readyTimeoutInSeconds: 120 + microvmHooks: + run: ENABLED + runTimeoutInSeconds: 30 + suspend: ENABLED + suspendTimeoutInSeconds: 30 + resume: ENABLED + resumeTimeoutInSeconds: 30 + terminate: ENABLED + terminateTimeoutInSeconds: 30 + # CloudWatch build/runtime logs. On a CREATE_FAILED the controller can't + # see the build output β€” this is where it lands: + # aws logs tail /aws/lambda/microvms/${schema.spec.name}-image + logging: + cloudWatch: + logGroup: /aws/lambda/microvms/${schema.spec.name}-image + # NOTE: there is deliberately NO `Microvm` resource here. The running MicroVM is + # a per-session, request-time resource β€” the shim (microvm-lifecycle controller, + # template 52) creates it with RunMicrovm and drives suspend/resume/terminate as + # imperative SDK ops (the ACK controller does not reconcile those). This RGD stops + # at "image built + exec role ready", handed off via status above. +{{- if .Values.microvm.image.enabled | default true }} +--- +# The ONE platform MicrovmSandbox INSTANCE β€” applied by GitOps, reconciled by KRO +# ONCE to build the coder image + its build/exec roles + artifact bucket. This is +# slow-changing platform infra (one image per cluster), NOT per-session β€” so it lives +# here in the chart, not in the per-claim shim. The shim reads THIS object's status +# (imageARN + executionRoleARN) to RunMicrovm per session. Rebuild the image by +# bumping microvm.codeArtifactUri (a new artifact) and re-syncing. +apiVersion: {{ .Values.microvm.apiGroup | default "kro.run" }}/v1alpha1 +kind: MicrovmSandbox +metadata: + name: {{ .Values.microvm.image.name | default "coder" }} + namespace: {{ include "agent-sandbox.namespace" . }} + labels: + {{- include "agent-sandbox.labels" . | nindent 4 }} + annotations: + # Sync AFTER the RGD (wave -1) so its generated CRD exists. SkipDryRunOnMissingResource + # lets the first sync proceed even if KRO hasn't registered the CRD in the same pass β€” + # ArgoCD retries + selfHeal converge once the CRD appears (no whole-app block). + argocd.argoproj.io/sync-wave: "1" + argocd.argoproj.io/sync-options: SkipDryRunOnMissingResource=true +spec: + name: {{ .Values.microvm.image.name | default "coder" }} + region: {{ .Values.microvm.region | quote }} + baseImageARN: {{ .Values.microvm.baseImageARN | quote }} + codeArtifactUri: {{ .Values.microvm.codeArtifactUri | quote }} +{{- end }} +{{- end }} diff --git a/gitops/addons/charts/agent-sandbox-lambda/templates/shim/00-controller-pod-identity.yaml b/gitops/addons/charts/agent-sandbox-lambda/templates/shim/00-controller-pod-identity.yaml new file mode 100644 index 00000000..f4f639ef --- /dev/null +++ b/gitops/addons/charts/agent-sandbox-lambda/templates/shim/00-controller-pod-identity.yaml @@ -0,0 +1,150 @@ +{{/* + Self-managed ACK lambdamicrovms controller β†’ AWS access via EKS Pod Identity, + declared ALL-ACK (same mechanism as the RGD's build/exec roles): an ACK + iam.services.k8s.aws Role (trusted by the EKS Pod Identity service principal) + + an ACK eks.services.k8s.aws PodIdentityAssociation binding it to the controller's + ServiceAccount (ack-lambdamicrovms-controller in ack-system, created by the ACK + chart). No Crossplane, no Terraform, no CLI. + + This is the ONE bootstrap IAM the KRO RGD can't self-create (the controller needs + creds before it can create anything). It's reconciled by the Managed-ACK iam + eks + controllers (both live on the hub). Everything DOWNSTREAM β€” the S3 bucket, the build + role, the exec role, the MicrovmImage β€” is created by the KRO RGD via ACK too (see + templates/image/). So the entire IAM surface is ACK/KRO/GitOps. + + Earlier sync-wave so the role + association exist before the controller pod needs + them; Pod Identity creds are vended on demand + ArgoCD selfHeal converges with no + manual steps. Gated by microvm.enabled. +*/}} +{{- if and .Values.microvm .Values.microvm.enabled }} +apiVersion: iam.services.k8s.aws/v1alpha1 +kind: Role +metadata: + name: {{ .Values.microvm.podIdentity.clusterName }}-ack-lambdamicrovms + namespace: {{ include "agent-sandbox.namespace" . }} + labels: + {{- include "agent-sandbox.labels" . | nindent 4 }} + annotations: + argocd.argoproj.io/sync-wave: "-2" +spec: + name: {{ .Values.microvm.podIdentity.clusterName }}-ack-lambdamicrovms-controller + # Trust the EKS Pod Identity service principal (not IRSA/OIDC). + assumeRolePolicyDocument: | + { + "Version": "2012-10-17", + "Statement": [{ + "Effect": "Allow", + "Principal": {"Service": "pods.eks.amazonaws.com"}, + "Action": ["sts:AssumeRole", "sts:TagSession"] + }] + } + inlinePolicies: + lambdamicrovms: | + { + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "LambdaMicrovms", + "Effect": "Allow", + "Action": [ + "lambda:CreateMicrovmImage","lambda:UpdateMicrovmImage","lambda:DeleteMicrovmImage", + "lambda:GetMicrovmImage","lambda:GetMicrovmImageVersion","lambda:ListMicrovmImages", + "lambda:RunMicrovm","lambda:GetMicrovm","lambda:TerminateMicrovm","lambda:ListMicrovms", + "lambda:SuspendMicrovm","lambda:ResumeMicrovm", + "lambda:CreateMicrovmAuthToken","lambda:CreateMicrovmShellAuthToken", + "lambda:TagResource","lambda:UntagResource","lambda:ListTagsForResource", + "lambda:ListNetworkConnectors","lambda:GetNetworkConnector" + ], + "Resource": "*" + }, + {{- /* + CreateMicrovmImage/RunMicrovm attach a network connector to the MicroVM. + With no explicit connectors in the RGD, the service uses the AWS-managed + default INTERNET_EGRESS connector, and passing it needs + lambda:PassNetworkConnector (verified: image build denied + 'lambda:PassNetworkConnector on .../network-connector:aws-network-connector: + INTERNET_EGRESS'). Scope to the AWS-managed connector ARNs in-region. + */ -}} + { + "Sid": "PassNetworkConnectors", + "Effect": "Allow", + "Action": "lambda:PassNetworkConnector", + "Resource": [ + "arn:aws:lambda:{{ .Values.microvm.region }}:aws:network-connector:*", + "arn:aws:lambda:{{ .Values.microvm.region }}:{{ .Values.microvm.accountId }}:network-connector:*" + ] + }, + {{- /* + Scope PassRole by the TARGET role ARN (the build/exec roles KRO creates), + NOT by an iam:PassedToService condition. CreateMicrovmImage/RunMicrovm pass + the role to the Lambda MicroVM sub-service whose principal is NOT plain + lambda.amazonaws.com β€” a StringEquals on lambda.amazonaws.com fails closed, + so the controller got AccessDenied on iam:PassRole for coder-microvm-build + (verified: sim ALLOWED for lambda.amazonaws.com yet the live API DENIED, i.e. + the real passed-to principal differs; microvms/microvm.lambda.amazonaws.com + also implicitDeny). ARN-scoping to *-microvm-build/-exec is net-TIGHTER than + the previous Resource:* β€” only these two purpose-built roles can be passed β€” + and is principal-agnostic so it survives whatever sub-service MicroVM uses. + */ -}} + { + "Sid": "PassBuildExecRoles", + "Effect": "Allow", + "Action": "iam:PassRole", + "Resource": [ + "arn:aws:iam::{{ .Values.microvm.accountId }}:role/*-microvm-build", + "arn:aws:iam::{{ .Values.microvm.accountId }}:role/*-microvm-exec" + ] + } + ] + } +--- +apiVersion: eks.services.k8s.aws/v1alpha1 +kind: PodIdentityAssociation +metadata: + name: {{ .Values.microvm.podIdentity.clusterName }}-ack-lambdamicrovms + namespace: {{ include "agent-sandbox.namespace" . }} + labels: + {{- include "agent-sandbox.labels" . | nindent 4 }} + annotations: + argocd.argoproj.io/sync-wave: "-2" +spec: + clusterName: {{ .Values.microvm.podIdentity.clusterName }} + namespace: {{ .Values.microvm.podIdentity.controllerNamespace | default "ack-system" }} + serviceAccount: {{ .Values.microvm.podIdentity.controllerServiceAccount | default "ack-lambdamicrovms-controller" }} + # ACK PodIdentityAssociation takes the role ARN (no role-ref selector). The ACK + # Role above has a deterministic name, so the ARN is constructed from the account id. + roleARN: "arn:aws:iam::{{ .Values.microvm.accountId }}:role/{{ .Values.microvm.podIdentity.clusterName }}-ack-lambdamicrovms-controller" +--- +# The BRIDGE SA (agent-sandbox-system) calls RunMicrovm/GetMicrovm/TerminateMicrovm β€” +# reuse the same lambda-microvms role via its own PodIdentityAssociation. +apiVersion: eks.services.k8s.aws/v1alpha1 +kind: PodIdentityAssociation +metadata: + name: {{ .Values.microvm.podIdentity.clusterName }}-microvm-bridge + namespace: {{ include "agent-sandbox.namespace" . }} + labels: + {{- include "agent-sandbox.labels" . | nindent 4 }} + annotations: + argocd.argoproj.io/sync-wave: "-2" +spec: + clusterName: {{ .Values.microvm.podIdentity.clusterName }} + namespace: {{ include "agent-sandbox.namespace" . }} + serviceAccount: microvm-bridge + roleARN: "arn:aws:iam::{{ .Values.microvm.accountId }}:role/{{ .Values.microvm.podIdentity.clusterName }}-ack-lambdamicrovms-controller" +--- +# The LIFECYCLE controller SA calls suspend-microvm/resume-microvm β€” same role. +apiVersion: eks.services.k8s.aws/v1alpha1 +kind: PodIdentityAssociation +metadata: + name: {{ .Values.microvm.podIdentity.clusterName }}-microvm-lifecycle + namespace: {{ include "agent-sandbox.namespace" . }} + labels: + {{- include "agent-sandbox.labels" . | nindent 4 }} + annotations: + argocd.argoproj.io/sync-wave: "-2" +spec: + clusterName: {{ .Values.microvm.podIdentity.clusterName }} + namespace: {{ include "agent-sandbox.namespace" . }} + serviceAccount: microvm-lifecycle + roleARN: "arn:aws:iam::{{ .Values.microvm.accountId }}:role/{{ .Values.microvm.podIdentity.clusterName }}-ack-lambdamicrovms-controller" +{{- end }} diff --git a/gitops/addons/charts/agent-sandbox-lambda/templates/shim/20-bridge-sandboxtemplate.yaml b/gitops/addons/charts/agent-sandbox-lambda/templates/shim/20-bridge-sandboxtemplate.yaml new file mode 100644 index 00000000..cc845c6a --- /dev/null +++ b/gitops/addons/charts/agent-sandbox-lambda/templates/shim/20-bridge-sandboxtemplate.yaml @@ -0,0 +1,469 @@ +{{- if and .Values.microvm .Values.microvm.enabled }} +{{- /* +Flow D β€” `lambda-microvm` SandboxTemplate + bridge RBAC + bridge script. + +The RuntimeClass "shim": Lambda MicroVM is a REMOTE AWS service, not a node-local +containerd handler, so there is no literal `lambda-microvm` RuntimeClass (that would +need a virtual-kubelet β€” out of scope). Instead this SandboxTemplate's pod is a thin +BRIDGE that preserves the Agent-Sandbox UX: + + 1. runs on a NORMAL Auto-Mode node (no kata runtimeClass / nodeSelector / taint) + 2. reads the ONE platform-built image (the committed MicrovmSandbox in template 50, + whose status carries imageARN + executionRoleARN β€” built ONCE by KRO/ACK, NOT + per session) and calls RunMicrovm (imperative AWS SDK) to launch a per-session + MicroVM running the SAME dark-factory-coder entrypoint + 3. records the microvmID on the owning Sandbox (annotation) so the microvm-lifecycle + controller (template 52) can suspend/resume it, and holds the pod so the pod + lifecycle mirrors the MicroVM; on real teardown it calls TerminateMicrovm + +ARCHITECTURE SPLIT (why the bridge no longer creates a MicrovmSandbox per claim): +image build = slow, declarative, ONE per cluster (KRO/ACK, template 50). Running a VM += fast, imperative, per SESSION (RunMicrovm/suspend/resume/terminate are SDK ops the +ACK controller does NOT reconcile). So the bridge drives the RUN side via the SDK and +only READS the platform image handoff β€” it does not re-run KRO per claim. + +To Flow B and the user this looks identical to a Flow A claim. The bridge needs a +ServiceAccount + AWS creds (Pod Identity) to call the Lambda MicroVM SDK. + +Gated behind microvm.enabled. Rendered as a SEPARATE SandboxTemplate +(`-microvm`) so both substrates can coexist on one cluster; a consumer +selects the substrate by which template its SandboxClaim references. +*/ -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: microvm-bridge + namespace: {{ include "agent-sandbox.namespace" . }} + labels: + {{- include "agent-sandbox.labels" . | nindent 4 }} +--- +{{- /* + Bridge API-server egress. + + The bridge pod carries agent-sandbox.io/role=coder (so Flow B's claim contract + + the shared coder-sandbox-egress policy apply to it). But that policy denies RFC-1918 + + the service CIDR on :443 to isolate UNTRUSTED coder code from the control plane β€” + and the K8s API server lives exactly there (kubernetes svc 172.20.0.1 + apiserver + endpoints in the VPC 10.0.0.0/8). NetworkPolicies are additive, so this ADDS an + egress allow for the API server, selected ONLY on the bridge's distinct + agent-sandbox.io/substrate=lambda-microvm label (Kata coders don't have it, so their + isolation is untouched). The bridge runs TRUSTED platform code (reads the MicrovmSandbox + image handoff + annotates the owning Sandbox with the microvmID) β€” unlike the Kata coder + it MUST reach the API server, or `kubectl get microvmsandbox` hangs and it never + RunMicrovm's (verified: in-pod kubectl to 172.20.0.1:443 timed out under coder-egress). +*/ -}} +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: microvm-bridge-apiserver-egress + namespace: {{ include "agent-sandbox.namespace" . }} + labels: + {{- include "agent-sandbox.labels" . | nindent 4 }} +spec: + podSelector: + matchLabels: + agent-sandbox.io/substrate: lambda-microvm + policyTypes: + - Egress + egress: + # K8s API server β€” service ClusterIP + the in-VPC apiserver endpoints, :443 only. + - to: + - ipBlock: + cidr: {{ .Values.microvm.apiServerCidr | default "172.20.0.1/32" | quote }} + {{- range .Values.microvm.apiServerEndpointCidrs | default (list "10.0.0.0/16") }} + - ipBlock: + cidr: {{ . | quote }} + {{- end }} + ports: + - protocol: TCP + port: 443 + # EKS Pod Identity credential endpoint (link-local 169.254.170.23:80). The bridge + # gets its AWS creds β€” to call lambda-microvms run/suspend/resume/terminate β€” from + # the Pod Identity agent here. The shared coder-egress policy denies 169.254.0.0/16 + # (to block IMDS for untrusted coder code), which ALSO blocks Pod Identity, so the + # bridge's run-microvm failed "retrieving credentials from container-role: connect + # timeout http://169.254.170.23/v1/credentials". Allow ONLY the Pod Identity /32 (NOT + # IMDS 169.254.169.254, which stays denied) and ONLY for the bridge selector. + - to: + - ipBlock: + cidr: {{ .Values.microvm.podIdentityEndpoint | default "169.254.170.23/32" | quote }} + ports: + - protocol: TCP + port: 80 +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: microvm-bridge + namespace: {{ include "agent-sandbox.namespace" . }} + labels: + {{- include "agent-sandbox.labels" . | nindent 4 }} +rules: + # READS the platform MicrovmSandbox (image handoff: imageARN + executionRoleARN) β€” + # GitOps-owned platform infra (template 50), built once. + - apiGroups: [{{ .Values.microvm.apiGroup | default "kro.run" | quote }}] + resources: ["microvmsandboxes"] + verbs: ["get", "list", "watch"] + # Per-session Microvm CR: the bridge CREATES it (declarative path β€” the only way the + # controller fires the /run hook that delivers runHookPayload) and DELETES it on + # teardown (controller terminates the VM). + - apiGroups: ["lambdamicrovms.services.k8s.aws"] + resources: ["microvms"] + verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] + # Per-session payload Secret (runHookPayload SecretKeyReference). + - apiGroups: [""] + resources: ["secrets"] + verbs: ["get", "create", "update", "patch", "delete"] + # Read the owning Sandbox (suspend-vs-teardown in preStop) + patch it to record the + # per-session microvmID (annotation) so the microvm-lifecycle controller can + # suspend/resume THIS session's VM. + - apiGroups: ["agents.x-k8s.io"] + resources: ["sandboxes"] + verbs: ["get", "patch", "update"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: microvm-bridge + namespace: {{ include "agent-sandbox.namespace" . }} + labels: + {{- include "agent-sandbox.labels" . | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: microvm-bridge +subjects: + - kind: ServiceAccount + name: microvm-bridge + namespace: {{ include "agent-sandbox.namespace" . }} +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: microvm-bridge-script + namespace: {{ include "agent-sandbox.namespace" . }} + labels: + {{- include "agent-sandbox.labels" . | nindent 4 }} +data: + bridge.sh: | + #!/bin/sh + # Flow D bridge β€” claim -> RunMicrovm (SDK) against the pre-built platform image -> + # mirror lifecycle. Idles until a SandboxClaim injects DF_ISSUE_NUMBER (Flow B), + # exactly like the Kata coder. Needs AWS creds (Pod Identity) for the SDK calls. + set -eu + # The bridge image is public.ecr.aws/aws-cli/aws-cli:latest β€” a glibc, always-current + # aws-cli v2 that KNOWS the pre-GA `lambda-microvms` service (the alpine/k8s image's + # aws-cli 1.34 does NOT: `aws lambda-microvms` printed the service list = unrecognized, + # so RunMicrovm never fired). That image has no kubectl, so fetch a static one here + # (same pattern as the security-agent bootstrap). Both are then on PATH. + if ! command -v kubectl >/dev/null 2>&1; then + echo "[microvm-bridge] fetching kubectl..." + ARCH="$(uname -m)"; case "$ARCH" in aarch64|arm64) A=arm64;; *) A=amd64;; esac + KV="$(curl -fsSL https://dl.k8s.io/release/stable.txt)" + curl -fsSL "https://dl.k8s.io/release/${KV}/bin/linux/${A}/kubectl" -o /tmp/kubectl + chmod +x /tmp/kubectl; export PATH="/tmp:$PATH" + fi + REGION="{{ .Values.microvm.region }}" + PLATFORM_IMAGE="{{ .Values.microvm.image.name | default "coder" }}" # the ONE committed MicrovmSandbox (template 50) + NS="{{ include "agent-sandbox.namespace" . }}" + echo "[microvm-bridge] idle β€” waiting for a SandboxClaim to inject DF_ISSUE_NUMBER..." + while [ -z "${DF_ISSUE_NUMBER:-}" ]; do sleep 5; done + # SANDBOX_NAME (downward API = owning Sandbox CR name) lets the microvm-lifecycle + # controller correlate THIS session's MicroVM for suspend/resume. + SANDBOX_NAME="${SANDBOX_NAME:-df-${DF_ISSUE_NUMBER}}" + echo "[microvm-bridge] claim for issue #${DF_ISSUE_NUMBER} (sandbox=${SANDBOX_NAME})" + + # 1) READ the platform image handoff (built ONCE by KRO/ACK β€” template 50). Wait + # until its image build is terminal (CREATED/UPDATED) before launching. + echo "[microvm-bridge] reading platform image ${PLATFORM_IMAGE} (waiting for build ready)..." + i=0; IMAGE_ARN=""; EXEC_ROLE="" + while [ "$i" -lt 240 ]; do + IST=$(kubectl get microvmsandbox "${PLATFORM_IMAGE}" -n "${NS}" -o jsonpath='{.status.imageState}' 2>/dev/null || echo "") + if [ "${IST}" = "CREATED" ] || [ "${IST}" = "UPDATED" ]; then + IMAGE_ARN=$(kubectl get microvmsandbox "${PLATFORM_IMAGE}" -n "${NS}" -o jsonpath='{.status.imageARN}' 2>/dev/null || echo "") + EXEC_ROLE=$(kubectl get microvmsandbox "${PLATFORM_IMAGE}" -n "${NS}" -o jsonpath='{.status.executionRoleARN}' 2>/dev/null || echo "") + [ -n "${IMAGE_ARN}" ] && [ -n "${EXEC_ROLE}" ] && break + fi + i=$((i+1)); sleep 5 + done + if [ -z "${IMAGE_ARN}" ] || [ -z "${EXEC_ROLE}" ]; then + echo "[microvm-bridge] ERROR: platform image not ready (imageState=${IST:-}) β€” cannot RunMicrovm"; exit 1 + fi + echo "[microvm-bridge] image=${IMAGE_ARN} execRole=${EXEC_ROLE}" + + # 2) Build the runHookPayload β€” the per-session context the coder needs, delivered + # as the /run hook body (hook-server.js background-spawns the coder from it). + # CRITICAL: runHookPayload is a SecretKeyReference delivered by the DECLARATIVE + # Microvm CR (the controller reads the Secret + drives the /run hook). The + # imperative `run-microvm --run-hook-payload` CLI launches the VM but NEVER fires + # /run (verified: VM RUNNING, hook-server listening, but coder never started). So + # we write a Secret + create a Microvm CR. GitHub token from the mounted secret; + # NO Bifrost key β€” the coder is Bedrock-direct via the exec role. + GH_TOKEN="$(cat /etc/df/gh-token 2>/dev/null || echo "")" + [ -z "${GH_TOKEN}" ] && echo "[microvm-bridge] WARN: no gh-token mounted β€” coder cannot open a PR" + # python3 (node is absent in the aws-cli image); json.dumps escapes token/title. One + # line so it stays inside the bridge.sh: | YAML block scalar. + # FIX ROUND: df-run injects DF_ITERATE_NOTE_B64 (the human change request) into this + # `coder` container's env (same claim contract as Kata). The MicroVM coder has no + # claim env, so fold the note into the runHookPayload β€” hook-server maps it back to + # the coder's DF_ITERATE_NOTE_B64. Without this a Lambda fix round runs with NO + # instructions and reports "done" on the OLD sha (zero commits). + [ -n "${DF_ITERATE_NOTE_B64:-}" ] && echo "[microvm-bridge] fix round: forwarding iterate note to coder" + PAYLOAD=$(GH="${GH_TOKEN}" REGION="${REGION}" python3 -c 'import json,os; e=os.environ.get; print(json.dumps({"ghToken":e("GH",""),"region":e("REGION","us-west-2"),"issueNumber":e("DF_ISSUE_NUMBER",""),"repo":e("DF_REPO",""),"branch":e("DF_BRANCH",""),"baseBranch":e("DF_BASE_BRANCH","main"),"issueTitle":e("DF_ISSUE_TITLE",""),"iterateNoteB64":e("DF_ITERATE_NOTE_B64",""),"iterateNote":e("DF_ITERATE_NOTE","")}))') + + MVM="mvm-${DF_ISSUE_NUMBER}" # Microvm CR + payload Secret name for this session + # RESUME-ON-FIX-ROUND (the suspend/resume highlight): the CR name is stable + # (mvm-). On a fix round the previous round's VM is still around β€” SUSPENDED + # after the first PR (idlePolicy.autoResumeEnabled=false keeps it down). Rather than + # terminate + rebuild a fresh VM, we RESUME the suspended one via the Sandbox CRD: + # flip operatingMode=Running β†’ the microvm-lifecycle controller calls resume-microvm β†’ + # the SAME VM (memory+disk preserved) comes back, and hook-server accepts a NEW /run + # because its guard is keyed on a per-invocation run-id (issue+note hash), not a + # one-shot boolean (see coder-microvm/hook-server.js). This is the whole Flow D value + # prop: scale-to-zero between rounds, warm-resume for the fix. Fresh VMs are only made + # on the FIRST round (no existing CR). + RESUME_ROUND="" + if kubectl get microvm "${MVM}" -n "${NS}" >/dev/null 2>&1; then + echo "[microvm-bridge] fix round: resuming suspended VM ${MVM} via Sandbox.operatingMode=Running" + kubectl patch sandbox "${SANDBOX_NAME}" -n "${NS}" --type merge \ + -p '{"spec":{"operatingMode":"Running"}}' >/dev/null 2>&1 \ + && echo "[microvm-bridge] operatingMode=Running set β€” microvm-lifecycle will resume the VM" \ + || echo "[microvm-bridge] WARN: could not set operatingMode=Running" + # Refresh the payload Secret so the controller/hook sees the NEW iterate note. + RESUME_ROUND=1 + fi + # Build BOTH manifests as JSON with python3 and pipe to kubectl apply. JSON (not a + # heredoc) on purpose: a heredoc's column-0 EOF terminator breaks out of the + # bridge.sh: | YAML block scalar. JSON is valid YAML and stays on indented lines. + MAXIDLE={{ .Values.microvm.defaults.maxIdleDurationSeconds }}; SUSPDUR={{ .Values.microvm.defaults.suspendedDurationSeconds }} + INGRESS="arn:aws:lambda:${REGION}:aws:network-connector:aws-network-connector:ALL_INGRESS" + LOGGRP="/aws/lambda/microvms/{{ .Values.microvm.image.name | default "coder" }}-image" + echo "[microvm-bridge] writing payload Secret + Microvm CR ${MVM} for issue #${DF_ISSUE_NUMBER}..." + MVM="${MVM}" NS="${NS}" PAYLOAD="${PAYLOAD}" python3 -c 'import json,os; e=os.environ; mvm=e["MVM"]; ns=e["NS"]; print(json.dumps({"apiVersion":"v1","kind":"Secret","metadata":{"name":mvm+"-payload","namespace":ns},"type":"Opaque","stringData":{"payload":e["PAYLOAD"]}}))' | kubectl apply -f - >/dev/null 2>&1 || { echo "[microvm-bridge] payload secret apply failed"; exit 1; } + # Microvm CR: ingress ALL_INGRESS (so the bridge can reach the endpoint to drive /run), + # egress INTERNET_EGRESS (Bedrock + git/gh), runtime logging β†’ CloudWatch (logStream + # 'runtime' so the coder's stdout is visible, separate from build logs). + MVM="${MVM}" NS="${NS}" IMG="${IMAGE_ARN}" EXECROLE="${EXEC_ROLE}" MAXIDLE="${MAXIDLE}" SUSPDUR="${SUSPDUR}" INGRESS="${INGRESS}" REGION="${REGION}" LOGGRP="${LOGGRP}" python3 -c 'import json,os; e=os.environ; mvm=e["MVM"]; ns=e["NS"]; r=e["REGION"]; print(json.dumps({"apiVersion":"lambdamicrovms.services.k8s.aws/v1alpha1","kind":"Microvm","metadata":{"name":mvm,"namespace":ns},"spec":{"imageIdentifier":e["IMG"],"executionRoleARN":e["EXECROLE"],"ingressNetworkConnectors":[e["INGRESS"]],"egressNetworkConnectors":["arn:aws:lambda:"+r+":aws:network-connector:aws-network-connector:INTERNET_EGRESS"],"runHookPayload":{"name":mvm+"-payload","key":"payload","namespace":ns},"logging":{"cloudWatch":{"logGroup":e["LOGGRP"],"logStream":"runtime-"+mvm}},"idlePolicy":{"autoResumeEnabled":False,"maxIdleDurationSeconds":int(e["MAXIDLE"]),"suspendedDurationSeconds":int(e["SUSPDUR"])}}}))' | kubectl apply -f - >/dev/null 2>&1 || { echo "[microvm-bridge] Microvm CR apply failed"; exit 1; } + + # 4) Wait for the controller to report the running VM's id, record it on the Sandbox + # (lifecycle controller reads this to suspend/resume THIS session's VM). + VMID=""; i=0 + while [ "$i" -lt 60 ]; do + VMID=$(kubectl get microvm "${MVM}" -n "${NS}" -o jsonpath='{.status.microvmID}' 2>/dev/null || echo "") + [ -n "${VMID}" ] && break + i=$((i+1)); sleep 5 + done + echo "[microvm-bridge] Microvm ${MVM} -> ${VMID:-}" + [ -n "${VMID}" ] && kubectl annotate sandbox "${SANDBOX_NAME}" -n "${NS}" \ + "microvm-lifecycle.agents.x-k8s.io/microvm-id=${VMID}" --overwrite >/dev/null 2>&1 || true + + # 4b) DRIVE the coder: wait for RUNNING + an endpoint, mint an auth token, and POST the + # payload to /run on the endpoint. This is the deterministic invocation (verified by + # probe): the service's internal /run auto-fire wasn't reliably starting the coder, so + # the bridge drives it explicitly like the reference run_session. hook-server's /run + # background-spawns the coder (returns fast); df-run's await-coder polls GitHub for the PR. + if [ -n "${VMID}" ]; then + EP=""; i=0 + while [ "$i" -lt 60 ]; do + S=$(aws lambda-microvms get-microvm --region "${REGION}" --microvm-identifier "${VMID}" --query 'state' --output text 2>/dev/null || echo "") + EP=$(aws lambda-microvms get-microvm --region "${REGION}" --microvm-identifier "${VMID}" --query 'endpoint' --output text 2>/dev/null || echo "") + [ "$S" = "RUNNING" ] && [ -n "${EP}" ] && [ "${EP}" != "None" ] && break + i=$((i+1)); sleep 5 + done + TOKEN=$(aws lambda-microvms create-microvm-auth-token --region "${REGION}" --microvm-identifier "${VMID}" \ + --expiration-in-minutes 60 --allowed-ports 'port=8080' 2>/dev/null \ + | python3 -c 'import json,sys; print(json.load(sys.stdin)["authToken"]["X-aws-proxy-auth"])' 2>/dev/null || echo "") + if [ -n "${EP}" ] && [ -n "${TOKEN}" ]; then + echo "[microvm-bridge] driving coder: POST /run on ${EP}" + RC=$(curl -sS -m 30 -o /tmp/run.out -w '%{http_code}' -X POST "https://${EP}/run" \ + -H "X-aws-proxy-auth: ${TOKEN}" -H 'Content-Type: application/json' -d "${PAYLOAD}" 2>/tmp/run.err || echo "000") + echo "[microvm-bridge] /run -> HTTP ${RC} $(cat /tmp/run.out 2>/dev/null | head -c 120)" + else + echo "[microvm-bridge] WARN: no endpoint/token β€” cannot drive /run (ep=${EP:-none} token=$([ -n "${TOKEN}" ] && echo yes || echo no))" + fi + fi + + # 5) Teardown vs suspend β€” decided by whether the OWNING SANDBOX STILL EXISTS. + # The agent-sandbox operator handles operatingMode=Suspended by DELETING THE POD + # (verified in the operator log: "Deleting Pod because .Spec.OperatingMode is + # Suspended") while KEEPING the Sandbox object alive in state SandboxSuspended. So + # the bridge pod is torn down on EVERY suspend β€” and its cleanup trap must NOT delete + # the Microvm CR then, or the VM is terminated instead of suspended (exactly the bug + # we hit: Sandbox survived Suspended, but the VM was gone). + # - Sandbox STILL EXISTS => this is a SUSPEND (or transient pod restart) => KEEP CR. + # - Sandbox GONE => real teardown (df-merge-teardown deleted the claim) => + # delete CR so the controller TerminateMicrovm's the VM. + # (Checking Sandbox existence is more robust than a preStop /tmp marker, which raced + # the SIGTERM and didn't reliably stick.) + cleanup() { + if kubectl get sandbox "${SANDBOX_NAME}" -n "${NS}" >/dev/null 2>&1; then + M=$(kubectl get sandbox "${SANDBOX_NAME}" -n "${NS}" -o jsonpath='{.spec.operatingMode}' 2>/dev/null || echo "") + echo "[microvm-bridge] pod stopping but Sandbox ${SANDBOX_NAME} still exists (operatingMode=${M:-?}) β€” KEEPING Microvm ${MVM} (suspend, not teardown)" + return + fi + echo "[microvm-bridge] Sandbox ${SANDBOX_NAME} gone β€” real teardown: deleting Microvm ${MVM} (controller terminates the VM)" + kubectl delete microvm "${MVM}" -n "${NS}" --ignore-not-found >/dev/null 2>&1 || true + kubectl delete secret "${MVM}-payload" -n "${NS}" --ignore-not-found >/dev/null 2>&1 || true + } + trap cleanup EXIT INT TERM + + # 6) Hold the pod so Sandbox lifecycle == Microvm lifecycle. Poll the in-VM coder /logs + # (OBSERVABILITY β€” runtime CloudWatch routing is unreliable on this runtime) ONLY + # until the coder pushes its PR. Then suspend and STOP touching the endpoint. + # + # SUSPEND-VIA-CRD (the Flow D highlight): suspend/resume is driven declaratively + # through Sandbox.spec.operatingMode, reconciled by the microvm-lifecycle controller + # (template 30) β€” NOT by an imperative suspend-microvm call here. Two reasons this + # matters and why the old imperative path FAILED to keep the VM suspended: + # (a) The CR is created with idlePolicy.autoResumeEnabled=FALSE, so a suspended VM + # stays suspended. With autoResume=true (the old value) ANY hit to the VM + # endpoint auto-resumes it β€” and this loop used to curl /logs every 20s + # FOREVER, so the VM bounced back to RUNNING seconds after every suspend + # (observed in the Lambda console: never actually suspended). + # (b) Routing suspend through operatingMode is the whole point β€” it shows the + # Agent Sandbox CRD driving MicroVM scale-to-zero via the shim controller. + # So: once the coder pushes, set operatingMode=Suspended (controller suspends the + # VM), then switch to a lightweight CR-existence watch that NEVER touches the + # endpoint again. The VM's memory+disk persist; a fix round flips operatingMode + # back to Running (controller resumes) and the pipeline claims a fresh session. + echo "[microvm-bridge] Microvm ${MVM} running β€” pod now mirrors its lifecycle." + while true; do + # CR gone/terminating => real teardown => exit (cleanup trap handles CR delete). + ST=$(kubectl get microvm "${MVM}" -n "${NS}" -o jsonpath='{.status.state}' 2>/dev/null || echo "GONE") + case "${ST}" in + TERMINATED|TERMINATING|GONE|"") echo "[microvm-bridge] Microvm state=${ST:-gone} β€” exiting."; break ;; + esac + if [ -n "${EP:-}" ] && [ -n "${TOKEN:-}" ]; then + # Poll /logs for observability + to detect "coder pushed PR". + LOG=$(curl -sS -m 10 "https://${EP}/logs" -H "X-aws-proxy-auth: ${TOKEN}" 2>/dev/null \ + | python3 -c 'import json,sys; d=json.load(sys.stdin); print(d.get("log","")[-800:])' 2>/dev/null || echo "") + [ -n "${LOG}" ] && echo "[microvm-bridge] coder-log-tail: $(echo "$LOG" | tail -1)" + if echo "${LOG}" | grep -qiE 'PR opened|done β€” PR|status success'; then + # Coder is done β†’ request SUSPEND declaratively. The operator then DELETES this + # pod (operatingMode=Suspended), and the microvm-lifecycle controller reconciles + # the same intent into suspend-microvm. We EXIT right after setting it: the pod is + # about to be killed anyway, and exiting cleanly lets the cleanup trap run while + # the Sandbox still exists β†’ it KEEPS the CR (VM suspends, not terminates). We do + # NOT keep polling /logs β€” every endpoint hit would auto-resume the VM. + echo "[microvm-bridge] coder pushed PR β€” requesting SUSPEND via Sandbox.operatingMode (controller reconciles suspend-microvm ${VMID})" + kubectl patch sandbox "${SANDBOX_NAME}" -n "${NS}" --type merge \ + -p '{"spec":{"operatingMode":"Suspended"}}' >/dev/null 2>&1 \ + && echo "[microvm-bridge] operatingMode=Suspended set β€” exiting bridge (pod will be removed; CR + VM persist)" \ + || echo "[microvm-bridge] WARN: could not set operatingMode=Suspended" + break + fi + fi + sleep 20 + done +--- +apiVersion: extensions.agents.x-k8s.io/v1beta1 +kind: SandboxTemplate +metadata: + name: {{ .Values.warmPool.templateName }}-microvm + namespace: {{ include "agent-sandbox.namespace" . }} + labels: + {{- include "agent-sandbox.labels" . | nindent 4 }} + agent-sandbox.io/substrate: lambda-microvm +spec: + # Same env-injection contract as the Kata template so Flow B is unchanged. + envVarsInjectionPolicy: {{ .Values.coderTemplate.envVarsInjectionPolicy | default "Allowed" }} + podTemplate: + metadata: + labels: + {{- include "agent-sandbox.selectorLabels" . | nindent 8 }} + agent-sandbox.io/role: coder + agent-sandbox.io/substrate: lambda-microvm + spec: + # NO kata runtimeClass / nodeSelector / toleration β€” the bridge is a normal + # pod on an Auto-Mode node. The isolation boundary is the remote MicroVM. + serviceAccountName: microvm-bridge + # The bridge needs a k8s token (read platform image, patch Sandbox) AND AWS creds + # (Pod Identity association on this SA) to call the Lambda MicroVM SDK + # (RunMicrovm/GetMicrovm/TerminateMicrovm). The Kata coder is credential-less; the + # bridge is not, because RUN is imperative. + automountServiceAccountToken: true + securityContext: + runAsNonRoot: true + runAsUser: 1000 + runAsGroup: 1000 + fsGroup: 1000 + seccompProfile: + type: RuntimeDefault + containers: + # Named `coder` (NOT `bridge`) on purpose: Flow B's SandboxClaim injects env + # (DF_ISSUE_NUMBER, DF_REPO, …) into a container called `coder` β€” the SAME claim + # contract as the Kata substrate. The operator REJECTS the claim + # ("target container coder not found") if this name differs, so the substrate + # must expose a `coder` container to stay transparent to df-run. This container + # is still the bridge (runs bridge.sh β†’ RunMicrovm); only the name matches Kata. + - name: coder + image: {{ .Values.microvm.bridgeImage }} + command: ["/bin/sh", "/scripts/bridge.sh"] + # SANDBOX_NAME = this pod's own name via the downward API. agent-sandbox names + # the Sandbox CR and its pod identically, so this IS the owning Sandbox name. + # Without it bridge.sh fell back to df- (e.g. df-9999) which does NOT + # match the real Sandbox (named after the claim, e.g. df-issue-smoke-d), so the + # microvm-id annotation write silently failed β†’ the lifecycle controller couldn't + # find the VM to suspend and the pod teardown TERMINATED it instead of suspending. + env: + - name: SANDBOX_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + # NOTE: no preStop suspend marker needed anymore β€” cleanup() decides suspend-vs- + # teardown by whether the owning Sandbox still EXISTS (it survives Suspended, + # is gone on real teardown), which is race-free unlike a preStop /tmp marker. + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: ["ALL"] + resources: + # The bridge runs python3 (JSON), curl, a fetched kubectl, and an aws-cli v2 + # (glibc) in a poll loop β€” 128Mi OOMKilled it mid fix-round (bridge died before + # the coder's new commit landed β†’ await-coder spun forever). Give it real headroom. + requests: { cpu: 100m, memory: 256Mi } + limits: { cpu: "1", memory: 1Gi } + volumeMounts: + - name: bridge-script + mountPath: /scripts + - name: tmp + mountPath: /tmp + # GitHub token (read-only) the bridge folds into the runHookPayload so the + # coder in the MicroVM can push + open the PR. Same secret the Kata coder uses. + - name: gh-token + mountPath: /etc/df + readOnly: true + volumes: + - name: bridge-script + configMap: + name: microvm-bridge-script + defaultMode: 0555 + - name: tmp + emptyDir: {} + - name: gh-token + secret: + secretName: {{ .Values.microvm.githubSecretName | default "dark-factory-github" }} + defaultMode: 0400 + optional: true +--- +{{- /* +Lambda-MicroVM SandboxWarmPool. SandboxClaim.spec.warmPoolRef is REQUIRED (a claim +can't bind a bare template), so Flow D needs its own pool the df-run claim step +targets when the darkfactory-lambda label fires. Kept small (bridge pods are tiny); +each idle member is a bridge waiting to RunMicrovm (from the pre-built platform image) +on claim. +*/ -}} +apiVersion: extensions.agents.x-k8s.io/v1beta1 +kind: SandboxWarmPool +metadata: + name: {{ .Values.microvm.warmPool.name | default "coder-warmpool-microvm" }} + namespace: {{ include "agent-sandbox.namespace" . }} + labels: + {{- include "agent-sandbox.labels" . | nindent 4 }} + agent-sandbox.io/substrate: lambda-microvm +spec: + replicas: {{ .Values.microvm.warmPool.targetIdle | default 1 }} + sandboxTemplateRef: + name: {{ .Values.warmPool.templateName }}-microvm +{{- end }} diff --git a/gitops/addons/charts/agent-sandbox-lambda/templates/shim/30-microvm-lifecycle.yaml b/gitops/addons/charts/agent-sandbox-lambda/templates/shim/30-microvm-lifecycle.yaml new file mode 100644 index 00000000..bc4e9757 --- /dev/null +++ b/gitops/addons/charts/agent-sandbox-lambda/templates/shim/30-microvm-lifecycle.yaml @@ -0,0 +1,184 @@ +{{- if and .Values.microvm .Values.microvm.enabled }} +{{- /* +Flow D β€” microvm-lifecycle controller (suspend/resume via Sandbox.operatingMode). + +WHY THIS EXISTS: the Agent Sandbox CRD exposes `spec.operatingMode ∈ {Running, +Suspended}` (the declarative suspend/resume intent), but the ACK Microvm CR has NO +suspend field β€” its spec is create-time only; suspend/resume are IMPERATIVE SDK ops +(`suspend-microvm`/`resume-microvm`) that the ACK controller deliberately does NOT +reconcile. So switching operatingMode does nothing on its own. This tiny always-on +reconcile loop closes that gap: + + Sandbox.operatingMode: Running -> Suspended : aws lambda-microvms suspend-microvm + Sandbox.operatingMode: Suspended -> Running : aws lambda-microvms resume-microvm + +It resolves the MicroVM id from the MicrovmSandbox (KRO) status (microvmID). The +MicrovmSandbox is NOT deleted on suspend (only on claim teardown -> terminate), so +the VM survives suspend/resume cycles. This is a reconcile loop (not preStop hooks) +so it is robust to pod/node loss and resume needs no live pod β€” pure shim, no +virtual-kubelet, no new image (alpine/k8s = kubectl + aws cli), matching the +pool-manager/bridge pattern. Auth via EKS Pod Identity (empty SA annotations; the +pod-identity association granting lambda-microvms Suspend/Resume/Get is created by +the platform). +*/ -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: microvm-lifecycle + namespace: {{ include "agent-sandbox.namespace" . }} + labels: + {{- include "agent-sandbox.labels" . | nindent 4 }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: microvm-lifecycle + namespace: {{ include "agent-sandbox.namespace" . }} + labels: + {{- include "agent-sandbox.labels" . | nindent 4 }} +rules: + # Read Sandboxes (watch operatingMode + read the bridge-written microvm-id annotation) + # and patch them (stamp the last-acted mode to detect transitions). No microvmsandboxes + # access needed β€” the per-session VM id lives on the Sandbox, not a KRO status. + - apiGroups: ["agents.x-k8s.io"] + resources: ["sandboxes"] + verbs: ["get", "list", "watch", "patch", "update"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: microvm-lifecycle + namespace: {{ include "agent-sandbox.namespace" . }} + labels: + {{- include "agent-sandbox.labels" . | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: microvm-lifecycle +subjects: + - kind: ServiceAccount + name: microvm-lifecycle + namespace: {{ include "agent-sandbox.namespace" . }} +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: microvm-lifecycle-script + namespace: {{ include "agent-sandbox.namespace" . }} + labels: + {{- include "agent-sandbox.labels" . | nindent 4 }} +data: + reconcile.sh: | + #!/bin/sh + # Reconcile Sandbox.operatingMode -> Lambda MicroVM suspend/resume. + # Idempotent: we stamp the last-acted mode on an annotation and only act on change. + set -eu + # bridgeImage is aws-cli v2 (has lambda-microvms; alpine/k8s's aws-cli 1.34 does not) + # but has no kubectl β€” fetch a static one (same as bridge.sh / security-agent). + if ! command -v kubectl >/dev/null 2>&1; then + echo "[microvm-lifecycle] fetching kubectl..." + ARCH="$(uname -m)"; case "$ARCH" in aarch64|arm64) A=arm64;; *) A=amd64;; esac + KV="$(curl -fsSL https://dl.k8s.io/release/stable.txt)" + curl -fsSL "https://dl.k8s.io/release/${KV}/bin/linux/${A}/kubectl" -o /tmp/kubectl + chmod +x /tmp/kubectl; export PATH="/tmp:$PATH" + fi + NS="{{ include "agent-sandbox.namespace" . }}" + REGION="{{ .Values.microvm.region }}" + APIGROUP="{{ .Values.microvm.apiGroup | default "kro.run" }}" + ANN="microvm-lifecycle.agents.x-k8s.io/last-mode" + INTERVAL="{{ .Values.microvm.lifecycle.intervalSeconds | default 15 }}" + echo "[microvm-lifecycle] reconciling every ${INTERVAL}s (ns=${NS} region=${REGION})" + while true; do + # Select Sandboxes on the lambda-microvm substrate by the microvm-id ANNOTATION the + # bridge writes after RunMicrovm β€” NOT a label. The agent-sandbox operator does NOT + # propagate SandboxTemplate labels onto the Sandbox object, so a label selector + # (agent-sandbox.io/substrate=lambda-microvm) matches NOTHING and the controller + # stays blind to every real session (observed: operatingMode=Suspended set, Sandbox + # went SandboxSuspended, but the VM was never suspended because this loop skipped it). + # Only lambda sessions carry the microvm-id annotation, so it's the reliable signal. + for sb in $(kubectl get sandbox -n "$NS" \ + -o jsonpath='{range .items[?(@.metadata.annotations.microvm-lifecycle\.agents\.x-k8s\.io/microvm-id)]}{.metadata.name}{"\n"}{end}' 2>/dev/null); do + MODE=$(kubectl get sandbox "$sb" -n "$NS" -o jsonpath='{.spec.operatingMode}' 2>/dev/null || echo "Running") + LAST=$(kubectl get sandbox "$sb" -n "$NS" -o jsonpath="{.metadata.annotations.${ANN}}" 2>/dev/null || echo "") + [ "$MODE" = "$LAST" ] && continue # no transition + # Resolve THIS session's MicroVM id from the annotation the BRIDGE writes on the + # Sandbox after RunMicrovm (microvm-lifecycle.agents.x-k8s.io/microvm-id). There + # is no per-session MicrovmSandbox anymore β€” the VM is created imperatively by the + # bridge (SDK), so the id lives on the Sandbox, not in a KRO status. + VMID=$(kubectl get sandbox "$sb" -n "$NS" -o jsonpath='{.metadata.annotations.microvm-lifecycle\.agents\.x-k8s\.io/microvm-id}' 2>/dev/null || echo "") + if [ -z "$VMID" ]; then + echo "[microvm-lifecycle] $sb: mode=$MODE but no microvm-id annotation yet β€” will retry" + continue + fi + case "$MODE" in + Suspended) + echo "[microvm-lifecycle] $sb: Running->Suspended -> suspend-microvm $VMID" + aws lambda-microvms suspend-microvm --microvm-identifier "$VMID" --region "$REGION" 2>&1 || true + ;; + Running) + echo "[microvm-lifecycle] $sb: Suspended->Running -> resume-microvm $VMID" + aws lambda-microvms resume-microvm --microvm-identifier "$VMID" --region "$REGION" 2>&1 || true + ;; + *) + echo "[microvm-lifecycle] $sb: unknown operatingMode '$MODE' β€” skipping"; continue ;; + esac + # Stamp the mode we acted on so we don't repeat the call. + kubectl annotate sandbox "$sb" -n "$NS" "${ANN}=${MODE}" --overwrite >/dev/null 2>&1 || true + done + sleep "$INTERVAL" + done +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: microvm-lifecycle + namespace: {{ include "agent-sandbox.namespace" . }} + labels: + {{- include "agent-sandbox.labels" . | nindent 4 }} +spec: + replicas: 1 + selector: + matchLabels: + {{- include "agent-sandbox.selectorLabels" . | nindent 6 }} + app.kubernetes.io/component: microvm-lifecycle + template: + metadata: + labels: + {{- include "agent-sandbox.selectorLabels" . | nindent 8 }} + app.kubernetes.io/component: microvm-lifecycle + spec: + serviceAccountName: microvm-lifecycle + securityContext: + runAsNonRoot: true + runAsUser: 1000 + runAsGroup: 1000 + seccompProfile: + type: RuntimeDefault + containers: + - name: lifecycle + image: {{ .Values.microvm.bridgeImage }} + command: ["/bin/sh", "/scripts/reconcile.sh"] + env: + - name: AWS_REGION + value: {{ .Values.microvm.region | quote }} + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: ["ALL"] + resources: + requests: { cpu: 25m, memory: 64Mi } + limits: { cpu: 100m, memory: 128Mi } + volumeMounts: + - name: script + mountPath: /scripts + - name: tmp + mountPath: /tmp + volumes: + - name: script + configMap: + name: microvm-lifecycle-script + defaultMode: 0555 + - name: tmp + emptyDir: {} +{{- end }} diff --git a/gitops/addons/charts/agent-sandbox-lambda/templates/shim/40-kro-graph-rbac.yaml b/gitops/addons/charts/agent-sandbox-lambda/templates/shim/40-kro-graph-rbac.yaml new file mode 100644 index 00000000..7d13f124 --- /dev/null +++ b/gitops/addons/charts/agent-sandbox-lambda/templates/shim/40-kro-graph-rbac.yaml @@ -0,0 +1,74 @@ +{{/* + KRO graph child-resource RBAC. + + The MicrovmSandbox ResourceGraphDefinition (templates/image/) is reconciled by EKS + Managed KRO, whose controller authenticates to the API server as the cluster's KRO + capability role (EKS access entry, session name "KRO"). The AWS-managed + AmazonEKSKROPolicy attached to that access entry grants KRO its own kro.run perms and + discovery, but NOT create/update/delete on the ACK kinds this graph composes. So when + KRO tries to materialize the graph it fails: + + resource reconciliation failed: buckets.s3.services.k8s.aws "coder-microvm-artifacts" + is forbidden: User ".../hub-KROCapabilityRole/KRO" cannot get resource "buckets" ... + + This ClusterRole grants KRO CRUD on EXACTLY the three ACK groups the MicrovmSandbox + graph creates as children β€” s3 Buckets, iam Roles, and the self-managed lambdamicrovms + MicrovmImages/Microvms β€” and nothing else. K8s RBAC cannot scope list/watch/create by + resource NAME, so the grant is per-kind; it deliberately does NOT include core/*, apps, + rbac, secrets, or any other ACK service. delete is required so KRO can garbage-collect + the graph's children when a MicrovmSandbox is removed. + + Gated by microvm.enabled AND microvm.podIdentity.kroCapability.enabled. Rendered as + ArgoCD sync-wave -2 (with the controller bootstrap IAM) so KRO can watch/CRUD the + children before the platform MicrovmSandbox instance (wave 1) reconciles. +*/}} +{{- if and .Values.microvm .Values.microvm.enabled }} +{{- with .Values.microvm.kroCapability }} +{{- if .enabled }} +{{- $cluster := $.Values.microvm.podIdentity.clusterName }} +{{- $account := $.Values.microvm.accountId | toString }} +{{- $role := .roleName | default (printf "%s-KROCapabilityRole" $cluster) }} +{{- $session := .sessionName | default "KRO" }} +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ $cluster }}-kro-microvm-graph + labels: + {{- include "agent-sandbox.labels" $ | nindent 4 }} + annotations: + argocd.argoproj.io/sync-wave: "-2" +rules: + # S3 artifact bucket (graph resource `bucket`). + - apiGroups: ["s3.services.k8s.aws"] + resources: ["buckets", "buckets/status"] + verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] + # Build + exec IAM roles (graph resources `buildRole`, `execRole`). + - apiGroups: ["iam.services.k8s.aws"] + resources: ["roles", "roles/status"] + verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] + # Platform image + (future) per-session instance (graph resource `image`; Microvm is + # driven imperatively by the shim but kept here so KRO can read/GC if ever graphed). + - apiGroups: ["lambdamicrovms.services.k8s.aws"] + resources: ["microvmimages", "microvmimages/status", "microvms", "microvms/status"] + verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ $cluster }}-kro-microvm-graph + labels: + {{- include "agent-sandbox.labels" $ | nindent 4 }} + annotations: + argocd.argoproj.io/sync-wave: "-2" +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: {{ $cluster }}-kro-microvm-graph +subjects: + # The EKS access-entry username Managed KRO's controller presents. + - kind: User + name: "arn:aws:sts::{{ $account }}:assumed-role/{{ $role }}/{{ $session }}" + apiGroup: rbac.authorization.k8s.io +{{- end }} +{{- end }} +{{- end }} diff --git a/gitops/addons/charts/agent-sandbox-lambda/values.yaml b/gitops/addons/charts/agent-sandbox-lambda/values.yaml new file mode 100644 index 00000000..60009822 --- /dev/null +++ b/gitops/addons/charts/agent-sandbox-lambda/values.yaml @@ -0,0 +1,125 @@ +# agent-sandbox-lambda β€” Flow D (Lambda MicroVM substrate) values. +# +# Opt-in second substrate for the Agent Sandbox capability. Disabled by default; +# set microvm.enabled=true (per-cluster overlay) to render it. Runs in the SAME +# namespace as the Kata agent-sandbox chart and reuses the same operator + CRDs β€” +# it only ADDS the Lambda-MicroVM image build (KRO) + the bridge/lifecycle shim. + +# Namespace the capability runs in (must match the agent-sandbox chart). +namespace: agent-sandbox-system + +# Warm-pool template name stem (the bridge SandboxTemplate is -microvm, +# matching the Kata chart's convention so df-run's claim step resolves it). +warmPool: + templateName: coder-sandbox + +# SandboxTemplate env-injection policy (same contract as the Kata template so Flow B +# is unchanged). +coderTemplate: + envVarsInjectionPolicy: Allowed + +# ── Lambda MicroVM (Flow D) ────────────────────────────────────────────────── +microvm: + # Master gate β€” Flow D stays dormant until a cluster opts in. + enabled: false + region: us-west-2 + # API group the generated MicrovmSandbox CRD is served under (KRO schema.group). + # MUST be "kro.run": EKS Managed KRO's controller only watches the kro.run group β€” + # an RGD whose schema.group is anything else (even a *.kro.run subdomain) never leaves + # state=Inactive ("cache sync timeout ... Resource=microvmsandboxes"), because the + # capability's controller identity (hub-KROCapabilityRole via AmazonEKSKROPolicy) has + # no list/watch on other groups. The generated CRD is microvmsandboxes.kro.run; the + # kind (MicrovmSandbox) is unchanged. Verified by group-probe on the hub 2026-08-03. + apiGroup: kro.run + # Bridge/lifecycle pod image: needs the AWS CLI that KNOWS the pre-GA lambda-microvms + # service (RunMicrovm/suspend/resume/terminate) AND kubectl. alpine/k8s bundles kubectl + # but its aws-cli 1.34 does NOT have lambda-microvms (verified: prints the service list + # = unrecognized). So use the glibc, always-current aws-cli v2 (has lambda-microvms) and + # fetch kubectl at start (bridge.sh / lifecycle loop do this, same as the security-agent). + bridgeImage: public.ecr.aws/aws-cli/aws-cli:latest + + # K8s API-server egress for the bridge (microvm-bridge-apiserver-egress NetworkPolicy). + # The bridge needs :443 to the API server to read the MicrovmSandbox handoff + annotate + # the Sandbox, but the shared coder-egress policy denies the service+VPC CIDRs. These + # scope the additive allow to the API server only. apiServerCidr = the `kubernetes` + # service ClusterIP /32; apiServerEndpointCidrs = the VPC range holding the apiserver + # endpoint IPs. Override per cluster (kubectl get svc kubernetes; get endpoints kubernetes). + apiServerCidr: "172.20.0.1/32" + apiServerEndpointCidrs: + - "10.0.0.0/16" + + # AWS account id β€” used to construct the controller role ARN for the ACK + # PodIdentityAssociation (which takes an ARN, not a role-ref). Overlay per cluster. + accountId: "" + + # Controller bootstrap IAM (ALL-ACK: iam.services.k8s.aws Role + eks.services.k8s.aws + # PodIdentityAssociation β€” templates/shim/00-controller-pod-identity.yaml). This is + # the ONE IAM the KRO RGD can't self-create (creds-before-create). Managed by the + # Managed-ACK iam+eks controllers on the hub. The bridge/lifecycle SAs reuse this + # same role (they also call lambda-microvms). + podIdentity: + clusterName: hub + controllerNamespace: ack-system + controllerServiceAccount: ack-lambdamicrovms-controller + + # EKS Managed KRO runs its controller as the cluster's KRO capability role. On this + # cluster its k8s identity is the EKS access-entry username + # arn:aws:sts:::assumed-role/-KROCapabilityRole/KRO + # (session name "KRO"). AmazonEKSKROPolicy grants KRO its own kro.run perms but NOT + # CRUD on the ACK children the MicrovmSandbox RGD graph creates (s3 buckets, iam + # roles, lambdamicrovms images/instances) β€” so KRO's instance reconcile hits + # "forbidden: ... cannot get resource buckets". templates/shim/40-kro-graph-rbac.yaml + # grants exactly those child kinds to this identity. Override roleName/sessionName if + # your cluster's capability wiring differs (confirm via + # aws eks describe-access-entry --principal-arn .../KROCapabilityRole). + kroCapability: + enabled: true + roleName: "" # defaults to "-KROCapabilityRole" + sessionName: KRO + + # The ONE platform image built by KRO/ACK (10-rgd-microvm-image.yaml). Built ONCE + # per cluster; the shim reads its status (imageARN + executionRoleARN) to RunMicrovm + # per session. Lambda MicroVM is ARM_64-ONLY. codeArtifactUri is an S3 URI + # (s3://bucket/key) of a zip containing the coder app + a Dockerfile β€” NOT an ECR + # image ref (the Dockerfile inside MAY pull private ECR base layers; the build role + # keeps ecr:Get*/BatchGetImage). Publish the arm64 coder artifact before enabling. + # baseImageARN: arn:aws:lambda::aws:microvm-image:al2023-1 + # codeArtifactUri: s3:///dark-factory-coder--arm64.zip + baseImageARN: "" + codeArtifactUri: "" + image: + # Render the single committed MicrovmSandbox instance that triggers the build. + enabled: true + # Name of that platform image object; the bridge reads its status by this name. + name: coder + + # Idle policy for RunMicrovm (auto-suspend/resume). EXPLICIT suspend/resume across + # the reviewβ†’fix loop is driven by the microvm-lifecycle controller off + # Sandbox.operatingMode β€” see lifecycle below and project_flow_d_lifecycle memory: + # coder codes β†’ SUSPEND β†’ agents review β†’ (fix findings) β†’ RESUME same VM β†’ + # … loop until cleared β†’ merge/exit β†’ TERMINATE. + defaults: + # maxIdleDurationSeconds: how long a RUNNING VM may sit idle (no inbound) before the + # runtime suspends it. Must exceed a coder run (a few min) β€” the bridge suspends + # explicitly anyway, this is just a backstop. + maxIdleDurationSeconds: 1800 + # suspendedDurationSeconds: how long a SUSPENDED VM is kept before the runtime + # AUTO-TERMINATES it. THIS IS CRITICAL for the reviewβ†’fix loop: the VM is suspended + # while the external review agents run (~8–15 min) and then waits for a human to post + # "fix findings" (minutes to hours). At the old 300s (5 min) the VM was ALWAYS + # auto-terminated before the fix round, so RESUME hit "has been terminated and its + # state cannot be changed" and the same-VM warm-resume was impossible. Keep it + # suspended long enough to span a realistic review+human cycle (24h); teardown + # terminates it explicitly at merge, so this only bounds abandoned PRs. + suspendedDurationSeconds: 86400 + + # microvm-lifecycle controller (30-microvm-lifecycle.yaml) β€” reconciles + # Sandbox.operatingMode Running↔Suspended β†’ suspend/resume-microvm. Loop interval. + lifecycle: + intervalSeconds: 15 + + # Lambda-MicroVM warm pool the df-run claim binds when the darkfactory-lambda label + # fires (Flow D). Small β€” bridge pods are tiny (the real coder runs in the MicroVM). + warmPool: + name: coder-warmpool-microvm + targetIdle: 1 diff --git a/gitops/addons/charts/agent-sandbox/Chart.yaml b/gitops/addons/charts/agent-sandbox/Chart.yaml new file mode 100644 index 00000000..1f8e75da --- /dev/null +++ b/gitops/addons/charts/agent-sandbox/Chart.yaml @@ -0,0 +1,26 @@ +apiVersion: v2 +name: agent-sandbox +description: >- + Agent Sandbox capability for the Open Agent Platform β€” hardware-isolated + Kata micro-VM sandboxes (agents.x-k8s.io Sandbox CRD) with a pre-warmed pool + kept ready by a pool-manager controller. The reusable isolation substrate any + agent workload (e.g. the Dark Factory coding pipeline) can claim on demand. +type: application +version: 0.1.0 +appVersion: "0.1.0" +keywords: + - kata + - sandbox + - isolation + - micro-vm + - dark-factory +sources: + - https://github.com/aws-samples/sample-open-agentic-platform + - https://github.com/elamaran11/eks-platform-openclaw +maintainers: + - name: Open Agent Platform +# NOTE: kata-deploy (the Kata runtime installer) is NOT bundled as a subchart +# dependency β€” an OCI subchart dep would block this chart from rendering when +# unbuilt and is fragile under ArgoCD. Instead it is delivered as a separate, +# gated ArgoCD Application (enable_agent_sandbox_kata) that pulls the upstream +# OCI chart directly. See the addon catalog + docs/dark-factory Β§12a. diff --git a/gitops/addons/charts/agent-sandbox/README.md b/gitops/addons/charts/agent-sandbox/README.md new file mode 100644 index 00000000..18c5a739 --- /dev/null +++ b/gitops/addons/charts/agent-sandbox/README.md @@ -0,0 +1,56 @@ +# agent-sandbox + +**Flow A of the [Dark Factory pattern](../../../../docs/dark-factory/README.md).** +Hardware-isolated **Kata micro-VM sandboxes** (`agents.x-k8s.io` Sandbox CRD) with a **pre-warmed +pool** kept ready by a pool-manager controller β€” the reusable isolation substrate any agent +workload (notably the Dark Factory coding pipeline) can claim on demand. + +## What it installs + +| Template | Resource | Purpose | +|---|---|---| +| `00-namespace.yaml` | Namespace (PSS `restricted`) | Runs the capability; enforces restricted Pod Security | +| `05-operator.yaml` | SA + ClusterRole + StatefulSet + Service | The `agents.x-k8s.io` controller (materializes a Kata-VM pod per Sandbox) | +| `10-runtimeclasses.yaml` | RuntimeClass Γ—N | `kata-clh` (default), `kata-qemu` β€” steer pods onto kata Karpenter pools | +| `20-sandboxtemplate.yaml` | SandboxTemplate | The coder pod spec the warm pool clones (isolation invariants baked in) | +| `30-networkpolicy.yaml` | NetworkPolicy | Default-deny egress β†’ DNS + Bifrost + HTTPS only (breaks the lethal trifecta) | +| `40-poolmanager-rbac.yaml` | SA + Role + RoleBinding | Narrowly-scoped RBAC for the pool-manager | +| `41-poolmanager-cronjob.yaml` | CronJob | Reconciles the warm buffer: refill / scale-to-zero / reap | + +## Enable + +```yaml +# gitops/overlays/environments/{dev,prod}/enabled-addons.yaml +enabledAddons: + agent_sandbox: true +``` + +The ApplicationSet cluster-generator (sync-wave 2) fans the chart onto any cluster carrying the +`enable_agent_sandbox` label. Installed on **spoke-dev and spoke-prod**; the Dark Factory pipeline +only *runs* on spoke-dev (prod pool stays dormant). + +## Key values + +| Value | Default | Purpose | +|---|---|---| +| `kata.defaultRuntimeClass` | `kata-clh` | VMM for coder sandboxes | +| `warmPool.targetIdle` | `3` | Idle sandboxes kept ready | +| `warmPool.idleScaleToZeroSeconds` | `900` | Idle β†’ `replicas:0` (PVC kept) | +| `warmPool.reapAfterSeconds` | `3600` | Reap abandoned claimed sandboxes | +| `coderTemplate.bifrostUrl` | `http://bifrost.bifrost.svc.cluster.local:8080` | LLM gateway (Bifrost, not LiteLLM) | + +## Prerequisites + +- **Kata runtime installed on nodes** (via `kata-deploy` + kata Karpenter pools from the base + platform / `eks-platform-openclaw`). +- **Sandbox CRDs** (`Sandbox`, `SandboxTemplate`, `SandboxClaim`) applied at an earlier sync-wave + from the upstream operator bundle (`public.ecr.aws/t6v6o5d5/agent-sandbox:v0.1.0`) β€” the ~4k-line + OpenAPI schema is not vendored into this chart. +- **Bifrost** LLM gateway reachable at the configured URL. + +## Notes + +- The pool-manager is a CronJob (kubectl + jq reconcile loop) for a dependency-free reference + implementation; swap for a real controller if reconcile latency matters. +- `warmPool.enabled=false` removes the pool-manager entirely (operator + RuntimeClasses + template + remain, for manual/consumer-driven claims). diff --git a/gitops/addons/charts/agent-sandbox/nodepool/README.md b/gitops/addons/charts/agent-sandbox/nodepool/README.md new file mode 100644 index 00000000..80772fa0 --- /dev/null +++ b/gitops/addons/charts/agent-sandbox/nodepool/README.md @@ -0,0 +1,143 @@ +# Kata node pool provisioning + +The `agent-sandbox` chart installs the Sandbox operator, RuntimeClasses, coder +template, and pool-manager β€” but Kata needs **hardware-virtualization nodes** to +actually run micro-VMs. On an **EKS Auto Mode** cluster (like the spokes), Auto +Mode's managed Bottlerocket nodes can't host Kata, so we add a **self-managed +nested-virt Managed Node Group** alongside Auto Mode. Coexistence + `/dev/kvm` +were validated by a live spike (see [`docs/dark-factory` Β§12a](../../../../../docs/dark-factory/README.md)). + +## βœ… Now GitOps-managed (Crossplane) β€” this is the default path + +The node group is a **first-class GitOps resource** β€” ArgoCD owns the node-group +INFRA end-to-end, no out-of-band `eksctl`/`terraform apply`. It's rendered by the +chart's `templates/` as Crossplane managed resources: + +| Template | Resource | +|---|---| +| `../templates/16-kata-launch-template.yaml` | Crossplane `LaunchTemplate` β€” `cpuOptions.nestedVirtualization=enabled` + nodeadm userData | +| `../templates/17-kata-nodegroup.yaml` | Crossplane `Nodegroup` β€” scale-to-zero, kata taint/labels, LT ref | +| `../templates/18-kata-eks-addons.yaml` | Crossplane `Addon` β€” vpc-cni + kube-proxy (the two Auto-Mode prerequisites) | + +**Enable it** β€” the chart default (`../values.yaml`) keeps `nodepool` cluster-agnostic; +the hub-specific coordinates live in the **per-cluster overlay** the addon +ApplicationSet already layers on (`valueFiles: clusters//addons`): + +``` +gitops/addons/clusters/hub/addons/agent-sandbox/values.yaml +``` + +```yaml +nodepool: + enabled: true # provision/adopt the kata MNG + manageAddons: true # also adopt vpc-cni + kube-proxy (skip if base platform owns them) + clusterName: hub # + clusterEndpoint / clusterCA / serviceCidr, subnetIds, + # nodeRoleArn, amiId, instanceType, launchTemplateId, region +``` + +> `clusterEndpoint` + `clusterCA` are **not secrets** β€” the CA is the cluster's +> PUBLIC api-server certificate (no private key) and the endpoint is public DNS; +> both ship in every kubeconfig. They live in the overlay (not the chart default) +> for reusability. They're baked into the LaunchTemplate `userData` at Helm render +> time, so a k8s Secret/env can't feed them. Dropping the custom `amiId` (letting +> EKS auto-inject the bootstrap) would remove them from git entirely β€” a possible +> future refactor. + +Requires the Crossplane AWS providers `provider-aws-eks` + `provider-aws-ec2` +(installed on the hub) and a `ProviderConfig` (default: `default`). + +**Adoption:** on a cluster that already has a kata node group / LT / addons (e.g. +from the earlier manual path), the templates carry `crossplane.io/external-name` +annotations that **import the existing resources in place** β€” no recreate, no node +churn. Set `launchTemplateId` + `nodegroupName` to the existing ids. + +## Reference files (superseded by the Crossplane templates above) + +Kept for reference / non-GitOps or air-gapped setups; **not** applied by ArgoCD: + +| File | Purpose | +|---|---| +| `kata-mng-eksctl.yaml` | eksctl `ClusterConfig` to add the kata MNG (declarative, simplest) | +| `kata-mng.tf` | Terraform launch template (`cpu_options.nested_virtualization=enabled`) + MNG. `terraform validate` passes. | +| `kata-mng-launch-template-userdata.mime` | The AL2023 **nodeadm MIME** userData (modprobe kvm_intel + join). | + +## βœ… PROVEN END-TO-END on EKS Auto Mode (spoke-dev, 2026-07-10) + +Kata micro-VMs **do run on an Auto Mode cluster** via a self-managed nested-virt MNG. +Verified with a pod under `runtimeClassName: kata-clh`: + +| | Value | +|---|---| +| Pod kernel (`uname -r` inside) | **`6.18.35`** β€” the Kata guest kernel | +| Host node kernel | `6.12.90-…amzn2023` | + +Different kernels β‡’ the pod ran in a **real VM with hardware isolation**, not a container. + +### The two Auto-Mode-specific prerequisites (the crux) + +Auto Mode's built-in networking applies ONLY to its own managed nodes. A self-managed +MNG node has **neither the CNI nor kube-proxy** that pods need β€” so you MUST install +both EKS addons, or pods on the kata node can't reach the API server: + +``` +aws eks create-addon --cluster-name --addon-name vpc-cni --resolve-conflicts OVERWRITE +aws eks create-addon --cluster-name --addon-name kube-proxy --resolve-conflicts OVERWRITE +``` + +- **Without `vpc-cni`** β†’ node stays `NotReady` (`cni plugin not initialized`). +- **Without `kube-proxy`** β†’ the node has no iptables rules for the `kubernetes.default.svc` + (172.20.0.1) service IP, so **kata-deploy crashloops** with `Failed to get node ... + client error (Connect)` β€” it connects to the API via the in-cluster service and times + out. This was the real blocker (NOT the containerd restart, and not the nydus + snapshotter β€” both were red herrings). Installing kube-proxy fixed it; kata-deploy then + reached `1/1 Running` with **zero restarts** and installed the runtime cleanly. + +Both `aws-node` and `kube-proxy` tolerate all taints (`operator: Exists`), so they land +on the tainted kata node automatically. + +### The startup-taint gate (from openclaw PR #10) + +The kata node registers with **two** taints: +- `kata=true:NoSchedule` β€” workload taint (only kata pods run here) +- `katacontainers.io/runtime-not-ready=true:NoSchedule` β€” **startup taint**; blocks all + workloads until the runtime is installed. Set via nodeadm + `--register-with-taints`. The **`kata-readiness` DaemonSet** watches kata-deploy's + `/readyz` and removes this taint once install completes β€” proven to work here + (`node ... untainted` in its log). + +### IAM access-entry gotcha (lesson from this test) + +If you recreate the node IAM role, its principal ID changes β€” **delete and recreate the +EKS access entry** (`type EC2_LINUX`) or the node's kubelet gets `Unauthorized` and never +registers. A stale access entry pointing at an old role ID is silent and confusing. + +## Enablement sequence (per kata-capable cluster, e.g. spoke-dev) + +1. **Provision the kata MNG** β€” apply the eksctl or Terraform manifest here. Nodes + come up tainted `kata=true:NoSchedule`, labeled `kata-enabled=true`, with + `/dev/kvm` (nested-virt) and `min=0` scale-to-zero. On Auto Mode, ensure the + `vpc-cni` addon is installed (see prerequisite above) or the node stays NotReady. +2. **Install the runtime** β€” label the cluster secret `enable_agent_sandbox_kata=true` + so the `kata-deploy` ArgoCD app (sync-wave 1) installs the containerd handlers. +3. **Install the capability** β€” label `enable_agent_sandbox=true` so the + `agent-sandbox` app (sync-wave 2) installs the operator + RuntimeClasses + + template + pool-manager. +4. **Enable the pool** β€” `warmPool.enabled=true` (default) pre-warms idle sandboxes. + +> Both labels are set via the environment overlays (`gitops/overlays/environments/dev`). +> They are commented out today until a kata MNG exists on the spokes. + +## Cost note + +Nested-virt `c8i`/`m8i` nodes are more expensive than the `c6*` Auto Mode +defaults. `min=0` scale-to-zero + the pool-manager's idle scale-down keep cost +proportional to actual sandbox activity β€” you pay for kata nodes only while a +sandbox is claimed/warming. + +## Teardown (spike lesson #2) + +Delete the **MNG first and let it drain** (set `min/desired=0` beforehand). Don't +terminate the instance out from under the MNG β€” the ASG respawns and can wedge +the delete on a `Pending:Wait` lifecycle hook. Recover with +`aws autoscaling terminate-instance-in-auto-scaling-group` + +`complete-lifecycle-action`. diff --git a/gitops/addons/charts/agent-sandbox/nodepool/kata-mng-eksctl.yaml b/gitops/addons/charts/agent-sandbox/nodepool/kata-mng-eksctl.yaml new file mode 100644 index 00000000..ebbca9bd --- /dev/null +++ b/gitops/addons/charts/agent-sandbox/nodepool/kata-mng-eksctl.yaml @@ -0,0 +1,50 @@ +# Self-managed nested-virt Managed Node Group for Kata sandboxes, added to an +# existing EKS Auto Mode cluster (spoke-dev). Auto Mode keeps running everything +# else; kata sandboxes schedule here via the kata=true taint. Validated by the +# 2026-07-10 spike (see docs/dark-factory Β§12a). +# +# Apply: eksctl create nodegroup -f kata-mng-eksctl.yaml +# Requires eksctl with EKS Auto Mode coexistence support and an AMI/instance +# that exposes nested virtualization (c8i/m8i). +apiVersion: eksctl.io/v1alpha5 +kind: ClusterConfig + +metadata: + name: spoke-dev # target cluster + region: us-west-2 + +managedNodeGroups: + - name: kata-sandbox + # Nested-virt Intel instances (expose VT-x via CpuOptions.NestedVirtualization). + instanceTypes: ["c8i.4xlarge", "m8i.4xlarge"] + amiFamily: AmazonLinux2023 + minSize: 0 # scale-to-zero when no sandbox is claimed + maxSize: 3 + desiredCapacity: 1 + privateNetworking: true + volumeSize: 100 + # Labels applied at node birth: kata-enabled is the STATIC label kata-deploy + # targets; katacontainers.io/kata-runtime is (re)emitted by kata-deploy. + labels: + kata-enabled: "true" + katacontainers.io/kata-runtime: "true" + node-type: kata-mng + # Only kata sandboxes tolerate this taint β€” keeps general workloads off. + taints: + - key: kata + value: "true" + effect: NoSchedule + # nested virtualization + the modprobe/nodeadm userData are supplied via a + # launch template (eksctl overrideBootstrapCommand cannot set CpuOptions). + # See kata-mng-launch-template-userdata.mime and the Terraform variant. + # launchTemplate: + # id: lt-xxxxxxxx + tags: + platform: open-agent-platform + capability: agent-sandbox + iam: + attachPolicyARNs: + - arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy + - arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy + - arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly + - arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore diff --git a/gitops/addons/charts/agent-sandbox/nodepool/kata-mng-launch-template-userdata.mime b/gitops/addons/charts/agent-sandbox/nodepool/kata-mng-launch-template-userdata.mime new file mode 100644 index 00000000..96e8b1c9 --- /dev/null +++ b/gitops/addons/charts/agent-sandbox/nodepool/kata-mng-launch-template-userdata.mime @@ -0,0 +1,48 @@ +MIME-Version: 1.0 +Content-Type: multipart/mixed; boundary="//" + +--// +Content-Type: text/x-shellscript; charset="us-ascii" + +#!/bin/bash +# Load the KVM module so /dev/kvm exists BEFORE the node goes Ready. +# cpuOptions.NestedVirtualization on the launch template only makes VT-x +# *visible*; the stock AL2023 image does not auto-load kvm_intel. Persist it so +# it survives reboots. (Lesson from the spike: setting nested-virt without this +# gives a node with no /dev/kvm.) +modprobe kvm_intel +printf 'kvm\nkvm_intel\n' > /etc/modules-load.d/kvm.conf + +--// +Content-Type: application/node.eks.aws + +# nodeadm NodeConfig β€” this is what actually joins the node to the cluster. +# CRITICAL (spike lesson #1): do NOT replace this with a plain-bash bootstrap or +# a custom AMI without nodeadm β€” the node will boot with /dev/kvm but never +# register with the control plane. +# +# LESSON #3 (live test): when you supply a CUSTOM ImageId in the launch template, +# nodeadm does NOT auto-discover the cluster API endpoint/CA β€” you must set +# apiServerEndpoint, certificateAuthority, and cidr explicitly, or nodeadm fails +# with "Apiserver endpoint is missing in cluster configuration". (The default +# EKS AMI path injects these for you; a custom AMI does not.) Substitute the +# cluster name + the three cluster values at render: +# aws eks describe-cluster --name \ +# --query 'cluster.{e:endpoint,ca:certificateAuthority.data,cidr:kubernetesNetworkConfig.serviceIpv4Cidr}' +apiVersion: node.eks.aws/v1alpha1 +kind: NodeConfig +spec: + cluster: + name: CLUSTER_NAME_PLACEHOLDER + apiServerEndpoint: API_SERVER_ENDPOINT_PLACEHOLDER + certificateAuthority: CERTIFICATE_AUTHORITY_DATA_PLACEHOLDER + cidr: SERVICE_IPV4_CIDR_PLACEHOLDER + kubelet: + flags: + # Labels + taint applied at node birth so kata-deploy schedules and the + # RuntimeClass nodeSelector matches. kata-enabled is the STATIC label + # kata-deploy targets; katacontainers.io/kata-runtime is emitted by + # kata-deploy after install. + - "--node-labels=kata-enabled=true,katacontainers.io/kata-runtime=true,node-type=kata-mng" + - "--register-with-taints=kata=true:NoSchedule" +--//-- diff --git a/gitops/addons/charts/agent-sandbox/nodepool/kata-mng.tf b/gitops/addons/charts/agent-sandbox/nodepool/kata-mng.tf new file mode 100644 index 00000000..b70e7348 --- /dev/null +++ b/gitops/addons/charts/agent-sandbox/nodepool/kata-mng.tf @@ -0,0 +1,127 @@ +# Terraform variant: nested-virt Kata Managed Node Group + launch template for +# an existing EKS Auto Mode cluster. Use this path when you need the launch +# template's CpuOptions.NestedVirtualization flag (eksctl cannot set it). +# Validated by the 2026-07-10 spike (docs/dark-factory Β§12a). +# +# Requires: aws provider (nested_virtualization support), AWS API/CLI vintage +# that exposes cpu_options.nested_virtualization. + +variable "cluster_name" { + type = string + default = "spoke-dev" +} +variable "region" { + type = string + default = "us-west-2" +} +variable "subnet_ids" { + type = list(string) +} +variable "node_role_arn" { + type = string +} +variable "instance_type" { + type = string + default = "c8i.4xlarge" +} +variable "kata_max_size" { + type = number + default = 3 +} + +# EKS-optimized AL2023 AMI for the cluster's k8s version (nodeadm bootstrap). +data "aws_ssm_parameter" "al2023" { + name = "/aws/service/eks/optimized-ami/1.35/amazon-linux-2023/x86_64/standard/recommended/image_id" +} + +# Cluster endpoint/CA/cidr β€” REQUIRED in the NodeConfig when using a custom AMI +# (lesson #3, live test): nodeadm does not auto-discover these with a custom +# ImageId and fails with "Apiserver endpoint is missing in cluster configuration". +data "aws_eks_cluster" "this" { + name = var.cluster_name +} + +locals { + # MIME userData: modprobe kvm_intel (so /dev/kvm exists) + nodeadm NodeConfig + # that joins the cluster with the kata labels/taint. See Β§12a lessons #1 + #3. + kata_userdata = base64encode(<<-MIME + MIME-Version: 1.0 + Content-Type: multipart/mixed; boundary="//" + + --// + Content-Type: text/x-shellscript; charset="us-ascii" + + #!/bin/bash + modprobe kvm_intel + printf 'kvm\nkvm_intel\n' > /etc/modules-load.d/kvm.conf + + --// + Content-Type: application/node.eks.aws + + apiVersion: node.eks.aws/v1alpha1 + kind: NodeConfig + spec: + cluster: + name: ${var.cluster_name} + apiServerEndpoint: ${data.aws_eks_cluster.this.endpoint} + certificateAuthority: ${data.aws_eks_cluster.this.certificate_authority[0].data} + cidr: ${data.aws_eks_cluster.this.kubernetes_network_config[0].service_ipv4_cidr} + kubelet: + flags: + - "--node-labels=kata-enabled=true,katacontainers.io/kata-runtime=true,node-type=kata-mng" + - "--register-with-taints=kata=true:NoSchedule" + --//-- + MIME + ) +} + +resource "aws_launch_template" "kata" { + name_prefix = "${var.cluster_name}-kata-" + image_id = data.aws_ssm_parameter.al2023.value + instance_type = var.instance_type + user_data = local.kata_userdata + + cpu_options { + # THE key flag β€” exposes VT-x on 8i instances so kata's VMM can run guests. + nested_virtualization = "enabled" + } + + tag_specifications { + resource_type = "instance" + tags = { platform = "open-agent-platform", capability = "agent-sandbox" } + } +} + +resource "aws_eks_node_group" "kata" { + cluster_name = var.cluster_name + node_group_name = "kata-sandbox" + node_role_arn = var.node_role_arn + subnet_ids = var.subnet_ids + + # Scale-to-zero when no sandbox is claimed; pool-manager / consumer scales up. + scaling_config { + min_size = 0 + desired_size = 1 + max_size = var.kata_max_size + } + + launch_template { + id = aws_launch_template.kata.id + version = aws_launch_template.kata.latest_version + } + + # Only kata sandboxes tolerate this β€” keeps general workloads off the pool. + taint { + key = "kata" + value = "true" + effect = "NO_SCHEDULE" + } + + labels = { + "kata-enabled" = "true" + "katacontainers.io/kata-runtime" = "true" + "node-type" = "kata-mng" + } + + tags = { platform = "open-agent-platform", capability = "agent-sandbox" } +} diff --git a/gitops/addons/charts/agent-sandbox/templates/00-namespace.yaml b/gitops/addons/charts/agent-sandbox/templates/00-namespace.yaml new file mode 100644 index 00000000..f62077c0 --- /dev/null +++ b/gitops/addons/charts/agent-sandbox/templates/00-namespace.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: {{ include "agent-sandbox.namespace" . }} + labels: + {{- include "agent-sandbox.labels" . | nindent 4 }} + # PSS = baseline (NOT restricted). This namespace hosts BOTH the coder + # sandboxes AND the agent-sandbox operator/kata-readiness β€” and the upstream + # operator + kata tooling are not restricted-compliant (they'd be blocked). + # The coder sandboxes get their real isolation from the KATA micro-VM + # boundary + the restricted securityContext baked into the SandboxTemplate + # pod spec β€” not from namespace PSS. (Live-test lesson: restricted here + # blocks the v0.5.1 controller pod entirely.) + pod-security.kubernetes.io/enforce: baseline + pod-security.kubernetes.io/audit: restricted + pod-security.kubernetes.io/warn: restricted diff --git a/gitops/addons/charts/agent-sandbox/templates/10-runtimeclasses.yaml b/gitops/addons/charts/agent-sandbox/templates/10-runtimeclasses.yaml new file mode 100644 index 00000000..cf7e3e46 --- /dev/null +++ b/gitops/addons/charts/agent-sandbox/templates/10-runtimeclasses.yaml @@ -0,0 +1,22 @@ +{{- /* +Kata RuntimeClasses. A workload selects a VMM with `runtimeClassName: kata-clh` +(or kata-qemu). The RuntimeClass admission controller force-merges the +scheduling.nodeSelector + tolerations below onto the pod, steering it onto the +matching Karpenter kata pool. The runtime binaries themselves are installed on +the node by kata-deploy (a prerequisite provided by the base platform). +*/ -}} +{{- range .Values.kata.runtimeClasses }} +--- +apiVersion: node.k8s.io/v1 +kind: RuntimeClass +metadata: + name: {{ .name }} + labels: + {{- include "agent-sandbox.labels" $ | nindent 4 }} +handler: {{ .handler }} +scheduling: + nodeSelector: + {{- toYaml $.Values.kata.nodeSelector | nindent 4 }} + tolerations: + {{- toYaml $.Values.kata.tolerations | nindent 4 }} +{{- end }} diff --git a/gitops/addons/charts/agent-sandbox/templates/15-kata-readiness.yaml b/gitops/addons/charts/agent-sandbox/templates/15-kata-readiness.yaml new file mode 100644 index 00000000..91f2b824 --- /dev/null +++ b/gitops/addons/charts/agent-sandbox/templates/15-kata-readiness.yaml @@ -0,0 +1,113 @@ +{{- if .Values.kataReadiness.enabled }} +{{- /* +kata-readiness β€” removes the katacontainers.io/runtime-not-ready STARTUP TAINT +once kata-deploy finishes installing the runtime on a node. + +Why needed (live-test lesson): kata nodes register with a runtime-not-ready +startup taint so no workload binds before the runtime exists. kata-deploy does +NOT remove that taint itself. This DaemonSet watches the kata-deploy pod on its +node reach Ready (its /readyz flips 503β†’200 only after install completes), then +removes the taint so kata sandboxes can schedule. Ported from +eks-platform-openclaw PR #10. +*/ -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: kata-readiness + namespace: {{ include "agent-sandbox.namespace" . }} + labels: + {{- include "agent-sandbox.labels" . | nindent 4 }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: agent-sandbox-kata-readiness + labels: + {{- include "agent-sandbox.labels" . | nindent 4 }} +rules: + - apiGroups: [""] + resources: ["nodes"] + verbs: ["get", "list", "patch"] + - apiGroups: [""] + resources: ["pods"] + verbs: ["get", "list"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: agent-sandbox-kata-readiness + labels: + {{- include "agent-sandbox.labels" . | nindent 4 }} +subjects: + - kind: ServiceAccount + name: kata-readiness + namespace: {{ include "agent-sandbox.namespace" . }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: agent-sandbox-kata-readiness +--- +apiVersion: apps/v1 +kind: DaemonSet +metadata: + name: kata-readiness + namespace: {{ include "agent-sandbox.namespace" . }} + labels: + {{- include "agent-sandbox.labels" . | nindent 4 }} +spec: + selector: + matchLabels: + app: kata-readiness + template: + metadata: + labels: + app: kata-readiness + {{- include "agent-sandbox.selectorLabels" . | nindent 8 }} + spec: + serviceAccountName: kata-readiness + # Tolerate everything so it lands BEFORE the runtime is ready β€” including + # the kata workload taint and the runtime-not-ready startup taint it removes. + tolerations: + - operator: Exists + # Static label present from node birth (kata-deploy emits + # katacontainers.io/kata-runtime only after install, so we can't select on it). + nodeSelector: + kata-enabled: "true" + securityContext: + runAsNonRoot: true + runAsUser: 1000 + seccompProfile: + type: RuntimeDefault + containers: + - name: kata-readiness + image: {{ .Values.kataReadiness.image }} + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: ["ALL"] + env: + - name: NODE_NAME + valueFrom: + fieldRef: + fieldPath: spec.nodeName + command: + - /bin/sh + - -c + - | + set -eu + echo "[kata-readiness] node=${NODE_NAME}: waiting for kata-deploy Ready..." + until [ "$(kubectl get pods -n kube-system -l name=kata-deploy \ + --field-selector "spec.nodeName=${NODE_NAME}" \ + -o jsonpath='{.items[0].status.conditions[?(@.type=="Ready")].status}')" = "True" ]; do + sleep 5 + done + echo "[kata-readiness] kata-deploy Ready; removing runtime-not-ready taint..." + kubectl taint node "${NODE_NAME}" katacontainers.io/runtime-not-ready:NoSchedule- || true + echo "[kata-readiness] node ${NODE_NAME} ready for kata workloads." + sleep infinity + resources: + requests: + cpu: 5m + memory: 32Mi +{{- end }} diff --git a/gitops/addons/charts/agent-sandbox/templates/16-kata-launch-template.yaml b/gitops/addons/charts/agent-sandbox/templates/16-kata-launch-template.yaml new file mode 100644 index 00000000..27622d24 --- /dev/null +++ b/gitops/addons/charts/agent-sandbox/templates/16-kata-launch-template.yaml @@ -0,0 +1,50 @@ +{{- if and .Values.nodepool .Values.nodepool.enabled }} +{{- /* +Kata nested-virt Launch Template β€” GitOps-managed via Crossplane (provider-aws-ec2). + +Kata micro-VMs need HARDWARE virtualization, which EKS Auto Mode's managed +Bottlerocket nodes can't provide. This LaunchTemplate carries the ONE flag that +makes it work β€” cpuOptions.nestedVirtualization=enabled (exposes VT-x on 8i +instances) β€” plus the AL2023 nodeadm userData that loads kvm_intel and joins the +cluster with the kata labels + startup taint. + +Previously this was applied out-of-band (nodepool/kata-mng.tf | eksctl). It is now +a first-class Crossplane managed resource so ArgoCD owns it end-to-end. + +ADOPTION: set nodepool.launchTemplateId to the existing LT id and Crossplane +imports it in place (external-name) rather than creating a duplicate. +*/ -}} +apiVersion: ec2.aws.upbound.io/v1beta2 +kind: LaunchTemplate +metadata: + name: {{ .Values.nodepool.clusterName }}-kata + labels: + {{- include "agent-sandbox.labels" . | nindent 4 }} + annotations: + {{- if .Values.nodepool.launchTemplateId }} + # Adopt the existing launch template in place (no duplicate). + crossplane.io/external-name: {{ .Values.nodepool.launchTemplateId | quote }} + {{- end }} +spec: + # ArgoCD-managed: let Crossplane reconcile toward this spec, but don't fight + # drift on late-binding fields AWS fills in. + forProvider: + region: {{ .Values.nodepool.region | quote }} + namePrefix: {{ .Values.nodepool.clusterName }}-kata- + imageId: {{ .Values.nodepool.amiId | quote }} + instanceType: {{ .Values.nodepool.instanceType | quote }} + # THE key flag β€” hardware nested virtualization for the Kata VMM. + cpuOptions: + - nestedVirtualization: enabled + # nodeadm MIME userData: modprobe kvm_intel (so /dev/kvm exists) + NodeConfig + # that joins the cluster with the kata labels + the two taints (workload + + # startup-not-ready gate that the kata-readiness DaemonSet later removes). + userData: {{ include "agent-sandbox.kataUserData" . | b64enc | quote }} + tagSpecifications: + - resourceType: instance + tags: + platform: open-agent-platform + capability: agent-sandbox + providerConfigRef: + name: {{ .Values.nodepool.providerConfigName | default "default" }} +{{- end }} diff --git a/gitops/addons/charts/agent-sandbox/templates/17-kata-nodegroup.yaml b/gitops/addons/charts/agent-sandbox/templates/17-kata-nodegroup.yaml new file mode 100644 index 00000000..f8dad554 --- /dev/null +++ b/gitops/addons/charts/agent-sandbox/templates/17-kata-nodegroup.yaml @@ -0,0 +1,57 @@ +{{- if and .Values.nodepool .Values.nodepool.enabled }} +{{- /* +Kata nested-virt Managed Node Group β€” GitOps-managed via Crossplane (provider-aws-eks). + +Self-managed MNG alongside EKS Auto Mode (Auto Mode's Bottlerocket nodes can't +host Kata). Uses the nested-virt LaunchTemplate (16-...). Scale-to-zero when no +sandbox is claimed; the pool-manager scales up on a claim. Only kata pods tolerate +the taint, so general workloads never land here. + +ADOPTION: crossplane.io/external-name = imports the existing node +group in place β€” no recreate, no node churn. The provider prepends clusterName +itself (external ID becomes ":"), so the external-name is the +BARE node-group name β€” NOT "/" (that yields ":/ +" and a 400 InvalidParameterException). Verified live 2026-07-21. +*/ -}} +apiVersion: eks.aws.upbound.io/v1beta2 +kind: NodeGroup +metadata: + name: {{ .Values.nodepool.nodegroupName | default "kata-sandbox" }} + labels: + {{- include "agent-sandbox.labels" . | nindent 4 }} + annotations: + # Adopt the existing node group in place β€” BARE nodegroup name (provider adds + # the ":" prefix). See the note above. + crossplane.io/external-name: {{ .Values.nodepool.nodegroupName | default "kata-sandbox" | quote }} +spec: + forProvider: + region: {{ .Values.nodepool.region | quote }} + clusterName: {{ .Values.nodepool.clusterName | quote }} + nodeRoleArn: {{ .Values.nodepool.nodeRoleArn | quote }} + subnetIds: + {{- range .Values.nodepool.subnetIds }} + - {{ . | quote }} + {{- end }} + # Scale-to-zero idle; pool-manager scales up per claim (max = pool ceiling). + scalingConfig: + - minSize: {{ .Values.nodepool.minSize | default 0 }} + desiredSize: {{ .Values.nodepool.desiredSize | default 1 }} + maxSize: {{ .Values.nodepool.maxSize | default 3 }} + launchTemplate: + - id: {{ .Values.nodepool.launchTemplateId | quote }} + version: {{ .Values.nodepool.launchTemplateVersion | default "$Latest" | quote }} + # Only kata sandboxes tolerate this β€” keeps general workloads off the pool. + taint: + - key: kata + value: "true" + effect: NO_SCHEDULE + labels: + kata-enabled: "true" + katacontainers.io/kata-runtime: "true" + node-type: kata-mng + tags: + platform: open-agent-platform + capability: agent-sandbox + providerConfigRef: + name: {{ .Values.nodepool.providerConfigName | default "default" }} +{{- end }} diff --git a/gitops/addons/charts/agent-sandbox/templates/18-kata-eks-addons.yaml b/gitops/addons/charts/agent-sandbox/templates/18-kata-eks-addons.yaml new file mode 100644 index 00000000..099560b8 --- /dev/null +++ b/gitops/addons/charts/agent-sandbox/templates/18-kata-eks-addons.yaml @@ -0,0 +1,52 @@ +{{- if and .Values.nodepool .Values.nodepool.enabled .Values.nodepool.manageAddons }} +{{- /* +EKS addons required for the self-managed kata node β€” GitOps-managed via Crossplane. + +Auto Mode's built-in networking applies ONLY to its own managed nodes. A +self-managed MNG node has NEITHER the CNI nor kube-proxy that pods need, so both +EKS addons MUST be present or pods on the kata node can't reach the API server: + - without vpc-cni β†’ node stays NotReady ("cni plugin not initialized") + - without kube-proxy β†’ no iptables for kubernetes.default.svc β†’ kata-deploy + crashloops ("Failed to get node ... Connect") +(Root-caused in the 2026-07-10 spike β€” docs/dark-factory Β§12a.) + +ADOPTION: external-name ":" imports the existing cluster addons in +place. Gated behind nodepool.manageAddons so it's opt-in (the addons may already +be owned by the base platform on some clusters). +*/ -}} +apiVersion: eks.aws.upbound.io/v1beta1 +kind: Addon +metadata: + name: {{ .Values.nodepool.clusterName }}-vpc-cni + labels: + {{- include "agent-sandbox.labels" . | nindent 4 }} + annotations: + crossplane.io/external-name: {{ printf "%s:vpc-cni" .Values.nodepool.clusterName | quote }} +spec: + forProvider: + region: {{ .Values.nodepool.region | quote }} + clusterName: {{ .Values.nodepool.clusterName | quote }} + addonName: vpc-cni + resolveConflictsOnCreate: OVERWRITE + resolveConflictsOnUpdate: OVERWRITE + providerConfigRef: + name: {{ .Values.nodepool.providerConfigName | default "default" }} +--- +apiVersion: eks.aws.upbound.io/v1beta1 +kind: Addon +metadata: + name: {{ .Values.nodepool.clusterName }}-kube-proxy + labels: + {{- include "agent-sandbox.labels" . | nindent 4 }} + annotations: + crossplane.io/external-name: {{ printf "%s:kube-proxy" .Values.nodepool.clusterName | quote }} +spec: + forProvider: + region: {{ .Values.nodepool.region | quote }} + clusterName: {{ .Values.nodepool.clusterName | quote }} + addonName: kube-proxy + resolveConflictsOnCreate: OVERWRITE + resolveConflictsOnUpdate: OVERWRITE + providerConfigRef: + name: {{ .Values.nodepool.providerConfigName | default "default" }} +{{- end }} diff --git a/gitops/addons/charts/agent-sandbox/templates/20-sandboxtemplate.yaml b/gitops/addons/charts/agent-sandbox/templates/20-sandboxtemplate.yaml new file mode 100644 index 00000000..7e7c4eaf --- /dev/null +++ b/gitops/addons/charts/agent-sandbox/templates/20-sandboxtemplate.yaml @@ -0,0 +1,97 @@ +{{- /* +SandboxTemplate β€” the podTemplate the warm pool (and Dark Factory claims) are +cloned from. Encodes the isolation invariants for running untrusted, +LLM-generated code: + - runtimeClassName: a Kata micro-VM (own kernel), not a shared-kernel container + - automountServiceAccountToken: false (no in-cluster API creds by default) + - runAsNonRoot + restricted seccomp + drop ALL caps + - workspace on an ephemeral PVC (per claim), model access via Bifrost only +The concrete coder image (claude-code | kiro) and per-claim secrets are patched +in by the consumer when it binds a SandboxClaim. +*/ -}} +apiVersion: extensions.agents.x-k8s.io/v1beta1 +kind: SandboxTemplate +metadata: + name: {{ .Values.warmPool.templateName }} + namespace: {{ include "agent-sandbox.namespace" . }} + labels: + {{- include "agent-sandbox.labels" . | nindent 4 }} +spec: + # Flow B (the Dark Factory) drives each claimed coder by injecting per-claim env + # (SPEC, target repo, branch, profile) via SandboxClaim.spec.env. The operator + # rejects that injection unless the template opts in β€” default is Disallowed. + # `Allowed` lets a claim ADD env vars (it cannot override the template's own, + # e.g. BIFROST_URL β€” use `Overrides` for that). Verified against the live CRD: + # without this, claims fail with reason=EnvVarsInjectionRejected. + envVarsInjectionPolicy: {{ .Values.coderTemplate.envVarsInjectionPolicy | default "Allowed" }} + podTemplate: + metadata: + labels: + {{- include "agent-sandbox.selectorLabels" . | nindent 8 }} + agent-sandbox.io/role: coder + spec: + runtimeClassName: {{ .Values.kata.defaultRuntimeClass }} + automountServiceAccountToken: false + nodeSelector: + {{- toYaml .Values.kata.nodeSelector | nindent 8 }} + tolerations: + {{- toYaml .Values.kata.tolerations | nindent 8 }} + securityContext: + runAsNonRoot: true + runAsUser: 1000 + runAsGroup: 1000 + fsGroup: 1000 + seccompProfile: + type: RuntimeDefault + containers: + - name: coder + image: {{ .Values.coderTemplate.image }} + # Warm pods start BEFORE a claim injects env, so the coder can't run at + # boot. This command idles until the SandboxClaim injects DF_ISSUE_NUMBER + # (Flow B), then execs the baked-in coder entrypoint. When the image has + # no entrypoint (e.g. the busybox placeholder), it just idles β€” safe for + # the warm pool. Consumers other than Flow B simply never set DF_*. + command: + {{- toYaml .Values.coderTemplate.command | nindent 12 }} + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: ["ALL"] + env: + # This platform uses Bifrost (not LiteLLM) as the LLM gateway. + - name: BIFROST_URL + value: {{ .Values.coderTemplate.bifrostUrl | quote }} + resources: + {{- toYaml .Values.coderTemplate.resources | nindent 12 }} + volumeMounts: + - name: workspace + mountPath: /workspace + - name: tmp + mountPath: /tmp + {{- if .Values.coderTemplate.secretsMount.enabled }} + # Short-TTL GitHub token (+ optional Bifrost key) the coder reads at + # /etc/secrets β€” mode 0400, never in env. The coder uses gh-token to + # clone/push + open the PR; Bifrost auth is optional on this platform. + - name: coder-secrets + mountPath: /etc/secrets + readOnly: true + {{- end }} + volumes: + # Ephemeral per-claim workspace. SandboxTemplate.spec only supports + # podTemplate (verified against the live CRD β€” it has NO + # volumeClaimTemplates field; that belongs on the Sandbox CR). Use an + # emptyDir sized by coderTemplate.workspaceSizeGi; consumers that need a + # persistent workspace across scale-to-zero set spec.volumeClaimTemplates + # on the Sandbox CR they create from this template. + - name: workspace + emptyDir: + sizeLimit: {{ .Values.coderTemplate.workspaceSizeGi }}Gi + - name: tmp + emptyDir: {} + {{- if .Values.coderTemplate.secretsMount.enabled }} + - name: coder-secrets + secret: + secretName: {{ .Values.coderTemplate.secretsMount.secretName }} + defaultMode: 0400 + {{- end }} diff --git a/gitops/addons/charts/agent-sandbox/templates/30-networkpolicy.yaml b/gitops/addons/charts/agent-sandbox/templates/30-networkpolicy.yaml new file mode 100644 index 00000000..545c3c2d --- /dev/null +++ b/gitops/addons/charts/agent-sandbox/templates/30-networkpolicy.yaml @@ -0,0 +1,83 @@ +{{- /* +Default-deny egress for coder sandboxes, then allow ONLY what a coder needs: + - DNS (kube-dns) + - Bifrost LLM gateway (model calls) + - HTTPS to the PUBLIC internet for git/gh + package registries the build needs + +This breaks the "lethal trifecta": untrusted issue text runs in a pod that +cannot reach arbitrary internal services or exfiltrate to arbitrary hosts. + +⚠️ HUB CONTROL-PLANE ISOLATION: the capability now runs on the hub, next to the +fleet control plane (Keycloak, ArgoCD, external-secrets, Argo). Those services +are reachable in-cluster via ClusterIPs / pod IPs inside the cluster + VPC +private ranges. So the HTTPS-egress rule must allow the PUBLIC internet ONLY β€” +it excludes RFC-1918 private ranges (and IMDS/link-local), which denies the +coder any path to the control plane, the node, the API server, or peer pods. +`values.privateCidrsDenied` lists the ranges carved out of the public allow. +Selects coder pods by the label the SandboxTemplate stamps. +*/ -}} +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: coder-sandbox-egress + namespace: {{ include "agent-sandbox.namespace" . }} + labels: + {{- include "agent-sandbox.labels" . | nindent 4 }} +spec: + podSelector: + matchLabels: + agent-sandbox.io/role: coder + policyTypes: + - Egress + egress: + # DNS resolution β€” port 53 only, to the cluster DNS service. Kept broad by + # namespace (not pinned to a k8s-app=kube-dns pod label) because EKS Auto + # Mode runs CoreDNS without that label; :53 is harmless and control-plane + # isolation is enforced by the :443 + private-CIDR rule below, not here. + - to: + - namespaceSelector: {} + ports: + - protocol: UDP + port: 53 + - protocol: TCP + port: 53 + # DNS to the PUBLIC resolvers the Kata guest VM actually uses (pod + # dnsConfig.nameservers). The in-cluster :53 rule above can't cover these β€” + # they're external IPs β€” and Auto Mode has no kube-dns Service, so without + # this the coder can't resolve ANY name (crashes EAI_AGAIN api.github.com even + # though :443 egress works). Scoped to the resolver /32s (from values), so this + # opens :53 to nothing but the DNS servers themselves. + {{- if .Values.networkPolicy.dnsResolvers }} + - to: + {{- range .Values.networkPolicy.dnsResolvers }} + - ipBlock: + cidr: {{ . | quote }} + {{- end }} + ports: + - protocol: UDP + port: 53 + - protocol: TCP + port: 53 + {{- end }} + # Bifrost LLM gateway (in-cluster) β€” the ONLY control-plane-side service the + # coder may reach, and only on :8080. + - to: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: bifrost + ports: + - protocol: TCP + port: 8080 + # HTTPS egress for git / gh / package registries β€” PUBLIC internet only. + # Private/link-local ranges are excluded so the coder cannot reach the hub's + # control-plane services, the node, the API server, or peer pods over :443. + - to: + - ipBlock: + cidr: 0.0.0.0/0 + except: + {{- range .Values.networkPolicy.privateCidrsDenied }} + - {{ . | quote }} + {{- end }} + ports: + - protocol: TCP + port: 443 diff --git a/gitops/addons/charts/agent-sandbox/templates/31-clusternetworkpolicy.yaml b/gitops/addons/charts/agent-sandbox/templates/31-clusternetworkpolicy.yaml new file mode 100644 index 00000000..922a8e49 --- /dev/null +++ b/gitops/addons/charts/agent-sandbox/templates/31-clusternetworkpolicy.yaml @@ -0,0 +1,47 @@ +{{- if .Values.networkPolicy.adminDenyControlPlane }} +{{- /* +Admin-tier ClusterNetworkPolicy β€” the AUTHORITATIVE control-plane isolation for +coder sandboxes on the hub. + +Why this and not only the standard NetworkPolicy (30-networkpolicy.yaml): + 1. EKS VPC-CNI standard NetworkPolicy "are only applied to Pods that are part + of a Deployment" β€” coder pods are owned by a `Sandbox` CR (not a + Deployment), so a namespaced NetworkPolicy does not reliably apply to them. + 2. Standard NetworkPolicy egress ipBlock is matched on the Service ClusterIP + before kube-proxy DNAT, so control-plane Services (172.20.x) slip past an + ipBlock `except`. +Admin-tier ClusterNetworkPolicy with a Deny action is evaluated FIRST, wins over +everything, and applies regardless of pod ownership β€” the correct primitive for +"untrusted coder must never reach the hub control plane". + +Requires: VPC-CNI β‰₯ 1.21 with the ClusterNetworkPolicy CRD +(networking.k8s.aws/v1alpha1). +*/ -}} +apiVersion: networking.k8s.aws/v1alpha1 +kind: ClusterNetworkPolicy +metadata: + name: coder-sandbox-deny-control-plane + labels: + {{- include "agent-sandbox.labels" . | nindent 4 }} +spec: + tier: Admin + priority: {{ .Values.networkPolicy.adminPriority | default 10 }} + subject: + namespaces: + matchLabels: + kubernetes.io/metadata.name: {{ include "agent-sandbox.namespace" . }} + egress: + # Deny the coder any egress to the hub control-plane / sensitive namespaces. + # Deny wins and cannot be overridden by a namespace-scoped policy. + - action: Deny + name: deny-control-plane-namespaces + to: + - namespaces: + matchExpressions: + - key: kubernetes.io/metadata.name + operator: In + values: + {{- range .Values.networkPolicy.controlPlaneNamespaces }} + - {{ . | quote }} + {{- end }} +{{- end }} diff --git a/gitops/addons/charts/agent-sandbox/templates/32-clusterip-egress-firewall.yaml b/gitops/addons/charts/agent-sandbox/templates/32-clusterip-egress-firewall.yaml new file mode 100644 index 00000000..199e1b1f --- /dev/null +++ b/gitops/addons/charts/agent-sandbox/templates/32-clusterip-egress-firewall.yaml @@ -0,0 +1,164 @@ +{{- if .Values.networkPolicy.clusteripFirewall.enabled }} +{{- /* +clusterip-egress-firewall β€” closes the EKS VPC-CNI ClusterIP egress gap. + +WHY: VPC-CNI NetworkPolicy/ClusterNetworkPolicy egress is evaluated against the +Service ClusterIP, but kube-proxy DNATs ClusterIPβ†’podIP before that eval, so +traffic to control-plane Services (172.20.x) slips through even though backend +pod IPs are blocked (verified live: external-secrets/webhook was reachable by +ClusterIP). Standard/Admin policy tiers cannot close this on pure VPC-CNI. + +HOW: a host-network, privileged DaemonSet on the kata node installs iptables +rules in the FORWARD chain (pod-routed traffic only β€” the node's own kubeletβ†’API +traffic goes through OUTPUT and is untouched) matching conntrack --ctorigdst, i.e. +the ORIGINAL ClusterIP destination BEFORE kube-proxy's DNAT. It ALLOWs the +coder's legitimate ClusterIPs (Bifrost) and DROPs the rest of the service CIDR. +Reconciles every interval so it survives kube-proxy flushes and is idempotent. + +This runs ONLY on the tainted kata node, so it governs only coder-sandbox egress. + +NOTE: deployed in kube-system (not agent-sandbox-system) because it needs +hostNetwork + NET_ADMIN, which the agent-sandbox namespace's `baseline` +PodSecurity forbids. kube-system is the standard home for privileged node +infra DaemonSets (aws-node, kube-proxy) and has no PSS restriction. +*/ -}} +{{- $fwNs := .Values.networkPolicy.clusteripFirewall.namespace | default "kube-system" }} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: clusterip-egress-firewall + namespace: {{ $fwNs }} + labels: + {{- include "agent-sandbox.labels" . | nindent 4 }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: clusterip-egress-firewall + namespace: {{ $fwNs }} + labels: + {{- include "agent-sandbox.labels" . | nindent 4 }} +rules: + # Discover the Bifrost ClusterIP to allow-list (read-only, cross-namespace via + # a dedicated Role in the bifrost namespace below). + - apiGroups: [""] + resources: ["services"] + verbs: ["get", "list"] +--- +# Read the Bifrost Service ClusterIP from its namespace. +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: clusterip-egress-firewall-bifrost + namespace: {{ .Values.networkPolicy.clusteripFirewall.bifrostNamespace | default "bifrost" }} + labels: + {{- include "agent-sandbox.labels" . | nindent 4 }} +rules: + - apiGroups: [""] + resources: ["services"] + verbs: ["get", "list"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: clusterip-egress-firewall + namespace: {{ $fwNs }} + labels: + {{- include "agent-sandbox.labels" . | nindent 4 }} +subjects: + - kind: ServiceAccount + name: clusterip-egress-firewall + namespace: {{ $fwNs }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: clusterip-egress-firewall +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: clusterip-egress-firewall-bifrost + namespace: {{ .Values.networkPolicy.clusteripFirewall.bifrostNamespace | default "bifrost" }} + labels: + {{- include "agent-sandbox.labels" . | nindent 4 }} +subjects: + - kind: ServiceAccount + name: clusterip-egress-firewall + namespace: {{ $fwNs }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: clusterip-egress-firewall-bifrost +--- +apiVersion: apps/v1 +kind: DaemonSet +metadata: + name: clusterip-egress-firewall + namespace: {{ $fwNs }} + labels: + {{- include "agent-sandbox.labels" . | nindent 4 }} +spec: + selector: + matchLabels: + app: clusterip-egress-firewall + template: + metadata: + labels: + app: clusterip-egress-firewall + {{- include "agent-sandbox.selectorLabels" . | nindent 8 }} + spec: + serviceAccountName: clusterip-egress-firewall + # Host netns + PID so iptables acts on the node's FORWARD chain. + hostNetwork: true + # Runs only on the tainted kata node; tolerate its taints. + nodeSelector: + kata-enabled: "true" + tolerations: + - operator: Exists + containers: + - name: firewall + image: {{ .Values.networkPolicy.clusteripFirewall.image | default "alpine/k8s:1.31.0" }} + # NET_ADMIN to manage iptables; privileged not required with hostNetwork + # + NET_ADMIN, but the host iptables binary needs raw socket access. + securityContext: + capabilities: + add: ["NET_ADMIN"] + env: + - name: SERVICE_CIDR + value: {{ .Values.networkPolicy.clusteripFirewall.serviceCidr | quote }} + - name: BIFROST_NAMESPACE + value: {{ .Values.networkPolicy.clusteripFirewall.bifrostNamespace | default "bifrost" | quote }} + - name: BIFROST_SVC + value: {{ .Values.networkPolicy.clusteripFirewall.bifrostService | default "bifrost" | quote }} + - name: BIFROST_PORT + value: {{ .Values.networkPolicy.clusteripFirewall.bifrostPort | default 8080 | quote }} + - name: RECONCILE_SECONDS + value: {{ .Values.networkPolicy.clusteripFirewall.reconcileSeconds | default 30 | quote }} + command: + - /bin/sh + - -c + - | + set -eu + CHAIN=AGENT_SANDBOX_EGRESS + echo "[clusterip-fw] service_cidr=${SERVICE_CIDR} bifrost=${BIFROST_NAMESPACE}/${BIFROST_SVC}:${BIFROST_PORT}" + # The alpine/k8s image ships kubectl but not iptables β€” install it once. + command -v iptables >/dev/null 2>&1 || apk add --no-cache iptables >/dev/null 2>&1 + while true; do + BIP="$(kubectl get svc "${BIFROST_SVC}" -n "${BIFROST_NAMESPACE}" -o jsonpath='{.spec.clusterIP}' 2>/dev/null || true)" + # Build/refresh a dedicated chain, hooked from FORWARD, matched on + # the pre-DNAT original destination (conntrack --ctorigdst). + iptables -N "${CHAIN}" 2>/dev/null || iptables -F "${CHAIN}" + if [ -n "${BIP}" ]; then + iptables -A "${CHAIN}" -m conntrack --ctorigdst "${BIP}" --ctorigdstport "${BIFROST_PORT}" -j RETURN + fi + # Everything else to the service CIDR (control-plane ClusterIPs) is dropped. + iptables -A "${CHAIN}" -m conntrack --ctorigdst "${SERVICE_CIDR}" -j DROP + # Hook the chain at the top of FORWARD (idempotent). + iptables -C FORWARD -j "${CHAIN}" 2>/dev/null || iptables -I FORWARD 1 -j "${CHAIN}" + sleep "${RECONCILE_SECONDS}" + done + resources: + requests: + cpu: 5m + memory: 32Mi +{{- end }} diff --git a/gitops/addons/charts/agent-sandbox/templates/40-sandboxwarmpool.yaml b/gitops/addons/charts/agent-sandbox/templates/40-sandboxwarmpool.yaml new file mode 100644 index 00000000..c58aaff2 --- /dev/null +++ b/gitops/addons/charts/agent-sandbox/templates/40-sandboxwarmpool.yaml @@ -0,0 +1,21 @@ +{{- if .Values.warmPool.enabled }} +{{- /* +Native SandboxWarmPool (operator v0.5.0+). The operator keeps `replicas` idle +sandboxes pre-warmed from the referenced SandboxTemplate; a consumer (the Dark +Factory) binds one via a SandboxClaim and the operator refills the buffer. This +replaces the custom pool-manager CronJob β€” the warm pool is now a first-class +operator primitive (requires the vendored v0.5.1 upstream, NOT v0.1.0). +*/ -}} +apiVersion: extensions.agents.x-k8s.io/v1beta1 +kind: SandboxWarmPool +metadata: + name: {{ .Values.warmPool.name | default "coder-warmpool" }} + namespace: {{ include "agent-sandbox.namespace" . }} + labels: + {{- include "agent-sandbox.labels" . | nindent 4 }} +spec: + # Target number of pre-warmed idle sandboxes (the warm buffer). + replicas: {{ .Values.warmPool.targetIdle }} + sandboxTemplateRef: + name: {{ .Values.warmPool.templateName }} +{{- end }} diff --git a/gitops/addons/charts/agent-sandbox/templates/_helpers.tpl b/gitops/addons/charts/agent-sandbox/templates/_helpers.tpl new file mode 100644 index 00000000..a25dc728 --- /dev/null +++ b/gitops/addons/charts/agent-sandbox/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Common labels applied to every resource this chart renders. +*/}} +{{- define "agent-sandbox.labels" -}} +app.kubernetes.io/name: agent-sandbox +app.kubernetes.io/part-of: open-agent-platform +app.kubernetes.io/managed-by: {{ .Release.Service }} +helm.sh/chart: {{ printf "%s-%s" .Chart.Name .Chart.Version }} +{{- end -}} + +{{/* +Selector labels (stable subset used by Services / controllers). +*/}} +{{- define "agent-sandbox.selectorLabels" -}} +app.kubernetes.io/name: agent-sandbox +{{- end -}} + +{{/* +The namespace the capability runs in. +*/}} +{{- define "agent-sandbox.namespace" -}} +{{- default "agent-sandbox-system" .Values.namespace -}} +{{- end -}} + +{{/* +Kata node nodeadm MIME userData (for the nested-virt Launch Template). +Loads kvm_intel so /dev/kvm exists, then joins the cluster via nodeadm NodeConfig +with the kata labels + two taints: + - kata=true:NoSchedule (workload taint) + - katacontainers.io/runtime-not-ready=true:NoSchedule (startup gate; the + kata-readiness DaemonSet removes it once kata-deploy finishes installing) +Cluster endpoint/CA/CIDR are REQUIRED with a custom AMI β€” nodeadm can't discover +them otherwise (validated lesson, docs/dark-factory Β§12a). +*/}} +{{- define "agent-sandbox.kataUserData" -}} +MIME-Version: 1.0 +Content-Type: multipart/mixed; boundary="//" + +--// +Content-Type: text/x-shellscript; charset="us-ascii" + +#!/bin/bash +modprobe kvm_intel +printf 'kvm\nkvm_intel\n' > /etc/modules-load.d/kvm.conf + +--// +Content-Type: application/node.eks.aws + +apiVersion: node.eks.aws/v1alpha1 +kind: NodeConfig +spec: + cluster: + name: {{ .Values.nodepool.clusterName }} + apiServerEndpoint: {{ .Values.nodepool.clusterEndpoint }} + certificateAuthority: {{ .Values.nodepool.clusterCA }} + cidr: {{ .Values.nodepool.serviceCidr }} + kubelet: + flags: + - "--node-labels=kata-enabled=true,katacontainers.io/kata-runtime=true,node-type=kata-mng" + - "--register-with-taints=kata=true:NoSchedule,katacontainers.io/runtime-not-ready=true:NoSchedule" +--//-- +{{- end -}} diff --git a/gitops/addons/charts/agent-sandbox/upstream/Chart.yaml b/gitops/addons/charts/agent-sandbox/upstream/Chart.yaml new file mode 100644 index 00000000..f4493f7f --- /dev/null +++ b/gitops/addons/charts/agent-sandbox/upstream/Chart.yaml @@ -0,0 +1,11 @@ +apiVersion: v2 +name: agent-sandbox-operator +description: >- + Upstream agent-sandbox v0.5.1 operator install (controller Deployment + CRDs + + RBAC + webhooks) vendored verbatim from kubernetes-sigs/agent-sandbox. Packaged + as a thin Helm chart so the platform ApplicationSet (which renders addons as + Helm charts) can deploy it. The manifest files under templates/ contain no Helm + templating β€” they are applied as-is. ServerSideApply handles the oversized CRDs. +type: application +version: 0.5.1 +appVersion: "0.5.1" diff --git a/gitops/addons/charts/agent-sandbox/upstream/templates/agent-sandbox-v0.5.1-extensions.yaml b/gitops/addons/charts/agent-sandbox/upstream/templates/agent-sandbox-v0.5.1-extensions.yaml new file mode 100644 index 00000000..4eb66967 --- /dev/null +++ b/gitops/addons/charts/agent-sandbox/upstream/templates/agent-sandbox-v0.5.1-extensions.yaml @@ -0,0 +1,8960 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.21.0 + name: sandboxclaims.extensions.agents.x-k8s.io +spec: + group: extensions.agents.x-k8s.io + names: + kind: SandboxClaim + listKind: SandboxClaimList + plural: sandboxclaims + shortNames: + - sandboxclaim + singular: sandboxclaim + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .status.conditions[?(@.type=='Ready')].status + name: Ready + type: string + - jsonPath: .status.sandbox.name + name: Sandbox + type: string + - jsonPath: .status.conditions[?(@.type=='Ready')].reason + name: Reason + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1beta1 + schema: + openAPIV3Schema: + properties: + apiVersion: + type: string + kind: + type: string + metadata: + type: object + spec: + properties: + additionalPodMetadata: + properties: + annotations: + additionalProperties: + type: string + type: object + labels: + additionalProperties: + type: string + type: object + type: object + env: + items: + properties: + containerName: + type: string + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + lifecycle: + properties: + shutdownPolicy: + default: Retain + enum: + - Delete + - DeleteForeground + - Retain + type: string + shutdownTime: + format: date-time + type: string + ttlSecondsAfterFinished: + format: int32 + minimum: 0 + type: integer + type: object + volumeClaimTemplates: + items: + properties: + metadata: + properties: + annotations: + additionalProperties: + type: string + type: object + labels: + additionalProperties: + type: string + type: object + name: + type: string + type: object + spec: + properties: + accessModes: + items: + type: string + type: array + x-kubernetes-list-type: atomic + dataSource: + properties: + apiGroup: + type: string + kind: + type: string + name: + type: string + required: + - kind + - name + type: object + x-kubernetes-map-type: atomic + dataSourceRef: + properties: + apiGroup: + type: string + kind: + type: string + name: + type: string + namespace: + type: string + required: + - kind + - name + type: object + resources: + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + selector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + storageClassName: + type: string + volumeAttributesClassName: + type: string + volumeMode: + type: string + volumeName: + type: string + type: object + required: + - spec + type: object + type: array + x-kubernetes-list-type: atomic + warmPoolRef: + properties: + name: + type: string + required: + - name + type: object + required: + - warmPoolRef + type: object + status: + properties: + conditions: + items: + properties: + lastTransitionTime: + format: date-time + type: string + message: + maxLength: 32768 + type: string + observedGeneration: + format: int64 + minimum: 0 + type: integer + reason: + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + enum: + - 'True' + - 'False' + - Unknown + type: string + type: + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + sandbox: + properties: + name: + type: string + podIPs: + items: + type: string + type: array + type: object + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} + - deprecated: true + deprecationWarning: extensions.agents.x-k8s.io/v1alpha1 SandboxClaim is deprecated; + use extensions.agents.x-k8s.io/v1beta1 SandboxClaim instead + name: v1alpha1 + schema: + openAPIV3Schema: + properties: + apiVersion: + type: string + kind: + type: string + metadata: + type: object + spec: + properties: + additionalPodMetadata: + properties: + annotations: + additionalProperties: + type: string + type: object + labels: + additionalProperties: + type: string + type: object + type: object + env: + items: + properties: + containerName: + type: string + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + lifecycle: + properties: + shutdownPolicy: + default: Retain + enum: + - Delete + - DeleteForeground + - Retain + type: string + shutdownTime: + format: date-time + type: string + ttlSecondsAfterFinished: + format: int32 + minimum: 0 + type: integer + type: object + sandboxTemplateRef: + properties: + name: + type: string + required: + - name + type: object + warmpool: + default: default + type: string + required: + - sandboxTemplateRef + type: object + status: + properties: + conditions: + items: + properties: + lastTransitionTime: + format: date-time + type: string + message: + maxLength: 32768 + type: string + observedGeneration: + format: int64 + minimum: 0 + type: integer + reason: + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + enum: + - 'True' + - 'False' + - Unknown + type: string + type: + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + sandbox: + properties: + name: + type: string + podIPs: + items: + type: string + type: array + type: object + type: object + required: + - spec + type: object + served: true + storage: false + subresources: + status: {} + conversion: + strategy: Webhook + webhook: + conversionReviewVersions: + - v1 + - v1beta1 + clientConfig: + service: + name: agent-sandbox-webhook-service + namespace: agent-sandbox-system + path: /convert +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.21.0 + name: sandboxtemplates.extensions.agents.x-k8s.io +spec: + group: extensions.agents.x-k8s.io + names: + kind: SandboxTemplate + listKind: SandboxTemplateList + plural: sandboxtemplates + shortNames: + - sandboxtemplate + singular: sandboxtemplate + scope: Namespaced + versions: + - name: v1beta1 + schema: + openAPIV3Schema: + properties: + apiVersion: + type: string + kind: + type: string + metadata: + type: object + spec: + properties: + envVarsInjectionPolicy: + default: Disallowed + enum: + - Allowed + - Overrides + - Disallowed + type: string + networkPolicy: + properties: + egress: + items: + properties: + ports: + items: + properties: + endPort: + format: int32 + type: integer + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + protocol: + type: string + type: object + type: array + x-kubernetes-list-type: atomic + to: + items: + properties: + ipBlock: + properties: + cidr: + type: string + except: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - cidr + type: object + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + podSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + type: object + type: array + x-kubernetes-list-type: atomic + type: object + type: array + ingress: + items: + properties: + from: + items: + properties: + ipBlock: + properties: + cidr: + type: string + except: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - cidr + type: object + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + podSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + type: object + type: array + x-kubernetes-list-type: atomic + ports: + items: + properties: + endPort: + format: int32 + type: integer + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + protocol: + type: string + type: object + type: array + x-kubernetes-list-type: atomic + type: object + type: array + type: object + networkPolicyManagement: + default: Managed + enum: + - Managed + - Unmanaged + type: string + podTemplate: + properties: + metadata: + properties: + annotations: + additionalProperties: + type: string + type: object + labels: + additionalProperties: + type: string + type: object + type: object + spec: + properties: + activeDeadlineSeconds: + format: int64 + type: integer + affinity: + properties: + nodeAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + preference: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchFields: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + weight: + format: int32 + type: integer + required: + - preference + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + properties: + nodeSelectorTerms: + items: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchFields: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + type: array + x-kubernetes-list-type: atomic + required: + - nodeSelectorTerms + type: object + x-kubernetes-map-type: atomic + type: object + podAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + podAffinityTerm: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + weight: + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + type: array + x-kubernetes-list-type: atomic + type: object + podAntiAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + podAffinityTerm: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + weight: + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + type: array + x-kubernetes-list-type: atomic + type: object + type: object + automountServiceAccountToken: + type: boolean + containers: + items: + properties: + args: + items: + type: string + type: array + x-kubernetes-list-type: atomic + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + env: + items: + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + configMapKeyRef: + properties: + key: + type: string + name: + default: '' + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + fileKeyRef: + properties: + key: + type: string + optional: + default: false + type: boolean + path: + type: string + volumeName: + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + properties: + key: + type: string + name: + default: '' + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + envFrom: + items: + properties: + configMapRef: + properties: + name: + default: '' + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + type: string + secretRef: + properties: + name: + default: '' + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + x-kubernetes-list-type: atomic + image: + type: string + imagePullPolicy: + type: string + lifecycle: + properties: + postStart: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + preStop: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + stopSignal: + type: string + type: object + livenessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: '' + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + name: + type: string + ports: + items: + properties: + containerPort: + format: int32 + type: integer + hostIP: + type: string + hostPort: + format: int32 + type: integer + name: + type: string + protocol: + default: TCP + type: string + required: + - containerPort + type: object + type: array + x-kubernetes-list-map-keys: + - containerPort + - protocol + x-kubernetes-list-type: map + readinessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: '' + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + resizePolicy: + items: + properties: + resourceName: + type: string + restartPolicy: + type: string + required: + - resourceName + - restartPolicy + type: object + type: array + x-kubernetes-list-type: atomic + resources: + properties: + claims: + items: + properties: + name: + type: string + request: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + restartPolicy: + type: string + restartPolicyRules: + items: + properties: + action: + type: string + exitCodes: + properties: + operator: + type: string + values: + items: + format: int32 + type: integer + type: array + x-kubernetes-list-type: set + required: + - operator + type: object + required: + - action + type: object + type: array + x-kubernetes-list-type: atomic + securityContext: + properties: + allowPrivilegeEscalation: + type: boolean + appArmorProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + capabilities: + properties: + add: + items: + type: string + type: array + x-kubernetes-list-type: atomic + drop: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + privileged: + type: boolean + procMount: + type: string + readOnlyRootFilesystem: + type: boolean + runAsGroup: + format: int64 + type: integer + runAsNonRoot: + type: boolean + runAsUser: + format: int64 + type: integer + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object + startupProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: '' + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + stdin: + type: boolean + stdinOnce: + type: boolean + terminationMessagePath: + type: string + terminationMessagePolicy: + type: string + tty: + type: boolean + volumeDevices: + items: + properties: + devicePath: + type: string + name: + type: string + required: + - devicePath + - name + type: object + type: array + x-kubernetes-list-map-keys: + - devicePath + x-kubernetes-list-type: map + volumeMounts: + items: + properties: + mountPath: + type: string + mountPropagation: + type: string + name: + type: string + readOnly: + type: boolean + recursiveReadOnly: + type: string + subPath: + type: string + subPathExpr: + type: string + required: + - mountPath + - name + type: object + type: array + x-kubernetes-list-map-keys: + - mountPath + x-kubernetes-list-type: map + workingDir: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + dnsConfig: + properties: + nameservers: + items: + type: string + type: array + x-kubernetes-list-type: atomic + options: + items: + properties: + name: + type: string + value: + type: string + type: object + type: array + x-kubernetes-list-type: atomic + searches: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + dnsPolicy: + type: string + enableServiceLinks: + type: boolean + ephemeralContainers: + items: + properties: + args: + items: + type: string + type: array + x-kubernetes-list-type: atomic + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + env: + items: + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + configMapKeyRef: + properties: + key: + type: string + name: + default: '' + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + fileKeyRef: + properties: + key: + type: string + optional: + default: false + type: boolean + path: + type: string + volumeName: + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + properties: + key: + type: string + name: + default: '' + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + envFrom: + items: + properties: + configMapRef: + properties: + name: + default: '' + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + type: string + secretRef: + properties: + name: + default: '' + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + x-kubernetes-list-type: atomic + image: + type: string + imagePullPolicy: + type: string + lifecycle: + properties: + postStart: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + preStop: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + stopSignal: + type: string + type: object + livenessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: '' + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + name: + type: string + ports: + items: + properties: + containerPort: + format: int32 + type: integer + hostIP: + type: string + hostPort: + format: int32 + type: integer + name: + type: string + protocol: + default: TCP + type: string + required: + - containerPort + type: object + type: array + x-kubernetes-list-map-keys: + - containerPort + - protocol + x-kubernetes-list-type: map + readinessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: '' + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + resizePolicy: + items: + properties: + resourceName: + type: string + restartPolicy: + type: string + required: + - resourceName + - restartPolicy + type: object + type: array + x-kubernetes-list-type: atomic + resources: + properties: + claims: + items: + properties: + name: + type: string + request: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + restartPolicy: + type: string + restartPolicyRules: + items: + properties: + action: + type: string + exitCodes: + properties: + operator: + type: string + values: + items: + format: int32 + type: integer + type: array + x-kubernetes-list-type: set + required: + - operator + type: object + required: + - action + type: object + type: array + x-kubernetes-list-type: atomic + securityContext: + properties: + allowPrivilegeEscalation: + type: boolean + appArmorProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + capabilities: + properties: + add: + items: + type: string + type: array + x-kubernetes-list-type: atomic + drop: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + privileged: + type: boolean + procMount: + type: string + readOnlyRootFilesystem: + type: boolean + runAsGroup: + format: int64 + type: integer + runAsNonRoot: + type: boolean + runAsUser: + format: int64 + type: integer + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object + startupProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: '' + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + stdin: + type: boolean + stdinOnce: + type: boolean + targetContainerName: + type: string + terminationMessagePath: + type: string + terminationMessagePolicy: + type: string + tty: + type: boolean + volumeDevices: + items: + properties: + devicePath: + type: string + name: + type: string + required: + - devicePath + - name + type: object + type: array + x-kubernetes-list-map-keys: + - devicePath + x-kubernetes-list-type: map + volumeMounts: + items: + properties: + mountPath: + type: string + mountPropagation: + type: string + name: + type: string + readOnly: + type: boolean + recursiveReadOnly: + type: string + subPath: + type: string + subPathExpr: + type: string + required: + - mountPath + - name + type: object + type: array + x-kubernetes-list-map-keys: + - mountPath + x-kubernetes-list-type: map + workingDir: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + hostAliases: + items: + properties: + hostnames: + items: + type: string + type: array + x-kubernetes-list-type: atomic + ip: + type: string + required: + - ip + type: object + type: array + x-kubernetes-list-map-keys: + - ip + x-kubernetes-list-type: map + hostIPC: + type: boolean + hostNetwork: + type: boolean + hostPID: + type: boolean + hostUsers: + type: boolean + hostname: + type: string + hostnameOverride: + type: string + imagePullSecrets: + items: + properties: + name: + default: '' + type: string + type: object + x-kubernetes-map-type: atomic + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + initContainers: + items: + properties: + args: + items: + type: string + type: array + x-kubernetes-list-type: atomic + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + env: + items: + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + configMapKeyRef: + properties: + key: + type: string + name: + default: '' + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + fileKeyRef: + properties: + key: + type: string + optional: + default: false + type: boolean + path: + type: string + volumeName: + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + properties: + key: + type: string + name: + default: '' + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + envFrom: + items: + properties: + configMapRef: + properties: + name: + default: '' + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + type: string + secretRef: + properties: + name: + default: '' + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + x-kubernetes-list-type: atomic + image: + type: string + imagePullPolicy: + type: string + lifecycle: + properties: + postStart: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + preStop: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + stopSignal: + type: string + type: object + livenessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: '' + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + name: + type: string + ports: + items: + properties: + containerPort: + format: int32 + type: integer + hostIP: + type: string + hostPort: + format: int32 + type: integer + name: + type: string + protocol: + default: TCP + type: string + required: + - containerPort + type: object + type: array + x-kubernetes-list-map-keys: + - containerPort + - protocol + x-kubernetes-list-type: map + readinessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: '' + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + resizePolicy: + items: + properties: + resourceName: + type: string + restartPolicy: + type: string + required: + - resourceName + - restartPolicy + type: object + type: array + x-kubernetes-list-type: atomic + resources: + properties: + claims: + items: + properties: + name: + type: string + request: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + restartPolicy: + type: string + restartPolicyRules: + items: + properties: + action: + type: string + exitCodes: + properties: + operator: + type: string + values: + items: + format: int32 + type: integer + type: array + x-kubernetes-list-type: set + required: + - operator + type: object + required: + - action + type: object + type: array + x-kubernetes-list-type: atomic + securityContext: + properties: + allowPrivilegeEscalation: + type: boolean + appArmorProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + capabilities: + properties: + add: + items: + type: string + type: array + x-kubernetes-list-type: atomic + drop: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + privileged: + type: boolean + procMount: + type: string + readOnlyRootFilesystem: + type: boolean + runAsGroup: + format: int64 + type: integer + runAsNonRoot: + type: boolean + runAsUser: + format: int64 + type: integer + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object + startupProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: '' + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + stdin: + type: boolean + stdinOnce: + type: boolean + terminationMessagePath: + type: string + terminationMessagePolicy: + type: string + tty: + type: boolean + volumeDevices: + items: + properties: + devicePath: + type: string + name: + type: string + required: + - devicePath + - name + type: object + type: array + x-kubernetes-list-map-keys: + - devicePath + x-kubernetes-list-type: map + volumeMounts: + items: + properties: + mountPath: + type: string + mountPropagation: + type: string + name: + type: string + readOnly: + type: boolean + recursiveReadOnly: + type: string + subPath: + type: string + subPathExpr: + type: string + required: + - mountPath + - name + type: object + type: array + x-kubernetes-list-map-keys: + - mountPath + x-kubernetes-list-type: map + workingDir: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + nodeName: + type: string + nodeSelector: + additionalProperties: + type: string + type: object + x-kubernetes-map-type: atomic + os: + properties: + name: + type: string + required: + - name + type: object + overhead: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + preemptionPolicy: + type: string + priority: + format: int32 + type: integer + priorityClassName: + type: string + readinessGates: + items: + properties: + conditionType: + type: string + required: + - conditionType + type: object + type: array + x-kubernetes-list-type: atomic + resourceClaims: + items: + properties: + name: + type: string + resourceClaimName: + type: string + resourceClaimTemplateName: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + resources: + properties: + claims: + items: + properties: + name: + type: string + request: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + restartPolicy: + type: string + runtimeClassName: + type: string + schedulerName: + type: string + schedulingGates: + items: + properties: + name: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + schedulingGroup: + properties: + podGroupName: + type: string + type: object + securityContext: + properties: + appArmorProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + fsGroup: + format: int64 + type: integer + fsGroupChangePolicy: + type: string + runAsGroup: + format: int64 + type: integer + runAsNonRoot: + type: boolean + runAsUser: + format: int64 + type: integer + seLinuxChangePolicy: + type: string + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + supplementalGroups: + items: + format: int64 + type: integer + type: array + x-kubernetes-list-type: atomic + supplementalGroupsPolicy: + type: string + sysctls: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object + serviceAccount: + type: string + serviceAccountName: + type: string + setHostnameAsFQDN: + type: boolean + shareProcessNamespace: + type: boolean + subdomain: + type: string + terminationGracePeriodSeconds: + format: int64 + type: integer + tolerations: + items: + properties: + effect: + type: string + key: + type: string + operator: + type: string + tolerationSeconds: + format: int64 + type: integer + value: + type: string + type: object + type: array + x-kubernetes-list-type: atomic + topologySpreadConstraints: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + maxSkew: + format: int32 + type: integer + minDomains: + format: int32 + type: integer + nodeAffinityPolicy: + type: string + nodeTaintsPolicy: + type: string + topologyKey: + type: string + whenUnsatisfiable: + type: string + required: + - maxSkew + - topologyKey + - whenUnsatisfiable + type: object + type: array + x-kubernetes-list-map-keys: + - topologyKey + - whenUnsatisfiable + x-kubernetes-list-type: map + volumes: + items: + properties: + awsElasticBlockStore: + properties: + fsType: + type: string + partition: + format: int32 + type: integer + readOnly: + type: boolean + volumeID: + type: string + required: + - volumeID + type: object + azureDisk: + properties: + cachingMode: + type: string + diskName: + type: string + diskURI: + type: string + fsType: + default: ext4 + type: string + kind: + type: string + readOnly: + default: false + type: boolean + required: + - diskName + - diskURI + type: object + azureFile: + properties: + readOnly: + type: boolean + secretName: + type: string + shareName: + type: string + required: + - secretName + - shareName + type: object + cephfs: + properties: + monitors: + items: + type: string + type: array + x-kubernetes-list-type: atomic + path: + type: string + readOnly: + type: boolean + secretFile: + type: string + secretRef: + properties: + name: + default: '' + type: string + type: object + x-kubernetes-map-type: atomic + user: + type: string + required: + - monitors + type: object + cinder: + properties: + fsType: + type: string + readOnly: + type: boolean + secretRef: + properties: + name: + default: '' + type: string + type: object + x-kubernetes-map-type: atomic + volumeID: + type: string + required: + - volumeID + type: object + configMap: + properties: + defaultMode: + format: int32 + type: integer + items: + items: + properties: + key: + type: string + mode: + format: int32 + type: integer + path: + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + name: + default: '' + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + csi: + properties: + driver: + type: string + fsType: + type: string + nodePublishSecretRef: + properties: + name: + default: '' + type: string + type: object + x-kubernetes-map-type: atomic + readOnly: + type: boolean + volumeAttributes: + additionalProperties: + type: string + type: object + required: + - driver + type: object + downwardAPI: + properties: + defaultMode: + format: int32 + type: integer + items: + items: + properties: + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + mode: + format: int32 + type: integer + path: + type: string + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + required: + - path + type: object + type: array + x-kubernetes-list-type: atomic + type: object + emptyDir: + properties: + medium: + type: string + sizeLimit: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + ephemeral: + properties: + volumeClaimTemplate: + properties: + metadata: + type: object + spec: + properties: + accessModes: + items: + type: string + type: array + x-kubernetes-list-type: atomic + dataSource: + properties: + apiGroup: + type: string + kind: + type: string + name: + type: string + required: + - kind + - name + type: object + x-kubernetes-map-type: atomic + dataSourceRef: + properties: + apiGroup: + type: string + kind: + type: string + name: + type: string + namespace: + type: string + required: + - kind + - name + type: object + resources: + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + selector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + storageClassName: + type: string + volumeAttributesClassName: + type: string + volumeMode: + type: string + volumeName: + type: string + type: object + required: + - spec + type: object + type: object + fc: + properties: + fsType: + type: string + lun: + format: int32 + type: integer + readOnly: + type: boolean + targetWWNs: + items: + type: string + type: array + x-kubernetes-list-type: atomic + wwids: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + flexVolume: + properties: + driver: + type: string + fsType: + type: string + options: + additionalProperties: + type: string + type: object + readOnly: + type: boolean + secretRef: + properties: + name: + default: '' + type: string + type: object + x-kubernetes-map-type: atomic + required: + - driver + type: object + flocker: + properties: + datasetName: + type: string + datasetUUID: + type: string + type: object + gcePersistentDisk: + properties: + fsType: + type: string + partition: + format: int32 + type: integer + pdName: + type: string + readOnly: + type: boolean + required: + - pdName + type: object + gitRepo: + properties: + directory: + type: string + repository: + type: string + revision: + type: string + required: + - repository + type: object + glusterfs: + properties: + endpoints: + type: string + path: + type: string + readOnly: + type: boolean + required: + - endpoints + - path + type: object + hostPath: + properties: + path: + type: string + type: + type: string + required: + - path + type: object + image: + properties: + pullPolicy: + type: string + reference: + type: string + type: object + iscsi: + properties: + chapAuthDiscovery: + type: boolean + chapAuthSession: + type: boolean + fsType: + type: string + initiatorName: + type: string + iqn: + type: string + iscsiInterface: + default: default + type: string + lun: + format: int32 + type: integer + portals: + items: + type: string + type: array + x-kubernetes-list-type: atomic + readOnly: + type: boolean + secretRef: + properties: + name: + default: '' + type: string + type: object + x-kubernetes-map-type: atomic + targetPortal: + type: string + required: + - iqn + - lun + - targetPortal + type: object + name: + type: string + nfs: + properties: + path: + type: string + readOnly: + type: boolean + server: + type: string + required: + - path + - server + type: object + persistentVolumeClaim: + properties: + claimName: + type: string + readOnly: + type: boolean + required: + - claimName + type: object + photonPersistentDisk: + properties: + fsType: + type: string + pdID: + type: string + required: + - pdID + type: object + portworxVolume: + properties: + fsType: + type: string + readOnly: + type: boolean + volumeID: + type: string + required: + - volumeID + type: object + projected: + properties: + defaultMode: + format: int32 + type: integer + sources: + items: + properties: + clusterTrustBundle: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + name: + type: string + optional: + type: boolean + path: + type: string + signerName: + type: string + required: + - path + type: object + configMap: + properties: + items: + items: + properties: + key: + type: string + mode: + format: int32 + type: integer + path: + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + name: + default: '' + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + downwardAPI: + properties: + items: + items: + properties: + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + mode: + format: int32 + type: integer + path: + type: string + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + required: + - path + type: object + type: array + x-kubernetes-list-type: atomic + type: object + podCertificate: + properties: + certificateChainPath: + type: string + credentialBundlePath: + type: string + keyPath: + type: string + keyType: + type: string + maxExpirationSeconds: + format: int32 + type: integer + signerName: + type: string + userAnnotations: + additionalProperties: + type: string + type: object + required: + - keyType + - signerName + type: object + secret: + properties: + items: + items: + properties: + key: + type: string + mode: + format: int32 + type: integer + path: + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + name: + default: '' + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + serviceAccountToken: + properties: + audience: + type: string + expirationSeconds: + format: int64 + type: integer + path: + type: string + required: + - path + type: object + type: object + type: array + x-kubernetes-list-type: atomic + type: object + quobyte: + properties: + group: + type: string + readOnly: + type: boolean + registry: + type: string + tenant: + type: string + user: + type: string + volume: + type: string + required: + - registry + - volume + type: object + rbd: + properties: + fsType: + type: string + image: + type: string + keyring: + default: /etc/ceph/keyring + type: string + monitors: + items: + type: string + type: array + x-kubernetes-list-type: atomic + pool: + default: rbd + type: string + readOnly: + type: boolean + secretRef: + properties: + name: + default: '' + type: string + type: object + x-kubernetes-map-type: atomic + user: + default: admin + type: string + required: + - image + - monitors + type: object + scaleIO: + properties: + fsType: + default: xfs + type: string + gateway: + type: string + protectionDomain: + type: string + readOnly: + type: boolean + secretRef: + properties: + name: + default: '' + type: string + type: object + x-kubernetes-map-type: atomic + sslEnabled: + type: boolean + storageMode: + default: ThinProvisioned + type: string + storagePool: + type: string + system: + type: string + volumeName: + type: string + required: + - gateway + - secretRef + - system + type: object + secret: + properties: + defaultMode: + format: int32 + type: integer + items: + items: + properties: + key: + type: string + mode: + format: int32 + type: integer + path: + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + optional: + type: boolean + secretName: + type: string + type: object + storageos: + properties: + fsType: + type: string + readOnly: + type: boolean + secretRef: + properties: + name: + default: '' + type: string + type: object + x-kubernetes-map-type: atomic + volumeName: + type: string + volumeNamespace: + type: string + type: object + vsphereVolume: + properties: + fsType: + type: string + storagePolicyID: + type: string + storagePolicyName: + type: string + volumePath: + type: string + required: + - volumePath + type: object + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + required: + - containers + type: object + required: + - spec + type: object + service: + type: boolean + volumeClaimTemplates: + items: + properties: + metadata: + properties: + annotations: + additionalProperties: + type: string + type: object + labels: + additionalProperties: + type: string + type: object + name: + type: string + type: object + spec: + properties: + accessModes: + items: + type: string + type: array + x-kubernetes-list-type: atomic + dataSource: + properties: + apiGroup: + type: string + kind: + type: string + name: + type: string + required: + - kind + - name + type: object + x-kubernetes-map-type: atomic + dataSourceRef: + properties: + apiGroup: + type: string + kind: + type: string + name: + type: string + namespace: + type: string + required: + - kind + - name + type: object + resources: + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + selector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + storageClassName: + type: string + volumeAttributesClassName: + type: string + volumeMode: + type: string + volumeName: + type: string + type: object + required: + - spec + type: object + type: array + x-kubernetes-list-type: atomic + volumeClaimTemplatesPolicy: + default: Disallowed + enum: + - Disallowed + - Allowed + - Overrides + type: string + required: + - podTemplate + type: object + required: + - spec + type: object + served: true + storage: true + - deprecated: true + deprecationWarning: extensions.agents.x-k8s.io/v1alpha1 SandboxTemplate is deprecated; + use extensions.agents.x-k8s.io/v1beta1 SandboxTemplate instead + name: v1alpha1 + schema: + openAPIV3Schema: + properties: + apiVersion: + type: string + kind: + type: string + metadata: + type: object + spec: + properties: + envVarsInjectionPolicy: + default: Disallowed + enum: + - Allowed + - Overrides + - Disallowed + type: string + networkPolicy: + properties: + egress: + items: + properties: + ports: + items: + properties: + endPort: + format: int32 + type: integer + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + protocol: + type: string + type: object + type: array + x-kubernetes-list-type: atomic + to: + items: + properties: + ipBlock: + properties: + cidr: + type: string + except: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - cidr + type: object + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + podSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + type: object + type: array + x-kubernetes-list-type: atomic + type: object + type: array + ingress: + items: + properties: + from: + items: + properties: + ipBlock: + properties: + cidr: + type: string + except: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - cidr + type: object + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + podSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + type: object + type: array + x-kubernetes-list-type: atomic + ports: + items: + properties: + endPort: + format: int32 + type: integer + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + protocol: + type: string + type: object + type: array + x-kubernetes-list-type: atomic + type: object + type: array + type: object + networkPolicyManagement: + default: Managed + enum: + - Managed + - Unmanaged + type: string + podTemplate: + properties: + metadata: + properties: + annotations: + additionalProperties: + type: string + type: object + labels: + additionalProperties: + type: string + type: object + type: object + spec: + properties: + activeDeadlineSeconds: + format: int64 + type: integer + affinity: + properties: + nodeAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + preference: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchFields: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + weight: + format: int32 + type: integer + required: + - preference + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + properties: + nodeSelectorTerms: + items: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchFields: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + type: array + x-kubernetes-list-type: atomic + required: + - nodeSelectorTerms + type: object + x-kubernetes-map-type: atomic + type: object + podAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + podAffinityTerm: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + weight: + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + type: array + x-kubernetes-list-type: atomic + type: object + podAntiAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + podAffinityTerm: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + weight: + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + type: array + x-kubernetes-list-type: atomic + type: object + type: object + automountServiceAccountToken: + type: boolean + containers: + items: + properties: + args: + items: + type: string + type: array + x-kubernetes-list-type: atomic + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + env: + items: + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + configMapKeyRef: + properties: + key: + type: string + name: + default: '' + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + fileKeyRef: + properties: + key: + type: string + optional: + default: false + type: boolean + path: + type: string + volumeName: + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + properties: + key: + type: string + name: + default: '' + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + envFrom: + items: + properties: + configMapRef: + properties: + name: + default: '' + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + type: string + secretRef: + properties: + name: + default: '' + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + x-kubernetes-list-type: atomic + image: + type: string + imagePullPolicy: + type: string + lifecycle: + properties: + postStart: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + preStop: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + stopSignal: + type: string + type: object + livenessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: '' + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + name: + type: string + ports: + items: + properties: + containerPort: + format: int32 + type: integer + hostIP: + type: string + hostPort: + format: int32 + type: integer + name: + type: string + protocol: + default: TCP + type: string + required: + - containerPort + type: object + type: array + x-kubernetes-list-map-keys: + - containerPort + - protocol + x-kubernetes-list-type: map + readinessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: '' + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + resizePolicy: + items: + properties: + resourceName: + type: string + restartPolicy: + type: string + required: + - resourceName + - restartPolicy + type: object + type: array + x-kubernetes-list-type: atomic + resources: + properties: + claims: + items: + properties: + name: + type: string + request: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + restartPolicy: + type: string + restartPolicyRules: + items: + properties: + action: + type: string + exitCodes: + properties: + operator: + type: string + values: + items: + format: int32 + type: integer + type: array + x-kubernetes-list-type: set + required: + - operator + type: object + required: + - action + type: object + type: array + x-kubernetes-list-type: atomic + securityContext: + properties: + allowPrivilegeEscalation: + type: boolean + appArmorProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + capabilities: + properties: + add: + items: + type: string + type: array + x-kubernetes-list-type: atomic + drop: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + privileged: + type: boolean + procMount: + type: string + readOnlyRootFilesystem: + type: boolean + runAsGroup: + format: int64 + type: integer + runAsNonRoot: + type: boolean + runAsUser: + format: int64 + type: integer + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object + startupProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: '' + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + stdin: + type: boolean + stdinOnce: + type: boolean + terminationMessagePath: + type: string + terminationMessagePolicy: + type: string + tty: + type: boolean + volumeDevices: + items: + properties: + devicePath: + type: string + name: + type: string + required: + - devicePath + - name + type: object + type: array + x-kubernetes-list-map-keys: + - devicePath + x-kubernetes-list-type: map + volumeMounts: + items: + properties: + mountPath: + type: string + mountPropagation: + type: string + name: + type: string + readOnly: + type: boolean + recursiveReadOnly: + type: string + subPath: + type: string + subPathExpr: + type: string + required: + - mountPath + - name + type: object + type: array + x-kubernetes-list-map-keys: + - mountPath + x-kubernetes-list-type: map + workingDir: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + dnsConfig: + properties: + nameservers: + items: + type: string + type: array + x-kubernetes-list-type: atomic + options: + items: + properties: + name: + type: string + value: + type: string + type: object + type: array + x-kubernetes-list-type: atomic + searches: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + dnsPolicy: + type: string + enableServiceLinks: + type: boolean + ephemeralContainers: + items: + properties: + args: + items: + type: string + type: array + x-kubernetes-list-type: atomic + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + env: + items: + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + configMapKeyRef: + properties: + key: + type: string + name: + default: '' + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + fileKeyRef: + properties: + key: + type: string + optional: + default: false + type: boolean + path: + type: string + volumeName: + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + properties: + key: + type: string + name: + default: '' + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + envFrom: + items: + properties: + configMapRef: + properties: + name: + default: '' + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + type: string + secretRef: + properties: + name: + default: '' + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + x-kubernetes-list-type: atomic + image: + type: string + imagePullPolicy: + type: string + lifecycle: + properties: + postStart: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + preStop: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + stopSignal: + type: string + type: object + livenessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: '' + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + name: + type: string + ports: + items: + properties: + containerPort: + format: int32 + type: integer + hostIP: + type: string + hostPort: + format: int32 + type: integer + name: + type: string + protocol: + default: TCP + type: string + required: + - containerPort + type: object + type: array + x-kubernetes-list-map-keys: + - containerPort + - protocol + x-kubernetes-list-type: map + readinessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: '' + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + resizePolicy: + items: + properties: + resourceName: + type: string + restartPolicy: + type: string + required: + - resourceName + - restartPolicy + type: object + type: array + x-kubernetes-list-type: atomic + resources: + properties: + claims: + items: + properties: + name: + type: string + request: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + restartPolicy: + type: string + restartPolicyRules: + items: + properties: + action: + type: string + exitCodes: + properties: + operator: + type: string + values: + items: + format: int32 + type: integer + type: array + x-kubernetes-list-type: set + required: + - operator + type: object + required: + - action + type: object + type: array + x-kubernetes-list-type: atomic + securityContext: + properties: + allowPrivilegeEscalation: + type: boolean + appArmorProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + capabilities: + properties: + add: + items: + type: string + type: array + x-kubernetes-list-type: atomic + drop: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + privileged: + type: boolean + procMount: + type: string + readOnlyRootFilesystem: + type: boolean + runAsGroup: + format: int64 + type: integer + runAsNonRoot: + type: boolean + runAsUser: + format: int64 + type: integer + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object + startupProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: '' + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + stdin: + type: boolean + stdinOnce: + type: boolean + targetContainerName: + type: string + terminationMessagePath: + type: string + terminationMessagePolicy: + type: string + tty: + type: boolean + volumeDevices: + items: + properties: + devicePath: + type: string + name: + type: string + required: + - devicePath + - name + type: object + type: array + x-kubernetes-list-map-keys: + - devicePath + x-kubernetes-list-type: map + volumeMounts: + items: + properties: + mountPath: + type: string + mountPropagation: + type: string + name: + type: string + readOnly: + type: boolean + recursiveReadOnly: + type: string + subPath: + type: string + subPathExpr: + type: string + required: + - mountPath + - name + type: object + type: array + x-kubernetes-list-map-keys: + - mountPath + x-kubernetes-list-type: map + workingDir: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + hostAliases: + items: + properties: + hostnames: + items: + type: string + type: array + x-kubernetes-list-type: atomic + ip: + type: string + required: + - ip + type: object + type: array + x-kubernetes-list-map-keys: + - ip + x-kubernetes-list-type: map + hostIPC: + type: boolean + hostNetwork: + type: boolean + hostPID: + type: boolean + hostUsers: + type: boolean + hostname: + type: string + hostnameOverride: + type: string + imagePullSecrets: + items: + properties: + name: + default: '' + type: string + type: object + x-kubernetes-map-type: atomic + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + initContainers: + items: + properties: + args: + items: + type: string + type: array + x-kubernetes-list-type: atomic + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + env: + items: + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + configMapKeyRef: + properties: + key: + type: string + name: + default: '' + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + fileKeyRef: + properties: + key: + type: string + optional: + default: false + type: boolean + path: + type: string + volumeName: + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + properties: + key: + type: string + name: + default: '' + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + envFrom: + items: + properties: + configMapRef: + properties: + name: + default: '' + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + type: string + secretRef: + properties: + name: + default: '' + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + x-kubernetes-list-type: atomic + image: + type: string + imagePullPolicy: + type: string + lifecycle: + properties: + postStart: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + preStop: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + stopSignal: + type: string + type: object + livenessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: '' + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + name: + type: string + ports: + items: + properties: + containerPort: + format: int32 + type: integer + hostIP: + type: string + hostPort: + format: int32 + type: integer + name: + type: string + protocol: + default: TCP + type: string + required: + - containerPort + type: object + type: array + x-kubernetes-list-map-keys: + - containerPort + - protocol + x-kubernetes-list-type: map + readinessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: '' + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + resizePolicy: + items: + properties: + resourceName: + type: string + restartPolicy: + type: string + required: + - resourceName + - restartPolicy + type: object + type: array + x-kubernetes-list-type: atomic + resources: + properties: + claims: + items: + properties: + name: + type: string + request: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + restartPolicy: + type: string + restartPolicyRules: + items: + properties: + action: + type: string + exitCodes: + properties: + operator: + type: string + values: + items: + format: int32 + type: integer + type: array + x-kubernetes-list-type: set + required: + - operator + type: object + required: + - action + type: object + type: array + x-kubernetes-list-type: atomic + securityContext: + properties: + allowPrivilegeEscalation: + type: boolean + appArmorProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + capabilities: + properties: + add: + items: + type: string + type: array + x-kubernetes-list-type: atomic + drop: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + privileged: + type: boolean + procMount: + type: string + readOnlyRootFilesystem: + type: boolean + runAsGroup: + format: int64 + type: integer + runAsNonRoot: + type: boolean + runAsUser: + format: int64 + type: integer + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object + startupProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: '' + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + stdin: + type: boolean + stdinOnce: + type: boolean + terminationMessagePath: + type: string + terminationMessagePolicy: + type: string + tty: + type: boolean + volumeDevices: + items: + properties: + devicePath: + type: string + name: + type: string + required: + - devicePath + - name + type: object + type: array + x-kubernetes-list-map-keys: + - devicePath + x-kubernetes-list-type: map + volumeMounts: + items: + properties: + mountPath: + type: string + mountPropagation: + type: string + name: + type: string + readOnly: + type: boolean + recursiveReadOnly: + type: string + subPath: + type: string + subPathExpr: + type: string + required: + - mountPath + - name + type: object + type: array + x-kubernetes-list-map-keys: + - mountPath + x-kubernetes-list-type: map + workingDir: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + nodeName: + type: string + nodeSelector: + additionalProperties: + type: string + type: object + x-kubernetes-map-type: atomic + os: + properties: + name: + type: string + required: + - name + type: object + overhead: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + preemptionPolicy: + type: string + priority: + format: int32 + type: integer + priorityClassName: + type: string + readinessGates: + items: + properties: + conditionType: + type: string + required: + - conditionType + type: object + type: array + x-kubernetes-list-type: atomic + resourceClaims: + items: + properties: + name: + type: string + resourceClaimName: + type: string + resourceClaimTemplateName: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + resources: + properties: + claims: + items: + properties: + name: + type: string + request: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + restartPolicy: + type: string + runtimeClassName: + type: string + schedulerName: + type: string + schedulingGates: + items: + properties: + name: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + schedulingGroup: + properties: + podGroupName: + type: string + type: object + securityContext: + properties: + appArmorProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + fsGroup: + format: int64 + type: integer + fsGroupChangePolicy: + type: string + runAsGroup: + format: int64 + type: integer + runAsNonRoot: + type: boolean + runAsUser: + format: int64 + type: integer + seLinuxChangePolicy: + type: string + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + supplementalGroups: + items: + format: int64 + type: integer + type: array + x-kubernetes-list-type: atomic + supplementalGroupsPolicy: + type: string + sysctls: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object + serviceAccount: + type: string + serviceAccountName: + type: string + setHostnameAsFQDN: + type: boolean + shareProcessNamespace: + type: boolean + subdomain: + type: string + terminationGracePeriodSeconds: + format: int64 + type: integer + tolerations: + items: + properties: + effect: + type: string + key: + type: string + operator: + type: string + tolerationSeconds: + format: int64 + type: integer + value: + type: string + type: object + type: array + x-kubernetes-list-type: atomic + topologySpreadConstraints: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + maxSkew: + format: int32 + type: integer + minDomains: + format: int32 + type: integer + nodeAffinityPolicy: + type: string + nodeTaintsPolicy: + type: string + topologyKey: + type: string + whenUnsatisfiable: + type: string + required: + - maxSkew + - topologyKey + - whenUnsatisfiable + type: object + type: array + x-kubernetes-list-map-keys: + - topologyKey + - whenUnsatisfiable + x-kubernetes-list-type: map + volumes: + items: + properties: + awsElasticBlockStore: + properties: + fsType: + type: string + partition: + format: int32 + type: integer + readOnly: + type: boolean + volumeID: + type: string + required: + - volumeID + type: object + azureDisk: + properties: + cachingMode: + type: string + diskName: + type: string + diskURI: + type: string + fsType: + default: ext4 + type: string + kind: + type: string + readOnly: + default: false + type: boolean + required: + - diskName + - diskURI + type: object + azureFile: + properties: + readOnly: + type: boolean + secretName: + type: string + shareName: + type: string + required: + - secretName + - shareName + type: object + cephfs: + properties: + monitors: + items: + type: string + type: array + x-kubernetes-list-type: atomic + path: + type: string + readOnly: + type: boolean + secretFile: + type: string + secretRef: + properties: + name: + default: '' + type: string + type: object + x-kubernetes-map-type: atomic + user: + type: string + required: + - monitors + type: object + cinder: + properties: + fsType: + type: string + readOnly: + type: boolean + secretRef: + properties: + name: + default: '' + type: string + type: object + x-kubernetes-map-type: atomic + volumeID: + type: string + required: + - volumeID + type: object + configMap: + properties: + defaultMode: + format: int32 + type: integer + items: + items: + properties: + key: + type: string + mode: + format: int32 + type: integer + path: + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + name: + default: '' + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + csi: + properties: + driver: + type: string + fsType: + type: string + nodePublishSecretRef: + properties: + name: + default: '' + type: string + type: object + x-kubernetes-map-type: atomic + readOnly: + type: boolean + volumeAttributes: + additionalProperties: + type: string + type: object + required: + - driver + type: object + downwardAPI: + properties: + defaultMode: + format: int32 + type: integer + items: + items: + properties: + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + mode: + format: int32 + type: integer + path: + type: string + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + required: + - path + type: object + type: array + x-kubernetes-list-type: atomic + type: object + emptyDir: + properties: + medium: + type: string + sizeLimit: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + ephemeral: + properties: + volumeClaimTemplate: + properties: + metadata: + type: object + spec: + properties: + accessModes: + items: + type: string + type: array + x-kubernetes-list-type: atomic + dataSource: + properties: + apiGroup: + type: string + kind: + type: string + name: + type: string + required: + - kind + - name + type: object + x-kubernetes-map-type: atomic + dataSourceRef: + properties: + apiGroup: + type: string + kind: + type: string + name: + type: string + namespace: + type: string + required: + - kind + - name + type: object + resources: + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + selector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + storageClassName: + type: string + volumeAttributesClassName: + type: string + volumeMode: + type: string + volumeName: + type: string + type: object + required: + - spec + type: object + type: object + fc: + properties: + fsType: + type: string + lun: + format: int32 + type: integer + readOnly: + type: boolean + targetWWNs: + items: + type: string + type: array + x-kubernetes-list-type: atomic + wwids: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + flexVolume: + properties: + driver: + type: string + fsType: + type: string + options: + additionalProperties: + type: string + type: object + readOnly: + type: boolean + secretRef: + properties: + name: + default: '' + type: string + type: object + x-kubernetes-map-type: atomic + required: + - driver + type: object + flocker: + properties: + datasetName: + type: string + datasetUUID: + type: string + type: object + gcePersistentDisk: + properties: + fsType: + type: string + partition: + format: int32 + type: integer + pdName: + type: string + readOnly: + type: boolean + required: + - pdName + type: object + gitRepo: + properties: + directory: + type: string + repository: + type: string + revision: + type: string + required: + - repository + type: object + glusterfs: + properties: + endpoints: + type: string + path: + type: string + readOnly: + type: boolean + required: + - endpoints + - path + type: object + hostPath: + properties: + path: + type: string + type: + type: string + required: + - path + type: object + image: + properties: + pullPolicy: + type: string + reference: + type: string + type: object + iscsi: + properties: + chapAuthDiscovery: + type: boolean + chapAuthSession: + type: boolean + fsType: + type: string + initiatorName: + type: string + iqn: + type: string + iscsiInterface: + default: default + type: string + lun: + format: int32 + type: integer + portals: + items: + type: string + type: array + x-kubernetes-list-type: atomic + readOnly: + type: boolean + secretRef: + properties: + name: + default: '' + type: string + type: object + x-kubernetes-map-type: atomic + targetPortal: + type: string + required: + - iqn + - lun + - targetPortal + type: object + name: + type: string + nfs: + properties: + path: + type: string + readOnly: + type: boolean + server: + type: string + required: + - path + - server + type: object + persistentVolumeClaim: + properties: + claimName: + type: string + readOnly: + type: boolean + required: + - claimName + type: object + photonPersistentDisk: + properties: + fsType: + type: string + pdID: + type: string + required: + - pdID + type: object + portworxVolume: + properties: + fsType: + type: string + readOnly: + type: boolean + volumeID: + type: string + required: + - volumeID + type: object + projected: + properties: + defaultMode: + format: int32 + type: integer + sources: + items: + properties: + clusterTrustBundle: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + name: + type: string + optional: + type: boolean + path: + type: string + signerName: + type: string + required: + - path + type: object + configMap: + properties: + items: + items: + properties: + key: + type: string + mode: + format: int32 + type: integer + path: + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + name: + default: '' + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + downwardAPI: + properties: + items: + items: + properties: + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + mode: + format: int32 + type: integer + path: + type: string + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + required: + - path + type: object + type: array + x-kubernetes-list-type: atomic + type: object + podCertificate: + properties: + certificateChainPath: + type: string + credentialBundlePath: + type: string + keyPath: + type: string + keyType: + type: string + maxExpirationSeconds: + format: int32 + type: integer + signerName: + type: string + userAnnotations: + additionalProperties: + type: string + type: object + required: + - keyType + - signerName + type: object + secret: + properties: + items: + items: + properties: + key: + type: string + mode: + format: int32 + type: integer + path: + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + name: + default: '' + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + serviceAccountToken: + properties: + audience: + type: string + expirationSeconds: + format: int64 + type: integer + path: + type: string + required: + - path + type: object + type: object + type: array + x-kubernetes-list-type: atomic + type: object + quobyte: + properties: + group: + type: string + readOnly: + type: boolean + registry: + type: string + tenant: + type: string + user: + type: string + volume: + type: string + required: + - registry + - volume + type: object + rbd: + properties: + fsType: + type: string + image: + type: string + keyring: + default: /etc/ceph/keyring + type: string + monitors: + items: + type: string + type: array + x-kubernetes-list-type: atomic + pool: + default: rbd + type: string + readOnly: + type: boolean + secretRef: + properties: + name: + default: '' + type: string + type: object + x-kubernetes-map-type: atomic + user: + default: admin + type: string + required: + - image + - monitors + type: object + scaleIO: + properties: + fsType: + default: xfs + type: string + gateway: + type: string + protectionDomain: + type: string + readOnly: + type: boolean + secretRef: + properties: + name: + default: '' + type: string + type: object + x-kubernetes-map-type: atomic + sslEnabled: + type: boolean + storageMode: + default: ThinProvisioned + type: string + storagePool: + type: string + system: + type: string + volumeName: + type: string + required: + - gateway + - secretRef + - system + type: object + secret: + properties: + defaultMode: + format: int32 + type: integer + items: + items: + properties: + key: + type: string + mode: + format: int32 + type: integer + path: + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + optional: + type: boolean + secretName: + type: string + type: object + storageos: + properties: + fsType: + type: string + readOnly: + type: boolean + secretRef: + properties: + name: + default: '' + type: string + type: object + x-kubernetes-map-type: atomic + volumeName: + type: string + volumeNamespace: + type: string + type: object + vsphereVolume: + properties: + fsType: + type: string + storagePolicyID: + type: string + storagePolicyName: + type: string + volumePath: + type: string + required: + - volumePath + type: object + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + required: + - containers + type: object + required: + - spec + type: object + service: + type: boolean + volumeClaimTemplates: + items: + properties: + metadata: + properties: + annotations: + additionalProperties: + type: string + type: object + labels: + additionalProperties: + type: string + type: object + name: + type: string + type: object + spec: + properties: + accessModes: + items: + type: string + type: array + x-kubernetes-list-type: atomic + dataSource: + properties: + apiGroup: + type: string + kind: + type: string + name: + type: string + required: + - kind + - name + type: object + x-kubernetes-map-type: atomic + dataSourceRef: + properties: + apiGroup: + type: string + kind: + type: string + name: + type: string + namespace: + type: string + required: + - kind + - name + type: object + resources: + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + selector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + storageClassName: + type: string + volumeAttributesClassName: + type: string + volumeMode: + type: string + volumeName: + type: string + type: object + required: + - spec + type: object + type: array + x-kubernetes-list-type: atomic + required: + - podTemplate + type: object + required: + - spec + type: object + served: true + storage: false + conversion: + strategy: Webhook + webhook: + conversionReviewVersions: + - v1 + - v1beta1 + clientConfig: + service: + name: agent-sandbox-webhook-service + namespace: agent-sandbox-system + path: /convert +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.21.0 + name: sandboxwarmpools.extensions.agents.x-k8s.io +spec: + group: extensions.agents.x-k8s.io + names: + kind: SandboxWarmPool + listKind: SandboxWarmPoolList + plural: sandboxwarmpools + shortNames: + - swp + singular: sandboxwarmpool + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .status.readyReplicas + name: Ready + type: integer + - jsonPath: .spec.replicas + name: Desired + type: integer + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1beta1 + schema: + openAPIV3Schema: + properties: + apiVersion: + type: string + kind: + type: string + metadata: + type: object + spec: + properties: + replicas: + default: 1 + format: int32 + minimum: 0 + type: integer + sandboxTemplateRef: + properties: + name: + type: string + required: + - name + type: object + updateStrategy: + properties: + type: + default: OnReplenish + enum: + - Recreate + - OnReplenish + type: string + type: object + required: + - sandboxTemplateRef + type: object + status: + properties: + readyReplicas: + format: int32 + type: integer + replicas: + format: int32 + type: integer + selector: + type: string + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + scale: + labelSelectorPath: .status.selector + specReplicasPath: .spec.replicas + statusReplicasPath: .status.replicas + status: {} + - additionalPrinterColumns: + - jsonPath: .status.readyReplicas + name: Ready + type: integer + - jsonPath: .spec.replicas + name: Desired + type: integer + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + deprecated: true + deprecationWarning: extensions.agents.x-k8s.io/v1alpha1 SandboxWarmPool is deprecated; + use extensions.agents.x-k8s.io/v1beta1 SandboxWarmPool instead + name: v1alpha1 + schema: + openAPIV3Schema: + properties: + apiVersion: + type: string + kind: + type: string + metadata: + type: object + spec: + properties: + replicas: + format: int32 + minimum: 0 + type: integer + sandboxTemplateRef: + properties: + name: + type: string + required: + - name + type: object + updateStrategy: + properties: + type: + default: OnReplenish + enum: + - Recreate + - OnReplenish + type: string + type: object + required: + - replicas + - sandboxTemplateRef + type: object + status: + properties: + readyReplicas: + format: int32 + type: integer + replicas: + format: int32 + type: integer + selector: + type: string + type: object + required: + - spec + type: object + served: true + storage: false + subresources: + scale: + labelSelectorPath: .status.selector + specReplicasPath: .spec.replicas + statusReplicasPath: .status.replicas + status: {} + conversion: + strategy: Webhook + webhook: + conversionReviewVersions: + - v1 + - v1beta1 + clientConfig: + service: + name: agent-sandbox-webhook-service + namespace: agent-sandbox-system + path: /convert +--- +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: agent-sandbox-controller-extensions +rules: +- apiGroups: + - "" + resources: + - pods + verbs: + - get + - list + - patch + - update + - watch +- apiGroups: + - "" + - events.k8s.io + resources: + - events + verbs: + - create + - patch + - update +- apiGroups: + - agents.x-k8s.io + resources: + - sandboxes + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - extensions.agents.x-k8s.io + resources: + - sandboxclaims + - sandboxtemplates + - sandboxwarmpools + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - extensions.agents.x-k8s.io + resources: + - sandboxclaims/finalizers + - sandboxclaims/status + - sandboxtemplates/finalizers + - sandboxwarmpools/finalizers + - sandboxwarmpools/status + verbs: + - get + - patch + - update +- apiGroups: + - networking.k8s.io + resources: + - networkpolicies + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +# NOTE: the agent-sandbox-controller Deployment that upstream ships in this +# extensions bundle has been removed to avoid a duplicate with the one in +# agent-sandbox-v0.5.1-manifest.yaml (ArgoCD RepeatedResourceWarning). The +# manifest.yaml Deployment now carries the union of both (--extensions + the +# 9443 webhook port + config-volume), so a single controller serves both the +# core API and the extensions conversion webhook. +--- + +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: agent-sandbox-controller-extensions +subjects: +- kind: ServiceAccount + name: agent-sandbox-controller + namespace: agent-sandbox-system +roleRef: + kind: ClusterRole + name: agent-sandbox-controller-extensions + apiGroup: rbac.authorization.k8s.io +--- diff --git a/gitops/addons/charts/agent-sandbox/upstream/templates/agent-sandbox-v0.5.1-manifest.yaml b/gitops/addons/charts/agent-sandbox/upstream/templates/agent-sandbox-v0.5.1-manifest.yaml new file mode 100644 index 00000000..a38aad60 --- /dev/null +++ b/gitops/addons/charts/agent-sandbox/upstream/templates/agent-sandbox-v0.5.1-manifest.yaml @@ -0,0 +1,8271 @@ +--- +kind: Namespace +apiVersion: v1 +metadata: + name: agent-sandbox-system + +--- + +kind: ServiceAccount +apiVersion: v1 +metadata: + name: agent-sandbox-controller + namespace: agent-sandbox-system + labels: + app: agent-sandbox-controller + +--- + +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: agent-sandbox-controller +subjects: +- kind: ServiceAccount + name: agent-sandbox-controller + namespace: agent-sandbox-system +roleRef: + kind: ClusterRole + name: agent-sandbox-controller + apiGroup: rbac.authorization.k8s.io + +--- + +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: agent-sandbox-controller + namespace: agent-sandbox-system +rules: +- apiGroups: + - "" + resources: + - secrets + verbs: + - create +- apiGroups: + - "" + resourceNames: + - agent-sandbox-webhook-certs + resources: + - secrets + verbs: + - get + - patch + - update + +--- + +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: agent-sandbox-controller + namespace: agent-sandbox-system +subjects: +- kind: ServiceAccount + name: agent-sandbox-controller + namespace: agent-sandbox-system +roleRef: + kind: Role + name: agent-sandbox-controller + apiGroup: rbac.authorization.k8s.io + +--- + +kind: Service +apiVersion: v1 +metadata: + name: agent-sandbox-controller + namespace: agent-sandbox-system + labels: + app: agent-sandbox-controller +spec: + selector: + app: agent-sandbox-controller + ports: + - name: metrics + port: 8080 + targetPort: metrics + protocol: TCP + +--- + +kind: Deployment +apiVersion: apps/v1 +metadata: + name: agent-sandbox-controller + namespace: agent-sandbox-system + labels: + app: agent-sandbox-controller +spec: + replicas: 1 + selector: + matchLabels: + app: agent-sandbox-controller + template: + metadata: + labels: + app: agent-sandbox-controller + spec: + serviceAccountName: agent-sandbox-controller + containers: + - name: agent-sandbox-controller + image: registry.k8s.io/agent-sandbox/agent-sandbox-controller:v0.5.1 + # Merged controller: --extensions (so the controller knows the + # SandboxTemplate/SandboxClaim/SandboxWarmPool extension types and the + # /convert conversion webhook can answer) PLUS the webhook port 9443 + # that agent-sandbox-webhook-service targets. Upstream ships these as + # two separate Deployments (manifest = core+webhook port, extensions = + # --extensions but no port); vendoring both into one chart made ArgoCD + # see the Deployment twice (RepeatedResourceWarning) and whichever + # applied last won β€” breaking either the conversion webhook (no 9443) + # or the extension types (no --extensions). This is the union of both. + args: + - --leader-elect=true + - --extensions + ports: + - name: metrics + containerPort: 8080 + protocol: TCP + - name: healthz + containerPort: 8081 + protocol: TCP + - name: webhook + containerPort: 9443 + protocol: TCP + volumeMounts: + - name: config-volume + mountPath: /etc/sandbox-config + readOnly: true + volumes: + - name: config-volume + configMap: + name: agent-sandbox-config + optional: true + +--- + +kind: Service +apiVersion: v1 +metadata: + name: agent-sandbox-webhook-service + namespace: agent-sandbox-system +spec: + selector: + app: agent-sandbox-controller + ports: + - name: webhook + port: 443 + targetPort: 9443 + protocol: TCP + +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.21.0 + name: sandboxes.agents.x-k8s.io +spec: + group: agents.x-k8s.io + names: + kind: Sandbox + listKind: SandboxList + plural: sandboxes + shortNames: + - sandbox + singular: sandbox + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .status.conditions[?(@.type=='Ready')].status + name: Ready + type: string + - jsonPath: .status.conditions[?(@.type=='Ready')].reason + name: Reason + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1beta1 + schema: + openAPIV3Schema: + properties: + apiVersion: + type: string + kind: + type: string + metadata: + type: object + spec: + properties: + operatingMode: + default: Running + enum: + - Running + - Suspended + type: string + podTemplate: + properties: + metadata: + properties: + annotations: + additionalProperties: + type: string + type: object + labels: + additionalProperties: + type: string + type: object + type: object + spec: + properties: + activeDeadlineSeconds: + format: int64 + type: integer + affinity: + properties: + nodeAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + preference: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchFields: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + weight: + format: int32 + type: integer + required: + - preference + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + properties: + nodeSelectorTerms: + items: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchFields: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + type: array + x-kubernetes-list-type: atomic + required: + - nodeSelectorTerms + type: object + x-kubernetes-map-type: atomic + type: object + podAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + podAffinityTerm: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + weight: + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + type: array + x-kubernetes-list-type: atomic + type: object + podAntiAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + podAffinityTerm: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + weight: + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + type: array + x-kubernetes-list-type: atomic + type: object + type: object + automountServiceAccountToken: + type: boolean + containers: + items: + properties: + args: + items: + type: string + type: array + x-kubernetes-list-type: atomic + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + env: + items: + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + configMapKeyRef: + properties: + key: + type: string + name: + default: '' + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + fileKeyRef: + properties: + key: + type: string + optional: + default: false + type: boolean + path: + type: string + volumeName: + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + properties: + key: + type: string + name: + default: '' + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + envFrom: + items: + properties: + configMapRef: + properties: + name: + default: '' + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + type: string + secretRef: + properties: + name: + default: '' + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + x-kubernetes-list-type: atomic + image: + type: string + imagePullPolicy: + type: string + lifecycle: + properties: + postStart: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + preStop: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + stopSignal: + type: string + type: object + livenessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: '' + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + name: + type: string + ports: + items: + properties: + containerPort: + format: int32 + type: integer + hostIP: + type: string + hostPort: + format: int32 + type: integer + name: + type: string + protocol: + default: TCP + type: string + required: + - containerPort + type: object + type: array + x-kubernetes-list-map-keys: + - containerPort + - protocol + x-kubernetes-list-type: map + readinessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: '' + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + resizePolicy: + items: + properties: + resourceName: + type: string + restartPolicy: + type: string + required: + - resourceName + - restartPolicy + type: object + type: array + x-kubernetes-list-type: atomic + resources: + properties: + claims: + items: + properties: + name: + type: string + request: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + restartPolicy: + type: string + restartPolicyRules: + items: + properties: + action: + type: string + exitCodes: + properties: + operator: + type: string + values: + items: + format: int32 + type: integer + type: array + x-kubernetes-list-type: set + required: + - operator + type: object + required: + - action + type: object + type: array + x-kubernetes-list-type: atomic + securityContext: + properties: + allowPrivilegeEscalation: + type: boolean + appArmorProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + capabilities: + properties: + add: + items: + type: string + type: array + x-kubernetes-list-type: atomic + drop: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + privileged: + type: boolean + procMount: + type: string + readOnlyRootFilesystem: + type: boolean + runAsGroup: + format: int64 + type: integer + runAsNonRoot: + type: boolean + runAsUser: + format: int64 + type: integer + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object + startupProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: '' + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + stdin: + type: boolean + stdinOnce: + type: boolean + terminationMessagePath: + type: string + terminationMessagePolicy: + type: string + tty: + type: boolean + volumeDevices: + items: + properties: + devicePath: + type: string + name: + type: string + required: + - devicePath + - name + type: object + type: array + x-kubernetes-list-map-keys: + - devicePath + x-kubernetes-list-type: map + volumeMounts: + items: + properties: + mountPath: + type: string + mountPropagation: + type: string + name: + type: string + readOnly: + type: boolean + recursiveReadOnly: + type: string + subPath: + type: string + subPathExpr: + type: string + required: + - mountPath + - name + type: object + type: array + x-kubernetes-list-map-keys: + - mountPath + x-kubernetes-list-type: map + workingDir: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + dnsConfig: + properties: + nameservers: + items: + type: string + type: array + x-kubernetes-list-type: atomic + options: + items: + properties: + name: + type: string + value: + type: string + type: object + type: array + x-kubernetes-list-type: atomic + searches: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + dnsPolicy: + type: string + enableServiceLinks: + type: boolean + ephemeralContainers: + items: + properties: + args: + items: + type: string + type: array + x-kubernetes-list-type: atomic + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + env: + items: + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + configMapKeyRef: + properties: + key: + type: string + name: + default: '' + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + fileKeyRef: + properties: + key: + type: string + optional: + default: false + type: boolean + path: + type: string + volumeName: + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + properties: + key: + type: string + name: + default: '' + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + envFrom: + items: + properties: + configMapRef: + properties: + name: + default: '' + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + type: string + secretRef: + properties: + name: + default: '' + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + x-kubernetes-list-type: atomic + image: + type: string + imagePullPolicy: + type: string + lifecycle: + properties: + postStart: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + preStop: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + stopSignal: + type: string + type: object + livenessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: '' + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + name: + type: string + ports: + items: + properties: + containerPort: + format: int32 + type: integer + hostIP: + type: string + hostPort: + format: int32 + type: integer + name: + type: string + protocol: + default: TCP + type: string + required: + - containerPort + type: object + type: array + x-kubernetes-list-map-keys: + - containerPort + - protocol + x-kubernetes-list-type: map + readinessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: '' + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + resizePolicy: + items: + properties: + resourceName: + type: string + restartPolicy: + type: string + required: + - resourceName + - restartPolicy + type: object + type: array + x-kubernetes-list-type: atomic + resources: + properties: + claims: + items: + properties: + name: + type: string + request: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + restartPolicy: + type: string + restartPolicyRules: + items: + properties: + action: + type: string + exitCodes: + properties: + operator: + type: string + values: + items: + format: int32 + type: integer + type: array + x-kubernetes-list-type: set + required: + - operator + type: object + required: + - action + type: object + type: array + x-kubernetes-list-type: atomic + securityContext: + properties: + allowPrivilegeEscalation: + type: boolean + appArmorProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + capabilities: + properties: + add: + items: + type: string + type: array + x-kubernetes-list-type: atomic + drop: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + privileged: + type: boolean + procMount: + type: string + readOnlyRootFilesystem: + type: boolean + runAsGroup: + format: int64 + type: integer + runAsNonRoot: + type: boolean + runAsUser: + format: int64 + type: integer + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object + startupProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: '' + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + stdin: + type: boolean + stdinOnce: + type: boolean + targetContainerName: + type: string + terminationMessagePath: + type: string + terminationMessagePolicy: + type: string + tty: + type: boolean + volumeDevices: + items: + properties: + devicePath: + type: string + name: + type: string + required: + - devicePath + - name + type: object + type: array + x-kubernetes-list-map-keys: + - devicePath + x-kubernetes-list-type: map + volumeMounts: + items: + properties: + mountPath: + type: string + mountPropagation: + type: string + name: + type: string + readOnly: + type: boolean + recursiveReadOnly: + type: string + subPath: + type: string + subPathExpr: + type: string + required: + - mountPath + - name + type: object + type: array + x-kubernetes-list-map-keys: + - mountPath + x-kubernetes-list-type: map + workingDir: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + hostAliases: + items: + properties: + hostnames: + items: + type: string + type: array + x-kubernetes-list-type: atomic + ip: + type: string + required: + - ip + type: object + type: array + x-kubernetes-list-map-keys: + - ip + x-kubernetes-list-type: map + hostIPC: + type: boolean + hostNetwork: + type: boolean + hostPID: + type: boolean + hostUsers: + type: boolean + hostname: + type: string + hostnameOverride: + type: string + imagePullSecrets: + items: + properties: + name: + default: '' + type: string + type: object + x-kubernetes-map-type: atomic + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + initContainers: + items: + properties: + args: + items: + type: string + type: array + x-kubernetes-list-type: atomic + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + env: + items: + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + configMapKeyRef: + properties: + key: + type: string + name: + default: '' + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + fileKeyRef: + properties: + key: + type: string + optional: + default: false + type: boolean + path: + type: string + volumeName: + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + properties: + key: + type: string + name: + default: '' + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + envFrom: + items: + properties: + configMapRef: + properties: + name: + default: '' + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + type: string + secretRef: + properties: + name: + default: '' + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + x-kubernetes-list-type: atomic + image: + type: string + imagePullPolicy: + type: string + lifecycle: + properties: + postStart: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + preStop: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + stopSignal: + type: string + type: object + livenessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: '' + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + name: + type: string + ports: + items: + properties: + containerPort: + format: int32 + type: integer + hostIP: + type: string + hostPort: + format: int32 + type: integer + name: + type: string + protocol: + default: TCP + type: string + required: + - containerPort + type: object + type: array + x-kubernetes-list-map-keys: + - containerPort + - protocol + x-kubernetes-list-type: map + readinessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: '' + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + resizePolicy: + items: + properties: + resourceName: + type: string + restartPolicy: + type: string + required: + - resourceName + - restartPolicy + type: object + type: array + x-kubernetes-list-type: atomic + resources: + properties: + claims: + items: + properties: + name: + type: string + request: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + restartPolicy: + type: string + restartPolicyRules: + items: + properties: + action: + type: string + exitCodes: + properties: + operator: + type: string + values: + items: + format: int32 + type: integer + type: array + x-kubernetes-list-type: set + required: + - operator + type: object + required: + - action + type: object + type: array + x-kubernetes-list-type: atomic + securityContext: + properties: + allowPrivilegeEscalation: + type: boolean + appArmorProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + capabilities: + properties: + add: + items: + type: string + type: array + x-kubernetes-list-type: atomic + drop: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + privileged: + type: boolean + procMount: + type: string + readOnlyRootFilesystem: + type: boolean + runAsGroup: + format: int64 + type: integer + runAsNonRoot: + type: boolean + runAsUser: + format: int64 + type: integer + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object + startupProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: '' + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + stdin: + type: boolean + stdinOnce: + type: boolean + terminationMessagePath: + type: string + terminationMessagePolicy: + type: string + tty: + type: boolean + volumeDevices: + items: + properties: + devicePath: + type: string + name: + type: string + required: + - devicePath + - name + type: object + type: array + x-kubernetes-list-map-keys: + - devicePath + x-kubernetes-list-type: map + volumeMounts: + items: + properties: + mountPath: + type: string + mountPropagation: + type: string + name: + type: string + readOnly: + type: boolean + recursiveReadOnly: + type: string + subPath: + type: string + subPathExpr: + type: string + required: + - mountPath + - name + type: object + type: array + x-kubernetes-list-map-keys: + - mountPath + x-kubernetes-list-type: map + workingDir: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + nodeName: + type: string + nodeSelector: + additionalProperties: + type: string + type: object + x-kubernetes-map-type: atomic + os: + properties: + name: + type: string + required: + - name + type: object + overhead: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + preemptionPolicy: + type: string + priority: + format: int32 + type: integer + priorityClassName: + type: string + readinessGates: + items: + properties: + conditionType: + type: string + required: + - conditionType + type: object + type: array + x-kubernetes-list-type: atomic + resourceClaims: + items: + properties: + name: + type: string + resourceClaimName: + type: string + resourceClaimTemplateName: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + resources: + properties: + claims: + items: + properties: + name: + type: string + request: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + restartPolicy: + type: string + runtimeClassName: + type: string + schedulerName: + type: string + schedulingGates: + items: + properties: + name: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + schedulingGroup: + properties: + podGroupName: + type: string + type: object + securityContext: + properties: + appArmorProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + fsGroup: + format: int64 + type: integer + fsGroupChangePolicy: + type: string + runAsGroup: + format: int64 + type: integer + runAsNonRoot: + type: boolean + runAsUser: + format: int64 + type: integer + seLinuxChangePolicy: + type: string + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + supplementalGroups: + items: + format: int64 + type: integer + type: array + x-kubernetes-list-type: atomic + supplementalGroupsPolicy: + type: string + sysctls: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object + serviceAccount: + type: string + serviceAccountName: + type: string + setHostnameAsFQDN: + type: boolean + shareProcessNamespace: + type: boolean + subdomain: + type: string + terminationGracePeriodSeconds: + format: int64 + type: integer + tolerations: + items: + properties: + effect: + type: string + key: + type: string + operator: + type: string + tolerationSeconds: + format: int64 + type: integer + value: + type: string + type: object + type: array + x-kubernetes-list-type: atomic + topologySpreadConstraints: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + maxSkew: + format: int32 + type: integer + minDomains: + format: int32 + type: integer + nodeAffinityPolicy: + type: string + nodeTaintsPolicy: + type: string + topologyKey: + type: string + whenUnsatisfiable: + type: string + required: + - maxSkew + - topologyKey + - whenUnsatisfiable + type: object + type: array + x-kubernetes-list-map-keys: + - topologyKey + - whenUnsatisfiable + x-kubernetes-list-type: map + volumes: + items: + properties: + awsElasticBlockStore: + properties: + fsType: + type: string + partition: + format: int32 + type: integer + readOnly: + type: boolean + volumeID: + type: string + required: + - volumeID + type: object + azureDisk: + properties: + cachingMode: + type: string + diskName: + type: string + diskURI: + type: string + fsType: + default: ext4 + type: string + kind: + type: string + readOnly: + default: false + type: boolean + required: + - diskName + - diskURI + type: object + azureFile: + properties: + readOnly: + type: boolean + secretName: + type: string + shareName: + type: string + required: + - secretName + - shareName + type: object + cephfs: + properties: + monitors: + items: + type: string + type: array + x-kubernetes-list-type: atomic + path: + type: string + readOnly: + type: boolean + secretFile: + type: string + secretRef: + properties: + name: + default: '' + type: string + type: object + x-kubernetes-map-type: atomic + user: + type: string + required: + - monitors + type: object + cinder: + properties: + fsType: + type: string + readOnly: + type: boolean + secretRef: + properties: + name: + default: '' + type: string + type: object + x-kubernetes-map-type: atomic + volumeID: + type: string + required: + - volumeID + type: object + configMap: + properties: + defaultMode: + format: int32 + type: integer + items: + items: + properties: + key: + type: string + mode: + format: int32 + type: integer + path: + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + name: + default: '' + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + csi: + properties: + driver: + type: string + fsType: + type: string + nodePublishSecretRef: + properties: + name: + default: '' + type: string + type: object + x-kubernetes-map-type: atomic + readOnly: + type: boolean + volumeAttributes: + additionalProperties: + type: string + type: object + required: + - driver + type: object + downwardAPI: + properties: + defaultMode: + format: int32 + type: integer + items: + items: + properties: + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + mode: + format: int32 + type: integer + path: + type: string + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + required: + - path + type: object + type: array + x-kubernetes-list-type: atomic + type: object + emptyDir: + properties: + medium: + type: string + sizeLimit: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + ephemeral: + properties: + volumeClaimTemplate: + properties: + metadata: + type: object + spec: + properties: + accessModes: + items: + type: string + type: array + x-kubernetes-list-type: atomic + dataSource: + properties: + apiGroup: + type: string + kind: + type: string + name: + type: string + required: + - kind + - name + type: object + x-kubernetes-map-type: atomic + dataSourceRef: + properties: + apiGroup: + type: string + kind: + type: string + name: + type: string + namespace: + type: string + required: + - kind + - name + type: object + resources: + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + selector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + storageClassName: + type: string + volumeAttributesClassName: + type: string + volumeMode: + type: string + volumeName: + type: string + type: object + required: + - spec + type: object + type: object + fc: + properties: + fsType: + type: string + lun: + format: int32 + type: integer + readOnly: + type: boolean + targetWWNs: + items: + type: string + type: array + x-kubernetes-list-type: atomic + wwids: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + flexVolume: + properties: + driver: + type: string + fsType: + type: string + options: + additionalProperties: + type: string + type: object + readOnly: + type: boolean + secretRef: + properties: + name: + default: '' + type: string + type: object + x-kubernetes-map-type: atomic + required: + - driver + type: object + flocker: + properties: + datasetName: + type: string + datasetUUID: + type: string + type: object + gcePersistentDisk: + properties: + fsType: + type: string + partition: + format: int32 + type: integer + pdName: + type: string + readOnly: + type: boolean + required: + - pdName + type: object + gitRepo: + properties: + directory: + type: string + repository: + type: string + revision: + type: string + required: + - repository + type: object + glusterfs: + properties: + endpoints: + type: string + path: + type: string + readOnly: + type: boolean + required: + - endpoints + - path + type: object + hostPath: + properties: + path: + type: string + type: + type: string + required: + - path + type: object + image: + properties: + pullPolicy: + type: string + reference: + type: string + type: object + iscsi: + properties: + chapAuthDiscovery: + type: boolean + chapAuthSession: + type: boolean + fsType: + type: string + initiatorName: + type: string + iqn: + type: string + iscsiInterface: + default: default + type: string + lun: + format: int32 + type: integer + portals: + items: + type: string + type: array + x-kubernetes-list-type: atomic + readOnly: + type: boolean + secretRef: + properties: + name: + default: '' + type: string + type: object + x-kubernetes-map-type: atomic + targetPortal: + type: string + required: + - iqn + - lun + - targetPortal + type: object + name: + type: string + nfs: + properties: + path: + type: string + readOnly: + type: boolean + server: + type: string + required: + - path + - server + type: object + persistentVolumeClaim: + properties: + claimName: + type: string + readOnly: + type: boolean + required: + - claimName + type: object + photonPersistentDisk: + properties: + fsType: + type: string + pdID: + type: string + required: + - pdID + type: object + portworxVolume: + properties: + fsType: + type: string + readOnly: + type: boolean + volumeID: + type: string + required: + - volumeID + type: object + projected: + properties: + defaultMode: + format: int32 + type: integer + sources: + items: + properties: + clusterTrustBundle: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + name: + type: string + optional: + type: boolean + path: + type: string + signerName: + type: string + required: + - path + type: object + configMap: + properties: + items: + items: + properties: + key: + type: string + mode: + format: int32 + type: integer + path: + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + name: + default: '' + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + downwardAPI: + properties: + items: + items: + properties: + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + mode: + format: int32 + type: integer + path: + type: string + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + required: + - path + type: object + type: array + x-kubernetes-list-type: atomic + type: object + podCertificate: + properties: + certificateChainPath: + type: string + credentialBundlePath: + type: string + keyPath: + type: string + keyType: + type: string + maxExpirationSeconds: + format: int32 + type: integer + signerName: + type: string + userAnnotations: + additionalProperties: + type: string + type: object + required: + - keyType + - signerName + type: object + secret: + properties: + items: + items: + properties: + key: + type: string + mode: + format: int32 + type: integer + path: + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + name: + default: '' + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + serviceAccountToken: + properties: + audience: + type: string + expirationSeconds: + format: int64 + type: integer + path: + type: string + required: + - path + type: object + type: object + type: array + x-kubernetes-list-type: atomic + type: object + quobyte: + properties: + group: + type: string + readOnly: + type: boolean + registry: + type: string + tenant: + type: string + user: + type: string + volume: + type: string + required: + - registry + - volume + type: object + rbd: + properties: + fsType: + type: string + image: + type: string + keyring: + default: /etc/ceph/keyring + type: string + monitors: + items: + type: string + type: array + x-kubernetes-list-type: atomic + pool: + default: rbd + type: string + readOnly: + type: boolean + secretRef: + properties: + name: + default: '' + type: string + type: object + x-kubernetes-map-type: atomic + user: + default: admin + type: string + required: + - image + - monitors + type: object + scaleIO: + properties: + fsType: + default: xfs + type: string + gateway: + type: string + protectionDomain: + type: string + readOnly: + type: boolean + secretRef: + properties: + name: + default: '' + type: string + type: object + x-kubernetes-map-type: atomic + sslEnabled: + type: boolean + storageMode: + default: ThinProvisioned + type: string + storagePool: + type: string + system: + type: string + volumeName: + type: string + required: + - gateway + - secretRef + - system + type: object + secret: + properties: + defaultMode: + format: int32 + type: integer + items: + items: + properties: + key: + type: string + mode: + format: int32 + type: integer + path: + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + optional: + type: boolean + secretName: + type: string + type: object + storageos: + properties: + fsType: + type: string + readOnly: + type: boolean + secretRef: + properties: + name: + default: '' + type: string + type: object + x-kubernetes-map-type: atomic + volumeName: + type: string + volumeNamespace: + type: string + type: object + vsphereVolume: + properties: + fsType: + type: string + storagePolicyID: + type: string + storagePolicyName: + type: string + volumePath: + type: string + required: + - volumePath + type: object + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + required: + - containers + type: object + required: + - spec + type: object + service: + type: boolean + shutdownPolicy: + default: Retain + enum: + - Delete + - Retain + type: string + shutdownTime: + format: date-time + type: string + volumeClaimTemplates: + items: + properties: + metadata: + properties: + annotations: + additionalProperties: + type: string + type: object + labels: + additionalProperties: + type: string + type: object + name: + type: string + type: object + spec: + properties: + accessModes: + items: + type: string + type: array + x-kubernetes-list-type: atomic + dataSource: + properties: + apiGroup: + type: string + kind: + type: string + name: + type: string + required: + - kind + - name + type: object + x-kubernetes-map-type: atomic + dataSourceRef: + properties: + apiGroup: + type: string + kind: + type: string + name: + type: string + namespace: + type: string + required: + - kind + - name + type: object + resources: + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + selector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + storageClassName: + type: string + volumeAttributesClassName: + type: string + volumeMode: + type: string + volumeName: + type: string + type: object + required: + - spec + type: object + type: array + x-kubernetes-list-type: atomic + required: + - podTemplate + type: object + status: + properties: + conditions: + items: + properties: + lastTransitionTime: + format: date-time + type: string + message: + maxLength: 32768 + type: string + observedGeneration: + format: int64 + minimum: 0 + type: integer + reason: + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + enum: + - 'True' + - 'False' + - Unknown + type: string + type: + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + nodeName: + type: string + podIPs: + items: + type: string + type: array + selector: + type: string + service: + type: string + serviceFQDN: + type: string + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} + - deprecated: true + deprecationWarning: agents.x-k8s.io/v1alpha1 Sandbox is deprecated; use agents.x-k8s.io/v1beta1 + Sandbox instead + name: v1alpha1 + schema: + openAPIV3Schema: + properties: + apiVersion: + type: string + kind: + type: string + metadata: + type: object + spec: + properties: + podTemplate: + properties: + metadata: + properties: + annotations: + additionalProperties: + type: string + type: object + labels: + additionalProperties: + type: string + type: object + type: object + spec: + properties: + activeDeadlineSeconds: + format: int64 + type: integer + affinity: + properties: + nodeAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + preference: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchFields: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + weight: + format: int32 + type: integer + required: + - preference + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + properties: + nodeSelectorTerms: + items: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchFields: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + type: array + x-kubernetes-list-type: atomic + required: + - nodeSelectorTerms + type: object + x-kubernetes-map-type: atomic + type: object + podAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + podAffinityTerm: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + weight: + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + type: array + x-kubernetes-list-type: atomic + type: object + podAntiAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + podAffinityTerm: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + weight: + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + type: array + x-kubernetes-list-type: atomic + type: object + type: object + automountServiceAccountToken: + type: boolean + containers: + items: + properties: + args: + items: + type: string + type: array + x-kubernetes-list-type: atomic + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + env: + items: + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + configMapKeyRef: + properties: + key: + type: string + name: + default: '' + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + fileKeyRef: + properties: + key: + type: string + optional: + default: false + type: boolean + path: + type: string + volumeName: + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + properties: + key: + type: string + name: + default: '' + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + envFrom: + items: + properties: + configMapRef: + properties: + name: + default: '' + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + type: string + secretRef: + properties: + name: + default: '' + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + x-kubernetes-list-type: atomic + image: + type: string + imagePullPolicy: + type: string + lifecycle: + properties: + postStart: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + preStop: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + stopSignal: + type: string + type: object + livenessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: '' + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + name: + type: string + ports: + items: + properties: + containerPort: + format: int32 + type: integer + hostIP: + type: string + hostPort: + format: int32 + type: integer + name: + type: string + protocol: + default: TCP + type: string + required: + - containerPort + type: object + type: array + x-kubernetes-list-map-keys: + - containerPort + - protocol + x-kubernetes-list-type: map + readinessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: '' + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + resizePolicy: + items: + properties: + resourceName: + type: string + restartPolicy: + type: string + required: + - resourceName + - restartPolicy + type: object + type: array + x-kubernetes-list-type: atomic + resources: + properties: + claims: + items: + properties: + name: + type: string + request: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + restartPolicy: + type: string + restartPolicyRules: + items: + properties: + action: + type: string + exitCodes: + properties: + operator: + type: string + values: + items: + format: int32 + type: integer + type: array + x-kubernetes-list-type: set + required: + - operator + type: object + required: + - action + type: object + type: array + x-kubernetes-list-type: atomic + securityContext: + properties: + allowPrivilegeEscalation: + type: boolean + appArmorProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + capabilities: + properties: + add: + items: + type: string + type: array + x-kubernetes-list-type: atomic + drop: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + privileged: + type: boolean + procMount: + type: string + readOnlyRootFilesystem: + type: boolean + runAsGroup: + format: int64 + type: integer + runAsNonRoot: + type: boolean + runAsUser: + format: int64 + type: integer + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object + startupProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: '' + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + stdin: + type: boolean + stdinOnce: + type: boolean + terminationMessagePath: + type: string + terminationMessagePolicy: + type: string + tty: + type: boolean + volumeDevices: + items: + properties: + devicePath: + type: string + name: + type: string + required: + - devicePath + - name + type: object + type: array + x-kubernetes-list-map-keys: + - devicePath + x-kubernetes-list-type: map + volumeMounts: + items: + properties: + mountPath: + type: string + mountPropagation: + type: string + name: + type: string + readOnly: + type: boolean + recursiveReadOnly: + type: string + subPath: + type: string + subPathExpr: + type: string + required: + - mountPath + - name + type: object + type: array + x-kubernetes-list-map-keys: + - mountPath + x-kubernetes-list-type: map + workingDir: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + dnsConfig: + properties: + nameservers: + items: + type: string + type: array + x-kubernetes-list-type: atomic + options: + items: + properties: + name: + type: string + value: + type: string + type: object + type: array + x-kubernetes-list-type: atomic + searches: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + dnsPolicy: + type: string + enableServiceLinks: + type: boolean + ephemeralContainers: + items: + properties: + args: + items: + type: string + type: array + x-kubernetes-list-type: atomic + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + env: + items: + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + configMapKeyRef: + properties: + key: + type: string + name: + default: '' + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + fileKeyRef: + properties: + key: + type: string + optional: + default: false + type: boolean + path: + type: string + volumeName: + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + properties: + key: + type: string + name: + default: '' + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + envFrom: + items: + properties: + configMapRef: + properties: + name: + default: '' + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + type: string + secretRef: + properties: + name: + default: '' + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + x-kubernetes-list-type: atomic + image: + type: string + imagePullPolicy: + type: string + lifecycle: + properties: + postStart: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + preStop: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + stopSignal: + type: string + type: object + livenessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: '' + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + name: + type: string + ports: + items: + properties: + containerPort: + format: int32 + type: integer + hostIP: + type: string + hostPort: + format: int32 + type: integer + name: + type: string + protocol: + default: TCP + type: string + required: + - containerPort + type: object + type: array + x-kubernetes-list-map-keys: + - containerPort + - protocol + x-kubernetes-list-type: map + readinessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: '' + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + resizePolicy: + items: + properties: + resourceName: + type: string + restartPolicy: + type: string + required: + - resourceName + - restartPolicy + type: object + type: array + x-kubernetes-list-type: atomic + resources: + properties: + claims: + items: + properties: + name: + type: string + request: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + restartPolicy: + type: string + restartPolicyRules: + items: + properties: + action: + type: string + exitCodes: + properties: + operator: + type: string + values: + items: + format: int32 + type: integer + type: array + x-kubernetes-list-type: set + required: + - operator + type: object + required: + - action + type: object + type: array + x-kubernetes-list-type: atomic + securityContext: + properties: + allowPrivilegeEscalation: + type: boolean + appArmorProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + capabilities: + properties: + add: + items: + type: string + type: array + x-kubernetes-list-type: atomic + drop: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + privileged: + type: boolean + procMount: + type: string + readOnlyRootFilesystem: + type: boolean + runAsGroup: + format: int64 + type: integer + runAsNonRoot: + type: boolean + runAsUser: + format: int64 + type: integer + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object + startupProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: '' + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + stdin: + type: boolean + stdinOnce: + type: boolean + targetContainerName: + type: string + terminationMessagePath: + type: string + terminationMessagePolicy: + type: string + tty: + type: boolean + volumeDevices: + items: + properties: + devicePath: + type: string + name: + type: string + required: + - devicePath + - name + type: object + type: array + x-kubernetes-list-map-keys: + - devicePath + x-kubernetes-list-type: map + volumeMounts: + items: + properties: + mountPath: + type: string + mountPropagation: + type: string + name: + type: string + readOnly: + type: boolean + recursiveReadOnly: + type: string + subPath: + type: string + subPathExpr: + type: string + required: + - mountPath + - name + type: object + type: array + x-kubernetes-list-map-keys: + - mountPath + x-kubernetes-list-type: map + workingDir: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + hostAliases: + items: + properties: + hostnames: + items: + type: string + type: array + x-kubernetes-list-type: atomic + ip: + type: string + required: + - ip + type: object + type: array + x-kubernetes-list-map-keys: + - ip + x-kubernetes-list-type: map + hostIPC: + type: boolean + hostNetwork: + type: boolean + hostPID: + type: boolean + hostUsers: + type: boolean + hostname: + type: string + hostnameOverride: + type: string + imagePullSecrets: + items: + properties: + name: + default: '' + type: string + type: object + x-kubernetes-map-type: atomic + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + initContainers: + items: + properties: + args: + items: + type: string + type: array + x-kubernetes-list-type: atomic + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + env: + items: + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + configMapKeyRef: + properties: + key: + type: string + name: + default: '' + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + fileKeyRef: + properties: + key: + type: string + optional: + default: false + type: boolean + path: + type: string + volumeName: + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + properties: + key: + type: string + name: + default: '' + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + envFrom: + items: + properties: + configMapRef: + properties: + name: + default: '' + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + type: string + secretRef: + properties: + name: + default: '' + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + x-kubernetes-list-type: atomic + image: + type: string + imagePullPolicy: + type: string + lifecycle: + properties: + postStart: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + preStop: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + stopSignal: + type: string + type: object + livenessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: '' + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + name: + type: string + ports: + items: + properties: + containerPort: + format: int32 + type: integer + hostIP: + type: string + hostPort: + format: int32 + type: integer + name: + type: string + protocol: + default: TCP + type: string + required: + - containerPort + type: object + type: array + x-kubernetes-list-map-keys: + - containerPort + - protocol + x-kubernetes-list-type: map + readinessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: '' + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + resizePolicy: + items: + properties: + resourceName: + type: string + restartPolicy: + type: string + required: + - resourceName + - restartPolicy + type: object + type: array + x-kubernetes-list-type: atomic + resources: + properties: + claims: + items: + properties: + name: + type: string + request: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + restartPolicy: + type: string + restartPolicyRules: + items: + properties: + action: + type: string + exitCodes: + properties: + operator: + type: string + values: + items: + format: int32 + type: integer + type: array + x-kubernetes-list-type: set + required: + - operator + type: object + required: + - action + type: object + type: array + x-kubernetes-list-type: atomic + securityContext: + properties: + allowPrivilegeEscalation: + type: boolean + appArmorProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + capabilities: + properties: + add: + items: + type: string + type: array + x-kubernetes-list-type: atomic + drop: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + privileged: + type: boolean + procMount: + type: string + readOnlyRootFilesystem: + type: boolean + runAsGroup: + format: int64 + type: integer + runAsNonRoot: + type: boolean + runAsUser: + format: int64 + type: integer + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object + startupProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: '' + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + stdin: + type: boolean + stdinOnce: + type: boolean + terminationMessagePath: + type: string + terminationMessagePolicy: + type: string + tty: + type: boolean + volumeDevices: + items: + properties: + devicePath: + type: string + name: + type: string + required: + - devicePath + - name + type: object + type: array + x-kubernetes-list-map-keys: + - devicePath + x-kubernetes-list-type: map + volumeMounts: + items: + properties: + mountPath: + type: string + mountPropagation: + type: string + name: + type: string + readOnly: + type: boolean + recursiveReadOnly: + type: string + subPath: + type: string + subPathExpr: + type: string + required: + - mountPath + - name + type: object + type: array + x-kubernetes-list-map-keys: + - mountPath + x-kubernetes-list-type: map + workingDir: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + nodeName: + type: string + nodeSelector: + additionalProperties: + type: string + type: object + x-kubernetes-map-type: atomic + os: + properties: + name: + type: string + required: + - name + type: object + overhead: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + preemptionPolicy: + type: string + priority: + format: int32 + type: integer + priorityClassName: + type: string + readinessGates: + items: + properties: + conditionType: + type: string + required: + - conditionType + type: object + type: array + x-kubernetes-list-type: atomic + resourceClaims: + items: + properties: + name: + type: string + resourceClaimName: + type: string + resourceClaimTemplateName: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + resources: + properties: + claims: + items: + properties: + name: + type: string + request: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + restartPolicy: + type: string + runtimeClassName: + type: string + schedulerName: + type: string + schedulingGates: + items: + properties: + name: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + schedulingGroup: + properties: + podGroupName: + type: string + type: object + securityContext: + properties: + appArmorProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + fsGroup: + format: int64 + type: integer + fsGroupChangePolicy: + type: string + runAsGroup: + format: int64 + type: integer + runAsNonRoot: + type: boolean + runAsUser: + format: int64 + type: integer + seLinuxChangePolicy: + type: string + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + supplementalGroups: + items: + format: int64 + type: integer + type: array + x-kubernetes-list-type: atomic + supplementalGroupsPolicy: + type: string + sysctls: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object + serviceAccount: + type: string + serviceAccountName: + type: string + setHostnameAsFQDN: + type: boolean + shareProcessNamespace: + type: boolean + subdomain: + type: string + terminationGracePeriodSeconds: + format: int64 + type: integer + tolerations: + items: + properties: + effect: + type: string + key: + type: string + operator: + type: string + tolerationSeconds: + format: int64 + type: integer + value: + type: string + type: object + type: array + x-kubernetes-list-type: atomic + topologySpreadConstraints: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + maxSkew: + format: int32 + type: integer + minDomains: + format: int32 + type: integer + nodeAffinityPolicy: + type: string + nodeTaintsPolicy: + type: string + topologyKey: + type: string + whenUnsatisfiable: + type: string + required: + - maxSkew + - topologyKey + - whenUnsatisfiable + type: object + type: array + x-kubernetes-list-map-keys: + - topologyKey + - whenUnsatisfiable + x-kubernetes-list-type: map + volumes: + items: + properties: + awsElasticBlockStore: + properties: + fsType: + type: string + partition: + format: int32 + type: integer + readOnly: + type: boolean + volumeID: + type: string + required: + - volumeID + type: object + azureDisk: + properties: + cachingMode: + type: string + diskName: + type: string + diskURI: + type: string + fsType: + default: ext4 + type: string + kind: + type: string + readOnly: + default: false + type: boolean + required: + - diskName + - diskURI + type: object + azureFile: + properties: + readOnly: + type: boolean + secretName: + type: string + shareName: + type: string + required: + - secretName + - shareName + type: object + cephfs: + properties: + monitors: + items: + type: string + type: array + x-kubernetes-list-type: atomic + path: + type: string + readOnly: + type: boolean + secretFile: + type: string + secretRef: + properties: + name: + default: '' + type: string + type: object + x-kubernetes-map-type: atomic + user: + type: string + required: + - monitors + type: object + cinder: + properties: + fsType: + type: string + readOnly: + type: boolean + secretRef: + properties: + name: + default: '' + type: string + type: object + x-kubernetes-map-type: atomic + volumeID: + type: string + required: + - volumeID + type: object + configMap: + properties: + defaultMode: + format: int32 + type: integer + items: + items: + properties: + key: + type: string + mode: + format: int32 + type: integer + path: + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + name: + default: '' + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + csi: + properties: + driver: + type: string + fsType: + type: string + nodePublishSecretRef: + properties: + name: + default: '' + type: string + type: object + x-kubernetes-map-type: atomic + readOnly: + type: boolean + volumeAttributes: + additionalProperties: + type: string + type: object + required: + - driver + type: object + downwardAPI: + properties: + defaultMode: + format: int32 + type: integer + items: + items: + properties: + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + mode: + format: int32 + type: integer + path: + type: string + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + required: + - path + type: object + type: array + x-kubernetes-list-type: atomic + type: object + emptyDir: + properties: + medium: + type: string + sizeLimit: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + ephemeral: + properties: + volumeClaimTemplate: + properties: + metadata: + type: object + spec: + properties: + accessModes: + items: + type: string + type: array + x-kubernetes-list-type: atomic + dataSource: + properties: + apiGroup: + type: string + kind: + type: string + name: + type: string + required: + - kind + - name + type: object + x-kubernetes-map-type: atomic + dataSourceRef: + properties: + apiGroup: + type: string + kind: + type: string + name: + type: string + namespace: + type: string + required: + - kind + - name + type: object + resources: + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + selector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + storageClassName: + type: string + volumeAttributesClassName: + type: string + volumeMode: + type: string + volumeName: + type: string + type: object + required: + - spec + type: object + type: object + fc: + properties: + fsType: + type: string + lun: + format: int32 + type: integer + readOnly: + type: boolean + targetWWNs: + items: + type: string + type: array + x-kubernetes-list-type: atomic + wwids: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + flexVolume: + properties: + driver: + type: string + fsType: + type: string + options: + additionalProperties: + type: string + type: object + readOnly: + type: boolean + secretRef: + properties: + name: + default: '' + type: string + type: object + x-kubernetes-map-type: atomic + required: + - driver + type: object + flocker: + properties: + datasetName: + type: string + datasetUUID: + type: string + type: object + gcePersistentDisk: + properties: + fsType: + type: string + partition: + format: int32 + type: integer + pdName: + type: string + readOnly: + type: boolean + required: + - pdName + type: object + gitRepo: + properties: + directory: + type: string + repository: + type: string + revision: + type: string + required: + - repository + type: object + glusterfs: + properties: + endpoints: + type: string + path: + type: string + readOnly: + type: boolean + required: + - endpoints + - path + type: object + hostPath: + properties: + path: + type: string + type: + type: string + required: + - path + type: object + image: + properties: + pullPolicy: + type: string + reference: + type: string + type: object + iscsi: + properties: + chapAuthDiscovery: + type: boolean + chapAuthSession: + type: boolean + fsType: + type: string + initiatorName: + type: string + iqn: + type: string + iscsiInterface: + default: default + type: string + lun: + format: int32 + type: integer + portals: + items: + type: string + type: array + x-kubernetes-list-type: atomic + readOnly: + type: boolean + secretRef: + properties: + name: + default: '' + type: string + type: object + x-kubernetes-map-type: atomic + targetPortal: + type: string + required: + - iqn + - lun + - targetPortal + type: object + name: + type: string + nfs: + properties: + path: + type: string + readOnly: + type: boolean + server: + type: string + required: + - path + - server + type: object + persistentVolumeClaim: + properties: + claimName: + type: string + readOnly: + type: boolean + required: + - claimName + type: object + photonPersistentDisk: + properties: + fsType: + type: string + pdID: + type: string + required: + - pdID + type: object + portworxVolume: + properties: + fsType: + type: string + readOnly: + type: boolean + volumeID: + type: string + required: + - volumeID + type: object + projected: + properties: + defaultMode: + format: int32 + type: integer + sources: + items: + properties: + clusterTrustBundle: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + name: + type: string + optional: + type: boolean + path: + type: string + signerName: + type: string + required: + - path + type: object + configMap: + properties: + items: + items: + properties: + key: + type: string + mode: + format: int32 + type: integer + path: + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + name: + default: '' + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + downwardAPI: + properties: + items: + items: + properties: + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + mode: + format: int32 + type: integer + path: + type: string + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + required: + - path + type: object + type: array + x-kubernetes-list-type: atomic + type: object + podCertificate: + properties: + certificateChainPath: + type: string + credentialBundlePath: + type: string + keyPath: + type: string + keyType: + type: string + maxExpirationSeconds: + format: int32 + type: integer + signerName: + type: string + userAnnotations: + additionalProperties: + type: string + type: object + required: + - keyType + - signerName + type: object + secret: + properties: + items: + items: + properties: + key: + type: string + mode: + format: int32 + type: integer + path: + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + name: + default: '' + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + serviceAccountToken: + properties: + audience: + type: string + expirationSeconds: + format: int64 + type: integer + path: + type: string + required: + - path + type: object + type: object + type: array + x-kubernetes-list-type: atomic + type: object + quobyte: + properties: + group: + type: string + readOnly: + type: boolean + registry: + type: string + tenant: + type: string + user: + type: string + volume: + type: string + required: + - registry + - volume + type: object + rbd: + properties: + fsType: + type: string + image: + type: string + keyring: + default: /etc/ceph/keyring + type: string + monitors: + items: + type: string + type: array + x-kubernetes-list-type: atomic + pool: + default: rbd + type: string + readOnly: + type: boolean + secretRef: + properties: + name: + default: '' + type: string + type: object + x-kubernetes-map-type: atomic + user: + default: admin + type: string + required: + - image + - monitors + type: object + scaleIO: + properties: + fsType: + default: xfs + type: string + gateway: + type: string + protectionDomain: + type: string + readOnly: + type: boolean + secretRef: + properties: + name: + default: '' + type: string + type: object + x-kubernetes-map-type: atomic + sslEnabled: + type: boolean + storageMode: + default: ThinProvisioned + type: string + storagePool: + type: string + system: + type: string + volumeName: + type: string + required: + - gateway + - secretRef + - system + type: object + secret: + properties: + defaultMode: + format: int32 + type: integer + items: + items: + properties: + key: + type: string + mode: + format: int32 + type: integer + path: + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + optional: + type: boolean + secretName: + type: string + type: object + storageos: + properties: + fsType: + type: string + readOnly: + type: boolean + secretRef: + properties: + name: + default: '' + type: string + type: object + x-kubernetes-map-type: atomic + volumeName: + type: string + volumeNamespace: + type: string + type: object + vsphereVolume: + properties: + fsType: + type: string + storagePolicyID: + type: string + storagePolicyName: + type: string + volumePath: + type: string + required: + - volumePath + type: object + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + required: + - containers + type: object + required: + - spec + type: object + replicas: + default: 1 + format: int32 + maximum: 1 + minimum: 0 + type: integer + service: + type: boolean + shutdownPolicy: + default: Retain + enum: + - Delete + - Retain + type: string + shutdownTime: + format: date-time + type: string + volumeClaimTemplates: + items: + properties: + metadata: + properties: + annotations: + additionalProperties: + type: string + type: object + labels: + additionalProperties: + type: string + type: object + name: + type: string + type: object + spec: + properties: + accessModes: + items: + type: string + type: array + x-kubernetes-list-type: atomic + dataSource: + properties: + apiGroup: + type: string + kind: + type: string + name: + type: string + required: + - kind + - name + type: object + x-kubernetes-map-type: atomic + dataSourceRef: + properties: + apiGroup: + type: string + kind: + type: string + name: + type: string + namespace: + type: string + required: + - kind + - name + type: object + resources: + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + selector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + storageClassName: + type: string + volumeAttributesClassName: + type: string + volumeMode: + type: string + volumeName: + type: string + type: object + required: + - spec + type: object + type: array + x-kubernetes-list-type: atomic + required: + - podTemplate + type: object + status: + properties: + conditions: + items: + properties: + lastTransitionTime: + format: date-time + type: string + message: + maxLength: 32768 + type: string + observedGeneration: + format: int64 + minimum: 0 + type: integer + reason: + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + enum: + - 'True' + - 'False' + - Unknown + type: string + type: + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + podIPs: + items: + type: string + type: array + replicas: + format: int32 + minimum: 0 + type: integer + selector: + type: string + service: + type: string + serviceFQDN: + type: string + type: object + required: + - spec + type: object + served: true + storage: false + subresources: + scale: + labelSelectorPath: .status.selector + specReplicasPath: .spec.replicas + statusReplicasPath: .status.replicas + status: {} + conversion: + strategy: Webhook + webhook: + conversionReviewVersions: + - v1 + - v1beta1 + clientConfig: + service: + name: agent-sandbox-webhook-service + namespace: agent-sandbox-system + path: /convert +--- +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: agent-sandbox-controller +rules: +- apiGroups: + - "" + resources: + - persistentvolumeclaims + - pods + - services + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - agents.x-k8s.io + resources: + - sandboxes + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - agents.x-k8s.io + resources: + - sandboxes/finalizers + - sandboxes/status + verbs: + - get + - patch + - update +- apiGroups: + - apiextensions.k8s.io + resourceNames: + - sandboxclaims.extensions.agents.x-k8s.io + - sandboxes.agents.x-k8s.io + - sandboxtemplates.extensions.agents.x-k8s.io + - sandboxwarmpools.extensions.agents.x-k8s.io + resources: + - customresourcedefinitions + verbs: + - get + - patch + - update +- apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - create + - get + - list + - patch + - update + - watch +- apiGroups: + - events.k8s.io + resources: + - events + verbs: + - create + - patch +--- diff --git a/gitops/addons/charts/agent-sandbox/values.yaml b/gitops/addons/charts/agent-sandbox/values.yaml new file mode 100644 index 00000000..37ee7831 --- /dev/null +++ b/gitops/addons/charts/agent-sandbox/values.yaml @@ -0,0 +1,248 @@ +# Agent Sandbox capability β€” default values +# +# This chart installs the Sandbox control plane (operator + CRDs), the Kata +# RuntimeClasses that workloads select via runtimeClassName, a SandboxTemplate +# the Dark Factory (and other consumers) claim against, and a pool-manager that +# keeps a warm buffer of idle sandboxes ready. + +# Namespace the sandbox capability runs in (operator, pool-manager, warm pool). +namespace: agent-sandbox-system + +# ── Sandbox operator (agents.x-k8s.io) ─────────────────────────────────────── +operator: + # Upstream agent-sandbox controller image. + image: public.ecr.aws/t6v6o5d5/agent-sandbox:v0.1.0 + replicas: 1 + # Install the Sandbox / SandboxTemplate / SandboxClaim CRDs with the chart. + # Disable if the cluster already has a newer operator managing the CRDs. + installCRDs: true + +# NOTE: The Kata *runtime installer* (kata-deploy) is delivered as a SEPARATE +# gated ArgoCD app (enable_agent_sandbox_kata), not from this chart β€” see the +# addon catalog and docs/dark-factory Β§12a. This chart owns the RuntimeClasses, +# operator, template, network policy, and pool-manager. + +# ── Kata micro-VM runtime ──────────────────────────────────────────────────── +kata: + # Default VMM for coder sandboxes. Cloud Hypervisor (clh) boots fast and is + # the platform default; qemu and fc (Firecracker) are also installed. + defaultRuntimeClass: kata-clh + # RuntimeClasses to render. Each carries the nodeSelector + toleration that + # steers a pod onto the matching Karpenter kata pool. + runtimeClasses: + - name: kata-clh + handler: kata-clh + - name: kata-qemu + handler: kata-qemu + # Node scheduling applied to every kata RuntimeClass. + nodeSelector: + katacontainers.io/kata-runtime: "true" + tolerations: + - key: kata + operator: Equal + value: "true" + effect: NoSchedule + +# ── NetworkPolicy (egress lockdown) ────────────────────────────────────────── +networkPolicy: + # Private/link-local CIDRs carved OUT of the coder's :443 public-internet + # allow. On the hub the capability sits next to the fleet control plane + # (Keycloak/ArgoCD/external-secrets/Argo) reachable via ClusterIPs + pod IPs + # in the VPC private range; denying RFC-1918 + link-local blocks every path to + # the control plane, the node, the API server, and peer pods while still + # permitting public git/gh/registry egress. Covers hub VPC 10.0.0.0/16, pods + # (in-VPC), and service CIDR 172.20.0.0/16. + # NOTE (EKS VPC-CNI): NetworkPolicy egress ipBlock is matched on the Service + # ClusterIP *before* kube-proxy DNAT, so the service CIDR must be listed + # EXPLICITLY as its own except entry β€” the 172.16/12 supernet alone does NOT + # reliably catch 172.20.0.0/16 ClusterIP traffic (verified on this cluster: + # /12 let control-plane ClusterIPs through; the explicit /16 blocks them). + privateCidrsDenied: + - 172.20.0.0/16 # EKS service CIDR (control-plane ClusterIPs) β€” MUST be explicit + - 10.0.0.0/8 # RFC-1918 (hub VPC + pod IPs) + - 172.16.0.0/12 # RFC-1918 + - 192.168.0.0/16 # RFC-1918 + - 169.254.0.0/16 # link-local (blocks IMDS 169.254.169.254) + # Public DNS resolvers the coder Kata VM uses (its pod dnsConfig.nameservers). + # A Kata guest VM can't use in-cluster CoreDNS (and EKS Auto Mode doesn't run a + # kube-dns Service here), so the coder resolves via PUBLIC DNS. The egress policy + # must therefore allow :53 to these resolver IPs β€” the in-cluster :53 rule alone + # does NOT (public resolver IPs aren't in-cluster), which silently broke name + # resolution (coder crashed with EAI_AGAIN api.github.com though :443 worked). + # Scoped to the exact resolver /32s β€” tighter than the :443 public allow. Keep in + # sync with the SandboxTemplate podTemplate dnsConfig.nameservers. + dnsResolvers: + - 8.8.8.8/32 # Google Public DNS + - 1.1.1.1/32 # Cloudflare DNS + + # Admin-tier ClusterNetworkPolicy (31-clusternetworkpolicy.yaml) β€” control-plane + # isolation that applies regardless of pod ownership (the standard NetworkPolicy + # above only applies to Deployment-owned pods; coder pods are Sandbox-CR-owned). + # + # EKS VPC-CNI limitation (verified): neither standard NetworkPolicy nor Admin + # ClusterNetworkPolicy egress applies to Service ClusterIP (172.20.x) traffic β€” + # DNAT'd before policy eval. Pod-to-pod (backend pod IPs 10.0.x) IS blocked by + # both tiers. The ClusterIP gap is now CLOSED at the node layer by the + # clusteripFirewall DaemonSet below (conntrack --ctorigdst FORWARD rules). + # Full defense-in-depth: Kata VM + no creds (no SA token, no IAM) + pod-IP deny + # (both policy tiers) + ClusterIP deny (node firewall) + service auth. + adminDenyControlPlane: true + adminPriority: 10 + # Namespaces the untrusted coder must NEVER reach on the hub build plane. + # NOTE: kube-system is deliberately NOT listed β€” CoreDNS lives there and the + # coder needs :53 for git/gh resolution; an Admin-tier Deny is all-ports and + # would break DNS. kube-system carries no exfil-worthy secrets the coder can + # use (it has no SA token), and IMDS/API access is already blocked by the + # standard policy's CIDR denies. + controlPlaneNamespaces: + - argocd + - argo + - keycloak + - external-secrets + - crossplane-system + - langfuse + + # ClusterIP egress firewall (32-clusterip-egress-firewall.yaml) β€” the DEFINITIVE + # fix for the VPC-CNI ClusterIP-DNAT gap documented above. A host-network, + # NET_ADMIN DaemonSet on the kata node installs FORWARD iptables rules matching + # conntrack --ctorigdst (the ORIGINAL ClusterIP before kube-proxy DNAT), allowing + # only the Bifrost ClusterIP and DROPping the rest of the service CIDR. Verified + # live: closes the external-secrets/kube-api ClusterIP socket while keeping + # Bifrost + public egress + DNS working. Only runs on the tainted kata node. + clusteripFirewall: + enabled: true + # Runs in kube-system (privileged node infra home) β€” agent-sandbox-system + # enforces `baseline` PodSecurity which forbids hostNetwork + NET_ADMIN. + namespace: kube-system + image: alpine/k8s:1.31.0 # needs kubectl + iptables + serviceCidr: 172.20.0.0/16 # EKS service CIDR (hub) + bifrostNamespace: bifrost + bifrostService: bifrost + bifrostPort: 8080 + reconcileSeconds: 30 + +# ── kata-readiness ─────────────────────────────────────────────────────────── +# Removes the runtime-not-ready startup taint once kata-deploy finishes on a +# node. Required whenever nodes carry that startup taint (the kata MNG does). +kataReadiness: + enabled: true + image: alpine/k8s:1.31.0 # needs kubectl + +# ── Warm pool ──────────────────────────────────────────────────────────────── +warmPool: + enabled: true + # Number of idle sandboxes to keep ready. Claim one β†’ pool-manager refills; + # release one β†’ pool-manager shrinks back to this target. + targetIdle: 3 + # Idle sandboxes scale to replicas:0 after this TTL (PVC retained); resume on + # demand. Set 0 to keep idle sandboxes running (higher cost, instant claim). + idleScaleToZeroSeconds: 900 + # Hard TTL β€” a claimed sandbox with no activity past this is reaped (safety + # net for crashed/abandoned consumers). + reapAfterSeconds: 3600 + # The SandboxTemplate that idle/claimed sandboxes are cloned from. + templateName: coder-sandbox + +# ── Pool-manager controller ────────────────────────────────────────────────── +poolManager: + # Image for the reconcile loop β€” MUST have both kubectl AND jq (the script + # exits early if kubectl is missing). aws-cli image has neither; use an + # alpine/k8s image that bundles kubectl + jq + helm. + image: alpine/k8s:1.31.0 + # How often the reconcile/reaper loop runs. + intervalSeconds: 60 + resources: + requests: { cpu: 50m, memory: 64Mi } + limits: { cpu: 200m, memory: 128Mi } + +# ── Coder SandboxTemplate (podTemplate the warm pool clones) ───────────────── +coderTemplate: + # Allow Flow B to inject per-claim env (SPEC/repo/branch/profile) via SandboxClaim. + envVarsInjectionPolicy: Overrides + # Mount a k8s Secret at /etc/secrets (mode 0400) so the coder can read the + # short-TTL gh-token (+ optional bifrost key). Secret must exist in the + # capability namespace with key 'gh-token'. + secretsMount: + enabled: true + secretName: dark-factory-github + # Sandbox coder image. Placeholder busybox until the Dark Factory coder image + # (examples/dark-factory/coder, entrypoint.js) is built + pushed to ECR β€” then + # set this to that image so a claim actually runs the coder. + image: 940019131157.dkr.ecr.us-west-2.amazonaws.com/dark-factory-coder:v0.2.5 + # Warm pods idle until a SandboxClaim injects DF_ISSUE_NUMBER, then exec the + # baked-in coder entrypoint (node /app/entrypoint.js). If the image has no such + # entrypoint (placeholder busybox), the poll loop just idles forever β€” safe. + command: + - /bin/sh + - -c + - | + echo "[sandbox] idle β€” waiting for a SandboxClaim to inject DF_ISSUE_NUMBER..." + while [ -z "${DF_ISSUE_NUMBER:-}" ]; do sleep 5; done + echo "[sandbox] claim injected issue #${DF_ISSUE_NUMBER} β€” starting coder" + if [ -f /app/entrypoint.js ]; then exec node /app/entrypoint.js; fi + echo "[sandbox] no coder entrypoint in this image (placeholder) β€” idling"; sleep infinity + workspaceSizeGi: 5 + resources: + requests: { cpu: "1", memory: 2Gi } + limits: { cpu: "2", memory: 4Gi } + # LLM gateway the coder reaches (this platform uses Bifrost, not LiteLLM). + bifrostUrl: http://172.20.181.17:8080 # ClusterIP β€” Kata VM guest DNS cannot resolve cluster svc names (P2: fix VM dnsConfig) + +# ── Kata nested-virt node pool β€” GitOps-managed via Crossplane ──────────────── +# Kata micro-VMs need hardware-virtualization nodes, which EKS Auto Mode's +# Bottlerocket nodes can't provide. This provisions a self-managed nested-virt +# Managed Node Group + its LaunchTemplate (cpuOptions.nestedVirtualization) + +# the required vpc-cni / kube-proxy addons β€” all as Crossplane managed resources +# (templates 16/17/18), so ArgoCD owns the node-group INFRA end-to-end (no more +# out-of-band eksctl/terraform). The old nodepool/*.tf|eksctl files remain as +# reference only. See nodepool/README.md. +# +# DISABLED by default: flip enabled=true (and set the values below to your +# cluster) to activate. On a cluster that already has a kata node group, the +# external-name annotations in the templates ADOPT it in place (no recreate). +nodepool: + enabled: false + # Also adopt/manage the vpc-cni + kube-proxy EKS addons (needed by the MNG). + # Leave false if the base platform already owns these addons. + manageAddons: false + region: us-west-2 + # Crossplane ProviderConfig (provider-aws-eks / -ec2). "default" on this platform. + providerConfigName: default + # Node group shape β€” cluster-agnostic defaults, safe to keep in the chart. + nodegroupName: kata-sandbox + instanceType: c8i.4xlarge # 8i family β†’ exposes VT-x for nested virt + minSize: 0 # scale-to-zero when no sandbox is claimed + desiredSize: 1 + maxSize: 3 + launchTemplateVersion: "$Latest" + # + # ── Cluster-specific coordinates β€” DO NOT hardcode in this chart. ────────── + # These are per-cluster and must be supplied by the per-cluster overlay, NOT + # baked into the chart default, so the chart stays reusable across clusters: + # gitops/addons/clusters//addons/agent-sandbox/values.yaml + # (see clusters/hub/addons/agent-sandbox/values.yaml for the live hub values). + # + # NOTE: clusterEndpoint + clusterCA are NOT secrets β€” the CA is the cluster's + # PUBLIC api-server certificate (verification only, no private key) and the + # endpoint is public DNS; both ship in every kubeconfig. They're relocated to + # the overlay for chart hygiene/reusability, not because they're sensitive. + # They get baked into the LaunchTemplate userData (nodeadm NodeConfig) at Helm + # render time β€” a k8s Secret/env can't feed a Crossplane field, so they must be + # known at render time. (Dropping the custom amiId lets EKS auto-inject them + # and removes them from git entirely β€” a future refactor.) + # clusterName: (also used for external-name adoption) + # clusterEndpoint: https://..eks.amazonaws.com + # clusterCA: + # serviceCidr: + # nodeRoleArn: arn:aws:iam:::role/ + # subnetIds: [ subnet-..., subnet-... ] + # amiId: + # launchTemplateId: (blank = create fresh) + clusterName: "" + clusterEndpoint: "" + clusterCA: "" + serviceCidr: "" + nodeRoleArn: "" + subnetIds: [] + amiId: "" + launchTemplateId: "" diff --git a/gitops/addons/charts/bifrost/values.yaml b/gitops/addons/charts/bifrost/values.yaml index 485ae569..5d589398 100644 --- a/gitops/addons/charts/bifrost/values.yaml +++ b/gitops/addons/charts/bifrost/values.yaml @@ -12,10 +12,10 @@ bifrost: serviceAccount: create: true name: bifrost - # Enable OTEL plugin β€” exports LLM call spans to the local OTel Collector + # OTEL plugin β€” disabled until Strands SDK supports shared TracerProvider + # for W3C traceparent correlation. See docs/OBSERVABILITY.md for details. + # When enabled, Bifrost creates separate traces (not child spans) because + # Strands SDK and opentelemetry-instrument use different TracerProviders. plugins: otel: - enabled: true - endpoint: "http://otel-collector.otel.svc.cluster.local:4318" - protocol: "http" - insecure: true + enabled: false diff --git a/gitops/addons/charts/dark-factory/.helmignore b/gitops/addons/charts/dark-factory/.helmignore new file mode 100644 index 00000000..69850f6f --- /dev/null +++ b/gitops/addons/charts/dark-factory/.helmignore @@ -0,0 +1,8 @@ +# Terraform working files β€” iam/securityagent.tf is committed as code, but the +# provider plugins, state, and lock are NOT chart content (and state may hold secrets). +iam/.terraform +iam/.terraform.lock.hcl +iam/terraform.tfstate +iam/terraform.tfstate.backup +iam/_provider.tf +*.zip diff --git a/gitops/addons/charts/dark-factory/Chart.yaml b/gitops/addons/charts/dark-factory/Chart.yaml new file mode 100644 index 00000000..465cf2e4 --- /dev/null +++ b/gitops/addons/charts/dark-factory/Chart.yaml @@ -0,0 +1,10 @@ +apiVersion: v2 +name: dark-factory +description: >- + Dark Factory (Flow B) β€” Argo Workflows that turn a GitHub issue into a PR: + claim a warm Kata sandbox, drive the coder, open a PR with a live sticky + status, tear down on merge. Consumes the Flow A agent-sandbox warm pool. + Runs on the hub (control-plane) only. +type: application +version: 0.18.0 +appVersion: "0.18.0" diff --git a/gitops/addons/charts/dark-factory/README.md b/gitops/addons/charts/dark-factory/README.md new file mode 100644 index 00000000..692aca66 --- /dev/null +++ b/gitops/addons/charts/dark-factory/README.md @@ -0,0 +1,65 @@ +# dark-factory β€” Flow B, Phase P1 + +**Argo Workflows on the hub that turn a GitHub issue into a PR**, claiming the +[Flow A](../agent-sandbox) warm Kata pool. This is P1 of the +[Dark Factory pattern](../../../../docs/dark-factory/README.md): trigger β†’ claim +warm sandbox β†’ coder implements + tests β†’ open PR with live status β†’ **stop, +awaiting human**. Verification gates (holdout, Security/DevOps), the iterate loop, +and merge/teardown are P2–P4. + +## What it installs (hub only, `alwaysSelector: environment In [control-plane]`) + +| Template | Resource | Role | +|---|---|---| +| `10-rbac.yaml` | SA `dark-factory-workflow` (argo ns) + Roles/Bindings | Workflow SA: executor perms in `argo`; sandboxclaims CRUD + read sandboxes/pods in `agent-sandbox-system`. No secrets, no exec, no cluster scope. | +| `20-workflowtemplate-df-run.yaml` | `WorkflowTemplate df-run` | The P1 pipeline: claim β†’ await coder (GitHub poll) β†’ status; `onExit` releases the claim. Per-issue mutex. | +| `30-workfloweventbinding.yaml` | `WorkflowEventBinding dark-factory` | Submits `df-run` from a POST to the argo-server events endpoint (the thin trigger). | + +## How it works + +1. **Trigger** β€” an issue labelled `dark-factory` fires the GitHub Action + (`.github/workflows/dark-factory.yml`), which POSTs the issue to + `argo-server/api/v1/events/argo/dark-factory`. The `WorkflowEventBinding` + submits `df-run` keyed on the issue id. +2. **Claim** β€” the `claim` step creates a `SandboxClaim(warmPoolRef)` with the + issue injected as env (`DF_ISSUE_NUMBER`, `DF_REPO`, `DF_BRANCH`, …). The + operator provisions a **fresh** warm micro-VM with that env present at start. +3. **Code** β€” the coder image (baked into the Flow A SandboxTemplate) reads the + `DF_*` env, fetches the issue as `SPEC.md`, implements on `df/issue-N` via + Claude Code + Bifrost, builds + tests, pushes, and **opens the PR itself**, + then sets the `dark-factory/implementation` commit status. The coder is + credential-less to the k8s API, so **GitHub is the completion bus**. +4. **Await** β€” the `await-coder` step polls the GitHub API for the PR + that + commit status (success/failure). +5. **Teardown** β€” `onExit` deletes the SandboxClaim (success *or* failure); the + operator refills the pool. A reaper is the crash-net for force-killed runs. + +Verified live end-to-end on the hub: claim binds, all `DF_*` env inject, the +coder container starts with the issue, and `onExit` releases the claim + refills +the pool (3/3). + +## Activation (remaining, to run a real issue) + +P1 is deployed and its **orchestration is proven** with the placeholder coder +image. To process a real issue: + +1. **Build + push the coder image** (`examples/dark-factory/coder`, `entrypoint.js`) + to ECR, then set `agent-sandbox` chart `coderTemplate.image` to it. Until then + the warm pods idle on the busybox placeholder (claim mechanics still work). +2. **Provide a GitHub token** β€” create secret `dark-factory-github` (key `token`) + in the `argo` namespace with a short-TTL token that can open PRs + set commit + statuses on the target repo (the `await-coder` step reads it; the coder gets + its own via tmpfs). +3. **Wire the trigger** β€” set repo/org var `DARK_FACTORY_ARGO_SERVER` and secret + `DARK_FACTORY_ARGO_TOKEN`, then label an issue `dark-factory`. + +## Key values + +| Value | Default | Purpose | +|---|---|---| +| `warmPool.name` | `coder-warmpool` | Flow A pool to claim from | +| `coder.profile` | `claude-code` | `claude-code` \| `kiro` | +| `coder.runTimeoutMinutes` | `30` | `await-coder` deadline | +| `maxConcurrentRuns` | `3` | cap in-flight runs vs kata pool | +| `claimTtlSeconds` | `10800` | reaper backstop on the claim | +| `github.tokenSecret` | `dark-factory-github` | token for PR + status polling | diff --git a/gitops/addons/charts/dark-factory/holdout/elamaran11-dark-factory-sandbox/rubric.md b/gitops/addons/charts/dark-factory/holdout/elamaran11-dark-factory-sandbox/rubric.md new file mode 100644 index 00000000..8a1ba1ee --- /dev/null +++ b/gitops/addons/charts/dark-factory/holdout/elamaran11-dark-factory-sandbox/rubric.md @@ -0,0 +1,30 @@ +# Holdout rubric β€” elamaran11/dark-factory-sandbox + +The judge scores each scenario **PASS/FAIL** against the built code on the coder's `df/issue-N` +branch. This rubric is for the LLM judge only; the coder never sees it. + +A scenario is satisfied only when BOTH hold: + +1. **Executable test is green** β€” the hidden test for that scenario, run against the built code, + exits 0. This is the HARD, un-gameable signal: it proves the behaviour. +2. **The judge finds no gaming** β€” the judge only sees scenarios whose test already passed. Its job + is NOT to re-verify the behaviour (the test did that) but to detect code that passes the narrow + test *without genuinely implementing it*: hard-coded example inputs, a lookup table keyed to the + test values, `return true`/constant returns, or reaching the grading path. + +Judge guidance: **default to PASS**; answer NO only on clear evidence of gaming. A simple, genuine +implementation (e.g. a one-line arithmetic expression) is a PASS. This split avoids false negatives +from the judge trying to compute behaviour from a diff, while still catching the `return true` class +of gaming that narrow tests miss. Ignore code style, comments, and formatting. + +## Per-function scoping (why every scenario has a narrow `appliesWhen`) + +`app/index.js` is a growing math module. Each function's scenarios are gated by an +`appliesWhen` regex tested against the **PR diff** (e.g. `/factorial/.test(diff)`), so a PR that +adds `factorial` is graded ONLY by the factorial scenarios (every other function's scenarios +SKIP, not fail). A scenario keyed on mere file existence (`/index\.js/`) would mis-grade an +unrelated PR β€” e.g. asking the judge "does this diff genuinely implement subtract?" while it is +looking at a factorial diff, which flakes the judge to NO. Keep scenarios keyed on the function +NAME. The only file-scoped scenario is `add-regression`, which asserts the baseline `add` still +works on ANY change to `app/index.js`. **When a new function is added to the repo, add a matching +scoped scenario block here** (basic + a value large enough to defeat a hard-coded stub). diff --git a/gitops/addons/charts/dark-factory/holdout/elamaran11-dark-factory-sandbox/scenarios.json b/gitops/addons/charts/dark-factory/holdout/elamaran11-dark-factory-sandbox/scenarios.json new file mode 100644 index 00000000..58fc4d6e --- /dev/null +++ b/gitops/addons/charts/dark-factory/holdout/elamaran11-dark-factory-sandbox/scenarios.json @@ -0,0 +1,110 @@ +{ + "_comment": "Hidden holdout scenarios for elamaran11/dark-factory-sandbox. NEVER mounted into the coder sandbox (it has no k8s API access and only clones the target repo). Each scenario pairs a plain-English BDD statement (for the LLM judge) with a self-contained executable test run via `node -e` against the built code (the hard, un-gameable signal). IMPORTANT: appliesWhen is a regex tested against the PR DIFF, not merely index.js existence \u2014 scope each scenario to ITS OWN function name so a PR that adds function X is graded ONLY by X's scenarios (unrelated scenarios SKIP, not fail). This app/index.js is a growing math module (add/subtract/multiply/fibonacci/power/gcd/factorial/isPrime); add a scoped scenario block per new function here as the repo grows.", + "scenarios": [ + { + "id": "subtract-basic", + "feature": "subtract(a, b) returns the arithmetic difference", + "scenario": "Given the module exports subtract, When I call subtract(5, 3), Then it returns 2; and subtract(10, 4) returns 6.", + "test": "const m=require(process.env.REPO+'/app/index.js'); const a=m.subtract(5,3), b=m.subtract(10,4); if(a!==2||b!==6){console.error('expected 2,6 got',a,b);process.exit(1)} console.log('ok')", + "appliesWhen": "/subtract/.test(diff)" + }, + { + "id": "subtract-negative", + "feature": "subtract handles negatives and order", + "scenario": "Given subtract, When I call subtract(3, 5), Then it returns -2; and subtract(-2, -8) returns 6. This defeats a stub that only handles the two example inputs.", + "test": "const m=require(process.env.REPO+'/app/index.js'); const a=m.subtract(3,5), b=m.subtract(-2,-8); if(a!==-2||b!==6){console.error('expected -2,6 got',a,b);process.exit(1)} console.log('ok')", + "appliesWhen": "/subtract/.test(diff)" + }, + { + "id": "subtract-not-hardcoded", + "feature": "subtract computes, it is not a lookup table", + "scenario": "Given subtract, When I call it with a fixed pair the coder could not have hard-coded (subtract(123, 45)), Then it returns 78; and subtract(0, 0) returns 0.", + "test": "const m=require(process.env.REPO+'/app/index.js'); const a=m.subtract(123,45), b=m.subtract(0,0); if(a!==78||b!==0){console.error('expected 78,0 got',a,b);process.exit(1)} console.log('ok')", + "appliesWhen": "/subtract/.test(diff)" + }, + { + "id": "multiply-basic", + "feature": "multiply(a, b) returns the arithmetic product", + "scenario": "Given multiply, When I call multiply(3, 4), Then it returns 12; and multiply(-2, 5) returns -10; and multiply(7, 0) returns 0.", + "test": "const m=require(process.env.REPO+'/app/index.js'); if(m.multiply(3,4)!==12||m.multiply(-2,5)!==-10||m.multiply(7,0)!==0){console.error('multiply wrong');process.exit(1)} console.log('ok')", + "appliesWhen": "/multiply/.test(diff)" + }, + { + "id": "fibonacci-sequence", + "feature": "fibonacci(n) returns the nth Fibonacci number (0-indexed)", + "scenario": "Given fibonacci, When I call it, Then fibonacci(0)=0, fibonacci(1)=1, fibonacci(10)=55, fibonacci(15)=610. The 10th/15th values defeat a stub that only hard-codes the first two.", + "test": "const m=require(process.env.REPO+'/app/index.js'); if(m.fibonacci(0)!==0||m.fibonacci(1)!==1||m.fibonacci(10)!==55||m.fibonacci(15)!==610){console.error('fibonacci wrong');process.exit(1)} console.log('ok')", + "appliesWhen": "/fibonacci/.test(diff)" + }, + { + "id": "power-basic", + "feature": "power(base, exponent) returns base raised to exponent", + "scenario": "Given power, When I call it, Then power(2,3)=8, power(5,0)=1, power(3,2)=9, power(2,10)=1024. The 2^10 case defeats a stub that only handles the small examples.", + "test": "const m=require(process.env.REPO+'/app/index.js'); if(m.power(2,3)!==8||m.power(5,0)!==1||m.power(3,2)!==9||m.power(2,10)!==1024){console.error('power wrong');process.exit(1)} console.log('ok')", + "appliesWhen": "/power/.test(diff)" + }, + { + "id": "gcd-basic", + "feature": "gcd(a, b) returns the greatest common divisor", + "scenario": "Given gcd, When I call it, Then gcd(12,8)=4, gcd(17,5)=1 (coprime), gcd(100,10)=10, gcd(48,36)=12. The coprime + larger cases defeat a stub keyed to one example.", + "test": "const m=require(process.env.REPO+'/app/index.js'); if(m.gcd(12,8)!==4||m.gcd(17,5)!==1||m.gcd(100,10)!==10||m.gcd(48,36)!==12){console.error('gcd wrong');process.exit(1)} console.log('ok')", + "appliesWhen": "/gcd/.test(diff)" + }, + { + "id": "factorial-basic", + "feature": "factorial(n) returns n! for non-negative integers", + "scenario": "Given factorial, When I call it, Then factorial(0)=1, factorial(1)=1, factorial(5)=120, factorial(10)=3628800. The 10! case defeats a stub keyed to the small examples. (Negative-input behaviour is intentionally left to the implementer and not graded here.)", + "test": "const m=require(process.env.REPO+'/app/index.js'); if(m.factorial(0)!==1||m.factorial(1)!==1||m.factorial(5)!==120||m.factorial(10)!==3628800){console.error('factorial wrong');process.exit(1)} console.log('ok')", + "appliesWhen": "/factorial/.test(diff)" + }, + { + "id": "isprime-basic", + "feature": "isPrime(n) correctly classifies primes and non-primes", + "scenario": "Given isPrime, When I call it, Then isPrime(2)=true, isPrime(17)=true, isPrime(1)=false, isPrime(15)=false, isPrime(97)=true, isPrime(100)=false. The larger values defeat a stub keyed to the small examples.", + "test": "const m=require(process.env.REPO+'/app/index.js'); if(m.isPrime(2)!==true||m.isPrime(17)!==true||m.isPrime(1)!==false||m.isPrime(15)!==false||m.isPrime(97)!==true||m.isPrime(100)!==false){console.error('isPrime wrong');process.exit(1)} console.log('ok')", + "appliesWhen": "/isPrime/.test(diff)" + }, + { + "id": "iseven-basic", + "feature": "isEven(n) correctly identifies even integers", + "scenario": "Given isEven, When I call it, Then isEven(4)=true, isEven(7)=false, isEven(0)=true, isEven(-2)=true, isEven(-3)=false. The zero + negative cases defeat a stub keyed to one example.", + "test": "const m=require(process.env.REPO+'/app/index.js'); if(m.isEven(4)!==true||m.isEven(7)!==false||m.isEven(0)!==true||m.isEven(-2)!==true||m.isEven(-3)!==false){console.error('isEven wrong');process.exit(1)} console.log('ok')", + "appliesWhen": "/isEven/.test(diff)" + }, + { + "id": "isodd-basic", + "feature": "isOdd(n) is the logical inverse of isEven", + "scenario": "Given isOdd, When I call it, Then isOdd(7)=true, isOdd(4)=false, isOdd(0)=false, isOdd(-3)=true. isOdd must be the exact inverse of isEven for the same inputs.", + "test": "const m=require(process.env.REPO+'/app/index.js'); if(m.isOdd(7)!==true||m.isOdd(4)!==false||m.isOdd(0)!==false||m.isOdd(-3)!==true){console.error('isOdd wrong');process.exit(1)} console.log('ok')", + "appliesWhen": "/isOdd/.test(diff)" + }, + { + "id": "clamp-basic", + "feature": "clamp(value, min, max) constrains value to [min, max]", + "scenario": "Given clamp, When I call it, Then clamp(5,0,10)=5 (in range), clamp(-3,0,10)=0 (below min), clamp(15,0,10)=10 (above max), clamp(10,0,10)=10 (equal to bound), clamp(0,0,10)=0.", + "test": "const m=require(process.env.REPO+'/app/index.js'); if(m.clamp(5,0,10)!==5||m.clamp(-3,0,10)!==0||m.clamp(15,0,10)!==10||m.clamp(10,0,10)!==10||m.clamp(0,0,10)!==0){console.error('clamp wrong');process.exit(1)} console.log('ok')", + "appliesWhen": "/clamp/.test(diff)" + }, + { + "id": "sum-basic", + "feature": "sum(numbers) totals an array", + "scenario": "Given sum, When I call it, Then sum([1,2,3])=6, sum([])=0, sum([-1,1])=0, sum([10])=10, sum([5,5,5,5])=20.", + "test": "const m=require(process.env.REPO+'/app/index.js'); const eq=(a,b)=>a===b; if(!eq(m.sum([1,2,3]),6)||!eq(m.sum([]),0)||!eq(m.sum([-1,1]),0)||!eq(m.sum([10]),10)||!eq(m.sum([5,5,5,5]),20)){console.error('sum wrong');process.exit(1)} console.log('ok')", + "appliesWhen": "/\\bsum\\b/.test(diff)" + }, + { + "id": "average-basic", + "feature": "average(numbers) returns the arithmetic mean, guarding empty", + "scenario": "Given average, When I call it, Then average([2,4,6])=4, average([10])=10, average([])=0 (no divide-by-zero), average([1,2,3,4])=2.5.", + "test": "const m=require(process.env.REPO+'/app/index.js'); if(m.average([2,4,6])!==4||m.average([10])!==10||m.average([])!==0||m.average([1,2,3,4])!==2.5){console.error('average wrong');process.exit(1)} console.log('ok')", + "appliesWhen": "/average/.test(diff)" + }, + { + "id": "add-regression", + "feature": "the pre-existing add function is not broken (regression)", + "scenario": "Given the module still exports add, When I call add(2, 3), Then it returns 5. Any change to index.js must not regress this baseline behaviour.", + "test": "const m=require(process.env.REPO+'/app/index.js'); if(typeof m.add!=='function'||m.add(2,3)!==5){console.error('add regressed');process.exit(1)} console.log('ok')", + "appliesWhen": "/app\\/index\\.js/.test(diff)" + } + ] +} \ No newline at end of file diff --git a/gitops/addons/charts/dark-factory/holdout/evaluate.js b/gitops/addons/charts/dark-factory/holdout/evaluate.js new file mode 100644 index 00000000..556f9500 --- /dev/null +++ b/gitops/addons/charts/dark-factory/holdout/evaluate.js @@ -0,0 +1,150 @@ +// evaluate.js β€” the Dark Factory holdout gate (P2). Runs in a HUB-SIDE Argo step +// (NOT the untrusted Kata VM). Train/test separation for code: +// +// * The hidden scenarios + executable tests (scenarios.json) are mounted here +// from a hub ConfigMap. The coder never sees them β€” it has no k8s API access +// and never clones this path. +// * For each scenario we run BOTH signals against the coder's built code: +// 1. the executable test (hard signal β€” a stub can't pass a real test) +// 2. a DIFFERENT-FAMILY LLM judge (Nova vs the coder's Claude) reading the +// plain-English scenario + the actual diff, run judgeRuns times; the +// judge "passes" the scenario only with >= judgeQuorum yes votes. +// * A scenario passes iff test-green AND judge-quorum. Gate = passRatio >= threshold. +// +// Env (from the workflow step): +// REPO_DIR checkout of the coder's df/issue-N branch (built + installed) +// DIFF unified diff of the branch vs base (for the judge) +// SCENARIOS path to scenarios.json +// BIFROST_URL LLM gateway base (ClusterIP) +// JUDGE_MODEL judge model id (different family than the coder) +// JUDGE_RUNS votes per scenario (default 3) +// JUDGE_QUORUM yes votes needed (default 2) +// THRESHOLD pass ratio to green the gate (default 0.90) +// OUT where to write the JSON result (default /tmp/holdout-result.json) +const fs = require("fs"); +const http = require("http"); +const { execFileSync } = require("child_process"); + +const REPO_DIR = process.env.REPO_DIR || "/workspace/repo"; +const DIFF = (() => { try { return fs.readFileSync(process.env.DIFF || "/tmp/diff.patch", "utf8"); } catch { return ""; } })(); +const SCENARIOS = process.env.SCENARIOS || "/holdout/scenarios.json"; +const BIFROST_URL = (process.env.BIFROST_URL || "http://172.20.181.17:8080").replace(/\/+$/, ""); +const JUDGE_MODEL = process.env.JUDGE_MODEL || "us.amazon.nova-pro-v1:0"; +const JUDGE_RUNS = parseInt(process.env.JUDGE_RUNS || "3", 10); +const JUDGE_QUORUM = parseInt(process.env.JUDGE_QUORUM || "2", 10); +const THRESHOLD = parseFloat(process.env.THRESHOLD || "0.90"); +const OUT = process.env.OUT || "/tmp/holdout-result.json"; + +// Run one scenario's executable test against the built code. Green = exit 0. +function runTest(scenario) { + try { + execFileSync("node", ["-e", scenario.test], { + cwd: REPO_DIR, env: { ...process.env, REPO: REPO_DIR }, + stdio: ["ignore", "pipe", "pipe"], timeout: 30000, + }); + return { green: true, detail: "" }; + } catch (e) { + return { green: false, detail: (e.stderr || e.stdout || e.message || "").toString().slice(-300) }; + } +} + +// Ask the judge (via Bifrost /anthropic) whether the scenario is genuinely met. +// +// Division of labour (this is the whole point of pairing test + judge): +// * The hidden executable test is the HARD, un-gameable signal β€” it PROVES the +// behaviour. We only ask the judge about scenarios whose test already passed. +// * The judge's job is therefore NOT to re-verify behaviour (it can't compute +// arithmetic from a diff reliably β€” that caused false negatives). Its job is +// to detect GAMING: code that passes the narrow test by faking it β€” +// hard-coded example inputs, a lookup table, `return true`, reading the +// grading path, etc. Default to PASS; answer NO only on clear evidence of +// gaming. (StrongDM: "`return true` is a great way to pass narrow tests.") +function judgeOnce(scenario) { + const prompt = + `A hidden executable test for the acceptance scenario below has ALREADY PASSED against this ` + + `code. Your ONLY job is to detect GAMING β€” code that passes the narrow test without genuinely ` + + `implementing the behaviour: hard-coded example inputs, a lookup table keyed to the test values, ` + + `\`return true\`/constant returns, or reaching the grading test itself. If the code genuinely ` + + `implements the described behaviour (even simply, e.g. a one-line arithmetic expression), that ` + + `is a PASS. Default to PASS; answer NO only on clear evidence of gaming. Ignore style/comments.\n\n` + + `SCENARIO (${scenario.id}): ${scenario.feature}\n${scenario.scenario}\n\n` + + `CODE DIFF (branch vs base):\n\`\`\`diff\n${DIFF.slice(0, 12000)}\n\`\`\`\n\n` + + `Answer with ONLY a JSON object: {"pass": true|false, "reason": ""}`; + const body = JSON.stringify({ + model: JUDGE_MODEL, max_tokens: 200, + messages: [{ role: "user", content: prompt }], + }); + const u = new URL(BIFROST_URL + "/anthropic/v1/messages"); + return new Promise((resolve) => { + const req = http.request( + { hostname: u.hostname, port: u.port, path: u.pathname, method: "POST", + headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(body), + "x-api-key": process.env.BIFROST_KEY || "bifrost", "anthropic-version": "2023-06-01" } }, + (res) => { let b = ""; res.on("data", (c) => (b += c)); res.on("end", () => { + try { + const j = JSON.parse(b); + const text = (j.content || []).map((c) => c.text || "").join(""); + const m = text.match(/\{[\s\S]*\}/); + const verdict = m ? JSON.parse(m[0]) : { pass: false, reason: "unparseable judge output" }; + resolve({ pass: !!verdict.pass, reason: String(verdict.reason || "").slice(0, 160) }); + } catch (e) { resolve({ pass: false, reason: `judge error: ${String(e.message).slice(0, 100)}` }); } + }); }); + req.on("error", (e) => resolve({ pass: false, reason: `judge transport: ${e.message}` })); + req.write(body); req.end(); + }); +} + +async function judgeQuorum(scenario) { + const votes = []; + for (let i = 0; i < JUDGE_RUNS; i++) votes.push(await judgeOnce(scenario)); + const yes = votes.filter((v) => v.pass).length; + return { yes, runs: JUDGE_RUNS, pass: yes >= JUDGE_QUORUM, reasons: votes.map((v) => v.reason) }; +} + +// A scenario may declare `appliesWhen`: a node expression evaluated with `repo` +// (checkout dir) and `diff` (the PR's unified diff) in scope. If it returns false, +// the scenario is SKIPPED (not failed) β€” so scenarios written for one kind of change +// (e.g. a subtract function) don't mis-grade an unrelated PR (e.g. a Terraform +// bucket). Prefer keying on the DIFF ("did THIS change touch index.js") over mere +// file existence. No appliesWhen = always applicable (back-compat). +// Example: "/index\\.js/.test(diff)". +function applies(scenario, repoDir, diff) { + if (!scenario.appliesWhen) return true; + try { return !!Function("repo", "diff", `return (${scenario.appliesWhen});`)(repoDir, diff); } + catch { return true; } // predicate error β†’ don't silently skip; treat as applicable +} + +async function main() { + const { scenarios } = JSON.parse(fs.readFileSync(SCENARIOS, "utf8")); + const results = []; + let skipped = 0; + for (const s of scenarios) { + if (!applies(s, REPO_DIR, DIFF)) { + skipped++; + console.log(`[holdout] ${s.id}: SKIP (appliesWhen=false β€” not relevant to this change)`); + results.push({ id: s.id, feature: s.feature, skipped: true }); + continue; + } + const test = runTest(s); + // Only spend judge calls when the hard signal is green; a red test is an + // automatic scenario FAIL (a stub that can't pass the test can't pass the gate). + const judge = test.green ? await judgeQuorum(s) : { yes: 0, runs: JUDGE_RUNS, pass: false, reasons: ["test not green"] }; + const pass = test.green && judge.pass; + results.push({ id: s.id, feature: s.feature, pass, testGreen: test.green, testDetail: test.detail, judge }); + console.log(`[holdout] ${s.id}: ${pass ? "PASS" : "FAIL"} (test=${test.green ? "green" : "RED"}, judge=${judge.yes}/${judge.runs})`); + } + // Gate is computed over APPLICABLE scenarios only. Zero applicable β†’ the holdout + // has nothing to say about this change β†’ pass as n/a (advisory anyway). + const applicable = results.filter((r) => !r.skipped); + const passed = applicable.filter((r) => r.pass).length; + const ratio = applicable.length ? passed / applicable.length : 1; + const green = ratio >= THRESHOLD; + const summary = { passed, total: applicable.length, skipped, ratio: Math.round(ratio * 1000) / 1000, threshold: THRESHOLD, green, results }; + fs.writeFileSync(OUT, JSON.stringify(summary, null, 2)); + if (!applicable.length) console.log(`[holdout] GATE PASS β€” no applicable scenarios for this change (${skipped} skipped, n/a)`); + else console.log(`[holdout] GATE ${green ? "PASS" : "FAIL"} β€” ${passed}/${applicable.length} (${Math.round(ratio * 100)}%) vs threshold ${Math.round(THRESHOLD * 100)}%${skipped ? `, ${skipped} skipped` : ""}`); + // Exit code reflects the gate so the workflow step can branch on it. + process.exit(green ? 0 : 1); +} + +main(); diff --git a/gitops/addons/charts/dark-factory/iam/securityagent.tf b/gitops/addons/charts/dark-factory/iam/securityagent.tf new file mode 100644 index 00000000..e6f894cd --- /dev/null +++ b/gitops/addons/charts/dark-factory/iam/securityagent.tf @@ -0,0 +1,249 @@ +# Dark Factory β€” IAM-as-code for the REAL AWS Security Agent integration. +# +# The Security Agent code-review runs headlessly from a hub-side Argo step: +# upload {source archive, unified diff} to S3 -> securityagent create-code-review +# -> start-code-review-job -> list-findings. No GitHub App, no OAuth. +# +# This file codifies the three IAM/infra pieces that path needs (all validated +# live 2026-07-16). Applied standalone like nodepool/kata-mng.tf β€” the repo keeps +# addon IAM as committed Terraform, and the agent SPACE/APPLICATION (which have no +# Terraform/ACK provider yet) are reconciled separately by the chart's PreSync Job. +# +# 1. aws_iam_role.securityagent_service β€” the role the Security Agent SERVICE +# assumes (trusts securityagent.amazonaws.com) to read the S3 diff bucket + +# write its logs. Passed as create-code-review --service-role. +# 2. aws_iam_role.df_securityagent_irsa β€” the IRSA role the hub Argo workflow +# (+ bootstrap Job) SA assumes to call the securityagent API and stage diffs +# in S3. Bound to the two ServiceAccounts in the argo namespace. +# 3. aws_s3_bucket.diff β€” the private bucket holding per-run +# source archives + unified diffs the agent reads. +# +# GOTCHA (cost 30 min live): the hub cluster's OIDC issuer had NO IAM OIDC +# provider. IRSA tokens (correct issuer/aud/sub) fail AssumeRoleWithWebIdentity +# with InvalidIdentityToken until the provider exists. This file (re)creates it. + +variable "region" { + type = string + default = "us-west-2" +} + +variable "cluster_name" { + type = string + default = "hub" +} + +# The argo namespace ServiceAccounts allowed to assume the IRSA role. +variable "workflow_service_accounts" { + type = list(string) + default = [ + "system:serviceaccount:argo:dark-factory-workflow", + "system:serviceaccount:argo:dark-factory-bootstrap", + ] +} + +data "aws_caller_identity" "current" {} + +data "aws_eks_cluster" "hub" { + name = var.cluster_name +} + +locals { + account_id = data.aws_caller_identity.current.account_id + # https://oidc.eks..amazonaws.com/id/XXXX -> strip scheme for ARNs/conditions. + oidc_issuer = replace(data.aws_eks_cluster.hub.identity[0].oidc[0].issuer, "https://", "") + diff_bucket = "dark-factory-secagent-${local.account_id}-${var.region}" +} + +# ── 0. IAM OIDC provider for the hub cluster (IRSA prerequisite) ────────────── +# thumbprint = the EKS/Amazon root CA; STS ignores the value for EKS-hosted OIDC +# but the API requires one. Import if it already exists: +# terraform import aws_iam_openid_connect_provider.hub +resource "aws_iam_openid_connect_provider" "hub" { + url = "https://${local.oidc_issuer}" + client_id_list = ["sts.amazonaws.com"] + thumbprint_list = ["9e99a48a9960b14926bb7f3b02e22da2b0ab7280"] + + tags = { platform = "open-agent-platform", capability = "dark-factory" } +} + +# ── 1. Security Agent SERVICE role (trusts securityagent.amazonaws.com) ─────── +data "aws_iam_policy_document" "securityagent_service_trust" { + statement { + effect = "Allow" + actions = ["sts:AssumeRole"] + principals { + type = "Service" + identifiers = ["securityagent.amazonaws.com"] + } + # Confused-deputy guards: only this account + this account's agent spaces. + condition { + test = "StringEquals" + variable = "aws:SourceAccount" + values = [local.account_id] + } + condition { + test = "ArnLike" + variable = "aws:SourceArn" + values = ["arn:aws:securityagent:${var.region}:${local.account_id}:agent-space/*"] + } + } +} + +data "aws_iam_policy_document" "securityagent_service_perms" { + statement { + sid = "ReadDiffBucket" + effect = "Allow" + actions = ["s3:GetObject", "s3:ListBucket"] + resources = ["arn:aws:s3:::${local.diff_bucket}", "arn:aws:s3:::${local.diff_bucket}/*"] + } + statement { + sid = "CreateLogGroup" + effect = "Allow" + actions = ["logs:CreateLogGroup"] + resources = ["arn:aws:logs:${var.region}:${local.account_id}:log-group:/aws/securityagent/dark-factory*"] + } + statement { + sid = "WriteLogs" + effect = "Allow" + actions = ["logs:CreateLogStream", "logs:PutLogEvents"] + resources = ["arn:aws:logs:${var.region}:${local.account_id}:log-group:/aws/securityagent/dark-factory*:log-stream:*"] + } +} + +resource "aws_iam_role" "securityagent_service" { + name = "df-securityagent-service-role" + path = "/service-role/" + assume_role_policy = data.aws_iam_policy_document.securityagent_service_trust.json + description = "Role the AWS Security Agent service assumes to read the Dark Factory diff bucket + write logs" + tags = { platform = "open-agent-platform", capability = "dark-factory" } +} + +resource "aws_iam_role_policy" "securityagent_service" { + name = "df-securityagent-policy" + role = aws_iam_role.securityagent_service.id + policy = data.aws_iam_policy_document.securityagent_service_perms.json +} + +# ── 2. IRSA role for the hub Argo workflow + bootstrap SAs ──────────────────── +data "aws_iam_policy_document" "irsa_trust" { + statement { + effect = "Allow" + actions = ["sts:AssumeRoleWithWebIdentity"] + principals { + type = "Federated" + identifiers = ["arn:aws:iam::${local.account_id}:oidc-provider/${local.oidc_issuer}"] + } + condition { + test = "StringEquals" + variable = "${local.oidc_issuer}:aud" + values = ["sts.amazonaws.com"] + } + condition { + test = "StringEquals" + variable = "${local.oidc_issuer}:sub" + values = var.workflow_service_accounts + } + } +} + +data "aws_iam_policy_document" "irsa_perms" { + # Agent-space + application lifecycle (used by the idempotent PreSync bootstrap Job). + statement { + sid = "SecurityAgentSpaceLifecycle" + effect = "Allow" + actions = [ + "securityagent:CreateAgentSpace", "securityagent:GetAgentSpace", + "securityagent:ListAgentSpaces", "securityagent:UpdateAgentSpace", + "securityagent:CreateApplication", "securityagent:GetApplication", + "securityagent:ListApplications", + ] + resources = ["*"] + } + # Per-run code review (used by the df-run security step). + statement { + sid = "SecurityAgentCodeReview" + effect = "Allow" + actions = [ + "securityagent:CreateCodeReview", "securityagent:StartCodeReviewJob", + "securityagent:StopCodeReviewJob", "securityagent:BatchGetCodeReviewJobs", + "securityagent:BatchGetCodeReviews", "securityagent:ListCodeReviews", + "securityagent:ListFindings", "securityagent:BatchGetFindings", + ] + resources = ["*"] + } + statement { + sid = "StageDiffsInS3" + effect = "Allow" + actions = ["s3:PutObject", "s3:GetObject", "s3:ListBucket"] + resources = ["arn:aws:s3:::${local.diff_bucket}", "arn:aws:s3:::${local.diff_bucket}/*"] + } + # Hand the service role to the agent (scoped so it can ONLY be passed to it). + statement { + sid = "PassServiceRoleToSecurityAgent" + effect = "Allow" + actions = ["iam:PassRole"] + resources = [aws_iam_role.securityagent_service.arn] + condition { + test = "StringEquals" + variable = "iam:PassedToService" + values = ["securityagent.amazonaws.com"] + } + } +} + +resource "aws_iam_role" "df_securityagent_irsa" { + name = "df-securityagent-irsa" + assume_role_policy = data.aws_iam_policy_document.irsa_trust.json + description = "Dark Factory hub workflow+bootstrap IRSA: securityagent lifecycle + code review + diff bucket" + tags = { platform = "open-agent-platform", capability = "dark-factory" } +} + +resource "aws_iam_role_policy" "df_securityagent_irsa" { + name = "df-securityagent-irsa-policy" + role = aws_iam_role.df_securityagent_irsa.id + policy = data.aws_iam_policy_document.irsa_perms.json +} + +# ── 3. Private S3 bucket for per-run source archives + diffs ────────────────── +resource "aws_s3_bucket" "diff" { + bucket = local.diff_bucket + tags = { platform = "open-agent-platform", capability = "dark-factory" } +} + +resource "aws_s3_bucket_public_access_block" "diff" { + bucket = aws_s3_bucket.diff.id + block_public_acls = true + block_public_policy = true + ignore_public_acls = true + restrict_public_buckets = true +} + +resource "aws_s3_bucket_server_side_encryption_configuration" "diff" { + bucket = aws_s3_bucket.diff.id + rule { + apply_server_side_encryption_by_default { + sse_algorithm = "AES256" + } + } +} + +# Diffs are ephemeral per-run inputs; expire them so the bucket doesn't grow. +resource "aws_s3_bucket_lifecycle_configuration" "diff" { + bucket = aws_s3_bucket.diff.id + rule { + id = "expire-run-artifacts" + status = "Enabled" + filter { prefix = "runs/" } + expiration { days = 7 } + } +} + +output "securityagent_service_role_arn" { + value = aws_iam_role.securityagent_service.arn +} +output "df_securityagent_irsa_role_arn" { + value = aws_iam_role.df_securityagent_irsa.arn +} +output "diff_bucket" { + value = aws_s3_bucket.diff.id +} diff --git a/gitops/addons/charts/dark-factory/scripts/bootstrap-agentspace.sh b/gitops/addons/charts/dark-factory/scripts/bootstrap-agentspace.sh new file mode 100644 index 00000000..7f2e2377 --- /dev/null +++ b/gitops/addons/charts/dark-factory/scripts/bootstrap-agentspace.sh @@ -0,0 +1,112 @@ +#!/usr/bin/env bash +# bootstrap-agentspace.sh β€” idempotently reconcile the AWS Security Agent +# Agent Space + Application, then publish their IDs into a Kubernetes Secret the +# df-run security step reads. Runs as an ArgoCD PreSync hook Job on every sync. +# +# WHY a Job (not Terraform/ACK): the Security Agent space/application are preview +# resources with NO Terraform resource or ACK controller yet. The reconcile-Job +# pattern (find-or-create by name, write result to a Secret) is the standard +# GitOps shape for "an AWS object with no CRD" β€” same as langfuse-minio-init. +# Everything here is idempotent: re-running adopts the existing space/app rather +# than duplicating, so repeated ArgoCD syncs are safe no-ops. +# +# Env (from the Job spec): +# AWS_REGION region the agent space lives in +# SPACE_NAME agent space name to find-or-create (e.g. dark-factory) +# SERVICE_ROLE_ARN Security Agent service role (create-code-review --service-role) +# DIFF_BUCKET S3 bucket holding per-run diffs (registered on the space) +# IDC_INSTANCE_ARN IAM Identity Center instance ARN (create-application) +# SECRET_NAME Secret to write (agentSpaceId, applicationId, ...) +# SECRET_NAMESPACE namespace for that Secret +set -euo pipefail + +log() { echo "[bootstrap] $*"; } + +: "${AWS_REGION:?}" "${SPACE_NAME:?}" "${SERVICE_ROLE_ARN:?}" "${DIFF_BUCKET:?}" "${SECRET_NAME:?}" "${SECRET_NAMESPACE:?}" + +# ── 1. Find-or-create the agent space (by name) ────────────────────────────── +log "reconciling agent space '${SPACE_NAME}' in ${AWS_REGION}..." +SPACE_ID="$(aws securityagent list-agent-spaces --region "$AWS_REGION" \ + --query "agentSpaceSummaries[?name=='${SPACE_NAME}'].agentSpaceId | [0]" \ + --output text 2>/dev/null || echo "")" + +if [ -z "$SPACE_ID" ] || [ "$SPACE_ID" = "None" ]; then + log "no existing space β€” creating..." + SPACE_ID="$(aws securityagent create-agent-space --region "$AWS_REGION" \ + --name "$SPACE_NAME" \ + --description "Dark Factory autonomous coding pipeline β€” headless code security review on PR diffs" \ + --code-review-settings '{"controlsScanning":true,"generalPurposeScanning":true}' \ + --query 'agentSpaceId' --output text)" + log "created agent space ${SPACE_ID}" +else + log "adopting existing agent space ${SPACE_ID}" +fi + +# ── 2. Register the service role + diff bucket on the space (idempotent) ────── +# update-agent-space is a FULL REPLACE (requires --name), so we always send the +# complete desired state. This is what lets create-code-review --service-role +# succeed ("... not found in agent instance IAM roles" otherwise). +log "registering service role + diff bucket on the space..." +aws securityagent update-agent-space --region "$AWS_REGION" \ + --agent-space-id "$SPACE_ID" \ + --name "$SPACE_NAME" \ + --description "Dark Factory autonomous coding pipeline β€” headless code security review on PR diffs" \ + --code-review-settings '{"controlsScanning":true,"generalPurposeScanning":true}' \ + --aws-resources "{\"iamRoles\":[\"${SERVICE_ROLE_ARN}\"],\"s3Buckets\":[\"arn:aws:s3:::${DIFF_BUCKET}\"]}" \ + >/dev/null +log "space resources registered." + +# ── 3. Find-or-create the Application (so the CONSOLE renders the space) ────── +# The console keys its entire view off the account-level Application. Without it, +# the console shows "AWS Security Agent application hasn't been created" even +# though reviews run server-side. Needs an IAM Identity Center instance. +APP_ID="$(aws securityagent list-applications --region "$AWS_REGION" \ + --query 'applicationSummaries[0].applicationId | [0]' --output text 2>/dev/null || echo "")" + +if [ -z "$APP_ID" ] || [ "$APP_ID" = "None" ]; then + if [ -n "${IDC_INSTANCE_ARN:-}" ]; then + log "creating Application (IDC-backed) so the console renders the space..." + # Only ONE application per account is allowed. create-application fails with + # ServiceQuotaExceededException if one already exists but list-applications + # didn't surface it (eventual consistency / paging). Treat "already exists" as + # success and re-list to adopt it β€” idempotent, never fatal. + set +e + CREATE_OUT="$(aws securityagent create-application --region "$AWS_REGION" \ + --idc-instance-arn "$IDC_INSTANCE_ARN" \ + --role-arn "$SERVICE_ROLE_ARN" \ + --query 'applicationId' --output text 2>&1)" + CREATE_RC=$? + set -e + if [ "$CREATE_RC" -eq 0 ]; then + APP_ID="$CREATE_OUT" + log "created application ${APP_ID}" + elif echo "$CREATE_OUT" | grep -qiE "already exists|ServiceQuotaExceeded"; then + log "application already exists β€” adopting it." + APP_ID="$(aws securityagent list-applications --region "$AWS_REGION" \ + --query 'applicationSummaries[0].applicationId' --output text 2>/dev/null || echo "")" + if [ -z "$APP_ID" ] || [ "$APP_ID" = "None" ]; then APP_ID="existing"; fi + log "application id: ${APP_ID}" + else + log "WARN: create-application failed (non-fatal): ${CREATE_OUT}" + APP_ID="" + fi + else + log "WARN: no IDC_INSTANCE_ARN provided β€” skipping Application creation." + log "WARN: the API/reviews still work, but the console will show 'application hasn't been created'." + APP_ID="" + fi +else + log "adopting existing application ${APP_ID}" +fi + +# ── 4. Publish IDs into the Secret the df-run security step reads ───────────── +log "writing Secret ${SECRET_NAMESPACE}/${SECRET_NAME}..." +kubectl create secret generic "$SECRET_NAME" -n "$SECRET_NAMESPACE" \ + --from-literal=agentSpaceId="$SPACE_ID" \ + --from-literal=applicationId="$APP_ID" \ + --from-literal=serviceRoleArn="$SERVICE_ROLE_ARN" \ + --from-literal=diffBucket="$DIFF_BUCKET" \ + --from-literal=region="$AWS_REGION" \ + --dry-run=client -o yaml | kubectl apply -f - + +log "done. agentSpaceId=${SPACE_ID} applicationId=${APP_ID:-}" diff --git a/gitops/addons/charts/dark-factory/scripts/comment.js b/gitops/addons/charts/dark-factory/scripts/comment.js new file mode 100644 index 00000000..46ddac14 --- /dev/null +++ b/gitops/addons/charts/dark-factory/scripts/comment.js @@ -0,0 +1,36 @@ +// comment.js β€” upsert ONE marker-based PR comment (edit in place, no spam). +// Shared by the review roles, holdout, and deploy-test so their findings land on +// the PR, not just in pod logs. Idempotent: same marker β†’ same comment edited. +// +// Usage: node comment.js (body read from stdin) +// Env: GH_TOKEN, REPO (owner/name), PR (number) +const https = require("https"); +const { GH_TOKEN, REPO, PR } = process.env; +const marker = process.argv[2]; +if (!marker || !PR) { console.error("[comment] missing marker or PR β€” skip"); process.exit(0); } +const H = { "User-Agent": "dark-factory-comment", Authorization: `Bearer ${GH_TOKEN}`, Accept: "application/vnd.github+json" }; + +function api(method, path, body) { + const data = body ? JSON.stringify(body) : null; + return new Promise((resolve, reject) => { + const req = https.request({ host: "api.github.com", method, path, headers: { ...H, ...(data ? { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(data) } : {}) } }, + (r) => { let b = ""; r.on("data", (c) => (b += c)); r.on("end", () => (r.statusCode < 300 ? resolve(b ? JSON.parse(b) : {}) : reject(new Error(`${method} ${path} ${r.statusCode}: ${b.slice(0, 120)}`)))); }); + req.on("error", reject); if (data) req.write(data); req.end(); + }); +} + +let body = ""; +process.stdin.on("data", (d) => (body += d)).on("end", async () => { + const full = `\n${body.trim()}`; + try { + // Find an existing comment carrying this marker (paginate a little). + let existing = null; + for (let page = 1; page <= 5 && !existing; page++) { + const cs = await api("GET", `/repos/${REPO}/issues/${PR}/comments?per_page=100&page=${page}`); + if (!cs.length) break; + existing = cs.find((c) => (c.body || "").includes(``)); + } + if (existing) { await api("PATCH", `/repos/${REPO}/issues/comments/${existing.id}`, { body: full }); console.log(`[comment] updated ${marker} on PR #${PR}`); } + else { await api("POST", `/repos/${REPO}/issues/${PR}/comments`, { body: full }); console.log(`[comment] created ${marker} on PR #${PR}`); } + } catch (e) { console.error(`[comment] non-fatal: ${e.message}`); process.exit(0); } +}); diff --git a/gitops/addons/charts/dark-factory/scripts/deploy-test.sh b/gitops/addons/charts/dark-factory/scripts/deploy-test.sh new file mode 100644 index 00000000..1217bc7f --- /dev/null +++ b/gitops/addons/charts/dark-factory/scripts/deploy-test.sh @@ -0,0 +1,102 @@ +#!/usr/bin/env bash +# deploy-test.sh β€” content-aware verification of a PR's deployable artifacts. +# Runs in the TRUSTED hub deploy-test step (the only step with K8s access β€” never +# the coder). Shipped as a ConfigMap file (not inline in the WorkflowTemplate) so +# there are no YAML-block-scalar quoting hazards. Kind-driven so it generalizes to +# future profiles (terraform | k8s | ... ). +# +# Env: GH_TOKEN, REPO (owner/name), BRANCH, ISSUE_NUMBER, KIND, MANIFEST_PATH, +# TF_PATH, READY_TIMEOUT, BLOCKING, WF_NAME +set -eu + +WORK=/tmp/dt +rm -rf "$WORK"; mkdir -p "$WORK" +git clone --quiet --depth 1 --branch "$BRANCH" \ + "https://x-access-token:${GH_TOKEN}@github.com/${REPO}.git" "$WORK/repo" +cd "$WORK/repo" + +STATE=success +DESC="deploy-test passed" +RC=0 +FENCE='```' +REPORT=/tmp/report.md +: > "$REPORT" + +case "$KIND" in + terraform) + cd "$TF_PATH" + echo "[deploy-test] terraform validate in $(pwd) ($(terraform version | head -1))" + if terraform init -backend=false -input=false -no-color >/tmp/tf.log 2>&1 \ + && terraform validate -no-color >>/tmp/tf.log 2>&1; then + FMT_N="$(terraform fmt -check -recursive -no-color 2>/dev/null | wc -l | tr -d ' ' || echo 0)" + echo "[deploy-test] terraform validate OK (fmt: ${FMT_N} unformatted)" + DESC="terraform validate passed" + echo "\`terraform init -backend=false\` + \`terraform validate\` passed. (fmt: ${FMT_N} file(s) not formatted)" >> "$REPORT" + else + echo "[deploy-test] terraform init/validate FAILED"; tail -30 /tmp/tf.log || true + STATE=failure; DESC="terraform validate failed"; RC=1 + { echo "\`terraform init/validate\` failed:"; echo "$FENCE"; tail -20 /tmp/tf.log; echo "$FENCE"; } >> "$REPORT" + fi + cd "$WORK/repo" + ;; + + k8s) + NS="df-test-${ISSUE_NUMBER}-${WF_NAME}" + NS="$(echo "$NS" | tr '[:upper:]' '[:lower:]' | cut -c1-63)" + cleanup() { echo "[deploy-test] tearing down namespace $NS"; kubectl delete namespace "$NS" --ignore-not-found --wait=false >/dev/null 2>&1 || true; } + trap cleanup EXIT + if [ ! -e "$MANIFEST_PATH" ]; then + echo "[deploy-test] no manifests at '$MANIFEST_PATH' (advisory)" + DESC="no manifests at $MANIFEST_PATH" + echo "No manifests found at \`$MANIFEST_PATH\`." >> "$REPORT" + elif kubectl create namespace "$NS" >/dev/null 2>&1 \ + && kubectl label namespace "$NS" dark-factory.io/ephemeral=true dark-factory.io/issue-number="$ISSUE_NUMBER" --overwrite >/dev/null 2>&1 \ + && kubectl apply -n "$NS" -f "$MANIFEST_PATH" >/tmp/apply.log 2>&1; then + cat /tmp/apply.log + echo "[deploy-test] waiting up to ${READY_TIMEOUT}s for workloads Available..." + if kubectl wait -n "$NS" --for=condition=Available --timeout="${READY_TIMEOUT}s" deploy --all >/dev/null 2>&1; then + echo "[deploy-test] all Deployments Available" + echo "Deployed to an ephemeral namespace; all Deployments became Available." >> "$REPORT" + else + BAD="$(kubectl get pods -n "$NS" --no-headers 2>/dev/null | grep -cE 'CrashLoopBackOff|Error|ImagePullBackOff' || true)" + if [ "${BAD:-0}" != "0" ]; then + STATE=failure; DESC="deploy-test: ${BAD} pod(s) not healthy"; RC=1 + echo "${BAD} pod(s) not healthy after apply." >> "$REPORT" + else + echo "Applied; no Deployments to wait on." >> "$REPORT" + fi + fi + else + cat /tmp/apply.log 2>/dev/null || true + STATE=failure; DESC="kubectl apply failed"; RC=1 + { echo "\`kubectl apply\` failed:"; echo "$FENCE"; tail -20 /tmp/apply.log 2>/dev/null; echo "$FENCE"; } >> "$REPORT" + fi + ;; + + *) + echo "[deploy-test] unknown kind '$KIND' β€” nothing to do" + DESC="no deploy test for kind=$KIND" + echo "No deploy test defined for kind \`$KIND\`." >> "$REPORT" + ;; +esac + +SHA="$(git rev-parse HEAD)" + +# 1) commit status +curl -fsS -X POST -H "Authorization: Bearer ${GH_TOKEN}" -H "Accept: application/vnd.github+json" \ + "https://api.github.com/repos/${REPO}/statuses/${SHA}" \ + -d "{\"state\":\"${STATE}\",\"context\":\"dark-factory/deploy-test\",\"description\":\"${DESC}\"}" >/dev/null 2>&1 \ + && echo "[deploy-test] posted dark-factory/deploy-test=${STATE}" \ + || echo "[deploy-test] WARN: status post failed" + +# 2) findings PR comment (marker upsert via comment.js) +ICON="βœ…"; [ "$STATE" = success ] || ICON="❌" +PR="$(curl -fsS -H "Authorization: Bearer ${GH_TOKEN}" \ + "https://api.github.com/repos/${REPO}/pulls?head=${REPO%%/*}:${BRANCH}&state=open" 2>/dev/null \ + | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{try{console.log(JSON.parse(s)[0].number||"")}catch(e){}})' 2>/dev/null || echo "")" +if [ -n "$PR" ]; then + { echo "### ${ICON} Deploy test (${KIND})"; echo "**${DESC}**"; echo; cat "$REPORT"; } \ + | GH_TOKEN="$GH_TOKEN" REPO="$REPO" PR="$PR" node /scripts/comment.js "dark-factory:deploy-test" +fi + +if [ "$BLOCKING" = "true" ]; then exit "$RC"; else exit 0; fi diff --git a/gitops/addons/charts/dark-factory/scripts/iterate.js b/gitops/addons/charts/dark-factory/scripts/iterate.js new file mode 100644 index 00000000..63d9bfd9 --- /dev/null +++ b/gitops/addons/charts/dark-factory/scripts/iterate.js @@ -0,0 +1,162 @@ +// iterate.js β€” route a human PR comment back to the coder as a revision request. +// Runs in the df-iterate workflow (fired by an issue_comment on a Dark Factory PR). +// +// The issue_comment payload gives us the PR number + comment text, but not the +// coder branch or the original df issue number. So we: (1) look up the PR to get +// head.ref = df/issue- β†’ issue number; (2) enforce the iteration cap via a +// label on the PR; (3) submit a df-run Workflow (same pipeline) with iterate-note +// = the comment, which df-run injects as DF_ITERATE_NOTE so the coder revises the +// existing branch. Submits via the in-cluster k8s API using the pod SA token +// (the df-iterate workflow runs as dark-factory-sensor, which can create Workflows). +// +// Env: GH_TOKEN, REPO, PR, COMMENT_BODY, MAX_ITERATIONS, ARGO_NAMESPACE, +// BIFROST_URL, CODER_PROFILE. +const fs = require("fs"); +const https = require("https"); + +const { GH_TOKEN, REPO, PR, COMMENT_BODY, COMMENT_AUTHOR, ARGO_NAMESPACE } = process.env; +const MAX_ITERATIONS = parseInt(process.env.MAX_ITERATIONS || "3", 10); +const GH = { "User-Agent": "dark-factory-iterate", Authorization: `Bearer ${GH_TOKEN}`, Accept: "application/vnd.github+json" }; +const ITER_LABEL_PREFIX = "df-iterations/"; + +function gh(method, path, body) { + const data = body ? JSON.stringify(body) : null; + return new Promise((resolve, reject) => { + const req = https.request({ host: "api.github.com", method, path, headers: { ...GH, ...(data ? { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(data) } : {}) } }, + (r) => { let b = ""; r.on("data", (c) => (b += c)); r.on("end", () => { + if (r.statusCode >= 200 && r.statusCode < 300) resolve(b ? JSON.parse(b) : {}); + else reject(Object.assign(new Error(`gh ${method} ${path} -> ${r.statusCode}: ${b.slice(0, 160)}`), { statusCode: r.statusCode })); + }); }); + req.on("error", reject); if (data) req.write(data); req.end(); + }); +} + +// Submit a Workflow to the in-cluster k8s API using the pod SA token. +function submitWorkflow(wf) { + const token = fs.readFileSync("/var/run/secrets/kubernetes.io/serviceaccount/token", "utf8"); + const ca = "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt"; + const body = JSON.stringify(wf); + return new Promise((resolve, reject) => { + const req = https.request({ + host: "kubernetes.default.svc", method: "POST", + path: `/apis/argoproj.io/v1alpha1/namespaces/${ARGO_NAMESPACE}/workflows`, + headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", "Content-Length": Buffer.byteLength(body) }, + ca: fs.readFileSync(ca), + }, (r) => { let b = ""; r.on("data", (c) => (b += c)); r.on("end", () => { + if (r.statusCode >= 200 && r.statusCode < 300) resolve(JSON.parse(b)); + else reject(Object.assign(new Error(`k8s submit -> ${r.statusCode}: ${b.slice(0, 200)}`), { statusCode: r.statusCode })); + }); }); + req.on("error", reject); req.write(body); req.end(); + }); +} + +async function main() { + // SELF-TRIGGER GUARD (critical): the factory posts its OWN comments to the PR + // (sticky status, review findings, iteration notices) using a real user's PAT β€” + // so GitHub reports comment.user.type="User", and the Sensor's "exclude Bot" + // filter does NOT exclude them. Without this guard, EVERY factory comment fires + // df-iterate β†’ new commit β†’ more comments β†’ runaway loop (observed: 3 runs + + // 3 commits + split statuses on one issue). All factory-authored comments carry a + // "dark-factory:" HTML marker; skip any comment that has one. + if (COMMENT_BODY && /"; +const icon = (s) => (s === "success" ? "βœ…" : s === "failure" || s === "error" ? "❌" : s === "pending" ? "⏳" : "⬜"); + +async function main() { + const owner = REPO.split("/")[0]; + const prs = await api("GET", `/repos/${REPO}/pulls?head=${owner}:${BRANCH}&state=open`); + if (!prs.length) { console.log("[df-run] no open PR β€” nothing to update"); return; } + const pr = prs[0]; + const concToState = (c) => ({ success: "success", neutral: "success", skipped: "success", + failure: "failure", timed_out: "failure", cancelled: "failure", action_required: "failure" }[c] || "pending"); + // MULTI-SHA ROBUSTNESS: the hub verify steps (holdout/security/deploy-test) post + // their commit statuses on the SHA that was HEAD when they ran. If the coder then + // pushes another commit (an impl re-post, or an agent re-review moves head), + // reading only pr.head.sha shows those steps as "not run" even though they passed + // on an earlier commit. So collect statuses + check-runs across ALL PR commits, + // oldest -> newest, letting a later commit's verdict override an earlier one. + const by = {}; + let shas = [pr.head.sha]; + try { + const commits = await api("GET", `/repos/${REPO}/pulls/${pr.number}/commits?per_page=100`); + if (Array.isArray(commits) && commits.length) shas = commits.map((c) => c.sha); // oldest -> newest + } catch (e) { /* fall back to head only */ } + for (const sha of shas) { + try { + const st = await api("GET", `/repos/${REPO}/commits/${sha}/status`); + // GitHub returns statuses newest-first; take the first (latest) per context on this commit. + const seen = {}; + for (const s of st.statuses || []) { + if (seen[s.context]) continue; + seen[s.context] = 1; + by[s.context] = { state: s.state, desc: s.description || "", url: s.target_url || "" }; + } + } catch (e) { /* skip this commit */ } + try { + const cr = await api("GET", `/repos/${REPO}/commits/${sha}/check-runs`); + for (const c of cr.check_runs || []) { + const state = c.status === "completed" ? concToState(c.conclusion) : "pending"; + by[c.name] = { state, desc: (c.output && c.output.title) || c.conclusion || c.status }; + } + } catch (e) { /* non-fatal */ } + } + const row = (ctx, label) => { + const s = by[ctx.includes("/") ? ctx : `dark-factory/${ctx}`]; + if (!s) return `- ⬜ **${label}:** _not run_`; + // A step can report success but be "not applicable" to this change (e.g. the + // holdout gate when no hidden scenario matches a Terraform-only PR). Render + // that as a neutral ⬜ n/a, not a green βœ… that would imply it actually ran. + const na = /not applicable|n\/a/i.test(s.desc || ""); + const mark = na ? "⬜" : icon(s.state); + return `- ${mark} **${label}:** ${s.desc || s.state}`; + }; + + // ── THE REAL AWS AGENTS ARE THE SOURCE OF TRUTH ────────────────────────────── + // Both agents run TWICE on a PR and the copies can DISAGREE: the GitHub App bots + // (aws-security-agent[bot], aws-devops-agent-*[bot]) review the PR directly, while + // our hub-side steps drive the SAME agents headlessly + post dark-factory/* commit + // statuses. The headless copy has been observed to miss findings the App bot caught + // (e.g. a wildcard-ARN IAM policy) β€” so trusting the headless status produced a + // FALSE "no findings / LGTM" next to a bot review listing real findings. Fix: the + // consolidation reads the AGENT BOTS' OWN reviews as authoritative. The headless + // dark-factory/* statuses are demoted to a fallback ONLY when a bot didn't post. + // + // The bots post a formal REVIEW (state COMMENTED) whose body begins with a summary, + // plus INLINE review comments per finding. They do NOT emit a check-run for findings + // and never use CHANGES_REQUESTED, so we parse the review body + count inline + // comments rather than reading a state flag. + const reviews = (await api("GET", `/repos/${REPO}/pulls/${pr.number}/reviews?per_page=100`).catch(() => [])) || []; + const prComments = (await api("GET", `/repos/${REPO}/pulls/${pr.number}/comments?per_page=100`).catch(() => [])) || []; + // ROUND-AWARENESS: only trust a bot review/comment tied to the CURRENT head SHA. + // On a fix round the coder force-pushes a new commit; a prior round's "1 finding" + // review still exists on the PR, so reading it would mirror a STALE verdict. + const headSha = pr.head.sha; + const forHead = (item) => (item.commit_id ? item.commit_id === headSha : true); + const latestBotReview = (pred) => (reviews.filter((r) => pred((r.user || {}).login || "") && forHead(r)).slice(-1)[0]) || null; + const inlineCountBy = (pred) => prComments.filter((c) => pred((c.user || {}).login || "") && forHead(c)).length; + const isSecBot = (l) => /^aws-security-agent(\[bot\]|-.*\[bot\])?$/i.test(l) || /security-agent/i.test(l) && /\[bot\]/i.test(l); + const isDevBot = (l) => /aws-devops-agent/i.test(l) && /\[bot\]/i.test(l); + + // Parse an agent bot review body into {state, desc}. A body that reports one or + // more findings β†’ failure; an explicit "no findings / no issues" β†’ success; a bot + // that only said it's "reviewing…" (no verdict yet) β†’ pending. + const parseAgentVerdict = (body, inlineFindings) => { + const b = (body || "").toLowerCase(); + const m = b.match(/(\d+)\s+(?:medium|high|low|critical|informational)?[- ]?severity?\s*finding/) || + b.match(/identified\s+\*{0,2}(\d+)\b[^.]*finding/) || b.match(/\b(\d+)\s+finding/); + const declaredNum = m ? parseInt(m[1], 10) : null; + const saysClean = /no (issues identified|findings|security issues)|no issues were|looks good|lgtm/i.test(body || ""); + const stillReviewing = /is reviewing|will post feedback|analysis in progress/i.test(body || "") && declaredNum === null && !saysClean; + const n = declaredNum !== null ? declaredNum : (inlineFindings > 0 ? inlineFindings : 0); + if (stillReviewing) return { state: "pending", desc: "review in progress", n: null }; + if (n > 0) return { state: "failure", desc: `${n} finding(s) β€” changes requested`, n }; + if (saysClean || (declaredNum === 0)) return { state: "success", desc: "no findings", n: 0 }; + // A bot review with a body we couldn't classify + inline comments = treat as findings. + if (inlineFindings > 0) return { state: "failure", desc: `${inlineFindings} finding(s) β€” changes requested`, n: inlineFindings }; + return null; // no usable bot signal + }; + + // Security: the dark-factory/security STATUS is authoritative β€” it's posted by the + // security-agent step (security-wait.js), which waits for the bot's TERMINAL verdict + // on THIS commit and encodes findings/clean there. Prefer it, so status.js and the + // waiter never disagree (both parse the same bot, but the waiter is round/commit-aware + // and won't resolve on stale/partial inline comments). Fall back to re-parsing the bot + // review only if the status is somehow absent. + const secBotReview = latestBotReview(isSecBot); + const secBotInline = inlineCountBy(isSecBot); + const secBot = secBotReview ? parseAgentVerdict(secBotReview.body, secBotInline) : null; + const secResolved = + (by["dark-factory/security"] ? { state: by["dark-factory/security"].state, desc: by["dark-factory/security"].desc || by["dark-factory/security"].state } : null) + || secBot + || (SECURITY_CHECK && by[SECURITY_CHECK] ? { state: by[SECURITY_CHECK].state, desc: by[SECURITY_CHECK].desc || by[SECURITY_CHECK].state } : null); + const securityRow = secResolved + ? `- ${icon(secResolved.state)} **Security review (AWS Security Agent):** ${secResolved.desc}` + : `- ⬜ **Security review (AWS Security Agent):** _not run_`; + + // DevOps: the App bot's release-readiness verdict lives in its commit STATUS/check + // (change approved / BLOCK / proceed-with-caution) β€” that IS the real bot. But it + // also posts inline review comments; if the status says "approved" yet the bot left + // change-requesting inline comments, surface that (do not silently call it clean). + const devBotStatus = (DEVOPS_CHECK && by[DEVOPS_CHECK]) ? by[DEVOPS_CHECK] : by["dark-factory/devops"]; + const devInline = inlineCountBy(isDevBot); + const devBlockedByStatus = devBotStatus && (devBotStatus.state === "failure" || /block|not (safe|ready)|changes? requested/i.test(devBotStatus.desc || "")); + const devResolved = devBotStatus + ? { state: devBlockedByStatus ? "failure" : devBotStatus.state, desc: devBotStatus.desc || devBotStatus.state, url: devBotStatus.url || "" } + : null; + // Surface the DevOps Agent's full release-readiness report link (target_url) so + // reviewers can open the assessment, plus a count of its inline comments. + const devopsRow = devResolved + ? `- ${icon(devResolved.state)} **DevOps review (AWS DevOps Agent):** ${devResolved.desc}` + + (devResolved.url ? ` β€” [view report β†—](${devResolved.url})` : "") + + (devInline ? ` _(+${devInline} inline comment(s))_` : "") + : `- ⬜ **DevOps review (AWS DevOps Agent):** _not run_`; + + // Overall (= merge readiness) = worst across the BLOCKING signals only: build + + // the two real agent bots. Holdout is ADVISORY (holdout.blocking=false) β€” it's a + // train/test quality signal, NOT a merge gate β€” so a red holdout is SHOWN in its + // row but does NOT flip the verdict to "changes requested" (only a real Security/ + // DevOps agent finding or a build break does). Set HOLDOUT_BLOCKING=true to include + // it in the gate. + const HOLDOUT_BLOCKING = (process.env.HOLDOUT_BLOCKING || "").toLowerCase() === "true"; + const overall = (() => { + const vals = [ + (by["dark-factory/implementation"] || {}).state, + (HOLDOUT_BLOCKING && by["dark-factory/holdout"] && !/not applicable|n\/a/i.test(by["dark-factory/holdout"].desc || "")) ? by["dark-factory/holdout"].state : undefined, + secResolved ? secResolved.state : undefined, + devResolved ? devResolved.state : undefined, + ].filter((v) => v !== undefined); + if (vals.includes("failure") || vals.includes("error")) return "failure"; + if (vals.includes("pending")) return "pending"; + return vals.length ? "success" : "pending"; + })(); + const block = [ + MARKER, + "### 🏭 Dark Factory β€” verification", + row("implementation", "Build + unit tests"), + // Holdout appears ONLY when it actually evaluated something. The scenarios are + // repo/language-specific (appliesWhen), so a Terraform-only PR has none β€” in + // that case the step reports "not applicable"; omit the row entirely (like + // deploy-test) rather than clutter the board with a not-applicable/​not-run line. + ...((by["dark-factory/holdout"] && !/not applicable|n\/a/i.test(by["dark-factory/holdout"].desc || "")) + ? [row("holdout", "Holdout gate")] : []), + securityRow, + devopsRow, + // deploy-test only appears when the PR was deployable; omit the row otherwise. + ...(by["dark-factory/deploy-test"] ? [row("deploy-test", "Deploy test")] : []), + "", + `_Overall: **${overall}**. Autonomously implemented in a hardware-isolated Kata micro-VM; verification ran as independent hub-side steps (see the checks above). Awaiting human review._`, + ].join("\n"); + + let body = pr.body || ""; + if (body.includes(MARKER)) { + body = body.slice(0, body.indexOf(MARKER)).trimEnd(); + body = (body ? body + "\n\n" : "") + block; + } else { + body = body.trimEnd(); + body = (body ? body + "\n\n" : "") + block; + } + await api("PATCH", `/repos/${REPO}/pulls/${pr.number}`, { body }); + console.log(`[df-run] PR #${pr.number} body updated β€” overall=${overall}`); + + // ── Consolidated verdict REVIEW ────────────────────────────────────────── + // The AWS agent Apps review autonomously and inconsistently (sometimes a formal + // review that lands in the sidebar, sometimes only an issue comment; and GitHub + // App bots cannot be added via the requested_reviewers API). So β€” for a + // CONSISTENT, always-present reviewer signal β€” the pipeline posts ONE formal PR + // review summarizing both agents' verdicts (as the workflow's GitHub identity). + // + // Gate: post once the steps the WORKFLOW controls are resolved (implementation + + // security). We deliberately do NOT wait for `overall` to be non-pending, because + // the DevOps Agent App reviews ASYNCHRONOUSLY and its check is often still PENDING + // when sticky-status runs (at workflow end) β€” and sticky-status runs only ONCE, so + // gating on it would mean the review never posts. A still-pending DevOps verdict is + // shown as "in progress" in the review body. Idempotent via a hidden marker. + const implState = (by["dark-factory/implementation"] || {}).state; + // Ready once build is done and the Security agent has a verdict (its findings are + // the strict gate). DevOps may still be async-pending β€” shown as "in progress". + const secState = secResolved ? secResolved.state : undefined; + const readyToReview = implState && implState !== "pending" && (!secState || secState !== "pending"); + if (POST_VERDICT_REVIEW && readyToReview) { + const RVMARK = ""; + // ROUND-AWARENESS: tag each verdict with the head SHA it evaluated. On a fix + // round the coder force-pushes a NEW commit β†’ new SHA β†’ we post a FRESH verdict + // (so the PR visibly moves βŒβ†’βœ…), rather than skipping because a prior-round + // verdict exists. Idempotent WITHIN a SHA (a re-run on the same commit no-ops). + const shaTag = ``; + try { + const existing = await api("GET", `/repos/${REPO}/pulls/${pr.number}/reviews?per_page=100`); + const already = (existing || []).some((r) => (r.body || "").includes(shaTag)); + if (already) { + console.log(`[df-run] verdict review already posted for ${pr.head.sha.slice(0,7)} β€” skipping`); + } else { + const secLine = securityRow.replace(/^- /, ""); + const devLine = devopsRow.replace(/^- /, ""); + const holdoutLine = (by["dark-factory/holdout"] && !/not applicable|n\/a/i.test(by["dark-factory/holdout"].desc || "")) + ? "\n" + row("holdout", "Holdout gate").replace(/^- /, "") : ""; + // Build a plain-English findings summary from whichever agents flagged issues. + const flagged = []; + if (secResolved && secResolved.state === "failure") flagged.push(`Security (${secResolved.desc})`); + if (devResolved && devResolved.state === "failure") flagged.push(`DevOps (${devResolved.desc})`); + // When overall is green, note if the advisory holdout is red (it doesn't + // block the merge, but the wording shouldn't claim "no findings" if a row is ❌). + const holdoutRed = by["dark-factory/holdout"] && by["dark-factory/holdout"].state === "failure" + && !/not applicable|n\/a/i.test(by["dark-factory/holdout"].desc || ""); + const verdictLine = + overall === "failure" + ? `**Overall: ❌ Changes requested β€” do NOT merge.** ${flagged.length ? flagged.join(" and ") + " flagged issues" : "One or more checks failed"}. Address the agents' findings (see their inline review comments), push a fix, and the pipeline re-evaluates. This is NOT approved.` + : overall === "pending" + ? "**Overall: ⏳ Security cleared; DevOps review still in progress** β€” final verdict pending the DevOps release-readiness review. Not yet approved." + : holdoutRed + ? "**Overall: βœ… Cleared to merge** β€” the AWS Security & DevOps agents found no blocking issues (build + both agents green). The holdout gate is below threshold but is ADVISORY (a train/test quality signal, not a merge gate) β€” review it before merging. Human approval still required." + : "**Overall: βœ… All checks green** β€” Build, Holdout, Security, and DevOps agents all cleared with no findings. Looks good to merge (human approval still required)."; + const reviewBody = [ + RVMARK, + shaTag, + "### 🏭 Dark Factory β€” consolidated agent verdict", + "", + row("implementation", "Build + unit tests").replace(/^- /, ""), + holdoutLine ? holdoutLine.trim() : null, + secLine, + devLine, + "", + verdictLine, + "", + "_The AWS Security & DevOps agents' own reviews are the source of truth; this consolidated review reads their verdicts (findings block the merge) so both are always visible in one place. Posted by the Dark Factory pipeline as a COMMENT β€” a human still owns the merge decision._", + ].filter((x) => x !== null).join("\n"); + // Event: REQUEST_CHANGES when an agent flagged findings (so the PR visibly + // shows changes-requested, not a bland comment); COMMENT otherwise. Never + // APPROVE β€” the human owns merge approval. If REQUEST_CHANGES is rejected + // (e.g. can't request changes on own PR in some setups), fall back to COMMENT. + const event = overall === "failure" ? "REQUEST_CHANGES" : "COMMENT"; + try { + await api("POST", `/repos/${REPO}/pulls/${pr.number}/reviews`, { event, body: reviewBody }); + } catch (e) { + await api("POST", `/repos/${REPO}/pulls/${pr.number}/reviews`, { event: "COMMENT", body: reviewBody }); + } + console.log(`[df-run] posted consolidated verdict review (event=${event}, overall=${overall})`); + + // ── AUTO-FIX loop ────────────────────────────────────────────────────── + // On a ❌ verdict from the real agents, feed their findings straight back to + // the coder (no human paraphrasing): submit a bounded df-run revision with + // iterate-note = the collected Security + DevOps findings. The human only + // approves at the end. Bounded by MAX_ITERATIONS via a df-iterations/ label. + if (overall === "failure" && AUTO_FIX && (secResolved && secResolved.state === "failure" || devResolved && devResolved.state === "failure")) { + await maybeAutoFix(pr, { secBotReview, secBotInline, devResolved, comments: prComments, reviews }); + } + } + } catch (e) { + console.log(`[df-run] verdict review skipped: ${e.message.slice(0, 140)}`); + } + } +} + +// Collect the agents' findings into a plain-text fix instruction, enforce the +// iteration cap, and submit a df-run revision via the in-cluster k8s API. +async function maybeAutoFix(pr, ctx) { + try { + const ITER = "df-iterations/"; + const issue = await api("GET", `/repos/${REPO}/issues/${pr.number}`).catch(() => ({})); + const labels = ((issue && issue.labels) || []).map((l) => (typeof l === "string" ? l : l.name)); + const cur = labels.filter((l) => l.startsWith(ITER)).map((l) => parseInt(l.slice(ITER.length), 10)).filter((n) => !isNaN(n)); + const count = cur.length ? Math.max(...cur) : 0; + if (count >= MAX_ITERATIONS) { + console.log(`[df-run] auto-fix cap reached (${count}/${MAX_ITERATIONS}) β€” leaving for a human`); + await api("POST", `/repos/${REPO}/issues/${pr.number}/comments`, { body: `\n🏭 Dark Factory: auto-fix cap reached (${count}/${MAX_ITERATIONS}). The agents still report findings β€” a human should resolve or push a fix.` }).catch(() => {}); + return; + } + const next = count + 1; + + // Gather the findings text: the Security bot's review summary + both agents' + // inline review comments (path:line β€” what is the issue), truncated for the note. + const findingLines = []; + const secRv = ctx.secBotReview; + if (secRv && secRv.body) findingLines.push(`SECURITY AGENT:\n${secRv.body.trim().slice(0, 1500)}`); + const inlineFor = (pred, label) => { + const items = (ctx.comments || []).filter((c) => pred((c.user || {}).login)); + if (!items.length) return; + findingLines.push(`${label} inline findings:`); + for (const c of items.slice(0, 8)) findingLines.push(`- ${c.path}${c.line ? `:${c.line}` : ""} β€” ${(c.body || "").replace(/\s+/g, " ").trim().slice(0, 240)}`); + }; + inlineFor((l) => /aws-security-agent/i.test(l) && /\[bot\]/i.test(l), "Security"); + inlineFor((l) => /aws-devops-agent/i.test(l) && /\[bot\]/i.test(l), "DevOps"); + if (ctx.devResolved && ctx.devResolved.state === "failure") findingLines.push(`DEVOPS AGENT: ${ctx.devResolved.desc}${ctx.devResolved.url ? ` (report: ${ctx.devResolved.url})` : ""}`); + const note = [ + "The AWS Security/DevOps agents requested changes on your PR. Address ALL of the findings below, then rebuild + re-run tests. Do not introduce new issues.", + "", + ...findingLines, + ].join("\n").slice(0, 8000); + + // Bump the counter label. + for (const l of cur) await api("DELETE", `/repos/${REPO}/issues/${pr.number}/labels/${encodeURIComponent(ITER + l)}`).catch(() => {}); + await api("POST", `/repos/${REPO}/issues/${pr.number}/labels`, { labels: [`${ITER}${next}`] }).catch(() => {}); + + const wf = { + apiVersion: "argoproj.io/v1alpha1", kind: "Workflow", + metadata: { name: `df-run-${ISSUE_NUMBER}-fix${next}`, namespace: ARGO_NAMESPACE }, + spec: { + workflowTemplateRef: { name: "df-run" }, + arguments: { parameters: [ + { name: "issue-id", value: `${ISSUE_NUMBER}` }, + { name: "issue-number", value: `${ISSUE_NUMBER}` }, + { name: "repo", value: REPO }, + { name: "issue-title", value: pr.title || "" }, + { name: "issue-body", value: "" }, + { name: "base-branch", value: BASE_BRANCH || "main" }, + // Pass the findings as BASE64 (iterate-note-b64), NOT raw iterate-note β€” + // the findings are multi-line markdown with quotes/braces that break the + // claim-sandbox manifest YAML when injected raw (observed: df-run-84-fix1 + // 'manifest must be a valid yaml'). The coder decodes it. + { name: "iterate-note", value: "" }, + { name: "iterate-note-b64", value: Buffer.from(note, "utf8").toString("base64") }, + { name: "trigger-label", value: TRIGGER_LABEL || "dark-factory" }, + ] }, + }, + }; + await submitWorkflow(wf); + console.log(`[df-run] AUTO-FIX submitted df-run-${ISSUE_NUMBER}-fix${next} (round ${next}/${MAX_ITERATIONS})`); + await api("POST", `/repos/${REPO}/issues/${pr.number}/comments`, { body: `\n🏭 Dark Factory β€” **auto-fix round ${next}/${MAX_ITERATIONS}**: the coder is revising \`${BRANCH}\` to address the agents' findings above. A new verdict will be posted when the reviews re-run.` }).catch(() => {}); + } catch (e) { + if (e && e.statusCode === 409) { console.log("[df-run] auto-fix already in flight (dedup) β€” no-op"); return; } + console.log(`[df-run] auto-fix skipped (non-fatal): ${(e && e.message || e).toString().slice(0, 160)}`); + } +} + +// Submit a Workflow to the in-cluster k8s API using the pod SA token (same as iterate.js). +function submitWorkflow(wf) { + const fs = require("fs"); + const token = fs.readFileSync("/var/run/secrets/kubernetes.io/serviceaccount/token", "utf8"); + const ca = fs.readFileSync("/var/run/secrets/kubernetes.io/serviceaccount/ca.crt"); + const body = JSON.stringify(wf); + return new Promise((resolve, reject) => { + const req = https.request({ + host: "kubernetes.default.svc", method: "POST", + path: `/apis/argoproj.io/v1alpha1/namespaces/${ARGO_NAMESPACE}/workflows`, + headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", "Content-Length": Buffer.byteLength(body) }, + ca, + }, (r) => { let b = ""; r.on("data", (c) => (b += c)); r.on("end", () => { + if (r.statusCode >= 200 && r.statusCode < 300) resolve(JSON.parse(b)); + else reject(Object.assign(new Error(`k8s submit -> ${r.statusCode}: ${b.slice(0, 200)}`), { statusCode: r.statusCode })); + }); }); + req.on("error", reject); req.write(body); req.end(); + }); +} + +main().catch((e) => { console.error(`[df-run] status update failed (non-fatal): ${e.message}`); process.exit(0); }); diff --git a/gitops/addons/charts/dark-factory/templates/05-externalsecret-github.yaml b/gitops/addons/charts/dark-factory/templates/05-externalsecret-github.yaml new file mode 100644 index 00000000..04ed8506 --- /dev/null +++ b/gitops/addons/charts/dark-factory/templates/05-externalsecret-github.yaml @@ -0,0 +1,92 @@ +{{- /* +GitHub credential β€” synced from AWS Secrets Manager via ExternalSecret (the same +mechanism the rest of the platform uses, e.g. hub/config, keycloak-clients). This +replaces the manually-created dark-factory-github Secrets so the credential is: + - GitOps-managed + reproducible on a cluster rebuild (no manual kubectl create), + - auto-refreshed on rotation (refreshInterval), + - single source of truth in Secrets Manager (secret id: github.secretsManagerKey). + +One ExternalSecret per namespace that needs the token, each mapping the SM secret's +JSON properties to that namespace's expected key names: + - argo β†’ key `token` (workflow steps) + - argo-events β†’ keys `token` + `webhook-secret` (EventSource HMAC) + - agent-sandbox-system β†’ key `gh-token` (projected into the coder VM) + +NOTE (future hardening, docs/dark-factory Β§credential): move to a GitHub App with +short-lived installation tokens, and a NARROWER token (contents:write only) for the +untrusted coder vs. the trusted orchestrator. This wiring makes that a values change. +*/ -}} +{{- if .Values.github.externalSecret.enabled }} +{{- $store := .Values.github.externalSecret.clusterSecretStore }} +{{- $key := .Values.github.externalSecret.secretsManagerKey }} +{{- $refresh := .Values.github.externalSecret.refreshInterval }} +# ── argo: token ────────────────────────────────────────────────────────────── +apiVersion: external-secrets.io/v1 +kind: ExternalSecret +metadata: + name: {{ .Values.github.tokenSecret }} + namespace: {{ .Values.argo.namespace }} + labels: + {{- include "dark-factory.labels" . | nindent 4 }} +spec: + refreshInterval: {{ $refresh }} + secretStoreRef: + kind: ClusterSecretStore + name: {{ $store }} + target: + name: {{ .Values.github.tokenSecret }} + creationPolicy: Owner + data: + - secretKey: {{ .Values.github.tokenKey }} + remoteRef: + key: {{ $key }} + property: token +--- +# ── argo-events: token + webhook-secret ────────────────────────────────────── +apiVersion: external-secrets.io/v1 +kind: ExternalSecret +metadata: + name: {{ .Values.github.tokenSecret }} + namespace: {{ .Values.trigger.argoEvents.namespace }} + labels: + {{- include "dark-factory.labels" . | nindent 4 }} +spec: + refreshInterval: {{ $refresh }} + secretStoreRef: + kind: ClusterSecretStore + name: {{ $store }} + target: + name: {{ .Values.github.tokenSecret }} + creationPolicy: Owner + data: + - secretKey: token + remoteRef: + key: {{ $key }} + property: token + - secretKey: webhook-secret + remoteRef: + key: {{ $key }} + property: webhook-secret +--- +# ── agent-sandbox-system: gh-token (projected into the coder VM) ────────────── +apiVersion: external-secrets.io/v1 +kind: ExternalSecret +metadata: + name: {{ .Values.github.tokenSecret }} + namespace: {{ .Values.warmPool.namespace }} + labels: + {{- include "dark-factory.labels" . | nindent 4 }} +spec: + refreshInterval: {{ $refresh }} + secretStoreRef: + kind: ClusterSecretStore + name: {{ $store }} + target: + name: {{ .Values.github.tokenSecret }} + creationPolicy: Owner + data: + - secretKey: gh-token + remoteRef: + key: {{ $key }} + property: token +{{- end }} diff --git a/gitops/addons/charts/dark-factory/templates/06-securityagent-bootstrap.yaml b/gitops/addons/charts/dark-factory/templates/06-securityagent-bootstrap.yaml new file mode 100644 index 00000000..f3adf955 --- /dev/null +++ b/gitops/addons/charts/dark-factory/templates/06-securityagent-bootstrap.yaml @@ -0,0 +1,156 @@ +{{- if .Values.securityAgent.enabled }} +{{- /* +AWS Security Agent bootstrap β€” reconciles the Agent Space + Application ONCE +(idempotent) and publishes their IDs into a Secret the df-run security step +reads. Runs as an ArgoCD PreSync hook Job so it completes before the workflow +templates that depend on the Secret are used. + +WHY a Job: the Security Agent space/application are preview resources with no +Terraform/ACK provider, so a find-or-create reconcile Job is the GitOps-native +way to manage them (same pattern as langfuse-minio-init). The agent space is +created ONCE and reused across all runs; only the per-PR code-review + job are +created per df-run. IAM (IRSA role, service role, S3 bucket, OIDC provider) is +committed as Terraform in iam/securityagent.tf β€” NOT minted here. +*/ -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: dark-factory-bootstrap + namespace: {{ .Values.argo.namespace }} + labels: + {{- include "dark-factory.labels" . | nindent 4 }} + annotations: + # IRSA: assume the committed df-securityagent-irsa role (securityagent + # lifecycle + S3). The role's trust policy allows this exact SA (see + # iam/securityagent.tf workflow_service_accounts). + eks.amazonaws.com/role-arn: {{ .Values.securityAgent.irsaRoleArn | quote }} + # This SA + its RBAC + the script CM are PreSync hooks at an EARLIER wave than + # the Job (-1 vs 0) so they exist when the hook Job's pod is created. Regular + # (non-hook) resources sync AFTER PreSync hooks, so the SA can't be a plain + # resource β€” the Job would fail "serviceaccount not found". + argocd.argoproj.io/hook: PreSync + argocd.argoproj.io/hook-delete-policy: BeforeHookCreation + argocd.argoproj.io/sync-wave: "-1" +--- +# The bootstrap Job writes the result Secret in the argo namespace. +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: dark-factory-bootstrap + namespace: {{ .Values.argo.namespace }} + labels: + {{- include "dark-factory.labels" . | nindent 4 }} + annotations: + argocd.argoproj.io/hook: PreSync + argocd.argoproj.io/hook-delete-policy: BeforeHookCreation + argocd.argoproj.io/sync-wave: "-1" +rules: + - apiGroups: [""] + resources: ["secrets"] + verbs: ["get", "create", "update", "patch"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: dark-factory-bootstrap + namespace: {{ .Values.argo.namespace }} + labels: + {{- include "dark-factory.labels" . | nindent 4 }} + annotations: + argocd.argoproj.io/hook: PreSync + argocd.argoproj.io/hook-delete-policy: BeforeHookCreation + argocd.argoproj.io/sync-wave: "-1" +subjects: + - kind: ServiceAccount + name: dark-factory-bootstrap + namespace: {{ .Values.argo.namespace }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: dark-factory-bootstrap +--- +# The bootstrap script as its OWN hooked ConfigMap (wave -1) β€” the Job can't rely +# on the regular df-review ConfigMap (that syncs after PreSync hooks). +apiVersion: v1 +kind: ConfigMap +metadata: + name: dark-factory-bootstrap-script + namespace: {{ .Values.argo.namespace }} + labels: + {{- include "dark-factory.labels" . | nindent 4 }} + annotations: + argocd.argoproj.io/hook: PreSync + argocd.argoproj.io/hook-delete-policy: BeforeHookCreation + argocd.argoproj.io/sync-wave: "-1" +data: + bootstrap-agentspace.sh: | +{{ .Files.Get "scripts/bootstrap-agentspace.sh" | indent 4 }} +--- +apiVersion: batch/v1 +kind: Job +metadata: + name: dark-factory-securityagent-bootstrap + namespace: {{ .Values.argo.namespace }} + labels: + {{- include "dark-factory.labels" . | nindent 4 }} + annotations: + # Reconcile before the sync so the Secret exists when df-run first fires. + argocd.argoproj.io/hook: PreSync + argocd.argoproj.io/hook-delete-policy: BeforeHookCreation + argocd.argoproj.io/sync-wave: "0" +spec: + backoffLimit: 3 + template: + metadata: + labels: + {{- include "dark-factory.labels" . | nindent 8 }} + spec: + serviceAccountName: dark-factory-bootstrap + restartPolicy: Never + volumes: + - name: scripts + configMap: + name: dark-factory-bootstrap-script + defaultMode: 0555 + containers: + - name: bootstrap + # Needs a CURRENT aws-cli v2 (the `securityagent` service is brand-new; + # older/musl aws-cli lacks it) PLUS kubectl (to write the result Secret). + # amazon/aws-cli is glibc + always-current v2 β†’ guaranteed securityagent + # verbs. kubectl is a static binary we fetch at start (works on glibc). + image: {{ .Values.securityAgent.bootstrapImage | default "public.ecr.aws/aws-cli/aws-cli:latest" }} + # amazon/aws-cli's ENTRYPOINT is `aws`; override to a shell. + command: ["/bin/sh", "-c"] + args: + - | + set -eu + echo "[bootstrap] aws version: $(aws --version 2>&1 | head -1)" + aws securityagent help >/dev/null 2>&1 || { echo "[bootstrap] ERROR: aws-cli lacks 'securityagent' β€” cannot reconcile"; exit 1; } + # Fetch a static kubectl (to write the Secret) if not present. + if ! command -v kubectl >/dev/null 2>&1; then + echo "[bootstrap] fetching kubectl..." + ARCH="$(uname -m)"; case "$ARCH" in aarch64|arm64) A=arm64;; *) A=amd64;; esac + KV="$(curl -fsSL https://dl.k8s.io/release/stable.txt)" + curl -fsSL "https://dl.k8s.io/release/${KV}/bin/linux/${A}/kubectl" -o /tmp/kubectl + chmod +x /tmp/kubectl; export PATH="/tmp:$PATH" + fi + bash /scripts/bootstrap-agentspace.sh + env: + - name: AWS_REGION + value: {{ .Values.securityAgent.region | quote }} + - name: SPACE_NAME + value: {{ .Values.securityAgent.spaceName | quote }} + - name: SERVICE_ROLE_ARN + value: {{ .Values.securityAgent.serviceRoleArn | quote }} + - name: DIFF_BUCKET + value: {{ .Values.securityAgent.diffBucket | quote }} + - name: IDC_INSTANCE_ARN + value: {{ .Values.securityAgent.idcInstanceArn | quote }} + - name: SECRET_NAME + value: {{ .Values.securityAgent.secretName | quote }} + - name: SECRET_NAMESPACE + value: {{ .Values.argo.namespace | quote }} + volumeMounts: + - name: scripts + mountPath: /scripts +{{- end }} diff --git a/gitops/addons/charts/dark-factory/templates/10-rbac.yaml b/gitops/addons/charts/dark-factory/templates/10-rbac.yaml new file mode 100644 index 00000000..5d0157bd --- /dev/null +++ b/gitops/addons/charts/dark-factory/templates/10-rbac.yaml @@ -0,0 +1,178 @@ +{{- /* +RBAC for the Dark Factory (Flow B) Argo workflows. + +Argo runs workflow pods in the ARGO namespace, so the workflow ServiceAccount +lives there. It manages SandboxClaims + reads sandboxes/pods in the +agent-sandbox namespace (where the warm pool + coder pods live) via a Role +bound cross-namespace. It deliberately has NO secrets access, no pod/exec, no +cluster scope β€” the coder VM is credential-less and the workflow never shells +into it. +*/ -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: dark-factory-workflow + namespace: {{ .Values.argo.namespace }} + labels: + {{- include "dark-factory.labels" . | nindent 4 }} +{{- if .Values.securityAgent.enabled }} + annotations: + # IRSA: the df-run security-agent step assumes this role to call the AWS + # Security Agent API + stage diffs in S3. Role + trust are committed Terraform + # (iam/securityagent.tf); the chart only consumes the ARN. The role's trust + # policy names this exact SA (workflow_service_accounts). + eks.amazonaws.com/role-arn: {{ .Values.securityAgent.irsaRoleArn | quote }} +{{- end }} +--- +# In the ARGO namespace: the Argo executor needs to write its task results. +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: dark-factory-workflow + namespace: {{ .Values.argo.namespace }} + labels: + {{- include "dark-factory.labels" . | nindent 4 }} +rules: + - apiGroups: ["argoproj.io"] + resources: ["workflowtaskresults", "workflowtasksets"] + verbs: ["create", "get", "list", "watch", "patch"] + - apiGroups: [""] + resources: ["pods", "pods/log"] + verbs: ["get", "list", "watch"] + # df-iterate (runs as this SA) submits a df-run Workflow via the k8s API to + # route a PR comment back to the coder as a bounded revision. + - apiGroups: ["argoproj.io"] + resources: ["workflows"] + verbs: ["create", "get", "list"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: dark-factory-workflow + namespace: {{ .Values.argo.namespace }} + labels: + {{- include "dark-factory.labels" . | nindent 4 }} +subjects: + - kind: ServiceAccount + name: dark-factory-workflow + namespace: {{ .Values.argo.namespace }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: dark-factory-workflow +--- +# In the AGENT-SANDBOX namespace: manage claims + read the bound sandbox/pods + +# per-issue state ConfigMaps. Bound to the SA that lives in the argo namespace. +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: dark-factory-workflow-sandbox + namespace: {{ .Values.warmPool.namespace }} + labels: + {{- include "dark-factory.labels" . | nindent 4 }} +rules: + - apiGroups: ["extensions.agents.x-k8s.io"] + resources: ["sandboxclaims"] + verbs: ["get", "list", "watch", "create", "delete"] + - apiGroups: ["agents.x-k8s.io"] + resources: ["sandboxes"] + # patch: Flow D suspend/resume flips Sandbox.spec.operatingMode from the workflow. + verbs: ["get", "list", "watch", "patch"] + - apiGroups: [""] + resources: ["pods"] + verbs: ["get", "list", "watch"] + - apiGroups: [""] + resources: ["configmaps"] + verbs: ["get", "list", "create", "update", "patch"] +{{- if .Values.microvm.enabled }} + # Flow D (df-run-lambda): provision-microvm creates the Microvm CR + its runHookPayload + # Secret directly (no SandboxClaim); set-microvm-power reads the CR for the id; + # df-merge-teardown deletes the CR at merge. Least-privilege, this namespace only. + - apiGroups: ["lambdamicrovms.services.k8s.aws"] + resources: ["microvms"] + verbs: ["get", "list", "watch", "create", "delete"] + - apiGroups: [""] + resources: ["secrets"] + verbs: ["get", "create", "update", "patch", "delete"] + # Read the platform image handoff (imageARN + execRoleARN) built once by KRO/ACK. + - apiGroups: ["kro.run"] + resources: ["microvmsandboxes"] + verbs: ["get", "list", "watch"] +{{- end }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: dark-factory-workflow-sandbox + namespace: {{ .Values.warmPool.namespace }} + labels: + {{- include "dark-factory.labels" . | nindent 4 }} +subjects: + - kind: ServiceAccount + name: dark-factory-workflow + namespace: {{ .Values.argo.namespace }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: dark-factory-workflow-sandbox +{{- if .Values.microvm.enabled }} +--- +# Flow D: bind the workflow SA to the lambda-microvms role via EKS Pod Identity, so the +# df-run-lambda steps (provision/power/terminate) can call aws lambda-microvms +# (get/suspend/resume/terminate-microvm + create-microvm-auth-token). Reuses the SAME +# role the bridge/lifecycle controllers already use (user-approved) β€” additive, +# no IAM policy change. The ACK eks controller reconciles this into a real EKS association. +apiVersion: eks.services.k8s.aws/v1alpha1 +kind: PodIdentityAssociation +metadata: + name: {{ .Values.microvm.podIdentity.clusterName }}-dark-factory-workflow + namespace: {{ .Values.warmPool.namespace }} + labels: + {{- include "dark-factory.labels" . | nindent 4 }} + annotations: + argocd.argoproj.io/sync-wave: "-2" +spec: + clusterName: {{ .Values.microvm.podIdentity.clusterName }} + namespace: {{ .Values.argo.namespace }} + serviceAccount: {{ .Values.microvm.workflowServiceAccount | default "dark-factory-workflow" }} + roleARN: "arn:aws:iam::{{ .Values.microvm.podIdentity.accountId }}:role/{{ .Values.microvm.podIdentity.clusterName }}-ack-lambdamicrovms-controller" +{{- end }} +{{- if .Values.deployTest.enabled }} +--- +# P4 deploy-test: the ONLY cluster-scoped grant. Lets the workflow create/delete +# ephemeral test namespaces and deploy the PR's manifests into them. Scoped to +# namespaces + the common workload/config kinds β€” no secrets, no RBAC, no +# cluster-wide resource mutation. This is deliberately the widest permission in +# the chart and is confined to deploy-test. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: dark-factory-deploy-test + labels: + {{- include "dark-factory.labels" . | nindent 4 }} +rules: + - apiGroups: [""] + resources: ["namespaces"] + verbs: ["create", "get", "list", "delete", "patch"] + - apiGroups: [""] + resources: ["pods", "pods/log", "services", "configmaps"] + verbs: ["create", "get", "list", "watch", "delete"] + - apiGroups: ["apps"] + resources: ["deployments", "replicasets", "statefulsets", "daemonsets"] + verbs: ["create", "get", "list", "watch", "delete", "patch"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: dark-factory-deploy-test + labels: + {{- include "dark-factory.labels" . | nindent 4 }} +subjects: + - kind: ServiceAccount + name: dark-factory-workflow + namespace: {{ .Values.argo.namespace }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: dark-factory-deploy-test +{{- end }} diff --git a/gitops/addons/charts/dark-factory/templates/20-workflowtemplate-df-run.yaml b/gitops/addons/charts/dark-factory/templates/20-workflowtemplate-df-run.yaml new file mode 100644 index 00000000..038a81d9 --- /dev/null +++ b/gitops/addons/charts/dark-factory/templates/20-workflowtemplate-df-run.yaml @@ -0,0 +1,821 @@ +{{- /* +df-run β€” the Flow B P1 pipeline as an Argo WorkflowTemplate. + + trigger β†’ claim warm sandbox β†’ coder implements+tests (in the Kata VM) β†’ + coder opens PR + posts a live sticky status β†’ await human approval. + +Single-cluster on the hub: the claim step creates a SandboxClaim with the issue +injected as env (verified contract: envVarsInjectionPolicy=Allowed), waits for +the operator to bind a warm micro-VM (status Ready), then the coder VM β€” which +boots the coder image from the SandboxTemplate β€” reads the env, implements on +df/issue-N, builds+tests, pushes, and opens the PR + sticky comment itself using +the short-TTL GitHub token projected into the VM. The workflow watches the coder +pod to completion, then its onExit handler releases the claim (the operator +refills the pool). P1 stops at "PR open, awaiting human" β€” merge/teardown + +verification gates are P2-P4. + +Parameters (supplied by the trigger / Sensor): + issue-id, issue-number, repo (owner/name), issue-title, issue-body, base-branch +*/ -}} +apiVersion: argoproj.io/v1alpha1 +kind: WorkflowTemplate +metadata: + name: df-run + namespace: {{ .Values.argo.namespace }} + labels: + {{- include "dark-factory.labels" . | nindent 4 }} +spec: + serviceAccountName: dark-factory-workflow + # Serialize per issue (no double-claim for the same issue) and cap total + # concurrent runs against the kata pool size. + synchronization: + mutex: + name: "df-issue-{{`{{workflow.parameters.issue-id}}`}}" + entrypoint: main + arguments: + parameters: + - name: issue-id + - name: issue-number + - name: repo + - name: issue-title + - name: issue-body + value: "" + - name: base-branch + value: main + # Optional revision request (df-iterate passes the PR comment here; empty on + # a first pass). Injected into the coder as DF_ITERATE_NOTE so it revises the + # existing branch to address the feedback. + - name: iterate-note + value: "" + # Base64 of the revision note β€” used when the note is arbitrary markdown + # (auto-fed agent findings) that can't be injected raw into the claim YAML. + # The coder decodes this first, falling back to the plain iterate-note. + - name: iterate-note-b64 + value: "" + # Which label fired the run selects the coder substrate (Kata default vs + # Lambda MicroVM for darkfactory-lambda). MUST be declared with a default so + # every trigger-label reference resolves even when the submitter (sensor / + # manual) does not pass it. Defaulting to dark-factory keeps the Kata path for + # sensor submits that do not map a label. + - name: trigger-label + value: "dark-factory" + # Always release the claimed sandbox, on success OR failure. + onExit: teardown + ttlStrategy: + secondsAfterCompletion: {{ .Values.argo.workflowTtlSecondsAfterCompletion | default 604800 }} +{{- if .Values.metrics.enabled }} + # Success metrics (Argo-native Prometheus). Argo exposes these on the workflow + # controller's :9090/metrics β€” scraped into the platform Prometheus. Gives the + # GitOps-native view of the factory: throughput, outcome mix, and lead time. + metrics: + prometheus: + - name: df_runs_total + help: "Dark Factory df-run workflows by status" + labels: + - { key: status, value: "{{`{{workflow.status}}`}}" } + counter: + value: "1" + - name: df_run_duration_seconds + help: "df-run wall-clock duration (lead time proxy)" + gauge: + value: "{{`{{workflow.duration}}`}}" +{{- end }} + templates: + + # ---- DAG ---- + # claim β†’ coder β†’ [ verify fan: holdout-gate βˆ₯ security-review βˆ₯ devops-review ] + # β†’ status. Every verify step runs OUTSIDE the coder (trusted hub pod), is gated + # on a PR existing, and is advisory in v1 (posts a commit status; never fails the + # run unless its *.blocking / blockSeverity is raised). `status` waits for all + # enabled verify steps via its dependencies list. + - name: main + dag: + tasks: + - name: claim + template: claim-sandbox + - name: drive-coder + template: await-coder + dependencies: [claim] + arguments: + parameters: + - name: sandbox + value: "{{`{{tasks.claim.outputs.parameters.sandbox}}`}}" +{{- if .Values.holdout.enabled }} + # P2 β€” holdout gate: hidden scenarios + a different-family judge. + - name: holdout-gate + template: holdout-gate + dependencies: [drive-coder] + when: "{{`{{tasks.drive-coder.outputs.parameters.pr-number}}`}} != \"\"" + arguments: + parameters: + - name: pr-number + value: "{{`{{tasks.drive-coder.outputs.parameters.pr-number}}`}}" +{{- end }} +{{- if .Values.review.enabled }} + # P3 β€” REAL AWS Frontier Agents, ordered: DevOps FIRST, then Security. + # + # DevOps Agent (release readiness) runs via the Claude Code plugin INSIDE + # the coder step (no headless API), so there is no separate DAG task for + # it here β€” its verdict is reported by the coder and, on a clear verdict, + # the coder applies the `needs-security-review` label. This `devops-gate` + # step confirms the label is present (DevOps cleared) before Security runs. + - name: devops-gate + template: devops-gate + dependencies: [drive-coder] + when: "{{`{{tasks.drive-coder.outputs.parameters.pr-number}}`}} != \"\"" + arguments: + parameters: + - name: pr-number + value: "{{`{{tasks.drive-coder.outputs.parameters.pr-number}}`}}" + # Security Agent β€” WAITS FOR the real aws-security-agent[bot]'s own review + # and mirrors its verdict into dark-factory/security. The App bot reviews + # autonomously + IN PARALLEL with the DevOps bot, so this runs alongside + # devops-gate (depends on drive-coder, NOT gated behind DevOps clearing β€” + # otherwise a slow DevOps review would skip the security signal entirely). + - name: security-agent + template: security-agent + dependencies: [drive-coder] + when: "{{`{{tasks.drive-coder.outputs.parameters.pr-number}}`}} != \"\"" +{{- end }} +{{- if .Values.deployTest.enabled }} + # P4 β€” detect whether the PR touches deployable artifacts (cheap grep of + # the diff). Its output gates the (expensive, K8s-touching) deploy-test. + - name: detect-deployable + template: detect-deployable + dependencies: [drive-coder] + when: "{{`{{tasks.drive-coder.outputs.parameters.pr-number}}`}} != \"\"" + # P4 β€” deploy-test: THE ONLY step that holds K8s access. Content-aware: + # k8s β†’ ephemeral-namespace apply+probe+teardown; terraform β†’ init+validate. + # Runs only when detect classified the change (kind != none). + - name: deploy-test + template: deploy-test + dependencies: [detect-deployable] + when: "{{`{{tasks.detect-deployable.outputs.parameters.kind}}`}} != none" + arguments: + parameters: + - name: kind + value: "{{`{{tasks.detect-deployable.outputs.parameters.kind}}`}}" +{{- end }} + - name: status + template: sticky-status + # Depend on BOTH agents (devops-gate + security-agent) plus holdout + + # deploy-test so the consolidated verdict is only computed once EVERY + # signal is terminal β€” required so a ❌ (and the auto-fix decision) is made + # on complete input, and so a clear verdict truly means both agents cleared. + dependencies: + {{- if or .Values.holdout.enabled .Values.review.enabled .Values.deployTest.enabled }} + {{- if .Values.holdout.enabled }} + - holdout-gate + {{- end }} + {{- if .Values.review.enabled }} + - devops-gate + - security-agent + {{- end }} + {{- if .Values.deployTest.enabled }} + - deploy-test + {{- end }} + {{- else }} + - drive-coder + {{- end }} + arguments: + parameters: + - name: phase + value: "pr-open" + + # ---- 1. Claim a warm sandbox (creates the SandboxClaim with issue env) ---- + - name: claim-sandbox + outputs: + parameters: + - name: sandbox + valueFrom: + jsonPath: "{.status.sandbox.name}" + resource: + action: create + setOwnerReference: false + successCondition: status.conditions.0.status == True + failureCondition: status.conditions.0.reason == EnvVarsInjectionRejected + manifest: | + apiVersion: extensions.agents.x-k8s.io/v1beta1 + kind: SandboxClaim + metadata: + name: df-issue-{{`{{workflow.parameters.issue-id}}`}} + namespace: {{ .Values.warmPool.namespace }} + labels: + dark-factory.io/issue: "{{`{{workflow.parameters.issue-id}}`}}" + # issue NUMBER too: df-merge-teardown / df-iterate are fired by PR + # events whose payload carries the branch (df/issue-) but NOT + # the issue id, so they select the claim to reap by this label. + dark-factory.io/issue-number: "{{`{{workflow.parameters.issue-number}}`}}" + dark-factory.io/managed-by: df-run + spec: + warmPoolRef: + name: {{ .Values.warmPool.name }} + lifecycle: + ttlSecondsAfterFinished: {{ .Values.claimTtlSeconds }} + env: + - { containerName: coder, name: DF_ISSUE_NUMBER, value: "{{`{{workflow.parameters.issue-number}}`}}" } + - { containerName: coder, name: DF_REPO, value: "{{`{{workflow.parameters.repo}}`}}" } + - { containerName: coder, name: DF_BRANCH, value: "df/issue-{{`{{workflow.parameters.issue-number}}`}}" } + - { containerName: coder, name: DF_BASE_BRANCH, value: "{{`{{workflow.parameters.base-branch}}`}}" } + - { containerName: coder, name: DF_ISSUE_TITLE, value: "{{`{{workflow.parameters.issue-title}}`}}" } + # Engine the coder VM runs: claude (default) | kiro. entrypoint.js + # branches on this (=="kiro" β†’ kiro headless, else claude -p). + - { containerName: coder, name: CODER_ENGINE, value: "{{ .Values.coder.engine }}" } + # Back-compat alias (older coder images read CODER_PROFILE). + - { containerName: coder, name: CODER_PROFILE, value: "{{ .Values.coder.engine }}" } + - { containerName: coder, name: BIFROST_URL, value: "{{ .Values.bifrost.url }}" } + # Revision note: prefer the base64 form (safe for arbitrary markdown β€” + # auto-fed agent findings contain newlines/quotes/braces that broke the + # raw YAML injection). Both are single-line values here; the coder + # decodes B64 first, else uses the plain one. + - { containerName: coder, name: DF_ITERATE_NOTE, value: "{{`{{workflow.parameters.iterate-note}}`}}" } + - { containerName: coder, name: DF_ITERATE_NOTE_B64, value: "{{`{{workflow.parameters.iterate-note-b64}}`}}" } +{{- if and .Values.devopsAgent.enabled (eq .Values.devopsAgent.gate "label") }} + # DevOps Agent via the coding-agent plugin, run inside the coder + # step BEFORE the PR opens β€” ONLY in label-gate mode. In the default + # check-gate mode the DevOps Agent GitHub App reviews the PR instead, + # so the coder does not attempt the (VM-incompatible) plugin path. + - { containerName: coder, name: DF_DEVOPS_AGENT_MODE, value: "claude-plugin" } + - { containerName: coder, name: DF_DEVOPS_CLEAR_LABEL, value: "{{ .Values.review.handoffLabel }}" } +{{- end }} + + # ---- 2. Wait for the coder VM to finish β€” GitHub is the completion bus ---- + # The coder image (baked into the SandboxTemplate) auto-runs on VM start, + # reads DF_* env, implements + tests, pushes df/issue-N, and opens the PR + # itself using the short-TTL gh-token projected into the VM. The coder is + # credential-less to the k8s API (no SA token), so it CANNOT signal via a pod + # annotation β€” it self-reports through GitHub. This step polls the GitHub API + # (with the workflow's own token) for a PR whose head is df/issue-N, and + # treats the head commit's dark-factory/implementation check as the verdict. + - name: await-coder + inputs: + parameters: + - name: sandbox + activeDeadlineSeconds: {{ mul .Values.coder.runTimeoutMinutes 60 }} + script: + image: {{ .Values.stepImage }} + command: [sh] + env: + - name: GH_TOKEN + valueFrom: + secretKeyRef: + name: {{ .Values.github.tokenSecret }} + key: {{ .Values.github.tokenKey }} + source: | + set -eu + command -v curl >/dev/null 2>&1 || apk add --no-cache curl jq >/dev/null 2>&1 + REPO="{{`{{workflow.parameters.repo}}`}}" + BRANCH="df/issue-{{`{{workflow.parameters.issue-number}}`}}" + ITERATE_NOTE="{{`{{workflow.parameters.iterate-note}}`}}" + ITERATE_NOTE_B64="{{`{{workflow.parameters.iterate-note-b64}}`}}" + API="https://api.github.com/repos/${REPO}" + # ROUND-AWARENESS: on a FIX ROUND (iterate-note set) the branch already + # exists and its OLD commit already has dark-factory/implementation=success. + # If we accept that, the verify fan (holdout/security/devops) runs against + # the STALE pre-fix commit before the coder's new push lands (observed 45s + # race). So record the branch's starting SHA and require a DIFFERENT head + # SHA (a genuinely new commit) before we consider the coder done on a fix + # round. First-pass runs (no iterate-note) have no prior commit β†’ accept the + # first commit that reports impl=success. + START_SHA="$(curl -fsS -H "Authorization: Bearer ${GH_TOKEN}" -H "Accept: application/vnd.github+json" \ + "${API}/branches/${BRANCH}" 2>/dev/null | jq -r '.commit.sha // empty' || echo "")" + IS_FIX="false"; { [ -n "${ITERATE_NOTE}" ] || [ -n "${ITERATE_NOTE_B64}" ]; } && IS_FIX="true" + echo "[df-run] polling GitHub for PR head=${BRANCH} (fix-round=${IS_FIX}, start-sha=${START_SHA:-none})..." + while true; do + pr="$(curl -fsS -H "Authorization: Bearer ${GH_TOKEN}" \ + -H "Accept: application/vnd.github+json" \ + "${API}/pulls?head=${REPO%%/*}:${BRANCH}&state=open" 2>/dev/null || echo '[]')" + n="$(echo "${pr}" | jq -r '.[0].number // empty')" + if [ -n "${n}" ]; then + sha="$(echo "${pr}" | jq -r '.[0].head.sha')" + # On a fix round, ignore the old commit until the coder pushes a new one. + if [ "${IS_FIX}" = "true" ] && [ -n "${START_SHA}" ] && [ "${sha}" = "${START_SHA}" ]; then + echo "[df-run] fix-round: head still at start sha ${sha} β€” waiting for the coder's new commit..." + sleep 15; continue + fi + st="$(curl -fsS -H "Authorization: Bearer ${GH_TOKEN}" -H "Accept: application/vnd.github+json" \ + "${API}/commits/${sha}/status" 2>/dev/null | jq -r '.statuses[] | select(.context=="dark-factory/implementation") | .state' | head -1)" + echo "[df-run] PR #${n} open (sha=${sha}) impl-status=${st:-pending}" + case "${st}" in + success) echo "[df-run] coder finished β€” PR #${n} @ ${sha} ready."; echo "${n}" > /tmp/pr; echo "${sha}" > /tmp/sha; exit 0 ;; + failure|error) echo "[df-run] coder reported failure."; exit 1 ;; + esac + else + echo "[df-run] no PR yet β€” coder still working..." + fi + sleep 15 + done + outputs: + parameters: + - name: pr-number + valueFrom: + path: /tmp/pr + default: "" + - name: head-sha + valueFrom: + path: /tmp/sha + default: "" + +{{- if .Values.holdout.enabled }} + # ---- 2b. Holdout gate (P2) β€” train/test separation for code ---- + # Runs on the HUB (a trusted step, NOT the Kata VM). Checks out the coder's + # PR branch, diffs it vs base, then runs evaluate.js: each hidden scenario + # gets its executable test run against the built code AND a different-family + # judge (Nova) vote (2-of-3). Gate = passRatio >= threshold. The hidden + # scenarios come from the df-holdout-* ConfigMaps mounted here β€” they are + # NEVER mounted into the sandbox, so the coder cannot see or edit them. + # v1 is advisory: the gate result is posted as the dark-factory/holdout commit + # status; the workflow only fails on a red gate when holdout.blocking=true. + - name: holdout-gate + inputs: + parameters: + - name: pr-number + activeDeadlineSeconds: 900 + volumes: + - name: holdout-eval + configMap: + name: df-holdout-eval + {{- range $repo := .Values.trigger.argoEvents.repositories }} + {{- range $name := $repo.names }} + - name: holdout-{{ printf "%s-%s" $repo.owner $name | lower }} + configMap: + name: df-holdout-{{ printf "%s-%s" $repo.owner $name | lower }} + {{- end }} + {{- end }} + script: + image: {{ .Values.holdout.evalImage }} + command: [bash] + env: + - name: GH_TOKEN + valueFrom: + secretKeyRef: + name: {{ .Values.github.tokenSecret }} + key: {{ .Values.github.tokenKey }} + - name: BIFROST_URL + value: {{ .Values.holdout.bifrostUrl | quote }} + - name: JUDGE_MODEL + value: {{ .Values.holdout.judgeModel | quote }} + - name: JUDGE_RUNS + value: {{ .Values.holdout.judgeRuns | quote }} + - name: JUDGE_QUORUM + value: {{ .Values.holdout.judgeQuorum | quote }} + - name: THRESHOLD + value: {{ .Values.holdout.threshold | quote }} + volumeMounts: + - name: holdout-eval + mountPath: /holdout/evaluate.js + subPath: evaluate.js + {{- range $repo := .Values.trigger.argoEvents.repositories }} + {{- range $name := $repo.names }} + - name: holdout-{{ printf "%s-%s" $repo.owner $name | lower }} + mountPath: /holdout/{{ printf "%s-%s" $repo.owner $name }} + {{- end }} + {{- end }} + source: | + set -eu + REPO="{{`{{workflow.parameters.repo}}`}}" + BRANCH="df/issue-{{`{{workflow.parameters.issue-number}}`}}" + BASE="{{`{{workflow.parameters.base-branch}}`}}" + SLUG="$(echo "${REPO}" | tr '/' '-')" + API="https://api.github.com/repos/${REPO}" + WORK=/tmp/holdout-work + rm -rf "${WORK}"; mkdir -p "${WORK}" + echo "[holdout] cloning ${REPO}@${BRANCH} for evaluation..." + git clone --quiet --branch "${BRANCH}" \ + "https://x-access-token:${GH_TOKEN}@github.com/${REPO}.git" "${WORK}/repo" + cd "${WORK}/repo" + # Diff vs base β€” use the GitHub COMPARE API (authoritative changed-file + # patch), NOT a local git diff. A branch clone shares no reliable merge-base + # with a shallow base fetch, so `origin/BASE...HEAD` yields the WHOLE file as + # "added" (observed: a sum-only PR's diff contained every pre-existing + # function β†’ every scenario's appliesWhen matched β†’ wrong scenarios graded β†’ + # false holdout failure). The compare API returns only the real per-file + # patch hunks, so appliesWhen keys on the ACTUAL change. (Same fix as + # detect-deployable.) Concatenate every file's patch into /tmp/diff.patch. + PR_NUM="{{`{{inputs.parameters.pr-number}}`}}" + curl -fsS -H "Authorization: Bearer ${GH_TOKEN}" -H "Accept: application/vnd.github+json" \ + "${API}/pulls/${PR_NUM}/files?per_page=100" 2>/dev/null \ + | python3 -c 'import json,sys; d=json.load(sys.stdin); files=d if isinstance(d,list) else []; open("/tmp/diff.patch","w").write("\n".join("diff --git a/%s b/%s\n%s"%(f.get("filename",""),f.get("filename",""),f.get("patch","")) for f in files))' 2>/dev/null || echo "" > /tmp/diff.patch + # Fallback: if the API path produced nothing, fall back to a local diff. + [ -s /tmp/diff.patch ] || { git fetch --quiet --depth 1 origin "${BASE}" 2>/dev/null || true; git diff "origin/${BASE}...HEAD" > /tmp/diff.patch 2>/dev/null || echo "" > /tmp/diff.patch; } + echo "[holdout] diff.patch $(wc -l < /tmp/diff.patch) lines (via compare API)" + # Build/install so executable tests can require the module. + if [ -f package.json ]; then npm install --no-audit --no-fund >/dev/null 2>&1 || true; fi + SHA="$(git rev-parse HEAD)" + + SCEN="/holdout/${SLUG}/scenarios.json" + if [ ! -f "${SCEN}" ]; then + echo "[holdout] no hidden scenarios for ${SLUG} β€” skipping gate (advisory)." + exit 0 + fi + + echo "[holdout] evaluating against hidden scenarios..." + set +e + REPO_DIR="${WORK}/repo" DIFF=/tmp/diff.patch SCENARIOS="${SCEN}" \ + OUT=/tmp/holdout-result.json node /holdout/evaluate.js + GATE=$? + set -e + + RATIO="$(node -e 'const r=require("/tmp/holdout-result.json");console.log(Math.round(r.ratio*100))' 2>/dev/null || echo 0)" + PASSED="$(node -e 'const r=require("/tmp/holdout-result.json");console.log(r.passed+"/"+r.total)' 2>/dev/null || echo '?/?')" + TOTAL="$(node -e 'const r=require("/tmp/holdout-result.json");console.log(r.total)' 2>/dev/null || echo 0)" + SKIPPED="$(node -e 'const r=require("/tmp/holdout-result.json");console.log(r.skipped||0)' 2>/dev/null || echo 0)" + # Distinguish the three outcomes so the PR reads honestly: + # total=0 β†’ no hidden scenario matched this change (e.g. a Terraform PR + # vs JS-only scenarios) β†’ NOT APPLICABLE, green as n/a. + # gate ok β†’ passed. gate fail β†’ below threshold. + if [ "${TOTAL}" = "0" ]; then + STATE=success; DESC="not applicable β€” no hidden scenarios match this change (${SKIPPED} skipped)" + elif [ "${GATE}" -eq 0 ]; then + STATE=success; DESC="holdout ${PASSED} (${RATIO}%) β€” gate passed" + else + STATE=failure; DESC="holdout ${PASSED} (${RATIO}%) β€” below threshold" + fi + + # Post the holdout verdict as a commit status on the PR head SHA. The + # coder image has no curl, so use node's https (always present). Retry + # transient failures so a blip doesn't drop the verdict. + GH_TOKEN="${GH_TOKEN}" REPO="${REPO}" SHA="${SHA}" STATE="${STATE}" DESC="${DESC}" node -e ' + const https=require("https"); + const body=JSON.stringify({state:process.env.STATE,context:"dark-factory/holdout",description:process.env.DESC}); + let n=0; + (function post(){ + const req=https.request({host:"api.github.com",method:"POST",path:"/repos/"+process.env.REPO+"/statuses/"+process.env.SHA, + headers:{"User-Agent":"dark-factory-holdout","Authorization":"Bearer "+process.env.GH_TOKEN,"Accept":"application/vnd.github+json","Content-Type":"application/json","Content-Length":Buffer.byteLength(body)}}, + r=>{let b="";r.on("data",c=>b+=c);r.on("end",()=>{if(r.statusCode>=300&&r.statusCode<500){console.error("status post "+r.statusCode+": "+b.slice(0,120))}else if(r.statusCode>=500&&++n<4){setTimeout(post,500*n);return} console.log("[holdout] posted dark-factory/holdout="+process.env.STATE)});}); + req.on("error",e=>{if(++n<4){setTimeout(post,500*n)}else{console.error("status post error: "+e.message)}}); + req.write(body);req.end(); + })(); + ' || true + + {{- if .Values.holdout.blocking }} + echo "[holdout] blocking mode β€” gate result gates the workflow." + exit ${GATE} + {{- else }} + echo "[holdout] advisory mode β€” gate result reported, workflow continues." + exit 0 + {{- end }} + outputs: + parameters: + - name: ratio + valueFrom: + path: /tmp/holdout-result.json + default: "{}" +{{- end }} + +{{- if .Values.review.enabled }} + # ---- 2c. devops-gate (P3, step 1) β€” wait for the AWS DevOps Agent to clear ---- + # The AWS DevOps Agent release-readiness review is its own GitHub App that + # auto-reviews every PR and posts a check-run / commit status (its native model + # β€” there is NO headless code-review API). This step polls the PR head for that + # check and emits cleared=true|false, which gates the Security Agent step so + # DevOps reviews FIRST, Security SECOND (docs Β§6.2). + # + # Mode (devopsAgent.gate): + # check β†’ wait for the DevOps Agent's own check-run (context matches + # devopsAgent.checkContext) to conclude success/neutral. This is the + # real GitHub-App path (needs the one-time console repo connect). + # label β†’ fallback: wait for the coder-applied handoffLabel (used only if + # you drive DevOps via the coding-agent plugin instead of the App). + # If DevOps never reports within the window, cleared=false β†’ Security is skipped + # and the sticky status shows DevOps not-run (NEVER a fake pass). Advisory: this + # step itself never fails the run. + - name: devops-gate + inputs: + parameters: + - name: pr-number + activeDeadlineSeconds: {{ add .Values.devopsAgent.waitSeconds 60 }} + volumes: + - name: review-script + configMap: + name: df-review + script: + image: {{ .Values.reviewImage }} + command: [bash] + env: + - name: GH_TOKEN + valueFrom: + secretKeyRef: + name: {{ .Values.github.tokenSecret }} + key: {{ .Values.github.tokenKey }} + - name: REPO + value: "{{`{{workflow.parameters.repo}}`}}" + - name: PR + value: "{{`{{inputs.parameters.pr-number}}`}}" + - name: BRANCH + value: "df/issue-{{`{{workflow.parameters.issue-number}}`}}" + - name: GATE_MODE + value: {{ .Values.devopsAgent.gate | quote }} + - name: LABEL + value: {{ .Values.review.handoffLabel | quote }} + - name: CHECK_CONTEXT + value: {{ .Values.devopsAgent.checkContext | quote }} + - name: WAIT_SECONDS + value: {{ .Values.devopsAgent.waitSeconds | quote }} + volumeMounts: + - name: review-script + mountPath: /scripts/comment.js + subPath: comment.js + source: | + set -eu + echo "[devops-gate] mode=${GATE_MODE} PR #${PR} β€” waiting for AWS DevOps Agent..." + DEADLINE=$(( $(date +%s) + WAIT_SECONDS )) + CLEARED=false + while [ "$(date +%s)" -lt "$DEADLINE" ]; do + if [ "$GATE_MODE" = "label" ]; then + # Fallback: coder-applied handoff label present on the PR. + R="$(GH_TOKEN="$GH_TOKEN" REPO="$REPO" PR="$PR" LABEL="$LABEL" node -e ' + const https=require("https");const {GH_TOKEN,REPO,PR,LABEL}=process.env; + https.get({host:"api.github.com",path:"/repos/"+REPO+"/issues/"+PR+"/labels",headers:{"User-Agent":"df-devops-gate","Authorization":"Bearer "+GH_TOKEN,"Accept":"application/vnd.github+json"}}, + r=>{let b="";r.on("data",c=>b+=c);r.on("end",()=>{try{const ls=JSON.parse(b).map(x=>x.name);console.log(ls.includes(LABEL)?"cleared":"waiting")}catch(e){console.log("waiting")}})}).on("error",()=>console.log("waiting"));' 2>/dev/null || echo waiting)" + else + # Default: the DevOps Agent GitHub App's check-run on the PR head SHA. + # cleared iff a matching context concluded success/neutral; "blocked" + # if it concluded failure (BLOCK) β€” we stop and leave cleared=false. + R="$(GH_TOKEN="$GH_TOKEN" REPO="$REPO" BRANCH="$BRANCH" CHECK_CONTEXT="$CHECK_CONTEXT" node -e ' + const https=require("https");const {GH_TOKEN,REPO,BRANCH,CHECK_CONTEXT}=process.env; + const g=(p)=>new Promise((res)=>{https.get({host:"api.github.com",path:p,headers:{"User-Agent":"df-devops-gate","Authorization":"Bearer "+GH_TOKEN,"Accept":"application/vnd.github+json"}},r=>{let b="";r.on("data",c=>b+=c);r.on("end",()=>{try{res(JSON.parse(b))}catch(e){res(null)}})}).on("error",()=>res(null))}); + (async()=>{ + const pr=await g("/repos/"+REPO+"/pulls?head="+REPO.split("/")[0]+":"+BRANCH+"&state=open"); + if(!pr||!pr[0]){return console.log("waiting")} + const sha=pr[0].head.sha; + const re=new RegExp(CHECK_CONTEXT,"i"); + // check-runs API + const cr=await g("/repos/"+REPO+"/commits/"+sha+"/check-runs"); + const runs=(cr&&cr.check_runs)||[]; + const m=runs.filter(x=>re.test(x.name)); + if(m.some(x=>x.status==="completed"&&["success","neutral"].includes(x.conclusion)))return console.log("cleared"); + if(m.some(x=>x.status==="completed"&&["failure","action_required","cancelled","timed_out"].includes(x.conclusion)))return console.log("blocked"); + // legacy commit statuses fallback + const st=await g("/repos/"+REPO+"/commits/"+sha+"/status"); + const ss=((st&&st.statuses)||[]).filter(x=>re.test(x.context)); + if(ss.some(x=>x.state==="success"))return console.log("cleared"); + if(ss.some(x=>["failure","error"].includes(x.state)))return console.log("blocked"); + console.log("waiting"); + })();' 2>/dev/null || echo waiting)" + fi + case "$R" in + cleared) CLEARED=true; echo "[devops-gate] AWS DevOps Agent CLEARED"; break ;; + blocked) CLEARED=false; echo "[devops-gate] AWS DevOps Agent returned BLOCK β€” Security stays gated"; break ;; + *) echo "[devops-gate] waiting for AWS DevOps Agent review..."; sleep 20 ;; + esac + done + echo "$CLEARED" > /tmp/cleared + echo "[devops-gate] cleared=${CLEARED}" + + # NOTE: the DevOps verdict is NO LONGER relayed as its own PR comment. + # To avoid mixed/duplicate signals, the pipeline posts a SINGLE + # consolidated review (status.js β†’ dark-factory:verdict-review) that + # mirrors the source-of-truth checks (Build/Holdout/Security/DevOps). + # The DevOps Agent's own commit status + the App bot's review remain + # the authoritative DevOps signal; `cleared` still gates the flow below. + outputs: + parameters: + - name: cleared + valueFrom: + path: /tmp/cleared + default: "false" + + # ---- 2d. security-agent (P3, step 2) β€” REAL AWS Security Agent, headless ---- + # Runs SECOND, only after DevOps cleared (label present). A trusted hub-side + # step that clones df/issue-N read-only, stages {source archive, unified diff} + # SINGLE SECURITY SIGNAL = THE REAL AWS SECURITY AGENT BOT. + # We do NOT run a second headless scan (that redundant path disagreed with the + # bot β€” reported "no findings" while aws-security-agent[bot] flagged real issues + # β€” and produced a false LGTM). Instead this step WAITS for the GitHub App bot's + # own review on the PR and MIRRORS its verdict into the dark-factory/security + # commit status (findings -> failure -> merge blocked; clean -> success). Because + # the consolidated `status` step depends on this step, the pipeline now naturally + # waits for the real bot before posting its verdict. Node-only β†’ runs on reviewImage. + - name: security-agent + activeDeadlineSeconds: {{ add .Values.securityAgent.pollTimeoutSeconds 300 }} + volumes: + - name: review-script + configMap: + name: df-review + script: + image: {{ .Values.reviewImage }} + command: [node] + env: + - name: GH_TOKEN + valueFrom: + secretKeyRef: + name: {{ .Values.github.tokenSecret }} + key: {{ .Values.github.tokenKey }} + - name: REPO + value: "{{`{{workflow.parameters.repo}}`}}" + - name: BRANCH + value: "df/issue-{{`{{workflow.parameters.issue-number}}`}}" + # Findings at/above this severity fail the status (block the merge). The bot + # doesn't expose per-severity counts uniformly, so ANY finding fails unless + # BLOCK_LEVEL=none (then findings are advisory). Default medium. + - name: BLOCK_LEVEL + value: {{ .Values.securityAgent.blockLevel | quote }} + - name: POLL_TIMEOUT + value: {{ .Values.securityAgent.pollTimeoutSeconds | quote }} + volumeMounts: + - name: review-script + mountPath: /scripts/security-wait.js + subPath: security-wait.js + source: | + require("/scripts/security-wait.js"); +{{- end }} + +{{- if .Values.deployTest.enabled }} + # ---- 2d. detect-deployable (P4) β€” does the diff touch deployable artifacts? ---- + # Emits `deployable = true|false`, which gates the expensive deploy-test step. + # Uses the GitHub compare API (authoritative changed-file list) rather than a + # local git diff β€” a depth-1 clone + depth-1 base fetch share no merge-base, so + # `origin/BASE...HEAD` yields nothing (observed: empty file list β†’ false). + - name: detect-deployable + script: + image: {{ .Values.reviewImage }} + command: [bash] + env: + - name: GH_TOKEN + valueFrom: + secretKeyRef: + name: {{ .Values.github.tokenSecret }} + key: {{ .Values.github.tokenKey }} + - name: REPO + value: "{{`{{workflow.parameters.repo}}`}}" + - name: BRANCH + value: "df/issue-{{`{{workflow.parameters.issue-number}}`}}" + - name: BASE + value: "{{`{{workflow.parameters.base-branch}}`}}" + - name: K8S_PATTERNS + value: {{ .Values.deployTest.k8sPatterns | quote }} + - name: TF_PATTERNS + value: {{ .Values.deployTest.terraformPatterns | quote }} + source: | + set -eu + FILES="$(GH_TOKEN="$GH_TOKEN" REPO="$REPO" BASE="$BASE" BRANCH="$BRANCH" node -e ' + const https=require("https"); + const {GH_TOKEN,REPO,BASE,BRANCH}=process.env; + https.get({host:"api.github.com",path:"/repos/"+REPO+"/compare/"+BASE+"..."+BRANCH,headers:{"User-Agent":"df-detect","Authorization":"Bearer "+GH_TOKEN,"Accept":"application/vnd.github+json"}}, + r=>{let b="";r.on("data",c=>b+=c);r.on("end",()=>{try{const j=JSON.parse(b);(j.files||[]).forEach(f=>console.log(f.filename));}catch(e){process.exit(0);}});}).on("error",()=>process.exit(0)); + ')" + echo "[detect] changed files:"; echo "$FILES" | sed 's/^/ /' + # Classify: k8s takes precedence over terraform when both are present. + if echo "$FILES" | grep -qE "$K8S_PATTERNS"; then KIND=k8s + elif echo "$FILES" | grep -qE "$TF_PATTERNS"; then KIND=terraform + else KIND=none; fi + echo "$KIND" > /tmp/kind + echo "[detect] kind=$KIND" + outputs: + parameters: + - name: kind + valueFrom: + path: /tmp/kind + default: "none" + + # ---- 2e. deploy-test (P4) β€” content-aware; THE ONLY step with K8s access ---- + # Trusted hub step. Validates the change with the RIGHT tool for its kind: + # kind=k8s β†’ ephemeral namespace apply + wait Ready + teardown (trap). + # kind=terraform β†’ terraform init -backend=false + validate (+ fmt check). + # Validation only β€” NO AWS creds, NO apply (no real infra). + # Posts the dark-factory/deploy-test commit status AND a marker PR comment with + # the details. Advisory in v1 (deployTest.blocking=false). The untrusted coder + # never has K8s access β€” it only produces the artifacts; this step runs them. + - name: deploy-test + inputs: + parameters: + - name: kind + activeDeadlineSeconds: {{ .Values.deployTest.timeoutSeconds }} + volumes: + - name: review-script + configMap: + name: df-review + script: + image: {{ .Values.deployTest.image }} + command: [bash] + volumeMounts: + - name: review-script + mountPath: /scripts/comment.js + subPath: comment.js + - name: review-script + mountPath: /scripts/deploy-test.sh + subPath: deploy-test.sh + env: + - name: WF_NAME + value: "{{`{{workflow.name}}`}}" + - name: GH_TOKEN + valueFrom: + secretKeyRef: + name: {{ .Values.github.tokenSecret }} + key: {{ .Values.github.tokenKey }} + - name: REPO + value: "{{`{{workflow.parameters.repo}}`}}" + - name: BRANCH + value: "df/issue-{{`{{workflow.parameters.issue-number}}`}}" + - name: ISSUE_NUMBER + value: "{{`{{workflow.parameters.issue-number}}`}}" + - name: KIND + value: "{{`{{inputs.parameters.kind}}`}}" + - name: MANIFEST_PATH + value: {{ .Values.deployTest.manifestPath | quote }} + - name: TF_PATH + value: {{ .Values.deployTest.terraformPath | quote }} + - name: READY_TIMEOUT + value: {{ .Values.deployTest.readyTimeoutSeconds | quote }} + - name: BLOCKING + value: {{ .Values.deployTest.blocking | quote }} + source: | + set -eu + # All logic lives in review/deploy-test.sh (ConfigMap file) β€” keeping it out + # of this YAML block scalar avoids the shell-quoting hazards that broke + # inline multi-line markdown. Kind-driven so it generalizes to new profiles. + bash /scripts/deploy-test.sh +{{- end }} + + # ---- 3. Sticky status β€” rewrite the PR body from the live verdicts ---- + # Runs AFTER every verify step (its DAG deps). The coder wrote the PR body at + # PR-open time, before verification ran, so its holdout/security/devops lines + # are placeholders ("pending"). This step reads the authoritative + # dark-factory/* commit STATUSES from GitHub (the source of truth the verify + # steps posted) and rewrites the PR body in place with the real verdicts β€” the + # "one live sticky status" from the design (README Β§7). Idempotent: the body + # between the markers is fully regenerated each run. + - name: sticky-status + inputs: + parameters: + - name: phase + volumes: + - name: review-script + configMap: + name: df-review + script: + image: {{ .Values.reviewImage }} + command: [bash] + env: + - name: GH_TOKEN + valueFrom: + secretKeyRef: + name: {{ .Values.github.tokenSecret }} + key: {{ .Values.github.tokenKey }} + - name: REPO + value: "{{`{{workflow.parameters.repo}}`}}" + - name: BRANCH + value: "df/issue-{{`{{workflow.parameters.issue-number}}`}}" + # For the AUTO-FIX loop (status.js): on a ❌ verdict from the real agents, + # collect their findings and submit a bounded df-run revision (iterate-note + # = the findings) so the coder auto-revises β€” no human paraphrasing needed. + - name: ISSUE_NUMBER + value: "{{`{{workflow.parameters.issue-number}}`}}" + - name: BASE_BRANCH + value: "{{`{{workflow.parameters.base-branch}}`}}" + - name: TRIGGER_LABEL + value: "{{`{{workflow.parameters.trigger-label}}`}}" + - name: ARGO_NAMESPACE + value: {{ .Values.argo.namespace | quote }} + - name: AUTO_FIX_FINDINGS + value: {{ .Values.review.autoFixFindings | default false | quote }} + - name: MAX_ITERATIONS + value: {{ .Values.iterate.maxIterations | default 3 | quote }} +{{- if .Values.devopsAgent.enabled }} + # Real AWS DevOps Agent posts a check-run (not a commit status) named this; + # status.js renders the DevOps row from the check-run when present. + - name: DEVOPS_CHECK + value: {{ .Values.devopsAgent.checkRunName | quote }} +{{- end }} +{{- if and .Values.securityAgent.app .Values.securityAgent.app.enabled }} + # Real AWS Security Agent GitHub App posts its own check/inline review; + # status.js renders the Security row from it when present (else the + # headless dark-factory/security status). + - name: SECURITY_CHECK + value: {{ .Values.securityAgent.app.checkRunName | quote }} +{{- end }} +{{- if .Values.postVerdictReview }} + # Post ONE consolidated verdict review (Security + DevOps results) on the + # PR so both agents' verdicts are ALWAYS visible in the Reviews section β€” + # the agent Apps review autonomously + inconsistently and cannot be added + # via the requested_reviewers API (verified no-op). status.js posts it once, + # when verification is terminal (idempotent via a hidden marker). + - name: POST_VERDICT_REVIEW + value: "true" +{{- end }} + # Holdout is a train/test QUALITY signal, advisory by default β€” it does NOT + # gate the merge verdict unless holdout.blocking=true. status.js reads this + # to decide whether a red holdout flips the consolidated verdict. + - name: HOLDOUT_BLOCKING + value: {{ .Values.holdout.blocking | quote }} + volumeMounts: + - name: review-script + mountPath: /scripts/status.js + subPath: status.js + source: | + set -eu + echo "[df-run] phase={{`{{inputs.parameters.phase}}`}} β€” updating PR body from live verdicts (${REPO} ${BRANCH})" + node /scripts/status.js + + # ---- onExit: release the claim (operator refills the pool) ---- + - name: teardown + resource: + action: delete + flags: ["--ignore-not-found"] + manifest: | + apiVersion: extensions.agents.x-k8s.io/v1beta1 + kind: SandboxClaim + metadata: + name: df-issue-{{`{{workflow.parameters.issue-id}}`}} + namespace: {{ .Values.warmPool.namespace }} diff --git a/gitops/addons/charts/dark-factory/templates/21-workflowtemplate-df-merge-teardown.yaml b/gitops/addons/charts/dark-factory/templates/21-workflowtemplate-df-merge-teardown.yaml new file mode 100644 index 00000000..da82dae0 --- /dev/null +++ b/gitops/addons/charts/dark-factory/templates/21-workflowtemplate-df-merge-teardown.yaml @@ -0,0 +1,148 @@ +{{- /* +df-merge-teardown β€” fired by the Sensor when a HUMAN approves the PR review +(pull_request_review, state=approved) on a df/issue-N PR. This is the ONLY path +that merges: the agent never self-merges (anti-pattern #5) β€” merge strictly +follows an explicit human approval event. + +Steps: verify the PR is green (all dark-factory/* checks succeeded) β†’ merge β†’ +delete the SandboxClaim (frees the warm pool). Guarded so a stray approval on a +non-green PR can't merge. Deterministic name df-merge- for dedup. + +Parameters (from the Sensor): issue-number (parsed from the PR branch df/issue-N), +repo, pr-number. The PR-review payload carries no issue id, so this keys on the +issue NUMBER and reaps the claim by label selector. +*/ -}} +{{- if .Values.trigger.argoEvents.enabled }} +apiVersion: argoproj.io/v1alpha1 +kind: WorkflowTemplate +metadata: + name: df-merge-teardown + namespace: {{ .Values.argo.namespace }} + labels: + {{- include "dark-factory.labels" . | nindent 4 }} +spec: + serviceAccountName: dark-factory-workflow + synchronization: + mutex: + name: "df-issue-num-{{`{{workflow.parameters.issue-number}}`}}" + entrypoint: main + arguments: + parameters: + - name: issue-number + - name: repo + - name: pr-number + ttlStrategy: + secondsAfterCompletion: {{ .Values.argo.workflowTtlSecondsAfterCompletion | default 604800 }} + templates: + - name: main + dag: + tasks: + - name: merge + template: merge-pr +{{- if .Values.microvm.enabled }} + # Flow D: this is the FINAL exit β€” TERMINATE the Lambda MicroVM that df-run-lambda + # kept SUSPENDED across the reviewβ†’fix loop. Deletes the Microvm CR (stable name + # mvm-) β†’ the controller TerminateMicrovm's the VM. No-op for Kata + # (there is no such CR). Advisory β€” never fails the merge. + - name: microvm-terminate + template: microvm-terminate + dependencies: [merge] +{{- end }} + - name: teardown + template: teardown-claim +{{- if .Values.microvm.enabled }} + dependencies: [microvm-terminate] +{{- else }} + dependencies: [merge] +{{- end }} + + # ---- Merge the PR β€” only if every dark-factory/* check is green ---- + - name: merge-pr + volumes: + - name: review-script + configMap: + name: df-review + script: + image: {{ .Values.reviewImage }} + command: [bash] + env: + - name: GH_TOKEN + valueFrom: + secretKeyRef: + name: {{ .Values.github.tokenSecret }} + key: {{ .Values.github.tokenKey }} + - name: REPO + value: "{{`{{workflow.parameters.repo}}`}}" + - name: PR + value: "{{`{{workflow.parameters.pr-number}}`}}" +{{- if .Values.devopsAgent.enabled }} + # Require the REAL AWS DevOps Agent check-run (Checks API) green before + # merge β€” merge.js reads both commit statuses and check-runs. + - name: DEVOPS_CHECK + value: {{ .Values.devopsAgent.checkRunName | quote }} + - name: REQUIRE_DEVOPS + value: "true" +{{- else }} + - name: REQUIRE_DEVOPS + value: "false" +{{- end }} +{{- if and .Values.securityAgent.app .Values.securityAgent.app.enabled }} + # Require the REAL AWS Security Agent GitHub App check green before merge + # so a Security BLOCK can't be merged past (in addition to the advisory + # dark-factory/security from the headless path β€” both run). + - name: SECURITY_CHECK + value: {{ .Values.securityAgent.app.checkRunName | quote }} + - name: REQUIRE_SECURITY + value: "true" +{{- end }} + volumeMounts: + - name: review-script + mountPath: /scripts/merge.js + subPath: merge.js + source: | + set -eu + echo "[df-merge] human-approved PR #${PR} in ${REPO} β€” verifying + merging" + node /scripts/merge.js + +{{- if .Values.microvm.enabled }} + # ---- Flow D: terminate the Lambda MicroVM (final exit) ---- + # df-run-lambda kept the VM SUSPENDED (CR mvm-) across the reviewβ†’fix + # loop. At merge we DELETE that CR β†’ the controller TerminateMicrovm's the VM. Stable + # name, so no Sandbox/annotation lookup needed. No-op for Kata (no such CR). Advisory. + - name: microvm-terminate + script: + image: {{ .Values.microvm.stepImage | default "public.ecr.aws/aws-cli/aws-cli:latest" }} + command: [sh] + source: | + set -eu + NS="{{ .Values.microvm.namespace | default "agent-sandbox-system" }}" + MVM="mvm-{{`{{workflow.parameters.issue-number}}`}}" + if ! command -v kubectl >/dev/null 2>&1; then + KV="$(curl -fsSL https://dl.k8s.io/release/stable.txt)" + curl -fsSL "https://dl.k8s.io/release/${KV}/bin/linux/amd64/kubectl" -o /tmp/kubectl + chmod +x /tmp/kubectl; export PATH="/tmp:$PATH" + fi + if kubectl get microvm "${MVM}" -n "${NS}" >/dev/null 2>&1; then + echo "[microvm-terminate] deleting Microvm/${MVM} β†’ controller TerminateMicrovm (final exit)" + kubectl delete microvm "${MVM}" -n "${NS}" --wait=false 2>&1 || echo "[microvm-terminate] delete failed (advisory)" + kubectl delete secret "${MVM}-payload" -n "${NS}" --ignore-not-found >/dev/null 2>&1 || true + else + echo "[microvm-terminate] no Microvm/${MVM} β€” nothing to terminate (Kata or already gone)" + fi +{{- end }} + + # ---- Teardown the claim (operator refills the pool) ---- + # Delete by the issue-number label (the PR-review event has no issue id). For Lambda + # this also removes the (idle) bridge pod; the VM is already terminated above. + - name: teardown-claim + resource: + action: delete + flags: + - "--ignore-not-found" + - "--selector=dark-factory.io/issue-number={{`{{workflow.parameters.issue-number}}`}}" + manifest: | + apiVersion: extensions.agents.x-k8s.io/v1beta1 + kind: SandboxClaim + metadata: + namespace: {{ .Values.warmPool.namespace }} +{{- end }} diff --git a/gitops/addons/charts/dark-factory/templates/22-workflowtemplate-df-iterate.yaml b/gitops/addons/charts/dark-factory/templates/22-workflowtemplate-df-iterate.yaml new file mode 100644 index 00000000..a02da4b5 --- /dev/null +++ b/gitops/addons/charts/dark-factory/templates/22-workflowtemplate-df-iterate.yaml @@ -0,0 +1,87 @@ +{{- /* +df-iterate β€” fired by the Sensor when a HUMAN comments on a Dark Factory PR +(issue_comment created, on a PR, non-bot). It routes the comment back to the +coder as a revision request so the coder revises the EXISTING df/issue-N branch. + +The issue_comment payload carries the PR number (body.issue.number) and the +comment text, but NOT the coder branch or the original df issue number. So this +workflow's first step resolves PR β†’ head.ref (df/issue-) β†’ issue number, then +submits df-run with iterate-note set (df-run injects it as DF_ITERATE_NOTE; the +coder appends it to SPEC.md and revises the branch). Bounded by +review.maxIterations via a label counter on the PR. + +Parameters (from the Sensor): repo, pr-number, comment-body, comment-id. +*/ -}} +{{- if and .Values.trigger.argoEvents.enabled .Values.iterate.enabled }} +apiVersion: argoproj.io/v1alpha1 +kind: WorkflowTemplate +metadata: + name: df-iterate + namespace: {{ .Values.argo.namespace }} + labels: + {{- include "dark-factory.labels" . | nindent 4 }} +spec: + # Runs in the argo ns as dark-factory-workflow (the sensor SA lives in + # argo-events, not here). iterate.js submits a df-run Workflow via the k8s API + # using this pod's SA token, so the role needs workflows:create (added in 10-rbac). + serviceAccountName: dark-factory-workflow + entrypoint: main + arguments: + parameters: + - name: repo + - name: pr-number + - name: comment-body + - name: comment-id + - name: comment-author + value: "" + ttlStrategy: + secondsAfterCompletion: {{ .Values.argo.workflowTtlSecondsAfterCompletion | default 604800 }} + templates: + - name: main + dag: + tasks: + - name: resolve-and-submit + template: resolve-and-submit + + # Resolve PR β†’ issue number, enforce the iteration cap, then submit df-run + # with the comment as the revision note. Runs as the sensor SA (can create + # Workflows). Uses node (coder image) for the GitHub API + submit via kubectl. + - name: resolve-and-submit + volumes: + - name: review-script + configMap: + name: df-review + script: + image: {{ .Values.reviewImage }} + command: [bash] + env: + - name: GH_TOKEN + valueFrom: + secretKeyRef: + name: {{ .Values.github.tokenSecret }} + key: {{ .Values.github.tokenKey }} + - name: REPO + value: "{{`{{workflow.parameters.repo}}`}}" + - name: PR + value: "{{`{{workflow.parameters.pr-number}}`}}" + - name: COMMENT_BODY + value: "{{`{{workflow.parameters.comment-body}}`}}" + - name: COMMENT_AUTHOR + value: "{{`{{workflow.parameters.comment-author}}`}}" + - name: MAX_ITERATIONS + value: {{ .Values.iterate.maxIterations | quote }} + - name: ARGO_NAMESPACE + value: {{ .Values.argo.namespace }} + - name: BIFROST_URL + value: {{ .Values.bifrost.url | quote }} + - name: CODER_PROFILE + value: {{ .Values.coder.engine | quote }} + volumeMounts: + - name: review-script + mountPath: /scripts/iterate.js + subPath: iterate.js + source: | + set -eu + echo "[df-iterate] PR #${PR} in ${REPO} β€” resolving branch + submitting revision" + node /scripts/iterate.js +{{- end }} diff --git a/gitops/addons/charts/dark-factory/templates/23-workflowtemplate-df-run-lambda.yaml b/gitops/addons/charts/dark-factory/templates/23-workflowtemplate-df-run-lambda.yaml new file mode 100644 index 00000000..692d7dec --- /dev/null +++ b/gitops/addons/charts/dark-factory/templates/23-workflowtemplate-df-run-lambda.yaml @@ -0,0 +1,945 @@ +{{- /* +df-run β€” the Flow B P1 pipeline as an Argo WorkflowTemplate. + + trigger β†’ claim warm sandbox β†’ coder implements+tests (in the Kata VM) β†’ + coder opens PR + posts a live sticky status β†’ await human approval. + +Single-cluster on the hub: the claim step creates a SandboxClaim with the issue +injected as env (verified contract: envVarsInjectionPolicy=Allowed), waits for +the operator to bind a warm micro-VM (status Ready), then the coder VM β€” which +boots the coder image from the SandboxTemplate β€” reads the env, implements on +df/issue-N, builds+tests, pushes, and opens the PR + sticky comment itself using +the short-TTL GitHub token projected into the VM. The workflow watches the coder +pod to completion, then its onExit handler releases the claim (the operator +refills the pool). P1 stops at "PR open, awaiting human" β€” merge/teardown + +verification gates are P2-P4. + +Parameters (supplied by the trigger / Sensor): + issue-id, issue-number, repo (owner/name), issue-title, issue-body, base-branch +*/ -}} +apiVersion: argoproj.io/v1alpha1 +kind: WorkflowTemplate +metadata: + name: df-run-lambda + namespace: {{ .Values.argo.namespace }} + labels: + {{- include "dark-factory.labels" . | nindent 4 }} +spec: + serviceAccountName: dark-factory-workflow + # Serialize per issue (no double-claim for the same issue) and cap total + # concurrent runs against the kata pool size. Keyed on issue-NUMBER (stable across + # first run + fix rounds) to match the claim/Sandbox name below β€” so a fix round is + # serialized against its own issue's first run and can't race a second claim. + synchronization: + mutex: + name: "df-issue-{{`{{workflow.parameters.issue-number}}`}}" + entrypoint: main + arguments: + parameters: + - name: issue-id + - name: issue-number + - name: repo + - name: issue-title + - name: issue-body + value: "" + - name: base-branch + value: main + # Optional revision request (df-iterate passes the PR comment here; empty on + # a first pass). Injected into the coder as DF_ITERATE_NOTE so it revises the + # existing branch to address the feedback. + - name: iterate-note + value: "" + # Base64 of the revision note β€” used when the note is arbitrary markdown + # (auto-fed agent findings) that can't be injected raw into the claim YAML. + # The coder decodes this first, falling back to the plain iterate-note. + - name: iterate-note-b64 + value: "" + # Which label fired the run selects the coder substrate (Kata default vs + # Lambda MicroVM for darkfactory-lambda). MUST be declared with a default so + # every trigger-label reference resolves even when the submitter (sensor / + # manual) does not pass it. Defaulting to dark-factory keeps the Kata path for + # sensor submits that do not map a label. + - name: trigger-label + value: "dark-factory" + # Always release the claimed sandbox, on success OR failure. + onExit: teardown + ttlStrategy: + secondsAfterCompletion: {{ .Values.argo.workflowTtlSecondsAfterCompletion | default 604800 }} +{{- if .Values.metrics.enabled }} + # Success metrics (Argo-native Prometheus). Argo exposes these on the workflow + # controller's :9090/metrics β€” scraped into the platform Prometheus. Gives the + # GitOps-native view of the factory: throughput, outcome mix, and lead time. + metrics: + prometheus: + - name: df_runs_total + help: "Dark Factory df-run workflows by status" + labels: + - { key: status, value: "{{`{{workflow.status}}`}}" } + counter: + value: "1" + - name: df_run_duration_seconds + help: "df-run wall-clock duration (lead time proxy)" + gauge: + value: "{{`{{workflow.duration}}`}}" +{{- end }} + templates: + + # ---- DAG ---- + # claim β†’ coder β†’ [ verify fan: holdout-gate βˆ₯ security-review βˆ₯ devops-review ] + # β†’ status. Every verify step runs OUTSIDE the coder (trusted hub pod), is gated + # on a PR existing, and is advisory in v1 (posts a commit status; never fails the + # run unless its *.blocking / blockSeverity is raised). `status` waits for all + # enabled verify steps via its dependencies list. + # ---- DAG (MicroVM-native, NO SandboxClaim/bridge/warm-pool) ---- + # provision-microvm (create Microvm CR + drive /run) β†’ drive-coder (poll GitHub for + # PR) β†’ suspend-microvm (scale-to-zero during review) β†’ [verify fan] β†’ status. + # On a FIX ROUND the workflow first RESUMES the same suspended VM (resume-microvm), + # re-drives /run with the new note, then re-suspends. Teardown (onExit) deletes the + # Microvm CR β†’ controller TerminateMicrovm. + - name: main + dag: + tasks: + # 1. Provision the MicroVM directly (create CR + Secret, wait RUNNING, POST /run). + # On a fix round (iterate-note set) it RESUMES the existing suspended VM instead. + - name: provision-microvm + template: provision-microvm + # 2. Poll GitHub for the PR the coder opens (substrate-agnostic; reused verbatim). + - name: drive-coder + template: await-coder + dependencies: [provision-microvm] + arguments: + parameters: + - name: sandbox + value: "" + # 3. SUSPEND the VM once the coder pushed its PR β€” scale-to-zero during review. + # Explicit DAG step (this template is Lambda-only, so no substrate gating). + - name: suspend-microvm + template: set-microvm-power + dependencies: [drive-coder] + when: "{{`{{tasks.drive-coder.outputs.parameters.pr-number}}`}} != \"\"" + arguments: + parameters: + - name: action + value: "suspend" +{{- if .Values.holdout.enabled }} + # P2 β€” holdout gate: hidden scenarios + a different-family judge. + - name: holdout-gate + template: holdout-gate + dependencies: [drive-coder] + when: "{{`{{tasks.drive-coder.outputs.parameters.pr-number}}`}} != \"\"" + arguments: + parameters: + - name: pr-number + value: "{{`{{tasks.drive-coder.outputs.parameters.pr-number}}`}}" +{{- end }} +{{- if .Values.review.enabled }} + # P3 β€” REAL AWS Frontier Agents, ordered: DevOps FIRST, then Security. + # + # DevOps Agent (release readiness) runs via the Claude Code plugin INSIDE + # the coder step (no headless API), so there is no separate DAG task for + # it here β€” its verdict is reported by the coder and, on a clear verdict, + # the coder applies the `needs-security-review` label. This `devops-gate` + # step confirms the label is present (DevOps cleared) before Security runs. + - name: devops-gate + template: devops-gate + dependencies: [drive-coder] + when: "{{`{{tasks.drive-coder.outputs.parameters.pr-number}}`}} != \"\"" + arguments: + parameters: + - name: pr-number + value: "{{`{{tasks.drive-coder.outputs.parameters.pr-number}}`}}" + # Security Agent β€” WAITS FOR the real aws-security-agent[bot]'s own review + # and mirrors its verdict into dark-factory/security. The App bot reviews + # autonomously + IN PARALLEL with the DevOps bot, so this runs alongside + # devops-gate (depends on drive-coder, NOT gated behind DevOps clearing β€” + # otherwise a slow DevOps review would skip the security signal entirely). + - name: security-agent + template: security-agent + dependencies: [drive-coder] + when: "{{`{{tasks.drive-coder.outputs.parameters.pr-number}}`}} != \"\"" +{{- end }} +{{- if .Values.deployTest.enabled }} + # P4 β€” detect whether the PR touches deployable artifacts (cheap grep of + # the diff). Its output gates the (expensive, K8s-touching) deploy-test. + - name: detect-deployable + template: detect-deployable + dependencies: [drive-coder] + when: "{{`{{tasks.drive-coder.outputs.parameters.pr-number}}`}} != \"\"" + # P4 β€” deploy-test: THE ONLY step that holds K8s access. Content-aware: + # k8s β†’ ephemeral-namespace apply+probe+teardown; terraform β†’ init+validate. + # Runs only when detect classified the change (kind != none). + - name: deploy-test + template: deploy-test + dependencies: [detect-deployable] + when: "{{`{{tasks.detect-deployable.outputs.parameters.kind}}`}} != none" + arguments: + parameters: + - name: kind + value: "{{`{{tasks.detect-deployable.outputs.parameters.kind}}`}}" +{{- end }} + - name: status + template: sticky-status + # Depend on BOTH agents (devops-gate + security-agent) plus holdout + + # deploy-test so the consolidated verdict is only computed once EVERY + # signal is terminal β€” required so a ❌ (and the auto-fix decision) is made + # on complete input, and so a clear verdict truly means both agents cleared. + dependencies: + {{- if or .Values.holdout.enabled .Values.review.enabled .Values.deployTest.enabled }} + {{- if .Values.holdout.enabled }} + - holdout-gate + {{- end }} + {{- if .Values.review.enabled }} + - devops-gate + - security-agent + {{- end }} + {{- if .Values.deployTest.enabled }} + - deploy-test + {{- end }} + {{- else }} + - drive-coder + {{- end }} + arguments: + parameters: + - name: phase + value: "pr-open" + + # ---- 1. Provision the Lambda MicroVM directly (NO SandboxClaim/bridge/warm-pool) ---- + # Creates the runHookPayload Secret + the Microvm CR (the ACK lambdamicrovms + # controller does RunMicrovm), waits for RUNNING + an endpoint, mints an auth token, + # and POSTs /run to start the coder. On a FIX ROUND (iterate-note set) it RESUMES the + # same suspended VM if still alive (warm resume β€” the Flow D value prop), else recreates. + # Every hard-won Flow D fix is encoded here: + # β€’ autoResumeEnabled=false + we never poll the endpoint after /run β†’ suspend STICKS. + # β€’ suspendedDurationSeconds=8h so the VM survives the reviewβ†’fix window. + # β€’ HTTP_INGRESS (ALL_INGRESS blocks auth-token minting). + # β€’ runHookPayload carries ghToken + DF_* + the iterate note (the coder has no claim env). + # β€’ CR/Secret named mvm- (stable across rounds β†’ one VM per issue). + # β€’ image/exec-role read from the platform MicrovmSandbox status (built once by KRO/ACK). + - name: provision-microvm + outputs: + parameters: + - name: microvm-id + valueFrom: { path: /tmp/vmid } + script: + image: {{ .Values.microvm.stepImage | default "public.ecr.aws/aws-cli/aws-cli:latest" }} + command: [sh] + env: + - name: GH_TOKEN + valueFrom: + secretKeyRef: + name: {{ .Values.github.tokenSecret }} + key: token + - name: ISSUE_NUMBER + value: "{{`{{workflow.parameters.issue-number}}`}}" + - name: REPO + value: "{{`{{workflow.parameters.repo}}`}}" + - name: BASE_BRANCH + value: "{{`{{workflow.parameters.base-branch}}`}}" + - name: ISSUE_TITLE + value: "{{`{{workflow.parameters.issue-title}}`}}" + - name: ITERATE_NOTE_B64 + value: "{{`{{workflow.parameters.iterate-note-b64}}`}}" + - name: ITERATE_NOTE + value: "{{`{{workflow.parameters.iterate-note}}`}}" + source: | + set -eu + REGION="{{ .Values.microvm.region }}" + NS="{{ .Values.microvm.namespace | default "agent-sandbox-system" }}" + PLATFORM_IMAGE="{{ .Values.microvm.image.name | default "coder" }}" + MVM="mvm-${ISSUE_NUMBER}" + BRANCH="df/issue-${ISSUE_NUMBER}" + MAXIDLE={{ .Values.microvm.defaults.maxIdleDurationSeconds | default 1800 }} + SUSPDUR={{ .Values.microvm.defaults.suspendedDurationSeconds | default 28800 }} + INGRESS="arn:aws:lambda:${REGION}:aws:network-connector:aws-network-connector:HTTP_INGRESS" + EGRESS="arn:aws:lambda:${REGION}:aws:network-connector:aws-network-connector:INTERNET_EGRESS" + LOGGRP="/aws/lambda/microvms/${PLATFORM_IMAGE}-image" + # aws-cli image has no kubectl β€” fetch a static one (same as the old bridge). + if ! command -v kubectl >/dev/null 2>&1; then + KV="$(curl -fsSL https://dl.k8s.io/release/stable.txt)" + curl -fsSL "https://dl.k8s.io/release/${KV}/bin/linux/amd64/kubectl" -o /tmp/kubectl + chmod +x /tmp/kubectl; export PATH="/tmp:$PATH" + fi + # 1) Read the platform image handoff (imageARN + execRoleARN β€” built ONCE by KRO/ACK). + echo "[provision] reading platform image ${PLATFORM_IMAGE} (waiting for build ready)..." + i=0; IMAGE_ARN=""; EXEC_ROLE="" + while [ "$i" -lt 240 ]; do + IST=$(kubectl get microvmsandbox "${PLATFORM_IMAGE}" -n "${NS}" -o jsonpath='{.status.imageState}' 2>/dev/null || echo "") + if [ "${IST}" = "CREATED" ] || [ "${IST}" = "UPDATED" ]; then + IMAGE_ARN=$(kubectl get microvmsandbox "${PLATFORM_IMAGE}" -n "${NS}" -o jsonpath='{.status.imageARN}' 2>/dev/null || echo "") + EXEC_ROLE=$(kubectl get microvmsandbox "${PLATFORM_IMAGE}" -n "${NS}" -o jsonpath='{.status.executionRoleARN}' 2>/dev/null || echo "") + [ -n "${IMAGE_ARN}" ] && [ -n "${EXEC_ROLE}" ] && break + fi + i=$((i+1)); sleep 5 + done + [ -z "${IMAGE_ARN}" ] || [ -z "${EXEC_ROLE}" ] && { echo "[provision] ERROR: platform image not ready (imageState=${IST:-none})"; exit 1; } + echo "[provision] image=${IMAGE_ARN} execRole=${EXEC_ROLE}" + # 2) FIX ROUND vs FIRST RUN: if a CR exists, check the VM's ACTUAL AWS state. + # RUNNING/SUSPENDED/PENDING β†’ RESUME the same VM (warm resume). Else (TERMINATED + # past the 8h suspend cap, or a service error) β†’ recreate a fresh VM. + FRESH=1 + if kubectl get microvm "${MVM}" -n "${NS}" >/dev/null 2>&1; then + PRIORVMID=$(kubectl get microvm "${MVM}" -n "${NS}" -o jsonpath='{.status.microvmID}' 2>/dev/null || echo "") + PRIORSTATE="" + [ -n "${PRIORVMID}" ] && PRIORSTATE=$(aws lambda-microvms get-microvm --region "${REGION}" --microvm-identifier "${PRIORVMID}" --query 'state' --output text 2>/dev/null || echo "") + echo "[provision] fix round: prior VM ${PRIORVMID:-none} state=${PRIORSTATE:-unknown}" + case "${PRIORSTATE}" in + RUNNING|SUSPENDED|PENDING) + echo "[provision] RESUMING same VM ${PRIORVMID} (warm resume)" + aws lambda-microvms resume-microvm --region "${REGION}" --microvm-identifier "${PRIORVMID}" 2>/dev/null || true + # Refresh the payload Secret so the resumed hook-server gets the NEW note. + FRESH=0; VMID="${PRIORVMID}" + ;; + *) + echo "[provision] prior VM not resumable (${PRIORSTATE:-gone}) β€” recreating fresh" + kubectl delete microvm "${MVM}" -n "${NS}" --wait=false >/dev/null 2>&1 || true + kubectl delete secret "${MVM}-payload" -n "${NS}" --ignore-not-found >/dev/null 2>&1 || true + j=0; while [ "$j" -lt 60 ]; do kubectl get microvm "${MVM}" -n "${NS}" >/dev/null 2>&1 || break; j=$((j+1)); sleep 5; done + ;; + esac + fi + # 3) Build the runHookPayload (ghToken + DF_* + iterate note) and (re)write the Secret. + PAYLOAD=$(GH="${GH_TOKEN}" REGION="${REGION}" IN="${ISSUE_NUMBER}" RP="${REPO}" BR="${BRANCH}" BB="${BASE_BRANCH}" IT="${ISSUE_TITLE}" NB="${ITERATE_NOTE_B64}" NP="${ITERATE_NOTE}" python3 -c 'import json,os; e=os.environ.get; print(json.dumps({"ghToken":e("GH",""),"region":e("REGION","us-west-2"),"issueNumber":e("IN",""),"repo":e("RP",""),"branch":e("BR",""),"baseBranch":e("BB","main"),"issueTitle":e("IT",""),"iterateNoteB64":e("NB",""),"iterateNote":e("NP","")}))') + MVM="${MVM}" NS="${NS}" PAYLOAD="${PAYLOAD}" python3 -c 'import json,os; e=os.environ; print(json.dumps({"apiVersion":"v1","kind":"Secret","metadata":{"name":e["MVM"]+"-payload","namespace":e["NS"]},"type":"Opaque","stringData":{"payload":e["PAYLOAD"]}}))' | kubectl apply -f - >/dev/null 2>&1 || { echo "[provision] payload secret apply failed"; exit 1; } + # 4) On a FIRST RUN (or recreate), create the Microvm CR (autoResume=false). + if [ "${FRESH}" = "1" ]; then + MVM="${MVM}" NS="${NS}" IMG="${IMAGE_ARN}" EXECROLE="${EXEC_ROLE}" MAXIDLE="${MAXIDLE}" SUSPDUR="${SUSPDUR}" INGRESS="${INGRESS}" EGRESS="${EGRESS}" LOGGRP="${LOGGRP}" python3 -c 'import json,os; e=os.environ; mvm=e["MVM"]; ns=e["NS"]; print(json.dumps({"apiVersion":"lambdamicrovms.services.k8s.aws/v1alpha1","kind":"Microvm","metadata":{"name":mvm,"namespace":ns},"spec":{"imageIdentifier":e["IMG"],"executionRoleARN":e["EXECROLE"],"ingressNetworkConnectors":[e["INGRESS"]],"egressNetworkConnectors":[e["EGRESS"]],"runHookPayload":{"name":mvm+"-payload","key":"payload","namespace":ns},"logging":{"cloudWatch":{"logGroup":e["LOGGRP"],"logStream":"runtime-"+mvm}},"idlePolicy":{"autoResumeEnabled":False,"maxIdleDurationSeconds":int(e["MAXIDLE"]),"suspendedDurationSeconds":int(e["SUSPDUR"])}}}))' | kubectl apply -f - >/dev/null 2>&1 || { echo "[provision] Microvm CR apply failed"; exit 1; } + VMID=""; i=0 + while [ "$i" -lt 60 ]; do + VMID=$(kubectl get microvm "${MVM}" -n "${NS}" -o jsonpath='{.status.microvmID}' 2>/dev/null || echo "") + [ -n "${VMID}" ] && break; i=$((i+1)); sleep 5 + done + fi + echo "${VMID:-}" > /tmp/vmid + [ -z "${VMID:-}" ] && { echo "[provision] ERROR: no microvmID"; exit 1; } + echo "[provision] Microvm ${MVM} -> ${VMID}" + # 5) Wait for RUNNING + endpoint, mint token, POST /run (background-spawns the coder). + EP=""; i=0 + while [ "$i" -lt 72 ]; do + S=$(aws lambda-microvms get-microvm --region "${REGION}" --microvm-identifier "${VMID}" --query 'state' --output text 2>/dev/null || echo "") + EP=$(aws lambda-microvms get-microvm --region "${REGION}" --microvm-identifier "${VMID}" --query 'endpoint' --output text 2>/dev/null || echo "") + [ "$S" = "RUNNING" ] && [ -n "${EP}" ] && [ "${EP}" != "None" ] && break + i=$((i+1)); sleep 5 + done + [ -z "${EP}" ] || [ "${EP}" = "None" ] && { echo "[provision] ERROR: VM never reached RUNNING+endpoint (state=${S:-none})"; exit 1; } + TOKEN=$(aws lambda-microvms create-microvm-auth-token --region "${REGION}" --microvm-identifier "${VMID}" --expiration-in-minutes 60 --allowed-ports 'port=8080' 2>/dev/null | python3 -c 'import json,sys; print(json.load(sys.stdin)["authToken"]["X-aws-proxy-auth"])' 2>/dev/null || echo "") + [ -z "${TOKEN}" ] && { echo "[provision] ERROR: could not mint auth token"; exit 1; } + echo "[provision] driving coder: POST /run on ${EP}" + RC=$(curl -sS -m 30 -o /tmp/run.out -w '%{http_code}' -X POST "https://${EP}/run" -H "X-aws-proxy-auth: ${TOKEN}" -H 'Content-Type: application/json' -d "${PAYLOAD}" 2>/tmp/run.err || echo "000") + echo "[provision] /run -> HTTP ${RC} $(cat /tmp/run.out 2>/dev/null | head -c 120)" + case "${RC}" in 2*) echo "[provision] coder started." ;; *) echo "[provision] ERROR: /run returned ${RC}"; exit 1 ;; esac + + # ---- 2. Wait for the coder VM to finish β€” GitHub is the completion bus ---- + # The coder image (baked into the SandboxTemplate) auto-runs on VM start, + # reads DF_* env, implements + tests, pushes df/issue-N, and opens the PR + # itself using the short-TTL gh-token projected into the VM. The coder is + # credential-less to the k8s API (no SA token), so it CANNOT signal via a pod + # annotation β€” it self-reports through GitHub. This step polls the GitHub API + # (with the workflow's own token) for a PR whose head is df/issue-N, and + # treats the head commit's dark-factory/implementation check as the verdict. + - name: await-coder + inputs: + parameters: + - name: sandbox + activeDeadlineSeconds: {{ mul .Values.coder.runTimeoutMinutes 60 }} + script: + image: {{ .Values.stepImage }} + command: [sh] + env: + - name: GH_TOKEN + valueFrom: + secretKeyRef: + name: {{ .Values.github.tokenSecret }} + key: {{ .Values.github.tokenKey }} + source: | + set -eu + command -v curl >/dev/null 2>&1 || apk add --no-cache curl jq >/dev/null 2>&1 + REPO="{{`{{workflow.parameters.repo}}`}}" + BRANCH="df/issue-{{`{{workflow.parameters.issue-number}}`}}" + ITERATE_NOTE="{{`{{workflow.parameters.iterate-note}}`}}" + ITERATE_NOTE_B64="{{`{{workflow.parameters.iterate-note-b64}}`}}" + API="https://api.github.com/repos/${REPO}" + # ROUND-AWARENESS: on a FIX ROUND (iterate-note set) the branch already + # exists and its OLD commit already has dark-factory/implementation=success. + # If we accept that, the verify fan (holdout/security/devops) runs against + # the STALE pre-fix commit before the coder's new push lands (observed 45s + # race). So record the branch's starting SHA and require a DIFFERENT head + # SHA (a genuinely new commit) before we consider the coder done on a fix + # round. First-pass runs (no iterate-note) have no prior commit β†’ accept the + # first commit that reports impl=success. + START_SHA="$(curl -fsS -H "Authorization: Bearer ${GH_TOKEN}" -H "Accept: application/vnd.github+json" \ + "${API}/branches/${BRANCH}" 2>/dev/null | jq -r '.commit.sha // empty' || echo "")" + IS_FIX="false"; { [ -n "${ITERATE_NOTE}" ] || [ -n "${ITERATE_NOTE_B64}" ]; } && IS_FIX="true" + echo "[df-run] polling GitHub for PR head=${BRANCH} (fix-round=${IS_FIX}, start-sha=${START_SHA:-none})..." + while true; do + pr="$(curl -fsS -H "Authorization: Bearer ${GH_TOKEN}" \ + -H "Accept: application/vnd.github+json" \ + "${API}/pulls?head=${REPO%%/*}:${BRANCH}&state=open" 2>/dev/null || echo '[]')" + n="$(echo "${pr}" | jq -r '.[0].number // empty')" + if [ -n "${n}" ]; then + sha="$(echo "${pr}" | jq -r '.[0].head.sha')" + # On a fix round, ignore the old commit until the coder pushes a new one. + if [ "${IS_FIX}" = "true" ] && [ -n "${START_SHA}" ] && [ "${sha}" = "${START_SHA}" ]; then + echo "[df-run] fix-round: head still at start sha ${sha} β€” waiting for the coder's new commit..." + sleep 15; continue + fi + st="$(curl -fsS -H "Authorization: Bearer ${GH_TOKEN}" -H "Accept: application/vnd.github+json" \ + "${API}/commits/${sha}/status" 2>/dev/null | jq -r '.statuses[] | select(.context=="dark-factory/implementation") | .state' | head -1)" + echo "[df-run] PR #${n} open (sha=${sha}) impl-status=${st:-pending}" + case "${st}" in + success) echo "[df-run] coder finished β€” PR #${n} @ ${sha} ready."; echo "${n}" > /tmp/pr; echo "${sha}" > /tmp/sha; exit 0 ;; + failure|error) echo "[df-run] coder reported failure."; exit 1 ;; + esac + else + echo "[df-run] no PR yet β€” coder still working..." + fi + sleep 15 + done + outputs: + parameters: + - name: pr-number + valueFrom: + path: /tmp/pr + default: "" + - name: head-sha + valueFrom: + path: /tmp/sha + default: "" + +{{- if .Values.holdout.enabled }} + # ---- 2b. Holdout gate (P2) β€” train/test separation for code ---- + # Runs on the HUB (a trusted step, NOT the Kata VM). Checks out the coder's + # PR branch, diffs it vs base, then runs evaluate.js: each hidden scenario + # gets its executable test run against the built code AND a different-family + # judge (Nova) vote (2-of-3). Gate = passRatio >= threshold. The hidden + # scenarios come from the df-holdout-* ConfigMaps mounted here β€” they are + # NEVER mounted into the sandbox, so the coder cannot see or edit them. + # v1 is advisory: the gate result is posted as the dark-factory/holdout commit + # status; the workflow only fails on a red gate when holdout.blocking=true. + - name: holdout-gate + inputs: + parameters: + - name: pr-number + activeDeadlineSeconds: 900 + volumes: + - name: holdout-eval + configMap: + name: df-holdout-eval + {{- range $repo := .Values.trigger.argoEvents.repositories }} + {{- range $name := $repo.names }} + - name: holdout-{{ printf "%s-%s" $repo.owner $name | lower }} + configMap: + name: df-holdout-{{ printf "%s-%s" $repo.owner $name | lower }} + {{- end }} + {{- end }} + script: + image: {{ .Values.holdout.evalImage }} + command: [bash] + env: + - name: GH_TOKEN + valueFrom: + secretKeyRef: + name: {{ .Values.github.tokenSecret }} + key: {{ .Values.github.tokenKey }} + - name: BIFROST_URL + value: {{ .Values.holdout.bifrostUrl | quote }} + - name: JUDGE_MODEL + value: {{ .Values.holdout.judgeModel | quote }} + - name: JUDGE_RUNS + value: {{ .Values.holdout.judgeRuns | quote }} + - name: JUDGE_QUORUM + value: {{ .Values.holdout.judgeQuorum | quote }} + - name: THRESHOLD + value: {{ .Values.holdout.threshold | quote }} + volumeMounts: + - name: holdout-eval + mountPath: /holdout/evaluate.js + subPath: evaluate.js + {{- range $repo := .Values.trigger.argoEvents.repositories }} + {{- range $name := $repo.names }} + - name: holdout-{{ printf "%s-%s" $repo.owner $name | lower }} + mountPath: /holdout/{{ printf "%s-%s" $repo.owner $name }} + {{- end }} + {{- end }} + source: | + set -eu + REPO="{{`{{workflow.parameters.repo}}`}}" + BRANCH="df/issue-{{`{{workflow.parameters.issue-number}}`}}" + BASE="{{`{{workflow.parameters.base-branch}}`}}" + SLUG="$(echo "${REPO}" | tr '/' '-')" + API="https://api.github.com/repos/${REPO}" + WORK=/tmp/holdout-work + rm -rf "${WORK}"; mkdir -p "${WORK}" + echo "[holdout] cloning ${REPO}@${BRANCH} for evaluation..." + git clone --quiet --branch "${BRANCH}" \ + "https://x-access-token:${GH_TOKEN}@github.com/${REPO}.git" "${WORK}/repo" + cd "${WORK}/repo" + # Diff vs base β€” use the GitHub COMPARE API (authoritative changed-file + # patch), NOT a local git diff. A branch clone shares no reliable merge-base + # with a shallow base fetch, so `origin/BASE...HEAD` yields the WHOLE file as + # "added" (observed: a sum-only PR's diff contained every pre-existing + # function β†’ every scenario's appliesWhen matched β†’ wrong scenarios graded β†’ + # false holdout failure). The compare API returns only the real per-file + # patch hunks, so appliesWhen keys on the ACTUAL change. (Same fix as + # detect-deployable.) Concatenate every file's patch into /tmp/diff.patch. + PR_NUM="{{`{{inputs.parameters.pr-number}}`}}" + curl -fsS -H "Authorization: Bearer ${GH_TOKEN}" -H "Accept: application/vnd.github+json" \ + "${API}/pulls/${PR_NUM}/files?per_page=100" 2>/dev/null \ + | python3 -c 'import json,sys; d=json.load(sys.stdin); files=d if isinstance(d,list) else []; open("/tmp/diff.patch","w").write("\n".join("diff --git a/%s b/%s\n%s"%(f.get("filename",""),f.get("filename",""),f.get("patch","")) for f in files))' 2>/dev/null || echo "" > /tmp/diff.patch + # Fallback: if the API path produced nothing, fall back to a local diff. + [ -s /tmp/diff.patch ] || { git fetch --quiet --depth 1 origin "${BASE}" 2>/dev/null || true; git diff "origin/${BASE}...HEAD" > /tmp/diff.patch 2>/dev/null || echo "" > /tmp/diff.patch; } + echo "[holdout] diff.patch $(wc -l < /tmp/diff.patch) lines (via compare API)" + # Build/install so executable tests can require the module. + if [ -f package.json ]; then npm install --no-audit --no-fund >/dev/null 2>&1 || true; fi + SHA="$(git rev-parse HEAD)" + + SCEN="/holdout/${SLUG}/scenarios.json" + if [ ! -f "${SCEN}" ]; then + echo "[holdout] no hidden scenarios for ${SLUG} β€” skipping gate (advisory)." + exit 0 + fi + + echo "[holdout] evaluating against hidden scenarios..." + set +e + REPO_DIR="${WORK}/repo" DIFF=/tmp/diff.patch SCENARIOS="${SCEN}" \ + OUT=/tmp/holdout-result.json node /holdout/evaluate.js + GATE=$? + set -e + + RATIO="$(node -e 'const r=require("/tmp/holdout-result.json");console.log(Math.round(r.ratio*100))' 2>/dev/null || echo 0)" + PASSED="$(node -e 'const r=require("/tmp/holdout-result.json");console.log(r.passed+"/"+r.total)' 2>/dev/null || echo '?/?')" + TOTAL="$(node -e 'const r=require("/tmp/holdout-result.json");console.log(r.total)' 2>/dev/null || echo 0)" + SKIPPED="$(node -e 'const r=require("/tmp/holdout-result.json");console.log(r.skipped||0)' 2>/dev/null || echo 0)" + # Distinguish the three outcomes so the PR reads honestly: + # total=0 β†’ no hidden scenario matched this change (e.g. a Terraform PR + # vs JS-only scenarios) β†’ NOT APPLICABLE, green as n/a. + # gate ok β†’ passed. gate fail β†’ below threshold. + if [ "${TOTAL}" = "0" ]; then + STATE=success; DESC="not applicable β€” no hidden scenarios match this change (${SKIPPED} skipped)" + elif [ "${GATE}" -eq 0 ]; then + STATE=success; DESC="holdout ${PASSED} (${RATIO}%) β€” gate passed" + else + STATE=failure; DESC="holdout ${PASSED} (${RATIO}%) β€” below threshold" + fi + + # Post the holdout verdict as a commit status on the PR head SHA. The + # coder image has no curl, so use node's https (always present). Retry + # transient failures so a blip doesn't drop the verdict. + GH_TOKEN="${GH_TOKEN}" REPO="${REPO}" SHA="${SHA}" STATE="${STATE}" DESC="${DESC}" node -e ' + const https=require("https"); + const body=JSON.stringify({state:process.env.STATE,context:"dark-factory/holdout",description:process.env.DESC}); + let n=0; + (function post(){ + const req=https.request({host:"api.github.com",method:"POST",path:"/repos/"+process.env.REPO+"/statuses/"+process.env.SHA, + headers:{"User-Agent":"dark-factory-holdout","Authorization":"Bearer "+process.env.GH_TOKEN,"Accept":"application/vnd.github+json","Content-Type":"application/json","Content-Length":Buffer.byteLength(body)}}, + r=>{let b="";r.on("data",c=>b+=c);r.on("end",()=>{if(r.statusCode>=300&&r.statusCode<500){console.error("status post "+r.statusCode+": "+b.slice(0,120))}else if(r.statusCode>=500&&++n<4){setTimeout(post,500*n);return} console.log("[holdout] posted dark-factory/holdout="+process.env.STATE)});}); + req.on("error",e=>{if(++n<4){setTimeout(post,500*n)}else{console.error("status post error: "+e.message)}}); + req.write(body);req.end(); + })(); + ' || true + + {{- if .Values.holdout.blocking }} + echo "[holdout] blocking mode β€” gate result gates the workflow." + exit ${GATE} + {{- else }} + echo "[holdout] advisory mode β€” gate result reported, workflow continues." + exit 0 + {{- end }} + outputs: + parameters: + - name: ratio + valueFrom: + path: /tmp/holdout-result.json + default: "{}" +{{- end }} + +{{- if .Values.review.enabled }} + # ---- 2c. devops-gate (P3, step 1) β€” wait for the AWS DevOps Agent to clear ---- + # The AWS DevOps Agent release-readiness review is its own GitHub App that + # auto-reviews every PR and posts a check-run / commit status (its native model + # β€” there is NO headless code-review API). This step polls the PR head for that + # check and emits cleared=true|false, which gates the Security Agent step so + # DevOps reviews FIRST, Security SECOND (docs Β§6.2). + # + # Mode (devopsAgent.gate): + # check β†’ wait for the DevOps Agent's own check-run (context matches + # devopsAgent.checkContext) to conclude success/neutral. This is the + # real GitHub-App path (needs the one-time console repo connect). + # label β†’ fallback: wait for the coder-applied handoffLabel (used only if + # you drive DevOps via the coding-agent plugin instead of the App). + # If DevOps never reports within the window, cleared=false β†’ Security is skipped + # and the sticky status shows DevOps not-run (NEVER a fake pass). Advisory: this + # step itself never fails the run. + - name: devops-gate + inputs: + parameters: + - name: pr-number + activeDeadlineSeconds: {{ add .Values.devopsAgent.waitSeconds 60 }} + volumes: + - name: review-script + configMap: + name: df-review + script: + image: {{ .Values.reviewImage }} + command: [bash] + env: + - name: GH_TOKEN + valueFrom: + secretKeyRef: + name: {{ .Values.github.tokenSecret }} + key: {{ .Values.github.tokenKey }} + - name: REPO + value: "{{`{{workflow.parameters.repo}}`}}" + - name: PR + value: "{{`{{inputs.parameters.pr-number}}`}}" + - name: BRANCH + value: "df/issue-{{`{{workflow.parameters.issue-number}}`}}" + - name: GATE_MODE + value: {{ .Values.devopsAgent.gate | quote }} + - name: LABEL + value: {{ .Values.review.handoffLabel | quote }} + - name: CHECK_CONTEXT + value: {{ .Values.devopsAgent.checkContext | quote }} + - name: WAIT_SECONDS + value: {{ .Values.devopsAgent.waitSeconds | quote }} + volumeMounts: + - name: review-script + mountPath: /scripts/comment.js + subPath: comment.js + source: | + set -eu + echo "[devops-gate] mode=${GATE_MODE} PR #${PR} β€” waiting for AWS DevOps Agent..." + DEADLINE=$(( $(date +%s) + WAIT_SECONDS )) + CLEARED=false + while [ "$(date +%s)" -lt "$DEADLINE" ]; do + if [ "$GATE_MODE" = "label" ]; then + # Fallback: coder-applied handoff label present on the PR. + R="$(GH_TOKEN="$GH_TOKEN" REPO="$REPO" PR="$PR" LABEL="$LABEL" node -e ' + const https=require("https");const {GH_TOKEN,REPO,PR,LABEL}=process.env; + https.get({host:"api.github.com",path:"/repos/"+REPO+"/issues/"+PR+"/labels",headers:{"User-Agent":"df-devops-gate","Authorization":"Bearer "+GH_TOKEN,"Accept":"application/vnd.github+json"}}, + r=>{let b="";r.on("data",c=>b+=c);r.on("end",()=>{try{const ls=JSON.parse(b).map(x=>x.name);console.log(ls.includes(LABEL)?"cleared":"waiting")}catch(e){console.log("waiting")}})}).on("error",()=>console.log("waiting"));' 2>/dev/null || echo waiting)" + else + # Default: the DevOps Agent GitHub App's check-run on the PR head SHA. + # cleared iff a matching context concluded success/neutral; "blocked" + # if it concluded failure (BLOCK) β€” we stop and leave cleared=false. + R="$(GH_TOKEN="$GH_TOKEN" REPO="$REPO" BRANCH="$BRANCH" CHECK_CONTEXT="$CHECK_CONTEXT" node -e ' + const https=require("https");const {GH_TOKEN,REPO,BRANCH,CHECK_CONTEXT}=process.env; + const g=(p)=>new Promise((res)=>{https.get({host:"api.github.com",path:p,headers:{"User-Agent":"df-devops-gate","Authorization":"Bearer "+GH_TOKEN,"Accept":"application/vnd.github+json"}},r=>{let b="";r.on("data",c=>b+=c);r.on("end",()=>{try{res(JSON.parse(b))}catch(e){res(null)}})}).on("error",()=>res(null))}); + (async()=>{ + const pr=await g("/repos/"+REPO+"/pulls?head="+REPO.split("/")[0]+":"+BRANCH+"&state=open"); + if(!pr||!pr[0]){return console.log("waiting")} + const sha=pr[0].head.sha; + const re=new RegExp(CHECK_CONTEXT,"i"); + // check-runs API + const cr=await g("/repos/"+REPO+"/commits/"+sha+"/check-runs"); + const runs=(cr&&cr.check_runs)||[]; + const m=runs.filter(x=>re.test(x.name)); + if(m.some(x=>x.status==="completed"&&["success","neutral"].includes(x.conclusion)))return console.log("cleared"); + if(m.some(x=>x.status==="completed"&&["failure","action_required","cancelled","timed_out"].includes(x.conclusion)))return console.log("blocked"); + // legacy commit statuses fallback + const st=await g("/repos/"+REPO+"/commits/"+sha+"/status"); + const ss=((st&&st.statuses)||[]).filter(x=>re.test(x.context)); + if(ss.some(x=>x.state==="success"))return console.log("cleared"); + if(ss.some(x=>["failure","error"].includes(x.state)))return console.log("blocked"); + console.log("waiting"); + })();' 2>/dev/null || echo waiting)" + fi + case "$R" in + cleared) CLEARED=true; echo "[devops-gate] AWS DevOps Agent CLEARED"; break ;; + blocked) CLEARED=false; echo "[devops-gate] AWS DevOps Agent returned BLOCK β€” Security stays gated"; break ;; + *) echo "[devops-gate] waiting for AWS DevOps Agent review..."; sleep 20 ;; + esac + done + echo "$CLEARED" > /tmp/cleared + echo "[devops-gate] cleared=${CLEARED}" + + # NOTE: the DevOps verdict is NO LONGER relayed as its own PR comment. + # To avoid mixed/duplicate signals, the pipeline posts a SINGLE + # consolidated review (status.js β†’ dark-factory:verdict-review) that + # mirrors the source-of-truth checks (Build/Holdout/Security/DevOps). + # The DevOps Agent's own commit status + the App bot's review remain + # the authoritative DevOps signal; `cleared` still gates the flow below. + outputs: + parameters: + - name: cleared + valueFrom: + path: /tmp/cleared + default: "false" + + # ---- 2d. security-agent (P3, step 2) β€” REAL AWS Security Agent, headless ---- + # Runs SECOND, only after DevOps cleared (label present). A trusted hub-side + # step that clones df/issue-N read-only, stages {source archive, unified diff} + # SINGLE SECURITY SIGNAL = THE REAL AWS SECURITY AGENT BOT. + # We do NOT run a second headless scan (that redundant path disagreed with the + # bot β€” reported "no findings" while aws-security-agent[bot] flagged real issues + # β€” and produced a false LGTM). Instead this step WAITS for the GitHub App bot's + # own review on the PR and MIRRORS its verdict into the dark-factory/security + # commit status (findings -> failure -> merge blocked; clean -> success). Because + # the consolidated `status` step depends on this step, the pipeline now naturally + # waits for the real bot before posting its verdict. Node-only β†’ runs on reviewImage. + - name: security-agent + activeDeadlineSeconds: {{ add .Values.securityAgent.pollTimeoutSeconds 300 }} + volumes: + - name: review-script + configMap: + name: df-review + script: + image: {{ .Values.reviewImage }} + command: [node] + env: + - name: GH_TOKEN + valueFrom: + secretKeyRef: + name: {{ .Values.github.tokenSecret }} + key: {{ .Values.github.tokenKey }} + - name: REPO + value: "{{`{{workflow.parameters.repo}}`}}" + - name: BRANCH + value: "df/issue-{{`{{workflow.parameters.issue-number}}`}}" + # Findings at/above this severity fail the status (block the merge). The bot + # doesn't expose per-severity counts uniformly, so ANY finding fails unless + # BLOCK_LEVEL=none (then findings are advisory). Default medium. + - name: BLOCK_LEVEL + value: {{ .Values.securityAgent.blockLevel | quote }} + - name: POLL_TIMEOUT + value: {{ .Values.securityAgent.pollTimeoutSeconds | quote }} + volumeMounts: + - name: review-script + mountPath: /scripts/security-wait.js + subPath: security-wait.js + source: | + require("/scripts/security-wait.js"); +{{- end }} + +{{- if .Values.deployTest.enabled }} + # ---- 2d. detect-deployable (P4) β€” does the diff touch deployable artifacts? ---- + # Emits `deployable = true|false`, which gates the expensive deploy-test step. + # Uses the GitHub compare API (authoritative changed-file list) rather than a + # local git diff β€” a depth-1 clone + depth-1 base fetch share no merge-base, so + # `origin/BASE...HEAD` yields nothing (observed: empty file list β†’ false). + - name: detect-deployable + script: + image: {{ .Values.reviewImage }} + command: [bash] + env: + - name: GH_TOKEN + valueFrom: + secretKeyRef: + name: {{ .Values.github.tokenSecret }} + key: {{ .Values.github.tokenKey }} + - name: REPO + value: "{{`{{workflow.parameters.repo}}`}}" + - name: BRANCH + value: "df/issue-{{`{{workflow.parameters.issue-number}}`}}" + - name: BASE + value: "{{`{{workflow.parameters.base-branch}}`}}" + - name: K8S_PATTERNS + value: {{ .Values.deployTest.k8sPatterns | quote }} + - name: TF_PATTERNS + value: {{ .Values.deployTest.terraformPatterns | quote }} + source: | + set -eu + FILES="$(GH_TOKEN="$GH_TOKEN" REPO="$REPO" BASE="$BASE" BRANCH="$BRANCH" node -e ' + const https=require("https"); + const {GH_TOKEN,REPO,BASE,BRANCH}=process.env; + https.get({host:"api.github.com",path:"/repos/"+REPO+"/compare/"+BASE+"..."+BRANCH,headers:{"User-Agent":"df-detect","Authorization":"Bearer "+GH_TOKEN,"Accept":"application/vnd.github+json"}}, + r=>{let b="";r.on("data",c=>b+=c);r.on("end",()=>{try{const j=JSON.parse(b);(j.files||[]).forEach(f=>console.log(f.filename));}catch(e){process.exit(0);}});}).on("error",()=>process.exit(0)); + ')" + echo "[detect] changed files:"; echo "$FILES" | sed 's/^/ /' + # Classify: k8s takes precedence over terraform when both are present. + if echo "$FILES" | grep -qE "$K8S_PATTERNS"; then KIND=k8s + elif echo "$FILES" | grep -qE "$TF_PATTERNS"; then KIND=terraform + else KIND=none; fi + echo "$KIND" > /tmp/kind + echo "[detect] kind=$KIND" + outputs: + parameters: + - name: kind + valueFrom: + path: /tmp/kind + default: "none" + + # ---- 2e. deploy-test (P4) β€” content-aware; THE ONLY step with K8s access ---- + # Trusted hub step. Validates the change with the RIGHT tool for its kind: + # kind=k8s β†’ ephemeral namespace apply + wait Ready + teardown (trap). + # kind=terraform β†’ terraform init -backend=false + validate (+ fmt check). + # Validation only β€” NO AWS creds, NO apply (no real infra). + # Posts the dark-factory/deploy-test commit status AND a marker PR comment with + # the details. Advisory in v1 (deployTest.blocking=false). The untrusted coder + # never has K8s access β€” it only produces the artifacts; this step runs them. + - name: deploy-test + inputs: + parameters: + - name: kind + activeDeadlineSeconds: {{ .Values.deployTest.timeoutSeconds }} + volumes: + - name: review-script + configMap: + name: df-review + script: + image: {{ .Values.deployTest.image }} + command: [bash] + volumeMounts: + - name: review-script + mountPath: /scripts/comment.js + subPath: comment.js + - name: review-script + mountPath: /scripts/deploy-test.sh + subPath: deploy-test.sh + env: + - name: WF_NAME + value: "{{`{{workflow.name}}`}}" + - name: GH_TOKEN + valueFrom: + secretKeyRef: + name: {{ .Values.github.tokenSecret }} + key: {{ .Values.github.tokenKey }} + - name: REPO + value: "{{`{{workflow.parameters.repo}}`}}" + - name: BRANCH + value: "df/issue-{{`{{workflow.parameters.issue-number}}`}}" + - name: ISSUE_NUMBER + value: "{{`{{workflow.parameters.issue-number}}`}}" + - name: KIND + value: "{{`{{inputs.parameters.kind}}`}}" + - name: MANIFEST_PATH + value: {{ .Values.deployTest.manifestPath | quote }} + - name: TF_PATH + value: {{ .Values.deployTest.terraformPath | quote }} + - name: READY_TIMEOUT + value: {{ .Values.deployTest.readyTimeoutSeconds | quote }} + - name: BLOCKING + value: {{ .Values.deployTest.blocking | quote }} + source: | + set -eu + # All logic lives in review/deploy-test.sh (ConfigMap file) β€” keeping it out + # of this YAML block scalar avoids the shell-quoting hazards that broke + # inline multi-line markdown. Kind-driven so it generalizes to new profiles. + bash /scripts/deploy-test.sh +{{- end }} + + # ---- 3. Sticky status β€” rewrite the PR body from the live verdicts ---- + # Runs AFTER every verify step (its DAG deps). The coder wrote the PR body at + # PR-open time, before verification ran, so its holdout/security/devops lines + # are placeholders ("pending"). This step reads the authoritative + # dark-factory/* commit STATUSES from GitHub (the source of truth the verify + # steps posted) and rewrites the PR body in place with the real verdicts β€” the + # "one live sticky status" from the design (README Β§7). Idempotent: the body + # between the markers is fully regenerated each run. + - name: sticky-status + inputs: + parameters: + - name: phase + volumes: + - name: review-script + configMap: + name: df-review + script: + image: {{ .Values.reviewImage }} + command: [bash] + env: + - name: GH_TOKEN + valueFrom: + secretKeyRef: + name: {{ .Values.github.tokenSecret }} + key: {{ .Values.github.tokenKey }} + - name: REPO + value: "{{`{{workflow.parameters.repo}}`}}" + - name: BRANCH + value: "df/issue-{{`{{workflow.parameters.issue-number}}`}}" + # For the AUTO-FIX loop (status.js): on a ❌ verdict from the real agents, + # collect their findings and submit a bounded df-run revision (iterate-note + # = the findings) so the coder auto-revises β€” no human paraphrasing needed. + - name: ISSUE_NUMBER + value: "{{`{{workflow.parameters.issue-number}}`}}" + - name: BASE_BRANCH + value: "{{`{{workflow.parameters.base-branch}}`}}" + - name: TRIGGER_LABEL + value: "{{`{{workflow.parameters.trigger-label}}`}}" + - name: ARGO_NAMESPACE + value: {{ .Values.argo.namespace | quote }} + - name: AUTO_FIX_FINDINGS + value: {{ .Values.review.autoFixFindings | default false | quote }} + - name: MAX_ITERATIONS + value: {{ .Values.iterate.maxIterations | default 3 | quote }} +{{- if .Values.devopsAgent.enabled }} + # Real AWS DevOps Agent posts a check-run (not a commit status) named this; + # status.js renders the DevOps row from the check-run when present. + - name: DEVOPS_CHECK + value: {{ .Values.devopsAgent.checkRunName | quote }} +{{- end }} +{{- if and .Values.securityAgent.app .Values.securityAgent.app.enabled }} + # Real AWS Security Agent GitHub App posts its own check/inline review; + # status.js renders the Security row from it when present (else the + # headless dark-factory/security status). + - name: SECURITY_CHECK + value: {{ .Values.securityAgent.app.checkRunName | quote }} +{{- end }} +{{- if .Values.postVerdictReview }} + # Post ONE consolidated verdict review (Security + DevOps results) on the + # PR so both agents' verdicts are ALWAYS visible in the Reviews section β€” + # the agent Apps review autonomously + inconsistently and cannot be added + # via the requested_reviewers API (verified no-op). status.js posts it once, + # when verification is terminal (idempotent via a hidden marker). + - name: POST_VERDICT_REVIEW + value: "true" +{{- end }} + # Holdout is a train/test QUALITY signal, advisory by default β€” it does NOT + # gate the merge verdict unless holdout.blocking=true. status.js reads this + # to decide whether a red holdout flips the consolidated verdict. + - name: HOLDOUT_BLOCKING + value: {{ .Values.holdout.blocking | quote }} + volumeMounts: + - name: review-script + mountPath: /scripts/status.js + subPath: status.js + source: | + set -eu + echo "[df-run] phase={{`{{inputs.parameters.phase}}`}} β€” updating PR body from live verdicts (${REPO} ${BRANCH})" + node /scripts/status.js + + # ---- suspend/resume the Lambda MicroVM directly (scale-to-zero) ---- + # Calls suspend-microvm / resume-microvm on the VM the provision step created (id read + # from the Microvm CR by stable name mvm-). Direct AWS call β€” NO Sandbox/ + # operatingMode/lifecycle-controller dependency (this template owns the VM outright). + # Advisory: never fails the run (suspend is a cost optimization, not correctness). + - name: set-microvm-power + inputs: + parameters: + - name: action # suspend | resume + script: + image: {{ .Values.microvm.stepImage | default "public.ecr.aws/aws-cli/aws-cli:latest" }} + command: [sh] + source: | + set -eu + ACTION="{{`{{inputs.parameters.action}}`}}" + REGION="{{ .Values.microvm.region }}" + NS="{{ .Values.microvm.namespace | default "agent-sandbox-system" }}" + MVM="mvm-{{`{{workflow.parameters.issue-number}}`}}" + if ! command -v kubectl >/dev/null 2>&1; then + KV="$(curl -fsSL https://dl.k8s.io/release/stable.txt)" + curl -fsSL "https://dl.k8s.io/release/${KV}/bin/linux/amd64/kubectl" -o /tmp/kubectl + chmod +x /tmp/kubectl; export PATH="/tmp:$PATH" + fi + VMID=$(kubectl get microvm "${MVM}" -n "${NS}" -o jsonpath='{.status.microvmID}' 2>/dev/null || echo "") + [ -z "${VMID}" ] && { echo "[power] no microvmID for ${MVM} β€” skipping (advisory)"; exit 0; } + echo "[power] ${ACTION}-microvm ${VMID}" + aws lambda-microvms "${ACTION}-microvm" --region "${REGION}" --microvm-identifier "${VMID}" 2>&1 || echo "[power] ${ACTION} skipped/failed (advisory)" + echo "[power] done." + activeDeadlineSeconds: 180 + + # ---- onExit: KEEP the suspended VM (terminated only at merge) ---- + # Flow D lifecycle: the VM is SUSPENDED (by suspend-microvm above) and must SURVIVE + # the reviewβ†’fix loop so a fix round can RESUME the SAME VM. The Microvm CR is + # therefore NOT deleted here β€” it is deleted at merge by df-merge-teardown, which + # triggers the controller's TerminateMicrovm. This onExit is a no-op guard: if the + # run FAILED before ever creating/suspending a VM, there's nothing to keep, but we + # still never delete on the normal path (the suspended VM is the whole point). + - name: teardown + script: + image: {{ .Values.microvm.stepImage | default "public.ecr.aws/aws-cli/aws-cli:latest" }} + command: [sh] + source: | + set -eu + MVM="mvm-{{`{{workflow.parameters.issue-number}}`}}" + echo "[teardown] lambda: KEEPING Microvm/${MVM} (stays SUSPENDED until merge β†’ df-merge-teardown terminates it)." + exit 0 diff --git a/gitops/addons/charts/dark-factory/templates/40-eventbus.yaml b/gitops/addons/charts/dark-factory/templates/40-eventbus.yaml new file mode 100644 index 00000000..5615b40d --- /dev/null +++ b/gitops/addons/charts/dark-factory/templates/40-eventbus.yaml @@ -0,0 +1,23 @@ +{{- if .Values.trigger.argoEvents.enabled }} +{{- /* +EventBus β€” the NATS JetStream backbone the EventSource + Sensor communicate over. +The appmod-blueprints argo-events addon installs the controller + CRDs but no +EventBus instance, so Flow B provides its own (namespaced to argo-events, the +default bus name every EventSource/Sensor uses unless overridden). +*/ -}} +apiVersion: argoproj.io/v1alpha1 +kind: EventBus +metadata: + name: default + namespace: {{ .Values.trigger.argoEvents.namespace }} + labels: + {{- include "dark-factory.labels" . | nindent 4 }} +spec: + jetstream: + # version MUST match an entry in the argo-events controller-config + # jetstream.versions list (verified live: 'latest' β†’ nats:2.10.10). Without + # it the controller rejects the EventBus: "a version for jetstream needs to + # be specified". + version: {{ .Values.trigger.argoEvents.jetstreamVersion | default "latest" | quote }} + replicas: {{ .Values.trigger.argoEvents.eventbusReplicas | default 3 }} +{{- end }} diff --git a/gitops/addons/charts/dark-factory/templates/41-eventsource-github.yaml b/gitops/addons/charts/dark-factory/templates/41-eventsource-github.yaml new file mode 100644 index 00000000..c14c4600 --- /dev/null +++ b/gitops/addons/charts/dark-factory/templates/41-eventsource-github.yaml @@ -0,0 +1,52 @@ +{{- if .Values.trigger.argoEvents.enabled }} +{{- /* +GitHub webhook EventSource β€” receives GitHub issue/PR webhooks and publishes +them onto the EventBus for the Sensor to act on. Runs in the argo-events +namespace. Exposed via the platform ALB ingress so GitHub can reach it; the +webhook secret validates the X-Hub-Signature-256 HMAC. +*/ -}} +apiVersion: argoproj.io/v1alpha1 +kind: EventSource +metadata: + name: dark-factory-github + namespace: {{ .Values.trigger.argoEvents.namespace }} + labels: + {{- include "dark-factory.labels" . | nindent 4 }} +spec: + service: + ports: + - port: {{ .Values.trigger.argoEvents.webhookPort | default 12000 }} + targetPort: {{ .Values.trigger.argoEvents.webhookPort | default 12000 }} + github: + dark-factory: + repositories: + {{- range .Values.trigger.argoEvents.repositories }} + - owner: {{ .owner | quote }} + names: + {{- range .names }} + - {{ . | quote }} + {{- end }} + {{- end }} + webhook: + endpoint: {{ .Values.trigger.argoEvents.webhookEndpoint | default "/dark-factory" | quote }} + port: {{ .Values.trigger.argoEvents.webhookPort | default 12000 | quote }} + method: POST + url: {{ .Values.trigger.argoEvents.webhookUrl | quote }} + # The events Flow B reacts to (P1 uses issues; iterate/merge come in P2+). + events: + - issues + - pull_request + - pull_request_review + - issue_comment + # Validate GitHub's HMAC signature; secret + API token from a k8s Secret. + webhookSecret: + name: {{ .Values.trigger.argoEvents.githubSecret }} + key: webhook-secret + apiToken: + name: {{ .Values.trigger.argoEvents.githubSecret }} + key: token + # Let Argo Events auto-register the webhook on the repo. + active: true + contentType: json + insecure: false +{{- end }} diff --git a/gitops/addons/charts/dark-factory/templates/42-sensor.yaml b/gitops/addons/charts/dark-factory/templates/42-sensor.yaml new file mode 100644 index 00000000..0e569ff8 --- /dev/null +++ b/gitops/addons/charts/dark-factory/templates/42-sensor.yaml @@ -0,0 +1,369 @@ +{{- if .Values.trigger.argoEvents.enabled }} +{{- /* +Sensor β€” the Flow B trigger brain. Subscribes to the GitHub EventSource on the +EventBus, filters for the events Flow B acts on, and submits the matching +Argo Workflow. P1 wires the issue-labeled β†’ df-run path; the iterate / merge +triggers (PR comment, review approved) are stubbed for P2+. + +The Sensor's own ServiceAccount needs to CREATE Workflows in the argo namespace +(and read WorkflowTemplates). That SA + RBAC are defined below. +*/ -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: dark-factory-sensor + namespace: {{ .Values.trigger.argoEvents.namespace }} + labels: + {{- include "dark-factory.labels" . | nindent 4 }} +--- +# The Sensor submits Workflows into the argo namespace β†’ needs create there. +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: dark-factory-sensor + namespace: {{ .Values.argo.namespace }} + labels: + {{- include "dark-factory.labels" . | nindent 4 }} +rules: + - apiGroups: ["argoproj.io"] + resources: ["workflows"] + verbs: ["create", "get", "list", "watch"] + - apiGroups: ["argoproj.io"] + resources: ["workflowtemplates"] + verbs: ["get", "list"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: dark-factory-sensor + namespace: {{ .Values.argo.namespace }} + labels: + {{- include "dark-factory.labels" . | nindent 4 }} +subjects: + - kind: ServiceAccount + name: dark-factory-sensor + namespace: {{ .Values.trigger.argoEvents.namespace }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: dark-factory-sensor +--- +apiVersion: argoproj.io/v1alpha1 +kind: Sensor +metadata: + name: dark-factory + namespace: {{ .Values.trigger.argoEvents.namespace }} + labels: + {{- include "dark-factory.labels" . | nindent 4 }} +spec: + template: + serviceAccountName: dark-factory-sensor + dependencies: + # TWO label dependencies β€” one per substrate β€” so each fires a DIFFERENT + # WorkflowTemplate (Argo Events trigger conditions can only reference dependency + # NAMES, not field values, so routing by label β‡’ separate dependencies + triggers): + # issue-labeled-kata β†’ df-run (certified Kata pipeline) + # issue-labeled-lambda β†’ df-run-lambda (Flow D MicroVM-native pipeline) + - name: issue-labeled-kata + eventSourceName: dark-factory-github + eventName: dark-factory + filters: + data: + - path: headers.X-Github-Event + type: string + value: ["issues"] + - path: body.action + type: string + value: ["labeled"] + - path: body.label.name + type: string + value: ["dark-factory"] + - name: issue-labeled-lambda + eventSourceName: dark-factory-github + eventName: dark-factory + filters: + data: + - path: headers.X-Github-Event + type: string + value: ["issues"] + - path: body.action + type: string + value: ["labeled"] + - path: body.label.name + type: string + value: ["darkfactory-lambda"] + - name: pr-approved + eventSourceName: dark-factory-github + eventName: dark-factory + filters: + data: + # A human APPROVED a review on a df/issue-N PR β†’ merge + teardown. + - path: headers.X-Github-Event + type: string + value: ["pull_request_review"] + - path: body.action + type: string + value: ["submitted"] + - path: body.review.state + type: string + value: ["approved"] + - path: body.pull_request.head.ref + type: string + # Only Dark Factory branches (df/issue-); string values are regex. + value: ["df/issue-.*"] +{{- if .Values.iterate.enabled }} + - name: pr-commented + eventSourceName: dark-factory-github + eventName: dark-factory + filters: + data: + # A human commented on a PR β†’ bounded df-iterate. issue_comment fires for + # both issues and PRs; require the PR link. + - path: headers.X-Github-Event + type: string + value: ["issue_comment"] + - path: body.action + type: string + value: ["created"] + - path: body.issue.pull_request.url + type: string + value: ["https://.*"] + - path: body.comment.user.type + type: string + value: ["User"] + # SELF-TRIGGER GUARD (important): the factory's OWN comments (sticky status, + # findings, iteration notices) are posted with a real PAT β†’ user.type="User", + # so the data check above does NOT exclude them. Each such comment would + # otherwise CREATE a df-iterate workflow (even though iterate.js then no-ops on + # the marker) β€” noisy runaway of empty workflows. So we ALSO exclude them HERE, + # at the sensor, with an EXPRESSION filter (expr-lang, argo-events >=1.9): + # drop any comment whose body contains a "dark-factory:" marker, so no workflow + # is even created. iterate.js keeps the marker + bot + identity guards as + # defense-in-depth (this expr is the primary stop). + exprs: + - expr: 'body_comment_body matches "(?s).*" && !(body_comment_body contains "dark-factory:")' + fields: + - name: body_comment_body + path: body.comment.body +{{- end }} + triggers: + # ---- KATA: dark-factory label β†’ df-run (certified pipeline) ---- + - template: + name: submit-df-run + conditions: "issue-labeled-kata" + argoWorkflow: + operation: submit + source: + resource: + apiVersion: argoproj.io/v1alpha1 + kind: Workflow + metadata: + # DEDUP: deterministic name keyed on the GitHub issue id (globally + # unique). GitHub delivers the `labeled` webhook more than once + # (delivery retries + rapid re-labels), so without this each + # delivery spawned a competing df-run that force-pushed its own + # commit and split the commit statuses across SHAs. With a fixed + # name, the 2nd+ submit collides (AlreadyExists) and is a harmless + # no-op β€” one issue = one in-flight run. + name: df-run-pending + namespace: {{ .Values.argo.namespace }} + spec: + workflowTemplateRef: + name: df-run + arguments: + parameters: + - name: issue-id + - name: issue-number + - name: repo + - name: issue-title + - name: issue-body + - name: base-branch + - name: trigger-label + parameters: + - src: + dependencyName: issue-labeled-kata + dataTemplate: "df-run-{{ `{{ .Input.body.issue.id | int64 }}` }}" + dest: metadata.name + - src: + dependencyName: issue-labeled-kata + dataKey: body.issue.id + dest: spec.arguments.parameters.0.value + - src: + dependencyName: issue-labeled-kata + dataKey: body.issue.number + dest: spec.arguments.parameters.1.value + - src: + dependencyName: issue-labeled-kata + dataKey: body.repository.full_name + dest: spec.arguments.parameters.2.value + - src: + dependencyName: issue-labeled-kata + dataKey: body.issue.title + dest: spec.arguments.parameters.3.value + - src: + dependencyName: issue-labeled-kata + dataKey: body.issue.body + dest: spec.arguments.parameters.4.value + - src: + dependencyName: issue-labeled-kata + dataKey: body.repository.default_branch + dest: spec.arguments.parameters.5.value + - src: + dependencyName: issue-labeled-kata + dataKey: body.label.name + dest: spec.arguments.parameters.6.value + + # ---- LAMBDA: darkfactory-lambda label β†’ df-run-lambda (Flow D MicroVM) ---- + - template: + name: submit-df-run-lambda + conditions: "issue-labeled-lambda" + argoWorkflow: + operation: submit + source: + resource: + apiVersion: argoproj.io/v1alpha1 + kind: Workflow + metadata: + name: df-run-lambda-pending + namespace: {{ .Values.argo.namespace }} + spec: + workflowTemplateRef: + name: df-run-lambda + arguments: + parameters: + - name: issue-id + - name: issue-number + - name: repo + - name: issue-title + - name: issue-body + - name: base-branch + - name: trigger-label + parameters: + # Dedup key = df-run-lambda- (stable across first run + fix + # rounds β†’ one workflow name per issue, matching the mvm- CR). + - src: + dependencyName: issue-labeled-lambda + dataTemplate: "df-run-lambda-{{ `{{ .Input.body.issue.number }}` }}" + dest: metadata.name + - src: + dependencyName: issue-labeled-lambda + dataKey: body.issue.id + dest: spec.arguments.parameters.0.value + - src: + dependencyName: issue-labeled-lambda + dataKey: body.issue.number + dest: spec.arguments.parameters.1.value + - src: + dependencyName: issue-labeled-lambda + dataKey: body.repository.full_name + dest: spec.arguments.parameters.2.value + - src: + dependencyName: issue-labeled-lambda + dataKey: body.issue.title + dest: spec.arguments.parameters.3.value + - src: + dependencyName: issue-labeled-lambda + dataKey: body.issue.body + dest: spec.arguments.parameters.4.value + - src: + dependencyName: issue-labeled-lambda + dataKey: body.repository.default_branch + dest: spec.arguments.parameters.5.value + - src: + dependencyName: issue-labeled-lambda + dataKey: body.label.name + dest: spec.arguments.parameters.6.value + + # ---- PR review approved β†’ df-merge-teardown (the ONLY merge path) ---- + - template: + name: submit-df-merge + conditions: "pr-approved" + argoWorkflow: + operation: submit + source: + resource: + apiVersion: argoproj.io/v1alpha1 + kind: Workflow + metadata: + # Dedup per PR head SHA so repeated approvals don't double-merge. + name: df-merge-pending + namespace: {{ .Values.argo.namespace }} + spec: + workflowTemplateRef: + name: df-merge-teardown + arguments: + parameters: + - name: issue-number + - name: repo + - name: pr-number + parameters: + # Name = df-merge- (unique per reviewed commit). + - src: + dependencyName: pr-approved + dataTemplate: "df-merge-{{ `{{ .Input.body.pull_request.head.sha | trunc 12 }}` }}" + dest: metadata.name + # issue-number parsed from the branch ref df/issue-. + - src: + dependencyName: pr-approved + dataTemplate: "{{ `{{ .Input.body.pull_request.head.ref | trimPrefix \"df/issue-\" }}` }}" + dest: spec.arguments.parameters.0.value + - src: + dependencyName: pr-approved + dataKey: body.repository.full_name + dest: spec.arguments.parameters.1.value + - src: + dependencyName: pr-approved + dataKey: body.pull_request.number + dest: spec.arguments.parameters.2.value +{{- if .Values.iterate.enabled }} + # ---- PR comment β†’ df-iterate (bounded revision loop) ---- + - template: + name: submit-df-iterate + conditions: "pr-commented" + argoWorkflow: + operation: submit + source: + resource: + apiVersion: argoproj.io/v1alpha1 + kind: Workflow + metadata: + # Dedup per comment id so a duplicate webhook is a no-op. + name: df-iterate-pending + namespace: {{ .Values.argo.namespace }} + spec: + workflowTemplateRef: + name: df-iterate + arguments: + parameters: + - name: repo + - name: pr-number + - name: comment-body + - name: comment-id + - name: comment-author + parameters: + - src: + dependencyName: pr-commented + dataTemplate: "df-iterate-{{ `{{ .Input.body.comment.id | int64 }}` }}" + dest: metadata.name + - src: + dependencyName: pr-commented + dataKey: body.repository.full_name + dest: spec.arguments.parameters.0.value + - src: + dependencyName: pr-commented + dataKey: body.issue.number + dest: spec.arguments.parameters.1.value + - src: + dependencyName: pr-commented + dataKey: body.comment.body + dest: spec.arguments.parameters.2.value + - src: + dependencyName: pr-commented + dataKey: body.comment.id + dest: spec.arguments.parameters.3.value + - src: + dependencyName: pr-commented + dataKey: body.comment.user.login + dest: spec.arguments.parameters.4.value +{{- end }} +{{- end }} diff --git a/gitops/addons/charts/dark-factory/templates/43-eventsource-ingress.yaml b/gitops/addons/charts/dark-factory/templates/43-eventsource-ingress.yaml new file mode 100644 index 00000000..bb66aa27 --- /dev/null +++ b/gitops/addons/charts/dark-factory/templates/43-eventsource-ingress.yaml @@ -0,0 +1,31 @@ +{{- if and .Values.trigger.argoEvents.enabled .Values.trigger.argoEvents.ingress.enabled .Values.trigger.argoEvents.ingress.host }} +{{- /* +Ingress for the GitHub webhook EventSource β€” routes {host}/dark-factory to the +EventSource service so GitHub webhooks reach it. Mirrors the platform ALB +pattern used by argo-workflows. Only rendered when an ingress host is set. +*/ -}} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: dark-factory-webhook + namespace: {{ .Values.trigger.argoEvents.namespace }} + labels: + {{- include "dark-factory.labels" . | nindent 4 }} + annotations: + alb.ingress.kubernetes.io/target-type: ip + alb.ingress.kubernetes.io/listen-ports: '[{"HTTPS":443}]' + alb.ingress.kubernetes.io/ssl-redirect: '443' +spec: + ingressClassName: platform + rules: + - host: {{ .Values.trigger.argoEvents.ingress.host | quote }} + http: + paths: + - path: {{ .Values.trigger.argoEvents.ingress.path | default "/dark-factory" }} + pathType: Prefix + backend: + service: + name: dark-factory-github-eventsource-svc + port: + number: {{ .Values.trigger.argoEvents.webhookPort | default 12000 }} +{{- end }} diff --git a/gitops/addons/charts/dark-factory/templates/50-holdout-configmap.yaml b/gitops/addons/charts/dark-factory/templates/50-holdout-configmap.yaml new file mode 100644 index 00000000..10424d5a --- /dev/null +++ b/gitops/addons/charts/dark-factory/templates/50-holdout-configmap.yaml @@ -0,0 +1,43 @@ +{{- /* +Holdout content β€” the hidden scenarios + executable tests + the evaluator, kept +in a ConfigMap ON THE HUB in the argo namespace. This is the WHOLE POINT of the +holdout gate (docs/dark-factory Β§6.1): the coder can neither read nor write it. +It lives in the argo namespace next to the workflow, is mounted ONLY into the +hub-side holdout-gate step, and is NEVER projected into the Kata VM sandbox (the +coder has no k8s API access and only ever clones the target repo). + +One ConfigMap per target repo (keyed by owner-name), plus the shared evaluate.js. +Rendered from files under charts/dark-factory/holdout/. +*/ -}} +{{- if .Values.holdout.enabled }} +apiVersion: v1 +kind: ConfigMap +metadata: + name: df-holdout-eval + namespace: {{ .Values.argo.namespace }} + labels: + {{- include "dark-factory.labels" . | nindent 4 }} +data: + evaluate.js: | +{{ .Files.Get "holdout/evaluate.js" | indent 4 }} +{{- range $repo := .Values.trigger.argoEvents.repositories }} +{{- range $name := $repo.names }} +{{- $slug := printf "%s-%s" $repo.owner $name }} +{{- $dir := printf "holdout/%s" $slug }} +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: df-holdout-{{ $slug | lower }} + namespace: {{ $.Values.argo.namespace }} + labels: + {{- include "dark-factory.labels" $ | nindent 4 }} + dark-factory.io/holdout-repo: {{ $slug | lower | quote }} +data: + scenarios.json: | +{{ $.Files.Get (printf "%s/scenarios.json" $dir) | indent 4 }} + rubric.md: | +{{ $.Files.Get (printf "%s/rubric.md" $dir) | indent 4 }} +{{- end }} +{{- end }} +{{- end }} diff --git a/gitops/addons/charts/dark-factory/templates/51-scripts-configmap.yaml b/gitops/addons/charts/dark-factory/templates/51-scripts-configmap.yaml new file mode 100644 index 00000000..b83d9676 --- /dev/null +++ b/gitops/addons/charts/dark-factory/templates/51-scripts-configmap.yaml @@ -0,0 +1,34 @@ +{{- /* +Hub-side scripts for df-run, in a ConfigMap in the argo namespace, mounted into +the workflow steps (never the Kata VM): + - security-wait.js β€” WAITS FOR + mirrors the real aws-security-agent[bot] + verdict into dark-factory/security (no 2nd scan). + - bootstrap-agentspace.sh β€” idempotent agent-space/application reconcile (PreSync). + - status.js β€” sticky-status updater (rewrites the PR body). + - comment.js / merge.js / iterate.js / deploy-test.sh β€” lifecycle helpers. +Always rendered: the sticky-status step always runs and needs status.js. +NOTE: the old review.js (linters+Nova stub) was REMOVED β€” reviews are now the +real AWS DevOps + Security Agents (docs Β§6.2), never a cooked-up reviewer. +*/ -}} +apiVersion: v1 +kind: ConfigMap +metadata: + name: df-review + namespace: {{ .Values.argo.namespace }} + labels: + {{- include "dark-factory.labels" . | nindent 4 }} +data: + status.js: | +{{ .Files.Get "scripts/status.js" | indent 4 }} + merge.js: | +{{ .Files.Get "scripts/merge.js" | indent 4 }} + iterate.js: | +{{ .Files.Get "scripts/iterate.js" | indent 4 }} + comment.js: | +{{ .Files.Get "scripts/comment.js" | indent 4 }} + deploy-test.sh: | +{{ .Files.Get "scripts/deploy-test.sh" | indent 4 }} + bootstrap-agentspace.sh: | +{{ .Files.Get "scripts/bootstrap-agentspace.sh" | indent 4 }} + security-wait.js: | +{{ .Files.Get "scripts/security-wait.js" | indent 4 }} diff --git a/gitops/addons/charts/dark-factory/templates/60-reaper-cronjob.yaml b/gitops/addons/charts/dark-factory/templates/60-reaper-cronjob.yaml new file mode 100644 index 00000000..16569ffb --- /dev/null +++ b/gitops/addons/charts/dark-factory/templates/60-reaper-cronjob.yaml @@ -0,0 +1,111 @@ +{{- /* +Reaper CronJob (P4) β€” the crash-net. Argo onExit + claim TTL handle the normal +teardown paths; this sweeps what slips through: + - ephemeral deploy-test namespaces (dark-factory.io/ephemeral=true) older than + the reap window (a deploy-test that died before its trap ran), and + - SandboxClaims managed by df-run that are older than the reap window with no + live df-run workflow (abandoned/crashed runs). +Runs as a dedicated SA scoped to exactly those deletes. Belt-and-suspenders to +the per-run onExit teardown and the claim's own ttlSecondsAfterFinished. +*/ -}} +{{- if .Values.reaper.enabled }} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: dark-factory-reaper + namespace: {{ .Values.argo.namespace }} + labels: + {{- include "dark-factory.labels" . | nindent 4 }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: dark-factory-reaper + labels: + {{- include "dark-factory.labels" . | nindent 4 }} +rules: + - apiGroups: [""] + resources: ["namespaces"] + verbs: ["get", "list", "delete"] + - apiGroups: ["extensions.agents.x-k8s.io"] + resources: ["sandboxclaims"] + verbs: ["get", "list", "delete"] + - apiGroups: ["argoproj.io"] + resources: ["workflows"] + verbs: ["get", "list"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: dark-factory-reaper + labels: + {{- include "dark-factory.labels" . | nindent 4 }} +subjects: + - kind: ServiceAccount + name: dark-factory-reaper + namespace: {{ .Values.argo.namespace }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: dark-factory-reaper +--- +apiVersion: batch/v1 +kind: CronJob +metadata: + name: dark-factory-reaper + namespace: {{ .Values.argo.namespace }} + labels: + {{- include "dark-factory.labels" . | nindent 4 }} +spec: + schedule: {{ .Values.reaper.schedule | quote }} + concurrencyPolicy: Forbid + successfulJobsHistoryLimit: 1 + failedJobsHistoryLimit: 2 + jobTemplate: + spec: + backoffLimit: 1 + activeDeadlineSeconds: 300 + template: + metadata: + labels: + {{- include "dark-factory.labels" . | nindent 12 }} + spec: + serviceAccountName: dark-factory-reaper + restartPolicy: Never + securityContext: + runAsNonRoot: true + runAsUser: 65532 + containers: + - name: reaper + image: {{ .Values.reaper.image }} + command: [bash, -c] + env: + - name: REAP_AFTER_SECONDS + value: {{ .Values.reaper.reapAfterSeconds | quote }} + - name: CLAIM_NS + value: {{ .Values.warmPool.namespace }} + - name: ARGO_NS + value: {{ .Values.argo.namespace }} + args: + - | + set -eu + NOW=$(date +%s) + CUTOFF=$((NOW - REAP_AFTER_SECONDS)) + echo "[reaper] cutoff=$(date -d @${CUTOFF} 2>/dev/null || echo ${CUTOFF}) β€” reaping older than ${REAP_AFTER_SECONDS}s" + + # 1) Stale ephemeral deploy-test namespaces. + for ns in $(kubectl get ns -l dark-factory.io/ephemeral=true -o jsonpath='{range .items[*]}{.metadata.name}={.metadata.creationTimestamp}{"\n"}{end}' 2>/dev/null); do + name="${ns%%=*}"; ts="${ns##*=}" + created=$(date -d "$ts" +%s 2>/dev/null || echo "$NOW") + if [ "$created" -lt "$CUTOFF" ]; then echo "[reaper] deleting stale ns $name (age $(( (NOW-created)/60 ))m)"; kubectl delete ns "$name" --wait=false >/dev/null 2>&1 || true; fi + done + + # 2) Abandoned df-run SandboxClaims with no live df-run workflow. + LIVE=$(kubectl -n "$ARGO_NS" get workflows -l workflows.argoproj.io/phase=Running -o jsonpath='{range .items[*]}{.metadata.name}{"\n"}{end}' 2>/dev/null || echo "") + for c in $(kubectl -n "$CLAIM_NS" get sandboxclaims -l dark-factory.io/managed-by=df-run -o jsonpath='{range .items[*]}{.metadata.name}={.metadata.creationTimestamp}{"\n"}{end}' 2>/dev/null); do + name="${c%%=*}"; ts="${c##*=}" + created=$(date -d "$ts" +%s 2>/dev/null || echo "$NOW") + if [ "$created" -lt "$CUTOFF" ]; then echo "[reaper] deleting abandoned claim $name (age $(( (NOW-created)/60 ))m)"; kubectl -n "$CLAIM_NS" delete sandboxclaim "$name" --wait=false >/dev/null 2>&1 || true; fi + done + echo "[reaper] done" +{{- end }} diff --git a/gitops/addons/charts/dark-factory/templates/70-metrics-service.yaml b/gitops/addons/charts/dark-factory/templates/70-metrics-service.yaml new file mode 100644 index 00000000..cc8f0971 --- /dev/null +++ b/gitops/addons/charts/dark-factory/templates/70-metrics-service.yaml @@ -0,0 +1,102 @@ +{{- /* +Argo workflow-controller metrics β†’ AMP (observability). + +The platform's AWS Managed Prometheus scraper (peeks-scraper-hub) discovers Services +annotated prometheus.io/scrape via its kubernetes-service-endpoints job. BUT the Argo +workflow-controller serves :9090/metrics over HTTPS with a SELF-SIGNED cert, and that +scrape job has no tls_config/insecure_skip_verify β€” so it can't scrape the controller +directly (verified: https verify β†’ TLS error 000; insecure β†’ 200). We can't edit the +platform scraper. + +Fix we own: a tiny proxy that scrapes the controller over HTTPS-insecure and re-serves +the same metrics over PLAIN HTTP. The annotated Service points at the proxy (scheme +http), so the scraper works exactly like every other http target. Exposes +argo_workflows_* incl. argo_workflows_df_runs_total / _df_run_duration_seconds. +*/ -}} +{{- if .Values.metrics.enabled }} +# Stable upstream Service the proxy dials (selects the controller pod; NOT scraped). +apiVersion: v1 +kind: Service +metadata: + name: dark-factory-controller-upstream + namespace: {{ .Values.argo.namespace }} + labels: + {{- include "dark-factory.labels" . | nindent 4 }} +spec: + type: ClusterIP + selector: + app: workflow-controller + ports: + - name: https-metrics + port: 9090 + targetPort: 9090 + protocol: TCP +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: dark-factory-metrics-proxy + namespace: {{ .Values.argo.namespace }} + labels: + {{- include "dark-factory.labels" . | nindent 4 }} + app: dark-factory-metrics-proxy +spec: + replicas: 1 + selector: + matchLabels: + app: dark-factory-metrics-proxy + template: + metadata: + labels: + {{- include "dark-factory.labels" . | nindent 8 }} + app: dark-factory-metrics-proxy + spec: + securityContext: + runAsNonRoot: true + runAsUser: 65532 + containers: + - name: proxy + image: {{ .Values.metrics.proxyImage }} + # socat: accept plain HTTP on :9091, forward to the controller's HTTPS + # :9090 (verify disabled β€” self-signed). One-shot per connection is fine + # for a 30s scrape. Uses OPENSSL to strip TLS on the upstream side. + command: [sh, -c] + args: + - | + exec socat -d TCP-LISTEN:9091,fork,reuseaddr \ + OPENSSL:dark-factory-controller-upstream.{{ .Values.argo.namespace }}.svc:9090,verify=0 + ports: + - name: http-metrics + containerPort: 9091 + resources: + requests: { cpu: 10m, memory: 16Mi } + limits: { cpu: 100m, memory: 64Mi } + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: { drop: ["ALL"] } +--- +apiVersion: v1 +kind: Service +metadata: + name: dark-factory-workflow-metrics + namespace: {{ .Values.argo.namespace }} + labels: + {{- include "dark-factory.labels" . | nindent 4 }} + annotations: + # Scraped by the platform AMP scraper over PLAIN HTTP (the proxy already + # terminated the controller's self-signed TLS upstream). + prometheus.io/scrape: "true" + prometheus.io/port: "9091" + prometheus.io/path: /metrics + prometheus.io/scheme: http +spec: + type: ClusterIP + selector: + app: dark-factory-metrics-proxy + ports: + - name: http-metrics + port: 9091 + targetPort: 9091 + protocol: TCP +{{- end }} diff --git a/gitops/addons/charts/dark-factory/templates/71-grafana-dashboard.yaml b/gitops/addons/charts/dark-factory/templates/71-grafana-dashboard.yaml new file mode 100644 index 00000000..0ac55c1c --- /dev/null +++ b/gitops/addons/charts/dark-factory/templates/71-grafana-dashboard.yaml @@ -0,0 +1,81 @@ +{{- /* +Dark Factory Grafana dashboard (observability) β€” managed by grafana-operator via a +GrafanaDashboard CR, exactly like the platform's agent-platform-* dashboards +(instanceSelector dashboards=external-grafana, folder "Agent Platform"). Panels are +built on the df_run_* metrics the df-run WorkflowTemplate emits (scraped into AMP +via the metrics Service in 70-metrics-service). GitOps-managed, no manual import. +*/ -}} +{{- if .Values.metrics.enabled }} +apiVersion: grafana.integreatly.org/v1beta1 +kind: GrafanaDashboard +metadata: + name: dark-factory-overview + namespace: {{ .Values.metrics.grafana.namespace }} + labels: + {{- include "dark-factory.labels" . | nindent 4 }} +spec: + allowCrossNamespaceImport: true + folder: {{ .Values.metrics.grafana.folder | quote }} + instanceSelector: + matchLabels: + {{- toYaml .Values.metrics.grafana.instanceSelector | nindent 6 }} + resyncPeriod: 10m + json: | + { + "title": "Dark Factory β€” Flow B", + "uid": "dark-factory-overview", + "tags": ["dark-factory", "agent-platform"], + "timezone": "browser", + "schemaVersion": 39, + "time": { "from": "now-24h", "to": "now" }, + "refresh": "1m", + "panels": [ + { + "id": 1, "type": "stat", "title": "df-run outcomes (total)", + "gridPos": { "h": 6, "w": 8, "x": 0, "y": 0 }, + "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" } } }, + "targets": [ + { "expr": "sum by (status) (argo_workflows_df_runs_total)", "legendFormat": "{{`{{status}}`}}", "refId": "A" } + ] + }, + { + "id": 2, "type": "timeseries", "title": "df-run outcomes over time (cumulative by status)", + "gridPos": { "h": 6, "w": 16, "x": 8, "y": 0 }, + "description": "argo_workflows_df_runs_total is a gauge (cumulative count per status).", + "targets": [ + { "expr": "sum by (status) (argo_workflows_df_runs_total)", "legendFormat": "{{`{{status}}`}}", "refId": "A" } + ] + }, + { + "id": 3, "type": "timeseries", "title": "df-run duration β€” lead time (s)", + "gridPos": { "h": 7, "w": 12, "x": 0, "y": 6 }, + "fieldConfig": { "defaults": { "unit": "s" } }, + "targets": [ + { "expr": "argo_workflows_df_run_duration_seconds", "legendFormat": "duration", "refId": "A" } + ] + }, + { + "id": 4, "type": "piechart", "title": "Outcome mix", + "gridPos": { "h": 7, "w": 6, "x": 12, "y": 6 }, + "targets": [ + { "expr": "sum by (status) (argo_workflows_df_runs_total)", "legendFormat": "{{`{{status}}`}}", "refId": "A" } + ] + }, + { + "id": 5, "type": "stat", "title": "Succeeded / Failed (cumulative)", + "gridPos": { "h": 7, "w": 6, "x": 18, "y": 6 }, + "targets": [ + { "expr": "sum(argo_workflows_df_runs_total{status=\"Succeeded\"})", "legendFormat": "succeeded", "refId": "A" }, + { "expr": "sum(argo_workflows_df_runs_total{status=\"Failed\"})", "legendFormat": "failed", "refId": "B" } + ] + }, + { + "id": 6, "type": "timeseries", "title": "Argo operational queue depth (df workflows)", + "gridPos": { "h": 6, "w": 24, "x": 0, "y": 13 }, + "targets": [ + { "expr": "sum(argo_workflows_count) by (status)", "legendFormat": "{{`{{status}}`}}", "refId": "A" } + ] + } + ] + } +{{- end }} diff --git a/gitops/addons/charts/dark-factory/templates/_helpers.tpl b/gitops/addons/charts/dark-factory/templates/_helpers.tpl new file mode 100644 index 00000000..efcb02bd --- /dev/null +++ b/gitops/addons/charts/dark-factory/templates/_helpers.tpl @@ -0,0 +1,6 @@ +{{- define "dark-factory.labels" -}} +app.kubernetes.io/name: dark-factory +app.kubernetes.io/part-of: dark-factory +app.kubernetes.io/managed-by: {{ .Release.Service }} +helm.sh/chart: {{ printf "%s-%s" .Chart.Name .Chart.Version }} +{{- end -}} diff --git a/gitops/addons/charts/dark-factory/values.yaml b/gitops/addons/charts/dark-factory/values.yaml new file mode 100644 index 00000000..2bd029ac --- /dev/null +++ b/gitops/addons/charts/dark-factory/values.yaml @@ -0,0 +1,387 @@ +# Dark Factory (Flow B) β€” default values. +# Deployed on the hub (control-plane) only, alongside Argo Workflows and the +# Flow A agent-sandbox warm pool. + +# Namespace where the warm pool, SandboxClaims, and coder pods live (Flow A). +# The workflow SA + RBAC are created here so claims are in-namespace. +namespace: agent-sandbox-system + +# Argo Workflows install (already on the hub in the `argo` namespace). +argo: + namespace: argo + # How long a COMPLETED workflow (success or failure) is retained before Argo's + # controller garbage-collects it. There is NO workflow archive/DB persistence on + # this cluster (workflow-controller-configmap has no `persistence:` block), so once + # TTL fires the run is gone for good β€” not archived. 7 days keeps demo/debug history + # visible in the Argo UI across a work week. Was 3600 (1h), which silently dropped + # runs an hour after they finished. + workflowTtlSecondsAfterCompletion: 604800 + +warmPool: + # The Flow A SandboxWarmPool the factory claims from (Kata substrate, default). + name: coder-warmpool + # Flow D: the Lambda-MicroVM SandboxWarmPool the factory claims from when the + # darkfactory-lambda label fires (coder runs in a MicroVM). Must match + # agent-sandbox microvm.warmPool.name. + lambdaName: coder-warmpool-microvm + # Namespace of the pool / where claims + coder pods are created. + namespace: agent-sandbox-system + +coder: + # In-VM control endpoint the workflow drives (the coder image listens here). + port: 8080 + # Agentic engine the coder VM runs. BOTH are first-class and selectable: + # claude β†’ claude -p (Claude Code, default; wired + tested) + # kiro β†’ kiro run --headless (Kiro CLI; supported, selectable) + # The coder image carries both CLIs; entrypoint.js branches on this value. + # (Retained name `profile` for back-compat; value is the engine id.) + engine: claude + profile: claude-code + # Max minutes for a single coder run before the step times out. + runTimeoutMinutes: 30 + +bifrost: + url: http://172.20.181.17:8080 # ClusterIP (Kata VM guest DNS cannot resolve svc names) + +# ── Observability: LLM tracing β†’ Langfuse (provided by Bifrost) ────────────── +# LLM traces/cost/tokens are exported to Langfuse by BIFROST's telemetry plugin +# (full per-call traces, tagged user-agent=dark-factory-coder). No coder-side or +# per-claim wiring needed β€” deliberately not duplicated here. See docs Β§7a. + +github: + # Secret (in .namespace) holding the GitHub App / token the workflow uses to + # open the PR + maintain the sticky status comment. Key: token. + tokenSecret: dark-factory-github + tokenKey: token + # Sync the token from AWS Secrets Manager via ExternalSecret (same mechanism as + # the rest of the platform) instead of a manual kubectl-created secret β€” so it's + # GitOps-managed, reproducible on rebuild, and auto-rotated. The SM secret is a + # JSON blob with keys: token, webhook-secret. Creates dark-factory-github in the + # argo / argo-events / agent-sandbox-system namespaces (see 05-externalsecret). + externalSecret: + enabled: true + clusterSecretStore: aws-secrets-manager + secretsManagerKey: dark-factory/github + refreshInterval: 1h + +# Step-container image: needs kubectl + curl + jq to drive claims, poll the +# coder, and call the GitHub API. alpine/k8s bundles kubectl; curl/jq added at start. +stepImage: alpine/k8s:1.31.0 + +# Concurrency: cap total in-flight df-run workflows against the kata pool size. +maxConcurrentRuns: 3 + +# Safety-net TTL on a SandboxClaim (reaper backstop if teardown never runs). +claimTtlSeconds: 10800 + +# ── P2: Holdout gate ───────────────────────────────────────────────────────── +# Train/test separation for code (docs/dark-factory Β§6.1). Hidden BDD scenarios +# + executable tests the coder NEVER sees are stored in a ConfigMap ON THE HUB +# (rendered from holdout// below). A hub-side eval step (NOT the Kata VM) +# checks out the coder's df/issue-N branch, runs the hidden executable tests, and +# asks a DIFFERENT-FAMILY LLM judge (Nova, vs the coder's Claude β€” defeats +# self-preference bias) whether each plain-English scenario is satisfied. A +# scenario PASSES iff its executable test is green AND the judge says yes >=2/3 +# runs. Gate = passRatio >= threshold. The coder is credential-less to the k8s +# API and never clones the holdout path, so it provably cannot read or edit it. +holdout: + enabled: true + # Fraction of scenarios that must pass for the gate to be green. + threshold: 0.90 + # 2-of-3 majority per scenario smooths judge non-determinism. + judgeRuns: 3 + judgeQuorum: 2 + # The judge model β€” MUST be a different family than the coder (claude-sonnet). + # Bifrost passes Bedrock model IDs through directly (models: ["*"]); verified + # us.amazon.nova-pro-v1:0 β†’ 200 on /anthropic. + judgeModel: us.amazon.nova-pro-v1:0 + # Bifrost gateway the judge reaches (ClusterIP β€” same reason as the coder). + bifrostUrl: http://172.20.181.17:8080 + # v1: report-only (advisory). Set true to make a failed gate fail the workflow. + blocking: false + # Node image for the eval step: needs git + node + the languages the executable + # tests run in. Reuse the coder image (has git/node/python/go + is already in ECR). + evalImage: 940019131157.dkr.ecr.us-west-2.amazonaws.com/dark-factory-coder:v0.2.5 + +# ── P3: Reviews via the REAL AWS Frontier Agents ───────────────────────────── +# Two independent reviewers, run OUTSIDE the coder VM (the coder never grades +# itself). These are the genuine managed AWS agents β€” NOT linters, NOT a Nova +# stand-in. Ordering (per the AI-DLC model): DevOps Agent FIRST (broad release +# readiness) β†’ on clear, add the `needs-security-review` label β†’ Security Agent +# SECOND (narrow/strict). See docs Β§6.2 + diagram B.5. +# +# 1. AWS DevOps Agent β€” Release Readiness code review (cross-repo deps, +# standards, access-control, build+test in an AWS-managed env β†’ BLOCK / +# Proceed-with-Caution / Safe-to-Release). Connected via its GitHub App; it +# auto-reviews each PR and posts the aws-devops-agent/release-readiness-review +# check. The df-run `devops-gate` step waits for that check. (Operational +# Readiness / incident-SRE is a separate capability, out of scope here.) +# 2. AWS Security Agent β€” code security review (OWASP Top 10, secrets, IAM +# misuse, dependency risk). TWO paths, both run: (a) GitHub App β†’ inline +# aws-security-agent[bot] findings; (b) headless API β€” clone β†’ stage +# {src,diff} in S3 β†’ create-code-review β†’ start-job β†’ list-findings via the +# workflow's IRSA role. Setup: docs/dark-factory/AGENT-INSTALL.md. +review: + enabled: true + # The label the DevOps clear-gate applies; the Security Agent step only runs + # once it's present (so DevOps reviews first, Security second). + handoffLabel: needs-security-review + # AUTO-FIX loop: DISABLED. When on, status.js auto-submitted a df-run fix round on a + # ❌ verdict β€” but that COLLIDED with the comment-driven df-iterate loop (the pipeline's + # own verdict/finding comments re-triggered df-iterate), spawning overlapping fix rounds + # and runaway commits. The stable model is: agents post findings β†’ ❌ verdict β†’ a HUMAN + # comments the fix request β†’ ONE df-iterate round β†’ re-review. Keep this false unless + # auto-fix is given hard mutual-exclusion with df-iterate. + autoFixFindings: false + +# Post ONE consolidated verdict review (Security + DevOps results, LGTM when green) +# on every PR, so both agents' verdicts are ALWAYS visible in the Reviews section. +# Why not just add the agents as reviewers? Their GitHub App bots CANNOT be added via +# the requested_reviewers API (verified: it silently no-ops β€” Apps aren't collaborators), +# and they review autonomously + inconsistently (sometimes a formal review, sometimes +# only an issue comment). This pipeline-posted review is the consistent signal; the +# agents' own checks (dark-factory/security, aws-devops-agent/...) remain the source of +# truth. Posted as the workflow's GitHub identity, as a COMMENT (human still owns merge). +postVerdictReview: true + +# ── AWS Security Agent (headless, GitOps-native) ───────────────────────────── +# The agent SPACE + APPLICATION are reconciled once by the PreSync bootstrap Job +# (templates/06-securityagent-bootstrap.yaml β†’ scripts/bootstrap-agentspace.sh), +# which writes their IDs into `secretName`. IAM (IRSA role, service role, S3 +# bucket, OIDC provider) is committed Terraform in iam/securityagent.tf. The +# per-run review step (scripts/security-agent.sh) reads the Secret. All values +# below are the live, validated resources (account 940019131157, us-west-2). +securityAgent: + enabled: true + region: us-west-2 + # Agent space name the bootstrap Job finds-or-creates (adopts as-0fa9566... ). + spaceName: dark-factory + # Committed IAM (iam/securityagent.tf) outputs β€” the chart consumes, never mints. + irsaRoleArn: arn:aws:iam::940019131157:role/df-securityagent-irsa + serviceRoleArn: arn:aws:iam::940019131157:role/service-role/df-securityagent-service-role + diffBucket: dark-factory-secagent-940019131157-us-west-2 + # IAM Identity Center instance (create-application β†’ console renders the space). + idcInstanceArn: arn:aws:sso:::instance/ssoins-7907c8a7931ceef3 + # Secret (argo ns) the bootstrap Job writes + the review step reads. + secretName: dark-factory-securityagent + # Bootstrap Job image β€” MUST have a current aws-cli v2 (the securityagent + # service is too new for musl/apk builds). amazon/aws-cli (glibc, always-current + # v2) guarantees the verbs; kubectl is fetched at start. Override if you bake a + # combined aws-cli+kubectl image. + bootstrapImage: public.ecr.aws/aws-cli/aws-cli:latest + # df-run security step image β€” same reason (needs securityagent verbs). The + # script is node-free (python3+curl+git+aws), so this glibc image + git suffices. + # NOTE: verified in-cluster that Alpine/musl aws-cli 2.32 does NOT have + # securityagent β€” do NOT point this at the coder image. + stepImage: public.ecr.aws/aws-cli/aws-cli:latest + # Findings BLOCK the merge (per-severity gate). A finding at/above this + # riskLevel fails the dark-factory/security check, which fails the run and gates + # the merge β€” so the consolidated verdict can never say "cleared/LGTM" while real + # findings exist (that mixed signal was the bug). medium = block medium/high/ + # critical; informational/low pass as advisory. Set to none to make purely + # advisory again, or low/high/critical to tune the gate. + blockLevel: medium + # Seconds to wait for a review job (validated: ~2.5 min on a small diff). + pollTimeoutSeconds: 900 + + # ── AWS Security Agent GitHub App (inline bot findings β€” ADDITIVE) ────────── + # The headless S3-diff path above stays running. This ADDS the aws-security-agent + # GitHub App (AWSHobbesSecureCode) which β€” once installed on the repo (one-time + # console OAuth, see docs/dark-factory/AGENT-INSTALL.md) β€” auto-reviews every PR + # and posts INLINE findings as `aws-security-agent [Bot]`, exactly like the + # DevOps Agent App. The App bot's inline review + the headless dark-factory/ + # security commit status both run; the pipeline no longer posts its own relay + # comment (single-signal design β€” findings surface in the ONE consolidated + # verdict review). merge.js/status.js read the real check so a Security BLOCK + # gates the merge. + app: + enabled: true + # The aws-security-agent[bot] posts INLINE PR COMMENTS ("AWS Security Agent is + # reviewing…" β†’ findings / "No issues identified") rather than a commit-status + # or check-run. So there's nothing for checkRunName/checkContext to match β€” leave + # them empty and don't require them in the merge gate. The merge gate uses the + # headless dark-factory/security signal; the App's inline comments are the visible + # bot review. If your setup posts a Security check/status, set these + flip + # REQUIRE_SECURITY (21-workflowtemplate) to "true" to gate the merge on it. + checkContext: "" + checkRunName: "" + waitSeconds: 900 + +# ── AWS DevOps Agent (Release Readiness) ───────────────────────────────────── +# NOTE: unlike the Security Agent, the DevOps Agent has NO headless code-review +# API. Its release-readiness review is reachable via (a) the GitHub App that +# auto-reviews every PR and posts a check-run [DEFAULT β€” native model, matches +# the AI-DLC blog], (b) MCP/A2A with an Agent-Spaces access key from a hub step, +# or (c) the coding-agent plugin inside the IDE/CLI. Because the coder VM is +# credential-less + network-locked AND Claude Code -p doesn't expand plugin +# commands, the in-VM plugin path can't run there β€” so we default to the GitHub +# App check-run and WAIT for it. All paths need ONE one-time console connect +# (repo ↔ Agent Space); that is the single manual step in the pipeline (docs Β§6.2). +devopsAgent: + enabled: true + # How the df-run devops-gate confirms the DevOps Agent cleared: + # check β†’ wait for the DevOps Agent GitHub App's check-run (DEFAULT, native). + # label β†’ wait for a coder-applied handoffLabel (fallback if you drive DevOps + # via the coding-agent plugin instead of the App). + gate: check + # Regex matched against check-run names / status contexts the DevOps Agent posts. + # CONFIRMED (internal DevSecOps walkthrough): the native GitHub check is + # "aws-devops-agent/release-readiness-review". Keep the broader alternation as a + # safety net in case the check context varies by environment. + # NOTE: JS RegExp β€” do NOT use a (?i) inline flag (Python/Go syntax; throws + # "Invalid group" in node). Case-insensitivity is applied via the `i` flag in + # the gate script's `new RegExp(CHECK_CONTEXT, "i")`. + checkContext: "(aws-devops-agent/release-readiness-review|devops[- ]?agent|release[- ]?readiness)" + # EXACT check-run name (Checks API) the DevOps Agent posts β€” used by the sticky + # status (status.js) + the merge gate (merge.js) to read/require the real verdict. + # This is the precise name (not a regex); tune if your environment reports a different name. + checkRunName: "aws-devops-agent/release-readiness-review" + # How long devops-gate waits for the DevOps Agent to post its verdict. The + # managed review typically completes in ~8-10 min (docs), so allow headroom. + waitSeconds: 900 + # (label-mode only) verdicts the coder treats as "cleared" when it drives the + # review via the plugin and applies the handoff label itself. + clearVerdicts: ["Safe to Release", "Proceed with Caution"] + +# Image for hub-side agent steps: needs aws-cli + kubectl + node + git + zip. +# Reuse the coder image (has node/git/zip); the bootstrap Job adds aws-cli. +reviewImage: 940019131157.dkr.ecr.us-west-2.amazonaws.com/dark-factory-coder:v0.2.5 + +# ── P3b: Iterate on PR comment ─────────────────────────────────────────────── +# A human comment on a Dark Factory PR routes back to the coder as a revision +# request (docs Β§8). df-iterate resolves the PR β†’ df/issue-, then re-submits +# df-run with iterate-note = the comment (coder appends it to SPEC.md and revises +# the existing branch). Bounded to maxIterations rounds (label counter on the PR); +# past the cap, a human breaks the tie. +iterate: + enabled: true + maxIterations: 3 + +# ── Flow D β€” Lambda MicroVM substrate (df-run-lambda ONLY) ─────────────────── +# The Kata df-run template has ZERO MicroVM logic (it's byte-identical to the certified +# Kata pipeline). Flow D runs in a SEPARATE WorkflowTemplate, df-run-lambda, which +# provisions the Lambda MicroVM directly (Microvm CR + /run), suspends it during review +# (scale-to-zero), resumes the SAME VM on a fix round, and terminates it at merge. These +# values feed that template; they mirror the agent-sandbox-lambda chart's microvm.* keys. +microvm: + enabled: true + region: us-west-2 + namespace: agent-sandbox-system # where the Microvm CR + platform MicrovmSandbox live + # aws-cli v2 (has the lambda-microvms verbs); kubectl is fetched at step start. + stepImage: public.ecr.aws/aws-cli/aws-cli:latest + image: + name: coder # the platform MicrovmSandbox name (image handoff) + defaults: + maxIdleDurationSeconds: 1800 # RUNNING-idle backstop (we drive /run immediately) + # Lambda caps SUSPEND at 8h (28800s). Use the max so the VM survives the full + # reviewβ†’fix window; df-merge-teardown terminates it explicitly at merge. + suspendedDurationSeconds: 28800 + # Pod Identity: the df-run-lambda steps run as the dark-factory-workflow SA (argo ns) + # and call aws lambda-microvms (get/suspend/resume/terminate/create-auth-token). Bind + # that SA to the SAME lambda-microvms role the bridge/lifecycle use (least-privilege; + # additive; no existing role policy changes). ACK eks PodIdentityAssociation. + podIdentity: + clusterName: hub + accountId: "940019131157" + # role: -ack-lambdamicrovms-controller + workflowServiceAccount: dark-factory-workflow + +# ── Language / stack support ───────────────────────────────────────────────── +# There is NO per-language profile config here β€” deliberately. Language support is +# decoupled two ways: +# 1. Toolchains live in the CODER IMAGE (examples/dark-factory/coder/Dockerfile) β€” +# a generic image carries git/node/python/go (+ add JDK/Cargo/terraform there +# when needed). Devs control the toolchain via the image, not platform config. +# 2. Build/test commands are DISCOVERED from the repo's own marker files by the +# coder (package.jsonβ†’npm, go.modβ†’go, pyprojectβ†’pytest, Cargo.tomlβ†’cargo, +# pom.xml/build.gradleβ†’maven/gradle, Makefile test targetβ†’make test). Devs +# control build/test by their repo layout β€” no central profile to maintain. +# 3. deploy-test verification kind is AUTO-DETECTED from the changed files +# (detect-deployable: *.tfβ†’terraform, k8s manifests/Dockerfileβ†’k8s). +# So adding a language = extend the coder image + rely on its marker files. No +# label, no stackProfiles, no pipeline change. + +# ── P4: Conditional deploy-test (content-aware) ────────────────────────────── +# For PRs that touch deployable artifacts, a TRUSTED hub step validates the change +# with the RIGHT tool for the artifact type (docs Β§9 / diagram B.4). This is the +# only step that holds K8s access β€” never the untrusted coder. detect-deployable +# classifies the change and emits kind = k8s | terraform | none: +# - k8s β†’ deploy into an ephemeral namespace, wait Ready, teardown (trap). +# - terraform β†’ terraform init -backend=false && terraform validate (+ fmt check). +# Validation only β€” no AWS creds, no apply (no real infra created). +# deploy-test runs only when kind != none; advisory in v1. +deployTest: + enabled: true + # Regex (grep -E) of changed files that mark each artifact kind. k8s takes + # precedence when a PR touches both. + k8sPatterns: "(Chart\\.yaml|/templates/|k8s/|deployment\\.ya?ml|Dockerfile)" + terraformPatterns: "\\.tf$" + # Where each kind's files live in the repo (dir or file). If absent, the step + # logs "nothing to test" and passes (advisory). + manifestPath: "k8s/" # k8s apply target + terraformPath: "infra" # terraform working dir (repo uses infra/ + app/ layout) + # Purpose-built image with kubectl + terraform + node + git + curl (content-aware + # deploy-test needs both toolchains). Built from examples/dark-factory/deploy-test. + image: 940019131157.dkr.ecr.us-west-2.amazonaws.com/dark-factory-deploy-test:v0.1.0 + # Overall step deadline and the per-workload readiness wait (k8s only). + timeoutSeconds: 600 + readyTimeoutSeconds: 120 + # Advisory in v1 β€” posts dark-factory/deploy-test but never fails the run. + blocking: false + +# ── Observability: metrics + Grafana dashboard ─────────────────────────────── +# df-run emits Prometheus metrics via its metrics: block (df_runs_total by status, +# df_run_duration_seconds). A headless annotated Service (70-metrics-service) exposes +# the Argo workflow-controller :9090/metrics so the platform's AMP scraper +# (kubernetes-service-endpoints job, prometheus.io/scrape) picks it up. A +# GrafanaDashboard CR (71-grafana-dashboard) renders the Dark Factory dashboard via +# grafana-operator (same pattern as the agent-platform-* dashboards). +metrics: + enabled: true + # The Argo controller serves :9090/metrics over HTTPS with a self-signed cert, + # which the platform AMP scraper can't verify. A tiny socat proxy (this image) + # terminates that TLS and re-serves plain HTTP :9091 for the scraper. + proxyImage: alpine/socat:1.8.0.0 + grafana: + # grafana-operator watches this namespace for GrafanaDashboard CRs. + namespace: grafana-operator + # Which Grafana instance to import into + folder (matches agent-platform-* CRs). + instanceSelector: + dashboards: external-grafana + folder: "Agent Platform" + +# ── P4: Reaper CronJob ─────────────────────────────────────────────────────── +# Crash-net for teardown: sweeps stale ephemeral deploy-test namespaces and +# abandoned df-run SandboxClaims older than reapAfterSeconds. Backs up the per-run +# onExit teardown + the claim's own TTL. +reaper: + enabled: true + schedule: "*/15 * * * *" # every 15 min + reapAfterSeconds: 10800 # 3h β€” matches claimTtlSeconds + image: alpine/k8s:1.31.0 # kubectl + coreutils date + +# Trigger β€” Argo Events: GitHub webhook EventSource + Sensor β†’ df-run Workflow. +trigger: + argoEvents: + enabled: true + namespace: argo-events + eventbusReplicas: 3 + # JetStream version β€” MUST match an entry in the argo-events controller-config + # jetstream.versions list (verified live: 'latest' β†’ nats:2.10.10). + jetstreamVersion: latest + # GitHub repos whose issues fire the factory. + repositories: + - owner: elamaran11 + names: + - dark-factory-sandbox + # Public URL GitHub posts webhooks to (ALB ingress host + endpoint). + webhookUrl: https://idp.elamaras.people.aws.dev + webhookEndpoint: /dark-factory + webhookPort: 12000 + # Secret (in argo-events ns) with keys: token (GitHub API), webhook-secret (HMAC). + githubSecret: dark-factory-github + # Expose the EventSource via the platform ALB ingress for GitHub to reach it. + ingress: + enabled: true + host: idp.elamaras.people.aws.dev + path: /dark-factory diff --git a/gitops/addons/charts/langfuse/Chart.yaml b/gitops/addons/charts/langfuse/Chart.yaml index edb25bcb..bfc62798 100644 --- a/gitops/addons/charts/langfuse/Chart.yaml +++ b/gitops/addons/charts/langfuse/Chart.yaml @@ -2,4 +2,4 @@ apiVersion: v2 name: langfuse description: Langfuse LLM observability platform with PostgreSQL type: application -version: 0.1.0 +version: 0.1.1 diff --git a/gitops/addons/charts/langfuse/templates/langfuse.yaml b/gitops/addons/charts/langfuse/templates/langfuse.yaml index 5d8b20df..f8d691f4 100644 --- a/gitops/addons/charts/langfuse/templates/langfuse.yaml +++ b/gitops/addons/charts/langfuse/templates/langfuse.yaml @@ -108,24 +108,32 @@ spec: optional: true resources: {{- toYaml .Values.resources | nindent 12 }} + # NOTE: all probes set timeoutSeconds explicitly. The k8s default is 1s, + # but /api/public/health checks Postgres + ClickHouse and can exceed 1s + # under load β€” with the 1s default the livenessProbe timed out, failed 3x, + # and the pod was SIGKILLed (exitCode 137) mid-boot, crash-looping and + # leaving the Service with no endpoints (UI 502). 5s gives real headroom. startupProbe: httpGet: path: /api/public/health port: 3000 initialDelaySeconds: 15 periodSeconds: 10 + timeoutSeconds: 5 failureThreshold: 30 livenessProbe: httpGet: path: /api/public/health port: 3000 - periodSeconds: 15 + periodSeconds: 30 + timeoutSeconds: 5 failureThreshold: 3 readinessProbe: httpGet: path: /api/public/health port: 3000 periodSeconds: 10 + timeoutSeconds: 5 failureThreshold: 3 --- apiVersion: v1 diff --git a/gitops/addons/charts/oam-agent-components/templates/agent.yaml b/gitops/addons/charts/oam-agent-components/templates/agent.yaml index 61e55f0a..7b73ce91 100644 --- a/gitops/addons/charts/oam-agent-components/templates/agent.yaml +++ b/gitops/addons/charts/oam-agent-components/templates/agent.yaml @@ -36,18 +36,18 @@ spec: apiVersion: "argoproj.io/v1alpha1" kind: "Rollout" metadata: { - name: parameter.name - namespace: parameter.namespace + name: context.name + namespace: context.namespace labels: { - "app.kubernetes.io/name": parameter.name + "app.kubernetes.io/name": context.name "app.kubernetes.io/component": "ai-agent" } } spec: { replicas: parameter.replicas strategy: blueGreen: { - activeService: parameter.name + "-stable" - previewService: parameter.name + "-preview" + activeService: context.name + "-stable" + previewService: context.name + "-preview" autoPromotionEnabled: parameter.autoPromotionEnabled if parameter.autoPromotionSeconds != _|_ { autoPromotionSeconds: parameter.autoPromotionSeconds @@ -56,25 +56,21 @@ spec: scaleDownDelaySeconds: parameter.scaleDownDelaySeconds } } - selector: matchLabels: "app.kubernetes.io/name": parameter.name + selector: matchLabels: "app.kubernetes.io/name": context.name template: { - metadata: labels: "app.kubernetes.io/name": parameter.name + metadata: labels: "app.kubernetes.io/name": context.name spec: { - serviceAccountName: parameter.serviceAccount + serviceAccountName: context.name containers: [{ - name: "agent" + name: context.name image: parameter.image - // Decentralized mode: wrap with opentelemetry-instrument for ADOT auto-instrumentation - if parameter.observability.mode == "decentralized" { - command: ["opentelemetry-instrument", "python", "-m", "app.main"] - } ports: [{ name: "a2a" containerPort: 8083 protocol: "TCP" }] env: [ - {name: "AGENT_NAME", value: parameter.name}, + {name: "AGENT_NAME", value: context.name}, {name: "AGENT_DESCRIPTION", value: parameter.description}, {name: "MODEL_ID", value: parameter.modelConfig.modelId}, {name: "SYSTEM_PROMPT", value: parameter.systemMessage}, @@ -82,7 +78,7 @@ spec: {name: "LLM_GATEWAY_URL", value: parameter.modelConfig.llmGatewayUrl}, {name: "LLM_GATEWAY_API_KEY", value: parameter.modelConfig.llmGatewayApiKey}, // Observability env vars β€” mode-dependent - {name: "OTEL_SERVICE_NAME", value: parameter.name}, + {name: "OTEL_SERVICE_NAME", value: context.name}, {name: "OTEL_TRACES_EXPORTER", value: "otlp"}, if parameter.observability.mode == "centralized" { {name: "OTEL_EXPORTER_OTLP_ENDPOINT", value: "http://otel-collector.otel.svc.cluster.local:4318"} @@ -97,7 +93,7 @@ spec: {name: "OTEL_EXPORTER_OTLP_PROTOCOL", value: "http/protobuf"} }, if parameter.observability.mode == "decentralized" { - {name: "OTEL_RESOURCE_ATTRIBUTES", value: "service.name=" + parameter.name} + {name: "OTEL_RESOURCE_ATTRIBUTES", value: "service.name=" + context.name} }, if parameter.observability.mode == "decentralized" { {name: "AGENT_OBSERVABILITY_ENABLED", value: "true"} @@ -141,17 +137,30 @@ spec: } outputs: { + // Dedicated ServiceAccount β€” the agent's single identity anchor. The + // gateway-identity and aws-service-identity traits attach their + // capabilities to this SA (name == context.name). + serviceAccount: { + apiVersion: "v1" + kind: "ServiceAccount" + metadata: { + name: context.name + namespace: context.namespace + labels: "app.kubernetes.io/name": context.name + } + } + // Stable service (active) stableService: { apiVersion: "v1" kind: "Service" metadata: { - name: parameter.name + "-stable" - namespace: parameter.namespace - labels: "app.kubernetes.io/name": parameter.name + name: context.name + "-stable" + namespace: context.namespace + labels: "app.kubernetes.io/name": context.name } spec: { - selector: "app.kubernetes.io/name": parameter.name + selector: "app.kubernetes.io/name": context.name ports: [{ name: "a2a", port: 8083, targetPort: 8083, protocol: "TCP" appProtocol: "kgateway.dev/a2a" @@ -165,12 +174,12 @@ spec: apiVersion: "v1" kind: "Service" metadata: { - name: parameter.name + "-preview" - namespace: parameter.namespace - labels: "app.kubernetes.io/name": parameter.name + name: context.name + "-preview" + namespace: context.namespace + labels: "app.kubernetes.io/name": context.name } spec: { - selector: "app.kubernetes.io/name": parameter.name + selector: "app.kubernetes.io/name": context.name ports: [{ name: "a2a", port: 8083, targetPort: 8083, protocol: "TCP" appProtocol: "kgateway.dev/a2a" @@ -184,15 +193,15 @@ spec: apiVersion: "v1" kind: "ConfigMap" metadata: { - name: parameter.name + "-card" - namespace: parameter.namespace + name: context.name + "-card" + namespace: context.namespace labels: { - "app.kubernetes.io/name": parameter.name + "app.kubernetes.io/name": context.name "agent.dev/type": "agent-card" } } data: { - name: parameter.name + name: context.name description: parameter.description model: parameter.modelConfig.modelId } @@ -210,9 +219,9 @@ spec: apiVersion: "gateway.networking.k8s.io/v1" kind: "HTTPRoute" metadata: { - name: parameter.name - namespace: parameter.namespace - labels: "app.kubernetes.io/name": parameter.name + name: context.name + namespace: context.namespace + labels: "app.kubernetes.io/name": context.name } spec: { parentRefs: [{ @@ -223,7 +232,7 @@ spec: matches: [{ path: { type: "PathPrefix" - value: "/" + parameter.name + value: "/" + context.name } }] filters: [{ @@ -234,9 +243,9 @@ spec: } }] backendRefs: [{ - name: parameter.name + "-stable" + name: context.name + "-stable" port: 8083 - namespace: parameter.namespace + namespace: context.namespace }] }] } @@ -247,8 +256,6 @@ spec: parameter: { // Required fields - name: string - namespace: string description: string systemMessage: string @@ -256,8 +263,7 @@ spec: image: *"public.ecr.aws/z0a4o2j5/strands-agent:latest" | string // Optional fields with defaults - replicas: *3 | int - serviceAccount: *"default" | string + replicas: *3 | int // Blue-green deployment settings autoPromotionEnabled: *true | bool diff --git a/gitops/addons/charts/oam-agent-components/templates/agentcore-memory.yaml b/gitops/addons/charts/oam-agent-components/templates/agentcore-memory.yaml index c80c0539..e82ab955 100644 --- a/gitops/addons/charts/oam-agent-components/templates/agentcore-memory.yaml +++ b/gitops/addons/charts/oam-agent-components/templates/agentcore-memory.yaml @@ -16,7 +16,7 @@ spec: let _autoName = strings.Replace(context.namespace+"_"+context.name, "-", "_", -1) output: { - apiVersion: "bedrockagentcore.aws.m.upbound.io/v1beta1" + apiVersion: "bedrockagentcore.aws.upbound.io/v1beta1" kind: "Memory" metadata: name: context.name spec: { @@ -26,10 +26,7 @@ spec: description: parameter.description eventExpiryDuration: parameter.eventExpiryDuration } - providerConfigRef: { - name: "provider-aws-config" - kind: "ClusterProviderConfig" - } + providerConfigRef: name: "default" } } @@ -61,7 +58,7 @@ spec: } """ } - providerConfigRef: name: "provider-aws-config" + providerConfigRef: name: "default" } } diff --git a/gitops/addons/charts/oam-agent-components/templates/aws-service-identity.yaml b/gitops/addons/charts/oam-agent-components/templates/aws-service-identity.yaml index 2cabe9c6..3aea90db 100644 --- a/gitops/addons/charts/oam-agent-components/templates/aws-service-identity.yaml +++ b/gitops/addons/charts/oam-agent-components/templates/aws-service-identity.yaml @@ -67,7 +67,7 @@ spec: policyArnRef: name: "\(context.appName)-\(c)-iam-policy" role: "\(context.name)-role" } - providerConfigRef: name: "provider-aws-config" + providerConfigRef: name: "default" } } } diff --git a/gitops/addons/clusters/hub/addons/agent-sandbox-lambda/values.yaml b/gitops/addons/clusters/hub/addons/agent-sandbox-lambda/values.yaml new file mode 100644 index 00000000..0abcde06 --- /dev/null +++ b/gitops/addons/clusters/hub/addons/agent-sandbox-lambda/values.yaml @@ -0,0 +1,48 @@ +# Hub (openclaw-eks / control-plane) overlay for the agent-sandbox-lambda chart +# (Flow D β€” Lambda MicroVM substrate). +# +# DORMANT (microvm.enabled=false) until the self-managed ack-lambdamicrovms +# controller is synced (gitops/addons/bootstrap/default/addons.yaml) and the arm64 +# coder artifact is published to S3. Managed KRO + Managed ACK capabilities are +# ACTIVE on the hub. The ARNs below are public identifiers (not secrets). +microvm: + enabled: true + region: us-west-2 + # MUST be kro.run β€” EKS Managed KRO only watches the kro.run group (see chart values.yaml). + apiGroup: kro.run + # aws-cli v2 (has the pre-GA lambda-microvms service; alpine/k8s's aws-cli 1.34 does NOT, + # so RunMicrovm never fired). bridge.sh/reconcile.sh fetch kubectl at start. See chart values. + bridgeImage: public.ecr.aws/aws-cli/aws-cli:latest + # Hub account (for the ACK PodIdentityAssociation role ARN). + accountId: "940019131157" + podIdentity: + clusterName: hub + controllerNamespace: ack-system + controllerServiceAccount: ack-lambdamicrovms-controller + # AWS-published base MicroVM image (ARM_64 β€” the only arch Lambda MicroVM supports). + baseImageARN: "arn:aws:lambda:us-west-2:aws:microvm-image:al2023-1" + # Lambda MicroVM codeArtifact.uri is S3-ONLY (a zip with the coder app + a Dockerfile); + # it is NOT an ECR image reference. The Dockerfile inside MAY pull the arm64 + # dark-factory-coder from ECR as a base layer (the build role keeps ecr:Get*). + # MUST live in the bucket the RGD creates: ${image.name}-microvm-artifacts (here + # `coder-microvm-artifacts`). Previously pointed at a hand-named bucket that the RGD + # never provisioned, so the image build hit NoSuchBucket/CREATE_FAILED. Publish the + # artifact zip to this exact bucket/key. + # r7: hook-server /run guard is now keyed on a per-invocation run-id (issue+note + # hash) instead of a one-shot boolean, so a RESUMED VM (fix round) accepts a fresh + # /run and re-runs the coder β€” the resume path was previously a no-op because the + # snapshot froze coderStarted=true. Also truncates /tmp/coder.log per run so the + # bridge's "PR pushed" grep doesn't match the previous round's line. + # r6: fix-round iterate note forwarded (bridge payload -> hook-server -> coder env). + codeArtifactUri: "s3://coder-microvm-artifacts/coder-v0.2.5-arm64-r7.zip" + image: + enabled: true + name: coder + defaults: + maxIdleDurationSeconds: 900 + suspendedDurationSeconds: 300 + lifecycle: + intervalSeconds: 15 + warmPool: + name: coder-warmpool-microvm + targetIdle: 1 diff --git a/gitops/addons/clusters/hub/addons/agent-sandbox/values.yaml b/gitops/addons/clusters/hub/addons/agent-sandbox/values.yaml new file mode 100644 index 00000000..bf10ad47 --- /dev/null +++ b/gitops/addons/clusters/hub/addons/agent-sandbox/values.yaml @@ -0,0 +1,40 @@ +# Hub (openclaw-eks / control-plane) overlay for the agent-sandbox capability. +# +# The chart default (charts/agent-sandbox/values.yaml) keeps the `nodepool` +# block cluster-agnostic. This overlay supplies the hub cluster's specific +# coordinates for the Kata nested-virt Managed Node Group and (optionally) flips +# it on. `nodepool.enabled` stays false until we explicitly activate/adopt it. +# +# clusterEndpoint + clusterCA are NOT secrets: the CA is the cluster's PUBLIC +# api-server certificate (no private key) and the endpoint is public DNS β€” both +# ship in every kubeconfig. They live here (per-cluster overlay) rather than the +# chart default purely for reusability; they get baked into the LaunchTemplate +# userData (nodeadm NodeConfig) at Helm render time, so a k8s Secret/env cannot +# feed them. A future refactor (drop the custom amiId β†’ let EKS auto-inject the +# bootstrap) would remove them from git entirely. +nodepool: + # Flip to true (and manageAddons as needed) to provision/adopt the kata MNG. + enabled: false + # Adopt the existing vpc-cni + kube-proxy addons on this cluster. + manageAddons: false + region: us-west-2 + providerConfigName: default + # Live hub cluster control-plane coordinates (public, non-secret). + clusterName: hub + clusterEndpoint: https://5E188804F70B5BD204A38FCAA233AF3D.gr7.us-west-2.eks.amazonaws.com + serviceCidr: 172.20.0.0/16 + clusterCA: "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURCVENDQWUyZ0F3SUJBZ0lJVTc5Y25OekQ3aW93RFFZSktvWklodmNOQVFFTEJRQXdGVEVUTUJFR0ExVUUKQXhNS2EzVmlaWEp1WlhSbGN6QWVGdzB5TmpBM01EWXhOekUzTWpkYUZ3MHpOakEzTURNeE56SXlNamRhTUJVeApFekFSQmdOVkJBTVRDbXQxWW1WeWJtVjBaWE13Z2dFaU1BMEdDU3FHU0liM0RRRUJBUVVBQTRJQkR3QXdnZ0VLCkFvSUJBUUNtTEc1bHBNb3BCKzlHMjVvVXlSQmF5RWdSZVd3N2VUU0taTWsyWmh0OWttMGFWaEE2UTNBMXZYVzEKc3FxcEhRVHdYR3RmeFB4eUt1RmRCMGVrV2s3WEV4TUVoblFTZWVOTEFkWkdUaGVpd3pZNVpSWTNNMG5kSGE5dQpZZXFBTkpWd2FXOXVTaVByM0dHL2pTR1JTZElKN2VuMU9qRTRHY2ZPdUNqV0hiUElvNVNKNndEQUtDbGRjbGplCkNnMklvVC8rb3NyTzBGay9aMjF2d2NLVDBMYy9SU3diVnExUFI3TncxSzBxaFQ0K1RBSHRKSktoK0Qxek80REoKTjd2amduam03R2RsdS9lOTBnS3NNTjhKbEhhMFJYNWh6ZmV2N0ZNdGZxdUxEcGlxcE03cWpzT08wcDQwajY4dgpmT1V2K2hEQUZvd3dZay9QYVhyVE44ZTNxR2NKQWdNQkFBR2pXVEJYTUE0R0ExVWREd0VCL3dRRUF3SUNwREFQCkJnTlZIUk1CQWY4RUJUQURBUUgvTUIwR0ExVWREZ1FXQkJTYkI0SUhxaFFVZ1lmd1dNa21zUUQ3N3JBU2JUQVYKQmdOVkhSRUVEakFNZ2dwcmRXSmxjbTVsZEdWek1BMEdDU3FHU0liM0RRRUJDd1VBQTRJQkFRQWtOUmhLWUZvMwp5NUlseGhRSVBZckhrM1Y3UjZtUFpnejF2SmxjWmpKcEhVd0E1U3MwTDBRSkRmVmdvMVpJaUFiVmpPV1VYTHJmCjNsUXN1M2FJSDE3K2NuVnA3ZDJUZVhLd0NNbTVWTTFuK25CL1d4U1U4enVIWTQ1TEVVOVRFSG1FcDBaV25XSkwKcmFZbTZ0UWMvbzJLWGxla3JYTHBQa052TnR4QkFJQitsQ3NZbzd3ZGcxYXQ4NDJtNUx5Z1BvWkJIblptbTRDdQpzdWZPR1VTT3M5SkVxekUwdzhGYWU0NVJHc2o0RmM5QXR1QVF3a1V0cVdVcjNLT2lEcGtpZDUweHRuaVZQNTRZCncveU9CTUNGaXM4TklteTZ6cTMrVDQxV3krTnJQNVlOSzd0MHpEajYxdW1XMjJFVXZPU3U0ZktUU0ZZeldOTVoKblM3c2pqREZmTXVvCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K" + # Node group shape (hub). + nodegroupName: kata-sandbox + nodeRoleArn: arn:aws:iam::940019131157:role/hub-kata-node-role + subnetIds: + - subnet-0694e405e202f825e + - subnet-0f161be05a87d5335 + instanceType: c8i.4xlarge # 8i family β†’ exposes VT-x for nested virt + amiId: ami-055ebeb74702bc758 # EKS-optimized AL2023 for k8s 1.35 (x86_64) + minSize: 0 + desiredSize: 1 + maxSize: 3 + # Adopt the existing LaunchTemplate in place (external-name). + launchTemplateId: lt-0e204ea3e305e2e1f + launchTemplateVersion: "$Latest" diff --git a/gitops/overlays/environments/control-plane/enabled-addons.yaml b/gitops/overlays/environments/control-plane/enabled-addons.yaml index 244e30bd..5be21431 100644 --- a/gitops/overlays/environments/control-plane/enabled-addons.yaml +++ b/gitops/overlays/environments/control-plane/enabled-addons.yaml @@ -18,7 +18,7 @@ enabledAddons: aws_efs_csi_driver: false image_prepuller: false # GitOps - argo_events: false + argo_events: true # Dark Factory (Flow B) trigger: GitHub-webhook EventSource + Sensor β†’ df-run argo_rollouts: true argo_workflows: true kargo: false diff --git a/gitops/overlays/environments/dev/enabled-addons.yaml b/gitops/overlays/environments/dev/enabled-addons.yaml index 56ba38ff..0b5df4fc 100644 --- a/gitops/overlays/environments/dev/enabled-addons.yaml +++ b/gitops/overlays/environments/dev/enabled-addons.yaml @@ -22,3 +22,14 @@ enabledAddons: # Agentic Platform agent_platform: true bifrost: true + # Agent Sandbox capability β€” Kata micro-VM sandboxes + warm pool. + # Enabled on DEV (where the Dark Factory runs). ArgoCD manages the IN-CLUSTER + # pieces (operator, CRDs, RuntimeClasses, SandboxTemplate, pool-manager). + # The kata-capable node layer is NOT GitOps-managed β€” it requires a + # self-managed nested-virt MNG + the vpc-cni & kube-proxy addons provisioned + # out-of-band (AWS infra ArgoCD can't create). See + # gitops/addons/charts/agent-sandbox/nodepool/README.md for the proven recipe + # (validated live on spoke-dev 2026-07-10: real kata VM, guest kernel 6.18.35). + agent_sandbox: true + # kata-deploy installs the runtime on the kata MNG nodes (separate app). + agent_sandbox_kata: true diff --git a/gitops/overlays/environments/prod/enabled-addons.yaml b/gitops/overlays/environments/prod/enabled-addons.yaml index 89763f51..c4c89eeb 100644 --- a/gitops/overlays/environments/prod/enabled-addons.yaml +++ b/gitops/overlays/environments/prod/enabled-addons.yaml @@ -23,3 +23,8 @@ enabledAddons: # Agentic Platform agent_platform: true bifrost: true + # Agent Sandbox capability β€” DISABLED for now (same reason as dev): EKS Auto + # Mode + Bottlerocket cannot host Kata. Re-enable once Auto Mode integration + # is designed. When on, prod holds the capability but the pool stays dormant + # (the Dark Factory only runs on spoke-dev β€” production-safety). + # agent_sandbox: true diff --git a/platform/oam/definitions/components/agent.cue b/platform/oam/definitions/components/agent.cue index e2b6d062..dc767b51 100644 --- a/platform/oam/definitions/components/agent.cue +++ b/platform/oam/definitions/components/agent.cue @@ -37,18 +37,18 @@ template: { apiVersion: "argoproj.io/v1alpha1" kind: "Rollout" metadata: { - name: parameter.name - namespace: parameter.namespace + name: context.name + namespace: context.namespace labels: { - "app.kubernetes.io/name": parameter.name + "app.kubernetes.io/name": context.name "app.kubernetes.io/component": "ai-agent" } } spec: { replicas: parameter.replicas strategy: blueGreen: { - activeService: parameter.name + "-stable" - previewService: parameter.name + "-preview" + activeService: context.name + "-stable" + previewService: context.name + "-preview" autoPromotionEnabled: parameter.autoPromotionEnabled if parameter.autoPromotionSeconds != _|_ { autoPromotionSeconds: parameter.autoPromotionSeconds @@ -58,28 +58,24 @@ template: { } } selector: matchLabels: { - "app.kubernetes.io/name": parameter.name + "app.kubernetes.io/name": context.name } template: { metadata: labels: { - "app.kubernetes.io/name": parameter.name + "app.kubernetes.io/name": context.name } spec: { - serviceAccountName: parameter.serviceAccount + serviceAccountName: context.name containers: [{ - name: "agent" + name: context.name image: parameter.image - // Decentralized mode: wrap with opentelemetry-instrument for ADOT auto-instrumentation - if parameter.observability.mode == "decentralized" { - command: ["opentelemetry-instrument", "python", "-m", "app.main"] - } ports: [{ name: "a2a" containerPort: 8083 protocol: "TCP" }] env: [ - {name: "AGENT_NAME", value: parameter.name}, + {name: "AGENT_NAME", value: context.name}, {name: "AGENT_DESCRIPTION", value: parameter.description}, {name: "MODEL_ID", value: parameter.modelConfig.modelId}, {name: "SYSTEM_PROMPT", value: parameter.systemMessage}, @@ -87,7 +83,7 @@ template: { {name: "LLM_GATEWAY_URL", value: parameter.modelConfig.llmGatewayUrl}, {name: "LLM_GATEWAY_API_KEY", value: parameter.modelConfig.llmGatewayApiKey}, // Observability env vars β€” mode-dependent - {name: "OTEL_SERVICE_NAME", value: parameter.name}, + {name: "OTEL_SERVICE_NAME", value: context.name}, {name: "OTEL_TRACES_EXPORTER", value: "otlp"}, if parameter.observability.mode == "centralized" { {name: "OTEL_EXPORTER_OTLP_ENDPOINT", value: "http://otel-collector.otel.svc.cluster.local:4318"} @@ -102,7 +98,7 @@ template: { {name: "OTEL_EXPORTER_OTLP_PROTOCOL", value: "http/protobuf"} }, if parameter.observability.mode == "decentralized" { - {name: "OTEL_RESOURCE_ATTRIBUTES", value: "service.name=" + parameter.name} + {name: "OTEL_RESOURCE_ATTRIBUTES", value: "service.name=" + context.name} }, if parameter.observability.mode == "decentralized" { {name: "AGENT_OBSERVABILITY_ENABLED", value: "true"} @@ -146,17 +142,30 @@ template: { } outputs: { + // Dedicated ServiceAccount β€” the agent's single identity anchor. The + // gateway-identity and aws-service-identity traits attach their + // capabilities to this SA (name == context.name). + serviceAccount: { + apiVersion: "v1" + kind: "ServiceAccount" + metadata: { + name: context.name + namespace: context.namespace + labels: "app.kubernetes.io/name": context.name + } + } + // Stable service (active) stableService: { apiVersion: "v1" kind: "Service" metadata: { - name: parameter.name + "-stable" - namespace: parameter.namespace - labels: "app.kubernetes.io/name": parameter.name + name: context.name + "-stable" + namespace: context.namespace + labels: "app.kubernetes.io/name": context.name } spec: { - selector: "app.kubernetes.io/name": parameter.name + selector: "app.kubernetes.io/name": context.name ports: [{ name: "a2a", port: 8083, targetPort: 8083, protocol: "TCP" appProtocol: "kgateway.dev/a2a" @@ -170,12 +179,12 @@ template: { apiVersion: "v1" kind: "Service" metadata: { - name: parameter.name + "-preview" - namespace: parameter.namespace - labels: "app.kubernetes.io/name": parameter.name + name: context.name + "-preview" + namespace: context.namespace + labels: "app.kubernetes.io/name": context.name } spec: { - selector: "app.kubernetes.io/name": parameter.name + selector: "app.kubernetes.io/name": context.name ports: [{ name: "a2a", port: 8083, targetPort: 8083, protocol: "TCP" appProtocol: "kgateway.dev/a2a" @@ -189,15 +198,15 @@ template: { apiVersion: "v1" kind: "ConfigMap" metadata: { - name: parameter.name + "-card" - namespace: parameter.namespace + name: context.name + "-card" + namespace: context.namespace labels: { - "app.kubernetes.io/name": parameter.name + "app.kubernetes.io/name": context.name "agent.dev/type": "agent-card" } } data: { - name: parameter.name + name: context.name description: parameter.description model: parameter.modelConfig.modelId } @@ -215,9 +224,9 @@ template: { apiVersion: "gateway.networking.k8s.io/v1" kind: "HTTPRoute" metadata: { - name: parameter.name - namespace: parameter.namespace - labels: "app.kubernetes.io/name": parameter.name + name: context.name + namespace: context.namespace + labels: "app.kubernetes.io/name": context.name } spec: { parentRefs: [{ @@ -228,7 +237,7 @@ template: { matches: [{ path: { type: "PathPrefix" - value: "/" + parameter.name + value: "/" + context.name } }] filters: [{ @@ -239,9 +248,9 @@ template: { } }] backendRefs: [{ - name: parameter.name + "-stable" + name: context.name + "-stable" port: 8083 - namespace: parameter.namespace + namespace: context.namespace }] }] } @@ -252,8 +261,6 @@ template: { parameter: { // Required fields - name: string - namespace: string description: string systemMessage: string @@ -261,8 +268,7 @@ template: { image: *"public.ecr.aws/z0a4o2j5/strands-agent:latest" | string // Optional fields with defaults - replicas: *3 | int - serviceAccount: *"default" | string + replicas: *3 | int // Blue-green deployment settings autoPromotionEnabled: *true | bool diff --git a/platform/oam/definitions/components/agentcore-memory.cue b/platform/oam/definitions/components/agentcore-memory.cue index 77ef8a59..c9d2c2b9 100644 --- a/platform/oam/definitions/components/agentcore-memory.cue +++ b/platform/oam/definitions/components/agentcore-memory.cue @@ -22,7 +22,7 @@ template: { let _autoName = strings.Replace(context.namespace + "_" + context.name, "-", "_", -1) output: { - apiVersion: "bedrockagentcore.aws.m.upbound.io/v1beta1" + apiVersion: "bedrockagentcore.aws.upbound.io/v1beta1" kind: "Memory" metadata: name: context.name spec: { @@ -32,10 +32,7 @@ template: { description: parameter.description eventExpiryDuration: parameter.eventExpiryDuration } - providerConfigRef: { - name: "provider-aws-config" - kind: "ClusterProviderConfig" - } + providerConfigRef: name: "default" } } @@ -67,7 +64,7 @@ template: { } """ } - providerConfigRef: name: "provider-aws-config" + providerConfigRef: name: "default" } } diff --git a/platform/oam/definitions/traits/aws-service-identity.cue b/platform/oam/definitions/traits/aws-service-identity.cue index c9e41cec..44727d99 100644 --- a/platform/oam/definitions/traits/aws-service-identity.cue +++ b/platform/oam/definitions/traits/aws-service-identity.cue @@ -81,7 +81,7 @@ template: { policyArnRef: name: "\(context.appName)-\(c)-iam-policy" role: "\(context.name)-role" } - providerConfigRef: name: "provider-aws-config" + providerConfigRef: name: "default" } } } diff --git a/platform/oam/examples/example-agent-agentcore-memory.yaml b/platform/oam/examples/example-agent-agentcore-memory.yaml index 62930f15..4c20eb6c 100644 --- a/platform/oam/examples/example-agent-agentcore-memory.yaml +++ b/platform/oam/examples/example-agent-agentcore-memory.yaml @@ -1,13 +1,13 @@ -# Example: Agent with AgentCore memory provisioned via Crossplane managed resource +# Example: Agent with AgentCore memory, using identity traits (no dp-service-account). # # Flow: -# 1. agentcore-memory provisions a Memory via Crossplane bedrockagentcore provider, -# and emits a ComponentPolicy claim for IAM permissions. -# The memoryId is passed to the agent via outputs/inputs. -# 2. dp-service-account creates IAM Role + ServiceAccount + PodIdentityAssociation, -# then emits PolicyAttachment claims to wire the permissions -# 3. agent references the ServiceAccount and gets AWS credentials via pod identity -# +# 1. agentcore-memory provisions a Memory via Crossplane and emits an IAM policy +# (--iam-policy) for accessing it; memoryId is passed to the agent. +# 2. The agent owns its ServiceAccount (context.name). The aws-service-identity trait +# creates the IAM role + PodIdentityAssociation via the XPodIdentity Composition +# (clusterName/region resolved from the env-config EnvironmentConfig β€” no params) +# and attaches the memory policy via accessFor. +# 3. The gateway-identity trait gives the agent a projected token for AgentGateway/MCP. apiVersion: core.oam.dev/v1beta1 kind: Application metadata: @@ -25,30 +25,28 @@ spec: description: "Memory for my-agent" eventExpiryDuration: 30 - - name: my-agent-sa - type: dp-service-account - properties: - componentNamesForAccess: - - agentcore-short-memory - clusterName: hub - clusterRegion: us-west-2 - - - name: my-agent-with-mem + # Component name (my-agent) is the identity: Rollout/Service/SA/container all + # named my-agent; the traits attach AWS + gateway identity to that same SA. + - name: my-agent type: agent dependsOn: - - my-agent-sa - agentcore-short-memory inputs: - from: memory-id parameterKey: properties.memory.config.memoryId properties: - name: my-agent - namespace: agents description: "Assistant agent with persistent memory" systemMessage: "You are a helpful assistant." - serviceAccount: my-agent-sa memory: provider: agentcore config: memoryId: "injected-from-agentcore-memory-output" region: us-west-2 + traits: + # Secretless identity to AgentGateway/MCP. + - type: gateway-identity + # Secretless AWS IAM identity (EKS Pod Identity); attach the memory policy. + - type: aws-service-identity + properties: + accessFor: + - agentcore-short-memory diff --git a/platform/oam/examples/example-agent-centralized-observability.yaml b/platform/oam/examples/example-agent-centralized-observability.yaml index a7a05ac9..74557a42 100644 --- a/platform/oam/examples/example-agent-centralized-observability.yaml +++ b/platform/oam/examples/example-agent-centralized-observability.yaml @@ -23,8 +23,6 @@ spec: - name: my-agent type: agent properties: - name: my-agent - namespace: default description: "Agent with centralized observability (Langfuse)" replicas: 1 systemMessage: "You are a helpful assistant." diff --git a/platform/oam/examples/example-agent-decentralized-observability.yaml b/platform/oam/examples/example-agent-decentralized-observability.yaml index a0fa41ed..1b67336b 100644 --- a/platform/oam/examples/example-agent-decentralized-observability.yaml +++ b/platform/oam/examples/example-agent-decentralized-observability.yaml @@ -24,8 +24,6 @@ spec: - name: my-cw-agent type: agent properties: - name: my-cw-agent - namespace: default description: "Agent with CloudWatch GenAI observability" replicas: 1 systemMessage: "You are a helpful assistant." @@ -42,3 +40,6 @@ spec: limits: cpu: 500m memory: 512Mi + traits: + # CloudWatch/X-Ray IAM identity for ADOT export (EKS Pod Identity). + - type: decentralized-observability-identity diff --git a/platform/oam/examples/example-agent-milvus-memory.yaml b/platform/oam/examples/example-agent-milvus-memory.yaml index b8de701a..45e14e76 100644 --- a/platform/oam/examples/example-agent-milvus-memory.yaml +++ b/platform/oam/examples/example-agent-milvus-memory.yaml @@ -8,8 +8,6 @@ spec: - name: assistant type: agent properties: - name: assistant - namespace: agents description: "AI assistant with Milvus-backed mem0 memory" systemMessage: | @@ -19,7 +17,7 @@ spec: modelConfig: modelId: claude-sonnet - # Memory β€” mem0 with Milvus vector store + # Memory β€” mem0 with Milvus vector store (in-cluster; no AWS identity needed) memory: provider: milvus config: @@ -35,3 +33,6 @@ spec: limits: cpu: 1000m memory: 1Gi + traits: + # Secretless identity to AgentGateway/MCP. + - type: gateway-identity diff --git a/platform/oam/examples/example-agent-minimal.yaml b/platform/oam/examples/example-agent-minimal.yaml index bc60abd8..39d082f7 100644 --- a/platform/oam/examples/example-agent-minimal.yaml +++ b/platform/oam/examples/example-agent-minimal.yaml @@ -5,20 +5,15 @@ metadata: namespace: default spec: components: + # Component name is the agent identity (context.name). - name: simple-agent type: agent properties: - name: simple-agent - namespace: default description: "Minimal AI assistant configuration" - - # Only required fields + # Only required field systemMessage: "You are a helpful AI assistant." - - # Everything else uses defaults: - # - image: 498530348755.dkr.ecr.us-east-1.amazonaws.com/strands-agent:latest - # - replicas: 3 - # - modelId: claude-sonnet - # - llmGatewayUrl: http://bifrost.bifrost.svc.cluster.local:8080/v1 - # - autoPromotionEnabled: true - # - registerWithGateway: true + # Everything else uses defaults (image, replicas, modelId=claude-sonnet, + # llmGatewayUrl, autoPromotionEnabled, registerWithGateway). + traits: + # Secretless identity to AgentGateway/MCP. + - type: gateway-identity diff --git a/platform/oam/examples/example-agent-simple.yaml b/platform/oam/examples/example-agent-simple.yaml index 0b13e431..587f43ab 100644 --- a/platform/oam/examples/example-agent-simple.yaml +++ b/platform/oam/examples/example-agent-simple.yaml @@ -2,51 +2,40 @@ apiVersion: core.oam.dev/v1beta1 kind: Application metadata: name: my-assistant - namespace: default + namespace: default # β†’ context.namespace for the component's resources spec: components: - - name: assistant + # The component name IS the agent's identity (context.name): it names the + # Rollout, Services, ServiceAccount, container, AGENT_NAME and gateway route. + - name: oap-assistant-a type: agent properties: - name: oap-assistant-a - namespace: default description: "General purpose AI assistant with A2A protocol support" - - # agent image - # image: 498530348755.dkr.ecr.us-west-2.amazonaws.com/strands-agent:latest - - # Multiple replicas with consistent hashing + + # agent image (defaults to the public strands-agent image) + # image: public.ecr.aws/z0a4o2j5/strands-agent:latest + replicas: 3 - - # System prompt + systemMessage: | You are a helpful AI assistant with access to various tools and capabilities. You can help with general questions, data analysis, and task automation. Always be clear, concise, and helpful in your responses. When asked about time use the tools. - - # Model configuration + modelConfig: modelId: claude-sonnet - - # Service account for AWS permissions (if needed) - serviceAccount: default - + # Blue-green deployment settings autoPromotionEnabled: true autoPromotionSeconds: 10 scaleDownDelaySeconds: 30 - + # AgentGateway registration registerWithGateway: true - # Memory β€” AgentCore (native Strands session manager, not mem0) - # memory: - # provider: agentcore - # config: - # memoryId: my-agentcore-mem - # region: us-west-2 + mcpServers: - name: mcp-time - # Resource limits + resources: requests: cpu: 500m @@ -54,3 +43,7 @@ spec: limits: cpu: 1000m memory: 1Gi + traits: + # Secretless identity to AgentGateway/MCP: projected ServiceAccount token + # (audience agentgateway) validated by the gateway against the cluster OIDC. + - type: gateway-identity diff --git a/platform/oam/examples/example-agent-with-mcp.yaml b/platform/oam/examples/example-agent-with-mcp.yaml index e106ab3c..55adb740 100644 --- a/platform/oam/examples/example-agent-with-mcp.yaml +++ b/platform/oam/examples/example-agent-with-mcp.yaml @@ -5,46 +5,34 @@ metadata: namespace: default spec: components: - - name: memory - type: memory - name: tool-agent type: agent properties: - name: tool-agent - namespace: default description: "AI assistant with MCP tool integration" - + replicas: 3 - + systemMessage: | You are an AI assistant with access to various tools via MCP. You can check weather, search the web, and perform calculations. Use tools when appropriate to provide accurate information. - + modelConfig: modelId: claude-sonnet - region: us-west-2 llmGatewayUrl: http://bifrost.bifrost.svc.cluster.local:8080/v1 - llmGatewayApiKey: sk-1234 - - # MCP tool servers + + # MCP tool servers (reached through AgentGateway) mcpServers: - name: weather-tools - name: web-search - name: calculator - agents: - - name: design-agent - - - serviceAccount: default - + autoPromotionEnabled: true autoPromotionSeconds: 10 scaleDownDelaySeconds: 30 - + registerWithGateway: true - gatewayNamespace: agentgateway-system - + resources: requests: cpu: 500m @@ -52,3 +40,6 @@ spec: limits: cpu: 1000m memory: 1Gi + traits: + # Secretless identity to AgentGateway/MCP. + - type: gateway-identity