Skip to content

K8s Bridge — Deployment & Install Guide

The k8s-bridge is an HTTP bridge that lets Cloudflare Workers manage sandbox pods on a Kubernetes cluster via a minimal REST API. Because Cloudflare Workers run in a V8 isolate and cannot load @kubernetes/client-node or any other native/Node-gyp library, they cannot call the Kubernetes API directly. k8s-bridge solves this by exposing a small, credential-gated HTTP API that maps to Kubernetes API operations.

Cloudflare Worker → k8s-bridge (HTTPS + Bearer) → Kubernetes API → Pod

The bridge itself ships as a container image and runs as a Deployment inside your cluster. The OMA sandbox adapter (packages/sandbox/src/adapters/k8s-bridge.ts) uses globalThis.fetch — zero driver deps — making it the only sandbox provider that works in a Cloudflare Worker while managing real Kubernetes pods.


RequirementVersion / Notes
Kubernetes clusterv1.25+ (tested on 1.28–1.31)
Helm 3helm version must show v3.x
kubectlConfigured with cluster-admin or equivalent
DNS domainPoints to your ingress controller (e.g. k8s-bridge.example.com)
cert-managerInstalled in cluster for automatic TLS
Metrics ServerOptional — required for /api/v1/sandboxes/metrics
OMA deploymentCloudflare Workers or self-host Node instance

Terminal window
# 1. Add the Helm repository
helm repo add oma https://charts.oma.duyet.net
helm repo update
# 2. Generate a strong random token for API authentication
export K8S_BRIDGE_TOKEN=$(openssl rand -base64 32)
echo "Save this token — you will need it in the OMA sandbox config:"
echo "$K8S_BRIDGE_TOKEN"
# 3. Install the chart
helm install oma-k8s-bridge oma/oma-k8s-bridge \
--namespace oma-sandbox \
--create-namespace \
--set secret.token=$K8S_BRIDGE_TOKEN \
--set ingress.enabled=true \
--set ingress.host=k8s-bridge.example.com
# 4. Wait for the deployment to roll out
kubectl -n oma-sandbox rollout status deployment/oma-k8s-bridge
# 5. Verify the bridge is healthy
curl -H "Authorization: Bearer $K8S_BRIDGE_TOKEN" \
https://k8s-bridge.example.com/api/v1/health
# Expected response:
# {"status":"ok","kubernetes":"v1.30.2","namespace":"oma-sandbox"}

All values in values.yaml:

