Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 17 additions & 5 deletions examples/sensorica-fleet/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@ The worked example behind the `nixos-holochain` modules: five Holochain edgenode

```
examples/sensorica-fleet/
├── flake.nix # inputs, the Holochain line, the five nixosConfigurations, the ISO, the colmena hive
├── happs.nix # the three hApp bundles, fetched by hash
├── flake.nix # inputs, the five nixosConfigurations, the ISO, the colmena hive, the parity check
├── hosts/
│ ├── common.nix # shared by every host: user, SSH keys, desktop, edgenode service
│ ├── edgenode-01/
Expand All @@ -20,7 +19,9 @@ examples/sensorica-fleet/

## Holochain line and hApps

The fleet runs **Holochain 0.6.3** (ADR-015), taken from the module repository's own `holochain-0_6` and `hc-0_6` outputs so it cannot drift onto a different 0.6.3 than the one the VM tests ran against. The line is not a preference: each of the three hApps below has a 0.6 release and none has a 0.7 one. The maintainers re-evaluate this seven days before the workshop date.
The fleet runs **Holochain 0.6.3** (ADR-015) and the three workshop hApps below, all from `nixos-holochain.nixosModules.sensorica-event-node` (#33): the module repository's own event profile, layered onto `holochain-edgenode` by every host in `flake.nix`'s `fleetModules`. It is the same export any other host rehearsing the workshop imports, so this fleet and that host cannot drift apart on the package, the hApp set or the seed; `checks.eventProfileParity` in `flake.nix` fails evaluation if `edgenode-01` ever overrides one of these away from the module's defaults.

The line is not a preference: each of the three hApps below has a 0.6 release and none has a 0.7 one. The maintainers re-evaluate this seven days before the workshop date.

Every node installs all three at boot, on one network seed (`sensorica-workshop-2026`), which is what makes the five machines one DHT per app rather than five isolated ones:

Expand All @@ -30,9 +31,20 @@ Every node installs all three at boot, on one network seed (`sensorica-workshop-
| Kando | `v0.17.5` | `kando.happ` |
| Requests & Offers | `v0.5.2` | `requests_and_offers.webhapp`, unpacked at build time |

Requests & Offers publishes a `.webhapp` and nothing else, and a conductor installs a `.happ`, so `happs.nix` unpacks it in a derivation with `hc web-app unpack` from the same line. Nothing binary is committed: every bundle is `pkgs.fetchurl` by sha256 (ADR-012).
Requests & Offers publishes a `.webhapp` and nothing else, and a conductor installs a `.happ`, so `modules/sensorica-happs.nix` (in the module repository) unpacks it in a derivation with `hc web-app unpack` from the same line. Nothing binary is committed: every bundle is `pkgs.fetchurl` by sha256 (ADR-012).

Three apps compile their wasm one after another on first boot, which on a Holoport is slow, so `installerTimeout` is 900 s (also from the profile). The installer polls for the result rather than trusting any single admin call, so that is a bound on each of its waits (per hApp, the install and then the enable settling), not on one call; the unit has no start timeout of its own.

## Consuming the event profile from another host

Any other flake that rehearses the same workshop node (as Soushi's homelab does) builds from the same export instead of repeating it:

```nix
# inputs: holonix-0_6.follows = "nixos-holochain/holonix-0_6";
modules = [nixos-holochain.nixosModules.holochain-edgenode nixos-holochain.nixosModules.sensorica-event-node];
```

Three apps compile their wasm one after another on first boot, which on a Holoport is slow, so `installerTimeout` is 900 s. The installer polls for the result rather than trusting any single admin call, so that is a bound on each of its waits (per hApp, the install and then the enable settling), not on one call; the unit has no start timeout of its own.
That is the whole profile: package, the three hApps, the network seed, the installer timeout and the two metrics options. A host can still override any single value (a different seed, a longer timeout) with an ordinary assignment, because the profile sets each one with `mkDefault`. Trimming the hApp set is different: `happs` is an attribute set of submodules, so a plain `happs = { hrea = ...; };` is merged with the profile's three apps rather than replacing them. Drop one app with `happs.kando.installed = false;`, or replace the whole set with `happs = lib.mkForce { ... };`. The comment at the top of `modules/sensorica-event-node.nix` explains both.

## Evaluate

Expand Down
4 changes: 4 additions & 0 deletions examples/sensorica-fleet/flake.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

103 changes: 82 additions & 21 deletions examples/sensorica-fleet/flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@
# Holochain toolchain comes from the module repository.
nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05";
holonix.follows = "nixos-holochain/holonix";
# `nixosModules.sensorica-event-node` resolves its 0.6-line packages
# through `inputs.holonix-0_6`, so any consumer of that module needs this
# follow too (the same way `holonix` above is needed for
# `holochain-edgenode`'s own default package).
holonix-0_6.follows = "nixos-holochain/holonix-0_6";
};

