# Helm Chart Layout

> Structure of the Helm chart: CRD shipping, deployment template, controller-args helper, RBAC bindings, and values knobs that map to controller flags.

- Repository: kubernetes-sigs/agent-sandbox
- GitHub: https://github.com/kubernetes-sigs/agent-sandbox
- Human wiki: https://grok-wiki.com/public/wiki/kubernetes-sigs-agent-sandbox-c3f2597a654a
- Complete Markdown: https://grok-wiki.com/public/wiki/kubernetes-sigs-agent-sandbox-c3f2597a654a/llms-full.txt

## Source Files

- `helm/Chart.yaml`
- `helm/values.yaml`
- `helm/templates/deployment.yaml`
- `helm/templates/_controller-args.tpl`
- `helm/README.md`
- `helm/crds/agents.x-k8s.io_sandboxes.yaml`

---

<details>
<summary>Relevant source files</summary>
The following files were used as context for generating this wiki page:
- [helm/Chart.yaml](helm/Chart.yaml)
- [helm/values.yaml](helm/values.yaml)
- [helm/README.md](helm/README.md)
- [helm/templates/deployment.yaml](helm/templates/deployment.yaml)
- [helm/templates/_controller-args.tpl](helm/templates/_controller-args.tpl)
- [helm/templates/_helpers.tpl](helm/templates/_helpers.tpl)
- [helm/templates/serviceaccount.yaml](helm/templates/serviceaccount.yaml)
- [helm/templates/service.yaml](helm/templates/service.yaml)
- [helm/templates/namespace.yaml](helm/templates/namespace.yaml)
- [helm/templates/rbac.generated.yaml](helm/templates/rbac.generated.yaml)
- [helm/templates/extensions-rbac.generated.yaml](helm/templates/extensions-rbac.generated.yaml)
- [helm/templates/clusterrolebinding.yaml](helm/templates/clusterrolebinding.yaml)
- [helm/templates/clusterrolebinding-extensions.yaml](helm/templates/clusterrolebinding-extensions.yaml)
- [helm/crds/agents.x-k8s.io_sandboxes.yaml](helm/crds/agents.x-k8s.io_sandboxes.yaml)
- [helm/crds/extensions.agents.x-k8s.io_sandboxclaims.yaml](helm/crds/extensions.agents.x-k8s.io_sandboxclaims.yaml)
- [helm/crds/extensions.agents.x-k8s.io_sandboxtemplates.yaml](helm/crds/extensions.agents.x-k8s.io_sandboxtemplates.yaml)
- [helm/crds/extensions.agents.x-k8s.io_sandboxwarmpools.yaml](helm/crds/extensions.agents.x-k8s.io_sandboxwarmpools.yaml)
</details>

# Helm Chart Layout

The `helm/` directory ships the agent-sandbox controller as a self-contained Helm chart named `agent-sandbox` (`type: application`, `version: 0.1.0`). The chart bundles the four custom resource definitions used by the controller, a single `Deployment` for the controller pod, a `Service` exposing the metrics endpoint, RBAC primitives (a `ServiceAccount`, two generated `ClusterRole`s, and the matching `ClusterRoleBinding`s), an optional `Namespace`, and a controller-args helper template that translates `values.yaml` knobs into command-line flags. This page maps every file in the chart to the runtime object it produces and shows how `controller.*` values become `args:` on the controller container.

The chart is intentionally small: there are no subcharts, no hooks, and no shared configmaps. Templating is concentrated in two named templates (`agent-sandbox.controllerArgs` and the helpers in `_helpers.tpl`); every other file is a thin wrapper around a single Kubernetes object.

Sources: [helm/Chart.yaml:1-15](), [helm/README.md:1-67]()

## Directory Layout

```text
helm/
├── Chart.yaml                    # chart metadata: name, version, sources
├── values.yaml                   # default knobs (image, controller flags, pod overrides)
├── README.md                     # install/upgrade docs + parameter reference
├── crds/                         # auto-installed by Helm before templates
│   ├── agents.x-k8s.io_sandboxes.yaml
│   ├── extensions.agents.x-k8s.io_sandboxclaims.yaml
│   ├── extensions.agents.x-k8s.io_sandboxtemplates.yaml
│   └── extensions.agents.x-k8s.io_sandboxwarmpools.yaml
└── templates/
    ├── _helpers.tpl              # name/namespace/labels/image helpers
    ├── _controller-args.tpl      # controller flag generator
    ├── namespace.yaml            # gated on namespace.create
    ├── serviceaccount.yaml
    ├── deployment.yaml           # the controller pod
    ├── service.yaml              # metrics service (8080)
    ├── rbac.generated.yaml       # core ClusterRole (Sandbox)
    ├── extensions-rbac.generated.yaml  # extensions ClusterRole
    ├── clusterrolebinding.yaml
    └── clusterrolebinding-extensions.yaml  # gated on controller.extensions
```