ParameterTypeDefaultDescription
replicaCountint2Number of bridge replicas
image.repositorystringghcr.io/duyet/oma/k8s-bridgeContainer image
image.tagstringlatestImage tag
image.pullPolicystringIfNotPresentImage pull policy
secret.tokenstring""Bearer token for API auth (REQUIRED)
secret.existingSecretstring""Name of existing Secret (alternative to token)
secret.tokenKeystring"token"Key inside existing secret that holds the token
service.typestringClusterIPService type
service.portint8080Service port
ingress.enabledboolfalseEnable ingress
ingress.hoststring""Ingress hostname (required when enabled)
ingress.classNamestring""Ingress class name (leave empty to use cluster default)
ingress.annotationsobject{}Additional ingress annotations
ingress.tlsbooltrueEnable TLS via cert-manager ClusterIssuer
ingress.clusterIssuerstringletsencrypt-prodcert-manager ClusterIssuer name
resources.requests.cpustring"100m"CPU request
resources.requests.memorystring"128Mi"Memory request
resources.limits.cpustring"500m"CPU limit
resources.limits.memorystring"512Mi"Memory limit
autoscaling.enabledboolfalseEnable HPA
autoscaling.minReplicasint2Minimum replicas
autoscaling.maxReplicasint10Maximum replicas
autoscaling.targetCPUUtilizationint70Target CPU utilization percentage
autoscaling.targetMemoryUtilizationint80Target memory utilization percentage
rbac.createbooltrueCreate ClusterRole + ClusterRoleBinding
rbac.namespacedbooltrueRestrict RBAC to a single namespace
rbac.targetNamespacestringoma-sandboxNamespace for sandbox pods
nodeSelectorobject{}Node selector constraints
tolerationslist[]Pod tolerations
affinityobject{}Pod affinity rules
podAnnotationsobject{}Additional pod annotations
podLabelsobject{}Additional pod labels
extraEnvlist[]Extra environment variables
networkPolicy.enabledbooltrueCreate NetworkPolicy
networkPolicy.egressCIDRslist["0.0.0.0/0"]Allowed egress CIDRs
securityContext.readOnlyRootFilesystembooltrueImmutable rootfs
securityContext.runAsNonRootbooltrueNon-root user
securityContext.runAsUserint65534nobody UID
securityContext.capabilities.droplist["ALL"]Drop all kernel capabilities
serviceAccount.createbooltrueCreate service account
serviceAccount.namestring""Service account name (default: release name)
podDisruptionBudget.enabledbooltrueEnable PDB
podDisruptionBudget.minAvailableint1Minimum available pods
livenessProbeobject{...}Liveness probe config
readinessProbeobject{...}Readiness probe config
logLevelstring"info"Log level (debug, info, warn, error)
box.defaultImagestring"ghcr.io/duyet/oma-runtime-base:latest"Default sandbox container image
box.defaultCpustring"1"Default CPU for sandbox pods
box.defaultMemorystring"512Mi"Default memory for sandbox pods
box.defaultTtlSecondsint3600Default TTL for sandbox pods (seconds)
box.maxCpustring"4"Maximum allowed CPU per box
box.maxMemorystring"4Gi"Maximum allowed memory per box
box.maxBoxesPerSessionint3Max boxes per session id
box.maxConcurrentBoxesint50Global max concurrent boxes

┌─────────────────────────────────────────────────────────────────┐
│ Cloudflare Worker │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ K8sBridgeSandbox (packages/sandbox/src/adapters/) │ │
│ │ • globalThis.fetch — no driver deps │ │
│ │ • K8S_BRIDGE_URL env → baseUrl │ │
│ │ • K8S_BRIDGE_TOKEN env → Bearer auth │ │
│ │ • Lazy box creation (ensureBox on first use) │ │
│ └────────────────────┬──────────────────────────────────────┘ │
└───────────────────────┼──────────────────────────────────────────┘
│ HTTPS + Authorization: Bearer <token>
▼
┌─────────────────────────────────────────────────────────────────┐
│ Kubernetes Cluster │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ k8s-bridge Pod (Deployment) │ │
│ │ • HTTP server :8080 │ │
│ │ • Validates Bearer token │ │
│ │ • Translates REST → Kubernetes API calls │ │
│ │ • Manages sandbox Pod lifecycle │ │
│ │ • Streams exec/fetch responses │ │
│ └────────────────────┬──────────────────────────────────────┘ │
│ │ In-cluster K8s API (ServiceAccount) │
│ ▼ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ Sandbox Pod (Ephemeral) │ │
│ │ • Runs container image (default: node:22-slim) │ │
│ │ • /workspace for agent file operations │ │
│ │ • TTL-based cleanup (default: 1 hour) │ │
│ │ • Dedicated ServiceAccount per namespace │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ cert-manager → Ingress → TLS termination │ │
│ └───────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘

Request flow for a typical sandbox operation:

1. Agent tool calls exec("npm test")
2. K8sBridgeSandbox.ensureBox() → POST /api/v1/boxes (lazy, first call only)
3. K8sBridgeSandbox.exec() → POST /api/v1/boxes/<id>/exec
4. k8s-bridge receives request, validates token
5. k8s-bridge creates Pod (if not exists) or exec into existing Pod
6. Streams stdout/stderr back to the Worker
7. Sandbox.destroy() → DELETE /api/v1/boxes/<id>

All endpoints require the Authorization: Bearer <token> header (value of K8S_BRIDGE_TOKEN). Responses are JSON unless noted.

Health check. Returns bridge and Kubernetes version info.