outputs = inputs @ {
Expand All @@ -27,35 +32,28 @@

hosts = ["edgenode-01" "edgenode-02" "edgenode-03" "edgenode-04" "edgenode-05"];

# Passed to every host: the module reads inputs.holonix for its packages.
# Passed to every host: the modules read inputs.holonix and
# inputs.holonix-0_6 for their packages.
specialArgs = {inherit inputs;};

# ADR-015: this fleet runs the 0.6 line for September. Not a preference —
# every hApp the workshop installs (hREA, Kando, Requests & Offers) has a
# 0.6 release and none has a 0.7 one. Both packages come from the module
# repository's own outputs, so the fleet adds no input of its own and cannot
# drift onto a different 0.6.3 than the one its VM tests ran against. The
# maintainers re-evaluate this seven days before the workshop date.
fleetLine = {
holochain = nixos-holochain.packages.${system}.holochain-0_6;
hc = nixos-holochain.packages.${system}.hc-0_6;
};

fleetHapps = import ./happs.nix {
inherit pkgs;
inherit (fleetLine) hc;
};

# ADR-015: this fleet runs the 0.6 line for September, from
# `nixosModules.sensorica-event-node` (#33), which every host in
# `fleetModules` below imports. Not a preference — every hApp the
# workshop installs (hREA, Kando, Requests & Offers) has a 0.6 release
# and none has a 0.7 one. The maintainers re-evaluate this seven days
# before the workshop date.
fleetModules = [
nixos-holochain.nixosModules.holochain-edgenode
nixos-holochain.nixosModules.holochain-grafana
# Imported so hosts/common.nix can turn it off in writing rather than by
# omission; see the comment there.
nixos-holochain.nixosModules.holochain-windtunnel
# Both the nixosConfigurations and the colmena hive get these, so a
# `colmena apply` and a `nixos-rebuild switch` install the same bundles
# from the same conductor.
{_module.args = {inherit fleetLine fleetHapps;};}
# The workshop event's package, hApps, seed, installer timeout and
# metrics options — the same export any external host rehearsing the
# event builds from. Both the nixosConfigurations and the colmena hive
# get it, so a `colmena apply` and a `nixos-rebuild switch` install the
# same bundles from the same conductor.
nixos-holochain.nixosModules.sensorica-event-node
];

mkEdgenode = name:
Expand Down Expand Up @@ -89,5 +87,68 @@
devShells.${system}.default = pkgs.mkShell {
buildInputs = with pkgs; [colmena nixos-rebuild alejandra];
};

checks.${system} = {
# Guards #33: fails evaluation if edgenode-01's effective event-profile
# values (package, hApp srcs, network seeds, installer timeout, the two
# metrics enables) diverge from `nixosModules.sensorica-event-node`'s
# own defaults, so a future override in this example that quietly
# re-forks the profile is caught here instead of drifting unnoticed.
#
# `assertion` is forced while the derivation is constructed, which
# happens during evaluation of `.drvPath` — so `nix flake check
# --no-build` (what CI and the review commands run) catches a
# divergence without building anything.
eventProfileParity = let
edge = self.nixosConfigurations.edgenode-01.config.services.holochain-edgenode;

# The module's own defaults, evaluated the way any bare consumer
# gets them: `holochain-edgenode` plus the profile, nothing else
# layered on top. `_module.check = false` skips the rest of the
# NixOS option set, the same way the root flake's own
# `docs/module-options.md` generator does, since nothing here reads
# `config` outside `services.holochain-edgenode`.
moduleDefaults =
(lib.evalModules {
specialArgs = {inherit pkgs inputs;};
modules = [
{_module.check = false;}
nixos-holochain.nixosModules.holochain-edgenode
nixos-holochain.nixosModules.sensorica-event-node
];
})
.config
.services
.holochain-edgenode;

# Fixed-output (fetchurl) and normally-built derivations both give a
# deterministic store path for identical inputs, so `toString`
# compares "the same content" without Nix trying (and failing) to
# structurally compare two derivation attrsets.
happShape = happs:
lib.mapAttrs (_: h: {
src = toString h.src;
inherit (h) installed networkSeed;
})
happs;

diffs = lib.filterAttrs (_: same: !same) {
package = toString edge.package == toString moduleDefaults.package;
hcPackage = toString edge.hcPackage == toString moduleDefaults.hcPackage;
installerTimeout = edge.installerTimeout == moduleDefaults.installerTimeout;
"metricsExporter.enable" = edge.metricsExporter.enable == moduleDefaults.metricsExporter.enable;
"conductorMetrics.enable" = edge.conductorMetrics.enable == moduleDefaults.conductorMetrics.enable;
happs = happShape edge.happs == happShape moduleDefaults.happs;
};
in
pkgs.runCommand "sensorica-event-profile-parity" {
assertion =
if diffs == {}
then "ok"
else throw "edgenode-01 diverges from nixosModules.sensorica-event-node on: ${toString (builtins.attrNames diffs)}";
} ''
echo "$assertion" > $out
'';
};
};
}
50 changes: 6 additions & 44 deletions examples/sensorica-fleet/hosts/common.nix
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
# Shared by every fleet host. Per-machine files set the hostname, import
# their hardware-configuration.nix and add roles (edgenode-01 adds Grafana).
{
pkgs,
fleetLine,
fleetHapps,
...
}: let
{pkgs, ...}: let
# Pasted once, used for the sensorica account and for root below.
operatorKeys = [
# "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA... operator@laptop"
Expand Down Expand Up @@ -61,44 +56,11 @@ in {
enable = true;
openFirewall = true;

# The 0.6 line, chosen in flake.nix; see the comment on `fleetLine` there.
# `hcPackage` must stay on the same line as `package`: the admin subcommand
# is `hc sandbox call --running` below 0.7 and `hc client call --port` from
# 0.7, and the module derives which one to use from `package.version`.
package = fleetLine.holochain;
hcPackage = fleetLine.hc;

# node_exporter for the host series, and the conductor metrics timer for
# the holochain_* series the fleet dashboard is built around. Every node
# runs both; edgenode-01 additionally scrapes and draws them.
metricsExporter.enable = true;
conductorMetrics.enable = true;

# Installed at boot from bundles fetched by hash, never committed
# (ADR-006, ADR-012). One network seed for the whole fleet: it is what makes
# the five nodes one DHT per app rather than five isolated ones, and it
# keeps the workshop off the public networks these apps otherwise share.
happs = {
hrea = {
src = fleetHapps.hrea;
networkSeed = "sensorica-workshop-2026";
};
kando = {
src = fleetHapps.kando;
networkSeed = "sensorica-workshop-2026";
};
requests-and-offers = {
src = fleetHapps.requests-and-offers;
networkSeed = "sensorica-workshop-2026";
};
};

# Three apps compile their wasm one after another on first boot, and a
# Holoport is not a fast machine. The module polls for the outcome rather
# than trusting the admin call's own deadline, so this is how long each
# poll (per hApp: installed, then enabled) may last, not how long any
# single call may take. The unit has no start timeout of its own.
installerTimeout = 900;
# Package, hApps, network seed, installer timeout and the two metrics
# options all come from `nixosModules.sensorica-event-node` (#33), which
# `flake.nix`'s `fleetModules` imports on every host, so the profile is
# defined once instead of repeated here (this file used to reference its
# own `fleetLine` and `fleetHapps` values for exactly these fields).
};

# The Wind Tunnel runner stays off on every fleet node (ADR-008 as amended).
Expand Down
6 changes: 6 additions & 0 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,12 @@
default = {
imports = [./modules bootstrapPackage];
};

# The Sensorica workshop event profile (#33): package, hApps and
# network seed for the workshop, layered on top of
# `holochain-edgenode`. Not part of `default` on purpose; see the
# comment at the top of the module.
sensorica-event-node = ./modules/sensorica-event-node.nix;
};