The `.generated.yaml` suffix on the two RBAC role files signals that the manifests are produced by upstream tooling (`controller-gen`) and copied into the chart; do not edit them by hand. The `clusterrolebinding*.yaml` files are hand-written because they reference Helm release context (`{{ include "agent-sandbox.namespace" . }}`).

Sources: [helm/templates/rbac.generated.yaml:1-60](), [helm/templates/extensions-rbac.generated.yaml:1-88](), [helm/templates/clusterrolebinding.yaml:1-15]()

## CRD Shipping

CRDs live under `helm/crds/`, which is a Helm-special directory: every file there is applied **before** any template renders, exactly once, on first `helm install`. Helm intentionally does **not** upgrade or delete CRDs across releases, so the chart's `README.md` documents the manual `kubectl apply -f helm/crds/` step for upgrades and the manual `kubectl delete -f helm/crds/` step for full removal.

Four CRDs ship with the chart:

| File | API group | Kind | Scope |
|------|-----------|------|-------|
| `agents.x-k8s.io_sandboxes.yaml` | `agents.x-k8s.io` | `Sandbox` | Namespaced |
| `extensions.agents.x-k8s.io_sandboxclaims.yaml` | `extensions.agents.x-k8s.io` | `SandboxClaim` | Namespaced |
| `extensions.agents.x-k8s.io_sandboxtemplates.yaml` | `extensions.agents.x-k8s.io` | `SandboxTemplate` | Namespaced |
| `extensions.agents.x-k8s.io_sandboxwarmpools.yaml` | `extensions.agents.x-k8s.io` | `SandboxWarmPool` | Namespaced |

