Files
dify/dify-agent/docs/dify-agent/user-manual/shell-layer/index.md
T

12 KiB

Shell layer

The dify.shell layer exposes shellctl-backed commands to an Agent. It does not select a backend or own persistent Home, Workspace, or Binding resources. It consumes the operation-scoped RuntimeLease opened by a sibling dify.runtime layer.

Public configuration

from dify_agent.layers.shell import (
    DIFY_SHELL_LAYER_TYPE_ID,
    DifyShellEnvVarConfig,
    DifyShellLayerConfig,
)
from dify_agent.protocol import RunLayerSpec

RunLayerSpec(
    name="shell",
    type=DIFY_SHELL_LAYER_TYPE_ID,
    deps={"execution_context": "execution_context", "runtime": "runtime"},
    config=DifyShellLayerConfig(
        env=[DifyShellEnvVarConfig(name="REPORT_FORMAT", value="markdown")],
        redact_patterns=["private-[A-Za-z0-9]+"],
    ),
)
Config field Meaning
cli_tools CLI bootstrap declarations with install commands and scoped environment metadata.
env Normal environment variables exported to Shell commands.
secret_refs Names of secret environment variables supplied by the backend environment.
redact_patterns Request-level regex patterns removed from Shell output shown to the model.

Endpoints, credentials, Home/Workspace paths, resource refs, timeouts, and network policy are not Shell config. Backend selection is server-private, and the opaque Binding ref belongs to DifyRuntimeLayerConfig.

Runtime requirements

The server constructs one coherent runtime backend profile. Local and E2B implement Home Snapshot and Execution Binding operations. Enterprise implements default-Home Binding creation, acquisition, and coupled destruction, while immutable Home Snapshot operations fail fast; there is no compatibility fallback to the retired Sandbox protocol.

from dify_agent.runtime.compositor_factory import create_default_layer_providers
from dify_agent.runtime_backend.profile import RuntimeBackendSettings, create_runtime_backend_profile

runtime_backend_profile = create_runtime_backend_profile(
    RuntimeBackendSettings(
        runtime_backend="local",
        local_sandbox_endpoint="http://127.0.0.1:5004",
        local_sandbox_auth_token="replace-with-shellctl-token",
    )
)

layer_providers = create_default_layer_providers(
    plugin_daemon_url="http://localhost:5002",
    plugin_daemon_api_key="replace-with-plugin-daemon-key",
    runtime_backend_profile=runtime_backend_profile,
)

Equivalent standalone environment settings are:

DIFY_AGENT_RUNTIME_BACKEND=local
DIFY_AGENT_LOCAL_SANDBOX_ENDPOINT=http://127.0.0.1:5004
DIFY_AGENT_LOCAL_SANDBOX_AUTH_TOKEN=replace-with-shellctl-token
# Optional when shellctl runs directly on a host without /home/dify:
# DIFY_AGENT_LOCAL_SANDBOX_MATERIALIZED_HOME_ROOT=/tmp/dify-agent/materialized-homes
# DIFY_AGENT_LOCAL_SANDBOX_WORKSPACE_ROOT=/tmp/dify-agent/workspaces
# DIFY_AGENT_LOCAL_SANDBOX_HOME_SNAPSHOT_ROOT=/tmp/dify-agent/home-snapshots

The auth token may be empty when shellctl authentication is disabled. Dify-created E2B Sandboxes disable public traffic at creation and access shellctl through the E2B port proxy with its traffic_access_token. Acquiring a RuntimeLease fails if E2B does not provide a non-empty token. This policy applies only to newly created Sandboxes and does not retrofit existing ones.

To let shell jobs call the Agent Stub with dify-agent ..., configure a Sandbox-reachable Agent Stub URL and a unique production secret. Remote deployments normally use a public Agent ingress. Local Compose uses http://agent_backend:5050/agent-stub, reached through the existing agent_ssrf_proxy; this configuration does not change the Compose network topology.

DIFY_AGENT_STUB_API_BASE_URL=https://agent.example.com/agent-stub
DIFY_AGENT_SANDBOX_FILES_BASE_URL=https://dify.example.com
DIFY_AGENT_STUB_UPLOAD_FILE_SIZE_LIMIT=50
DIFY_AGENT_SERVER_SECRET_KEY=replace-with-unpadded-base64url-for-32-random-bytes