Terminal window
curl -H "Authorization: Bearer $K8S_BRIDGE_TOKEN" \
https://k8s-bridge.example.com/api/v1/health
{
"status": "ok",
"version": "1.0.0",
"kubernetes": "v1.30.2",
"namespace": "oma-sandbox",
"uptime_seconds": 84320,
"active_boxes": 3,
"total_boxes_created": 157
}

Returns cluster metadata.

Terminal window
curl -H "Authorization: Bearer $K8S_BRIDGE_TOKEN" \
https://k8s-bridge.example.com/api/v1/cluster/info
{
"version": "v1.30.2",
"platform": "linux/amd64",
"nodes": 5,
"capacity": {
"cpu": "16",
"memory": "65536Ki",
"pods": "110"
},
"allocatable": {
"cpu": "14",
"memory": "62000Ki",
"pods": "105"
}
}

List cluster nodes with status, roles, and version.

Terminal window
curl -H "Authorization: Bearer $K8S_BRIDGE_TOKEN" \
https://k8s-bridge.example.com/api/v1/cluster/nodes
{
"nodes": [
{
"name": "pool-1-abcde",
"status": "Ready",
"roles": ["control-plane", "worker"],
"version": "v1.30.2",
"cpu": {"capacity": "4", "allocatable": "3.5"},
"memory": {"capacity": "16384Mi", "allocatable": "15500Mi"},
"pods": {"capacity": "110", "allocatable": "105", "running": 12}
}
]
}

List all sandbox pods in the target namespace.

Terminal window
curl -H "Authorization: Bearer $K8S_BRIDGE_TOKEN" \
https://k8s-bridge.example.com/api/v1/sandboxes
{
"sandboxes": [
{
"name": "box-abc123",
"namespace": "oma-sandbox",
"status": "Running",
"node": "pool-1-abcde",
"image": "node:22-slim",
"created_at": "2026-07-15T10:00:00Z",
"age_seconds": 3420,
"session_id": "sess_xyz",
"cpu": "1",
"memory": "512Mi"
}
],
"total": 1,
"namespace": "oma-sandbox"
}

Fetch logs from a sandbox pod. Supports optional ?tailLines= parameter.

Terminal window
# Last 100 lines
curl -H "Authorization: Bearer $K8S_BRIDGE_TOKEN" \
https://k8s-bridge.example.com/api/v1/sandboxes/box-abc123/logs?tailLines=100
# All logs (no tailLines)
curl -H "Authorization: Bearer $K8S_BRIDGE_TOKEN" \
https://k8s-bridge.example.com/api/v1/sandboxes/box-abc123/logs
> npm test
PASS tests/index.test.ts
✓ should return hello (2ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total

Pod resource metrics. Requires metrics-server in the cluster.

Terminal window
curl -H "Authorization: Bearer $K8S_BRIDGE_TOKEN" \
https://k8s-bridge.example.com/api/v1/sandboxes/metrics
{
"metrics": [
{
"pod_name": "box-abc123",
"namespace": "oma-sandbox",
"cpu": "12m",
"memory": "45Mi",
"timestamp": "2026-07-15T12:00:00Z",
"window_seconds": 60
}
]
}

Create a sandbox box (ephemeral pod).

Terminal window
curl -X POST -H "Authorization: Bearer $K8S_BRIDGE_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"sessionId": "sess_xyz",
"image": "node:22-slim",
"cpu": 1,
"memory": 512,
"env": {
"NODE_ENV": "development",
"DEBUG": "true"
}
}' \
https://k8s-bridge.example.com/api/v1/boxes
{
"box_id": "box-abc123",
"status": "Pending",
"namespace": "oma-sandbox",
"pod_name": "box-abc123",
"image": "node:22-slim",
"cpu": "1",
"memory": "512Mi",
"created_at": "2026-07-15T10:00:00Z"
}
FieldTypeRequiredDefaultDescription
sessionIdstringYes—OMA session identifier
imagestringNoghcr.io/duyet/oma-runtime-base:latestContainer image for the sandbox
cpunumberNo1CPU cores (fractional OK)
memorynumberNo512Memory in MiB
envobjectNo{}Environment variables
runtimeClassNamestringNo—Kubernetes RuntimeClass (e.g. kata)
serviceAccountNamestringNo—Custom ServiceAccount for the pod

