EchoForge OS (EFOS) is a NixOS-based operating system for everyone who touches Cardano — stakers, dApp developers, stake-pool operators and DePIN fleet runners. Instead of four separate distros, it ships one shared, hardened base and four build targets that differ only in which modules get wired in. Everything is a flake output: rebuild it anywhere, bit-for-bit, from this repository.
| Profile | Audience | Form | In one line |
|---|---|---|---|
echoforge-desktop |
Hobbyist · staking & browsing | GUI light desktop | Read-only desktop with Lace + Ledger; the node is entirely absent — zero overhead |
echoforge-dev |
Smart-contract developer | GUI/CLI sandbox | On-demand local devnet (~200 MB, second-level blocks) + Aiken / GHC / Ogmios / Kupo + Zed |
echoforge-spo |
Power user · protocol & full node | GUI/TUI hybrid | Mithril snapshot sync for preview / preprod / mainnet, TUI monitoring, optional block producer |
echoforge-depin |
DePIN edge node | Headless · x86_64 mini-PC (RPi4 variant) | tmpfs root, ZRAM, hardware watchdog — survives power loss unattended |
git clone https://github.com/EchoForge-Dev/EchoForge-OS.git
cd EchoForge-OS
# 1. Build any profile closure (on NixOS or a Linux builder)
nix build .#nixosConfigurations.echoforge-dev.config.system.build.toplevel
# 2. Activate it on the target machine
sudo nixos-rebuild switch --flake .#echoforge-dev
# 3. depin on a Raspberry Pi 4: flashable SD image
# (.img.zst — root partition auto-expands on first boot)
nix build .#packages.aarch64-linux.depin-sd-imageDisk conventions — partition by label at install time, no code changes needed:
echoforge-root(ext4) +EFOS-BOOT(vfat) for the desktop-class profiles; the depin SSD deployment (echoforge-depin,echoforge-depin-rpi4) expectsechoforge-nix,echoforge-data, optionalechoforge-swapandFIRMWARE; the SD-image form (echoforge-depin-rpi4-sd) needs no manual partitioning at all.
To bump an upstream binary, edit the version in pkgs/cardano/*.nix and run
./scripts/prefetch-hashes.sh to re-verify and fill in the SRI hashes.
The system never boots a heavyweight node. No node unit declares wantedBy, so nothing
enters the boot dependency tree — ef-cli is the only way up, and stop is a full release:
ef-cli node start --mode devnet # local private chain (~200 MB, second-level blocks)
ef-cli node start --mode mithril --network preview # Mithril snapshot → preview/preprod/mainnet
ef-cli node stop # release all node memory & CPU
ef-cli node status [--waybar] # status query / status-bar JSON
ef-cli pool status [--network N] [--json] # producer / KES-period status
ef-cli pool rotate-kes [--network N] # new KES pair + offline re-signing steps
ef-cli version # version / profile / build revision
ef-cli profile switch <desktop|dev|spo|depin> # nixos-rebuild into another profileThe monochrome breathing light on the Waybar tracks it live: gray = OFF, bright white = running.
One socket for everything: /run/echoforge/node.socket; Ogmios at 127.0.0.1:1337, Kupo at 127.0.0.1:1442.
Network configuration (config.json, genesis files, public topology) is taken from the
share/cardano/ directory shipped inside the pinned cardano-node release, not fetched at
runtime from book.world.dev.cardano.org — the published files always track the newest node,
so a pinned binary eventually fails to parse them. Bundled config keeps the two in lockstep
and lets a node start with no network at all. The devnet derives its own config from the
preview template: genesis hashes replaced, checkpoints dropped, every hard fork at epoch 0
(Conway from block one) and k=36 / f=0.5 / epochLength=720 — ~2 s blocks, 12-minute epochs.
- Immutable system —
users.mutableUsers = false; every change goes throughflake.nix+nixos-rebuild. No runtimeapt/pacman, no hand-editing/etc. - Zero secret leakage — sensitive material exists only as sops-nix Age ciphertext,
decrypted to the in-memory mount
/run/secrets/(see secrets/README.md). - Network isolation — the RPC surface (Ogmios, Kupo, and the devnet node) binds to
127.0.0.1, guarded by a build-time assertion. The full node's P2P socket binds0.0.0.0because outbound peering requires it, and reachability is decided by the firewall — which denies all inbound by default, opening SSH (key-only) onspo/depinand the P2P port only when a relay explicitly asks for it.
| App | Integration |
|---|---|
| Lace Wallet | Forced install via Chrome enterprise policy ExtensionInstallForcelist — works out of the box |
| Ledger | hardware.ledger.enable = true + udev rules — plug and use |
| Zed Editor | Preinstalled with the EFDS dark high-contrast theme + Aiken / Haskell / Nix extensions |
| Google Chrome | Homepage & new-tab policy locked to echoforgellc.tech / stickmancharles.com |
Out of the box the spo profile is an observer: it peers outbound over the public topology
and syncs, but no inbound port is opened and the RPC surface stays on loopback. Two commented
role blocks in profiles/spo.nix turn it into a real pool — pick exactly one per machine.
Relay — reachable, public port open, localRoots pointing at your own producer and
sibling relays:
mithril.openFirewall = true;
mithril.topology.localRoots = [ { address = "10.0.0.10"; port = 3001; } ];
mithril.topology.bootstrapPeers = [ { address = "backbone.cardano.iog.io"; port = 3001; } ];Block producer — stays out of allowedTCPPorts, so nothing reaches it from outside;
localRoots lists only your own relays, and the generated topology pins
useLedgerAfterSlot = -1 with bootstrapPeers = null, so the producer never touches public
peer discovery:
mithril.blockProducer.enable = true;
mithril.topology.localRoots = [ { address = "relay-1.example.com"; port = 3001; } ];With blockProducer.enable, the ef-node@ unit appends --shelley-kes-key /
--shelley-vrf-key / --shelley-operational-certificate, sourced from sops
(secrets/secrets.yaml → /run/secrets/pool/). Missing secrets fail the build — you cannot
ship a producer without its keys. Build-time guards also catch openFirewall with a loopback
p2pAddr (assertion), and warn when a producer runs public topology, opens its port, or sets
bootstrap peers.
Two addresses, and mixing them up is expensive.
node.mithril.p2pAddr(default0.0.0.0) is what the node binds for peer-to-peer;node.hostAddr(default127.0.0.1) is where the indexer layer and RPC listen.p2pAddrdefaulting to0.0.0.0does not expose anything — inbound reachability is the firewall's job — but--host-addralso fixes the source address of outbound connections, so binding it to loopback makes every outboundconnectreturnEINVAL. The node then sits at zero peers, frozen at the block the snapshot ended on, whilesyncProgressstill reads 99%+ and everything looks healthy.
Day-to-day operation runs through ef-cli pool. cardano-cli is on PATH on any
node-enabled profile, with CARDANO_NODE_SOCKET_PATH pre-pointed at
/run/echoforge/node.socket (the operator account is in the cardano group; the socket is
0770, the KES/VRF keys stay 0400 and out of reach):
ef-cli pool status # KES period, certificate validity window, on-chain vs local counter
ef-cli pool rotate-kes # new KES pair in tmpfs + the exact offline issue-op-cert commandrotate-kes generates the KES pair into $XDG_RUNTIME_DIR (memory-backed, never on disk) and
computes the current KES period from chain tip and Shelley genesis. It stops there by design:
the cold key lives on your offline signer, so re-issuing the operational certificate and
folding both artifacts back into sops stay manual steps.
Not covered: on-chain pool onboarding — cold/stake key generation, pool registration and
delegation certificates, metadata hosting, and the 500 ADA deposit transaction — is plain
cardano-cli work against the running node.
An immutable, declarative system is an unusual place for an agent trained on Ubuntu habits:
installing a package, editing /etc, enabling a service — the reflexes are all wrong here, and
they fail quietly rather than loudly. AGENTS.md states the rules that actually
apply, the ef-cli surface, and a diagnosis section built from failures observed on hardware —
including several where the surface reading (syncProgress at 99%, a unit reporting active)
stayed reassuring while the node was doing nothing at all. It also marks the line an agent must
not cross alone: anything touching keys, funds, or a producing node.
Claude Code, Cursor and Codex read AGENTS.md automatically.
AGENTS.md # operating rules for an AI agent running this machine
flake.nix # four profile build targets + package outputs
├── profiles/ # desktop / dev / spo / depin (+ depin hardware & disk layers)
├── modules/
│ ├── options.nix # the echoforge.* option namespace
│ ├── common/ # immutable base · security policy · sops-nix secrets
│ ├── cardano/ # dynamic node engine (devnet / mithril / ogmios+kupo units)
│ └── desktop/ # Hyprland + Waybar GUI, Lace / Ledger / Zed / Chrome integration
├── home/ # EFDS visual layer (Waybar breathing light, Zed theme)
├── pkgs/ # ef-cli + static Cardano binary packaging
├── scripts/ # node runners + hash-prefetch tooling
├── secrets/ # sops-nix Age templates (decrypt to /run/secrets)
└── docs/ # engineering figures + brand assets
- aarch64 node trust chain — the
cardano-node-binaarch64 static binaries come from the community Armada Alliance builds (musl static, version-aligned with the official x86_64 11.0.1 binaries). If you require same-origin builds, compile from source via haskell.nix and overridecardano-node-binin the overlay. - Real-hardware validation —
devandspohave been run on an x86_64 VPS (devnet and Mithril-synced preview, Ogmios/Kupo on top). Theechoforge-depin-rpi4-sdimage (sd-image-aarch64 + nixos-hardware RPi4) has not yet been flashed and verified on a physical RPi4. - RAM is the binding constraint for a full node — measured at the tip: ~3.1 GB resident,
~3.3 GB peak during ledger replay, and replay is memory-bound rather than CPU-bound. A
4 GB board cannot hold a node; 8 GB covers preview.
depintherefore targets an x86_64 mini-PC by default, with the RPi4 as a hardware overlay. - Devnet chain is disposable — the devnet genesis is generated by the pinned
cardano-cli, and its parameter set moves with the binary. Bumpingcardano-nodetherefore regenerates the genesis and discards the local chain and its Kupo index (announced on the nextef-cli node start --mode devnet). Nothing you need to keep should live on the devnet.
Code is licensed under Apache-2.0. The EchoForge name, wordmarks and logo
assets under docs/brand/ are not open source — see LICENSE-NOTICE.md.




