The NixOS module
The flake ships a NixOS module exposing a single services.omw option set that
runs omw as a systemd service. It is the recommended way to run an omw agent (or
several — a single service can run omw run / omw loop, which already
supports multiple agents from one config) on NixOS.
The full option reference is generated from the module by the flake’s
omw-options package; this page explains the design and how to use it.
Enabling the module
{
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-26.05";
omw.url = "github:haras-unicorn/omw";
};
outputs =
{ nixpkgs, omw, ... }:
{
nixosConfigurations.my-machine = nixpkgs.lib.nixosSystem {
modules = [
omw.nixosModules.default
{
services.omw = {
enable = true;
mode = "loop";
settingsFile = "/etc/omw.toml";
environmentFile = "/var/lib/omw/env";
};
}
];
};
};
}
How the service runs
The unit runs omw <mode> --config <file> directly (ExecStart, no shell):
omw loop --config /etc/omw.toml
Two things follow from this:
settingsandsettingsFileare mutually exclusive.settingsis an attribute set rendered to TOML at build time;settingsFileis a path to a TOML file on the system. Choose whichever fits.- Secrets are layered from the environment.
OMW__-prefixed variables (__separator, e.g.OMW__PROVIDERS__OPENAI__API_KEY) override file values at runtime, so API keys never have to live in the Nix store. Set them with theenvironmentoption (systemdEnvironment=) or anenvironmentFile.
mode selects run (every agent once) or loop (keep agents running,
restarting on failure — the default, suited to a service).
extraArgs passes extra CLI flags after the mode; use [ "--watch" ] to
hot-reload agent scripts (a changed brain file restarts its agent while inboxes
and subscriptions survive).
variant selects which package variant runs: default (the
crates.io-equivalent build, no script runtime), rhai (the omw-rhai package,
which compiles the bundled rhai interpreter in) or js (the omw-js package,
which compiles the bundled js interpreter in). Overridable entirely with
package.
Users and state
By default the service runs under a systemd dynamic user (no user /
group). Set user and/or group to pin a specific identity. stateDir
declares a StateDirectory (created under /var/lib, also used as
WorkingDirectory), which is where a filesystem MCP tooling’s workspace would
live and where the service can persist state.
Example with an MCP filesystem tooling rooted at the state directory:
{
services.omw = {
enable = true;
variant = "rhai";
user = "omw";
group = "omw";
stateDir = "omw";
settings.providers.openai = {
kind = "openai";
model = "gpt-4o";
};
settings.tooling.fs = {
kind = "mcp";
transport = "stdio";
command = "npx";
args = [
"-y"
"@modelcontextprotocol/server-filesystem"
"/var/lib/omw"
];
};
settings.runtime.rhai.kind = "rhai";
settings.agents = [
{
name = "alice";
runtime = "rhai";
script = "/var/lib/omw/brain.rhai";
}
];
environment.OMW__PROVIDERS__OPENAI__API_KEY = "…";
};
}
Hardening
services.omw.hardening (default true) applies a systemd sandbox modeled on a
service that spawns nodejs MCP servers and bwrap wrappers:
- Identity/capabilities:
NoNewPrivileges=true(confirmed compatible withbwrap-wrapped MCP servers),RestrictSUIDSGID=true,RestrictRealtime=true,LockPersonality=true,SystemCallArchitectures=native, emptyCapabilityBoundingSet/AmbientCapabilities.LimitMEMLOCK=infinityraises the unit’s ceiling, but inside containers the outerRLIMIT_MEMLOCKstill wins — setOMW__TUNABLES__ALLOW_UNLOCKED_SECRETS=truein the environment there instead (see tunables), since secrets cannotmlock()under the container’s limit. Deliberately omitted:MemoryDenyWriteExecute=truewould break the wasmtime JIT and nodejs MCP servers;SystemCallFilter=is left open for the same reason (wasmtime/node need broad syscalls). - Filesystem/IPC:
ProtectSystem=strict,ProtectHome=true,ProtectClock=true,ProtectKernelModules=true,ProtectControlGroups=true,ProtectProc=invisible,PrivateIPC=true,RemoveIPC=true,UMask=0077.PrivateDevices=falsekeeps/devusable for MCP servers andbwrap. - Network:
RestrictAddressFamilies=AF_UNIX AF_NETLINK AF_INET AF_INET6— provider egress plus local stdio sockets and NSS lookups.
Under ProtectSystem=strict the unit only sees API mounts plus its state
directory. Use services.omw.readOnlyPaths (BindReadOnlyPaths=) when brains
or the config file live elsewhere (e.g. the /etc/brain.rhai used in tests),
and services.omw.readWritePaths (ReadWritePaths=) for filesystem MCP
workspaces outside the state directory. Set services.omw.hardening = false to
drop the sandbox entirely, or override individual keys with
services.omw.serviceConfig, which merges last.