Skip to content

Service modules

Full-configured AdGuard Home for local gateway / router.

  • enable bool Enable local adguardhome service
darkone.service.adguardhome.enable = false;

Local Artifical Intelligence (open-webui + ollama + llms).

  • enable bool Enable local AI service
darkone.service.ai.enable = false;

Audio services: alsa, pulse (not jack for the moment). Automatically adds users to the audio group when enabled.

  • enable bool Enable sound system
darkone.service.audio.enable = false;

Pre-configured dnsmasq for local gateway / router.

  • enable bool Enable local dnsmasq service
darkone.service.dnsmasq.enable = false;

A full-configured LaSuite Docs module.

  • enable bool Enable local docs service
  • s3Host str S3 backend hostname
  • s3Port port S3 backend port
  • s3Bucket str S3 bucket name for document storage
darkone.service.docs = {
enable = false;
s3Host = "127.0.0.1";
s3Port = dnfConfig.network.ports.garage;
s3Bucket = "docs";
};

Element web client for local matrix service.

  • enable bool Enable local element service
darkone.service.element.enable = false;

Fail2ban DNF specific module.

  • enable bool Enable fail2ban with DNF specificities
darkone.service.fail2ban.enable = false;

A full-configured forgejo git forge.

  • enable bool Enable local forgejo service
darkone.service.forgejo.enable = false;

A full-configured local Garage S3 service.

Provides an internal S3-compatible object storage backend accessible only on 127.0.0.1:3900.

  • enable bool Enable local Garage S3 service
  • srvPort port S3 API port exposed on the internal IP
  • s3Region str S3 region name (must match consumer config)
  • capacity str Node storage capacity hint passed to garage layout assign. Used only at first boot for layout initialization; does not act as a hard quota on the underlying filesystem. Supported suffixes: B, KB, MB, GB, TB, PB.
darkone.service.garage = {
enable = false;
srvPort = srvPort;
s3Region = s3Region;
capacity = "500GB";
};

GeneWeb — Powerful Genealogy Service.

  • enable bool Enable local GeneWeb genealogy service
  • enablePasswords bool Enable sops passwords (not recommanded)
darkone.service.geneweb = {
enable = false;
enablePasswords = false;
};

Harmonia: local Nix binary cache server (serves this host’s /nix/store).

Exposes locally built / realised store paths over plain HTTP (port 5000), signed with the deployment-wide binary-cache key. Enable per host from etc/config.yaml (services.harmonia), like any other DNF service.

  • enable bool Enable a local Harmonia Nix binary cache server
darkone.service.harmonia.enable = false;

DNF headscale: headscale-audit. Doc: ../headscale.nix header.


DNF headscale: tailnet pivot DNS. Doc: ../headscale.nix header.


DNF headscale: dnf-tailnet-enroll. Doc: ../headscale.nix header.


A full-configured headscale service for HCS.

The tailnet ACL policy is generated from the topology (zones, hosts, users) by dnfLib.mkHeadscalePolicy, validated by headscale policy check at build time, then reloaded on change (SIGHUP) without restarting headscale.

  • Machines are tagged: tag:hcs, tag:gw-<zone>, tag:admin (host whose groups contains admin).
  • Machines and zone LANs reach each other. Personal devices: DNS and HTTPS on the HCS, HTTPS on zone gateways (every zone for admins, else their zone-* groups). SSH from admin stations and policy.adminDevices.
  • Every addressed zone host is a policy alias (nas -> its LAN IP): any other service opens through policy.extraAcls (nas:445).
  • Personal devices log in through Kanidm (OIDC), for members of the Kanidm tailnet group; their keys expire after nodeExpiry.
darkone.service.headscale.policy.adminDevices.phone-alice = "100.64.0.9";

Sub-modules (headscale/):

  • dns.nix: unbound, the tailnet pivot DNS, and its tailnet-machines view;
  • audit.nix: headscale-audit, live nodes against the declared topology;
  • enroll.nix: dnf-tailnet-enroll, behind just tailnet-enroll.

unbound on the HCS. Internal names reach it through headscale split DNS; it forwards each zone domain to the zone gateway, the rest to Quad9 over TLS.

View tailnet-machines: tagged nodes (HCS aside) get the zone LAN address of the global services a zone serves (git.<domain> on a gateway), everyone else the public one. unbound-tailnet-view lists their tailnet IPs from headscale, declared nowhere.

headscale-audit, every 15 min: live nodes against the declared topology. Metrics for the dnf-headscale-<zone> alerts, details in its journal. It never tags nor deletes.

