Zero-egress / local-first execution¶
Community Edition
Included in the free, open-source Community Edition. See Editions.
vornik is built to run agents that have no direct path to the internet. Each agent runs in its own rootless Podman container; in the zero-egress posture that container has no network device at all. Anything that genuinely needs to leave the host — an LLM completion, an MCP tool call, a broker request — is made by the daemon, not the agent. The agent asks the daemon over a local socket; the daemon is the only process with outbound access.
This makes vornik a natural fit for air-gapped and data-sensitive environments, especially when paired with self-hosted, open-weight models.
Container hardening¶
Every agent container is launched with the same baseline regardless of network mode:
--cap-drop ALL— no Linux capabilities,--security-opt no-new-privileges— no privilege escalation,- a user-namespace mapping, and an optional non-root
--user, - a small set of scoped bind mounts (the step's input, output, and workspace);
the project's
.gitis mounted read-only.
Network modes¶
A role's network access is one of five modes:
| Mode | What the container gets |
|---|---|
none |
no network device — fully isolated |
daemon-only |
no network device, plus a bind-mounted socket to the local daemon |
egress |
its own network namespace with outbound internet |
host |
shares the host network (denied unless explicitly allowed) |
| (unset) | inherits the daemon-wide default |
daemon-only is the zero-egress workhorse. On rootless Podman there is no
network setting that reaches the host daemon while blocking the internet, so
vornik removes the network device entirely and gives the agent a Unix socket to
the daemon instead. The daemon rewrites the agent's API, LLM, and memory
endpoints to that socket, then performs the real outbound calls itself. If no
daemon socket is configured the mount is simply omitted and the container stays
fully isolated — it fails closed, never open.
Choosing modes per role¶
Set the mode on a swarm role, and the daemon-wide default for everything that doesn't override it:
# daemon config
runtime:
default_network: daemon-only # zero-egress by default
# a swarm role that genuinely needs to fetch packages
roles:
- name: builder
runtime:
network: egress
A typical setup runs reasoning/review roles on daemon-only and grants egress
only to the few roles that must install dependencies. An invalid
default_network value fails closed to daemon-only, so a typo can't
silently restore egress.
Better than granting egress: declare the project's dependencies and let
the daemon provision them. A project with a dependencies: manifest gets its
packages materialised into a content-addressed cache OUTSIDE the container and
bind-mounted read-only, so the agent imports them without ever reaching an
index — and the role stays on daemon-only:
# in the project's config
dependencies:
- ecosystem: pip
lockfile: requirements.lock # hash-pinned
Every role with a workspace gets the mount; it is not a grant. That matters
most for the roles that are not coders: a REVIEWER is handed a diff and never
runs an install step, so without this it cannot run the suite it is reviewing
and cannot tell "the tests pass" from "the tests did not run". vornikctl
doctor reports, per project, whether each declared set is materialised.
Two guardrails to know:
daemon-onlyrequiresserver.unix_socketto be set (that's the socket the container connects to). A role whose LLM endpoint is a direct external URL must useegress, notdaemon-only.hostis denied by default. Because host networking defeats the sandbox, it only works when the operator setsVORNIK_ALLOW_NETWORK_HOST=1on the daemon — a deliberate host-level trust decision.
The daemon does not force zero-egress on you: with
runtime.default_networkunset, containers fall back to the historical rootless-egress behaviour. The zero-egress posture is enabled by settingruntime.default_network: daemon-only(which the shipped configuration template does).
Local-first / open-weight models¶
Because the agent's LLM endpoint is plain OpenAI-compatible HTTP, pointing vornik at a self-hosted model — Ollama on a LAN host, vLLM, or any compatible endpoint — is configuration only:
runtime:
agent_llm:
endpoint: "http://192.168.1.20:11434/v1" # e.g. Ollama on the LAN
model: "qwen3.6:35b"
context_size: 262144
Any field left empty under runtime.agent_llm falls back to the daemon's
chat.* settings, so a single chat: section is enough when agents share the
daemon's model. Individual roles can override the model with a model: key. In a
daemon-only deployment these calls are made by the daemon, so the model
endpoint only has to be reachable from the daemon host — never from the agent
containers.
What crosses which boundary¶
In the zero-egress posture:
- Out of the container: nothing over the network (there is no network device). The only channel is the local socket to the daemon, and filesystem access is limited to the step's scoped mounts.
- Out of the daemon: all real egress — LLM completions, MCP tool calls, broker/external API requests — performed by the daemon on the host.
The orchestrator holds the credentials and the network reach; the agent runtime does not.