Wires the catalyst-api backend the Sandbox FE (PR #1621 — getSandboxes /
createSandbox / getByosStatus in sandbox.api.ts) has been calling into.
Without this handler the /sandbox surface on the Sovereign Console rendered
its empty state forever — every getSandboxes() 404'd at the catalyst-api
ingress and every "Start a session" click hit the same wall.
Handler — products/catalyst/bootstrap/api/internal/handler/sandbox_sessions.go
- GET /api/v1/sandbox/sessions — list Sandbox CRs in the
operator's Org namespace
- POST /api/v1/sandbox/sessions — create Sandbox CR with agent
validated against the 6-agent
catalogue (aider / claude-code /
cursor-agent / little-coder /
opencode / qwen-code)
- GET /api/v1/sandbox/sessions/{id} — fetch single Sandbox detail
- DELETE /api/v1/sandbox/sessions/{id} — graceful delete (the controller
fires finalizers + cleans up
the per-Sandbox vcluster
namespace + PVCs + RBAC)
Client resolution mirrors the Family E compliance + k8s_resource_actions.go
seam: k8sCache.Factory.DynamicClientFor(resolveChrootClusterID("")) is the
primary path; sovereignDepsFor() — rest.InClusterConfig() — is the chroot
in-cluster fallback per feedback_chroot_in_cluster_fallback.md. Both 503
when unavailable so the FE renders its "API pending" pill rather than a
spinner.
Org-scoping uses claims.Org (the org_id Keycloak claim PR #1619 lit up)
for the CR namespace + spec.owner.orgRef.slug. Single-tenant chroots
without an org_id fall back through CATALYST_SANDBOX_DEFAULT_NAMESPACE
to a sensible default per docs/INVIOLABLE-PRINCIPLES.md #4. Wave-1 quota
defaults (4 CPU / 8Gi memory / 50Gi storage / 3 concurrent sessions)
mirror products/sandbox/docs/architecture.md §7 — the FE doesn't yet
expose a quota picker.
Status projection: CRD vocabulary (Pending|Provisioning|Ready|Failed)
maps to FE vocabulary (pending|running|stopped|failed|unknown) in
mapSandboxStatus so a fresh Sandbox shows the spinner rather than
"unknown" until the controller catches up.
k8sCache.DefaultKinds — products/catalyst/bootstrap/api/internal/k8scache/kinds.go
- Adds sandbox.openova.io/v1 Sandbox so the generic /k8s/{kind} surface
enumerates Sandboxes the same way it does Applications + UserAccess.
Per feedback_chroot_in_cluster_fallback.md every new GVR here needs a
matching rule on the cutover-driver SA.
Cutover-driver RBAC — products/catalyst/chart/templates/clusterrole-cutover-driver.yaml
- Adds sandboxes.sandbox.openova.io with verbs split per
feedback_rbac_create_no_resourcenames.md:
rule 1: ["create"]
rule 2: ["get","list","watch","delete"]
- Read-only on status (the controller owns status); write is spec-only
on POST + the apiserver delete on DELETE.
Routes — products/catalyst/bootstrap/api/cmd/api/main.go
- Registered inside the RequireSession group alongside the existing
/api/v1/sandbox/byos/claude-code/* surface; same auth gate, same
patternless leading "/api/v1/sandbox/...".
Verified: go build clean, go vet clean, k8scache test suite green
(2.7s), helm template renders the new RBAC block.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>