# The one system in the root flake: a single edgenode with no hApp, so
Expand Down
96 changes: 96 additions & 0 deletions modules/sensorica-event-node.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# modules/sensorica-event-node.nix — the Sensorica workshop event profile
# (#33).
#
# Before this module existed, examples/sensorica-fleet and Soushi's private
# homelab (which rehearses the same workshop node by importing
# examples/sensorica-fleet/happs.nix by path) each repeated the Holochain
# line, the three hApp bundles and the network seed. Two copies drift; this
# is the one export both build from.
#
# Deliberately NOT part of `nixosModules.default` (modules/default.nix): the
# other four modules are generic capabilities, and importing `default` should
# never hand a consumer the Sensorica workshop's opinionated hApp set. Import
# this module explicitly, alongside `holochain-edgenode`, to opt into the
# event profile.
#
# It declares no options of its own: it only supplies `mkDefault` values for
# options `holochain-edgenode.nix` already declares (package, hcPackage,
# happs, installerTimeout, the two metrics enables). A host overrides any one
# of them with an ordinary assignment (default priority beats `mkDefault`).
#
# `happs` is set leaf by leaf (`happs.hrea.src = mkDefault ...;`,
# `happs.hrea.networkSeed = mkDefault ...;`, one pair per hApp) rather than as
# one `happs = mkDefault {...};`: `attrsOf (submodule ...)` merges each hApp's
# fields as independent options, and a whole-set `mkDefault` does not survive
# a host overriding a sibling field (verified empirically: a host setting
# only `happs.hrea.networkSeed` left `happs.hrea.src` "accessed but has no
# value defined", since NixOS's module merge does not push an outer
# `mkDefault` recursively through a raw nested attribute set here). Per-leaf
# `mkDefault` has no such gap: each field stands on its own default,
# independent of what a host does to any other field.
#
# The same merge means a plain reassignment cannot trim the hApp set. `happs`
# is `attrsOf`, so a host writing `happs = { hrea = ...; };` is unioned with
# this profile's keys, and the per-leaf defaults still fill in kando and
# requests-and-offers. To drop one app, set `happs.<app>.installed = false;`
# (the installer then skips it and its bundle is never built). To replace the
# set entirely, write `happs = lib.mkForce { ... };`. `services.holochain-
# edgenode.enable` is left to the host: this module carries the workshop's
# content shape, not whether the service runs at all.
{
lib,
pkgs,
inputs,
...
}: let
system = pkgs.stdenv.hostPlatform.system;

