Skip to content

feat: Nix package and NixOS module for the release binaries - #1

Merged
Riceman2000 merged 8 commits into
mainfrom
cursor/nixos-module-1ded
Oct 11, 2026
Merged

Riceman2000 merged 8 commits into
mainfrom
cursor/nixos-module-1ded

Conversation

@Riceman2000

@Riceman2000 Riceman2000 commented Oct 11, 2026 •

Copy link
Copy Markdown
Owner

Summary

Adds first-class Nix and NixOS support without changing any Bend sources. The package is the official Linux release tarball (v0.11.1, x86_64-linux and aarch64-linux), not a from-source Bend build.

  • packages.default / overlays.default install backplane and backplane-serve with web/ and the bun helpers beside the real binaries, so the hub's exe-dir lookup still finds them. autoPatchelf plus an rpath cover libX11 (NEEDED and dlopen'd) and the dlopen'd libfreetype.so.6 / libfontconfig.so.1. The wrappers set BACKPLANE_NO_UPDATE=1 and expand LD_LIBRARY_PATH in a way that is safe under set -u.
  • nixosModules.default is services.backplane: user, state and cache dirs, listen address and port, firewall, optional tailnet listener ordered after tailscaled and (optionally) tailscaled-autoconnect, extra packages, environmentFile / LoadCredential with a store-path assertion, a KiStack seed, and a PathChanged restart when credential files change.
  • checks cover module assertions, GET /hello as the stand-in for the missing --version flag, and a TCG-friendly nixosTest (accel=kvm:tcg, requiredFeatures.kvm = false).
  • nix fmt runs alejandra. nix/update.sh rewrites nix/sources.nix from GitHub SHA256SUMS and that tag's KiStack.revision().
  • Usage, the module example, and the bump procedure are in nix/README.md. The project README only links there.

The service does not filter the hub's stdout. When the tailnet listener binds, the pairing token line is written to the journal as the binary prints it. With --no-tailscale the binary does not print it.

Verified

nix flake check on x86_64-linux exited 0 (all checks passed) on this revision. Alejandra reports the Nix files comply. QEMU fell back to TCG (failed to initialize kvm / no kvm system feature).

The package check starts backplane --gpu off -- --headless --foreground --no-tailscale and asserts:

  • GET /hello contains 0.11.1 and backplane (CBOR, not a --version flag)
  • the pairing token file exists and is not printed when --no-tailscale is set
  • the wrapper is set -u safe (${LD_LIBRARY_PATH:-})
  • libfreetype.so.6, libfontconfig.so.1, and libX11.so.6 are mapped

The NixOS test boots the module and asserts:

  • backplane.service is up and GET /hello on port 3787 contains 0.11.1
  • the listen address is 127.0.0.1:3787 and not 0.0.0.0, [::], or *
  • state and cache dirs are owned by backplane, and /var/lib/systemd/linger/backplane exists
  • with --no-tailscale, the journal does not contain the pairing token (the binary does not print it; the unit does not filter stdout)
  • LoadCredential and EnvironmentFile reach the process (BACKPLANE_TEST_TOKEN, BACKPLANE_FROM_FILE), along with HOME, XDG_CACHE_HOME, and BACKPLANE_NO_UPDATE=1
  • the secret path unit uses PathChanged and not PathExists
  • with tailscale disabled, the unit does not want tailscaled
  • the seeded KiStack tree is readable by the service user and not writable, owned by root
  • systemctl --failed is empty

Eval checks reject /nix/store secret paths and, with tailscale.enable, put tailscaled.service and tailscaled-autoconnect.service in Wants= and After= but not Requires=.

Not verified

  • aarch64-linux is pinned (hash from the v0.11.1 SHA256SUMS) but not built. nix flake check omitted it: incompatible systems: aarch64-linux.
  • The bun helpers (backplane-browser, backplane-step2glb, backplane-voice) are installed and autoPatchelf'd; they were not executed.
  • Tailscale listen, --tailscale-https, firewall opening, and a real tailscaled-autoconnect.service were checked by evaluation only, not by booting tailscaled. The token line on a tailnet bind was not observed in a journal.
  • Agent CLIs (claude, codex, grok), KiCad, and FreeCAD were not run under the unit. Hardening leaves ProtectHome off and does not set MemoryDenyWriteExecute, ProtectControlGroups, or a syscall filter, which is what those CLIs need, but that is from reading the sources, not from running them.
  • The credential path unit's PathChanged lines are present. A live rewrite of a secret file was not performed.
  • kistack.readOnly makes the cache non-writable after the seed. Upstream's background refresh still runs; its failed writes are ignored. That refresh was not observed against GitHub in the test (the skill file was already present).

Upstream behavior this module works around

