# Static Manifests & Generated RBAC

> The kubectl-apply-ready manifests in k8s/ plus the generated ClusterRole/Binding files used by both core and extensions controllers.

- 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

- `k8s/controller.yaml`
- `k8s/extensions.controller.yaml`
- `k8s/extensions.yaml`
- `k8s/rbac.generated.yaml`
- `k8s/extensions-rbac.generated.yaml`

---

<details>
<summary>Relevant source files</summary>
The following files were used as context for generating this wiki page:
- [k8s/controller.yaml](k8s/controller.yaml)
- [k8s/extensions.controller.yaml](k8s/extensions.controller.yaml)
- [k8s/extensions.yaml](k8s/extensions.yaml)
- [k8s/rbac.generated.yaml](k8s/rbac.generated.yaml)
- [k8s/extensions-rbac.generated.yaml](k8s/extensions-rbac.generated.yaml)
- [codegen.go](codegen.go)
- [dev/tools/deploy-to-kube](dev/tools/deploy-to-kube)
- [controllers/sandbox_controller.go](controllers/sandbox_controller.go)
- [extensions/controllers/sandboxclaim_controller.go](extensions/controllers/sandboxclaim_controller.go)
- [extensions/controllers/sandboxtemplate_controller.go](extensions/controllers/sandboxtemplate_controller.go)
- [extensions/controllers/sandboxwarmpool_controller.go](extensions/controllers/sandboxwarmpool_controller.go)
- [cmd/agent-sandbox-controller/main.go](cmd/agent-sandbox-controller/main.go)
</details>

# Static Manifests & Generated RBAC

The `k8s/` directory is the canonical, `kubectl apply`-ready home for the agent-sandbox controller's cluster install: it bundles the `Namespace`, `ServiceAccount`, `Service`, `Deployment`, and `ClusterRoleBinding` for the controller, together with machine-generated `CustomResourceDefinition` and `ClusterRole` files. The split into hand-written and `controller-gen`–produced manifests matters because RBAC and CRD changes must follow code changes to the kubebuilder markers — never the other way around.

This page documents what each file in `k8s/` provides, how the generated RBAC files are produced from `//+kubebuilder:rbac` markers, how the core and extensions controllers compose their permissions, and how the deploy tooling assembles these manifests for both the core-only and `--extensions` deployment modes.

## Layout of `k8s/`

| File | Type | Generated? | Role |
| --- | --- | --- | --- |
| `controller.yaml` | Multi-doc YAML | hand-written | Core install: `Namespace`, `ServiceAccount`, `ClusterRoleBinding`, `Service`, `Deployment` |
| `extensions.controller.yaml` | YAML | hand-written | `Deployment` override that adds `--extensions` to the controller args |
| `extensions.yaml` | YAML | hand-written | `ClusterRoleBinding` for the extensions `ClusterRole` |
| `rbac.generated.yaml` | YAML | `controller-gen` | `ClusterRole agent-sandbox-controller` (core permissions) |
| `extensions-rbac.generated.yaml` | YAML | `controller-gen` | `ClusterRole agent-sandbox-controller-extensions` (extension permissions) |
| `crds/` | YAML files | `controller-gen` | One CRD per Go API type (`sandboxes`, `sandboxclaims`, `sandboxtemplates`, `sandboxwarmpools`) |

A parallel set of generated files lives under `helm/templates/` (`rbac.generated.yaml`, `extensions-rbac.generated.yaml`, `clusterrolebinding*.yaml`, `deployment.yaml`, etc.) for the Helm install path, produced by the same `controller-gen` invocations.

Sources: [k8s/controller.yaml:1-82](), [k8s/extensions.controller.yaml:1-33](), [k8s/extensions.yaml:1-15](), [k8s/rbac.generated.yaml:1-60](), [k8s/extensions-rbac.generated.yaml:1-88]()

## Core controller install (`controller.yaml`)

The hand-written `controller.yaml` is a single multi-document YAML that creates the namespace, identity, in-cluster service, and workload for the core controller. The five documents are emitted in install order:

1. `Namespace agent-sandbox-system` — every other object lives here.
2. `ServiceAccount agent-sandbox-controller` — the workload identity.
3. `ClusterRoleBinding agent-sandbox-controller` — binds the generated core `ClusterRole` (same name) to the service account.
4. `Service agent-sandbox-controller` — exposes the `metrics` port (`8080/TCP`) so a scraper can reach the controller pod.
5. `Deployment agent-sandbox-controller` — one replica, runs the controller with `--leader-elect=true`, exposes container ports `metrics` (`8080`) and `healthz` (`8081`).

The image is encoded as a `ko://` reference (`ko://sigs.k8s.io/agent-sandbox/cmd/agent-sandbox-controller`) and is rewritten by the deploy tooling before apply; the manifest is not directly applicable from a clean checkout without that rewrite.

```yaml
# k8s/controller.yaml (Deployment excerpt)
containers:
- name: agent-sandbox-controller
  image: ko://sigs.k8s.io/agent-sandbox/cmd/agent-sandbox-controller # placeholder value, replaced by deployment scripts
  args:
  - --leader-elect=true
  ports:
  - name: metrics
    containerPort: 8080
  - name: healthz
    containerPort: 8081
```

Sources: [k8s/controller.yaml:1-82]()

## Extensions overlay (`extensions.controller.yaml`, `extensions.yaml`)

The extensions install is intentionally minimal: rather than duplicating the whole core stack, two small documents add the extension surface on top.

- `k8s/extensions.controller.yaml` is a `Deployment` with the **same name and namespace** as the core controller. Its only material difference is an additional `--extensions` arg on the container:

  ```yaml
  args:
  - "--leader-elect=true"
  - "--extensions"
  ```

  The deploy tool treats this as a replacement, not an addition (see "Deploy ordering" below).

- `k8s/extensions.yaml` is a single `ClusterRoleBinding` named `agent-sandbox-controller-extensions` that binds the generated extensions `ClusterRole` to the same `ServiceAccount agent-sandbox-controller`. There is no separate service account for extensions — the controller process is one binary that opts into reconcilers based on the `--extensions` flag.

The `--extensions` flag is read by the controller entrypoint and gates registration of the extensions scheme and the three reconcilers (`SandboxClaim`, `SandboxTemplate`, `SandboxWarmPool`):

```go
// cmd/agent-sandbox-controller/main.go (excerpt)
flag.BoolVar(&extensions, "extensions", false, "Enable extensions controllers.")
...
if extensions {
    utilruntime.Must(extensionsv1beta1.AddToScheme(scheme))
}
...
if extensions {
    if err = (&extensionscontrollers.SandboxClaimReconciler{ ... }).SetupWithManager(mgr); err != nil { ... }
    if err = (&extensionscontrollers.SandboxTemplateReconciler{ ... }).SetupWithManager(mgr); err != nil { ... }
    if err = (&extensionscontrollers.SandboxWarmPoolReconciler{ ... }).SetupWithManager(mgr); err != nil { ... }
}
```

Sources: [k8s/extensions.controller.yaml:1-33](), [k8s/extensions.yaml:1-15](), [cmd/agent-sandbox-controller/main.go:55-78](), [cmd/agent-sandbox-controller/main.go:175-271]()

## Generated RBAC pipeline

The two `*.generated.yaml` files in `k8s/` are produced by `sigs.k8s.io/controller-tools/cmd/controller-gen` and **must not be hand-edited**. The pipeline is declared as `go:generate` directives in [codegen.go](codegen.go):

```go
// codegen.go (excerpt)
//go:generate go tool -modfile=tools.mod sigs.k8s.io/controller-tools/cmd/controller-gen \
//   paths=./controllers/... output:rbac:dir=k8s \
//   rbac:roleName=agent-sandbox-controller,fileName=rbac.generated.yaml
//go:generate go tool -modfile=tools.mod sigs.k8s.io/controller-tools/cmd/controller-gen \
//   paths=./extensions/controllers/... output:rbac:dir=k8s \
//   rbac:roleName=agent-sandbox-controller-extensions,fileName=extensions-rbac.generated.yaml
```