HTTP URLs may be either the service root or the explicit /agent-stub root. The server normalizes a service root and rejects unrelated paths. The separate Sandbox file base must point to the Dify API ingress serving /files/*; it is used for CLI upload/download bytes, including Config file and skill pulls. DIFY_AGENT_STUB_UPLOAD_FILE_SIZE_LIMIT belongs to the Agent service, is in MiB, and defaults to 50. The Agent service converts it to the signed upload URL's byte limit. Any ingress or proxy in front of /files/* must allow that file limit plus multipart framing and header overhead; its request-body limit does not need to be numerically identical.

After dify-agent file upload <path> succeeds, the CLI prints JSON such as:

{
  "transfer_method": "tool_file",
  "reference": "dify-file-ref:...",
  "public_download_url": "https://dify.example.com/files/tools/..."
}

reference is the persistent canonical file identity and should be stored in structured output. public_download_url is a short-lived frontend presentation address: it is an absolute URL when Dify API FILES_URL has a public origin, or a same-origin /files/... relative URI when FILES_URL is empty. The CLI does not access this field. The former ambiguous download_url upload-output key is now named public_download_url.

Server-side Binding downloads use dify-agent file upload --no-download-link <path>. This additive mode performs the same streaming ToolFile upload but skips the download-request step and prints only transfer_method plus the canonical reference. The regular file upload command keeps the link-producing behavior shown above.

If the upload succeeds but creating the public URL fails, the command still prints the canonical mapping and exits with an error containing an exact dify-agent file public-url <reference> retry command. Run that command from a new shell tool call to create the public URL without uploading the file again. On success, file public-url prints the complete JSON mapping containing transfer_method, reference, and public_download_url.

The Agent Stub authorization injected into a shell job is valid for five minutes. It does not refresh inside an already-running process. If a command reports that the authorization expired, start a new shell tool call and retry the command (or its reported file public-url recovery command).

The injected JWE is masked as *** in model-facing shell_run, shell_wait, and shell_input observations. Raw shellctl output and files referenced by output_path remain unchanged.

Request graph

A shell-enabled run contains Execution Context, Runtime, and Shell layers:

flowchart LR
    EC["execution_context"] --> SH["shell"]
    RT["runtime<br/>backend_binding_ref"] --> SH

DifyRuntimeLayer acquires the Binding when the run's resource context opens and releases it when that operation exits. DifyShellLayer uses the active lease's commands, Home path, and Workspace path. It performs only best-effort cleanup of shell jobs; the persistent Binding lifecycle remains in Dify API.

Example request

The Binding must already have been resolved or created by Dify API. Its backend ref is opaque to the request builder:

from agenton_collections.layers.plain import PromptLayerConfig
from dify_agent.layers.dify_plugin.configs import DifyPluginLLMLayerConfig
from dify_agent.layers.execution_context import (
    DIFY_EXECUTION_CONTEXT_LAYER_TYPE_ID,
    DifyExecutionContextLayerConfig,
)
from dify_agent.layers.runtime import DIFY_RUNTIME_LAYER_TYPE_ID, DifyRuntimeLayerConfig
from dify_agent.layers.shell import DIFY_SHELL_LAYER_TYPE_ID, DifyShellLayerConfig
from dify_agent.protocol import DIFY_AGENT_MODEL_LAYER_ID
from dify_agent.protocol.schemas import CreateRunRequest, RunComposition, RunLayerSpec


request = CreateRunRequest(
    composition=RunComposition(
        layers=[
            RunLayerSpec(
                name="prompt",
                type="plain.prompt",
                config=PromptLayerConfig(
                    prefix="Use the workspace when local computation helps.",
                    user="Create report.txt containing the current UTC timestamp, then summarize it.",
                ),
            ),
            RunLayerSpec(
                name="execution_context",
                type=DIFY_EXECUTION_CONTEXT_LAYER_TYPE_ID,
                config=DifyExecutionContextLayerConfig(
                    tenant_id="92cca973-2d6f-45e0-906e-0b7eda5f2ccf",
                    user_id="replace-with-user-id",
                    user_from="account",
                    app_id="replace-with-app-id",
                    agent_id="8d542564-159d-4168-985c-dde8d8ff6092",
                    agent_config_version_id="931a4cee-4434-4c1c-8fbd-0a3c7591095d",
                    agent_config_version_kind="snapshot",
                    agent_mode="workflow_run",
                    invoke_from="debugger",
                ),
            ),
            RunLayerSpec(
                name="runtime",
                type=DIFY_RUNTIME_LAYER_TYPE_ID,
                config=DifyRuntimeLayerConfig(backend_binding_ref="opaque-backend-binding-ref"),
            ),
            RunLayerSpec(
                name="shell",
                type=DIFY_SHELL_LAYER_TYPE_ID,
                deps={"execution_context": "execution_context", "runtime": "runtime"},
                config=DifyShellLayerConfig(),
            ),
            RunLayerSpec(
                name=DIFY_AGENT_MODEL_LAYER_ID,
                type="dify.plugin.llm",
                deps={"execution_context": "execution_context"},
                config=DifyPluginLLMLayerConfig(
                    plugin_id="langgenius/gemini",
                    model_provider="google",
                    model="gemini-2.5-flash",
                ),
            ),
        ]
    )
)

The resource part serializes as:

{
  "layers": [
    {
      "name": "runtime",
      "type": "dify.runtime",
      "config": {"backend_binding_ref": "opaque-backend-binding-ref"}
    },
    {
      "name": "shell",
      "type": "dify.shell",
      "deps": {"execution_context": "execution_context", "runtime": "runtime"},
      "config": {}
    }
  ]
}

Paths and persistence

RuntimeLease.layout.home_dir and workspace_dir are absolute paths inside the backend execution namespace. They are not host filesystem paths and are not sent in the run request. Shell commands start in workspace_dir, while HOME is forced to home_dir; ~ therefore resolves to the current Binding's materialized Home. The runner also sets TMPDIR, TMP, and TEMP directly to workspace_dir, making the active Workspace both the default cwd and the temporary working space.

Workspace content persists with the Workspace until Dify API retires and collects it. Releasing a RuntimeLease ends only the current operation. Dify API can later browse the Binding filesystem through Dify Agent's private /execution-bindings/files/list, /execution-bindings/files/read, and /execution-bindings/files/download routes, each of which acquires a fresh lease. Relative paths start in the Workspace, while ~ starts in the Binding's Home. Download uses the installed CLI to stream a ToolFile upload and returns only its canonical reference to Dify API.

On Local, multiple Bindings may share a Workspace while each receives a separate materialized Home. Those directories may be siblings in one shellctl namespace; path isolation restricts a lease to its Home and Workspace. On E2B, one physical E2B resource currently represents both Binding and Workspace, so shared Workspace attachment is unsupported.

See Runtime resources for the ledger and lifecycle contract. The Operations Guide covers Local and E2B validation.