# Shell layer

The shell layer lets a Dify Agent run expose a `shellctl`-backed workspace to the
model. This page is for Dify Agent clients that build `CreateRunRequest`
payloads. It explains how to add the layer to a run composition and how the
server-side runtime must be wired.

The layer type id is `dify.shell`. Its public config is intentionally empty:

```python
from dify_agent.layers.shell import DIFY_SHELL_LAYER_TYPE_ID, DifyShellLayerConfig
from dify_agent.protocol import RunLayerSpec

RunLayerSpec(
    name="shell",
    type=DIFY_SHELL_LAYER_TYPE_ID,
    config=DifyShellLayerConfig(),
)
```

Server-only settings, such as the `shellctl` HTTP entrypoint and auth token, are
injected by the Dify Agent runtime provider. They are not part of
`DifyShellLayerConfig` and should not be submitted by clients in the public run
request.

## Runtime requirements

When a run includes `dify.shell`, the Dify Agent server must construct its layer
providers with a non-empty shellctl entrypoint:

```python
from dify_agent.runtime.compositor_factory import create_default_layer_providers

layer_providers = create_default_layer_providers(
    plugin_daemon_url="http://localhost:5002",
    plugin_daemon_api_key="replace-with-plugin-daemon-key",
    shellctl_entrypoint="http://127.0.0.1:5004",
    shellctl_auth_token="replace-with-shellctl-token",  # optional; defaults to no token
)
```

In the FastAPI server, these values are read from environment-backed
`ServerSettings` fields:

```env
DIFY_AGENT_SHELLCTL_ENTRYPOINT=http://127.0.0.1:5004
DIFY_AGENT_SHELLCTL_AUTH_TOKEN=replace-with-shellctl-token
```

`DIFY_AGENT_SHELLCTL_AUTH_TOKEN` defaults to `None`/empty, which keeps the shell
client on the no-token path. Set it only when the shellctl server is started with
bearer authentication.

To let commands inside user-visible shell jobs call back to the Dify Agent server
with `dify-agent ...`, also enable the Agent Stub:

```env
DIFY_AGENT_STUB_URL=https://agent.example.com/agent-stub
DIFY_AGENT_SERVER_SECRET_KEY=replace-with-base64url-32-byte-secret
```

`DIFY_AGENT_SERVER_SECRET_KEY` must be unpadded base64url text for exactly 32
decoded bytes. One way to generate it is:

```bash
python -c 'import base64, secrets; print(base64.urlsafe_b64encode(secrets.token_bytes(32)).rstrip(b"=").decode())'
```

## Client request shape

A client adds the shell layer as an ordinary composition layer. Basic shell jobs
do not need dependencies. To inject `DIFY_AGENT_STUB_URL` and
`DIFY_AGENT_STUB_AUTH_JWE` into user-visible `shell.run` jobs, declare the
execution-context layer as the shell layer's `execution_context` dependency. A
typical run still also includes:

- a prompt layer that supplies the task;
- an execution-context layer carrying tenant/user context;
- an LLM layer named `llm`.

When clients want the shell workspace and shellctl job records to be removed at
the end of the run, set `on_exit.default` to `delete`.

## Example: CSV analysis run

The following example mirrors a verified run with a real Gemini model and a
temporary shellctl server. The client gives the model a small CSV-shaped dataset
and asks for computed metrics without prescribing the exact shell commands.

### Request

```python {test="skip" lint="skip"}
from agenton.layers import ExitIntent
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.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, LayerExitSignals, RunComposition, RunLayerSpec


request = CreateRunRequest(
    composition=RunComposition(
        layers=[
            RunLayerSpec(
                name="prompt",
                type="plain.prompt",
                config=PromptLayerConfig(
                    prefix="You are a practical data analyst. Give a concise final answer.",
                    user="""Analyze this small sales dataset with pandas. Use any local computation you think is useful.

region,product,units,unit_price
north,widget,12,3.50
north,gadget,5,9.00
south,widget,7,3.50
south,gadget,9,9.00
west,widget,4,3.50
west,gadget,11,9.00

Report the total revenue, the region with the most revenue, total units by
product, and a SHA-256 hash of the CSV content.""",
                ),
            ),
            RunLayerSpec(
                name="shell",
                type=DIFY_SHELL_LAYER_TYPE_ID,
                deps={"execution_context": "execution_context"},
                config=DifyShellLayerConfig(),
            ),
            RunLayerSpec(
                name="execution_context",
                type=DIFY_EXECUTION_CONTEXT_LAYER_TYPE_ID,
                config=DifyExecutionContextLayerConfig(
                    tenant_id="92cca973-2d6f-45e0-906e-0b7eda5f2ccf",
                    invoke_from="workflow_run",
                ),
            ),
            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-3.5-flash",
                    credentials={"google_api_key": "<redacted>"},
                ),
            ),
        ]
    ),
    on_exit=LayerExitSignals(default=ExitIntent.DELETE),
)
```

The same request serialized as JSON has these important layer entries:

```json
{
  "composition": {
    "schema_version": 1,
    "layers": [
      {"name": "prompt", "type": "plain.prompt"},
      {
        "name": "shell",
        "type": "dify.shell",
        "deps": {"execution_context": "execution_context"},
        "config": {}
      },
      {"name": "execution_context", "type": "dify.execution_context"},
      {
        "name": "llm",
        "type": "dify.plugin.llm",
        "deps": {"execution_context": "execution_context"}
      }
    ]
  },
  "on_exit": {"default": "delete", "layers": {}}
}
```

### Final answer

The terminal `run_succeeded` output was:

````markdown
Here is the analysis of the sales dataset:

* **Total Revenue:** **$305.50**
* **Top Region:** **west** with **$113.00**
* **Total Units by Product:** gadget: 25 units, widget: 23 units
* **SHA-256 Hash:** `e86521a0d759037a09b059cb3cb2419f0a3f06e674db8151ccf2f93811dac0b8`
````

## Running shellctl in Docker

Build the shellctl image from the Dify Agent package root:

```bash
docker build -f docker/shellctl/Dockerfile -t dify-agent-shellctl:local .
```

Run it with a bearer token and publish the API on localhost:

```bash
docker run --rm --name dify-agent-shellctl \
  -e SHELLCTL_AUTH_TOKEN=replace-with-a-token \
  -p 127.0.0.1:5004:5004 \
  dify-agent-shellctl:local
```

The image starts `shellctl serve --listen 0.0.0.0:5004` as the non-root
`dify` user and leaves shellctl state/runtime directories at their package
defaults.

## Docker image contents

The provided `docker/shellctl/Dockerfile` installs:

- `tmux`, required by `shellctl` to manage shell jobs;
- `shell-session-manager==2.2.0`, which provides the `shellctl` CLI/server;
- `uv`, so uv shebang scripts with PEP 723 metadata can run inside the shell
  workspace;
- a non-root default user named `dify`.