Destroy a sandbox box.

Terminal window
curl -X DELETE -H "Authorization: Bearer $K8S_BRIDGE_TOKEN" \
https://k8s-bridge.example.com/api/v1/boxes/box-abc123
{
"box_id": "box-abc123",
"status": "Terminated",
"deleted_at": "2026-07-15T12:00:00Z"
}

Returns 404 if the box does not exist (idempotent cleanup).

Execute a command inside a running sandbox box.

Terminal window
curl -X POST -H "Authorization: Bearer $K8S_BRIDGE_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"command": "npm test",
"timeoutMs": 30000,
"workdir": "/workspace"
}' \
https://k8s-bridge.example.com/api/v1/boxes/box-abc123/exec
{
"exit_code": 0,
"stdout": "PASS tests/index.test.ts\n ✓ should return hello (2ms)\n\nTest Suites: 1 passed, 1 total\nTests: 1 passed, 1 total\n",
"stderr": "",
"duration_ms": 2843
}
FieldTypeRequiredDefaultDescription
commandstringYes—Shell command to execute
timeoutMsnumberNo120000Execution timeout in milliseconds
workdirstringNo/workspaceWorking directory

Read a file from the sandbox box.

Terminal window
curl -H "Authorization: Bearer $K8S_BRIDGE_TOKEN" \
"https://k8s-bridge.example.com/api/v1/boxes/box-abc123/files?path=/workspace/output.json"
{
"path": "/workspace/output.json",
"size": 1234,
"content": "{\"result\": \"ok\", \"data\": [...]}",
"encoding": "text"
}

For binary files, append ?path=...&base64=true:

{
"path": "/workspace/screenshot.png",
"size": 45678,
"content": "iVBORw0KGgo... (base64-encoded)",
"encoding": "base64"
}

Write a file to the sandbox box.

Terminal window
# Write text content
curl -X PUT -H "Authorization: Bearer $K8S_BRIDGE_TOKEN" \
-H "Content-Type: text/plain" \
-d 'console.log("hello world");' \
"https://k8s-bridge.example.com/api/v1/boxes/box-abc123/files?path=/workspace/index.js"
# Write binary content (base64-encoded)
curl -X PUT -H "Authorization: Bearer $K8S_BRIDGE_TOKEN" \
-H "Content-Type: text/plain" \
-d 'iVBORw0KGgo...' \
"https://k8s-bridge.example.com/api/v1/boxes/box-abc123/files?path=/workspace/image.png&base64=true"
{
"path": "/workspace/index.js",
"size": 26,
"encoding": "text"
}

Set environment variables on a running box. Does not restart the pod — variables are injected into subsequent exec calls.

Terminal window
curl -X POST -H "Authorization: Bearer $K8S_BRIDGE_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"envVars": {
"ANTHROPIC_API_KEY": "sk-ant-...",
"NODE_OPTIONS": "--max-old-space-size=4096"
}
}' \
https://k8s-bridge.example.com/api/v1/boxes/box-abc123/env
{
"box_id": "box-abc123",
"env_count": 2,
"updated_at": "2026-07-15T12:05:00Z"
}

Get the current status of a sandbox box.

Terminal window
curl -H "Authorization: Bearer $K8S_BRIDGE_TOKEN" \
https://k8s-bridge.example.com/api/v1/boxes/box-abc123/status
{
"box_id": "box-abc123",
"pod_name": "box-abc123",
"namespace": "oma-sandbox",
"status": "Running",
"phase": "Running",
"node": "pool-1-abcde",
"image": "node:22-slim",
"cpu": "1",
"memory": "512Mi",
"created_at": "2026-07-15T10:00:00Z",
"age_seconds": 7500,
"restarts": 0,
"host_ip": "10.0.0.5",
"pod_ip": "10.42.0.12",
"conditions": [
{"type": "Initialized", "status": "True"},
{"type": "Ready", "status": "True"},
{"type": "ContainersReady", "status": "True"},
{"type": "PodScheduled", "status": "True"}
]
}

