OpenShell Runtime Backend
This guide covers OpenShell-specific configuration, deployment, and validation. See the Operations Guide for shared server configuration and scheduling behavior.
Configuration
ServerSettings loads environment variables with the DIFY_AGENT_ prefix. It
also reads .env and dify-agent/.env when present. Select
DIFY_AGENT_RUNTIME_BACKEND=openshell to use this backend. Shared settings,
including Agent Stub and file-service URLs, are documented in the
server configuration reference.
| Environment variable | Default | Description |
|---|---|---|
DIFY_AGENT_OPENSHELL_GATEWAY_ENDPOINT |
empty | OpenShell gateway gRPC endpoint as host:port (no scheme); required for OpenShell. Prefer localhost over an IPv6 literal when the gateway listens on loopback. |
DIFY_AGENT_OPENSHELL_WORKSPACE |
default |
OpenShell workspace that owns the sandboxes. Production multi-tenant deployments must use one workspace and one dedicated shared volume per tenant. |
DIFY_AGENT_OPENSHELL_BEARER_TOKEN |
empty | Static OIDC bearer token sent to the gateway. Combines with the mTLS bundle. The token is not refreshed in-process; restart agent_backend or switch to a long-lived token when it expires. |
DIFY_AGENT_OPENSHELL_TLS_CA_PATH |
empty | Custom gateway CA certificate path. Empty uses the system trust store. |
DIFY_AGENT_OPENSHELL_TLS_CLIENT_CERT_PATH |
empty | mTLS client certificate path; set together with the key path. |
DIFY_AGENT_OPENSHELL_TLS_CLIENT_KEY_PATH |
empty | mTLS client key path; set together with the certificate path. |
DIFY_AGENT_OPENSHELL_INSECURE |
false |
Use a plaintext gRPC channel. Local development only. |
DIFY_AGENT_OPENSHELL_SANDBOX_IMAGE |
langgenius/dify-agent-local-sandbox:latest |
Build the runtime image yourself from this checkout and explicitly set this to the resulting image reference. Do not rely on the code fallback or a published latest tag: older images may lack iproute2, which the OpenShell supervisor requires alongside shellctl. The env templates use the self-built docker.io/library/dify-agent-runtime:latest; see the build command below. |
DIFY_AGENT_OPENSHELL_DRIVER_CONFIG |
empty | JSON SandboxTemplate.driver_config; required for OpenShell and must be a non-empty object. Must mount the shared Home Snapshot volume at the shared mount path in every sandbox. |
DIFY_AGENT_OPENSHELL_SHARED_MOUNT_PATH |
/mnt/dify-agent-shared |
In-sandbox mount path of the shared volume; Home Snapshots live under home-snapshots/<tenant-digest>/. |
DIFY_AGENT_OPENSHELL_EGRESS_ALLOW |
empty | Optional comma-separated host:port egress allowlist (no scheme or path). Empty sends no network policy (gateway/driver default egress). When set, sandbox egress is enforced to exactly these endpoints — include the Agent Stub and files endpoints. |
DIFY_AGENT_OPENSHELL_SHELLCTL_AUTH_TOKEN |
empty | Required bearer token the exec-bootstrapped shellctl expects on its data plane. Empty is rejected at startup. |
DIFY_AGENT_OPENSHELL_SHELLCTL_PORT |
5004 |
Sandbox-loopback port shellctl listens on; reached through the gateway ForwardTcp tunnel. |
DIFY_AGENT_OPENSHELL_READY_TIMEOUT_SECONDS |
300 |
Maximum wait for a sandbox to reach the READY phase. |
DIFY_AGENT_OPENSHELL_EXEC_TIMEOUT_SECONDS |
120 |
Timeout for gateway exec calls (bootstrap and maintenance scripts). |
Deploy with the OpenShell backend
The OpenShell backend runs each Binding in its own sandbox on a self-hosted
NVIDIA OpenShell gateway. Dify Agent
only talks to the gateway gRPC API: it creates sandboxes from the configured
image, bootstraps shellctl through gateway exec, and reaches the shellctl data
plane through an authenticated ForwardTcp tunnel.
New Bindings are initialized and then stopped. Acquire starts a stopped sandbox when needed, verifies its Home and Workspace, ensures shellctl is running, and opens a fresh lease-local tunnel. Release closes only that lease's HTTP client and tunnel; it does not stop the sandbox or shellctl. Subsequent operations can reuse the running sandbox, and releasing one lease does not interrupt another lease on the same Binding.
There is no automatic idle-stop or sandbox TTL in this adapter. After a successful acquire/release cycle, the sandbox remains running until explicitly stopped or destroyed, or a runtime failure stops it. Operators must account for these running resources. Binding creation and failed-acquire cleanup retain their existing stop behavior; this change only makes lease release lightweight. Explicitly stopped sandboxes are restarted on the next acquire.
Backend selection lives in docker/.env; create it from the template first if
this deployment does not have one yet (cp docker/.env.example docker/.env).
Deployment prerequisites:
- An OpenShell gateway, version 0.0.106 or newer (validated against
0.0.106–0.0.110; the bundled SDK is pinned
>=0.0.106,<0.1.0), reachable fromagent_backendatDIFY_AGENT_OPENSHELL_GATEWAY_ENDPOINT, with credentials viaDIFY_AGENT_OPENSHELL_BEARER_TOKENor the mTLS bundle paths. Preferlocalhost:17670when the gateway listens on loopback. Sandboxes reconnect to the gateway throughhost.openshell.internal; on macOS local development bind the gateway to127.0.0.1:17670so that alias can reach it. - Sandboxes must reach
DIFY_AGENT_STUB_API_BASE_URLandDIFY_AGENT_SANDBOX_FILES_BASE_URL. When the gateway runs outside this Compose stack, set both to externally reachable URLs; compose-internal hostnames are not resolvable from OpenShell sandboxes. When the deployment restricts sandbox egress, setDIFY_AGENT_OPENSHELL_EGRESS_ALLOWso the sandbox policy carries an enforced allowlist covering at least those two endpoints:
text
DIFY_AGENT_OPENSHELL_EGRESS_ALLOW=agent.example.com:443,dify.example.com:443
Left empty (the default), the policy carries no network rules and egress follows the gateway/driver default — a stock Docker driver leaves sandbox outbound unrestricted, which agent-run tools (package installs, web access) may rely on. When set, egress is restricted to exactly the listed endpoints, from any in-sandbox binary.
- Build the runtime image yourself from the current checkout. It must
bundle shellctl and
iproute2. A published or cachedlanggenius/dify-agent-local-sandbox:latestis not a substitute: older images lackiproute2, causing the OpenShell supervisor to exit during startup. - A required
DIFY_AGENT_OPENSHELL_SHELLCTL_AUTH_TOKEN. shellctl is bootstrapped withSHELLCTL_ENABLE_PATH_ISOLATION=falsebecause the sandbox Landlock policy is authoritative; stacking both would require re-granting every device path twice. The token still authenticates the in-sandbox command plane. - A shared Home Snapshot volume mounted into every sandbox via
DIFY_AGENT_OPENSHELL_DRIVER_CONFIG. Production multi-tenant deployments must use one OpenShell workspace and one dedicated volume per tenant: Landlock is best-effort and every sandbox runs as the same image UID, so a shared volume is a shared trust boundary. Snapshots are stored underhome-snapshots/<tenant-digest>/; the sandbox policy grants only that tenant directory.
Build from the repository root (the directory containing dify-agent-runtime/):
docker build -f dify-agent-runtime/docker/Dockerfile \
-t dify-agent-runtime:latest dify-agent-runtime
Set the exact built image reference in dify-agent/.env for a local Agent
Backend process, or in docker/.env for Docker Compose:
DIFY_AGENT_OPENSHELL_SANDBOX_IMAGE=docker.io/library/dify-agent-runtime:latest
The gateway's Docker driver must use the same Docker daemon containing this
image. For a remote gateway or a multi-node deployment, tag and push the image
to a registry accessible to the compute nodes and configure that registry
reference instead. A local build on your laptop is not available to a remote
gateway automatically. If the configured tag is absent, the driver attempts to
pull it; a mismatched name can fail with ImagePullFailed / pull access denied.
Initialize the volume once. It is operator-owned and shared with the OpenShell
gateway, so create it with these commands rather than declaring it in a compose
file — docker compose down -v on a managing stack would delete every Home
Snapshot. Docker named volume (single-tenant local development; 1777 is not
a multi-tenant control):
docker volume create dify-agent-shared
docker run --rm -v dify-agent-shared:/mnt busybox \
sh -c "mkdir -p /mnt/home-snapshots && chmod 1777 /mnt /mnt/home-snapshots"
Kubernetes: provision one ReadWriteMany PVC per tenant trust zone and mount
it read-write at DIFY_AGENT_OPENSHELL_SHARED_MOUNT_PATH through that
driver's driver_config (shape is driver-specific; the Docker example
above is the local-dev analogue). Initialize the volume with an
initContainer or fsGroup so the sandbox UID can write
home-snapshots/ — do not rely on chmod 1777 as isolation.
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: dify-agent-shared-tenant-a
spec:
accessModes: ["ReadWriteMany"]
resources:
requests:
storage: 20Gi
Configure the backend in docker/.env. The full DIFY_AGENT_OPENSHELL_*
variable reference lives in docker/envs/core-services/dify-agent.env.example;
values may also be set in the envs/core-services/dify-agent.env env_file,
but docker/.env is loaded last into agent_backend and overrides it. These
lines are .env file content, not shell commands — values are taken verbatim,
so the driver_config JSON needs no quoting:
# docker/.env
DIFY_AGENT_RUNTIME_BACKEND=openshell
DIFY_AGENT_OPENSHELL_GATEWAY_ENDPOINT=gateway.example.com:17670
DIFY_AGENT_OPENSHELL_BEARER_TOKEN=replace-with-gateway-token
DIFY_AGENT_OPENSHELL_SANDBOX_IMAGE=docker.io/library/dify-agent-runtime:latest
DIFY_AGENT_OPENSHELL_DRIVER_CONFIG={"docker":{"mounts":[{"type":"volume","source":"dify-agent-shared","target":"/mnt/dify-agent-shared","read_only":false}]}}
DIFY_AGENT_OPENSHELL_SHELLCTL_AUTH_TOKEN=replace-with-shellctl-token
Then start the stack normally:
docker compose -f docker/docker-compose.yaml up -d
The Local backend's local_sandbox container and its dedicated SSRF proxy
stay in the stack unused, exactly as they do in an E2B deployment. The standard
Compose file does not build the OpenShell runtime image or provision its
gateway; complete the prerequisites above and the deployment-specific
networking and certificate setup below before starting the stack.
Home Snapshots are directory copies under
<shared mount>/home-snapshots/<tenant-digest>/; snapshot deletion runs a
short-lived maintenance sandbox granted only that tenant's snapshot root
(unlinking the snapshot directory itself requires write on its parent).
Validate the OpenShell deployment
For local development, run the API and Agent Backend from the current checkout.
Copy dify-agent/.example.env to dify-agent/.env, select
DIFY_AGENT_RUNTIME_BACKEND=openshell, and build the runtime image as described
above. The local template targets localhost:17670 and the Homebrew gateway's
mTLS bundle; adjust those paths if your gateway registration has a different
name. Install Agent Backend dependencies with uv sync --extra server from
dify-agent/ before starting the server.
For Docker Compose, use docker/docker-compose.yaml and explicitly configure
your deployment:
- Use API and Agent Backend images containing the OpenShell integration. If your
images predate it, build them from the checkout using
api/Dockerfileanddify-agent/Dockerfile, and update the corresponding Compose service image references. Building the runtime image alone does not update these services. Keep the Agent Backend's glibc ≥ 2.39 base (python:3.13-slim-trixiein its Dockerfile): the bundled OpenShell wheel requires it. - The gateway endpoint must be reachable from the
agent_backendcontainer. For a gateway on the Docker host, usehost.docker.internal:17670, notlocalhost:17670. On Linux Docker, addextra_hosts: ["host.docker.internal:host-gateway"]to that service and ensure the gateway listens on a container-reachable address. Verify that the gateway certificate covers the hostname you use. - When using mTLS, add a read-only bind mount for the gateway client bundle to
agent_backendand setDIFY_AGENT_OPENSHELL_TLS_*_PATHto the paths inside the container. There is no automatic certificate mount. Ensure the service user can traverse the mounted directory and read the files; copy the bundle to a dedicated directory with appropriate permissions rather than mounting the CLI's private gateway directory directly. - Set sandbox-reachable Agent Stub and file-service URLs. Any required service port publishing must be configured explicitly; the Agent Backend control plane should remain private.
The runtime image must be available to the gateway's compute driver, not merely in the Docker daemon used to build the API or Agent Backend images.
Smoke-test through the Agent Backend control plane. Set AGENT_BACKEND_BASE_URL
to its reachable service root (http://localhost:5050 for a local process on
port 5050), and export the matching DIFY_AGENT_API_TOKEN from your deployment.
Use a disposable test tenant and binding; the destroy request deletes the test
sandbox and its workspace.
curl -s -w '\n%{http_code}\n' -X POST "$AGENT_BACKEND_BASE_URL/execution-bindings" \
-H "Authorization: Bearer $DIFY_AGENT_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"tenant_id":"smoke-tenant","agent_id":"smoke-agent","binding_id":"smoke-binding-1","workspace_id":"smoke-ws-1"}'
# → {"binding_ref":"dify-<digest>","workspace_ref":"dify-<digest>"} then 201
curl -s -w '\n%{http_code}\n' -X POST "$AGENT_BACKEND_BASE_URL/execution-bindings/destroy" \
-H "Authorization: Bearer $DIFY_AGENT_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"binding_ref":"<binding_ref>","destroy_workspace":true,"workspace_ref":"<binding_ref>"}'
# → 204
A created ref verifies gateway authentication, sandbox startup from the configured runtime image, and Binding initialization. Home Snapshot operations also require the initialized shared volume described above.
Run the OpenShell integration contract
Run the real OpenShell contract against a reachable gateway. The driver config must mount an initialized shared Home Snapshot volume (see the OpenShell deployment section above); pass the gateway credential through the bearer-token or mTLS-path variables that match your gateway:
cd dify-agent
DIFY_AGENT_TEST_OPENSHELL_GATEWAY_ENDPOINT=gateway.example.com:17670 \
DIFY_AGENT_TEST_OPENSHELL_SANDBOX_IMAGE=docker.io/library/dify-agent-runtime:latest \
DIFY_AGENT_TEST_OPENSHELL_DRIVER_CONFIG='{"docker":{"mounts":[{"type":"volume","source":"dify-agent-shared","target":"/mnt/dify-agent-shared","read_only":false}]}}' \
DIFY_AGENT_TEST_OPENSHELL_BEARER_TOKEN=replace-with-gateway-token \
uv run --extra server pytest --import-mode=importlib \
tests/integration/dify_agent/runtime_backend/test_working_environment.py \
-k openshell -q -rs
The contract creates unique resources and performs explicit cleanup in finally
blocks.