Skip to content

dify

Dify is a self-hosted LLM app platform (Flask API, Celery worker, Next.js web) deployed with Docker Compose, with a dify-agent backend that runs shell jobs through shellctl, a Go sandbox runtime, and difyctl, a CLI client. It came into the catalog from the agent framework sweep for issue #64; see research/frameworks.md for the frameworks that got no catalog line. Its parser is in the backlog (issue 142); the chat and run history is in PostgreSQL, which the analyzer cannot read.

Source and evidence

langgenius/dify, commit 519ef8b8c2d75f597b8a1dfff79ef4b8c5013e3e. Licence: modified Apache 2.0 with multi-tenant and logo conditions (LICENSE:1-5). All claims are from source unless marked inferred.

Dify executes tools and shell commands, inside containers on the host. The Code node posts to dify-sandbox (code_executor.py:19, 81, docker-compose.yaml:510-511), and the agent runs shell jobs in local_sandbox through shellctl (539-548, 677).

Observed

  • No real install: nothing was installed or run.

Source reviews

  • 2026-10-21: langgenius/dify at ac17912, from source only: difyctl's login-pending.yml is now flagged as a credential in its three locations (Credentials).
  • 2026-10-19: HEAD ac17912 against the previously cited 519ef8b, from source only. Added shellctl's jobs columns and retention, the PostgreSQL model and Human Input records, the /workspace scratch volume and difyctl's login-pending.yml (Per-user storage, Credentials); the compose file's line numbers moved, and shellctl's state paths and schema are unchanged.

Collection

Per-user storage

The server has no default path under the home.

Scratch directories

The agent's shell jobs run in the dify_agent_local_sandbox_workspace volume, mounted at /workspace (docker-compose.yaml:564-565), which is the agent's own working directory, not a user project. The DOCKER_VOLUMES line collects it whole, and each job's working directory is the cwd column of shellctl.db. It is outside every home, so the analyzer's home-catalog rule for scratch directories does not apply to it.

Compose bind mounts