The CRDs always install — there is no value gate around `crds/` (Helm does not template that directory). The three extension CRDs are installed even when `controller.extensions=false`; what the flag actually gates is the *controller* logic that reconciles them and the extension RBAC binding (see [Conditional Templates](#conditional-templates)).

Each CRD carries the `controller-gen.kubebuilder.io/version: v0.19.0` annotation, confirming they are generated artifacts mirrored into the chart rather than maintained inline.

Sources: [helm/README.md:48-66](), [helm/crds/agents.x-k8s.io_sandboxes.yaml:1-18](), [helm/crds/extensions.agents.x-k8s.io_sandboxclaims.yaml:1-17]()

## Rendered Object Graph

```mermaid
flowchart LR
    subgraph Values["values.yaml"]
        VImg["image.{repository,tag,pullPolicy}"]
        VNs["namespace.{create,name}"]
        VCtrl["controller.*"]
        VPod["replicaCount / podAnnotations<br/>podLabels / resources<br/>nodeSelector / tolerations / affinity"]
    end

    subgraph Helpers["templates/_*.tpl"]
        HArgs["agent-sandbox.controllerArgs"]
        HHelp["agent-sandbox.{name,namespace,<br/>labels,selectorLabels,image}"]
    end

    subgraph Workload["Controller workload"]
        Ns["Namespace<br/>(if namespace.create)"]
        SA["ServiceAccount<br/>agent-sandbox-controller"]
        Dep["Deployment<br/>agent-sandbox-controller"]
        Svc["Service :8080 metrics"]
    end

    subgraph RBAC["Cluster-scoped RBAC"]
        CRcore["ClusterRole<br/>agent-sandbox-controller"]
        CRext["ClusterRole<br/>agent-sandbox-controller-extensions"]
        CRBcore["ClusterRoleBinding<br/>core"]
        CRBext["ClusterRoleBinding<br/>extensions (gated)"]
    end

    subgraph CRDs["helm/crds/ (pre-install, once)"]
        CRDsb["Sandbox CRD"]
        CRDsc["SandboxClaim CRD"]
        CRDst["SandboxTemplate CRD"]
        CRDsw["SandboxWarmPool CRD"]
    end

    VCtrl --> HArgs --> Dep
    VImg --> HHelp --> Dep
    VPod --> Dep
    VNs --> Ns
    VNs --> HHelp

    SA --> Dep
    SA --> CRBcore
    SA --> CRBext
    CRcore --> CRBcore
    CRext --> CRBext
    Svc -. selects .-> Dep
```

The diagram traces three independent value→object pathways: `controller.*` knobs flow through the `agent-sandbox.controllerArgs` helper into the `Deployment`'s `args:`; `image.*` and naming knobs flow through `_helpers.tpl` into image and label fields; and `namespace.*` controls both the optional `Namespace` object and the namespace placement for every other object via `agent-sandbox.namespace`.

Sources: [helm/templates/deployment.yaml:1-65](), [helm/templates/_helpers.tpl:1-43](), [helm/values.yaml:1-63]()

## The Controller Deployment

`helm/templates/deployment.yaml` renders a single `Deployment` named `agent-sandbox-controller`. Notable wiring:

- `replicas: {{ .Values.replicaCount }}` — defaults to 1; leader election (on by default) makes >1 safe.
- `serviceAccountName: agent-sandbox-controller` — hard-coded, matches `serviceaccount.yaml` and both `ClusterRoleBinding`s.
- `image: {{ include "agent-sandbox.image" . }}` — uses the `_helpers.tpl` builder, which `require`s a non-empty `image.tag` and produces `repository:tag`.
- `args:` is rendered entirely by `{{- include "agent-sandbox.controllerArgs" . | nindent 8 }}`.
- Two named container ports: `metrics` on 8080 and `healthz` on 8081. The `Service` only exposes the metrics port.
- `livenessProbe` hits `GET /healthz` on the `healthz` port (15s initial delay, 20s period); `readinessProbe` hits `GET /readyz` on the same port (5s/10s).
- Pod-level overrides — `resources`, `nodeSelector`, `affinity`, `tolerations` — are all rendered behind `{{- with }}` guards, so an empty value renders nothing rather than an empty field.

Pod template labels merge the chart's `selectorLabels` with `Values.podLabels` so user labels coexist with the immutable selector:

```yaml
{{- $selectorLabels := include "agent-sandbox.selectorLabels" . | fromYaml }}
labels:
  {{- toYaml (merge (dict) $selectorLabels .Values.podLabels) | nindent 8 }}
```

The order of arguments to `merge` matters: in Helm's `merge`, earlier maps win on conflict, so selector labels take precedence over user-supplied `podLabels` — preventing a stray override from breaking selector matching.

Sources: [helm/templates/deployment.yaml:1-65](), [helm/templates/_helpers.tpl:31-42]()

## The controller-args Helper

`templates/_controller-args.tpl` defines the named template `agent-sandbox.controllerArgs`, which is the single source of truth for converting `controller.*` values into CLI flags. Each block follows the same pattern: `{{- if .Values.controller.X }}` guards a line that emits `- --flag-name={{ value }}`.

```text
controller.<key>                       --> --<flag-name>=<value>
─────────────────────────────────────────────────────────────────
leaderElect                            --> --leader-elect
clusterDomain                          --> --cluster-domain
leaderElectionNamespace                --> --leader-election-namespace
extensions                             --> --extensions
enableTracing                          --> --enable-tracing
enablePprof                            --> --enable-pprof
enablePprofDebug                       --> --enable-pprof-debug
pprofBlockProfileRate                  --> --pprof-block-profile-rate
pprofMutexProfileFraction              --> --pprof-mutex-profile-fraction
kubeApiQps                             --> --kube-api-qps
kubeApiBurst                           --> --kube-api-burst
sandboxConcurrentWorkers               --> --sandbox-concurrent-workers
sandboxClaimConcurrentWorkers          --> --sandbox-claim-concurrent-workers
sandboxWarmPoolConcurrentWorkers       --> --sandbox-warm-pool-concurrent-workers
sandboxTemplateConcurrentWorkers       --> --sandbox-template-concurrent-workers
extraArgs[]                            --> verbatim, one per element (quoted)
```

Two consequences of the `{{- if }}` guards:

1. **Unset values produce no flag.** The controller's own defaults apply for any key the user does not set. `values.yaml` deliberately leaves most knobs commented out (`# clusterDomain: "cluster.local"`, `# kubeApiQps: -1.0`, …) so the rendered `args:` list stays minimal.
2. **Falsy values suppress the flag.** Because the guard is `if`, not `not (kindIs "invalid" ...)`, a setting like `controller.leaderElect: false` will *omit* `--leader-elect=false` rather than emit it. This is intentional for the boolean toggles but worth noting when overriding defaults.

`extraArgs` is the escape hatch for any flag the helper does not cover (the chart's example use case is `zap` logging flags). Each element is emitted verbatim and quoted:

```yaml
{{- range .Values.controller.extraArgs }}
- {{ . | quote }}
{{- end }}
```

Sources: [helm/templates/_controller-args.tpl:1-50](), [helm/values.yaml:29-49]()

## Values Knobs at a Glance

The full reference lives in `helm/README.md`; the table below highlights the knobs that the helper translates into flags, plus the ones that shape the pod itself.

| Value | Default | Effect |
|-------|---------|--------|
| `image.tag` | `""` | **Required.** Enforced by `required "image.tag is required"` in `agent-sandbox.image`. |
| `image.repository` | `registry.k8s.io/agent-sandbox/agent-sandbox-controller` | Image repository half of the ref. |
| `replicaCount` | `1` | `Deployment.spec.replicas`. |
| `namespace.create` | `true` | Gates rendering of `namespace.yaml`. |
| `namespace.name` | `agent-sandbox-system` | Overrides `Release.Namespace` for every object. |
| `controller.leaderElect` | `true` | Emits `--leader-elect=true`. |
| `controller.extensions` | `false` | Emits `--extensions=true` **and** renders the extensions `ClusterRoleBinding`. |
| `controller.enableTracing` | unset | Emits `--enable-tracing=true` when truthy. |
| `controller.enablePprof` / `enablePprofDebug` | unset | Toggle pprof endpoint on the metrics port. |
| `controller.kubeApiQps` / `kubeApiBurst` | unset | Tune client-go rate limits. |
| `controller.sandbox*ConcurrentWorkers` | unset | Reconciler concurrency per controller. |
| `controller.extraArgs` | `[]` | Free-form passthrough flags. |
| `podAnnotations` / `podLabels` | `{}` | Added to the pod template; selector labels win on label conflict. |
| `resources` / `nodeSelector` / `tolerations` / `affinity` | `{}` / `[]` | Standard pod overrides, all `{{- with }}`-guarded. |

Sources: [helm/values.yaml:1-63](), [helm/README.md:72-101](), [helm/templates/_helpers.tpl:39-42]()

## Naming and Label Helpers

`_helpers.tpl` defines five named templates that the rest of the chart depends on:

| Helper | Purpose |
|--------|---------|
| `agent-sandbox.name` | Truncates chart name (or `nameOverride`) to 63 chars. Used inside the label helpers. |
| `agent-sandbox.namespace` | `default .Release.Namespace .Values.namespace.name` — every object uses this to resolve its namespace. |
| `agent-sandbox.labels` | Common labels: `helm.sh/chart`, `app.kubernetes.io/{name,instance,managed-by}`, plus `app.kubernetes.io/version` when `image.tag` is set. |
| `agent-sandbox.selectorLabels` | Stable subset (`name` + `instance`) used by both `Deployment.selector` and `Service.selector`. |
| `agent-sandbox.image` | `repository:tag`; calls `required` on `image.tag` so installs fail fast when the user forgets to set it. |

The selector helper is deliberately narrower than the labels helper: `Deployment.spec.selector` is immutable after creation, so it must not include chart-version-derived labels.

Sources: [helm/templates/_helpers.tpl:1-43]()

## Service, ServiceAccount, and Namespace

These three templates are minimal:

- `serviceaccount.yaml` creates `agent-sandbox-controller` in the chart's namespace. No annotations are templated, so workload-identity wiring is not built in.
- `service.yaml` is a ClusterIP `Service` named `agent-sandbox-controller` exposing only the `metrics` port (8080 → named target port `metrics`). There is no service for the `healthz` port — probes go straight to the pod.
- `namespace.yaml` is wrapped entirely in `{{- if .Values.namespace.create }}`. When `false`, no namespace is rendered and the user is expected to point the release at a pre-existing namespace (the README's "Install into an existing namespace" path).

Sources: [helm/templates/service.yaml:1-16](), [helm/templates/serviceaccount.yaml:1-8](), [helm/templates/namespace.yaml:1-9]()

## RBAC Bindings

Cluster-scoped RBAC ships as two independent role/binding pairs:

```text
ServiceAccount: agent-sandbox-controller (in namespace)
        │
        ├── ClusterRoleBinding: agent-sandbox-controller
        │       └── ClusterRole: agent-sandbox-controller            (always)
        │              ├── core:           pods, services, persistentvolumeclaims (CRUD+watch)
        │              ├── agents.x-k8s.io: sandboxes (CRUD+watch),
        │              │                    sandboxes/{finalizers,status} (get/patch/update)
        │              ├── coordination.k8s.io: leases (CRU+watch)
        │              └── events.k8s.io: events (create/patch)
        │
        └── ClusterRoleBinding: agent-sandbox-controller-extensions   (if controller.extensions)
                └── ClusterRole: agent-sandbox-controller-extensions  (always present in chart)
                       ├── core: pods (read+patch+update+watch), events (create/patch/update)
                       ├── coordination.k8s.io: leases (CRUD+watch)
                       ├── extensions.agents.x-k8s.io: sandboxclaims, sandboxtemplates,
                       │                                sandboxwarmpools (CRUD+watch) and
                       │                                their finalizers/status subresources
                       ├── agents.x-k8s.io: sandboxes (CRUD+watch)
                       └── networking.k8s.io: networkpolicies (CRUD+watch)
```

A subtlety: `extensions-rbac.generated.yaml` (the `ClusterRole`) is **not** gated on `controller.extensions`, but `clusterrolebinding-extensions.yaml` **is**. The role is therefore present even when extensions are disabled — it simply has no subjects bound to it until the user opts in. Both binding files reference `agent-sandbox-controller` as the only subject and the matching `ClusterRole` name as `roleRef`.

Sources: [helm/templates/rbac.generated.yaml:1-60](), [helm/templates/extensions-rbac.generated.yaml:1-88](), [helm/templates/clusterrolebinding.yaml:1-15](), [helm/templates/clusterrolebinding-extensions.yaml:1-16]()

## Conditional Templates

Only a handful of templates have install-time gates; the rest always render.

| Template | Gate | Behavior when gate is false |
|----------|------|-----------------------------|
| `namespace.yaml` | `Values.namespace.create` | Nothing rendered; existing namespace is reused via `agent-sandbox.namespace`. |
| `clusterrolebinding-extensions.yaml` | `Values.controller.extensions` | The extensions `ClusterRole` exists but is unbound; the controller flag is also omitted. |
| `_controller-args.tpl` entries | per-key `{{- if .Values.controller.X }}` | Flag is omitted; controller falls back to its own default. |
| `Deployment` pod fields | `{{- with }}` on `resources` / `nodeSelector` / `affinity` / `tolerations` / `podAnnotations` | Field is omitted entirely from the rendered manifest. |

Both `namespace.yaml` and `clusterrolebinding-extensions.yaml` use a top-level `{{- if … }} … {{- end }}` wrapper, so disabling them yields zero output (no empty document separator) and Helm produces a clean manifest.

Sources: [helm/templates/namespace.yaml:1-9](), [helm/templates/clusterrolebinding-extensions.yaml:1-16](), [helm/templates/deployment.yaml:49-64]()

## End-to-End: From values to args

The flow below shows what the user sets, how it threads through the chart, and what lands inside the running pod's argv. This is the path a maintainer follows when adding a new controller flag.

```mermaid
sequenceDiagram
    autonumber
    actor User
    participant Vals as values.yaml
    participant Tpl as templates/_controller-args.tpl
    participant Dep as templates/deployment.yaml
    participant K8s as kube-apiserver
    participant Pod as agent-sandbox-controller pod

    User->>Vals: --set controller.extensions=true<br/>--set controller.kubeApiQps=50
    Vals->>Tpl: render agent-sandbox.controllerArgs
    Note over Tpl: emits --extensions=true and<br/>--kube-api-qps=50,<br/>skips guarded-out flags
    Tpl-->>Dep: include via {{ include "agent-sandbox.controllerArgs" . }}
    Dep->>K8s: apply Deployment with args[]
    K8s->>Pod: start container with rendered argv
    Pod->>Pod: parse flags and start reconcilers
```

To add a new flag, append a guarded block to `_controller-args.tpl`, add the default (or commented-out placeholder) to `values.yaml`, and document it in `helm/README.md`'s configuration table — no other template needs to change.

Sources: [helm/templates/_controller-args.tpl:1-50](), [helm/templates/deployment.yaml:28-29](), [helm/values.yaml:29-49]()

## Summary

The agent-sandbox Helm chart is a deliberately thin packaging of one controller `Deployment`, its `Service`, a `ServiceAccount`, two generated `ClusterRole`s with their bindings, an optional `Namespace`, and four CRDs shipped under the auto-installed `crds/` directory. All controller-tuning lives in `controller.*` values, and the `agent-sandbox.controllerArgs` named template is the single junction that turns those values into command-line flags — keeping the deployment template itself almost free of templating logic. The `controller.extensions` toggle is the one knob with multi-object impact: it both injects `--extensions=true` and renders the extensions `ClusterRoleBinding`, while the extension CRDs and `ClusterRole` always ship.