Each invocation:
1. Walks the Go packages in `paths=…`.
2. Collects every `//+kubebuilder:rbac:…` marker found on reconciler types.
3. Aggregates the markers into a single `ClusterRole` whose name comes from `rbac:roleName=…` and writes it to `k8s/<fileName>`.

Identical directives target `helm/templates/` to keep the Helm chart in sync. Regeneration is wired through `make all` (which runs `dev/tools/fix-go-generate`), so editing markers without rerunning the generator will fail CI.

```mermaid
flowchart LR
    subgraph Source["Go sources (//+kubebuilder:rbac markers)"]
        Core["controllers/sandbox_controller.go"]
        Claim["extensions/controllers/sandboxclaim_controller.go"]
        Tmpl["extensions/controllers/sandboxtemplate_controller.go"]
        Warm["extensions/controllers/sandboxwarmpool_controller.go"]
    end
    subgraph Gen["codegen.go go:generate"]
        CG1["controller-gen<br/>roleName=agent-sandbox-controller"]
        CG2["controller-gen<br/>roleName=agent-sandbox-controller-extensions"]
    end
    subgraph Out["k8s/ generated outputs"]
        R1["rbac.generated.yaml<br/>ClusterRole agent-sandbox-controller"]
        R2["extensions-rbac.generated.yaml<br/>ClusterRole agent-sandbox-controller-extensions"]
    end
    subgraph Bind["Hand-written bindings"]
        B1["controller.yaml<br/>ClusterRoleBinding agent-sandbox-controller"]
        B2["extensions.yaml<br/>ClusterRoleBinding ...-extensions"]
        SA["ServiceAccount<br/>agent-sandbox-controller"]
    end
    Core --> CG1 --> R1
    Claim --> CG2
    Tmpl --> CG2
    Warm --> CG2
    CG2 --> R2
    R1 -. roleRef .-> B1
    R2 -. roleRef .-> B2
    B1 --> SA
    B2 --> SA
```

Sources: [codegen.go:20-27](), [controllers/sandbox_controller.go:130-137](), [extensions/controllers/sandboxclaim_controller.go:127-136](), [extensions/controllers/sandboxtemplate_controller.go:46-50](), [extensions/controllers/sandboxwarmpool_controller.go:61-64]()

## Core `ClusterRole` (`rbac.generated.yaml`)

The core role grants the controller exactly what the `Sandbox` reconciler needs: full lifecycle on pods, services, and PVCs in the user namespaces it manages; full lifecycle plus subresources on `agents.x-k8s.io/sandboxes`; leases for leader election; and event emission.

| API group | Resources | Verbs |
| --- | --- | --- |
| `""` (core) | `pods`, `services`, `persistentvolumeclaims` | `create, delete, get, list, patch, update, watch` |
| `agents.x-k8s.io` | `sandboxes` | `create, delete, get, list, patch, update, watch` |
| `agents.x-k8s.io` | `sandboxes/finalizers`, `sandboxes/status` | `get, patch, update` |
| `coordination.k8s.io` | `leases` | `create, get, list, patch, update, watch` (no `delete`) |
| `events.k8s.io` | `events` | `create, patch` |

These rules are the exact union of the markers above `SandboxReconciler`:

```go
// controllers/sandbox_controller.go
//+kubebuilder:rbac:groups=agents.x-k8s.io,resources=sandboxes,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=agents.x-k8s.io,resources=sandboxes/finalizers,verbs=get;update;patch
//+kubebuilder:rbac:groups=agents.x-k8s.io,resources=sandboxes/status,verbs=get;update;patch
//+kubebuilder:rbac:groups=core,resources=pods,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=core,resources=services,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=core,resources=persistentvolumeclaims,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=events.k8s.io,resources=events,verbs=create;patch
//+kubebuilder:rbac:groups=coordination.k8s.io,resources=leases,verbs=get;list;watch;create;update;patch
```

