feat(sandbox): Wave 1 controller + chart scaffold
Wires the sandbox-controller binary + standalone Helm chart for the
Wave 1 slice of the Sandbox product (architecture spec:
products/sandbox/docs/architecture.md §7). Companion to the CRD
shipped in PR #1615.
Controller (core/controllers/sandbox/):
- cmd/sandbox-controller/main.go — controller-runtime entry point,
env-driven config, leader election on by default. Mirrors
core/controllers/organization/cmd/main.go shape.
- internal/controller/sandbox_controller.go — Reconciler that
Get's the Sandbox CR, renders Wave 1 manifests, and writes them
into the per-Org `catalyst-tenant` Gitea repo under
`sandbox/<owner-uid>/`. Same idiom organization-controller uses
for vcluster manifests (organization_controller.go:188-225).
- internal/gitops/manifests.go — text/template renderer for the
Wave 1 set: Namespace + ResourceQuota + ServiceAccount + Role +
RoleBinding + placeholder Secret + 1 PVC per spec.repos[] +
kustomization. Sorted iteration + no time.Now() = deterministic
output (byte-equal PutFile short-circuit on steady state).
- internal/controller/sandbox_controller_test.go — 5 cases:
happy-path + idempotency + 2 drift paths (missing orgRef /
missing email) + missing-CR. Cloned from
organization_controller_test.go.
- Dockerfile — two-stage build, alpine:3.20 runtime, non-root
UID 65534. Mirrors organization/Containerfile (same CC1 shared
go.mod + pkg layout).
Chart (platform/sandbox/chart/):
- Chart.yaml / values.yaml — opt-in via .enabled=false default.
- templates/{serviceaccount,clusterrole,clusterrolebinding,deployment}.yaml
+ _helpers.tpl — modelled on
products/catalyst/chart/templates/controllers/organization-controller-*.yaml.
- ClusterRole grants are least-privilege (sandboxes +
sandboxes/status + sandboxes/finalizers + leases + events).
No secrets:get yet (deferred to Wave 2 when the long-lived
org-scoped token issuance flow lands).
- Image tag is required at render time (fail-fast on empty per
Inviolable Principle #4a — no :latest).
Wave 1 does NOT ship: pty-server / openova-sandbox-mcp Deployments,
HTTPRoutes for preview subdomains, long-lived token issuance, per-repo
git-clone initContainer. Those land in Wave 2.
Hard rules respected: READ-ONLY clusters, no chart 1.4.156 bump, no
UI touched, no npm/tsc.
Verified locally:
- `go build ./sandbox/...` clean (go 1.23.4)
- `go test -count=1 ./sandbox/...` ok (5/5 pass)
- `go vet ./sandbox/...` clean
- `helm lint platform/sandbox/chart` clean
- `helm template ... --set enabled=true --set image.tag=abc123 ...`
renders 4 resources (SA + ClusterRole + CRB + Deployment).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>