Add a Nix flake with a package and a NixOS module #2
No reviewers
Labels
No labels
accessibility
bug
documentation
duplicate
enhancement
good first issue
help wanted
invalid
question
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
backmeupplz/agentboard!2
Loading…
Reference in a new issue
No description provided.
Delete branch "nix-flake"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Adds a flake so agentboard can be run and deployed with Nix.
packages.<system>.default: builds withbuildNpmPackageon Node 24, with dependencies fromimportNpmLock(no hash to maintain). It runsnode --testduring the build and installsagentboard(the server) andkb(the CLI).nix run github:backmeupplz/agentboardstarts the server with its data in./data, likenpm start.nixosModules.default: addsservices.agentboardwithenable,package,host(default127.0.0.1),port(default3000),openFirewallandenvironmentFile(forADMIN_EMAIL,ADMIN_PASSWORDandADMIN_NAME). The default package is built with the host'spkgs. It runs as a hardenedDynamicUsersystemd unit, with data in/var/lib/agentboard, and getsCAP_NET_BIND_SERVICEonly for ports below 1024.overlays.default: addspkgs.agentboard.checks: the package on x86_64-linux, aarch64-linux and aarch64-darwin, plus a NixOS VM test on Linux with two machines. One runs the defaults (port 3000, no capabilities, setup link in the journal). The other binds port 80 with an environment file written at runtime, after first checking that a unit which cannot start stops at the start limit instead of restarting forever.All Nix files live in
nix/exceptflake.nixandflake.lock, and the flake's only input is nixpkgs. Nothing outside Nix changes, apart from the README and aresultline in.gitignore.Tested:
nix buildon aarch64-darwin with the sandbox on, andnix flake check(including the VM test) on x86_64-linux.🤖 Generated with Claude Code
Thanks for adding Nix support! I left some comments inline. The main fixes needed before merge:
packageuse the host'spkgs, notself.packages.__darwinAllowLocalNetworkingso the check passes under the Darwin sandbox.npmDepsHash(importNpmLock) or add a CI job that builds the flake.The rest are smaller module fixes (low ports,
openFirewallwith a localhosthost,pathWithcompatibility,network-online) plus a request to drop thecore-flakeinput. Could you address these?@ -0,0 +1,42 @@{core-flake(which brings flake-utils with it) is only used here to map over three systems. Every consumer now locks and fetches two extra inputs from a personal repo, which adds supply-chain surface and indirection (flake.packagesvsflake.output.checks, an empty devShell).nixpkgs.lib.genAttrs [ "x86_64-linux" "aarch64-linux" "aarch64-darwin" ]does the same in about 10 lines of plain flake.nix. Could you drop the input?@ -0,0 +1,140 @@{The default package comes from
self.packages.${system}, built from the flake's own pinned nixpkgs. On a host system the flake doesn't list (e.g. riscv64-linux), evaluation fails withattribute missing. On supported hosts it pulls a second nixpkgs/nodejs closure into the system and ignores the host's overlays.Please default to the host's pkgs instead:
With
CapabilityBoundingSet = ""andDynamicUser, a port below 1024 can never bind.port = 80throws EACCES,Restart=on-failureloops until the start limit, and the unit ends up failed. Please either grantCAP_NET_BIND_SERVICE(bounding set +AmbientCapabilities) whencfg.port < 1024, or add an assertion.pathWith { inStore = false; }only checks the string prefix, so a secret can still reach the store through an/etcsymlink, andnix/test.nixdoes exactly that. Also,lib.types.pathWithonly exists in nixpkgs ≥ 25.05. The module is evaluated with the consumer'slib, so importing it on 24.11 fails evaluation even whenenvironmentFileis unset.I'd use
lib.types.nullOr lib.types.str(orpath) and spell out in the description that the file shouldn't live in the store.If
hostis a specific LAN/Tailscale IP, the service can start before that address exists.listen()fails with EADDRNOTAVAIL, and with the default 100msRestartSecit hitsStartLimitBurstwithin a second and stays failed. Please usewants/after = [ "network-online.target" ]and/or setRestartSec = 5.@ -0,0 +51,4 @@description = "Port to listen on.";};openFirewall = lib.mkOption {hostdefaults to127.0.0.1, soopenFirewall = trueon its own opens the port while connections are still refused. Please add a warning/assertion foropenFirewall && host == "127.0.0.1", or say in the description thathostmust be changed too.@ -0,0 +1,68 @@{This hard-coded hash has to be bumped by hand on every
package-lock.jsonchange, and no CI job builds the flake (docker.ymlonly runsnpm test). The next dependency bump will silently breaknix run github:backmeupplz/agentboardand NixOS rebuilds.Please either switch to
importNpmLock, which needs no hash:or add a CI job that runs
nix flake check/nix build. A CI job would also catch the fileset (lines 15-26) silently dropping any new fileserver.jsstarts reading, and module regressions the Docker CI won't see.Nit:
npm prune --omit=devdoes nothing because there are no devDependencies, andNODE_ENV(line 48) is never read byserver.js. Both can go.@ -0,0 +40,4 @@doCheck = true;__darwinAllowLocalNetworking = true;checkPhase = ''node --testrunstest.js, which startsserver.json127.0.0.1:0and makes HTTP requests. With the sandbox on (common on nix-darwin/Determinate installs), Darwin blocks loopback networking, so the check fails or hangs. Please add:@ -0,0 +1,50 @@self: {environment.etc."agentboard.env".textwritesADMIN_PASSWORDinto a world-readable/nix/storefile. That's fine for a throwaway VM, but it's the pattern people will copy. Please use something likesystemd.tmpfiles/ a oneshot that writes the file at runtime, or at least leave a comment saying not to do this in real configs.Thanks a lot for this, @purplenoodlesoop! The flake, the module hardening and the VM test are all really appreciated. I did a review pass and have a few findings:
Blocking
nix runcrashes on startup (nix/package.nix, theagentboardwrapper).DATA_DIRisn't set, soserver.jsfalls back topath.join(ROOT, 'data'), which is inside the read-only Nix store.mkdirSyncthen throwsEROFSbefore the server starts listening, so thenix run github:backmeupplz/agentboardusage in the README fails. Suggested fix: add--set-default DATA_DIR datato the wrapper so the path resolves against the cwd.Should fix
2. The unit restarts forever when startup always fails (
nix/module.nix).Restart=on-failurewithRestartSec=5never reaches systemd's default start limit (5 starts per 10s). AnADMIN_PASSWORDunder 8 chars or a mistypedenvironmentFilepath makes the unit restart every 5s indefinitely. SettingStartLimitBurst/StartLimitIntervalSecinunitConfigwould let it end up failed instead.3.
x86_64-darwinis missing from the flake systems (flake.nix), even thoughmeta.platformsincludes it. On Intel Macs,nix run/nix buildfails with "does not provide attribute packages.x86_64-darwin".4.
network-online.targetisn't needed (nix/module.nix). The service binds to loopback by default and makes no outbound connections, so it can delay boot on hosts that have a wait-online service.after = [ "network.target" ]is enough.Nits
5. The
openFirewallloopback warning only matches the literal strings127.0.0.1/::1/localhost. Other127.xaddresses and the expanded form of::1slip past (lib.hasPrefix "127."would cover most of these).6. The list of runtime files now lives in three places: the Dockerfile COPY, the Nix fileset and the install
cp. A new runtime file added only to the Dockerfile would fail in the Nix package at runtime.7.
DATA_DIR = "/var/lib/agentboard"repeats whatStateDirectory = "agentboard"already implies.%S/agentboardwould keep a single source of truth.8. The VM test only covers port 80 with an env file. A second case with defaults (port 3000, empty
CapabilityBoundingSet, no env file, checking the journal for?setup=) would cover the setup most users will run.Happy to help with any of these if useful!
Thanks for the thorough pass! Everything except 3 is addressed in the latest commit. Two notes:
3. x86_64-darwin. Leaving it out on purpose. nixpkgs dropped x86_64-darwin in 26.11, and evaluating
legacyPackages.x86_64-darwinon the pinned nixos-unstable aborts with a pointer to the release notes. Listing it would breaknix flake showandnix flake checkfor everyone.nodejs_24.meta.platformsdoesn't include it either (builtins.elem "x86_64-darwin" nodejs_24.meta.platformsisfalse).4. network-online.target. You're right for the default. The unit now pulls in
network-online.targetonly whenhostis a specific address (a LAN or Tailscale IP), which is the case from the first review wherelisten()fails with EADDRNOTAVAIL. Loopback and wildcard (0.0.0.0,::) binds just useAfter=network.target, so they don't delay boot.