Configuration and environment variables
MemDiver needs no configuration to run locally: every setting below has a safe
default, and the two directories that do not (upload_dir and oracle_dir) are
chosen from the web UI the first time they are needed. This page lists every
MEMDIVER_* environment variable the code reads, where it is read, and what it
changes.
How the web backend reads settings
The FastAPI backend’s settings live in one Pydantic Settings class
(api/config.py) with env_prefix="MEMDIVER_" and env_file=".env". So every
field foo can be set as the environment variable MEMDIVER_FOO, or as a
MEMDIVER_FOO=… line in a .env file in the directory the server is started
from. A real environment variable beats the .env file. List-typed fields
(cors_origins) take a JSON array, e.g.
MEMDIVER_CORS_ORIGINS='["http://localhost:5173"]'.
Settings are read once, when the server starts (get_settings() is cached), so
changing a variable needs a restart. The exceptions are upload_dir and
oracle_dir, which the UI can also set while the server is running (see
Directories chosen in the UI).
Web backend (memdiver web)
Variable |
Default |
Effect |
Source |
|---|---|---|---|
|
|
Interface |
|
|
|
Port |
|
|
unset |
Opt-in token auth. When set, every |
|
|
|
Lets |
|
|
|
Allowed CORS origins (JSON array). A literal |
|
|
unset ( |
Port of the Vite dev server whose origins ( |
|
|
unset |
Extra |
|
|
unset (unrestricted) |
Opt-in read-path sandbox. |
|
|
|
Seconds uvicorn may spend draining open connections on shutdown ( |
|
|
|
Dataset loaded into the tool session at startup. When unset, |
|
|
|
The repo-level |
|
|
|
Where |
|
|
unset (chosen in the UI) |
Root for uploaded captures ( |
|
|
unset (oracles disabled) |
Directory BYO oracles are stored in and executed from. Pins the value; the UI can no longer change it. |
|
|
|
Where pipeline task records ( |
|
|
|
Aggregate size cap for the task artifact store. |
|
|
|
Worker processes in the pipeline task manager’s process pool. |
|
|
|
Aggregate cap for |
|
|
|
Aggregate cap for imported dumps ( |
|
|
|
Directory the prebuilt React bundle is served from, for packaged or relocated installs. Read directly from the environment (not via |
|
Settings also declares max_workers (default min(4, cpu_count - 1), at least
and
db_path(<memdiver home>/project.duckdb), soMEMDIVER_MAX_WORKERSandMEMDIVER_DB_PATHare accepted, but no code currently reads those two fields. Setting them has no effect.
Analysis memory budget
max_rss_mb (MEMDIVER_MAX_RSS_MB, default 0 = no cap) is an upper cap on the
analysis memory budget computed by core/memory_budget.py. The budget is
budget = min(available_memory × 0.7, max_rss_mb, MEMDIVER_MEMORY_BUDGET_MB)
where the smallest configured cap wins and an unset or non-positive cap is
ignored. Available memory is measured with psutil when it is installed
(optional, not a dependency), otherwise /proc/meminfo MemAvailable on Linux
(lowered to the cgroup v2 memory.max headroom inside containers),
vm_stat free + inactive + speculative + purgeable pages on macOS, and as a last
resort total physical RAM (reported as source total, which overestimates what
is free). Operations whose estimated peak exceeds the budget are refused with a
message giving the estimate, the budget and how it was measured (error code
memory_budget_exceeded). Every interface has an explicit override that proceeds
anyway (a warning is logged):
Interface |
Override |
Refusal surfaces as |
|---|---|---|
CLI |
|
message + “Re-run with –force-memory”, non-zero exit |
MCP |
|
tool error with code |
REST |
|
HTTP 507 |
Web UI |
“Run anyway” in the memory-budget dialog (re-sends with |
dialog with estimate vs budget |
Python |
|
|
Prefer narrowing the work first (a region filter such as heap,stack, fewer dumps)
over forcing it.
Variable |
Default |
Effect |
Source |
|---|---|---|---|
|
|
Upper cap (MiB) on the analysis memory budget, read through |
|
|
unset |
Upper cap (MiB) on the analysis memory budget, read directly from the environment (applies to the CLI and library too, not via |
|
Security model
memdiver web is a local single-user workbench. Its defences are layered:
Network. Binds
127.0.0.1by default. A non-loopbackMEMDIVER_HOSTis refused unlessMEMDIVER_API_TOKEN(or the loudMEMDIVER_ALLOW_INSECURE) is set.Browser — always on. A loopback bind does not stop web pages you visit from sending requests to
127.0.0.1. So:Host allow-list (
HostAllowListMiddleware): anyHostheader other than loopback /MEMDIVER_ALLOWED_HOSTSis refused with421, for HTTP and WebSocket alike. This blocks DNS-rebinding attacks.Cross-site guard (
CrossSiteRequestGuard): every state-changing request (POST/PUT/PATCH/DELETE) and every WebSocket handshake is refused with403whenSec-Fetch-Site: cross-siteis sent, or when itsOriginis neither the server’s own origin nor listed inMEMDIVER_CORS_ORIGINS(Origin: nullis always refused). Requests without anOrigin(curl, scripts, the CLI) are not browser-originated and pass.JSON bodies only: a state-changing request whose body has no
Content-Type, ortext/plain/application/x-www-form-urlencoded(the types a cross-site form can send without a CORS preflight), is refused with415.multipart/form-datauploads are allowed and rely on theOrigin/Sec-Fetch-Sitechecks. The Marimo mount (/notebook) is exempt from this one rule, not from the origin checks.Trusted origins are exactly: the request’s own origin (
Originscheme-lesshost:portequal to theHostheader), theMEMDIVER_CORS_ORIGINSlist, and the Vite dev server as bothhttp://localhost:<port>andhttp://127.0.0.1:<port>, where<port>isMEMDIVER_FRONTEND_PORT, elseFRONTEND_PORT, else5173. A loopbackOriginon any other port is refused like a foreign site, so another local web service (a notebook, a second dev server rendering untrusted content) cannot drive MemDiver. The Vite dev proxy (frontend/vite.config.ts) runs withchangeOrigin: false, soHoststays the dev server’s and proxied requests are plain same-origin, on whatever--portVite got. If you serve the frontend from some other origin that talks to the API directly, add it toMEMDIVER_CORS_ORIGINS.
Auth — opt-in.
MEMDIVER_API_TOKEN.Writes — always contained. A client-supplied write location (
output_diron/api/dumps/import-pathand/api/analysis/key-pattern,output_pathon/api/analysis/export-keylog) must resolve inside the upload directory, and client-supplied file names (patternname,.ksymeta.id, session names) must be bare names. The CLI and MCP tools are not restricted this way — they write wherever your shell can.Reads — opt-in. Read paths (dump paths, the file browser, dataset roots) are unrestricted by default because pointing MemDiver at any local dump is its core job. Set
MEMDIVER_ALLOWED_ROOTSto sandbox them. The policy is enforced once per router (api/path_policy.enforce_request_read_paths) on the well-known path fields of the query string and JSON body.
Changing the upload directory or favourites (/api/settings/*) widens what is
writable/readable, so those endpoints are covered by the cross-site guard like
every other mutation.
Engine and other entry points
These are read directly with os.environ (not through Settings), so a .env
file does not apply to them.
Variable |
Default |
Effect |
Source |
|---|---|---|---|
|
|
Wall-clock limit for the subprocess that replays an oracle’s |
|
|
|
|
|
|
|
|
|
|
unset |
Explicit |
|
|
unset |
Interpreter that runs a |
|
|
unset |
Also read by the Marimo notebook app ( |
|
|
|
Locale of the Marimo UI strings ( |
|
Other variables that matter:
XDG_DATA_HOME— when set, the MemDiver home directory is$XDG_DATA_HOME/memdiverinstead of~/.memdiver(core/constants.py:memdiver_home). This is where the user-local prefs file, sessions, the project DB and the default oracle directory live.MEMDIVER_FIXTURE_ROOT— read only by developer scripts underscripts/(ground-truth-run0001.py,precompute_pipeline_fixtures.py) to find a local dataset. The app never reads it.
MEMDIVER_PID=, MEMDIVER_KEY=, MEMDIVER_IV= and MEMDIVER_READY= are not
environment variables: they are lines a target process prints on stdout for the
experiment dump driver (core/dump_driver.py), see
Experiment quickstart.
Directories chosen in the UI
upload_dir and oracle_dir have no silent default. Both resolve by the same
precedence (api/config.py):
MEMDIVER_UPLOAD_DIR/MEMDIVER_ORACLE_DIRfrom the environment or.env;otherwise the value saved in the user-local prefs file
<memdiver home>/config.json(api/user_prefs.py) — never the git-tracked repoconfig.json;otherwise unconfigured (
None).
An empty value (MEMDIVER_ORACLE_DIR="") would otherwise parse as the
server’s working directory; it is treated as unset and logged as a warning.
When the variable is set in the environment or in .env, the setting is
pinned: the save endpoints refuse with 409 (… is pinned by MEMDIVER_…; unset it to configure the directory from the UI), because a value
saved to the prefs file would be shadowed by the variable forever. The status
endpoints report this as env_pinned: true.
Upload directory |
Oracle directory |
|
|---|---|---|
Status |
|
|
Save |
|
|
If unset |
uploads answer 409 |
oracle upload, example-load and |
Default offered |
none — the user picks |
|
GET /api/oracles/status always answers 200 with enabled, path, source
("env", "user_config" or null), env_pinned, default_path and
orphans (oracle files with no metadata; DELETE /api/oracles/orphans removes
them). Enabling the oracle directory validates the path, creates it with mode
0o700, writes the prefs file and points the running registry at it — no
restart needed. See Running oracles from the web UI.
Shutdown and child processes
memdiver web runs uvicorn with timeout_graceful_shutdown = MEMDIVER_WEB_GRACEFUL_TIMEOUT_S. On a normal shutdown the app’s lifespan closes
the task manager (an in-flight run still publishes its terminal event), resets
the oracle registry and closes cached dump readers. A second Ctrl-C makes uvicorn
skip the lifespan shutdown; memdiver web then tears the task manager down
itself so its process pool and multiprocessing manager child do not outlive the
server (cli/dataset.py:_teardown_task_manager).
If the server is killed outright (SIGKILL), those children notice that their
parent is gone and exit on their own: core/process_guard.py installs a
watchdog thread in the pool workers and the manager child that polls the parent
PID every 5 seconds. On the next start, task records left in PENDING or
RUNNING are marked FAILED (api/services/task_manager.py).