The server is deployed with Compose from the clone (cd dify, cd docker, docker compose up -d, README.md:77-82), and almost all state goes to bind mounts under ./volumes beside the compose file, not to named volumes:

  • user files and tenant keys: ./volumes/app/storage (docker-compose.yaml:263);
  • PostgreSQL: ./volumes/db/data (profile postgresql, the default, 425-428, 444, .env.example:299);
  • Redis: ./volumes/redis/data (499);
  • the code sandbox: ./volumes/sandbox/* (532-533);
  • plugins: ./volumes/plugin_daemon (644);
  • vector stores: ./volumes/weaviate and similar.

Chat and run history is in PostgreSQL: tables conversations, messages and message_agent_thoughts (agent tool calls) (models/model.py:1117, 1441, 2431), plus workflow_runs and workflow_node_executions (models/workflow.py:797, 968). messages and conversations carry model_provider and model_id (models/model.py:1146-1148, 1454-1455). Workflow Human Input nodes, the platform's only approval record, leave human_input_forms (with a status) and human_input_form_deliveries, whose delivery_method_type is webapp or email (models/human_input.py:29, 94, human_input_adapter.py:27-29); a form delivered by email is a relay of the run through a messaging platform. These are raw PostgreSQL data files, which the analyzer cannot read without a PostgreSQL server.

The clone location is not fixed. The README's cd dify suggests a clone named dify, often in a home directory, but that is an inference, so the bind mounts have no catalog line. Collecting them by hand is a manual step in collectors/docs/coverage.md.

Named volumes

The compose file declares four (docker-compose.yaml:1325-1328). dify_agent_local_sandbox_home is mounted at /home/dify and dify_agent_local_sandbox_workspace at /workspace (559-561; at ac17912 lines 564-565, and the volume list at 1328-1332); neither is behind a profile. At that commit the local_sandbox service starts shellctl serve --require-auth-token. The container home is /home/dify (dify-agent-runtime/docker/Dockerfile:74-80), and shellctl keeps its state in ~/.local/share/shellctl (internal/server/config.go:132-143): the shellctl.db SQLite database (table jobs) and jobs/<id>/output.log, the agent's shell job output (config.go:114-119, db.go:150, service.go:173). That state is in the home volume. The compose file sets no name:, so Compose prefixes the volume names with the directory name (docker_); the DOCKER_VOLUMES lines are *dify_agent_local_sandbox_home and *dify_agent_local_sandbox_workspace. dify_es01_data and oradata are opt-in vector-store profiles and are left unmatched.

difyctl

The CLI client, described as an internal "edge" build, keeps config.yml, login.yml, and tokens.yml when the OS keyring is unavailable, in ~/.config/difyctl on Linux and macOS and AppData/Roaming/difyctl on Windows, with a LOCALAPPDATA fallback (cli/src/sys/index.ts:58-121, store/manager.ts:8, 37-64, README.md:1-3).

shellctl jobs

shellctl's jobs table has job_id, script_path, output_path, cwd, terminal_cols, terminal_rows, status, session_name, pane_target, exit_code, reason, message, created_at, started_at, ended_at and updated_at (db.go:150-167); the command is the job's jobs/<id>/script file beside output.log (service.go:172-173). No column names a model, a conversation or a Dify user, so a job joins the Dify conversation that asked for it only by time, and nothing in the shellctl volume or difyctl's files records an approval (not found in dify-agent-runtime/internal/server at ac17912, grepped for approv and permission).

A garbage collector runs every 60 seconds and deletes every job that ended more than 300 seconds earlier: its row, and its jobs/<id>/ directory with the script and the output (config.go:24-25, service.go:97, 590-620). An explicit delete does the same at once (service.go:548-567). A collection therefore holds the jobs of the last few minutes before it ran, plus whatever the deleted rows left in the database's free pages; shellctl keeps no other copy of a job.

Credentials

  • difyctl tokens.yml, in all three locations: flagged.
  • difyctl login-pending.yml, added after 519ef8b: server, insecure, no_keyring and device_code, the code of a device login started with login --no-wait and finished with login --resume (auth/pending-login.ts:8-15). The device code is what the CLI polls the server with to receive the token once the user approves the login (auth/device-flow.ts:34-38), so while a login is pending the file can yield a token. It is flagged in the same three locations as tokens.yml.
  • Not collected (outside the home): docker/.env holds SECRET_KEY (.env.example:33). Per-tenant RSA keys are written to privkeys/<tenant_id>/private.pem in storage (api/libs/rsa.py:37), which is the OpenDAL fs root storage and so volumes/app/storage by default (.env.example:159-161). These keys decrypt the provider API keys held in PostgreSQL.

Exclusions

None under the home or in the two named volumes. In the bind mounts the vector-store directories (volumes/weaviate, qdrant, milvus and so on) and volumes/sandbox/dependencies are large.

Project-local files

None in the sense of the project catalog; the deployment's own docker/ directory is the state (Compose bind mounts).

Where the project path is recorded

Nowhere under the home.

Parsing

Dify is backlog: its parser is issue 142, and nothing it collects is parsed yet. A parser would read the shellctl state in the dify_agent_local_sandbox_home volume: the jobs table of shellctl.db (each job's cwd, status, exit_code and times) with each job's jobs/<id>/script and output.log, read whole since the log is terminal output (shellctl jobs). Those hold only the jobs of the last few minutes before the collection, carry no model, conversation or user, and join a Dify conversation only by time. The conversations, messages, agent tool calls, models and Human Input forms are in PostgreSQL, which the analyzer cannot read without a PostgreSQL server (Compose bind mounts).

Confidence

  • High: the named volumes, shellctl's state and the table names (source).
  • Low: the clone location, which is why it has no catalog line.
  • Not determined: whether the dify-agent Python backend persists anything beyond Redis; it mounts no volume (660-697).