> ## Documentation Index
> Fetch the complete documentation index at: https://docs.genesiscomputing.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Deploying the Genesis Sandbox Runner on GKE

> Configure the Kubernetes sandbox runner and its cluster resources on Google Kubernetes Engine

This guide configures the optional **sandbox thread runner** on an existing Google
Kubernetes Engine (GKE) cluster. Each selected Genesis chat thread runs its Claude
Code agent in a separate Pod, while Genesis provides the LLM proxy, MCP endpoint,
and thread transcript.

<Note>
  This is the `sandbox_runner.runtime: kubernetes` runner. It is separate from the
  chart's older `sandbox.enabled` backend. The runner is off until you explicitly
  enable `sandbox_runner.enabled` and select at least one bot.
</Note>

<Note>
  For a full base install of Genesis on a new GKE cluster with Helm — preflight and
  grants, images and chart, the web console, and the on-demand per-user sandbox
  terminal — see [Installing Genesis on GKE with Helm](/docs/deployments/GCP/Genesis-On-GCP-GKE-Install).
</Note>

## Prerequisites

* An existing GKE cluster with `kubectl` access and a Genesis Helm deployment.
* Helm 3.8 or later and the Genesis chart package or OCI chart.
* The Genesis backend running in the `genesis` namespace. The supplied manifest
  assumes the backend uses the `default` ServiceAccount and that the `genesis`
  Service exposes FastAPI on port `8080`.