The chart creates a ClusterRole with the following rules (scoped to a single namespace when rbac.namespaced=true):

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: oma-k8s-bridge
rules:
# Cluster capacity reporting — get node count, allocatable resources,
# and pod density for the /api/v1/cluster/info and /api/v1/cluster/nodes endpoints.
- apiGroups: [""]
resources: ["nodes"]
verbs: ["list", "get"]
# Sandbox lifecycle management — create, list, watch, and delete
# ephemeral pods in the sandbox namespace.
- apiGroups: [""]
resources: ["pods"]
verbs: ["create", "get", "list", "watch", "delete", "patch"]
# Pod log streaming — required for /api/v1/sandboxes/:podName/logs.
- apiGroups: [""]
resources: ["pods/log"]
verbs: ["get"]
# Pod status patching — required for status reporting on boxes and
# condition tracking in /api/v1/boxes/:id/status.
- apiGroups: [""]
resources: ["pods/status"]
verbs: ["patch"]
# Resource usage metrics — required for /api/v1/sandboxes/metrics.
# Only needed if the metrics-server is installed and you use that endpoint.
- apiGroups: ["metrics.k8s.io"]
resources: ["pods"]
verbs: ["get", "list"]
# Exec into running pods — required for /api/v1/boxes/:id/exec.
- apiGroups: [""]
resources: ["pods/exec"]
verbs: ["create"]

Why each permission is needed:

PermissionUsed ByPurpose
nodes list/get/api/v1/cluster/info, /api/v1/cluster/nodesCapacity reporting, scheduling decisions
pods create/get/list/watch/delete/patchAll box endpointsFull sandbox pod lifecycle
pods/log get/api/v1/sandboxes/:podName/logsFetch build/test output for the agent
pods/status patch/api/v1/boxes/:id/statusTrack pod conditions and phase
pods/exec create/api/v1/boxes/:id/execRun commands inside the sandbox
metrics.k8s.io pods get/list/api/v1/sandboxes/metricsResource usage monitoring

When rbac.namespaced=true (default), the ClusterRole is paired with a RoleBinding (not ClusterRoleBinding) that scopes all pod operations to rbac.targetNamespace (default: oma-sandbox). Node and metrics permissions still require ClusterRole since those resources are cluster-scoped, but the binding limits which namespace the bridge can create pods in.


Every request must include Authorization: Bearer <token>. The token is generated at install time and stored in a Kubernetes Secret:

apiVersion: v1
kind: Secret
metadata:
name: oma-k8s-bridge-token
namespace: oma-sandbox
type: Opaque
data:
token: <base64-encoded>

Rotate the token at any time without redeploying:

Terminal window
# Generate new token
NEW_TOKEN=$(openssl rand -base64 32)
# Update the secret
kubectl -n oma-sandbox patch secret oma-k8s-bridge-token \
-p "{\"data\":{\"token\":\"$(echo -n "$NEW_TOKEN" | base64 -w0)\"}}"
# Restart the deployment to pick up the new token
kubectl -n oma-sandbox rollout restart deployment/oma-k8s-bridge
# Update OMA environment variable
# (set K8S_BRIDGE_TOKEN=$NEW_TOKEN in your OMA config)

The chart uses cert-manager to provision and renew TLS certificates automatically:

ingress:
enabled: true
host: oma-k8s-bridge.example.com
tls: true
clusterIssuer: letsencrypt-prod

This produces an Ingress + Certificate pair. The certificate is stored in a Secret named oma-k8s-bridge-tls and mounted automatically.

The bridge runs with a hardened security context:

securityContext:
readOnlyRootFilesystem: true # immutable rootfs — no writes to /bin, /etc, /usr
runAsNonRoot: true # reject if runAsUser is 0
runAsUser: 65534 # nobody
capabilities:
drop: ["ALL"] # no kernel capabilities

Sandbox pods (boxes) also run with restricted defaults, enforced via a PodSecurityAdmission label on the oma-sandbox namespace:

Terminal window
kubectl label ns oma-sandbox pod-security.kubernetes.io/enforce=restricted

The chart creates a default-deny NetworkPolicy when networkPolicy.enabled=true:

# Only allow ingress from the cluster (for kubelet health probes)
# and egress to the outside world (for the agent to fetch dependencies).
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: oma-k8s-bridge
spec:
podSelector:
matchLabels:
app.kubernetes.io/name: oma-k8s-bridge
policyTypes:
- Ingress
- Egress
ingress:
- from:
- podSelector: {} # same-namespace pods
- namespaceSelector: {} # kubelet health probes
ports:
- port: 8080
egress:
- to:
- ipBlock:
cidr: 0.0.0.0/0 # override with networkPolicy.egressCIDRs

The bridge enforces per-client rate limits at the application layer:

LimitValue
Requests per second100
Burst200
Concurrent execs per box5
Max exec duration10 minutes

Terminal window
helm upgrade oma-k8s-bridge oma/oma-k8s-bridge \
--set autoscaling.enabled=true \
--set autoscaling.minReplicas=2 \
--set autoscaling.maxReplicas=10 \
--set autoscaling.targetCPUUtilization=70 \
--set autoscaling.targetMemoryUtilization=80

Control sandbox resource consumption at the bridge level:

Terminal window
helm upgrade oma-k8s-bridge oma/oma-k8s-bridge \
--set box.defaultCpu=1 \
--set box.defaultMemory=512Mi \
--set box.maxCpu=4 \
--set box.maxMemory=4Gi \
--set box.maxConcurrentBoxes=50 \
--set box.defaultTtlSeconds=3600

The bridge enforces maxCpu and maxMemory server-side — requests exceeding these limits receive a 400 Bad Request response.

The /api/v1/health endpoint returns operational metrics for external monitoring:

Terminal window
# Prometheus blackbox probe
curl -H "Authorization: Bearer $K8S_BRIDGE_TOKEN" \
https://k8s-bridge.example.com/api/v1/health

Key metrics to alert on:

MetricThresholdAction
active_boxes> 80% of maxConcurrentBoxesScale up or investigate leak
Uptime< 60s after restartInvestigate crash loop
Health statusnot "ok"Page operator

For Prometheus-native monitoring, deploy the chart with podAnnotations to enable metric scraping:

Terminal window
helm upgrade oma-k8s-bridge oma/oma-k8s-bridge \
--set podAnnotations."prometheus\.io/scrape"=true \
--set podAnnotations."prometheus\.io/port"=8080 \
--set podAnnotations."prometheus\.io/path"=/metrics

Each OMA deployment (staging, production, team-specific) should use a dedicated namespace:

Terminal window
helm install oma-k8s-bridge oma/oma-k8s-bridge \
--namespace oma-sandbox-prod \
--create-namespace \
--set rbac.targetNamespace=oma-sandbox-prod \
--set secret.token=$K8S_BRIDGE_TOKEN_PROD \
...

This ensures:

  • Pods from one deployment cannot see pods from another
  • Resource quotas per namespace
  • Independent NetworkPolicy boundaries
  • Clean RBAC scoping
Terminal window
helm repo update
helm upgrade oma-k8s-bridge oma/oma-k8s-bridge \
--namespace oma-sandbox \
--reuse-values

Review changes before applying:

Terminal window
helm diff upgrade oma-k8s-bridge oma/oma-k8s-bridge --namespace oma-sandbox

Set environment variables on the oma-server process:

Terminal window
# .env or docker-compose environment
SANDBOX_PROVIDER=k8s-bridge
K8S_BRIDGE_URL=http://k8s-bridge:8080
K8S_BRIDGE_TOKEN=<the-token-you-generated>
SANDBOX_IMAGE=node:22-slim
K8S_CPU=1
K8S_MEMORY=512

The sandboxFactory in packages/sandbox/src/adapters/k8s-bridge.ts reads these env vars at boot and constructs a K8sBridgeSandbox instance for each session.

Add the environment bindings to your wrangler.jsonc:

{
"name": "managed-agents-agent",
"vars": {
"SANDBOX_PROVIDER": "k8s-bridge",
"K8S_BRIDGE_URL": "https://k8s-bridge.example.com",
"K8S_BRIDGE_TOKEN": "",
"SANDBOX_IMAGE": "node:22-slim",
"K8S_CPU": "1",
"K8S_MEMORY": "512"
}
}

The K8S_BRIDGE_TOKEN should be set as a secret, not a plain var:

Terminal window
echo "$K8S_BRIDGE_TOKEN" | npx wrangler secret put K8S_BRIDGE_TOKEN

In the OMA agent config, reference the k8s-bridge sandbox provider:

{
"name": "k8s-coder",
"model": "claude-sonnet-4-6",
"system": "You are a coding assistant running on Kubernetes.",
"tools": [{ "type": "agent_toolset_20260401" }],
"environment_id": "env_k8s"
}

Where the environment env_k8s has:

{
"name": "k8s-sandbox",
"config": {
"sandbox_provider": "k8s-bridge",
"image": "node:22-slim"
}
}

On the self-host Node runtime, k8s-bridge is registered through the SandboxProviderRegistry in packages/sandbox. It is available alongside all other providers. No additional registration step is needed — set SANDBOX_PROVIDER=k8s-bridge and the factory resolves it.

Provider Registration (Cloudflare Workers)

Section titled “Provider Registration (Cloudflare Workers)”

On the Cloudflare runtime, k8s-bridge is wired at build time through the agent worker’s resolveCfSandbox function. Ensure the following are true:

  1. The agent worker has K8S_BRIDGE_URL as a binding (env var or secret)
  2. SANDBOX_PROVIDER=k8s-bridge or the environment’s config.sandbox_provider is set to "k8s-bridge"
  3. K8S_BRIDGE_TOKEN is set as a wrangler secret
Terminal window
# 1. Create an agent on the k8s-bridge environment
AGENT_ID=$(curl -s -X POST $OMA_BASE/v1/agents \
-H "x-api-key: $OMA_API_KEY" \
-H "content-type: application/json" \
-d '{
"name": "k8s-test",
"model": "claude-sonnet-4-6",
"system": "You are a helpful assistant.",
"tools": [{"type": "agent_toolset_20260401"}]
}' | jq -r .id)
# 2. Create a session
SESSION_ID=$(curl -s -X POST $OMA_BASE/v1/sessions \
-H "x-api-key: $OMA_API_KEY" \
-H "content-type: application/json" \
-d "{\"agent_id\":\"$AGENT_ID\"}" | jq -r .id)
# 3. Send a message that triggers a sandbox operation
curl -s -X POST $OMA_BASE/v1/sessions/$SESSION_ID/events \
-H "x-api-key: $OMA_API_KEY" \
-H "content-type: application/json" \
-d '{"events":[{"type":"user.message","content":[{"type":"text","text":"Run: echo hello from k8s && uname -a"}]}]}'
# 4. Verify the pod was created on the cluster
kubectl -n oma-sandbox get pods -l app.kubernetes.io/created-by=oma-k8s-bridge
# 5. Check k8s-bridge health for active box count
curl -H "Authorization: Bearer $K8S_BRIDGE_TOKEN" \
https://k8s-bridge.example.com/api/v1/health | jq .active_boxes
SymptomLikely CauseCheck
k8s-bridge create failed: 401Wrong or missing tokenVerify K8S_BRIDGE_TOKEN matches the Secret
k8s-bridge create failed: 403RBAC misconfigurationkubectl auth can-i --list --as=system:serviceaccount:oma-sandbox:k8s-bridge
Pod stuck in PendingInsufficient cluster resourceskubectl describe pod -n oma-sandbox <box-name>
Exec hangs or times outPod not Ready yetCheck kubectl get pods -n oma-sandbox -w
/api/v1/sandboxes/metrics returns 500Metrics Server not installedkubectl get pods -n kube-system -l k8s-app=metrics-server
Bridge pod crash loopingInvalid config or missing token secretkubectl -n oma-sandbox logs deployment/oma-k8s-bridge