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'slogin-pending.ymlis now flagged as a credential in its three locations (Credentials). - 2026-10-19: HEAD
ac17912against the previously cited519ef8b, from source only. Added shellctl'sjobscolumns and retention, the PostgreSQL model and Human Input records, the/workspacescratch volume and difyctl'slogin-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(profilepostgresql, 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/weaviateand 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 after519ef8b:server,insecure,no_keyringanddevice_code, the code of a device login started withlogin --no-waitand finished withlogin --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 astokens.yml. - Not collected (outside the home):
docker/.envholdsSECRET_KEY(.env.example:33). Per-tenant RSA keys are written toprivkeys/<tenant_id>/private.pemin storage (api/libs/rsa.py:37), which is the OpenDAL fs rootstorageand sovolumes/app/storageby 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-agentPython backend persists anything beyond Redis; it mounts no volume (660-697).