Skip to main content

OpenShell Gateways

The gateway is the control plane of OpenShell. Every CLI command, SDK call, and UI interaction goes through it. It owns all durable platform state — sandboxes, policy revisions, provider credentials, inference configuration, and SSH sessions — and it coordinates the sandbox supervisor processes that run inside each agent environment.

OpenShell is structured around three runtime components:

CLI / SDK / UI ──gRPC──► Gateway ──provisions──► Compute Driver
│ │
│◄──outbound connection── Supervisor (inside sandbox)

The critical design choice: supervisors connect outbound to the gateway, never the other way around. This eliminates NAT traversal, port-mapping complexity, and the need for the gateway to know sandbox network addresses.


The Supervisor

The supervisor is a process that runs inside every sandbox, alongside the agent. It is not part of the gateway or the host — it is the local security boundary at the workload level.

Where it runs

DriverSupervisor location
Docker / PodmanA process inside the sandbox container, started before the agent
KubernetesA process inside the sandbox pod
MicroVMA process inside the VM

What it does

The supervisor enforces all security controls locally, from inside the sandbox:

LayerWhat it enforces
ProcessDrops privileges; starts the agent as an unprivileged child process
FilesystemLocks undeclared paths using Landlock LSM before the agent starts
NetworkRoutes all outbound traffic through a local proxy backed by an OPA policy engine
CredentialsInjects credentials as opaque placeholders into the agent's environment; resolves them at the proxy boundary when the agent makes HTTP requests
InferenceIntercepts calls to inference.local and routes them to the configured provider, hiding API keys from the agent
ObservabilityCollects sandbox logs and relays them to the gateway; manages session state

Why enforcement happens inside the sandbox

Policy enforcement cannot happen at the gateway level for one key reason: the gateway cannot observe which process is making an outbound connection. The proxy inside the supervisor can — it runs in the same namespace as the agent and can match each outbound TCP connection to the binary that opened it. This is what allows a policy rule like "only /usr/bin/gh may call api.github.com" to actually mean something.

Static vs. dynamic controls

Some supervisor controls are locked in at sandbox creation time:

  • Filesystem path locks (Landlock rules)
  • Process isolation settings (seccomp filters, unprivileged user/group)

Others can be updated over the live stream while the sandbox is running:

CategoryCan update live?Requires sandbox restart?
Network egress policyYesNo
Credential valuesYesNo
Inference routingYesNo
Filesystem locksNoYes
Process isolation (seccomp, Landlock)NoYes

Compute Drivers

DriverSandbox locationBest for
DockerContainers on the gateway hostSolo development, quick iteration
PodmanRootless containersWorkstations avoiding rootful Docker daemon
KubernetesOperator-managed cluster podsShared clusters, cloud environments, GPU workloads
MicroVMVM-backed sandboxesWorkflows requiring VM-level isolation

Auto-detection order when no driver is configured: Kubernetes → Podman → Docker. MicroVM is never auto-detected and must be configured explicitly.

All drivers expose an identical gateway API surface. Sandboxes, policies, and providers behave consistently regardless of which driver is active.


Authentication

The gateway listens on a single service port and multiplexes gRPC and HTTP traffic. Supported authentication modes:

ModeWhen to use
mTLS user authLocal single-user Docker, Podman, or VM gateways
PlaintextLocal development behind a trusted reverse proxy
Unauthenticated local usersTrusted Kubernetes dev clusters; requires allow_unauthenticated_users = true
Cloudflare JWTEdge-authenticated deployments where Cloudflare Access supplies identity
OIDCBearer-token auth with browser PKCE or client credentials login

Kaiden uses allow_unauthenticated_users = true for local single-user operation — the gateway only listens on loopback and there is only one user.


Registering and Managing Gateways

Adding a gateway

# Local gateway (trusted, no browser login)
openshell gateway add http://127.0.0.1:17670 --local --name local

# Remote authenticated gateway (triggers browser PKCE login)
openshell gateway add https://gateway.example.com --name production

# Re-authenticate after token expiry
openshell gateway login production

Active gateway resolution (priority order)

  1. OPENSHELL_GATEWAY environment variable (per-command override, highest priority)
  2. -g flag (per-command override)
  3. Persisted default set by gateway add or gateway select
# Switch persisted default
openshell gateway select production

# Single-command override
openshell status -g staging
openshell sandbox list -g staging

Useful commands

openshell gateway list # all registered gateways
openshell gateway info # active gateway metadata and compute driver details
openshell gateway remove prod # remove user-level registration
openshell status # quick health check against active gateway

Service Forwarding

When an agent starts a network service inside the sandbox (a dev server, Jupyter notebook, REST API), you can reach it from your host browser via service forwarding.

Exposed sandbox services are reachable at:

http://<sandbox-name>.openshell.localhost:<port>/
http://<sandbox-name>--<service-name>.openshell.localhost:<port>/

The gateway routes these requests through the supervisor relay — no manual port binding or Docker -p flags needed.


Configuration (TOML)

The gateway reads configuration from three sources, in descending priority:

CLI flags > OPENSHELL_* env vars > TOML config file > built-in defaults

The TOML file is optional. When neither --config nor OPENSHELL_GATEWAY_CONFIG is set, the gateway uses only flags and env vars.

[openshell]
version = 1

[openshell.gateway]
bind_address = "127.0.0.1:17670"
log_level = "info"
compute_drivers = ["kubernetes"]

[openshell.gateway.auth]
allow_unauthenticated_users = false

[openshell.gateway.oidc]
issuer = "https://idp.example.com/realms/openshell"
audience = "openshell-cli"
admin_role = "openshell-admin"
user_role = "openshell-user"

Driver-specific tables ([openshell.drivers.podman], [openshell.drivers.kubernetes], etc.) set image pull policy, socket paths, and network names.

note

database_url must be supplied via OPENSHELL_DB_URL or --db-url — it is rejected if present in the TOML file to prevent accidental credential commits.


How Credentials Work End to End

When an agent uses a credential inside a sandbox (e.g., a GitHub PAT for the gh CLI):

  1. The gateway stores the token value encrypted in SQLite. The CLI never echoes it back.
  2. At sandbox creation, the supervisor injects the token as an opaque placeholder in the agent's environment:
    GITHUB_TOKEN=openshell:resolve:env:GITHUB_TOKEN
  3. When the agent makes an HTTP request to api.github.com, the supervisor's proxy intercepts it, detects the placeholder in the Authorization header, calls the gateway to resolve the real token, and forwards the request with the real credential.
  4. The agent receives a normal response. The real token never appears in the sandbox process environment, in logs, or in the agent's output.

Fail-closed: If the placeholder cannot be resolved (expired provider, gateway unreachable), the proxy rejects the request — the raw placeholder is never forwarded.


Troubleshooting

SymptomWhere to look
"cannot connect to gateway"openshell status and openshell doctor check
Sandbox stuck in ProvisioningDriver-specific logs (Docker daemon, Kubernetes events, Podman socket)
exec/connect hangsGateway logs for relay errors; supervisor relay session may have dropped
Sandboxes all Unknown after restartSupervisors reconnect automatically; re-initiate running exec sessions
# Health check
openshell status
openshell doctor check

# systemd (Docker/Podman)
systemctl --user status openshell-gateway
journalctl --user -u openshell-gateway --no-pager -n 50

# Kubernetes
kubectl -n openshell get pods
kubectl -n openshell logs deployment/openshell -c openshell-gateway --tail=100