Sources: [k8s/rbac.generated.yaml:1-60](), [controllers/sandbox_controller.go:130-137]()

## Extensions `ClusterRole` (`extensions-rbac.generated.yaml`)

The extensions role is the union of markers from three reconcilers and is broader than the core role in two notable ways: it can manage `extensions.agents.x-k8s.io/*` resources (claims, templates, warm pools), and it can `create/delete/get/list/patch/update/watch` `networking.k8s.io/networkpolicies` — needed because `SandboxTemplate` and `SandboxClaim` materialize network policy isolation around the sandboxes they own.

| API group | Resources | Verbs | Contributed by |
| --- | --- | --- | --- |
| `""` | `pods` | `get, list, patch, update, watch` | `SandboxClaim` |
| `""`, `events.k8s.io` | `events` | `create, patch, update` | all three |
| `agents.x-k8s.io` | `sandboxes` | `create, delete, get, list, patch, update, watch` | `SandboxClaim`, `SandboxWarmPool` |
| `coordination.k8s.io` | `leases` | `create, delete, get, list, patch, update, watch` | `SandboxClaim` |
| `extensions.agents.x-k8s.io` | `sandboxclaims`, `sandboxtemplates`, `sandboxwarmpools` | `create, delete, get, list, patch, update, watch` | all three |
| `extensions.agents.x-k8s.io` | `sandboxclaims/finalizers`, `sandboxclaims/status`, `sandboxtemplates/finalizers`, `sandboxwarmpools/finalizers`, `sandboxwarmpools/status` | `get, patch, update` | per-reconciler |
| `networking.k8s.io` | `networkpolicies` | `create, delete, get, list, patch, update, watch` | `SandboxTemplate` (full), `SandboxClaim` (`get, list, watch, delete`) |

Two quirks worth noting:

- The role contains rules for both API groups `""` and `events.k8s.io` on `events`. That's because some markers declare `groups=core` and others declare `groups=events.k8s.io` — `controller-gen` collapses them into a single rule that lists both groups.
- The extensions role permits `delete` on `leases`, while the core role does not. This comes from `SandboxClaim`'s marker (`verbs=get;list;watch;create;update;patch;delete`) and exists because the extensions controllers manage their own per-claim leases rather than only leader-election ones.

Sources: [k8s/extensions-rbac.generated.yaml:1-88](), [extensions/controllers/sandboxclaim_controller.go:127-136](), [extensions/controllers/sandboxtemplate_controller.go:46-50](), [extensions/controllers/sandboxwarmpool_controller.go:61-64]()

## How bindings tie roles to the service account

Both `ClusterRoleBinding`s point at the same `ServiceAccount agent-sandbox-controller` in `agent-sandbox-system` but reference different `ClusterRole`s by name:

```text
ServiceAccount: agent-sandbox-controller (ns: agent-sandbox-system)
        ^                 ^
        |                 |
ClusterRoleBinding        ClusterRoleBinding
  agent-sandbox-controller     agent-sandbox-controller-extensions
        |                 |
ClusterRole               ClusterRole
  agent-sandbox-controller     agent-sandbox-controller-extensions
  (rbac.generated.yaml)        (extensions-rbac.generated.yaml)
```

The names of the two `ClusterRole`s are not coincidental — they are passed directly to `controller-gen` via `rbac:roleName=…` in [codegen.go](codegen.go), so the bindings in `controller.yaml` and `extensions.yaml` are coupled by string to the generator invocation.

Sources: [k8s/controller.yaml:19-30](), [k8s/extensions.yaml:1-15](), [codegen.go:22-23]()

## Deploy ordering and the core/extensions composition

The `k8s/` directory is consumed by `dev/tools/deploy-to-kube` (and indirectly by `make deploy-kind`). The script walks `k8s/` recursively, then filters and sorts documents before `kubectl apply`:

