nodeterm gets a canvas on ishtar and a server on minerva #124

Merged
lament merged 8 commits from nodeterm into main 2026-08-17 11:06:28 -05:00
Owner

nodeterm.dev, on trial - the desktop AppImage on ishtar, the headless Server Edition at canvas.lament.gay. Rides along with a few unrelated commits that were already sitting on the working copy.

  • static/packages/nodeterm.nix - the released x64 AppImage via appimageTools.wrapType2, installed through the standalone desktop HM

    • tmux goes in extraPkgs: it is the session backend (the main process drives it directly rather than going through a login shell), and upstream's linux build just assumes a system copy where the mac build ships its own
    • upstream names everything node-terminal on disk but presents as nodeterm, so the .desktop entry and icon get renamed to match the wrapper or the launcher never resolves
    • callPackage'd in desktop.nix rather than wired through the packages.nix overlay - it's BUSL-1.1 and perSystem's pkgs carries no nixpkgs config, so a flake.packages entry throws for every host
  • static/packages/nodeterm-server.nix + services/web/nodeterm.nix - Server Edition on minerva, VPN-only

    • the whole build hinges on one trap, lifted from upstream's own Dockerfile: postinstall runs electron-rebuild, which compiles node-pty against Electron's ABI - fatal under plain node. So --ignore-scripts throughout, then one npm rebuild node-pty in postBuild. smart-whisper is deliberately left unbuilt (upstream doesn't either - lazy require on a voice path we don't use)
    • dedicated nodeterm user, because this service is a shell - it hands out live terminals and runs coding agents, and that blast radius stays off lament
      • it needs a real writable $HOME, not isSystemUser's /var/empty: every agent CLI's credentials, nodeterm's own context-link instructions, and direnv's allow-state are all $HOME-rooted. Shared with the data dir on purpose so one /persist entry keeps both the canvas and the agent logins
    • password auth seeded once from sops, no OIDC - it's the one thing on the fleet outside the identity tower, so directory.services.canvas gets no public and the only listener is minerva's WG address
    • programs.direnv on minerva: a terminal node opened in a repo should pick up that repo's devshell. develop.nix would have dragged docker/libvirtd/jetbrains onto a server, so just the one program
  • claude-code moves off the fleet-wide shared HM module

    • desktop-scoped in users/lament.nix (wanted in any scratch dir on ishtar, never on a server) and pinned in the pantheon devshell alongside codex, so repo work gets a flake.lock-versioned copy either way
    • top-level.nix re-instantiates perSystem's pkgs with allowUnfree for the devshell - deliberately with no overlays, so every existing packages.* keeps its derivation hash byte-identical
  • drive-bys, unrelated to the above:

    • xrdp module, enabled on brigid (openFirewall = false - internal is already trusted)
    • niri Print/Mod+Shift+S -> grim | slurp | satty for annotated region screenshots, replacing the bare screenshot action
    • vscode userSettings commented out wholesale - settings go imperative
    • devenv added to both HM profiles
    • .nodeterm/ gitignored: viewport coords and a rev that bumps on every pan/zoom, written by both the ishtar app and the minerva server - a conflict machine

left for later: direnv allow + agent logins still need doing by hand from one of minerva's own terminal nodes. Proxy-asserted identity is possible (NODETERM_TRUST_PROXY_HEADER) but would need oauth2-proxy in front, parked while this is still a trial.

