fetch() is Python that Datris executes on your server. By default it runs
in-process inside the server. For stronger separation, Datris can instead run each tap in a
dedicated, isolated runner container, cut off from the platform’s credentials and internal
services.
On docker compose / prod, the isolated runner is on by default (
USE_TAP_RUNNER=true).
sbt / IDE runs (no sidecar) stay in-process. Tap behavior is identical either way — the same
fetch() contract, the same environment variables, the same packages. The only difference is
where the code runs and what it can reach. scripts/install.sh and docker/vault-init.sh mint TAP_RUNNER_TOKEN;
the server refuses to start with isolation on and a missing or changeme token.Execution modes
In-process mode runs tap code inside the server. Tap code in this mode
shares the server’s filesystem and network, so it can reach internal services. This is
the
sbt / IDE path (no sidecar). Compose/prod should keep the isolated runner on.
In isolated runner mode (USE_TAP_RUNNER=true), tap code runs in a container that holds
no platform credentials and sits on a network with no route to the platform’s internal
services. Even a misbehaving tap has nothing to read and nowhere to pivot — it can fetch from
its source, install the packages it declares, call back into the Datris API, and return
records. That is all. Enable it when you want defense-in-depth beyond the
always-on protections.
The runner container
The isolated runner is a dedicateddatris-tap-runner sidecar — a small, standalone container
built from its own minimal Python image (not the server image). It is hardened to hold and
grant as little as possible:
- No platform secrets. The image carries no secrets-store credentials, no
.envfile, and no AI/database credentials — there is nothing sensitive inside it to read. - Network-isolated. It runs on a dedicated network with no route to the platform’s internal services (database, object store, secrets store). It can reach the public internet and the Datris API — nothing else internal.
- Non-root, read-only filesystem. Tap code runs as an unprivileged user on a read-only root filesystem, with all Linux capabilities dropped and privilege escalation disabled.
- Ephemeral scratch. The only writable area is a small in-memory scratch space that is wiped after every run, so nothing a tap writes — temp files, installed packages — survives into the next run.
runsc), or a microVM (Firecracker /
Kata).
Datris does not ship or configure these. But because the runner is a standard OCI container,
you can run it under a hardened runtime yourself if your host provides one — for example, with
gVisor installed, run the datris-tap-runner container under runsc (Docker’s
--runtime=runsc, or runtime: runsc on the service). Datris hasn’t validated this path, so
treat it as an advanced, self-managed hardening step.
Always-on protections
These apply in both modes, regardless of the runner setting:- Platform secrets are never placed in a tap’s environment. The credentials the platform
uses for itself (secrets-store token, AI provider keys, database passwords) are stripped
from the tap’s environment. A tap receives only Datris-injected variables (
DATRIS_*), any per-run parameters, and its own configured secret — nothing else. - Extra packages install in isolation. Packages a tap declares are installed into a throwaway virtual environment, never into the system Python. The pre-installed common packages remain available; the per-tap environment is discarded after the run.
- Private-network egress is blocked. HTTP taps and REST endpoint destinations refuse to
connect to loopback, link-local, private, or cloud-metadata addresses, so a tap cannot be
pointed at internal services. If your endpoints legitimately live on an internal network,
set
DATRIS_ALLOW_PRIVATE_EGRESS=truein.env(see the Configuration Reference).
Configuration
Set these on thedatris service (for example in your .env). Defaults work out of the box.
The runner is defined as the
datris-tap-runner service in docker-compose.yml; a normal
docker compose up starts it alongside the server and routes taps there unless you set
USE_TAP_RUNNER=false. To force in-process (for example a no-sidecar sbt run):
What changes for tap authors
Almost nothing. Taps fetch data and return records; the platform writes them to the destination pipeline. Reading a source over the internet, installing declared packages, using the tap’s own secret, and calling the Datris API viaDATRIS_PLATFORM_HOST / DATRIS_PLATFORM_PORT
all work the same in both modes.
The one thing to know: in isolated runner mode a tap cannot open a direct connection to
the platform’s internal database / object store by hostname — those live on a network the
runner can’t reach. This is intentional, and it’s not how taps are meant to work anyway: a tap
returns records and lets the pipeline handle the destination. If you have a legacy tap that
connected directly to an internal service, run it in-process or rework it to return records.
Reading platform data from a tap
A tap can read data already in Datris — for example astock_tickers table — to drive its
fetch. Do this through the query API, using the injected platform host and database name.
The same code works in both execution modes, because DATRIS_PLATFORM_HOST is set correctly
for each:
DATRIS_PLATFORM_TOKEN) and the tap wrapper attaches it to requests / urllib calls aimed at DATRIS_PLATFORM_HOST automatically — the script above works unchanged in both modes and with keys on or off. The token is read-only and scoped to the run; the audit log records the callback as the tap.
Use /api/v1/query/mongodb (or /api/v1/query/natural) the same way for Mongo-backed data.
In the isolated runner, a tap cannot instead open a direct database connection (e.g. to
postgres:5432) — internal services aren’t reachable from the runner’s network. Querying
through the API is the supported path and works regardless of mode.
When to use which
- Isolated runner (compose/prod default) — tap code stays separated from platform credentials and internal services. Isolated taps cannot open a direct connection to internal DB / MinIO / Vault; they return records and the pipeline writes destinations.
- In-process (
sbt/ IDE, orUSE_TAP_RUNNER=false) — tapfetch()runs inside the server JVM. Fine for local development without the sidecar, or a legacy tap that still opens a direct internal connection. The server logs a loud warning. Do not treat a laptop compose install as in-process — compose isolates by default.