1. **Extensions filter.** Files whose names start with `extensions` are skipped unless `--extensions` was passed.
2. **Core controller replacement.** When `--extensions` is set, the core `Deployment` from `controller.yaml` is filtered out so the `extensions.controller.yaml` `Deployment` (with `--extensions` in `args`) takes its place — they share the same name and namespace.
3. **Image rewrite.** Any `ko://…` image or `:latest` tag is rewritten using the configured `image_prefix` and `image_tag`.
4. **Flag append.** A `CONTROLLER_ARGS` string is appended to the controller container's `args`.
5. **Three-phase apply, in order:**
   - prerequisites — `Namespace` and `CustomResourceDefinition` objects (from `controller.yaml` and `k8s/crds/`)
   - other documents — `ServiceAccount`, `Service`, both `ClusterRole`s, the core `ClusterRoleBinding`, and the chosen `Deployment`
   - extension documents (only if `--extensions`) — the extensions `ClusterRoleBinding` from `extensions.yaml`

```mermaid
flowchart TD
    Start["dev/tools/deploy-to-kube"] --> Walk["gather_manifests(k8s/)"]
    Walk --> Filter{extensions flag?}
    Filter -- no --> SkipExt["drop files starting with 'extensions'"]
    Filter -- yes --> KeepExt["keep extensions files;<br/>drop core Deployment"]
    SkipExt --> Process["find_and_replace_images +<br/>append_controller_flags"]
    KeepExt --> Process
    Process --> Sort{kind?}
    Sort -- CRD/Namespace --> Pre["prereq_docs"]
    Sort -- extensions file --> Ext["extensions_docs"]
    Sort -- otherwise --> Other["other_docs"]
    Pre --> Apply1["kubectl apply (prereqs)"]
    Apply1 --> Apply2["kubectl apply (others)"]
    Apply2 --> ApplyExt{extensions?}
    ApplyExt -- yes --> Apply3["kubectl apply (extensions)"]
    ApplyExt -- no --> Done["done"]
    Apply3 --> Done
```

Because both `ClusterRole`s and both bindings are accepted regardless of whether `--extensions` is set (only the binding from `extensions.yaml` is gated by the `extensions` filename prefix), an extensions-enabled deploy ends up with both roles, both bindings, the extensions-enabled `Deployment`, and all CRDs applied to the cluster.

Sources: [dev/tools/deploy-to-kube:155-230](), [codegen.go:20-27]()

## Modifying RBAC: the source-of-truth rule

The static manifests in `k8s/` are deliberately the **install artifact**, not the **specification**. To change what the controller can do, edit the `//+kubebuilder:rbac` markers on the reconciler type in `controllers/` or `extensions/controllers/`, then regenerate:

```sh
make all          # runs fix-go-generate, which invokes controller-gen
# or, narrowly:
go generate ./...
```

This regenerates the matching files under both `k8s/` and `helm/templates/`. Hand-editing `rbac.generated.yaml` or `extensions-rbac.generated.yaml` is unsafe — the next generator run will overwrite the change and CI's `fix-go-generate` check will reject the PR.

Conversely, the hand-written files (`controller.yaml`, `extensions.controller.yaml`, `extensions.yaml`) are where bindings, namespaces, services, deployment shape, and controller flags live. New permissions belong in markers; new wiring belongs in the hand-written manifests.

Sources: [codegen.go:20-27](), [Makefile:1-10]()

## Summary

`k8s/` is a small, deliberately layered install surface: hand-written cluster identity and workload definitions (`controller.yaml`, `extensions.controller.yaml`, `extensions.yaml`) bind to `ClusterRole`s that are mechanically reproduced from `//+kubebuilder:rbac` markers on the reconcilers (`rbac.generated.yaml`, `extensions-rbac.generated.yaml`). The extensions install is an overlay rather than a parallel stack — same `ServiceAccount`, same `Deployment` name, with `--extensions` and an extra binding turning on additional reconcilers. The `dev/tools/deploy-to-kube` script orchestrates the apply, ensuring CRDs and namespaces land before any objects that depend on them and that the right controller `Deployment` wins when extensions are enabled.