* A registry image built from `container/Dockerfile.sandbox-claude` and reachable
  from the GKE cluster. See [Genesis on Docker — Sandbox Claude Code runner image](/docs/deployments/Docker/Genesis-Container-Distribution#sandbox-claude-code-runner-image).

Verify cluster access before applying the platform resources:

```bash theme={null}
kubectl get nodes
kubectl -n genesis get statefulset genesis \
  -o jsonpath='{.spec.template.spec.serviceAccountName}'
```

An empty ServiceAccount value means `default`, which is the manifest's assumption.
If your deployment uses another namespace, ServiceAccount, Service name, or port,
update the manifest before applying it.

## Apply the GKE platform manifest

The Helm chart includes `gcp-gke/sandbox-runner-platform.yaml`. Apply it once per
cluster, before enabling the runner:

```bash theme={null}
kubectl apply -f gcp-gke/sandbox-runner-platform.yaml
```

The manifest creates:

* The `genesis-sandbox` namespace.
* Role-based access for the Genesis backend to create, inspect, execute in, and
  delete runner Pods, plus create and remove per-thread PersistentVolumeClaims.
* A `ResourceQuota` limiting the namespace to `25` Pods and `40` PersistentVolumeClaims.
* A `NetworkPolicy` that denies ingress and permits egress only to DNS and the
  Genesis backend on TCP port `8080`. The backend fronts both the sandbox LLM proxy
  and MCP endpoint.

## Enable the runner

Set the existing `sandbox_runner` settings in Advanced Settings, your
`genesis-config.yaml`, or the advanced config API. The minimum GKE values are:

```yaml theme={null}
sandbox_runner:
  enabled: true
  runtime: kubernetes
  image: <registry>/genesis-sandbox-claude:latest
  bot_ids: ["eve"]
  k8s_namespace: genesis-sandbox
  backend_service: genesis
```

The feature remains disabled by default (`sandbox_runner.enabled: false`), and an
empty `sandbox_runner.bot_ids` list routes no bot by default. For the complete
runner settings, per-thread override, and lifecycle behavior, see [Sandbox thread
runner](/docs/Setup/Advanced-Settings#sandbox-thread-runner-prototype).

## Enable the Kubernetes CLI terminal

The direct CLI terminal can use the same GKE cluster with a separate namespace and
quota. Unlike the sandbox thread runner, it creates one long-lived workspace Pod per
user and keeps that user's two PersistentVolumeClaims after the Pod is reaped.

The chart renders the cluster objects and switches the feature on together — set
`cliTerminal.enabled=true` in your values file. The [GKE Helm install
runbook](/docs/deployments/GCP/Genesis-On-GCP-GKE-Install#move-2-grant-the-cluster-objects)
covers the chart's force-locked runtime values and the interaction with Advanced Settings:

```yaml theme={null}
cliTerminal:
  enabled: true
  namespace: "genesis-cli"
  image: ""                                # empty = APP_CONFIG/extraEnv owns it; set to override
  maxWorkspaces: 25                        # renders the quota AND the backend's own cap
  maxLifetimeUsers: 50                     # people who may EVER attach; claims outlive Pods
  networkPolicy:
    enabled: false
```

That creates the `genesis-cli` namespace, grants the ServiceAccount the backend actually
runs as (`default` unless `serviceAccount.create`/`.name` is set) permission to manage
workspace Pods, Pod exec connections, and PersistentVolumeClaims, adds the ResourceQuota,
and sets plus force-locks `GENESIS_CLI_TERMINAL_ENABLED`, `_RUNTIME` and `_K8S_NAMESPACE`
on the backend. `cliTerminal.maxWorkspaces` is rendered as an integer in both the quota and
the backend environment, including when Helm receives a value such as `10.0`.
The default `cli_terminal.home_volume_size` is `10Gi` for each claim, so plan for `20Gi`
per user. Claims are not reclaimed automatically; an operator must retire both claims for
a departing user.

<Note>
  **The `genesis-cli` namespace survives both `helm uninstall` and
  `cliTerminal.enabled: false`.** It holds every user's home volume and therefore their
  saved conversations, so it carries `helm.sh/resource-policy: keep` — otherwise turning
  the feature off would delete their work as a side effect. Switching it off revokes the
  RBAC and releases the quota, but does not force the application's own setting to `false`;
  deployments with an externally provisioned namespace and RBAC can keep working. A
  chart-managed deployment should treat the missing grants as disabled until the chart
  switch is turned back on. It does not stop agents already running,
  because the Pods are retained with the namespace and the reaper goes with the backend.
  Two explicit, separate commands: `cluster/scripts/drain-workspaces.sh <release>` (Pods
  go; volumes, namespace and release all stay) and `cluster/scripts/teardown.sh <name>   --delete-workspaces` (tears the instance down and destroys the volumes).
</Note>

Each workspace Pod **reserves** `cli_terminal.memory_request` (default `4g` → `4Gi`) and
may grow to the `cli_terminal.memory` **ceiling** (default `8g` → `8Gi`). The ceiling is a
hard cap: exceeding it OOM-kills the container, which kills every session that user has
open. The request is what the scheduler reserves and therefore what cluster capacity must
supply — plan cluster capacity at `4Gi` per concurrent user, with headroom to burst to
`8Gi`. (The chart's ResourceQuota counts Pods and claims, not memory, so this is capacity
to provide rather than a quota to set.) No CPU limit is set (CPU is compressible), so `cli_terminal.cpu_request` (`500m`) is
the only CPU number the scheduler sees.

A `1Gi` memory-backed `/dev/shm` (`cli_terminal.shm_size`) is mounted for the headless
Chrome the agent drives, replacing the runtime's 64Mi default. Its used pages are charged
against the memory **ceiling** as the shm fills (Kubernetes does not pre-reserve the
`sizeLimit`), so a full shm can leave up to `1Gi` less for the process — and it needs no
share of the request.

Application-level settings can also be set in Advanced Settings, `genesis-config.yaml`, or
the advanced config API:

```yaml theme={null}
cli_terminal:
  enabled: true
  runtime: kubernetes
  image: <registry>/genesis-cli-terminal:latest
  k8s_namespace: genesis-cli
  home_volume_size: 10Gi
  memory: 8g
  memory_request: 4g
  cpu_request: 500m
  shm_size: 1Gi
```

The feature still requires the `use_cli_terminal` role permission. Workspace egress is
**unrestricted by default** — a workspace has the same network access as any other Pod in
the cluster, which is deliberate for a development workspace that has to clone
repositories and install packages. `cliTerminal.networkPolicy.enabled=true` denies ingress
entirely and confines egress to DNS plus the Genesis backend on the backend's container
port `8080`, which fronts the LLM proxy and MCP endpoint; `git`/`gh`, npm and pip then
time out. A NetworkPolicy is only as good as your CNI — GKE enforces it, a stock EKS
cluster with the default VPC CNI was measured enforcing nothing — so verify rather than
assume.

If your platform objects are applied outside Helm, render them from the same template
rather than keeping a hand-edited copy:

```bash theme={null}
helm template genesis <chart> \
  --set cliTerminal.enabled=true \
  --show-only templates/cli-terminal-rbac.yaml | kubectl apply -f -
```

### Optional gVisor isolation

On GKE, you can run runner Pods under gVisor:

```yaml theme={null}
sandbox_runner:
  k8s_runtime_class: gvisor
  k8s_pod_ready_timeout_seconds: 420.0
```

The manifest is designed for the agent's unrestricted execution inside the Pod;
the Pod boundary and the NetworkPolicy provide the cluster-side containment. A cold
start on GKE Autopilot can include node provisioning, so the default
`sandbox_runner.k8s_pod_ready_timeout_seconds: 420.0` accommodates measured
169–187-second starts when the sandboxed node pool has scaled to zero. If the
configured runtime or image cannot be used, Genesis hides the Claude Code picker and
falls back to the normal Genesis harness rather than failing the turn at routing time.

## Avoid Helm ownership conflicts

The chart's `sandbox-rbac.yaml` also creates a `genesis-sandbox` namespace when
`sandbox.enabled: true` and `sandbox.createSharedResources: true`. Do not enable
that older backend while this manifest owns the namespace: a later Helm upgrade can
fail because the namespace lacks Helm ownership metadata. Leave `sandbox.enabled`
at its default `false`, or transfer ownership to Helm before upgrading:

```bash theme={null}
kubectl label ns genesis-sandbox app.kubernetes.io/managed-by=Helm --overwrite
kubectl annotate ns genesis-sandbox \
  meta.helm.sh/release-name=<release> \
  meta.helm.sh/release-namespace=<namespace> --overwrite
```

`sandbox.createSharedResources: false` alone does not resolve the conflict because
the chart still renders bindings for resources that this manifest does not define.

## Verify the runner resources

```bash theme={null}
kubectl get namespace genesis-sandbox
kubectl -n genesis-sandbox get role,rolebinding,resourcequota,networkpolicy
kubectl -n genesis-sandbox get pods,persistentvolumeclaims
```

After a selected bot starts a chat thread, the runner creates a thread Pod and a
thread-scoped PVC in `genesis-sandbox`. The quota and policy should remain in place
for the lifetime of the deployment. The Pod limit bounds concurrent runner threads,
while PVCs can outlive their Pods so an idle thread can be resumed with its context.
The manifest therefore allows more PVCs than Pods; when the PVC ceiling is reached,
the runner reclaims the oldest idle volume before creating storage for a new thread
instead of returning a bare `403`.