dnf-tailnet-enroll, root side of just tailnet-enroll: single-use keys tagged from the declared topology; the enrolled node gets its declared name and tags.

  • enable bool Enable headscale DNF service
  • enableGRPC bool Open GRPC TCP port
  • nodeExpiry str Key expiry of personal devices (0: never). Tagged machines never expire.
  • policy.enforce bool Default-deny tailnet ACLs. false allows all traffic, for a migration only.
  • policy.adminDevices attrs Personal devices granted SSH on every machine and zone: name -> tailnet IPv4.
  • policy.exitNodeSources listOf str Policy sources allowed to use the HCS as exit node.
  • policy.extraHosts attrs Extra policy host aliases (name -> CIDR), to use in extraAcls. Declared zone hosts already have one.
  • policy.extraAcls listOf (attrsOf anything) ACL rules appended after the generated ones.
  • shared raw Headscale values shared with the headscale/ sub-modules.
darkone.service.headscale = {
enable = false;
enableGRPC = false;
nodeExpiry = "180d";
policy.enforce = true;
policy.adminDevices = { };
policy.exitNodeSources = [ ];
policy.extraHosts = { };
policy.extraAcls = [ ];
shared = {
inherit
policy
auditSpec
idmUrl
hcsTailnetIpv4
;
};
};

A Home Assistant with some plugins (wip).

  • enable bool Enable home assitant
darkone.service.home-assistant.enable = false;

Dynamically configured homepage dashboard for your local network.

  • enable bool Enable homepage dashboard + httpd + host
  • protect bool Restrict homepage to authenticated Kanidm users (oauth2-proxy forward auth)
  • localServices listOf attrs Services to add in Local Applications section
  • globalServices listOf attrs Full network common & public-accessible services
  • remoteServices listOf attrs Services to add in Remote Applications section
  • bookmarks listOf attrs Replace default bookmarks (links)
  • widgets listOf attrs Replace default widgets
darkone.service.homepage = {
enable = false;
protect = true;
localServices = [ ];
globalServices = [ ];
remoteServices = [ ];
bookmarks = [ ];
widgets = [ ];
};

DNF idm: Kanidm provisioning. Doc: ../idm.nix header.

  • oauth2 attrs OAuth2/OIDC client templates contributed by service modules. Kanidm provisions one client per matching entry in network.services, with clientId = dnfLib.oauth2ClientName.
    • enable bool Whether to provision OAuth2 clients for this template.
    • clientName nullOr str Override the kanidm client name. Defaults to dnfLib.oauth2ClientName.
    • displayName str Human-readable name shown on the kanidm consent screen.
    • imageFile path Application icon. Re-uploaded on every kanidm-provision run.
    • redirectPaths listOf str OAuth2 redirect paths (one per accepted callback URL).
    • landingPath str Auto-connect entry point path on the service.
    • enableLegacyCrypto bool Allow legacy JWT signing algorithms (eg. RS256).
    • allowInsecureClientDisablePkce bool Disable PKCE on the client (only for clients that do not implement it).
    • preferShortUsername nullOr bool Use the short username (no domain) in the preferred_username claim.
    • extra attrs Extra attributes merged into the provisioned client (claimMaps, etc).
darkone.service.idm.provision = {
oauth2.enable = true;
oauth2.clientName = null;
oauth2.displayName = null;
oauth2.imageFile = null;
oauth2.redirectPaths = [ ];
oauth2.landingPath = "/";
oauth2.enableLegacyCrypto = false;
oauth2.allowInsecureClientDisablePkce = false;
oauth2.preferShortUsername = null;
oauth2.extra = { };
};

DNF idm: Kanidm replication. Doc: ../idm.nix header.

  • replication raw Replication facts of this node.
darkone.service.idm.replication.replication = { inherit isMainReplica isReplConsumer; };

Kanidm (identity manager) DNF Service.

Sub-modules (idm/):

  • replication.nix: multi-zone read-only replication, HCS to zone gateways;
  • provision.nix: OAuth2 clients (template registry), groups and persons.

Automatic, derived from where idm is declared:

  • idm on the HCS only, or on a gateway without HCS: one instance;
  • on the HCS and >= 1 zone gateway: the HCS supplies (WriteReplica), each idm gateway consumes.

Two-step bootstrap: just apply generates every replication certificate (gateways stay WriteReplicaNoUI), then just idm-sync-certs + just apply adds the partners and flips the gateways to ReadOnlyReplica.

Every OIDC-capable service module contributes a client template to darkone.service.idm.oauth2; one client is provisioned per (template, instance), merged by clientId across zones. Never on a replication consumer: it mirrors the HCS.

  • enable bool Enable local SSO with Kanidm
darkone.service.idm.enable = false;

Immich (photo management) full-configured service.

  • enable bool Enable local immich service
  • enableMachineLearning bool Enable machine learning features (face recognition, object detection)
  • enableRedis bool Enable Redis for caching (recommended for performance)
darkone.service.immich = {
enable = false;
enableMachineLearning = false;
enableRedis = false;
};

A full-configured jellyfin server.

  • enable bool Enable jellyfin service
darkone.service.jellyfin.enable = false;

A full-configured jitsi-meet service.

  • enable bool Enable local jitsi-meet service
darkone.service.jitsi-meet.enable = false;

Loki + Alloy, http stats with grafana.

  • enable bool Deploys the Loki server + Grafana datasource (colocated with Grafana).
  • isClient bool Deploys Alloy to collect local Caddy access logs.
  • retentionTime str Log retention duration in Loki (30 days by default).
darkone.service.loki = {
enable = config.darkone.service.monitoring.enable;
isClient = config.services.caddy.enable && hasMonitoringHost;
retentionTime = "720h";
};

DNF matrix: mautrix bridges. Doc: ../matrix.nix header.


DNF matrix: authentication delegated to MAS. Doc: ../matrix.nix header.


DNF matrix: MatrixRTC calls (LiveKit). Doc: ../matrix.nix header.


Matrix administration UI (Ketesa, formerly synapse-admin) for the local server.

Static SPA, no backend: the browser talks straight to synapse’s admin API and to MAS’s, so the whole service is a Caddy file_server over a store path.

The DNF service name is deliberately not the package name. The UI is swappable (Element Admin, a later fork) without touching the URL, the DNS record or etc/config.yaml.

Reachable from the LAN and the tailnet only (proxy.isInternal): the bundle holds no secret, but an admin login page has no reason to face the Internet.

  • enable bool Enable the local matrix administration UI
darkone.service.matrix-admin.enable = false;

DNF matrix (synapse) homeserver.

Sub-modules (matrix/):

  • mas.nix: authentication, delegated to Matrix Authentication Service;
  • bridges.nix: mautrix bridges, usable by every local account;
  • rtc.nix: MatrixRTC backend (LiveKit) for Element Call.

Federation is configurable via darkone.service.matrix.federation:

  • enable = false: blocks all federation (empty domain whitelist).
  • enable = true; whitelist = [ ]: open federation with every server.
  • enable = true; whitelist = [ "ami.org" ]: allowlist (inbound + outbound).

Discovery stays locked regardless (rooms absent from remote directories, profiles private over federation), so the network is reachable but not searchable.

Friend self-registration (friendRegistration.enable) opens token-gated local password accounts alongside Kanidm OIDC users. Mint a token on the host with sudo dnf-mas manage issue-user-registration-token.

network.matrix.admins (local parts) is the single declarative source for both administrations, applied at rebuild with no database write:

  • bridges: admin level in every mautrix permissions map;
  • server: policy.data.admin_users, which the bundled MAS policy turns into the urn:mas:admin and urn:synapse:admin:* scopes (MAS admin API, synapse admin API, matrix-admin UI login).

All authentication goes through Matrix Authentication Service, always: Element X, QR login and the /account self-service portal only exist there, and synapse’s own auth is deprecated upstream (MSC3861). Kanidm stays the identity source: MAS is the OIDC client instead of synapse, and synapse only asks MAS to introspect tokens.

Same vhost: MAS owns the root + compat auth endpoints, synapse keeps /_matrix/* and /_synapse/*. Client discovery is automatic (synapse serves auth_metadata itself), no well-known change.

Required sops secrets, all created by just configure-admin-host: mas-encryption-secret, mas-synapse-secret and mas-rsa-private-key.

dnf-mas (root) wraps mas-cli for host-side administration: registration tokens, promote-admin, register-user, syn2mas. MAS runs with DynamicUser and reads its secrets from systemd credentials, absent outside the running unit; the wrapper rebuilds an equivalent credentials directory from the sops files, so it works whether the service is up or down (syn2mas needs it down).

mas-cli only acts, never lists: enable the matrix-admin service for the read side (accounts, devices, sessions), it speaks both admin APIs.

mautrix bridges: WhatsApp, Signal, Telegram, Messenger, Discord. Each user links their own remote account by talking to the bridge bot (@whatsappbot, @signalbot, … then login); sessions are isolated per user. Bridge administrators: cf. “Administrators” above.

Double puppeting: official appservice method (https://docs.mau.fi/bridges/general/double-puppeting.html 🡕). A shared doublepuppet appservice token lets bridges send remote-originated messages as the user’s real matrix account. Required sops secrets: mautrix-doublepuppet-as-token and mautrix-doublepuppet-hs-token (generated by just configure-admin-host).

Every bridge’s appservice as/hs tokens are sops-provided, never auto-generated: the registration file is a pure function of the secrets, so resetting a bridge’s state dir never invalidates the registration synapse loaded (“as_token was not accepted”, registration wipe + synapse restart).

Two stacks coexist, because no single one covers every client:

  • Legacy 1:1 WebRTC, negotiated over synapse and relayed by coturn (darkone.service.turn). The only thing Element Classic speaks.
  • MatrixRTC (matrixRtc.enable): a LiveKit SFU plus its authorization service, for group calls and for Element Call. Element X speaks only this one and reports “call is not supported” (MISSING_MATRIX_RTC_TRANSPORT) without it; Element Web/Desktop embed Element Call too and gain group calls from it.

Both are served from the matrix vhost, next to synapse and MAS: the SFU websocket on /livekit/sfu, the JWT service on /livekit/jwt. Clients discover it from matrix_rtc.transports (synapse, MSC4143) and, as a fallback for older ones, from rtc_foci in the well-known.

Required sops secret: livekit-secret (just configure-admin-host), shared by the SFU and the JWT service.

  • enable bool Enable matrix (synapse) service
  • federation.enable bool Allow server-to-server federation. False blocks all federation.
  • federation.whitelist listOf str Empty = federate with all servers; non-empty = only these domains (inbound + outbound).
  • matrixRtc.enable bool Enable the MatrixRTC backend (LiveKit SFU) for Element Call group calls.
  • friendRegistration.enable bool Allow friends to self-register with an invite token (token-gated).
  • bridges.whatsapp.enable bool Mautrix WhatsApp bridge (login by QR code).
  • bridges.signal.enable bool Mautrix Signal bridge (login by QR code).
  • bridges.telegram.enable bool Mautrix Telegram bridge (login by phone number).
  • bridges.messenger.enable bool Mautrix Facebook Messenger bridge (login by cookies).
  • bridges.discord.enable bool Mautrix Discord bridge (login by QR code, experimental).
  • shared raw Matrix values shared with the matrix/ sub-modules.
darkone.service.matrix = {
enable = false;
federation.enable = false;
federation.whitelist = [ ];
matrixRtc.enable = false;
friendRegistration.enable = false;
bridges.whatsapp.enable = true;
bridges.signal.enable = true;
bridges.telegram.enable = true;
bridges.messenger.enable = true;
bridges.discord.enable = false;
shared = {
inherit
params
masKanidmUlid
livekitJwtUrl
matrixAdmins
;
};
};

Mealie Recipe Management

  • enable bool Enable mealie service
darkone.service.mealie.enable = false;

A full-configured local MinIO S3 service. (wip)

Provides an internal S3-compatible object storage backend accessible only on 127.0.0.1:9000. The console web UI runs on 127.0.0.1:9001 for administration and debugging.

  • enable bool Enable local MinIO S3 service
darkone.service.minio.enable = false;

✨ darkone.service.monitoring.fs-readonly

Section titled “✨ darkone.service.monitoring.fs-readonly”

DNF monitoring: rw mounts the kernel turned read-only. Doc: ../monitoring.nix header.


Supervision module: Grafana dashboards + per-node exporter.

Read-only watch (monitoring/fs-readonly.nix): every node exports dnf_filesystem_forced_readonly, its rw mounts whose filesystem the kernel turned read-only, behind FilesystemReadOnly. Intended ro mounts and binds never appear; the file lives on tmpfs, so it outlives the failing disk.

  • enable bool Enable monitoring with prometheus, grafana and node exporter
  • isNode bool Is a monitoring node
  • kioskTarget str Target (relative, no /) for the automatic redirect to Grafana from the monitoring domain root. Overridden by the Loki module when active to point to a multi-source home dashboard.
darkone.service.monitoring = {
enable = false;
isNode = lib.hasAttrByPath [ "features" "monitoring-node" ] host;
kioskTarget = "d/dnf-monitoring-home/home?kiosk";
};

Nextcloud full-configured service.

  • enable bool Enable local nextcloud service

  • adminUser str Admin username for Nextcloud

  • enableSsoRedirect bool Send users straight to Kanidm instead of showing the Nextcloud login form (user_oidc allow_multiple_user_backends). Saves one click on every login, including the desktop client’s Login Flow v2.

    No effect when Kanidm is absent from the network.

  • plugins listOf (enum (appstoreApps ++ shippedToggleableApps)) Nextcloud apps to enable. Only calendar, contacts and tasks are on by default; Talk (spreed), the dashboard, activity feed, photos, and every other app stay disabled until listed here.

    user_oidc (Kanidm SSO) and notify_push (client push) are always on, regardless of this list.

darkone.service.nextcloud = {
enable = false;
adminUser = "admin";
enableSsoRedirect = true;
plugins = [ ];
};

NFS server + client for home shares.

  • enable bool Enable NFS DNF server (avoid enable manually)
  • serverDomain str NFS Server FQDN
darkone.service.nfs = {
enable = hasServer && (isServer || isClient);
serverDomain = "nfs";
};

Per-zone Nix binary-cache proxy (nginx proxy_cache).

Caches the public Nix cache (cache.nixos.org) on the zone gateway so a fleet-wide deploy pulls each closure from the WAN once, then serves it over the LAN to every other host. Locally built paths are served straight from harmonia (see harmonia.nix); this proxy never fronts harmonia.

  • enable bool the per-zone Nix binary-cache proxy (nginx in front of cache.nixos.org)
  • maxSize str Maximum on-disk cache size; nginx evicts least-recently-used entries beyond it.
  • roaming bool Nomadic host (laptop moved between zones): replace the zone-pinned substituters with the zone-neutral names every zone’s DNS resolves to its own cache services. Outside any DNF zone the names NXDOMAIN instantly and Nix falls back to cache.nixos.org. Declared by the roaming feature in config.yaml; set it by hand for a temporary move.
darkone.service.nix-cache = {
enable = false;
maxSize = "100g";
roaming = (host.features or { }) ? "roaming";
};

OpenClaw assistant of an AI space (gateway + control UI, behind the zone SSO).

One instance per ai-space host, declared like any service:

- hostname: "ia-pro"
profile: "ai-space"
users: ["alice"] # members: Kanidm group `space-ia-pro`
features: ["monitoring-node"] # alerts: space down, gateway unit failed
services: { openclaw: {} }
  • access: Caddy forward auth, members of space-<host> only (proxy.spaceMembersOnly); OpenClaw trusts the zone gateway as proxy (trusted-proxy) and reads the identity from x-auth-request-email;
  • devices: a member’s browser is paired on first visit, without operator admin (granted from the host CLI only);
  • port: reachable from the zone gateway only;
  • host CLI: openclaw (alone: the TUI), re-run as the service user, past trusted-proxy with the gateway password (openclaw-gateway-password, just space-init); admins and cliUsers (default: the owner) only;
  • state: /var/lib/openclaw (provider logins, sessions, workspace, Matrix E2EE keys);
  • owner (owner, default: the only member): its subscription runs the agent, it gets approval requests and proactive messages;
  • Matrix: DM channel with the bot @<host>:<domain>, members only, rooms ignored, one session per member when several, when the network has Matrix and just space-init <host> created the bot (marker var/generated/spaces/<host>.nix, token in the space’s sops file); exec approvals asked to the owner in DM;
  • agent tooling: MCP, skills, Claude Code / Codex / OpenCode and packages of the space (darkone.host.ai-space), shared with the members’ homes;
  • identity (identity: name, emoji, avatar): declared, the config being read-only (openclaw agents set-identity fails);
  • mutableConfig: config writable from the UI and the CLI, Nix keys merged at each start (imposed: access, channels, plugins, store paths; seeded once: model, exec, identity…), owner gets operator.admin;
  • workspace (/var/lib/openclaw/workspace): workspace files (AGENTS.md, SOUL.md…) replaced at each start, others left to the agent;
  • projects: the space’s shared projectsDir is the agent’s working dir (agents.defaults.cwd), projects git-cloned once into it;
  • coding agents (Claude Code, Codex, OpenCode of the space): background workers (coding-agent skill), chat-bound sessions (/acp spawn, acpx);
  • Talk (talk): live voice conversation in the browser, OpenAI GPT-Live, work delegated to the agent (agent-consult), ChatGPT login first;
  • workflows: Lobster pipelines (lobster, .lobster files through workspace), declared automations (automations, kept in sync, others left to the agent and the UI);
  • voice notes: whisper.cpp (transcription); on a microVM guest the hypervisor’s server (large-v3-turbo), local ggml small as fallback; local only on bare metal.
darkone.service.openclaw = {
workspace."AGENTS.md" = ./agents/AGENTS.md;
projects.notes = "git@forge.example.com:alice/notes.git";
};

Models: model + extraModels (/model in the chat), the owner’s subscriptions first, else an API key (apiKeys, sops openclaw/<VAR>); providers adds OpenAI-compatible endpoints (Nous Portal…).

  • enable bool Enable the OpenClaw assistant of an AI space

  • owner nullOr str Member owning the space: its subscription logs the agent in (model), it receives approval requests and proactive messages. Required with several members (users order is not kept).

  • cliUsers listOf str Members running the host CLI (openclaw) as the service user, by a sudo rule: TUI, provider logins, admin. Its ! shell commands run as openclaw too, the space’s tokens within reach.

  • model str Main model, &lt;provider&gt;/&lt;model&gt;: through the owner’s subscription when its provider is in subscriptions, else an API key (apiKeys).

  • extraModels listOf str Other models of the catalog, picked in the chat with /model; same rules as model.

  • subscriptions listOf ( enum [ "anthropic" "openai" ] ) Subscriptions of the owner, used first by the models of their provider:

    • anthropic: Claude Pro/Max through the claude CLI (needs darkone.host.ai-space.enableClaude);
    • openai: ChatGPT Plus/Pro through the codex plugin.

    Other providers, or one left out, go through an API key (apiKeys).

  • apiKeys listOf (strMatching "[A-Z][A-Z0-9_]*") Provider API keys, by variable: read from the space’s sops file (openclaw/&lt;VAR&gt;, just space-sops &lt;host&gt;) into the gateway environment. ANTHROPIC_API_KEY is dropped beside the anthropic subscription, which it would override.

  • providers attrs Providers absent from OpenClaw (Nous Portal…), OpenAI-compatible by default.

    • baseUrl str API endpoint.
    • api str API flavour (OpenClaw models.providers.&lt;id&gt;.api).
    • apiKey nullOr (strMatching "[A-Z][A-Z0-9_]*") Key variable, read like apiKeys.
    • models listOf str Model ids, referenced as &lt;provider&gt;/&lt;id&gt;.
  • mutableConfig bool Config writable from the UI and openclaw config set, the owner being operator admin. Nix keys merged at each start:

    • imposed, rewritten (also on any change of the file): gateway and access, Matrix channel, plugins, MCP servers, store paths;
    • seeded, written when absent, then the UI rules: model, exec, identity, talk, ACP, skill switches.
  • exec enum [ "full" "auto" "ask" "allowlist" "deny" ] Command policy of the agent (OpenClaw tools.exec.mode):

    • full: any command, unapproved (OpenClaw default);
    • ask: allowlisted commands run, any other asks the owner;
    • auto: a model reviews misses, the owner decides when it is unsure;
    • allowlist, deny: allowlisted commands only, none.

    Allowlist: openclaw approvals allowlist add --agent main &lt;path&gt;. With the claude-cli runtime, pipes, redirections and substitutions are refused before any prompt outside full.

  • identity.name nullOr str Name of the assistant, shown in the UI.

  • identity.emoji nullOr str Emoji of the assistant, shown in the UI.

  • identity.avatar nullOr str Avatar image (URL or workspace-relative path).

  • workspace attrs Files of the agent workspace, by relative path: content or file. Replaced at each gateway start; undeclared files belong to the agent.

  • projects attrs Git repositories cloned once into &lt;projectsDir&gt;/&lt;name&gt; (darkone.host.ai-space.projectsDir), never pulled. SSH remotes use the key of openclaw (/var/lib/openclaw/.ssh, created by hand); the server key is accepted on first clone.

  • talk.enable bool Talk: live voice conversation in the browser (OpenAI GPT-Live)

  • talk.model str Realtime model. gpt-live-1-codex: the owner’s ChatGPT login (openclaw models auth login --provider openai), else a Platform key (OPENAI_API_KEY); gpt-live-1, gpt-realtime-2.1 (camera): Platform key only. Pinned: unset, a Platform key wins over ChatGPT.

  • talk.voice str Voice of the model’s route (gpt-live-1-codex: cove, arbor, breeze, ember, juniper, maple, sol, spruce, vale).

  • talk.instructions nullOr lines Instructions appended to OpenClaw’s realtime prompt.

  • lobster bool Lobster workflows: pipelines run by the gateway, with approval steps and resume, plus llm-task (structured LLM step). Workflow files go through workspace ("workflows/&lt;name&gt;.lobster").

  • automations attrs Scheduled agent runs declared in Nix, kept in sync at each start (nix-&lt;name&gt;, edits from the UI overwritten); those of the agent and the UI are left alone.

    • schedule str Cron expression (5 or 6 fields), or every &lt;duration&gt; (every 2h).
    • message lines Prompt of the agent’s run.
    • announce bool Send the final reply to the chat (owner’s last channel).
    • model nullOr str Model override, &lt;provider&gt;/&lt;model&gt; or alias.
    • enable bool False: kept disabled; once so, re-enabled from the UI only (openclaw automations enable &lt;id&gt;).
  • transcription.enable bool Transcribe voice notes (whisper.cpp, CPU).

  • transcription.backend enum [ "hypervisor" "local" ] Where voice notes are transcribed:

    • hypervisor: the hypervisor’s whisper.cpp server (bigger model, more cores), model as fallback;
    • local: on this host, with model.
  • transcription.model path whisper.cpp ggml model of local transcription (local backend, fallback).

  • transcription.language str Spoken language (auto: detected per note).

darkone.service.openclaw = {
enable = false;
owner = "alice";
cliUsers = [ ];
model = "openai/gpt-6-astra";
extraModels = [ ];
subscriptions = [ ];
apiKeys = [ ];
providers.baseUrl = "https://inference-api.nousresearch.com/v1";
providers.api = "openai-completions";
providers.apiKey = "NOUS_API_KEY";
providers.models = [ ];
mutableConfig = false;
exec = "ask";
identity.name = "Jarvis";
identity.emoji = "🦉";
identity.avatar = null;
workspace = { };
projects = { };
talk.enable = false;
talk.model = "gpt-live-1-codex";
talk.voice = "sol";
talk.instructions = "Réponds en français, en phrases courtes.";
lobster = false;
automations.schedule = "0 7 * * 1-5";
automations.message = "";
automations.announce = true;
automations.model = null;
automations.enable = true;
transcription.enable = true;
transcription.backend = if config.darkone.system.microvm.hypervisor != null then "hypervisor" else "local";
transcription.model = pkgs.fetchurl {
url = "https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-small.bin";
sha256 = "0ywqxbziyp2bv72riyjpw4brk9v46d4cfbjfwqvvjrrq0srakqqv";
};
transcription.language = "fr";
};

A full-configured outline wiki.

  • enable bool Enable local outline service
darkone.service.outline.enable = false;

OxiCloud — Fast Sovereign Cloud (file storage, WebDAV, CalDAV & CardDAV).

  • enable bool Enable local OxiCloud service
darkone.service.oxicloud.enable = false;

Postfix SMTP Relay.

  • enable bool Enable Postfix SMTP Relay
darkone.service.postfix.enable = false;

Printers and scanners.

  • enable bool Default useful packages
  • loadAll bool Full printers and scanners
  • enableScanners bool Enable scanners
  • enableHpPrinters bool HP printers only
  • enableManualInstall bool Manual drivers installation
darkone.service.printing = {
enable = false;
loadAll = false;
enableScanners = false;
enableHpPrinters = false;
enableManualInstall = false;
};

Prometheus monitoring server with declarative Alertmanager escalation.

  • enable bool Enable the Prometheus metrics + alerting server

  • retentionTime str Prometheus metrics retention duration

  • alerting.enable bool Enable Alertmanager (defaults on once the Matrix alert rooms are provisioned)

  • alerting.matrix.enable bool Deliver alerts to Matrix rooms via the matrix-alertmanager bot

  • alerting.matrix.userId str Matrix user ID of the alert bot (defaults from network.matrix.bot)

  • alerting.matrix.warningsRoom str Matrix room ID for warnings (defaults from network.matrix.warningsRoom)

  • alerting.matrix.incidentsRoom str Matrix room ID for incidents (defaults from network.matrix.incidentsRoom)

  • alerting.email.enable bool Also send critical alerts by mail (through the local Postfix relay)

  • alerting.email.to str Recipient of critical alert mails

  • alerting.network.enable bool Probe network reachability (gateway/tailnet/DNS) with blackbox_exporter

  • alerting.network.internetProbeTargets listOf str External IPs the zone gateway pings to detect a WAN outage. When all fail, ZoneInternetDown fires and inhibits the (false) down-alerts of hosts reachable only across the WAN (e.g. the HCS). Empty disables it.

  • alerting.network.httpProbeUrls listOf str HTTP(S) URLs probed for liveness (ServiceEndpointDown) and TLS certificate expiry (CertificateExpiringSoon/Critical). Empty by default: list the public web endpoints worth watching. Redirects to an SSO login (3xx) count as up; cert expiry is read from the TLS handshake regardless of HTTP status.

  • alerting.silenceOnRebuild bool Ship the dnf-maintenance flag (textfile collector) so rebuilds inhibit a node’s alerts

  • alerting.thresholds attrs Override resource alert thresholds (see dnf/lib/alerts.nix defaults)

  • alerting.silences listOf submodule Known, accepted alerts that must never notify. Each entry becomes an Alertmanager route to the null receiver: the alert still fires and stays visible in the Prometheus/Alertmanager/Grafana UIs, it just stops reaching Matrix and mail.

    Declared on the host running Prometheus, since that is where the zone’s rules and routes are generated. A fleet-wide consumer module (usr/modules/alerts.nix) is the practical place: it is imported everywhere and only takes effect where Prometheus runs.

    • alert str Alert name to mute (alertname label). Always constrained, so sibling severities stay armed.
    • host nullOr str Restrict the silence to one host. Null mutes the alert fleet-wide.
    • matchers attrs Extra exact label matchers, ANDed with the others (e.g. the mount or device the alert is about).
    • reason str Why this alert is accepted, and what would make the silence removable.
darkone.service.prometheus = {
enable = false;
retentionTime = "30d";
alerting.enable = roomsReady;
alerting.matrix.enable = true;
alerting.matrix.userId = "@alertbot:domain.tld";
alerting.matrix.warningsRoom = "!warnings:domain.tld";
alerting.matrix.incidentsRoom = "!incidents:domain.tld";
alerting.email.enable = true;
alerting.email.to = "admin@${network.domain}";
alerting.network.enable = true;
alerting.network.internetProbeTargets = [ ];
alerting.network.httpProbeUrls = [ ];
alerting.silenceOnRebuild = true;
alerting.thresholds = { };
alerting.silences = [ ];
alerting.silences.alert = "DiskSpaceLow";
alerting.silences.host = "srv-main";
alerting.silences.matchers = { };
alerting.silences.reason = null;
};

DNF restic: backup freshness metrics. Doc: ../restic.nix header.


DNF restic: the REST server. Doc: ../restic.nix header.


Restic backup module: REST server + per-host backup targets.

Example (machine config):

darkone.service.restic = {
enable = true;
targets = [
{ name = "main"; root = "rest:http://restic.my-zone.my-domain.tld:8888";
zone = "ag"; categories = [ "system" "nfs" ]; }
];
};

Repository layout per target: one repo per category, named after it.

<root>/<hostname>/system <- "system" category (/ minus excludes)
<root>/<hostname>/nfs <- "nfs" category (/srv/nfs/<...>)
<root>/<hostname>/medias <- "medias" category (/srv/medias/<...>)

Sub-modules (restic/):

  • server.nix: the REST server, one account per fleet host;
  • metrics.nix: backup freshness metrics for the restic alerts.

One account per fleet host (restic/<hostname>/rest-password), checked against an htpasswd assembled at boot; privateRepos confines each host to its own <hostname>/ prefix.

Monitored nodes only (monitoring-node feature): each job stamps its last success in the textfile collector, and restic-declared lists the declared jobs so a job that never succeeds still ages into a dnf-restic-<zone> alert.

  • enable bool Enable restic backup client
  • enableDryRun bool Dry Run mode
  • enableWaitRemoteFs bool Run backups only after remote-fs.target
  • enableServer bool Enable restic REST server
  • listenAll bool Bind the REST server on 0.0.0.0 (off-site clients)
  • serverDataDir str Local storage root of the REST server (all hosts’ repos)
  • targets listOf submodule Backup destinations for this host (local path or REST URL)
    • name str Target id, used in backup/unit names (<category>-<name>)
    • root str Repository root: local path or REST URL
    • zone str Zone selecting the repo passphrase (restic-password-<zone>)
    • categories listOf ( enum [ "system" "nfs" "medias" ] ) What to back up to this target
  • nfsPaths listOf str NFS dirs (/srv/nfs/<xxx>) included in the ‘nfs’ category
  • mediasPaths listOf str Medias dirs (/srv/medias/<xxx>) included in the ‘medias’ category
  • shared raw Restic values shared with the restic/ sub-modules.
darkone.service.restic = {
enable = false;
enableDryRun = false;
enableWaitRemoteFs = false;
enableServer = false;
listenAll = false;
serverDataDir = "/mnt/backup/restic";
targets = [ ];
targets.name = "main";
targets.root = "rest:http://restic.${zone.domain}:${toString srvPort}";
targets.zone = zone.name;
targets.categories = [ ];
nfsPaths = [ ];
mediasPaths = [ ];
shared = { inherit params; };
};

A full-configured hardened search engine.

  • enable bool Enable local search proxy
darkone.service.searx.enable = false;

SuperTuxKart shared tracks, served over NFS.

  • enable bool Export the shared SuperTuxKart tracks (avoid enable manually)
  • enableClient bool Mount the shared SuperTuxKart tracks of the zone
  • serverName nullOr str Hostname serving the tracks, default is the zone’s stk service host
darkone.service.stk = {
enable = stk.isServer;
enableClient = false;
serverName = stk.server;
};

DNF tailscale: roaming auto-pause. Doc: ../tailscale.nix header.


DNF tailscale: HCS certificate sync. Doc: ../tailscale.nix header.


DNF tailscale: self-heal watchdog. Doc: ../tailscale.nix header.


Tailscale client of the DNF tailnet (headscale on the HCS).

Joins the HCS, zone gateways (isGateway: zone subnet advertised, routes accepted, own resolver kept) and roaming hosts.

Sub-modules (tailscale/):

  • cert-sync.nix: zone gateways pull the HCS Caddy certificates;
  • selfheal.nix: watchdog restarting a disconnected tailscaled;
  • autopause.nix: roaming clients pause tailscale on a zone LAN.

Public certificates are issued on the HCS only; each zone gateway pulls the whole Caddy storage over the tailnet (rsync as nix, elevated to caddy on the HCS side), then publishes it to its own Caddy.

Every minute, checks that the backend runs and the control plane sees the node online; restarts tailscaled after a sustained loss. Exports dnf_tailscale_* metrics for the TailscaleFlapping/TailscaleUnhealthy alerts.

Roaming clients only. On a DNF zone LAN, --accept-dns would hijack resolv.conf (local dnsmasq/AdGuard) and --accept-routes collide with the connected zone subnet: a NetworkManager dispatcher pauses tailscale there and resumes it elsewhere.

  • enable bool Enable tailscale client to connect HCS
  • isGateway bool This tailscale node is a subnet gateway
  • isExitNode bool Configure this client as exit node
  • selfHeal.enable bool Watchdog: detect headscale disconnection and restart tailscaled.
  • autoPauseOnLan.enable bool Roaming client: pause tailscale (down) while plugged into a known DNF zone LAN, resume it elsewhere. Non-gateway NetworkManager clients only.
  • shared raw Tailscale values shared with the tailscale/ sub-modules.
darkone.service.tailscale = {
enable = false;
isGateway = false;
isExitNode = false;
selfHeal.enable = true;
autoPauseOnLan.enable = false;
shared = {
inherit
tsBin
isHcsSubnetGateway
selfHealEnable
selfHealStateDir
autoPauseEnable
autoPauseStateFile
;
};
};

Coturn server (matrix).

Add DNS entries to optimize :

Type,Name,Priority,Pds,Port,Target
SRV,_stun._udp,0,0,3478,turn.mydomain.tld
SRV,_stun._tcp,0,0,3478,turn.mydomain.tld
SRV,_turn._udp,0,0,3478,turn.mydomain.tld
SRV,_turn._tcp,0,0,3478,turn.mydomain.tld
SRV,_turns._tcp,0,0,5349,turn.mydomain.tld
  • enable bool Enable local turn service (visio)
darkone.service.turn.enable = false;

A full-configured vaultwarden server.

  • enable bool Enable local Vaultwarden service
  • enableSmtp bool Enable SMTP to send emails (recommended, needs network.smtp)
darkone.service.vaultwarden = {
enable = false;
enableSmtp = true;
};