nodeterm.dev, on trial - the desktop AppImage on `ishtar`, the headless Server Edition at `canvas.lament.gay`. Rides along with a few unrelated commits that were already sitting on the working copy. - `static/packages/nodeterm.nix` - the released x64 AppImage via `appimageTools.wrapType2`, installed through the standalone desktop HM - `tmux` goes in `extraPkgs`: it *is* the session backend (the main process drives it directly rather than going through a login shell), and upstream's linux build just assumes a system copy where the mac build ships its own - upstream names everything `node-terminal` on disk but presents as `nodeterm`, so the `.desktop` entry and icon get renamed to match the wrapper or the launcher never resolves - `callPackage`'d in `desktop.nix` rather than wired through the `packages.nix` overlay - it's BUSL-1.1 and `perSystem`'s `pkgs` carries no nixpkgs config, so a `flake.packages` entry throws for every host - `static/packages/nodeterm-server.nix` + `services/web/nodeterm.nix` - Server Edition on `minerva`, VPN-only - the whole build hinges on one trap, lifted from upstream's own Dockerfile: `postinstall` runs `electron-rebuild`, which compiles `node-pty` against Electron's ABI - fatal under plain `node`. So `--ignore-scripts` throughout, then one `npm rebuild node-pty` in `postBuild`. `smart-whisper` is deliberately left unbuilt (upstream doesn't either - lazy require on a voice path we don't use) - dedicated `nodeterm` user, because this service *is* a shell - it hands out live terminals and runs coding agents, and that blast radius stays off `lament` - it needs a real writable `$HOME`, not `isSystemUser`'s `/var/empty`: every agent CLI's credentials, nodeterm's own context-link instructions, and direnv's allow-state are all `$HOME`-rooted. Shared with the data dir on purpose so one `/persist` entry keeps both the canvas and the agent logins - password auth seeded once from `sops`, no OIDC - it's the one thing on the fleet outside the identity tower, so `directory.services.canvas` gets no `public` and the only listener is minerva's WG address - `programs.direnv` on `minerva`: a terminal node opened in a repo should pick up that repo's devshell. `develop.nix` would have dragged docker/libvirtd/jetbrains onto a server, so just the one program - `claude-code` moves off the fleet-wide shared HM module - desktop-scoped in `users/lament.nix` (wanted in any scratch dir on `ishtar`, never on a server) *and* pinned in the pantheon devshell alongside `codex`, so repo work gets a `flake.lock`-versioned copy either way - `top-level.nix` re-instantiates `perSystem`'s `pkgs` with `allowUnfree` for the devshell - deliberately with no overlays, so every existing `packages.*` keeps its derivation hash byte-identical - drive-bys, unrelated to the above: - `xrdp` module, enabled on `brigid` (`openFirewall = false` - `internal` is already trusted) - niri `Print`/`Mod+Shift+S` -> `grim | slurp | satty` for annotated region screenshots, replacing the bare `screenshot` action - vscode `userSettings` commented out wholesale - settings go imperative - `devenv` added to both HM profiles - `.nodeterm/` gitignored: viewport coords and a `rev` that bumps on every pan/zoom, written by both the ishtar app and the minerva server - a conflict machine left for later: `direnv allow` + agent logins still need doing by hand from one of minerva's own terminal nodes. Proxy-asserted identity is possible (`NODETERM_TRUST_PROXY_HEADER`) but would need oauth2-proxy in front, parked while this is still a trial.
appimage wrap rather than a source build: upstream ships an x64 appimage and
the electron tree needs electron-rebuild + a whisper native module to build
from source. tmux rides in extraPkgs - the mac build bundles its own, the
linux one assumes a system copy the fleet does not install.

callPackage'd in the desktop HM rather than the packages.nix overlay:
BUSL-1.1 is unfree and perSystem's pkgs sets no allowUnfree.
it rode `homeShell` (a fleet-wide sharedModule), so every server carried a
package none of them run. `lamentDesktop` keeps it everywhere on ishtar; the
devshell pins its own copy beside codex for repo work.

perSystem gets a bare `legacyPackages` with no nixpkgs config, so the unfree
assertion fires there even though every host allows it - re-instantiate with
that one knob. no overlays, so `packages.*` hashes are unchanged.
built from source per upstream's Dockerfile: `--ignore-scripts` throughout
(their postinstall builds node-pty against electron's ABI, which segfaults
under plain node) then one rebuild against node's own.

it hands out live shells behind a single password with no OIDC, so it stays
off `public` - the one service outside the identity tower. direnv on minerva
so a terminal node opened in a repo picks up that repo's devshell.
gitignore .nodeterm/ canvas state
All checks were successful
CI / check (pull_request) Successful in 54s
CI / deploy (pull_request) Has been skipped
CI / build (pull_request) Successful in 3m23s
CI / dry-activate (pull_request) Successful in 1m4s
CI / check (push) Has been skipped
CI / build (push) Has been skipped
CI / dry-activate (push) Has been skipped
CI / deploy (push) Successful in 3m2s
f2e73192c2
lament merged commit f2e73192c2 into main 2026-08-17 11:06:28 -05:00
lament deleted branch nodeterm 2026-08-17 11:06:28 -05:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Reference
lament/pantheon!124
No description provided.