Hummingbird Sandbox
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:
make hummingbird-sandbox/setup
Usage
Create a backend through the shared factory, then always clean it up:
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:
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-v2Security 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, andactiveDeadlineSecondsare configurable viasettings.sandboxin the config file. Network access is denied by aNetworkPolicyon 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 claimspods/exec: create– command execution viakubectl execvirtualmachineinstances.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.
Development
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 file for details.