Verified against the sources at this commit and the v0.11.1 binaries:

  • No --version. Unknown flags are ignored. A non-numeric --port falls back to 3787. GET /hello is CBOR {"backplane": "<version>", "busy": N}.
  • The window binary NEEDs libX11.so.6 and also dlopens it, plus libfreetype.so.6 and libfontconfig.so.1. backplane-serve NEEDs only libm and libc.
  • backplane --headless detaches unless --foreground is also passed. The service runs backplane-serve, which stays in the foreground.
  • BACKPLANE_NO_UPDATE (any non-empty value) disables self-update. There is no env var for the home directory, the tailscale binary, or the KiStack directory.
  • The pairing token is printed in full on stdout when the tailnet listener binds. The unit does not filter it. With --no-tailscale it is not printed.
  • KiStack lives at $HOME/.cache/backplane/kistack, not under XDG_CACHE_HOME. Startup can write plugin.json into the revision directory, and a background refresh records GitHub HEAD in current, which the next start prefers. kistack.readOnly is a module-side approximation (copy, point current at the pin, drop write permission). Upstream has no read-only or pin switch.
  • Skills.kistack (lexicographic last directory name) is not on the startup path. Main.prepare uses KiStack.current.
  • The tailnet address is decided once at start (tailscale ip -4). Peer discovery retries every two minutes. The binary runs bare tailscale from PATH.
  • Agents inherit the hub environment at spawn and are not restarted when a credential file changes unless the hub itself restarts. systemd-run --user is used only when a user manager answers; otherwise the agent is unconfined. The module enables lingering.

Follow-ups that need Bend changes

  • Retry tailscale ip -4 until it succeeds, and accept a path to the tailscale binary.
  • A KiStack directory override and a switch to keep the pin (no HEAD refresh, no write into the tree), and honor XDG_CACHE_HOME for that cache.
  • Re-read agent credentials at each spawn.
  • Reject unknown flags and a bad --port. Add a real --version.
Open in Web Open in Cursor 

cursoragent and others added 7 commits October 11, 2026 19:16
Package the official Linux tarballs (x86_64 and aarch64), put the
dlopen'd libraries on the rpath, and add services.backplane with a
TCG-friendly nixosTest.

Co-authored-by: Rice <Riceman2000@users.noreply.github.com>
Nix rejects a toFile script that mentions a derivation. Substitute
@libPath@ at install time, and use the top-level libx11 names.

Co-authored-by: Rice <Riceman2000@users.noreply.github.com>
pkgs.nixosTest was renamed. Give the default app a description so
flake check does not warn.

Co-authored-by: Rice <Riceman2000@users.noreply.github.com>
A shallow // replaced services.backplane, so the secret-path cases
never enabled the module and the assertions were not checked.

Co-authored-by: Rice <Riceman2000@users.noreply.github.com>
QEMU still uses accel=kvm:tcg, so a builder with no KVM emulates
instead of being refused the kvm system feature.

Co-authored-by: Rice <Riceman2000@users.noreply.github.com>
The legacy nixosTest wrapper rejects requiredFeatures. The module
interface can turn the kvm feature off so the test emulates.

Co-authored-by: Rice <Riceman2000@users.noreply.github.com>
linger-users.service is Type=oneshot without RemainAfterExit, so a
successful run is inactive and wait_for_unit treats it as a failure.
The test checks the linger file instead.

Co-authored-by: Rice <Riceman2000@users.noreply.github.com>
@Riceman2000
Riceman2000 marked this pull request as ready for review October 11, 2026 19:52
The Nix and NixOS write-up lives in nix/README.md. The project README
only links to it. The service no longer filters the pairing token: the
journal keeps the line the hub prints.

Co-authored-by: Rice <Riceman2000@users.noreply.github.com>
@Riceman2000
Riceman2000 merged commit e66cfdc into main Oct 11, 2026
cursor Bot pushed a commit that referenced this pull request Oct 18, 2026
* feat: add a Nix flake and NixOS module for the release binaries

Package the official Linux tarballs (x86_64 and aarch64), put the
dlopen'd libraries on the rpath, and add services.backplane with a
TCG-friendly nixosTest.

Co-authored-by: Rice <Riceman2000@users.noreply.github.com>

* fix: keep the library path out of builtins.toFile

Nix rejects a toFile script that mentions a derivation. Substitute
@libPath@ at install time, and use the top-level libx11 names.

Co-authored-by: Rice <Riceman2000@users.noreply.github.com>

* fix: call testers.nixosTest on nixpkgs 26.05

pkgs.nixosTest was renamed. Give the default app a description so
flake check does not warn.

Co-authored-by: Rice <Riceman2000@users.noreply.github.com>

* fix: merge eval-check options without dropping enable

A shallow // replaced services.backplane, so the secret-path cases
never enabled the module and the assertions were not checked.

Co-authored-by: Rice <Riceman2000@users.noreply.github.com>

* fix: run the NixOS test without requiring KVM

QEMU still uses accel=kvm:tcg, so a builder with no KVM emulates
instead of being refused the kvm system feature.

Co-authored-by: Rice <Riceman2000@users.noreply.github.com>

* fix: pass requiredFeatures through runNixOSTest

The legacy nixosTest wrapper rejects requiredFeatures. The module
interface can turn the kvm feature off so the test emulates.

Co-authored-by: Rice <Riceman2000@users.noreply.github.com>

* fix: wait for multi-user instead of the linger oneshot

linger-users.service is Type=oneshot without RemainAfterExit, so a
successful run is inactive and wait_for_unit treats it as a failure.
The test checks the linger file instead.

Co-authored-by: Rice <Riceman2000@users.noreply.github.com>

* docs: move Nix notes out of the project README

The Nix and NixOS write-up lives in nix/README.md. The project README
only links to it. The service no longer filters the pairing token: the
journal keeps the line the hub prints.

Co-authored-by: Rice <Riceman2000@users.noreply.github.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Rice <Riceman2000@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants