Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 58 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,61 @@ docker run -p 3000:3000 \

**Prerequisites:** Docker running on the host.

### Kubernetes Operator (recommended for clusters)
### Kubernetes with Agent Sandbox

The `kubernetes-sandbox` backend is a newer option that runs each user's terminal on the
upstream [Agent Sandbox](https://github.com/kubernetes-sigs/agent-sandbox) project (SIG Apps),
alongside the existing [operator backend](#kubernetes-with-the-bundled-operator-custom-crd).
Each user+policy maps to a single `Sandbox` custom resource; the agent-sandbox
controller reconciles it into a Pod, a headless Service (a stable `serviceFQDN`),
and a PersistentVolume when a workspace is requested. Idle terminals are
**suspended** (`operatingMode: Suspended`, scale-to-zero with storage and identity
preserved) and resumed on the next request.

Workspace persistence: the per-user `PersistentVolumeClaim` is created from the
Sandbox's `volumeClaimTemplates` and owned by the Sandbox. Suspending keeps the
Sandbox object, so `/workspace` data survives idle and resume. **Tearing a terminal
down deletes the Sandbox and its PVC** (workspace data is removed) — idle reaping uses
suspend, not teardown, so normal inactivity never destroys data.

For Kubernetes deployments, the operator manages `Terminal` custom resources automatically — handling pod creation, storage, and cleanup through CRDs.
```bash
# 1. Install the agent-sandbox controller + extensions (pin a release version)
export VERSION="v0.5.0" # see https://github.com/kubernetes-sigs/agent-sandbox/releases
kubectl apply -f https://github.com/kubernetes-sigs/agent-sandbox/releases/download/${VERSION}/manifest.yaml
kubectl apply -f https://github.com/kubernetes-sigs/agent-sandbox/releases/download/${VERSION}/extensions.yaml

# 2. Grant the Terminals service access to the Sandbox CRDs
kubectl apply -f manifests/sandbox-rbac.yaml
```

Set `TERMINALS_BACKEND=kubernetes-sandbox` when deploying the Terminals service
(with `serviceAccountName: terminals`). For stronger isolation of user code, set
`TERMINALS_SANDBOX_RUNTIME_CLASS=gvisor` (or `kata-qemu`) once the runtime is
installed on your nodes.

> [!NOTE]
> Agent Sandbox is a young upstream project — this backend targets `v1beta1` (v0.5.x);
> pin a release version and track changes. Two trade-offs to be aware of: the per-user
> API key lives as a plaintext env value in the `Sandbox` pod template (rely on RBAC +
> etcd encryption-at-rest), and **idle tracking is in-memory** in the Terminals process
> (run a single Terminals replica, or expect idle to be tracked per-replica). Warm pools
> are intentionally not used — they are mutually exclusive with per-user API keys (an
> env-injecting claim bypasses the pool), so first connection pays pod start-up (the
> image is cached on the node after the first pull).
>
> These last two are things the backend self-manages **because the controller does not
> yet**. Both are on the [upstream roadmap](https://github.com/kubernetes-sigs/agent-sandbox/blob/main/roadmap.md)
> (*Auto Suspend/Resume*, *Scale to Zero*, *Sandbox/Pod Identity Association*); the
> backend is pinned to `v1beta1` specifically so we can drop these shims as they land.

### Kubernetes with the bundled operator (custom CRD)

The operator backend reconciles a custom `Terminal` CRD with a self-hosted
[Kopf](https://kopf.readthedocs.io/) operator. Use this if you prefer a
self-contained controller over the upstream Agent Sandbox project.

```bash
# Install the CRD and operator
# 1. Install the Terminal CRD and the operator (Deployment + RBAC)
kubectl apply -f manifests/terminal-crd.yaml
kubectl apply -f manifests/operator-deployment.yaml
```
Expand All @@ -54,9 +103,12 @@ terminals serve
| Backend | Best for | How it works |
|---------|----------|-------------|
| `docker` | Single-node, local dev | One container per user via Docker socket |
| `kubernetes-operator` | Production K8s clusters | Operator watches `Terminal` CRDs for automated lifecycle |
| `kubernetes-sandbox` | Production K8s clusters (upstream CRD) | One [Agent Sandbox](https://github.com/kubernetes-sigs/agent-sandbox) `Sandbox` per user; suspend/resume on idle |
| `kubernetes-operator` | Production K8s clusters (self-hosted operator) | Custom `Terminal` CRD reconciled by the bundled Kopf operator |
| `kubernetes` | K8s without CRDs | Direct Pod + PVC + Service per user (you manage resources) |

> Both `kubernetes-sandbox` and `kubernetes-operator` are fully supported. The sandbox backend builds on the upstream Agent Sandbox controller; the operator backend uses our own `Terminal` CRD. Pick one per deployment via `TERMINALS_BACKEND`.

Set the backend with `TERMINALS_BACKEND` (defaults to `docker`).

## Policies
Expand Down Expand Up @@ -109,14 +161,15 @@ All settings are configured through environment variables prefixed with `TERMINA

| Variable | Default | Description |
|----------|---------|-------------|
| `TERMINALS_BACKEND` | `docker` | `docker`, `kubernetes`, or `kubernetes-operator` |
| `TERMINALS_BACKEND` | `docker` | `docker`, `kubernetes`, `kubernetes-operator`, or `kubernetes-sandbox` |
| `TERMINALS_API_KEY` | *(auto-generated)* | Bearer token for API auth |
| `TERMINALS_IMAGE` | `ghcr.io/open-webui/open-terminal:latest` | Default container image |
| `TERMINALS_MAX_CPU` | | Hard cap on CPU per container |
| `TERMINALS_MAX_MEMORY` | | Hard cap on memory per container |
| `TERMINALS_MAX_STORAGE` | | Hard cap on storage per container |
| `TERMINALS_ALLOWED_IMAGES` | | Comma-separated list of allowed image patterns |
| `TERMINALS_KUBERNETES_STORAGE_MODE` | `per-user` | `per-user`, `shared`, or `shared-rwo` |
| `TERMINALS_SANDBOX_RUNTIME_CLASS` | | RuntimeClass for sandbox isolation, e.g. `gvisor` or `kata-qemu` |

See [`config.py`](terminals/config.py) for the full list.

Expand Down
50 changes: 50 additions & 0 deletions manifests/sandbox-rbac.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# RBAC for the Terminals service to drive Agent Sandbox CRDs.
#
# Apply AFTER installing the agent-sandbox controller + extensions (v0.5.0+):
# kubectl apply -f https://github.com/kubernetes-sigs/agent-sandbox/releases/download/v0.5.0/manifest.yaml
# kubectl apply -f https://github.com/kubernetes-sigs/agent-sandbox/releases/download/v0.5.0/extensions.yaml
# kubectl apply -f manifests/sandbox-rbac.yaml
#
# The Terminals Deployment must set `serviceAccountName: terminals` and
# `TERMINALS_BACKEND=kubernetes-sandbox`.
---
apiVersion: v1
kind: Namespace
metadata:
name: terminals
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: terminals
namespace: terminals
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: terminals-sandbox
namespace: terminals
rules:
# Core Sandbox resource: create/delete per user, patch replicas for
# suspend/resume, and read status (serviceFQDN, Ready condition).
- apiGroups: ["agents.x-k8s.io"]
resources: ["sandboxes", "sandboxes/status"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
# Read-only access to the Services/Pods the controller creates.
- apiGroups: [""]
resources: ["services", "pods"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: terminals-sandbox
namespace: terminals
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: terminals-sandbox
subjects:
- kind: ServiceAccount
name: terminals
namespace: terminals
4 changes: 4 additions & 0 deletions terminals/backends/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,8 @@ def create_backend() -> Backend:
from terminals.backends.kubernetes_operator import KubernetesOperatorBackend

return KubernetesOperatorBackend()
elif settings.backend == "kubernetes-sandbox":
from terminals.backends.kubernetes_sandbox import KubernetesSandboxBackend

return KubernetesSandboxBackend()
raise ValueError(f"Unknown backend: {settings.backend!r}")
Loading