# The 0.6 line (ADR-015): every hApp below has a 0.6 release and none has a
# 0.7 one. Same inputs.holonix-0_6 the root flake's own `holochain-0_6` and
# `hc-0_6` packages come from, so a consumer that follows this repository's
# inputs cannot drift onto a different 0.6.3 than the one this repository's
# own VM tests ran against.
line = {
holochain = inputs.holonix-0_6.packages.${system}.holochain;
hc = inputs.holonix-0_6.packages.${system}.hc;
};

happs = import ./sensorica-happs.nix {
inherit pkgs;
inherit (line) hc;
};

# One seed for the whole event: it is what makes every node one DHT per app
# rather than isolated ones, and keeps the event off the public networks
# these apps otherwise share.
networkSeed = "sensorica-workshop-2026";
in {
config = {
services.holochain-edgenode = {
package = lib.mkDefault line.holochain;
hcPackage = lib.mkDefault line.hc;

metricsExporter.enable = lib.mkDefault true;
conductorMetrics.enable = lib.mkDefault true;

# Three apps compile their wasm one after another on first boot, and a
# Holoport is not a fast machine (see examples/sensorica-fleet/README.md).
installerTimeout = lib.mkDefault 900;

happs = {
hrea = {
src = lib.mkDefault happs.hrea;
networkSeed = lib.mkDefault networkSeed;
};
kando = {
src = lib.mkDefault happs.kando;
networkSeed = lib.mkDefault networkSeed;
};
requests-and-offers = {
src = lib.mkDefault happs.requests-and-offers;
networkSeed = lib.mkDefault networkSeed;
};
};
};
};
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
# The hApps the Sensorica fleet runs in September (ADR-015).
# The hApps the Sensorica workshop event profile runs (ADR-015). Used by
# ./sensorica-event-node.nix, which is what examples/sensorica-fleet and any
# external host rehearsing the same event build from (#33): moved here from
# examples/sensorica-fleet/happs.nix so the bundle set is defined once.
#
# Nothing binary enters git (ADR-012): every bundle is fetched by hash at build
# time. Each `sha256` is `nix-prefetch-url` cross-checked against `sha256sum` of
# the resulting store path, and each URL was confirmed to answer 200 on
# 2026-08-28.
#
# All three are 0.6-line bundles, which is why the fleet pins Holochain 0.6.3:
# neither hREA, Kando nor Requests & Offers has published a 0.7 release.
# All three are 0.6-line bundles, which is why the profile pins Holochain
# 0.6.3: neither hREA, Kando nor Requests & Offers has published a 0.7 release.
{
pkgs,
hc,
Expand Down
Loading