# Hummingbird Sandbox

LLMS index: [llms.txt](https://hummingbird-project.io/llms.txt) | Full content: [llms-full.txt](https://hummingbird-project.io/llms-full.txt)

---

Shared Python library for isolated command execution used by Hummingbird agents.

## Features

- Podman, Kubernetes, and KubeVirt sandbox backends.
- Command execution, streamed input/output, file transfer, and sandbox cleanup.
- Kubernetes and KubeVirt pools for reusing pre-warmed sandboxes.

## Prerequisites

- Python 3.11 or later.
- Podman for local containers, or Kubernetes credentials and namespace-scoped
  permissions for Kubernetes-backed sandboxes.
- A sandbox image; Kubernetes deployments must also provide the required
  network policy and resource controls.

## Installation

Install the development package from the tools repository:

```bash
make hummingbird-sandbox/setup
```

## Usage

Create a backend through the shared factory, then always clean it up:

```python
from hummingbird_sandbox import config, sandbox

backend = sandbox.create_sandbox(config.SandboxConfig(image="example:latest"))
backend.start()
try:
    result = backend.exec("printf 'hello\\n'")
    if result.exit_code:
        raise RuntimeError(result.stderr)
    print(result.stdout)
finally:
    backend.cleanup()
```

`create_sandbox` accepts `sandbox_type="podman"`, `"k8s"`, or `"kubevirt"`;
Kubernetes-backed modes also accept a context and namespace. The API exposes
streaming file operations and pool backends for callers that need them.

## Configuration

`SandboxConfig` holds the backend image, namespace, active deadline, pool linger
limits, pod metadata/resources, and KubeVirt image and SSH settings. Callers are
responsible for loading configuration and keeping their credentials outside the
sandbox.

Hummingbird Agent supplies these settings in `settings.sandbox`. This example
is moved from the agent documentation; the library itself accepts a
`SandboxConfig` object and does not load YAML or environment variables:

```yaml
sandbox: # sandbox pod configuration
  image: quay.io/.../gitlab-ci:latest #   container image (k8s mode only)
  namespace: default #   K8s namespace (required for K8s/pool/kubevirt)
  active_deadline_seconds: 1800 #   pod hard timeout / reap interval
  linger_seconds: 300 #   keep pod alive after success (pool mode, 0=disable)
  max_lingering_pods: 2 #   max idle lingering pods before eviction (pool mode)
  vm_image: quay.io/.../vm-disk:latest #   containerDisk image (kubevirt mode only)
  vm_memory: 1Gi #   VM guest memory (kubevirt mode only)
  vm_ssh_private_key: /path/to/key #   SSH private key for pool VMs (kubevirtpool mode)
  metadata: #   pod/VMI metadata (k8s/kubevirt mode)
    labels:
      app.kubernetes.io/name: hummingbird-agent-sandbox
  resources: #   K8s resource requests/limits (k8s mode)
    requests:
      cpu: "100m"
      memory: "256Mi"
      ephemeral-storage: "256Mi"
    limits:
      cpu: "1"
      memory: "1Gi"
      ephemeral-storage: "2Gi"
```

## Security and Design Constraints

`hummingbird-sandbox` supplies execution backends; it does not create the
caller's namespace RBAC or `NetworkPolicy`, load configuration, or make
deployment decisions. Callers own configuration and credentials, and must
provide the infrastructure controls described below. The following security
and backend documentation is moved from Hummingbird Agent without changing its
behavior claims.

The agent is designed to run in a shared OpenShift cluster without
cluster-admin access, processing potentially untrusted merge requests. These
constraints shaped the architecture:

**Sandbox isolation.** All arbitrary commands executed by the LLM run inside an
ephemeral container, never on the host:

- **Podman (local)**: `--network=none`, `--user 65532`, no host mounts.
  Complete network isolation.
- **Kubernetes (production)**: Pods comply with OpenShift's `restricted-v2`
  Security Context Constraint:
  `runAsNonRoot`, `seccompProfile: RuntimeDefault`,
  `allowPrivilegeEscalation: false`, `capabilities.drop: ["ALL"]`,
  `automountServiceAccountToken: false` (no K8s API access from sandbox),
  `activeDeadlineSeconds` (configurable, default 1800). Security context
  fields are hardcoded in the pod manifest for portability to vanilla
  Kubernetes with Pod Security Admission (restricted level). Resource
  requests/limits, metadata, and `activeDeadlineSeconds` are configurable
  via `settings.sandbox` in the config file. Network access is denied by
  a `NetworkPolicy` on the sandbox namespace that blocks all egress from
  all pods (`podSelector: {}`).

**No cluster-admin required.** The agent operates with namespace-scoped
permissions only. The orchestrator's ServiceAccount needs only:

- `pods: create, get, list, delete, patch` -- sandbox pod lifecycle and pool claims
- `pods/exec: create` -- command execution via `kubectl exec`
- `virtualmachineinstances.kubevirt.io: create, get, list, delete, patch` --
  KubeVirt VMI lifecycle (kubevirt/kubevirtpool modes only)
- `secrets: create, get, delete` -- SSH key Secrets for VMI provisioning
  (kubevirt/kubevirtpool modes only)

These permissions are granted via a Role in the sandbox namespace, not the
orchestrator's own namespace. No CRDs, no custom runtimes, no cluster-scoped
resources. Konflux data is fetched via bearer token from kubeconfig, not from
inside the cluster.

**Namespace separation.** Sandbox pods are created in a dedicated namespace,
separate from the orchestrator. This limits blast radius: even if a sandbox
pod is compromised, it has no visibility into the orchestrator's Secrets,
Pods, or ServiceAccount tokens. The sandbox namespace is locked down with
standard K8s resources:

- **RBAC**: Role + RoleBinding scoped to the namespace, granting only the
  permissions above to the orchestrator's ServiceAccount
- **NetworkPolicy**: uses `podSelector: {}` to select all pods in the
  dedicated sandbox namespace, denying all egress (`egress: []`). The
  sandbox cannot reach the internet, the K8s API, or other pods.
- **activeDeadlineSeconds**: sandbox pods self-terminate after the
  configured timeout (default 1800s / 30 minutes) even if the
  orchestrator crashes or is killed, preventing orphaned pods

**Credential separation.** Data source credentials (GitLab tokens, kubeconfig)
live in the orchestrator process only, injected via K8s Secrets. The sandbox
container has no credentials, no SA token
(`automountServiceAccountToken: false`), and no network access. Data flows
into the sandbox via `stdin` piping through `write_file`.

**Command execution via kubectl exec.** The K8s sandbox uses a hybrid approach:
the Kubernetes Python client manages pod lifecycle (create, wait, delete),
while `kubectl exec` handles command execution. This avoids the complexity and
reliability issues of the websocket-based exec API.

**KubeVirt VM isolation.** KubeVirt sandbox VMs run as `root` inside the guest
OS, but the VM itself is contained by the KubeVirt hypervisor (QEMU/KVM). The
VM has no access to the Kubernetes API, no ServiceAccount token, and no
credentials. SSH keys are ephemeral (generated per sandbox start for direct
mode, or shared per pool for pool mode) and cleaned up with the VMI.

## Sandbox Backends

|             | Podman (local)                 | K8s (direct)                    | K8sPool (production)              | KubeVirt (direct)               | KubeVirtPool                 |
| ----------- | ------------------------------ | ------------------------------- | --------------------------------- | ------------------------------- | ---------------------------- |
| **Start**   | `podman run -d --network=none` | `create_namespaced_pod`         | Claim standby pod from Deployment | Create VMI + SSH Secret         | Claim standby VMI from VMIRS |
| **Exec**    | `podman exec`                  | `kubectl exec`                  | `kubectl exec`                    | `ssh`                           | `ssh`                        |
| **Auth**    | Local Podman socket            | In-cluster SA or kubeconfig     | In-cluster SA or kubeconfig       | In-cluster SA or kubeconfig     | In-cluster SA or kubeconfig  |
| **Network** | None (`--network=none`)        | None (deny-all `NetworkPolicy`) | None (deny-all `NetworkPolicy`)   | Cluster pod network (SSH)       | Cluster pod network (SSH)    |
| **User**    | `65532` (fixed)                | Namespace UID range (SCC)       | Namespace UID range (SCC)         | `root` (inside VM)              | `root` (inside VM)           |
| **Cleanup** | `podman rm -f`                 | `delete_namespaced_pod`         | `delete_namespaced_pod`           | Delete VMI + Secret + temp keys | Delete VMI                   |

All five implement the `Sandbox` protocol: `start()`, `exec()`,
`write_file()`, `read_file()`, `cleanup()`, `linger()`.

The pod pool backend (`k8spool`) eliminates pod startup latency by claiming
pre-warmed pods from a Kubernetes Deployment. Claimed pods are detached
from the ReplicaSet and the Deployment automatically creates replacements.
After a successful workflow, pool pods linger for `linger_seconds` (default
300, configurable, 0 to disable) so that user replies can reuse the same
pod without re-creating it or restoring from S3. The reaper runs once per
workflow execution and deletes expired lingering pods. It also evicts excess
lingering pods beyond `max_lingering_pods` (default 2), starting with those
closest to their deadline. See the design doc (section 8.8) for details.

The KubeVirt backends (`kubevirt`, `kubevirtpool`) provide full VM isolation
using KubeVirt VirtualMachineInstances. The VM boots from a containerDisk
image and SSH keys are injected via a Kubernetes Secret volume (the VM
image's `inject-ssh-keys.service` reads from `/dev/disk/by-id/virtio-ssh-pubkeys`).
Command execution uses SSH instead of `kubectl exec`. The `kubevirtpool`
backend claims pre-warmed VMIs from a VirtualMachineInstanceReplicaSet,
with the same claim/reap/linger semantics as the pod pool. The sandbox
backend can be set per-workflow via the `sandbox:` field in the workflow
config, with resolution order: workflow config > CLI `--sandbox` > default.

The Python API does not pass caller credentials into sandbox processes.
Callers must continue to keep credentials on the trusted side of the API and
provide the namespace network and resource controls required by their backend.
For workflow and CLI integration details, see [Hummingbird Agent][agent-docs].

## Development

```bash
cd hummingbird-sandbox
python3 -m unittest discover -s tests -t .
ruff check .
```

## License

This project is licensed under the GNU General Public License v3.0 or later -
see the [LICENSE][license] file for details.

[agent-docs]: https://hummingbird-project.io/docs/background/tools/hummingbird-agent/
[license]: https://gitlab.com/redhat/hummingbird/tools/-/blob/main/LICENSE
