This commit is contained in:
Sridhar Ratnakumar 2026-07-07 20:43:41 -04:00
commit 4c85b9d8af
45 changed files with 2188 additions and 456 deletions

3
.envrc
View file

@ -1,3 +0,0 @@
watch_file \
modules/flake-parts/devshell.nix
use flake

View file

@ -13,9 +13,11 @@ in
"${homeMod}/claude-code" "${homeMod}/claude-code"
"${homeMod}/work/juspay.nix" "${homeMod}/work/juspay.nix"
"${homeMod}/work/opencode.nix"
"${homeMod}/services/obsidian.nix" "${homeMod}/services/obsidian.nix"
"${homeMod}/services/kolu.nix" "${homeMod}/services/kolu.nix"
"${homeMod}/services/drishti"
# Remote builders # Remote builders
"${homeMod}/nix/buildMachines" "${homeMod}/nix/buildMachines"
@ -30,8 +32,22 @@ in
services.kolu.host = "100.90.229.113"; # Tailscale IP of zest services.kolu.host = "100.90.229.113"; # Tailscale IP of zest
# The pu-managed kolu-ci-* hosts are reachable only from pureintent, so they
# live in pureintent's drishti (see configurations/nixos/pureintent), not here.
services.drishti.hosts = [
"localhost"
"sincereintent"
"pureintent"
"naiveintent"
"vanjaram.tail12b27.ts.net"
"nix-infra@rasam.tail12b27.ts.net"
];
home.packages = [ home.packages = [
inputs.disc-scrape.packages.${pkgs.stdenv.hostPlatform.system}.default inputs.disc-scrape.packages.${pkgs.stdenv.hostPlatform.system}.default
pkgs.zellij-one pkgs.zellij-one
pkgs.twitter-convert
pkgs.python3
pkgs.portfwd
]; ];
} }

View file

@ -6,7 +6,7 @@ let
homeMod = self + /modules/home; homeMod = self + /modules/home;
in in
{ {
nixos-unified.sshTarget = "srid@192.168.2.219"; nixos-unified.sshTarget = "srid@naiveintent";
nixos-unified.localPrivilegeMode = "sudo-nixos-rebuild"; nixos-unified.localPrivilegeMode = "sudo-nixos-rebuild";
security.sudo.extraRules = [ security.sudo.extraRules = [
@ -24,6 +24,7 @@ in
imports = [ imports = [
self.nixosModules.default self.nixosModules.default
./configuration.nix ./configuration.nix
(self + /modules/nixos/linux/llm-debugging.nix)
]; ];
users.users.${flake.config.me.username}.linger = true; users.users.${flake.config.me.username}.linger = true;
@ -31,7 +32,10 @@ in
"${homeMod}/gui/1password.nix" "${homeMod}/gui/1password.nix"
"${homeMod}/services/kolu.nix" "${homeMod}/services/kolu.nix"
{ {
services.kolu.host = "192.168.2.219"; # Tailscale IP of pureintent services.kolu.host = "100.78.88.70"; # Tailscale IP of naiveintent
# Browser origin differs from the Host kolu sees (served via Tailscale
# MagicDNS reverse proxy), so allow it for the CSWSH origin gate.
services.kolu.allowedOrigins = [ "https://naiveintent.rooster-blues.ts.net" ];
} }
"${homeMod}/nix/gc.nix" "${homeMod}/nix/gc.nix"

View file

@ -28,18 +28,29 @@
# Enable networking # Enable networking
networking.networkmanager.enable = true; networking.networkmanager.enable = true;
# Wired (enp1s0) and Wi-Fi (drapeau/wlp2s0) are on the same LAN. Letting both # Wired (enp1s0) and Wi-Fi (wlp2s0) are on the same LAN. Letting both
# autoconnect causes ARP flux: the gateway's neighbor entry for our IP flips # autoconnect causes ARP flux: the gateway's neighbor entry for our IP flips
# between the two NIC MACs and the Ethernet path goes silently dead. # between the two NIC MACs and the Ethernet path goes silently dead.
# See docs/LINUX-INTERNET-ISSUES.md. # See docs/LINUX-INTERNET-ISSUES.md.
systemd.services.nm-drapeau-noautoconnect = { #
description = "Disable autoconnect on drapeau Wi-Fi profile"; # Disable autoconnect on *every* saved Wi-Fi profile (not a single hardcoded
# SSID) so a newly-joined network can't slip past and bring Wi-Fi up beside
# Ethernet. Wi-Fi stays usable on demand via `nmcli connection up <name>`.
systemd.services.nm-wifi-noautoconnect = {
description = "Disable autoconnect on all Wi-Fi profiles (avoid dual-NIC ARP flux)";
after = [ "NetworkManager.service" ]; after = [ "NetworkManager.service" ];
wants = [ "NetworkManager.service" ]; wants = [ "NetworkManager.service" ];
wantedBy = [ "multi-user.target" ]; wantedBy = [ "multi-user.target" ];
serviceConfig.Type = "oneshot"; serviceConfig.Type = "oneshot";
script = '' script = ''
${pkgs.networkmanager}/bin/nmcli connection modify drapeau connection.autoconnect no || true nmcli=${pkgs.networkmanager}/bin/nmcli
"$nmcli" -t -f TYPE,NAME connection show | while IFS=: read -r type name; do
if [ "$type" = "802-11-wireless" ]; then
"$nmcli" connection modify "$name" connection.autoconnect no || true
fi
done
# Drop any Wi-Fi link that already came up, so the fix applies without a reboot.
"$nmcli" device disconnect wlp2s0 || true
''; '';
}; };
@ -81,12 +92,17 @@
# Allow unfree packages # Allow unfree packages
nixpkgs.config.allowUnfree = true; nixpkgs.config.allowUnfree = true;
# The NixOS options manual is rarely consulted locally (options are searched
# online), but generating it evaluates the doc string of every option — a
# measurable chunk of eval time. Package man pages (`man git`) are unaffected.
documentation.nixos.enable = false;
# List packages installed in system profile. To search, run: # List packages installed in system profile. To search, run:
# $ nix search wget # $ nix search wget
environment.systemPackages = with pkgs; [ environment.systemPackages = with pkgs; [
git git
btop btop
flake.inputs.zmx.packages.${pkgs.stdenv.hostPlatform.system}.default python3
]; ];
# Some programs need SUID wrappers, can be configured further or are # Some programs need SUID wrappers, can be configured further or are

View file

@ -4,6 +4,10 @@ let
inherit (flake) inputs; inherit (flake) inputs;
inherit (inputs) self; inherit (inputs) self;
homeMod = self + /modules/home; homeMod = self + /modules/home;
# pu-managed CI fleet (kolu-ci-1 .. kolu-ci-8). These are ssh_config aliases
# provided by `pu` (Include ~/.pu-state/*/ssh_config) and are reachable only
# from pureintent, so drishti monitors them from here.
ciHosts = map (n: "kolu-ci-${toString n}") (lib.range 1 8);
in in
{ {
nixos-unified.sshTarget = "srid@pureintent"; nixos-unified.sshTarget = "srid@pureintent";
@ -25,10 +29,16 @@ in
imports = [ imports = [
self.nixosModules.default self.nixosModules.default
./configuration.nix ./configuration.nix
./devbox.nix
(self + /modules/nixos/linux/beszel.nix) (self + /modules/nixos/linux/beszel.nix)
(self + /modules/nixos/linux/incus) (self + /modules/nixos/linux/incus)
(self + /modules/nixos/linux/llm-debugging.nix)
]; ];
# anywhen runs as an incus-pet container (see modules/nixos/linux/incus/incus-pet).
# Deployed with:
# incus-pet deploy github:srid/anywhen --port 6111 --listen 100.122.32.106
# Expose the incus UI on the Tailscale interface only. # Expose the incus UI on the Tailscale interface only.
virtualisation.incus.preseed.config."core.https_address" = "100.122.32.106:8443"; virtualisation.incus.preseed.config."core.https_address" = "100.122.32.106:8443";
@ -38,10 +48,17 @@ in
"${homeMod}/cli/controlpersist.nix" "${homeMod}/cli/controlpersist.nix"
"${homeMod}/claude-code" "${homeMod}/claude-code"
"${homeMod}/work/juspay.nix" "${homeMod}/work/juspay.nix"
"${homeMod}/work/opencode.nix"
"${homeMod}/services/vira.nix" "${homeMod}/services/vira.nix"
"${homeMod}/services/kolu.nix" "${homeMod}/services/kolu.nix"
"${homeMod}/services/drishti"
{ {
services.kolu.host = "100.122.32.106"; # Tailscale IP of pureintent services.kolu.host = "100.122.32.106"; # Tailscale IP of pureintent
# Browser origin differs from the Host kolu sees (served via Tailscale
# MagicDNS reverse proxy), so allow it for the CSWSH origin gate.
services.kolu.allowedOrigins = [ "https://pureintent.rooster-blues.ts.net" ];
# Watch the pu-managed CI fleet from here (only reachable from pureintent).
services.drishti.hosts = [ "localhost" ] ++ ciHosts;
} }
# "${homeMod}/services/dropbox.nix" # "${homeMod}/services/dropbox.nix"
@ -82,4 +99,14 @@ in
# Workaround the annoying `Failed to start Network Manager Wait Online` error on switch. # Workaround the annoying `Failed to start Network Manager Wait Online` error on switch.
# https://github.com/NixOS/nixpkgs/issues/180175 # https://github.com/NixOS/nixpkgs/issues/180175
systemd.services.NetworkManager-wait-online.enable = false; systemd.services.NetworkManager-wait-online.enable = false;
# Workaround `nixos-rebuild switch` hanging at "reloading the following units:
# dbus-broker.service". The reload step stalls (broker has long-lived clients
# holding the bus); skip reload/restart during activation. Bus policy changes
# land on next boot instead. Same applies to the per-user broker
# (`Failed to reload user unit dbus-broker.service` → exit 4).
systemd.services.dbus-broker.reloadIfChanged = lib.mkForce false;
systemd.services.dbus-broker.restartIfChanged = lib.mkForce false;
systemd.user.services.dbus-broker.reloadIfChanged = lib.mkForce false;
systemd.user.services.dbus-broker.restartIfChanged = lib.mkForce false;
} }

View file

@ -0,0 +1,58 @@
# Depends on programs.jumphost.socks5Proxy (https://github.com/srid/jumphost-nix)
# for the local SOCKS5 listener. See modules/home/work/juspay.nix:29-31.
{ config, flake, pkgs, ... }:
let
socksPort = config.home-manager.users.${flake.config.me.username}.programs.jumphost.socks5Proxy.port;
proxychainsBin = "${config.programs.proxychains.package}/bin/proxychains4";
proxyExports = ''
export ALL_PROXY=socks5://127.0.0.1:${toString socksPort}
export HTTPS_PROXY=socks5://127.0.0.1:${toString socksPort}
export HTTP_PROXY=socks5://127.0.0.1:${toString socksPort}
'';
vanjaram-run = pkgs.writeShellScriptBin "vanjaram-run" ''
${proxyExports}
exec ${proxychainsBin} "$@"
'';
puBin = "${flake.inputs.project-unknown.packages.${pkgs.stdenv.hostPlatform.system}.default}/bin/pu";
pu = pkgs.writeShellScriptBin "pu" ''
${proxyExports}
exec ${proxychainsBin} ${puBin} "$@"
'';
in
{
programs.proxychains = {
enable = true;
quietMode = true;
chain.type = "strict";
proxyDNS = true;
proxies.devbox = {
enable = true;
type = "socks5";
host = "127.0.0.1";
port = socksPort;
};
};
environment.systemPackages = [
vanjaram-run
pu
];
# pu writes per-instance ssh_config files under ~/.pu-state/<name>/. Including
# them lets `ssh <name>` work directly. The inner ssh those configs spawn goes
# to `pu@<PU_HOST>` which is only reachable via vanjaram — route any ssh to
# user `pu` through the SOCKS5 proxy.
home-manager.users.${flake.config.me.username}.programs.ssh = {
includes = [ "~/.pu-state/*/ssh_config" ];
matchBlocks."pu-jumphost" = {
match = "user pu";
proxyCommand = "${pkgs.netcat-openbsd}/bin/nc -X 5 -x 127.0.0.1:${toString socksPort} %h %p";
};
};
}

View file

@ -0,0 +1,55 @@
# Pureintent-specific operational recipes.
#
# Imported from the top-level Justfile. Each recipe bakes in the values
# that are stable on this host (tailscale IP, container names, port
# choices) so the day-to-day operator just types `just anywhen-deploy`,
# not the full incus-pet invocation.
LISTEN_IP := "100.122.32.106" # pureintent's tailscale IP
# ----------------------------------------------------------------------
# anywhen — github:srid/anywhen as a per-app incus-pet container.
# ----------------------------------------------------------------------
# Deploy anywhen (default: master); pass `ref` to deploy a branch/PR.
[group('anywhen')]
anywhen-deploy ref="github:srid/anywhen":
nix run .#incus-pet -- deploy {{ ref }} anywhen --port 6111 --listen {{ LISTEN_IP }}
# Drop the anywhen container entirely (state goes with it — back up first).
[group('anywhen')]
anywhen-rm:
nix run .#incus-pet -- rm anywhen
# Pull a WAL-checkpointed snapshot of the live SQLite DB to PATH.
[group('anywhen')]
anywhen-backup path="/tmp/anywhen.db":
incus exec anywhen -- systemctl stop anywhen.service
incus exec anywhen -- sh -c 'cd /var/lib/private/anywhen && sqlite3 anywhen.db "PRAGMA wal_checkpoint(TRUNCATE);" 2>/dev/null || true'
incus file pull anywhen/var/lib/private/anywhen/anywhen.db {{ path }}
incus exec anywhen -- systemctl start anywhen.service
@echo "backup -> {{ path }}"
# Push a DB file into the anywhen container (stop/copy/chown/wipe-wal/start).
[group('anywhen')]
anywhen-restore db:
incus exec anywhen -- systemctl stop anywhen.service
incus file push {{ db }} anywhen/var/lib/private/anywhen/anywhen.db --mode 644
incus exec anywhen -- sh -c 'chown $(stat -c "%u:%g" /var/lib/private/anywhen) /var/lib/private/anywhen/anywhen.db'
incus exec anywhen -- rm -f /var/lib/private/anywhen/anywhen.db-wal /var/lib/private/anywhen/anywhen.db-shm
incus exec anywhen -- systemctl start anywhen.service
@echo "restored {{ db }} -> anywhen.service active"
# Health check (curl /api/health, in-container service status, full list).
[group('anywhen')]
anywhen-status:
@echo "--- tailnet ---"
@curl -sS http://{{ LISTEN_IP }}:6111/api/health && echo
@echo "--- container ---"
@incus exec anywhen -- systemctl is-active anywhen.service
@nix run .#incus-pet -- list
# Drop into a root shell inside the anywhen container.
[group('anywhen')]
anywhen-shell:
incus exec anywhen -- bash

View file

@ -239,6 +239,57 @@ ping -c 2 -W 2 1.1.1.1
curl -4 -sS --connect-timeout 4 --max-time 8 https://github.com >/dev/null curl -4 -sS --connect-timeout 4 --max-time 8 https://github.com >/dev/null
``` ```
## Variant: MagicDNS with no upstream resolver
Different outage, similar-looking symptom: `/etc/resolv.conf` again pointed only
at `100.100.100.100`, but this time IP-layer routing was healthy and the cause
was purely DNS.
Symptoms:
- `ping 1.1.1.1` works.
- `ping google.com` fails with `Name or service not known`.
- `getent hosts github.com` returns nothing.
- `*.ts.net` lookups still work (split-DNS route is independent).
Smoking gun in `journalctl -u tailscaled`:
```text
dns: resolver: forward: no upstream resolvers set, returning SERVFAIL
```
And `tailscale dns status` shows:
```text
Resolvers (in preference order):
(no resolvers configured, system default will be used: see 'System DNS configuration' below)
...
(failed to read system DNS configuration: Access denied: dns-osconfig dump access denied)
```
What's happening: Tailscale is managing `/etc/resolv.conf` (`accept-dns=true`)
and MagicDNS handles `*.ts.net` via the split-DNS route, but for every other
query it needs an upstream resolver. If the tailnet admin console has no
**Global nameservers** configured, Tailscale tries to fall back to the device's
system DNS — which on NixOS/tailscale 1.98 fails with the `dns-osconfig`
access-denied error above. Result: SERVFAIL for everything non-tailnet.
Fix in the admin console at <https://login.tailscale.com/admin/dns>:
1. Under **Global nameservers**, add an upstream (e.g. Cloudflare `1.1.1.1`).
2. Turn **Override DNS servers ON**. With it off, the global nameserver is only
used when the device's own OS DNS is readable — which on this host it isn't.
Netmap propagation is near-instant; no daemon restart needed. Verify:
```bash
ssh pureintent 'tailscale dns status | sed -n "/Resolvers/,/Split/p"'
ssh pureintent 'getent hosts github.com && ping -c2 google.com'
```
The "Resolvers (in preference order)" list should now contain the upstream IPs
instead of `(no resolvers configured ...)`.
## Main lesson ## Main lesson
When both Ethernet and Wi-Fi are on the same subnet, a stale primary interface can When both Ethernet and Wi-Fi are on the same subnet, a stale primary interface can

View file

@ -0,0 +1,79 @@
# pureintent vs naiveintent — which to use for dev + agentic work
Both are AMD Zen 4 (Phoenix) 8c/16t boxes, so CPU is nearly a wash. The
real differences are **RAM, SSD, and form factor**. All numbers below
were measured directly on each machine (June 2026).
## Measured comparison
| | **pureintent** (Beelink SER8) | **naiveintent** (Lenovo ThinkPad 21ME) |
|---|---|---|
| CPU | Ryzen 7 **8845HS**, 8c/16t | Ryzen 7 PRO **8840HS**, 8c/16t |
| Sustained all-core clock | **4.59 GHz** | 4.27 GHz |
| Single-core throughput | ~4.55 GB/s sha (tie) | ~4.70 GB/s sha (tie) |
| RAM | 32 GB — 20 GB free, **already swapping (5.5 GB)** | **64 GB** — 55 GB free, 0 swap |
| SSD | Crucial P3 Plus (QLC, **DRAM-less**) | KIOXIA KXG8 (premium TLC) |
| Sustained write (4 GB, fdatasync) | **729 MB/s** | **3.1 GB/s** (4.3×) |
| Sequential read (direct) | 5.3 GB/s | 3.2 GB/s (through LUKS) |
| Free disk | 200 GB (**77% full**) | 782 GB (13% full) |
| Encryption | none | LUKS full-disk |
| Form factor | always-on mini PC | laptop (battery, lid, roams) |
Both run the `powersave` governor with `balance_performance` EPP
(amd-pstate active mode — normal, not a real powersave), so that's not
a differentiator.
## What it means
- **CPU: a wash.** Same silicon. pureintent holds ~7% higher *sustained*
all-core clock (mini-PC cooling beats a thin laptop chassis), so large
parallel Nix builds finish marginally faster on it. Single-core /
interactive latency is a tie. Not a deciding factor.
- **RAM strongly favors naiveintent (2×).** This is what bites agentic
work — several Claude Code agents + language servers + builds + a
browser + incus containers eat RAM fast. pureintent already dips into
swap at light load; naiveintent has 55 GB free.
- **Disk strongly favors naiveintent.** 4.3× faster sustained writes and
6× more free space, on a better drive. Agentic/dev I/O is write-heavy
(nix store, git, node_modules, compile artifacts, logs). pureintent's
QLC + DRAM-less drive at 77% full only degrades further. LUKS on
naiveintent is effectively free (AES-NI).
## Verdict
For *doing* heavy interactive + agentic work, **naiveintent wins
clearly** — 2× RAM and 4× write throughput dwarf pureintent's ~7%
multi-core edge.
The only caveat was form factor: a laptop that sleeps/roams makes a poor
always-on host for a service like Kolu. **naiveintent will be deployed
always-on and plugged in**, so that caveat is moot → **migrate to
naiveintent.**
## Migration notes
naiveintent's config is currently near-empty (`kolu` + `gc`); pureintent
carries the workhorse stack. To port the setup from
`configurations/nixos/pureintent/default.nix`:
- **Kolu**`services.kolu.host = "naiveintent"` is already set. Revisit
`allowedOrigins` (pureintent allows its Tailscale MagicDNS origin) and
any Tailscale-IP binding.
- **home-manager shared modules** naiveintent lacks: `ssh-agent-forwarding`,
`controlpersist`, `claude-code`, `work/juspay.nix`, `services/vira.nix`,
`services/drishti`.
- **incus** stack (`beszel.nix`, `incus`) + the `core.https_address`
preseed, if you want anywhen/containers and the incus UI.
- **remote builders** (`buildMachines` + `sincereintent.nix`).
- the dbus-broker / NetworkManager-wait-online activation workarounds.
Won't move cleanly:
- **drishti** CI-fleet monitoring depends on pureintent-local ssh aliases
(`pu`-managed `ssh_config`, `kolu-ci-1..8`, reachable only from
pureintent). Either keep drishti on pureintent or move the `pu` state.
- Decide whether pureintent is decommissioned or kept as a secondary
builder.
Regardless of the outcome: **GC pureintent** — at 77% full on a degrading
QLC drive it needs headroom.

32
docs/eval-bench.sh Executable file
View file

@ -0,0 +1,32 @@
#!/usr/bin/env bash
# Ralph eval-time benchmark — counter-based.
# Usage: ralph-bench.sh [target] target = pureintent | sincereintent | (both)
#
# Primary metric: deterministic eval-work counters from NIX_SHOW_STATS:
# nrThunks, nrFunctionCalls, nrPrimOpCalls.
# These are byte-identical across runs (independent of CPU clock/load), so a
# single eval gives an exact number. cpuTime is NOT used: this machine thermally
# throttles, inflating CPU-seconds for identical work (11s..21s for one eval).
# Eval cache disabled; fetcher cache warm via persistent $HOME.
set -uo pipefail
export HOME=/tmp/ralph-evalhome
mkdir -p "$HOME"
FLAKE="${FLAKE:-/home/srid/code/nixos-config/.worktrees/ralph}"
ONLY="${1:-}"
declare -A ATTR
ATTR[pureintent]="$FLAKE#nixosConfigurations.pureintent.config.system.build.toplevel.drvPath"
ATTR[sincereintent]="$FLAKE#homeConfigurations.\"srid@sincereintent\".activationPackage.drvPath"
num() { grep -o "\"$1\": [0-9.]*" /tmp/ralph-stats.json | grep -o '[0-9.]*' | head -1; }
for target in pureintent sincereintent; do
[ -n "$ONLY" ] && [ "$ONLY" != "$target" ] && continue
NIX_SHOW_STATS=1 nix eval --raw "${ATTR[$target]}" --option eval-cache false \
>/dev/null 2>/tmp/ralph-stats.json
if [ $? -ne 0 ]; then echo "$target FAILED"; tail -8 /tmp/ralph-stats.json; exit 1; fi
printf '%-11s thunks=%-10s calls=%-10s primops=%-9s | values=%s sets=%s\n' \
"$target" "$(num nrThunks)" "$(num nrFunctionCalls)" "$(num nrPrimOpCalls)" \
"$(num number)" "$(grep -o '"sets": {[^}]*"number": [0-9]*' /tmp/ralph-stats.json | grep -o '[0-9]*$')"
done

View file

@ -0,0 +1,143 @@
# Nix Eval-Time Optimization (Ralph)
Iterative measurement-driven reduction of Nix **evaluation** time for the two
configs that get evaluated most often on this machine:
- `pureintent` — the NixOS system (`nixosConfigurations.pureintent`)
- `sincereintent` — the home-manager config (`homeConfigurations."srid@sincereintent"`)
## Methodology
Primary metric: **deterministic eval-work counters** from `NIX_SHOW_STATS=1`
`nrThunks`, `nrFunctionCalls`, `nrPrimOpCalls`. These count the actual evaluation
work and are **byte-identical across runs** (independent of CPU clock, load, GC).
> **Why not `cpuTime`/wall?** Initially planned, but rejected after measuring:
> this is a live, thermally-throttling laptop. The *same* eval (identical
> nrThunks every run) reported cpuTime climbing 11s → 21s monotonically as the
> CPU clock dropped under sustained load. Time is meaningless here; counters are
> exact. A 1% drop in nrThunks is a real 1% less work the evaluator must do, on
> any machine. Single eval per target — no median needed.
```sh
# docs/eval-bench.sh [target] target = pureintent | zest | (both)
# - persistent $HOME=/tmp/ralph-evalhome keeps the fetcher/git cache warm
# - --option eval-cache false forces a full re-eval every run
# - reports nrThunks / nrFunctionCalls / nrPrimOpCalls per target (deterministic)
nix eval --raw .#nixosConfigurations.pureintent.config.system.build.toplevel.drvPath --option eval-cache false
nix eval --raw '.#homeConfigurations."srid@sincereintent".activationPackage.drvPath' --option eval-cache false
```
No `nix store gc` between runs (per request). The eval cache is disabled rather
than cleared, so the warm fetcher cache removes input-fetch noise.
**Commit rule:** commit only if `nrThunks` improves >3% for a target with no
regression on the other. (Counters have ~zero noise, so any real reduction
counts; >3% is the bar for a "meaningful" cycle.) Behaviour may change *only* by
dropping inputs/features first confirmed unused.
Profiling tool: `nix eval … --eval-profiler flamegraph --eval-profile-file f`
then aggregate self/inclusive frames (collapsed-stack format).
## Baseline (nrThunks / nrFunctionCalls / nrPrimOpCalls)
| Target | nrThunks | nrFunctionCalls | nrPrimOpCalls |
|------------|------------|-----------------|---------------|
| pureintent | 20,400,114 | 12,975,268 | 6,507,025 |
| sincereintent | 5,323,428 | 3,227,404 | 1,565,120 |
Env: nix 2.34.7, x86_64-linux.
## Optimization log
(Δ% is on nrThunks vs baseline for the affected target.)
| Cycle | Change | pureintent thunks | sincereintent thunks | Verdict |
|-------|--------|-------------------|----------------------|---------|
| 0 | baseline | 20,400,114 | 5,323,428 | — |
| 1 | pureintent: `documentation.nixos.enable = false` (drop NixOS options manual; option doc-strings no longer evaluated) | **19,201,996 (5.9%)** | 5,323,428 | ✅ commit |
| 2 | home: `manual.manpages.enable = false` in shared `modules/home/default.nix` (drop home-manager options manpage; evaluated every HM option's doc) | **17,431,437 (9.2%)** | **3,551,679 (33.3%)** | ✅ commit |
> Note: `modules/home/default.nix` is also fed to every Linux system's *embedded*
> home-manager via `modules/nixos/default.nix`, so cycle 2 reduced pureintent
> too (its HM user's manpages were previously on).
| 3 | (profiling cycle — no change) investigated nixpkgs double-instantiation, unused inputs, `escapeShellArg` hotspot | 17,431,437 | 3,551,679 | — no behaviour-preserving win found |
| 4 | home: drop heavy `home.packages` from shared `cli/terminal.nix``yt-dlp`, `lima`, `omnix`, `pandoc`, `google-cloud-sdk` (kept `hledger`). *Behaviour change, user-approved.* | **16,989,720 (2.5%)** | **2,757,201 (22.4%)** | ✅ commit |
## Dead ends
_(investigated, no improvement — recorded so we don't retry)_
- **nixpkgs is NOT double-instantiated.** `nixos-unified.lib.mkLinuxSystem` calls
`nixpkgs.lib.nixosSystem` without pinning `nixpkgs.pkgs`, but the NixOS
`nixpkgs` module imports the package set exactly once; the `pkgs/top-level/impure.nix`
frame in the profile is that single import. `home-manager.useGlobalPkgs = true`
(set by nixos-unified) already shares it with the embedded HM. No win available.
- **Dropping unused flake inputs does not reduce eval counters.** Nix input
evaluation is lazy: an input not referenced during a config's eval (e.g.
`nixos-hardware`, `git-hooks` — 0 refs) contributes ~0 thunks to that config.
Pruning them shrinks `flake.lock`/`nix flake` overhead only, not `nrThunks`.
- **`escapeShellArg` (~72% inclusive on pureintent) is not a fixable hotspot.**
It is driven by `home-manager` file-linking (`files.nix:442`, ~43% — one
escaped command per managed dotfile) and NixOS `/etc` generation
(`etc.nix:58`). Both scale with how much is managed; nixpkgs/HM own the
implementation. Not patchable from this repo.
- **`programs.command-not-found` already disabled** by the `nix-index-database`
module; nothing to gain.
## Remaining cost is feature-bound
After the two documentation wins, the dominant `derivationStrict` targets in the
pureintent eval profile are all *wanted* features (samples ≈ inclusive share):
`home-manager-generation`/`home-manager-files` (~43%, embedded HM), `/etc`
(~29%), `vira.service`+`vira-wrapped`+`vira-0.1.0.0` (Haskell app), `kolu.service`,
`pipewire`, plus heavy packages in `home.packages` (`google-cloud-sdk`, `pandoc`,
`omnix` — Haskell). Further reductions require trading a feature, not a free
structural change.
## Final measurement (median irrelevant — counters are exact)
| Target | nrThunks (base → final) | Δ | nrFunctionCalls | nrPrimOpCalls |
|--------|-------------------------|------|-----------------|---------------|
| pureintent | 20,400,114 → **16,989,720** | **16.7%** | 12,975,268 → 10,794,555 (16.8%) | 6,507,025 → 5,327,976 (18.1%) |
| sincereintent | 5,323,428 → **2,757,201** | **48.2%** | 3,227,404 → 1,631,213 (49.5%) | 1,565,120 → 771,171 (50.7%) |
Both configs still instantiate cleanly (`nix build --dry-run` on the system
toplevel / home activationPackage succeeds).
## Key findings
1. **On a thermally-throttling machine, time is a lie — count work instead.**
The same eval reported cpuTime from 11s to 21s as the CPU clock sagged, while
`nrThunks` was byte-identical every run. Eval-work counters (`nrThunks`,
`nrFunctionCalls`, `nrPrimOpCalls`) are deterministic, zero-noise, and
machine-independent — a far better optimization metric than wall/cpu time.
2. **Generated option documentation is the biggest free win.** The NixOS options
manual (`documentation.nixos.enable`) and the home-manager options manpage
(`manual.manpages.enable`) each evaluate the doc string of *every* option.
Disabling both: pureintent 14.6%, sincereintent 33% — with zero behaviour
cost (options are searched online).
3. **A shared module multiplies.** `manual.manpages.enable = false` in
`modules/home/default.nix` helped both the standalone home config *and* every
Linux system's embedded home-manager (via `modules/nixos/default.nix`).
4. **Eval is lazy — unused inputs are already free.** Pruning unreferenced flake
inputs does not move the counters; it only trims `flake.lock`.
5. **The remainder is feature-bound.** After the doc wins, eval cost is dominated
by embedded home-manager file-linking, `/etc` generation, and the closures of
wanted services/packages. Trimming 5 heavy `home.packages` (user-approved) took
sincereintent to 48% total; the rest are features worth their eval cost.
## Cost breakdown (per-package eval cost, standalone `pkgs.<p>.drvPath` thunks)
Shared base floor ≈ 1.09M thunks; the excess is each package's own closure-eval:
| Package | thunks | excess | removed? |
|---------|--------|--------|----------|
| yt-dlp | 2.15M | +1.05M | ✅ |
| lima | 1.78M | +0.69M | ✅ |
| omnix | 1.76M | +0.67M | ✅ |
| pandoc | 1.46M | +0.37M | ✅ |
| google-cloud-sdk | 1.39M | +0.30M | ✅ |
| hledger | 1.35M | +0.26M | kept (wanted) |
| ripgrep / fd / just / television | ~1.09M | ~0 | kept (at floor) |

View file

@ -0,0 +1,258 @@
# Ralph: pureintent eval-time optimization
Iterative measurement-driven shrinking of `nixosConfigurations.pureintent`
evaluation time, following the [Ralph
skill](https://github.com/srid/agency/blob/master/.apm/skills/ralph/SKILL.md).
## Methodology
- **Host**: `srid-nc` (NixOS, x86_64-linux, Nix 2.31.5)
- **Repo**: cloned at `~/ralph/nixos-config`, branch `optimize-eval`
- **Command**:
```
/usr/bin/env time -f "%e" \
nixos-rebuild dry-build --flake .#pureintent \
--option eval-cache false
```
- `dry-build` so no derivations are actually realised.
- `--option eval-cache false` because every meaningful source change
invalidates the flake eval-cache — measuring with a hot eval-cache
just measures sqlite lookups (~0.5 s) and tells us nothing about the
work this PR is meant to cut.
- Filesystem / Nix-store cache is warm (we ran the build once to
populate everything).
- **Baseline policy**: 57 consecutive runs, report **median**, range,
and `NIX_SHOW_STATS` counters (`nrFunctionCalls`, `nrThunks`,
`totalBytes`, etc.).
- **Noise floor**: ≈0.5 % (range was 0.06 s on a 10.87 s run). Commit
threshold per Ralph rules: **> 3 %**.
- **Constraints**: (1) other NixOS / home configs must still build,
(2) pureintent runtime behaviour preserved (drv hash may shift),
(3) reducing flake inputs is in scope (npins-style relocation OK).
## Baseline
7 warm runs, eval-cache disabled, `nixos-rebuild dry-build .#pureintent`:
| run | seconds |
|----:|--------:|
| 1 | 10.87 |
| 2 | 10.87 |
| 3 | 10.86 |
| 4 | 10.88 |
| 5 | 10.86 |
| 6 | 10.85 |
| 7 | 10.91 |
**Median: 10.87 s** &middot; range 0.06 s (≈ 0.5 %).
`NIX_SHOW_STATS` for one baseline eval:
| counter | value |
|---|---:|
| CPU time | 16.29 s |
| GC fraction | 3.3 % |
| `nrFunctionCalls` | 25 043 557 |
| `nrPrimOpCalls` | 12 110 404 |
| `nrThunks` | 38 038 010 |
| `nrAvoided` | 32 199 960 |
| `nrLookups` | 16 259 247 |
| `nrOpUpdates` | 2 469 935 |
| `nrOpUpdateValuesCopied` | 85 701 830 |
| `values.number` | 57 025 753 |
| `values.bytes` | 912 412 048 |
| `sets.elements` | 120 709 352 |
| `sets.bytes` | 2 059 750 432 |
| `totalBytes` | 4 139 856 480 |
| `maxRss` | 3 120 MB |
## Flake input inventory
Inputs declared in `flake.nix`:
`agenix`, `disc-scrape`, `disko`, `emanote`, `flake-parts`, `git-hooks`,
`github-nix-ci`, `home-manager`, `imako`, `jumphost-nix`, `kolu`,
`landrun-nix`, `llm-agents`, `nix-darwin`, `nix-index-database`,
`nixos-hardware`, `nixos-unified`, `nixos-vscode-server`, `nixpkgs`,
`nixvim`, `project-unknown`, `vira`, `zmx`.
Per-input blame from Phase-1 probes (drop the module that imports the
input; everything else held constant; 3 runs each, eval-cache off):
| input / module | path | wall when dropped | Δ saved | share |
|---|---|---:|---:|---:|
| baseline | — | 10.87 | — | 100 % |
| `nixvim` | `modules/home/editors/neovim/` | 6.90 | **3.97** | **36.5 %** |
| `vira` | `modules/home/services/vira.nix` | 9.59 | 1.28 | 11.8 % |
| `jumphost-nix` | `modules/home/work/juspay.nix` | 8.16 (vira out, jumphost-nix import removed) | 1.43 | 13.1 % |
| `kolu` | `modules/home/services/kolu.nix` | 10.37 | 0.50 | 4.6 % |
| `agenix` (HM module) | `modules/home/agenix.nix` | ≈ 8.07 | ≈ 0 | < noise |
| `agenix` (NixOS module) | `modules/nixos/common.nix:agenix` | 10.82 | ≈ 0 | < noise |
| `programs.jumphost.*` body | `modules/home/work/juspay.nix` | 9.51 → 9.56 | 0.05 | < noise |
| `claude-code` | `modules/home/claude-code` | 11.01 | ≈ 0 | < noise |
| `buildMachines` | `modules/home/nix/buildMachines*` | 10.89 | ≈ 0 | < noise |
| `incus`, `beszel`, `firefox`, `ttyd` | various | ≈ 10.8510.92 | ≈ 0 | < noise |
Compound floors:
| drop | wall (s) | reduction |
|---|---:|---:|
| baseline | 10.87 | — |
| nixvim + vira + juspay + agenix (HM+NixOS) | 4.29 | 60.5 % |
| above + kolu | 3.85 | 64.6 % |
So the absolute lower bound (with these four heavies stubbed out) is
≈ 3.85 s — that's the cost of nixpkgs + home-manager + nixos-unified +
everything else combined.
The `home-manager.sharedModules` plumbing block in
`modules/nixos/common.nix` was probed independently: removing it left
the wall time unchanged (10.95 s ≈ 10.87 s), so the per-host HM-setup
boilerplate itself isn't a contributor.
## Optimization log
| # | hypothesis | mutation | wall (s) | Δ vs baseline | committed? | notes |
|---|---|---|---|---|---|---|
| 0 | — | baseline | 10.87 | — | — | reference |
| 1 | nixvim is 36 % of eval; the cost is the option system, not the resulting nvim binary | delete `inputs.nixvim` + the nixvim module; replace `modules/home/editors/neovim/` with a minimal `programs.neovim { enable; defaultEditor; vimAlias; viAlias; }` | **6.98** | **3.89 s (35.8 %)** | ✅ | 7-run median; range 7.736.94 (first-run warm-up jitter). Other configs (`naiveintent`, `infinitude-macos`, `srid@zest`) still eval cleanly. **Behaviour note:** nvim is now plain — none of the previous plugins (rose-pine, telescope, treesitter, lualine, noice, LSP keymaps, nvim-tree, lazygit, outline-nvim, mapleader) survive. User explicitly approved dropping nixvim. |
| 2a | jumphost-nix module is ~1.5 s | inline `${jumphost-nix}/module.nix` into `juspay.nix` (drop input, hard-code values, drop devbox.nix's port-option read) | 7.03 | +0.05 (noise) | ❌ | The shallow drilldown was misleading (eval-error truncation). The cost moves; it doesn't disappear. Reverted. |
| 2b | vira HM module is ~1.3 s | inline `inputs.vira.homeManagerModules.vira` into `vira.nix` as direct `systemd.user.services.vira` + `age.secrets` | 6.94 | -0.04 (noise) | ❌ | Same explanation as 2a. Reverted. |
| 2c | unused flake inputs accumulate cost | drop `llm-agents` (literally only commented-out reference) | 7.01 | +0.02 (noise) | ❌ | Lazy inputs don't contribute. Reverted. |
| 2d | flake-parts modules add overhead | drop `claude-sandboxed.nix`, `devshell.nix`, `landrun-nix` | 6.916.99 | ≤0.06 (noise) | ❌ | Reverted. |
| 2e | `programs.ssh.matchBlocks` is the systemd-style cost driver | strip `controlpersist.nix` (3 matchBlocks) | 7.00 | +0.01 (noise) | ❌ | matchBlocks aren't the contributor either; the cost is the **per-entry merge** that pureintent has anyway via devbox.nix's `pu-jumphost` etc. Reverted. |
| 2f | nixos-unified's `autoWire` is overhead | replace with a hand-rolled `flake.nixosConfigurations.pureintent = mkLinuxSystem …` | ERR | — | ❌ | Pureintent itself reaches into `self.nixosModules.default` (created by autoWire), so a useful comparison needs reproducing the auto-wired attrsets first — out of scope. |
## Final measurement
7 warm runs, eval-cache disabled, `nixos-rebuild dry-build .#pureintent`,
on `optimize-eval` HEAD (cycle 1 applied):
| run | seconds |
|----:|--------:|
| 1 | 7.00 |
| 2 | 7.01 |
| 3 | 6.94 |
| 4 | 6.99 |
| 5 | 6.97 |
| 6 | 7.01 |
| 7 | 6.94 |
**Median: 6.99 s** &middot; range 6.947.01 (≈ 1 %).
| | wall (s) | Δ |
|---|---:|---:|
| baseline | 10.87 | — |
| after cycle 1 | **6.99** | **3.88 s (35.7 %)** |
Three other configurations continue to evaluate cleanly:
`nixosConfigurations.naiveintent`,
`darwinConfigurations.infinitude-macos`,
`homeConfigurations."srid@zest"`.
## Dead ends
All of the following were tried after cycle 1 and produced **no
measurable improvement** (≤ noise floor). Reported here so they don't
have to be re-tried.
- **Inline `${jumphost-nix}/module.nix`** into `juspay.nix`. Replaced the
152-line work-jump-host module with an equivalent inline
`programs.ssh.matchBlocks` / `systemd.user.services` /
`programs.git.includes`. Result: 7.03 s — within noise. Earlier
drilldown numbers that suggested ~1.5 s of jumphost-nix cost were
*eval-error truncation* (devbox.nix references
`programs.jumphost.socks5Proxy.port` and aborted eval before the rest
of pureintent was processed). The real cost is in the resulting
submodule materialisation (programs.ssh.matchBlocks, etc.), which is
the same whether the values arrive via a wrapper module or a literal.
- **Inline `inputs.vira.homeManagerModules.vira`**. 167-line option
schema + submodule replaced with a hand-rolled
`systemd.user.services.vira` and `age.secrets` block.
Result: 6.94 s — within noise. Same explanation as above.
- **Inline `inputs.kolu.homeManagerModules.default`**. Similar pattern;
eval errored when `services.kolu.host` setter outlived the option
declaration. With downstream-fix, expected zero benefit by the same
argument.
- **Prune `inputs.llm-agents`** (only reference was already commented
out). Result: 7.01 s — within noise.
- **Drop `modules/flake-parts/claude-sandboxed.nix`** (landrun-nix
consumer at the flake-parts level). Result: 6.91 s — within noise.
- **Drop `modules/flake-parts/devshell.nix`** at flake-parts level.
Result: 6.99 s — within noise.
- **Drop `landrun-nix` input + `claude-sandboxed.nix`**. Result:
6.97 s — within noise.
- **Strip `modules/home/cli/controlpersist.nix`** entirely (3
`programs.ssh.matchBlocks` entries). Result: 7.00 s — within noise.
- **Replace `nixos-unified.flakeModules.autoWire`** with a manual
`flake.nixosConfigurations.pureintent = mkLinuxSystem …` — errors,
because pureintent's own `default.nix` reaches into
`self.nixosModules.default` which autoWire is responsible for
creating. Measuring nixos-unified's residual overhead would require
reproducing the auto-wired module attrsets by hand; not worth the
effort given everything else has plateaued.
## Why we plateau at ~7 s
After cycle 1, the post-mortem profile (each "drop X" probe is run with
all *downstream consumers stubbed out* so the measurement isn't
eval-error-truncated):
| component | wall when dropped | Δ saved |
|---|---:|---:|
| (post-cycle-1 baseline) | 6.99 | — |
| `vira` HM module | ≈ 5.71 | 1.28 |
| `kolu` HM module | ≈ 6.49 | 0.50 |
| jumphost-nix module + body | (cannot measure cleanly; ~1.3 cost is real but inlining gives it back) | — |
| home CLI modules (tmux, starship, terminal, git, direnv, just, npm, nix-index-database, ttyd) | ≈ 0 each, ≈ 0.4 s cumulative | — |
| `agenix` (NixOS + HM) | ≈ 0 | — |
| `claude-code`, `buildMachines`, `incus`, `beszel`, `firefox`, `pipewire` | ≈ 0 each | — |
The remaining ≈ 4 s is the cost of evaluating nixpkgs `lib`, the NixOS
module system, home-manager's option universe, and `nixos-unified`'s
auto-wiring — together they are the irreducible floor for any host that
uses this flake.
The HM modules that *are* expensive (vira, kolu, jumphost-nix) cost what
they cost because their values land in
`systemd.user.services.<name>` or `programs.ssh.matchBlocks.<name>`,
each of which forces a per-entry home-manager submodule. Inlining the
wrapper module doesn't help because the merge is the same on either
side. The only way to reclaim that time is to (a) drop the service /
the matchBlock entirely, or (b) write the unit / ssh_config file
directly via `home.file` / `xdg.configFile` and bypass home-manager's
own ssh and systemd modules across the whole user — a much larger
refactor than fit inside this PR's scope.
## Key findings
1. **nixvim is 36 % of pureintent's eval time.** Its
home-manager-style options module materialises hundreds of plugin
submodules every eval. Cycle 1 dropped it (user-approved behaviour
change to plain `programs.neovim`) and reclaimed 3.88 s.
2. **HM submodule materialisation, not option-declaration count, is the
driver.** Inlining wrapper modules (jumphost-nix, vira) gives the
work-saving illusion in shallow probes but no real saving because
`systemd.user.services` and `programs.ssh.matchBlocks` re-do the
same submodule work regardless of where their values came from.
3. **Unused flake inputs cost essentially nothing** at pureintent
eval-time; flake inputs are lazy and only inputs reached
transitively from `nixosConfigurations.pureintent` contribute.
4. **Eval-error-truncated probes look like wins.** Any "drop X" probe
where a downstream module reads X's option silently shortens the
eval and lies about its cost. Always validate probes succeed (we
started checking exit codes after the jumphost-nix dead end).
## Methodology cost
- 1 successful commit + push (cycle 1).
- ≈ 50 probe runs on `srid-nc` over the cycle (3-run medians of dry-build).
- 1 draft PR open: <https://github.com/srid/nixos-config/pull/117>.

387
flake.lock generated
View file

@ -55,11 +55,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1771437256, "lastModified": 1776249299,
"narHash": "sha256-bLqwib+rtyBRRVBWhMuBXPCL/OThfokA+j6+uH7jDGU=", "narHash": "sha256-Dt9t1TGRmJFc0xVYhttNBD6QsAgHOHCArqGa0AyjrJY=",
"owner": "numtide", "owner": "numtide",
"repo": "blueprint", "repo": "blueprint",
"rev": "06ee7190dc2620ea98af9eb225aa9627b68b0e33", "rev": "56131e8628f173d24a27f6d27c0215eff57e40dd",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -69,12 +69,33 @@
} }
}, },
"bun2nix": { "bun2nix": {
"inputs": {
"flake-parts": "flake-parts_2",
"nixpkgs": "nixpkgs_2",
"systems": "systems_3",
"treefmt-nix": "treefmt-nix"
},
"locked": {
"lastModified": 1779931262,
"narHash": "sha256-8ypPqVPCxiLD6GAvGjA+cJ3REJzgMJDGkGJLZYuCfGo=",
"owner": "juspay",
"repo": "bun2nix",
"rev": "ce67cc1885e5e28fcc3ff865479be1a19ff396cb",
"type": "github"
},
"original": {
"owner": "juspay",
"ref": "rawflake",
"repo": "bun2nix",
"type": "github"
}
},
"bun2nix_2": {
"inputs": { "inputs": {
"flake-parts": [ "flake-parts": [
"llm-agents", "llm-agents",
"flake-parts" "flake-parts"
], ],
"import-tree": "import-tree",
"nixpkgs": [ "nixpkgs": [
"llm-agents", "llm-agents",
"nixpkgs" "nixpkgs"
@ -89,11 +110,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1770895533, "lastModified": 1778446047,
"narHash": "sha256-v3QaK9ugy9bN9RXDnjw0i2OifKmz2NnKM82agtqm/UY=", "narHash": "sha256-oQvcadh2BCkrog+SGrG6YffKJrveYpjj3TdQJWaKhaM=",
"owner": "nix-community", "owner": "nix-community",
"repo": "bun2nix", "repo": "bun2nix",
"rev": "c843f477b15f51151f8c6bcc886954699440a6e1", "rev": "f2bc12af1a6369648aac41041ceeaa0b866599c6",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -257,6 +278,24 @@
"type": "github" "type": "github"
} }
}, },
"drishti": {
"inputs": {
"bun2nix": "bun2nix"
},
"locked": {
"lastModified": 1782927932,
"narHash": "sha256-OjOH2WguMohk/NAYdnS/rj0Ag+0G/k9XW5HsvcmErUE=",
"owner": "srid",
"repo": "drishti",
"rev": "f692afb245a00a28f5b8bac2fa2462a58db9388a",
"type": "github"
},
"original": {
"owner": "srid",
"repo": "drishti",
"type": "github"
}
},
"ema": { "ema": {
"flake": false, "flake": false,
"locked": { "locked": {
@ -280,14 +319,14 @@
"commonmark-wikilink": "commonmark-wikilink", "commonmark-wikilink": "commonmark-wikilink",
"ema": "ema", "ema": "ema",
"emanote-template": "emanote-template", "emanote-template": "emanote-template",
"flake-parts": "flake-parts_2", "flake-parts": "flake-parts_3",
"fourmolu-nix": "fourmolu-nix", "fourmolu-nix": "fourmolu-nix",
"git-hooks": "git-hooks_2", "git-hooks": "git-hooks_2",
"haskell-flake": "haskell-flake", "haskell-flake": "haskell-flake",
"heist-extra": "heist-extra", "heist-extra": "heist-extra",
"lvar": "lvar", "lvar": "lvar",
"nixos-unified": "nixos-unified", "nixos-unified": "nixos-unified",
"nixpkgs": "nixpkgs_2", "nixpkgs": "nixpkgs_3",
"unionmount": "unionmount" "unionmount": "unionmount"
}, },
"locked": { "locked": {
@ -342,6 +381,28 @@
} }
}, },
"flake-parts_2": { "flake-parts_2": {
"inputs": {
"nixpkgs-lib": [
"drishti",
"bun2nix",
"nixpkgs"
]
},
"locked": {
"lastModified": 1777988971,
"narHash": "sha256-qIoWPDs+0/8JecyYgE3gpKQxW/4bLW/gp45vow9ioCQ=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "0678d8986be1661af6bb555f3489f2fdfc31f6ff",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "flake-parts",
"type": "github"
}
},
"flake-parts_3": {
"inputs": { "inputs": {
"nixpkgs-lib": [ "nixpkgs-lib": [
"emanote", "emanote",
@ -362,7 +423,7 @@
"type": "github" "type": "github"
} }
}, },
"flake-parts_3": { "flake-parts_4": {
"inputs": { "inputs": {
"nixpkgs-lib": "nixpkgs-lib" "nixpkgs-lib": "nixpkgs-lib"
}, },
@ -380,7 +441,7 @@
"type": "github" "type": "github"
} }
}, },
"flake-parts_4": { "flake-parts_5": {
"inputs": { "inputs": {
"nixpkgs-lib": [ "nixpkgs-lib": [
"imako", "imako",
@ -401,7 +462,7 @@
"type": "github" "type": "github"
} }
}, },
"flake-parts_5": { "flake-parts_6": {
"inputs": { "inputs": {
"nixpkgs-lib": [ "nixpkgs-lib": [
"llm-agents", "llm-agents",
@ -409,32 +470,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1775087534, "lastModified": 1778716662,
"narHash": "sha256-91qqW8lhL7TLwgQWijoGBbiD4t7/q75KTi8NxjVmSmA=", "narHash": "sha256-m1Yf0wZ8j1OHjTc2UwHwyQRSnNeSgLJOd7q5Y45hzi4=",
"owner": "hercules-ci", "owner": "hercules-ci",
"repo": "flake-parts", "repo": "flake-parts",
"rev": "3107b77cd68437b9a76194f0f7f9c55f2329ca5b", "rev": "f7c1a2d347e4c52d5fb8d10cb4d94b5884e546fb",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "flake-parts",
"type": "github"
}
},
"flake-parts_6": {
"inputs": {
"nixpkgs-lib": [
"nixvim",
"nixpkgs"
]
},
"locked": {
"lastModified": 1777988971,
"narHash": "sha256-qIoWPDs+0/8JecyYgE3gpKQxW/4bLW/gp45vow9ioCQ=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "0678d8986be1661af6bb555f3489f2fdfc31f6ff",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -464,24 +504,6 @@
"type": "github" "type": "github"
} }
}, },
"flake-utils": {
"inputs": {
"systems": "systems_5"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"fourmolu-nix": { "fourmolu-nix": {
"locked": { "locked": {
"lastModified": 1707266073, "lastModified": 1707266073,
@ -758,12 +780,12 @@
"aeson-typescript": "aeson-typescript", "aeson-typescript": "aeson-typescript",
"commonmark-simple": "commonmark-simple_2", "commonmark-simple": "commonmark-simple_2",
"commonmark-wikilink": "commonmark-wikilink_2", "commonmark-wikilink": "commonmark-wikilink_2",
"flake-parts": "flake-parts_4", "flake-parts": "flake-parts_5",
"fourmolu-nix": "fourmolu-nix_2", "fourmolu-nix": "fourmolu-nix_2",
"git-hooks": "git-hooks_4", "git-hooks": "git-hooks_4",
"haskell-flake": "haskell-flake_2", "haskell-flake": "haskell-flake_2",
"lvar": "lvar_2", "lvar": "lvar_2",
"nixpkgs": "nixpkgs_3", "nixpkgs": "nixpkgs_4",
"process-compose-flake": "process-compose-flake", "process-compose-flake": "process-compose-flake",
"unionmount": "unionmount_2" "unionmount": "unionmount_2"
}, },
@ -781,21 +803,6 @@
"type": "github" "type": "github"
} }
}, },
"import-tree": {
"locked": {
"lastModified": 1763762820,
"narHash": "sha256-ZvYKbFib3AEwiNMLsejb/CWs/OL/srFQ8AogkebEPF0=",
"owner": "vic",
"repo": "import-tree",
"rev": "3c23749d8013ec6daa1d7255057590e9ca726646",
"type": "github"
},
"original": {
"owner": "vic",
"repo": "import-tree",
"type": "github"
}
},
"jumphost-nix": { "jumphost-nix": {
"flake": false, "flake": false,
"locked": { "locked": {
@ -812,18 +819,41 @@
"type": "github" "type": "github"
} }
}, },
"kolu": { "juspay-ai": {
"inputs": {
"llm-agents": [
"llm-agents"
],
"nixpkgs": [
"nixpkgs"
]
},
"locked": { "locked": {
"lastModified": 1778618379, "lastModified": 1782075422,
"narHash": "sha256-V870SzI1lvwZuhqroDDB5p2fja2OmaIl0mzlLGt1o8g=", "narHash": "sha256-VVgVpxWFruch4X0PTfesyXVeppsSyBQFja+QIAuCQLo=",
"owner": "juspay", "owner": "juspay",
"repo": "kolu", "repo": "AI",
"rev": "5c86cbd4567f27fb81762a0399b48d344fb37a90", "rev": "c24b8d17ee80a8d03ef589eec0e68cd157e4ae9b",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "juspay", "owner": "juspay",
"ref": "feat/minimap-hide-parked-toggle", "repo": "AI",
"type": "github"
}
},
"kolu": {
"locked": {
"lastModified": 1783019894,
"narHash": "sha256-0SsUGZl+0tYSDVgdNFX80fDGzZEcEr6dfDYnC6tzXqU=",
"owner": "juspay",
"repo": "kolu",
"rev": "da15c87a740b9cc512438131c968c21bfb0e8839",
"type": "github"
},
"original": {
"owner": "juspay",
"ref": "W1",
"repo": "kolu", "repo": "kolu",
"type": "github" "type": "github"
} }
@ -847,26 +877,23 @@
"llm-agents": { "llm-agents": {
"inputs": { "inputs": {
"blueprint": "blueprint", "blueprint": "blueprint",
"bun2nix": "bun2nix", "bun2nix": "bun2nix_2",
"flake-parts": "flake-parts_5", "flake-parts": "flake-parts_6",
"nixpkgs": [ "nixpkgs": "nixpkgs_5",
"nixpkgs" "systems": "systems_4",
], "treefmt-nix": "treefmt-nix_2"
"systems": "systems_3",
"treefmt-nix": "treefmt-nix"
}, },
"locked": { "locked": {
"lastModified": 1775790657, "lastModified": 1782068377,
"narHash": "sha256-kAJGGBOI+2DFJSkN3RH1Qk9uUSFqMfp6cK0+eORs+OA=", "narHash": "sha256-pKjKMGfTGlL+qOfDnV6h1XNXiS3/NXLAM2dnnmLxjsQ=",
"owner": "numtide", "owner": "numtide",
"repo": "llm-agents.nix", "repo": "llm-agents.nix",
"rev": "d9583b68fdc553936b35dc6ca206d8d8dd552e5b", "rev": "3bf60a513d1ebab0b46aee641b2323394905d543",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "numtide", "owner": "numtide",
"repo": "llm-agents.nix", "repo": "llm-agents.nix",
"rev": "d9583b68fdc553936b35dc6ca206d8d8dd552e5b",
"type": "github" "type": "github"
} }
}, },
@ -1083,6 +1110,22 @@
} }
}, },
"nixpkgs_2": { "nixpkgs_2": {
"locked": {
"lastModified": 1777954456,
"narHash": "sha256-hGdgeU2Nk87RAuZyYjyDjFL6LK7dAZN5RE9+hrDTkDU=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "549bd84d6279f9852cae6225e372cc67fb91a4c1",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_3": {
"locked": { "locked": {
"lastModified": 1752900028, "lastModified": 1752900028,
"narHash": "sha256-dPALCtmik9Wr14MGqVXm+OQcv7vhPBXcWNIOThGnB/Q=", "narHash": "sha256-dPALCtmik9Wr14MGqVXm+OQcv7vhPBXcWNIOThGnB/Q=",
@ -1098,7 +1141,7 @@
"type": "github" "type": "github"
} }
}, },
"nixpkgs_3": { "nixpkgs_4": {
"locked": { "locked": {
"lastModified": 1770169770, "lastModified": 1770169770,
"narHash": "sha256-awR8qIwJxJJiOmcEGgP2KUqYmHG4v/z8XpL9z8FnT1A=", "narHash": "sha256-awR8qIwJxJJiOmcEGgP2KUqYmHG4v/z8XpL9z8FnT1A=",
@ -1114,7 +1157,23 @@
"type": "github" "type": "github"
} }
}, },
"nixpkgs_4": { "nixpkgs_5": {
"locked": {
"lastModified": 1781607440,
"narHash": "sha256-rxO+uc/KFbSJp+pgyXRuAX6QlG9hJdnt0BXpEQRXY+U=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "3e41b24abd260e8f71dbe2f5737d24122f972158",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_6": {
"locked": { "locked": {
"lastModified": 1778443072, "lastModified": 1778443072,
"narHash": "sha256-zi7/fsqM/kFdNuED//4WOCUtezGtKKqRNORjMvfwjnA=", "narHash": "sha256-zi7/fsqM/kFdNuED//4WOCUtezGtKKqRNORjMvfwjnA=",
@ -1130,7 +1189,7 @@
"type": "github" "type": "github"
} }
}, },
"nixpkgs_5": { "nixpkgs_7": {
"locked": { "locked": {
"lastModified": 1765803225, "lastModified": 1765803225,
"narHash": "sha256-xwaZV/UgJ04+ixbZZfoDE8IsOWjtvQZICh9aamzPnrg=", "narHash": "sha256-xwaZV/UgJ04+ixbZZfoDE8IsOWjtvQZICh9aamzPnrg=",
@ -1146,43 +1205,6 @@
"type": "github" "type": "github"
} }
}, },
"nixpkgs_6": {
"locked": {
"lastModified": 1764635402,
"narHash": "sha256-6rYcajRLe2C5ZYnV1HYskJl+QAkhvseWTzbdQiTN9OI=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "5f53b0d46d320352684242d000b36dcfbbf7b0bc",
"type": "github"
},
"original": {
"owner": "nixos",
"repo": "nixpkgs",
"type": "github"
}
},
"nixvim": {
"inputs": {
"flake-parts": "flake-parts_6",
"nixpkgs": [
"nixpkgs"
],
"systems": "systems_4"
},
"locked": {
"lastModified": 1778510615,
"narHash": "sha256-cMNCx8mQTJnVkA6kt3B3ArGpCOOniYn644hH0mJHSsw=",
"owner": "nix-community",
"repo": "nixvim",
"rev": "fa8cd368d27cf9541f086485884928315abdcc8c",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "nixvim",
"type": "github"
}
},
"process-compose-flake": { "process-compose-flake": {
"locked": { "locked": {
"lastModified": 1767863885, "lastModified": 1767863885,
@ -1213,6 +1235,26 @@
"type": "github" "type": "github"
} }
}, },
"project-unknown": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1779830167,
"narHash": "sha256-j8WvZUrFIXajfk0O8jk4PnwzibgYIfrSwShy7rcnSwI=",
"owner": "juspay",
"repo": "project-unknown",
"rev": "6c54c2a6e4bdc2199010a5a1bd83c89cc67e0ea3",
"type": "github"
},
"original": {
"owner": "juspay",
"repo": "project-unknown",
"type": "github"
}
},
"record-hasfield": { "record-hasfield": {
"flake": false, "flake": false,
"locked": { "locked": {
@ -1234,13 +1276,15 @@
"agenix": "agenix", "agenix": "agenix",
"disc-scrape": "disc-scrape", "disc-scrape": "disc-scrape",
"disko": "disko", "disko": "disko",
"drishti": "drishti",
"emanote": "emanote", "emanote": "emanote",
"flake-parts": "flake-parts_3", "flake-parts": "flake-parts_4",
"git-hooks": "git-hooks_3", "git-hooks": "git-hooks_3",
"github-nix-ci": "github-nix-ci", "github-nix-ci": "github-nix-ci",
"home-manager": "home-manager", "home-manager": "home-manager",
"imako": "imako", "imako": "imako",
"jumphost-nix": "jumphost-nix", "jumphost-nix": "jumphost-nix",
"juspay-ai": "juspay-ai",
"kolu": "kolu", "kolu": "kolu",
"landrun-nix": "landrun-nix", "landrun-nix": "landrun-nix",
"llm-agents": "llm-agents", "llm-agents": "llm-agents",
@ -1249,10 +1293,9 @@
"nixos-hardware": "nixos-hardware", "nixos-hardware": "nixos-hardware",
"nixos-unified": "nixos-unified_2", "nixos-unified": "nixos-unified_2",
"nixos-vscode-server": "nixos-vscode-server", "nixos-vscode-server": "nixos-vscode-server",
"nixpkgs": "nixpkgs_4", "nixpkgs": "nixpkgs_6",
"nixvim": "nixvim", "project-unknown": "project-unknown",
"vira": "vira", "vira": "vira"
"zmx": "zmx"
} }
}, },
"rust-flake": { "rust-flake": {
@ -1376,21 +1419,6 @@
"type": "github" "type": "github"
} }
}, },
"systems_5": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"tabler-icons-hs": { "tabler-icons-hs": {
"flake": false, "flake": false,
"locked": { "locked": {
@ -1410,7 +1438,8 @@
"treefmt-nix": { "treefmt-nix": {
"inputs": { "inputs": {
"nixpkgs": [ "nixpkgs": [
"llm-agents", "drishti",
"bun2nix",
"nixpkgs" "nixpkgs"
] ]
}, },
@ -1428,6 +1457,27 @@
"type": "github" "type": "github"
} }
}, },
"treefmt-nix_2": {
"inputs": {
"nixpkgs": [
"llm-agents",
"nixpkgs"
]
},
"locked": {
"lastModified": 1780220602,
"narHash": "sha256-eynAfOmbmxJnkp7YewvCEbShNnnYJ9gLLqkzsYtBPeM=",
"owner": "numtide",
"repo": "treefmt-nix",
"rev": "db947814a175b7ca6ded66e21383d938df01c227",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "treefmt-nix",
"type": "github"
}
},
"unionmount": { "unionmount": {
"flake": false, "flake": false,
"locked": { "locked": {
@ -1475,7 +1525,7 @@
"nix-serve-ng": "nix-serve-ng", "nix-serve-ng": "nix-serve-ng",
"nix-systems": "nix-systems", "nix-systems": "nix-systems",
"nixos-unified": "nixos-unified_3", "nixos-unified": "nixos-unified_3",
"nixpkgs": "nixpkgs_5", "nixpkgs": "nixpkgs_7",
"process-compose-flake": "process-compose-flake_2", "process-compose-flake": "process-compose-flake_2",
"record-hasfield": "record-hasfield", "record-hasfield": "record-hasfield",
"servant-event-stream": "servant-event-stream", "servant-event-stream": "servant-event-stream",
@ -1511,43 +1561,6 @@
"repo": "warp-tls-simple", "repo": "warp-tls-simple",
"type": "github" "type": "github"
} }
},
"zig2nix": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs_6"
},
"locked": {
"lastModified": 1764678235,
"narHash": "sha256-NNQWR3DAufaH7fs6ZplfAv1xPHEc0Ne3Z0v4MNHCqSw=",
"owner": "Cloudef",
"repo": "zig2nix",
"rev": "8b6ec85bccdf6b91ded19e9ef671205937e271e6",
"type": "github"
},
"original": {
"owner": "Cloudef",
"repo": "zig2nix",
"type": "github"
}
},
"zmx": {
"inputs": {
"zig2nix": "zig2nix"
},
"locked": {
"lastModified": 1776804427,
"narHash": "sha256-GMzvteQTfdLyy8ZvAKW1RkZQrNK2OJE/ZX6j89/Bzmc=",
"owner": "neurosnap",
"repo": "zmx",
"rev": "375046461dafb9ab2df1faf93cf35b25e37ed042",
"type": "github"
},
"original": {
"owner": "neurosnap",
"repo": "zmx",
"type": "github"
}
} }
}, },
"root": "root", "root": "root",

View file

@ -27,7 +27,6 @@
nixos-vscode-server.url = "github:nix-community/nixos-vscode-server"; nixos-vscode-server.url = "github:nix-community/nixos-vscode-server";
nix-index-database.url = "github:nix-community/nix-index-database"; nix-index-database.url = "github:nix-community/nix-index-database";
nix-index-database.inputs.nixpkgs.follows = "nixpkgs"; nix-index-database.inputs.nixpkgs.follows = "nixpkgs";
zmx.url = "github:neurosnap/zmx";
# vira.url = "github:juspay/vira/github"; # vira.url = "github:juspay/vira/github";
vira.url = "github:juspay/vira"; vira.url = "github:juspay/vira";
# landrun-nix.url = "github:srid/landrun-nix"; # landrun-nix.url = "github:srid/landrun-nix";
@ -35,17 +34,35 @@
jumphost-nix.url = "github:srid/jumphost-nix"; jumphost-nix.url = "github:srid/jumphost-nix";
jumphost-nix.flake = false; jumphost-nix.flake = false;
# KOLU # Pinned to PR #1652 (W1): https://github.com/juspay/kolu/pull/1652
kolu.url = "github:juspay/kolu/feat/minimap-hide-parked-toggle"; kolu.url = "github:juspay/kolu/W1";
# claude-code 2.1.98 (newer versions are nerfed) # drishti remote host monitor (home-manager module)
# See: https://x.com/Sthiven_R/status/2043992488109899849 drishti.url = "github:srid/drishti";
llm-agents.url = "github:numtide/llm-agents.nix/d9583b68fdc553936b35dc6ca206d8d8dd552e5b";
llm-agents.inputs.nixpkgs.follows = "nixpkgs";
# Neovim # Juspay's AI tooling repo. We consume only its opencode home-manager
nixvim.url = "github:nix-community/nixvim"; # module (config only, not the package) via homeModules.opencode.
nixvim.inputs.nixpkgs.follows = "nixpkgs"; juspay-ai.url = "github:juspay/AI";
juspay-ai.inputs.nixpkgs.follows = "nixpkgs";
juspay-ai.inputs.llm-agents.follows = "llm-agents";
# anywhen is NOT a flake input — it's deployed as an incus-pet
# container, with the flake ref passed at deploy time (see
# `just pureintent anywhen-deploy`). The host config doesn't import
# anything from anywhen, so locking it here would just bloat
# flake.lock without buying us anything.
project-unknown.url = "github:juspay/project-unknown";
project-unknown.inputs.nixpkgs.follows = "nixpkgs";
# Source for opencode (see modules/home/work/opencode.nix).
# NOTE: previously pinned to d9583b68 for claude-code 2.1.98 (newer
# versions are nerfed; https://x.com/Sthiven_R/status/2043992488109899849),
# but claude-code is no longer consumed from here (see
# modules/home/claude-code), so we now track latest.
llm-agents.url = "github:numtide/llm-agents.nix";
# Don't force nixpkgs.follows here: latest llm-agents needs a newer
# nixpkgs than ours (e.g. pnpm_11), so let it use its own pinned nixpkgs.
# Emanote & Imako # Emanote & Imako
emanote.url = "github:srid/emanote"; emanote.url = "github:srid/emanote";

View file

@ -1,6 +1,10 @@
default: default:
@just --list @just --list
# Per-host operational recipes. Namespaced via `mod` so each host's
# recipes live behind their own prefix (e.g. `just pureintent anywhen-deploy`).
mod pureintent 'configurations/nixos/pureintent/mod.just'
# Main commands # Main commands
# -------------------------------------------------------------------------------------------------- # --------------------------------------------------------------------------------------------------

View file

@ -0,0 +1,14 @@
# Expose the incus-pet CLI as a flake package.
#
# The CLI itself lives under modules/nixos/linux/incus/incus-pet/ so the
# whole incus/ tree stays a self-contained transplant unit (when it
# eventually lifts to its own repo, this flake-parts wiring stays
# behind).
{ ... }:
{
perSystem = { pkgs, ... }: {
packages.incus-pet = pkgs.callPackage
../nixos/linux/incus/incus-pet
{ };
};
}

View file

@ -16,7 +16,6 @@
"nix-darwin" "nix-darwin"
# "nixos-hardware" # "nixos-hardware"
"nix-index-database" "nix-index-database"
"nixvim"
]; ];
}; };
}; };

View file

@ -19,6 +19,15 @@
# CLAUDE_CODE_DISABLE_ADAPTIVE_THINKING = "1"; # CLAUDE_CODE_DISABLE_ADAPTIVE_THINKING = "1";
CLAUDE_CODE_DISABLE_AUTO_MEMORY = "1"; CLAUDE_CODE_DISABLE_AUTO_MEMORY = "1";
}; };
# Stop Claude from crawling /nix (the store is huge and ripgrep/find
# over it wedges sessions).
permissions.deny = [
"Bash(bfs /nix*)"
"Bash(grep * /nix*)"
"Bash(rg * /nix*)"
"Bash(find /nix*)"
"Bash(fd * /nix*)"
];
}; };
}; };
} }

View file

@ -17,29 +17,22 @@ in
gnumake gnumake
killall killall
television television
yt-dlp
gh gh
# Broken, https://github.com/NixOS/nixpkgs/issues/299680 # Broken, https://github.com/NixOS/nixpkgs/issues/299680
# ncdu # ncdu
# Useful for Nix development # Useful for Nix development
ci ci
omnix
nixpkgs-fmt nixpkgs-fmt
just just
watchexec watchexec
fswatch fswatch
eternal-terminal eternal-terminal
lima
# AI
google-cloud-sdk
# Publishing # Publishing
asciinema asciinema
ispell ispell
pandoc
# Dev # Dev
fuckport fuckport
@ -56,7 +49,8 @@ in
hledger hledger
gnupg gnupg
compress-video # Temporarily disabled: pulls in ffmpeg-full which fails to build (kvazaar test failures)
# compress-video
]; ];
fonts.fontconfig.enable = true; fonts.fontconfig.enable = true;

View file

@ -1,8 +1,15 @@
{ {
home.stateVersion = "24.05"; home.stateVersion = "24.05";
home.sessionVariables = { home.sessionVariables = {
DO_NOT_TRACK = "1"; # Disabled, see
# https://github.com/anthropics/claude-code/issues/69358#issuecomment-4755677033
# DO_NOT_TRACK = "1";
}; };
# Home-manager generates an options manpage (`home-configuration.nix(5)`) by
# default, which evaluates the doc string of every HM option. Options are
# searched online, not via `man`, so skip it — measurable eval-time win.
manual.manpages.enable = false;
imports = [ imports = [
./cli/tmux.nix ./cli/tmux.nix
./editors/neovim ./editors/neovim

View file

@ -1,127 +1,11 @@
{ flake, pkgs, ... }: { ... }:
let
inherit (flake) inputs;
in
{ {
imports = [ programs.neovim = {
inputs.nixvim.homeModules.nixvim
];
programs.nixvim = {
enable = true; enable = true;
defaultEditor = true; defaultEditor = true;
vimAlias = true;
imports = [ viAlias = true;
./nvim-tree.nix withRuby = false;
./lazygit.nix withPython3 = false;
];
# Theme
colorschemes.rose-pine.enable = true;
# Settings
opts = {
expandtab = true;
shiftwidth = 2;
smartindent = true;
tabstop = 2;
number = true;
clipboard = "unnamedplus";
termguicolors = true;
};
# Keymaps
globals = {
mapleader = " ";
};
extraPlugins = [
pkgs.vimPlugins.outline-nvim
];
plugins = {
# UI
web-devicons.enable = true;
lualine.enable = true;
bufferline.enable = true;
treesitter = {
enable = true;
};
haskell-scope-highlighting.enable = true;
which-key = {
enable = true;
};
noice = {
# WARNING: This is considered experimental feature, but provides nice UX
enable = true;
settings.presets = {
bottom_search = true;
command_palette = true;
long_message_to_split = true;
#inc_rename = false;
#lsp_doc_border = false;
};
};
telescope = {
enable = true;
keymaps = {
"<leader>ff" = {
options.desc = "file finder";
action = "find_files";
};
"<leader>fr" = {
options.desc = "recent files";
action = "oldfiles";
};
"<leader>fg" = {
options.desc = "find via grep";
action = "live_grep";
};
"<leader>T" = {
options.desc = "switch colorscheme";
action = "colorscheme";
};
};
extensions = {
file-browser.enable = true;
ui-select.enable = true;
frecency.enable = true;
fzf-native.enable = true;
};
};
# LSP
# https://github.com/nix-community/nixvim/blob/main/plugins/lsp/default.nix
lsp = {
enable = true;
keymaps = {
lspBuf = {
"gd" = "definition";
"gD" = "references";
"gt" = "type_definition";
"gi" = "implementation";
"K" = "hover";
"<leader>A" = "code_action";
};
diagnostic = {
"<leader>k" = "goto_prev";
"<leader>j" = "goto_next";
};
};
servers = {
hls = {
enable = true;
installGhc = false;
};
marksman.enable = true;
nil_ls.enable = true;
rust_analyzer = {
# enable = true;
installCargo = false;
installRustc = false;
};
};
};
};
}; };
} }

View file

@ -1,10 +0,0 @@
{
plugins.lazygit.enable = true;
keymaps = [
{
action = "<cmd>LazyGit<CR>";
key = "<leader>gg";
}
];
}

View file

@ -1,13 +0,0 @@
{
plugins.nvim-tree.enable = true;
keymaps = [
{
action = "<cmd>NvimTreeFindFileToggle<CR>";
key = "<leader>tt";
}
{
action = "<cmd>NvimTreeFindFile<CR>";
key = "<leader>tf";
}
];
}

View file

@ -0,0 +1,19 @@
{ flake, pkgs, ... }:
let
inherit (flake) inputs;
in
{
imports = [
inputs.drishti.homeManagerModules.default
];
# Generic enabler. The host list is machine-specific (zest watches the
# tailnet boxes; pureintent watches its pu-managed CI fleet), so each
# consuming config sets `services.drishti.hosts`.
services.drishti = {
enable = true;
package = inputs.drishti.packages.${pkgs.stdenv.hostPlatform.system}.default;
port = 7720;
};
}

View file

@ -15,14 +15,14 @@ in
]; ];
services.emanote = { services.emanote = {
enable = true; enable = false;
notes = [ myVault ]; notes = [ myVault ];
port = 7001; port = 7001;
package = inputs.emanote.packages.${pkgs.stdenv.hostPlatform.system}.default; package = inputs.emanote.packages.${pkgs.stdenv.hostPlatform.system}.default;
}; };
services.imako = { services.imako = {
enable = true; enable = false;
package = inputs.imako.packages.${pkgs.stdenv.hostPlatform.system}.default; package = inputs.imako.packages.${pkgs.stdenv.hostPlatform.system}.default;
vaultDir = myVault; vaultDir = myVault;
port = 7002; port = 7002;

View file

@ -15,7 +15,7 @@ in
programs.jumphost = { programs.jumphost = {
enable = true; enable = true;
host = "nix-infra@dosa.tail12b27.ts.net"; host = "srid@vanjaram.tail12b27.ts.net";
sshHosts = { sshHosts = {
"ssh.bitbucket.juspay.net".user = "git"; "ssh.bitbucket.juspay.net".user = "git";

View file

@ -0,0 +1,44 @@
# Juspay's opencode *configuration* (not the package), via the home-manager
# module exposed upstream by juspay/AI (homeModules.opencode).
#
# The config points opencode at Juspay's LLM gateway and authenticates with
# JUSPAY_API_KEY, which we source from the agenix-managed secret and export
# into interactive shells below.
{ flake, pkgs, config, ... }:
let
homeMod = flake.inputs.self + /modules/home;
in
{
imports = [
flake.inputs.juspay-ai.homeModules.opencode
"${homeMod}/agenix.nix"
];
programs.opencode-juspay.enable = true;
# YOLO mode: auto-approve all permission prompts in the TUI (opencode has no
# --dangerously-skip-permissions flag for the TUI, only for `run`; see
# https://github.com/anomalyco/opencode/issues/8463). Explicit "deny" rules
# would still win.
programs.opencode-juspay.settings.permission = "allow";
# The upstream module is config-only by design; install the binary from
# llm-agents (numtide/llm-agents.nix) so the Juspay config above is usable
# out of the box.
home.packages = [
flake.inputs.llm-agents.packages.${pkgs.stdenv.hostPlatform.system}.opencode
];
# The Juspay litellm provider authenticates with JUSPAY_API_KEY at runtime.
# Decrypt the secret via agenix and export it into interactive shells.
age.secrets.juspay-anthropic-api-key.file =
flake.inputs.self + /secrets/juspay-anthropic-api-key.age;
programs.zsh.initContent = ''
export JUSPAY_API_KEY="$(cat "${config.age.secrets.juspay-anthropic-api-key.path}")"
'';
programs.bash.initExtra = ''
export JUSPAY_API_KEY="$(cat "${config.age.secrets.juspay-anthropic-api-key.path}")"
'';
}

View file

@ -0,0 +1,80 @@
# Container-side essentials for incus-pet — the OTHER half of this tree.
#
# default.nix configures the incus daemon ON THE HOST (bridge, UI, etc).
# THIS module is imported INSIDE each containerized app's nixosConfiguration,
# alongside the app's own nixosModules.incus. It declares the contract
# options that every app's incus module sets, and the boring NixOS-inside-
# a-container plumbing (sshd, base packages, firewall).
#
# The incus-pet CLI's marshaling flake imports this file directly by store
# path — no flake-input dance — so this file stays as a plain NixOS module.
{ config, lib, pkgs, modulesPath, ... }:
let
cfg = config.incus.container;
in
{
# `lxc-container.nix` is the upstream nixpkgs module that turns a
# NixOS evaluation into something that boots as an LXC/incus container
# — skips the boot loader, sets `fileSystems` defaults, wires up
# systemd-networkd with eth0 DHCP, and trims a pile of host-only
# services. The official `images:nixos/25.11` image imports the same
# module via its /etc/nixos/configuration.nix; we re-import here so
# `nixos-rebuild --target-host` doesn't lose any of that when it
# replaces /etc/nixos at switch time.
imports = [ "${modulesPath}/virtualisation/lxc-container.nix" ];
# The CONVENTION: every containerized service binds 8080 inside its
# own netns. The host-side incus proxy device translates a unique
# <listen-ip>:<host-port> to <container>:8080 at deploy time. App
# modules just hardcode `services.<app>.port = 8080;` — no shared
# option, no forward reference, no coupling between an app's flake
# and this tree at evaluation time.
options.incus.container = {
enable = lib.mkEnableOption "incus-pet container essentials";
hostname = lib.mkOption {
type = lib.types.str;
description = ''
Container hostname. Each app's nixosModules.incus sets this
with mkDefault; the operator can override per deploy if they
want a different name than the app's default.
'';
};
};
config = lib.mkIf cfg.enable {
networking.hostName = cfg.hostname;
# NixOS-inside-a-container essentials. The community NixOS incus
# image ships with neither sshd nor flakes; the incus-pet CLI does
# a one-time bootstrap via `incus exec` to push the operator pubkey
# and apply a temporary config that enables both. From the FIRST
# `nixos-rebuild --target-host` onwards, the deployed config (this
# module) carries those settings forward.
services.openssh = {
enable = true;
settings.PermitRootLogin = "yes";
};
nix.settings.experimental-features = [ "nix-command" "flakes" ];
# The hardcoded service port — opening it here too is cheap
# insurance against a mis-authored app module forgetting to.
networking.firewall.allowedTCPPorts = [ 8080 ];
environment.systemPackages = with pkgs; [
git
vim
curl
htop
];
# Workaround the same dbus-broker reload stall pureintent hits
# (NixOS#... — the broker has long-lived clients holding the bus,
# the reload step times out, switch-to-configuration exits 4 even
# though activation succeeded). Skip reload/restart at activation
# time; bus policy changes land on next container restart.
systemd.services.dbus-broker.reloadIfChanged = lib.mkForce false;
systemd.services.dbus-broker.restartIfChanged = lib.mkForce false;
};
}

View file

@ -0,0 +1,197 @@
# incus-pet
Per-app incus container CLI ("pet PaaS"). Takes a NixOS service flake,
materialises it into a dedicated incus container on the local host, and
exposes the service on a chosen `<listen-ip>:<host-port>` that proxies
to a fixed `8080` inside the container.
```
incus-pet deploy github:srid/anywhen --port 7700 --listen 100.122.32.106
incus-pet list
incus-pet rm anywhen
```
See `SKILL.md` for the agent-facing recipe; this README is for humans
running the CLI directly.
## Prerequisites
- The host runs the incus daemon (this repo's `modules/nixos/linux/incus`
enables it).
- The operator's user is in the `incus-admin` group.
- An SSH key exists at `~/.ssh/id_ed25519.pub` (or `~/.ssh/id_rsa.pub`)
that root inside the container will trust.
- The target flake ships `nixosModules.incus` (see `SKILL.md` Branch C
for what that means, or the anywhen flake for a worked example).
## The deploy unit — what a flake must ship
```nix
# in the app's flake.nix outputs
nixosModules.incus = { config, lib, pkgs, ... }: {
imports = [ self.nixosModules.default ];
incus.container = {
enable = true;
hostname = lib.mkDefault "anywhen";
};
system.stateVersion = "25.05";
services.anywhen = {
enable = true;
package = lib.mkDefault self.packages.${pkgs.stdenv.hostPlatform.system}.default;
host = lib.mkDefault "0.0.0.0";
port = 8080; # the incus-pet contract — fixed
};
};
```
The CLI's marshaling flake imports this alongside `container.nix` (the
in-tree essentials) and runs `nixos-rebuild switch --target-host`
against the container.
## Network model — fixed inside, unique outside
This is the section worth reading twice.
### Inside the container
Every containerized service binds **the same port**: `8080`. This is a
convention, declared as a `readOnly` option (`incus.container.servicePort`)
in `container.nix`. App authors do not pick a port — they wire
`services.<app>.port = config.incus.container.servicePort` and move on.
Cross-app port collisions become impossible because there is only one
port to collide with, and each container has its own network namespace.
### Outside the container (the host)
The host runs an **incus proxy device** per container that translates a
unique `<listen-ip>:<host-port>` on the host into a connection to the
container's `127.0.0.1:8080`:
```
incus config device add <name> web proxy \
listen=tcp:<listen-ip>:<host-port> \
connect=tcp:127.0.0.1:8080
```
`incus-pet deploy` wires this for you idempotently. The `<host-port>`
and `<listen-ip>` are stored in container metadata
(`user.incus-pet.host-port`, `user.incus-pet.listen`) so re-running
`incus-pet deploy <flake-ref>` doesn't need the flags repeated.
### Picking a listen IP
`<listen-ip>` can be any address bound on the host. The three meaningful
choices:
| Listen IP | Reach | Firewall edit needed? |
|------------------|----------------------|---------------------------------------------|
| `0.0.0.0` | Anyone who can route to this host's NIC | Yes — open `<host-port>` in `networking.firewall.allowedTCPPorts` |
| Host's LAN IP | Same as above, scoped to that NIC | Yes — same as above |
| Host's tailnet IP (e.g. `100.122.32.106`) | Tailnet only | **No**`tailscale0` is in `networking.firewall.trustedInterfaces` (see `configurations/nixos/pureintent/default.nix:78`), so traffic on that interface bypasses the host firewall |
For "internal apps on my box, reachable only from my tailnet" (the
common case), pick the tailscale IP. On a host where you always want
that, export the IP once:
```sh
# in your shell profile, or via environment.variables in the host config
export INCUS_PET_LISTEN=100.122.32.106
```
…and every `incus-pet deploy` binds there automatically.
### What the container's own firewall does
`container.nix` opens `8080` inside the container, so the proxy device's
`connect=tcp:127.0.0.1:8080` reaches the service. The container has its
own veth on `incusbr0`; nothing on the host's interfaces is involved
until traffic hits the proxy device on `<listen-ip>:<host-port>`.
## Operator flow
### First deploy of an app
```sh
incus-pet deploy github:srid/anywhen --port 7700 --listen 100.122.32.106
```
This:
1. Synthesises a marshaling flake under `~/.local/state/incus-pet/anywhen/`
exposing `nixosModules.default` (essentials + app's
`nixosModules.incus` + operator pubkey overlay) — no nixpkgs pin in
per-deploy state.
2. Launches `images:nixos/25.11` as container `anywhen` with
`-c security.nesting=true`.
3. Pushes your `~/.ssh/id_ed25519.pub` into the container's
`/root/.ssh/authorized_keys` (the image already runs sshd via systemd
socket activation).
4. Builds the system toplevel against `github:nixos/nixpkgs/nixos-25.11`
(stable, matching the image baseline) via `nix build --impure --expr`.
5. Copies the closure with `nix copy --to ssh://root@<container-ip>
--substitute-on-destination`.
6. SSHes to set the system profile and run `switch-to-configuration switch`.
7. Records `--port` and `--listen` in container metadata.
8. Adds the `web` proxy device.
### Subsequent deploys
```sh
incus-pet deploy github:srid/anywhen # picks up new commits on the branch
```
No flags needed — `--port` and `--listen` are read from metadata.
### Listing what's running
```sh
incus-pet list
```
Filters `incus list` to only containers tagged with
`user.incus-pet.flake-ref`.
### Removing
```sh
incus-pet rm anywhen
```
Stops and deletes the container, removes the marshaling flake from
`~/.local/state/incus-pet/anywhen/`. Container metadata goes with the
container.
## Failure modes
- **`incus not in PATH`** — the host needs the incus daemon installed
and the operator's user in `incus-admin` (log out and back in after
the group is added).
- **`first deploy of <name> needs --port N`** — pass `--port`; it will
be recorded in container metadata for next time.
- **`container did not get an IPv4 address within 30s`** — usually
means `incusbr0` didn't come up. Check `incus network list` and
`systemctl status incus-preseed`.
- **`Failed to start Firewall.` inside the container** — the launch
flag `-c security.nesting=true` is supposed to handle this; if it
recurs, see `../README.md` and lxc/incus#526.
- **`switch-to-configuration` exits 4** — "some units failed to
reload/restart". Treated as success: the activation itself
completed, only a unit reload (typically dbus-broker) failed. The
service is up.
- **`switch-to-configuration` exits 11 / "Could not acquire lock"** —
a previous activation is still running (often hung). `incus exec
<name> -- pkill -9 -f switch-to-configuration` clears it.
## State
| What | Where |
|-------------------------------|-----------------------------------------------------------------------|
| Marshaling flake + lock | `~/.local/state/incus-pet/<name>/{flake.nix, flake.lock}` |
| Operator-chosen host port | `incus config get <name> user.incus-pet.host-port` |
| Operator-chosen listen IP | `incus config get <name> user.incus-pet.listen` |
| Source flake ref | `incus config get <name> user.incus-pet.flake-ref` |
| Service data | Inside the container, per the app's `services.<app>.stateDir` |
The marshaling flake carries no app-specific values — only the
assembly. App config lives entirely in the upstream flake's
`nixosModules.incus`.

View file

@ -0,0 +1,140 @@
---
name: incus-pet
description: Deploy a NixOS service flake into a per-app incus container
("pet PaaS") on the local host — OR add the required nixosModules.incus
contract to a flake so it becomes deployable. Use when the user says
"deploy X", "make this app deployable", "add incus to this flake", or
"publish this on my box".
argument-hint: "deploy <flake-ref> [<name>] [--port N] [--listen IP]"
---
# incus-pet
A per-app incus container deploys a flake-shipped NixOS service onto the
local host, exposed on a chosen `<listen-ip>:<host-port>` that proxies to
a fixed `8080` inside the container.
Pick the branch from context:
- User in their OWN app's repo, "make this deployable" → **Branch C**
- User wants to add the contract to an UPSTREAM they can PR to
(e.g. juspay/kolu) — agent has push/PR access → **Branch C**
- Flake-ref already ships `nixosModules.incus` → **Branch A**
- Upstream won't accept a PR and user explicitly wants a local
wrapper → **Branch B**
## Branch A — deploy a flake that ships the contract
Run:
```
incus-pet deploy <flake-ref> [<name>] [--port N] [--listen IP]
```
`--port` is required on the first deploy of a given `<name>` and recorded
in container metadata; subsequent deploys read it back. Report the
`<listen>:<port>` URL to the user.
## Branch B — local stand-alone wrapper (RARE; Branch C is preferred)
Only when upstream won't accept the contract. Create a tiny wrapper flake
locally:
- `flake.nix` exposing `nixosModules.incus` per the contract
- `service-module.nix` authoring `services.<app>` as a NixOS system
module (mirror the upstream's home-manager module if any)
- Add the upstream as a flake input
Then `incus-pet deploy ./path/to/wrapper <app>`.
## Branch C — add the contract to a flake (yours or upstream-via-PR)
Steps:
1. Verify the flake exposes `packages.<system>.default`. If not, stop —
prerequisite is a package derivation with `meta.mainProgram`.
2. Verify `nixosModules.default` (the service module). If MISSING but the
flake has `homeManagerModules.default` (kolu case):
- Author `nix/nixos/module.nix` as a system NixOS module mirroring the
home-manager surface — same options (`enable`, `package`, `host`,
`port`, ...), emitting `systemd.services.<app>` instead of a
home-manager unit.
- Expose as `nixosModules.default` in flake outputs.
If MISSING entirely (no module at all), stop — prerequisite.
3. Add `nixosModules.incus` to outputs (fill in `<app>`):
```nix
nixosModules.incus = { config, lib, pkgs, ... }: {
imports = [ self.nixosModules.default ];
incus.container = {
enable = true;
hostname = lib.mkDefault "<app>";
};
system.stateVersion = "25.05";
services.<app> = {
enable = true;
package = lib.mkDefault
self.packages.${pkgs.stdenv.hostPlatform.system}.default;
host = lib.mkDefault "0.0.0.0";
port = 8080; # fixed by the incus-pet contract
};
};
```
4. If `services.<app>` doesn't expose `{host, port, package}` (or
equivalents), surface as prerequisite. Do not invent options that
aren't there — add them to the service module first.
5. Smoke-test:
```
nix flake check
nix eval .#nixosModules.incus
```
6. If upstream (PR mode), open the PR. Else commit.
7. Tell the user how to deploy:
```
incus-pet deploy github:<owner>/<repo> --port <chosen> --listen <ip>
```
Do NOT add `example/`-style sub-flakes or VM tests in this branch —
separate scope.
## Contract for `nixosModules.incus`
The module must set:
- `incus.container.enable = true`
- `incus.container.hostname = "<app>"` (`mkDefault` ok)
- `system.stateVersion = "25.05"`
- `services.<app>.{enable, package, host, port}` (`port = 8080`,
hardcoded — this is the incus-pet convention)
The module must NOT set:
- `services.<app>.port` to anything other than `8080`
- `networking.firewall.allowedTCPPorts``container.nix` opens 8080
## Idempotence + failure modes
- Re-running `incus-pet deploy <flake-ref>` against an existing container
is safe: marshaling flake is rewritten and re-locked; container
bootstrap is idempotent; proxy device is `set` if present, else `add`.
- Container metadata (`user.incus-pet.{host-port,listen,flake-ref}`) is
the source of truth for the operator-chosen exposure. `incus-pet list`
filters by the `flake-ref` key.
- If `--port` is missing on the first deploy, the command fails before
launching anything.
- Activation is two ssh steps: `nix-env --set` (atomic pointer swap on
`/nix/var/nix/profiles/system`) then `switch-to-configuration switch`.
If the second fails partway, the system profile is already at the new
toplevel; running deploy again finishes the switch.
- Exit 4 from `switch-to-configuration` is treated as success (a unit
failed to reload — typically dbus-broker — but activation itself
completed).

View file

@ -0,0 +1,423 @@
# incus-pet — per-app incus container CLI ("pet PaaS").
#
# Subcommands: deploy, list, rm.
#
# Container port is fixed at 8080 (declared by ../container.nix); the
# host-side <listen-ip>:<host-port> is unique per app and stored in
# container metadata so re-deploys are flagless. See ./README.md for
# the network model and operator flow.
#
# Marshaling-flake-as-module design (vs. nixos-rebuild --target-host):
# the per-deploy state-dir flake exposes ONLY `nixosModules.default`
# (combined container essentials + app's nixosModules.incus +
# operator-key overlay). The CLI picks nixpkgs at build time (pinned
# to nixos-25.11, matching the LXC image baseline) and assembles the
# nixosSystem via `nix build --impure --expr`. Closure copy + activation
# happen via `nix copy --to ssh://…` + ssh-driven
# switch-to-configuration. Lets us dodge unstable nixpkgs regressions
# (e.g. the dbus-broker reload stall) without touching the marshaling
# flake's shape.
{ writeShellApplication
, nix
, openssh
, jq
, coreutils
, gnused
, ...
}:
let
# The container essentials module — baked in at build time as a store
# path. The marshaling flake imports it directly so we don't have to
# turn the incus/ tree into a flake.
essentialsModule = ../container.nix;
in
writeShellApplication {
name = "incus-pet";
runtimeInputs = [ nix openssh jq coreutils gnused ];
meta.description = "Deploy a NixOS service flake into a per-app incus container (pet PaaS).";
text = ''
set -euo pipefail
ESSENTIALS_MODULE='${essentialsModule}'
CONTAINER_PORT=8080
STATE_ROOT="''${XDG_STATE_HOME:-$HOME/.local/state}/incus-pet"
INCUS_IMAGE="images:nixos/25.11"
# Pinned to stable to match the LXC image baseline. Unstable triggers
# a dbus-broker reload stall in switch-to-configuration (~Jul 2026
# systemd/dbus-broker regression).
NIXPKGS_REF="github:nixos/nixpkgs/nixos-25.11"
log() { printf '[incus-pet] %s\n' "$*" >&2; }
die() { log "error: $*"; exit 1; }
usage() {
cat >&2 <<'EOF'
Usage:
incus-pet deploy <flake-ref> [<name>] [--port N] [--listen IP]
incus-pet list
incus-pet rm <name>
Environment:
INCUS_PET_LISTEN default listen IP for the host-side proxy device
(overridden by --listen)
First-time `deploy` requires --port; subsequent deploys read it from
container metadata. See README.md for the network model.
EOF
exit 2
}
# ----- helpers -----
ensure_incus() {
command -v incus >/dev/null 2>&1 || die "incus not in PATH (host needs the incus daemon)"
}
operator_pubkey() {
# The key root@<container> will trust. Try ed25519 then rsa.
local f
for f in "$HOME/.ssh/id_ed25519.pub" "$HOME/.ssh/id_rsa.pub"; do
if [ -r "$f" ]; then cat "$f"; return; fi
done
die "no ssh public key found at ~/.ssh/id_ed25519.pub or ~/.ssh/id_rsa.pub"
}
container_exists() {
incus info "$1" >/dev/null 2>&1
}
container_ipv4() {
# Poll briefly — fresh containers take a second to get DHCP.
local name="$1" tries=0 ip=""
while [ "$tries" -lt 30 ]; do
ip=$(incus list "$name" --format=json 2>/dev/null \
| jq -r '.[0].state.network.eth0.addresses[]? | select(.family=="inet") | .address' \
| head -n1 || true)
if [ -n "$ip" ] && [ "$ip" != "null" ]; then
printf '%s\n' "$ip"; return 0
fi
sleep 1
tries=$((tries + 1))
done
die "container $name did not get an IPv4 address within 30s"
}
config_get() {
# Echo empty string if the key is unset (incus returns empty + 0).
incus config get "$1" "$2" 2>/dev/null || true
}
write_marshaling_flake() {
# Per-deploy state-dir flake. Exposes ONLY nixosModules.default —
# the CLI assembles the full nixosSystem at build time, picking
# nixpkgs (so we can pin stable to dodge unstable regressions and
# not bake the choice into per-deploy state).
local flake_ref="$1" pubkey="$2"
local dir="$STATE_ROOT/$3" # caller-chosen subdir (probe or real name)
mkdir -p "$dir"
# Copy the essentials module into the marshaling flake dir as
# ./container.nix and import it via relative path — keeps the
# generated flake pure-evaluable (pure-eval forbids absolute
# /nix/store imports not declared as flake inputs).
cp -f "$ESSENTIALS_MODULE" "$dir/container.nix"
cat > "$dir/flake.nix" <<EOF
# Generated by incus-pet — do not edit. Re-run \`incus-pet deploy\` to refresh.
{
inputs.target.url = "$flake_ref";
outputs = { target, ... }: {
nixosModules.default = { ... }: {
imports = [
./container.nix
target.nixosModules.incus
];
users.users.root.openssh.authorizedKeys.keys = [
"$pubkey"
];
};
};
}
EOF
printf '%s\n' "$dir"
}
precheck_target_flake() {
local flake_ref="$1"
if ! nix eval "$flake_ref#nixosModules.incus" --apply 'm: true' >/dev/null 2>&1; then
die "flake $flake_ref does not expose nixosModules.incus; see SKILL.md Branch C for how to add the contract"
fi
}
# Boilerplate that assembles the full nixosSystem on demand. Pinned
# nixpkgs (NIXPKGS_REF) is fetched via builtins.getFlake; --impure
# is required because builtins.getFlake on a URL isn't
# pure-evaluable. The marshaling flake is loaded by absolute store
# path so the operator's machine doesn't re-fetch it.
sys_expr() {
local dir="$1"
printf 'let
marshaling = builtins.getFlake "path:%s";
nixpkgs = builtins.getFlake "%s";
in nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [ marshaling.nixosModules.default ];
}' "$dir" "$NIXPKGS_REF"
}
eval_config_attr() {
local dir="$1" attr="$2"
nix eval --raw --impure --expr "($(sys_expr "$dir")).config.$attr"
}
build_toplevel() {
local dir="$1"
nix build --print-out-paths --no-link --impure --expr \
"($(sys_expr "$dir")).config.system.build.toplevel"
}
bootstrap_container() {
# The official NixOS incus image already runs sshd via systemd
# socket activation on port 22; the only thing we need to add is
# the operator's pubkey to root's authorized_keys (for interactive
# ssh debugging — the deploy path uses `incus exec`, not ssh).
# Idempotent — `tee` overwrites, so a rotated key gets picked up
# on next deploy.
local name="$1" pubkey="$2"
log "ensuring authorized_keys on $name"
incus exec "$name" -- mkdir -p /root/.ssh
incus exec "$name" -- chmod 700 /root/.ssh
printf '%s\n' "$pubkey" | incus exec "$name" -- tee /root/.ssh/authorized_keys >/dev/null
incus exec "$name" -- chmod 600 /root/.ssh/authorized_keys
}
# Mount the host's /nix/store into the container as read-only —
# `shift=true` idmaps host UIDs into the unprivileged container.
# Idempotent; takes effect on next container start. With this in
# place, every container shares the host's /nix/store on disk —
# zero duplication of nixpkgs closures across containers.
ensure_nix_store_mount() {
local name="$1"
if incus config device show "$name" 2>/dev/null | grep -q '^nix-store:'; then
return 0
fi
log "adding shared /nix/store mount on $name (host's store, read-only)"
incus config device add "$name" nix-store disk \
source=/nix/store \
path=/nix/store \
readonly=true \
shift=true
}
# Pin the toplevel against host-side GC. The container's nix DB
# doesn't know about the path (it lives behind the shared mount),
# so without this the host could nix-collect-garbage the running
# system out from under the container. `nix-store --add-root
# --indirect` creates an indirect root: a user-writable symlink
# (in our state dir, no sudo) plus a tracked entry in
# /nix/var/nix/gcroots/auto/. Idempotent; overwrites prior.
register_host_gcroot() {
local name="$1" toplevel="$2"
local link="$STATE_ROOT/$name/system"
mkdir -p "$STATE_ROOT/$name"
nix-store --add-root "$link" --indirect --realise "$toplevel" >/dev/null
}
wire_proxy_device() {
local name="$1" listen="$2" host_port="$3"
local devname="web"
if incus config device show "$name" 2>/dev/null | grep -q "^$devname:"; then
log "refreshing proxy device $devname on $name $listen:$host_port"
incus config device set "$name" "$devname" \
listen="tcp:$listen:$host_port" \
connect="tcp:127.0.0.1:$CONTAINER_PORT"
else
log "adding proxy device $devname on $name $listen:$host_port"
incus config device add "$name" "$devname" proxy \
listen="tcp:$listen:$host_port" \
connect="tcp:127.0.0.1:$CONTAINER_PORT"
fi
}
# ----- subcommands -----
cmd_deploy() {
local flake_ref="" name="" port="" listen=""
[ $# -ge 1 ] || usage
flake_ref="$1"; shift
# Optional positional name (anything that doesn't start with '-').
if [ $# -ge 1 ] && [ "''${1#-}" = "$1" ]; then
name="$1"; shift
fi
while [ $# -gt 0 ]; do
case "$1" in
--port) port="$2"; shift 2 ;;
--listen) listen="$2"; shift 2 ;;
*) die "unknown flag: $1" ;;
esac
done
ensure_incus
# Normalize `.` to an absolute path so nix flake commands work
# from anywhere downstream.
if [ "$flake_ref" = "." ]; then
flake_ref="path:$(pwd)"
fi
precheck_target_flake "$flake_ref"
local pubkey
pubkey=$(operator_pubkey)
# We need the hostname to know the container name, but the hostname
# lives in the evaluated config — which means we need to write the
# marshaling flake first. If --name wasn't passed, use a temporary
# placeholder; we'll re-evaluate after.
local tentative_name="''${name:-_probe}"
local dir
dir=$(write_marshaling_flake "$flake_ref" "$pubkey" "$tentative_name")
log "locking marshaling flake at $dir"
# `nix flake update` (not `lock`) so every deploy bumps the target
# input to the current tip of its ref (otherwise re-running `deploy
# github:srid/anywhen` would stay pinned at the first commit we
# saw). `--refresh` bypasses nix's fetcher cache so the resolution
# is fresh, not a 5-min-stale entry.
( cd "$dir" && nix flake update --refresh )
if [ -z "$name" ]; then
name=$(eval_config_attr "$dir" "incus.container.hostname")
log "container name from flake: $name"
# Re-emit the marshaling flake under the real name so the
# state-dir matches what `incus-pet list` shows.
rm -rf "$STATE_ROOT/_probe"
dir=$(write_marshaling_flake "$flake_ref" "$pubkey" "$name")
# `nix flake update` (not `lock`) so every deploy bumps the target
# input to the current tip of its ref (otherwise re-running `deploy
# github:srid/anywhen` would stay pinned at the first commit we
# saw). `--refresh` bypasses nix's fetcher cache so the resolution
# is fresh, not a 5-min-stale entry.
( cd "$dir" && nix flake update --refresh )
fi
# Resolve host port (CLI flag > container metadata > error).
if [ -z "$port" ]; then
if container_exists "$name"; then
port=$(config_get "$name" user.incus-pet.host-port)
fi
[ -n "$port" ] || die "first deploy of $name needs --port N (host-side port)"
fi
# Resolve listen IP (CLI flag > env > container metadata > 0.0.0.0).
if [ -z "$listen" ]; then
listen="''${INCUS_PET_LISTEN:-}"
fi
if [ -z "$listen" ] && container_exists "$name"; then
listen=$(config_get "$name" user.incus-pet.listen)
fi
: "''${listen:=0.0.0.0}"
# Ensure container exists.
if ! container_exists "$name"; then
log "launching container $name from $INCUS_IMAGE"
incus launch "$INCUS_IMAGE" "$name" -c security.nesting=true
fi
local ip
ip=$(container_ipv4 "$name")
log "container $name has ipv4 $ip"
bootstrap_container "$name" "$pubkey"
log "building toplevel (pinned $NIXPKGS_REF)"
local toplevel
toplevel=$(build_toplevel "$dir")
log "toplevel: $toplevel"
log "pinning host gcroot for $name $toplevel"
register_host_gcroot "$name" "$toplevel"
# Update the container's system profile FIRST (while it can still
# see its own /nix/store), then add the shared-mount device if
# missing, then restart so the new init picks up via the shared
# mount. Order matters: if we added the mount before updating
# the profile, the container's existing /sbin/init -> old toplevel
# would point at a path masked by the mount and the restart would
# fail.
log "setting system profile on $name $toplevel"
incus exec "$name" -- ln -sfn "$toplevel" /nix/var/nix/profiles/system
# The NixOS LXC image hard-links /sbin/init to the image's own
# toplevel; re-pointing it through the profile symlink is what
# makes "update profile + restart" actually pick up the new
# toplevel's init (which sets up /etc, runs activation, then
# execs systemd). NixOS's switch-to-configuration normally does
# this — we have to do it ourselves since we skip s-t-c.
# Idempotent across deploys.
incus exec "$name" -- ln -sfn /nix/var/nix/profiles/system/init /sbin/init
ensure_nix_store_mount "$name"
log "restarting $name to boot via shared /nix/store"
incus restart "$name"
ip=$(container_ipv4 "$name")
log "$name back up at $ip"
# Persist host-port + listen IP for next deploy. The `key=value`
# form is required since incus 6.x; the space-separated form is
# deprecated and warns.
incus config set "$name" "user.incus-pet.host-port=$port"
incus config set "$name" "user.incus-pet.listen=$listen"
incus config set "$name" "user.incus-pet.flake-ref=$flake_ref"
wire_proxy_device "$name" "$listen" "$port"
log "done: $name exposed on $listen:$port container:8080"
}
cmd_list() {
ensure_incus
printf '%-20s %-50s %-25s %-10s\n' "NAME" "FLAKE" "EXPOSED" "STATUS"
incus list --format=json | jq -r '.[] | select(.config["user.incus-pet.flake-ref"]) |
[
.name,
.config["user.incus-pet.flake-ref"],
(.config["user.incus-pet.listen"] + ":" + .config["user.incus-pet.host-port"]),
.status
] | @tsv' | while IFS=$'\t' read -r n f e s; do
printf '%-20s %-50s %-25s %-10s\n' "$n" "$f" "$e" "$s"
done
}
cmd_rm() {
ensure_incus
[ $# -eq 1 ] || die "rm takes exactly one argument: the container name"
local name="$1"
log "stopping + deleting container $name"
incus stop "$name" --force 2>/dev/null || true
incus delete "$name" 2>/dev/null || true
rm -rf "''${STATE_ROOT:?}/$name"
log "removed $name"
}
# ----- dispatch -----
[ $# -ge 1 ] || usage
sub="$1"; shift
case "$sub" in
deploy) cmd_deploy "$@" ;;
list) cmd_list "$@" ;;
rm) cmd_rm "$@" ;;
-h|--help) usage ;;
*) usage ;;
esac
'';
}

View file

@ -0,0 +1,40 @@
# hello-web — incus-pet reference example
Smallest possible flake that satisfies the [incus-pet contract](../../SKILL.md).
A static HTTP server (`darkhttpd` wrapping a one-page `index.html`) packaged
through the full stack:
- `packages.<sys>.default` — the binary
- `nixosModules.default``services.hello-web.{enable, package, host, port}` + a systemd unit
- `nixosModules.incus` — the deploy contract (binds 8080 inside a `hello-web` container)
## Build it standalone
```sh
nix build path:./modules/nixos/linux/incus/incus-pet/example/hello-web
# Then run it locally — listens on 0.0.0.0:8080.
./result/bin/hello-web &
curl http://127.0.0.1:8080
```
## Deploy it as an incus-pet container
From a host that has the incus daemon and the `incus-pet` CLI on PATH:
```sh
nix run path:.#incus-pet -- deploy \
path:./modules/nixos/linux/incus/incus-pet/example/hello-web \
hello-web --port 8081 --listen 100.122.32.106
curl http://100.122.32.106:8081
```
`incus-pet list` will show the running container; `incus-pet rm hello-web`
tears it down.
## What to read next
The flake here is meant to be a working template. Compare the three
outputs (`packages.default`, `nixosModules.default`, `nixosModules.incus`)
side-by-side with what srid/anywhen ships — same shape, scaled down.

View file

@ -0,0 +1,27 @@
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1779357205,
"narHash": "sha256-cCO8aTqss5x9Ky8GWkpY0Hy5fyTZEbtifSUV8QjSzic=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "f83fc3c307e74bc5fd5adb7eb6b8b13ffd2a36e1",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}

View file

@ -0,0 +1,115 @@
# hello-web — minimal incus-pet example.
#
# Self-contained reference flake that ships the three outputs incus-pet
# consumes:
#
# - packages.<sys>.default a tiny static-file server (darkhttpd
# wrapped to read HOST/PORT from env)
# - nixosModules.default services.hello-web.{enable, package,
# host, port} + a systemd unit
# - nixosModules.incus the deploy contract — wires the
# service to bind 8080 inside an incus
# container at hostname "hello-web"
#
# Deploy with (from a host that has the incus daemon + incus-pet CLI):
#
# nix run github:srid/nixos-config#incus-pet -- \
# deploy <path-to-this-flake> --port 8081 --listen <ip>
{
inputs.nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
outputs = { self, nixpkgs, ... }:
let
systems = [ "x86_64-linux" "aarch64-linux" ];
eachSystem = f: nixpkgs.lib.genAttrs systems
(system: f nixpkgs.legacyPackages.${system});
in
{
packages = eachSystem (pkgs:
let
www = pkgs.writeTextDir "index.html" ''
<!doctype html>
<meta charset="utf-8">
<title>hello-web</title>
<h1>Hello from incus-pet</h1>
<p>Served by darkhttpd inside an incus container.</p>
'';
hello-web = pkgs.writeShellApplication {
name = "hello-web";
runtimeInputs = [ pkgs.darkhttpd ];
text = ''
HOST="''${HOST:-0.0.0.0}"
PORT="''${PORT:-8080}"
exec darkhttpd ${www} --addr "$HOST" --port "$PORT"
'';
meta.mainProgram = "hello-web";
};
in
{
default = hello-web;
hello-web = hello-web;
});
nixosModules.default = { config, lib, pkgs, ... }:
let
cfg = config.services.hello-web;
in
{
options.services.hello-web = {
enable = lib.mkEnableOption "hello-web example server";
package = lib.mkOption {
type = lib.types.package;
description = "The hello-web package to run (must expose bin/hello-web via meta.mainProgram).";
};
host = lib.mkOption {
type = lib.types.str;
default = "0.0.0.0";
description = "Address the HTTP server binds to.";
};
port = lib.mkOption {
type = lib.types.port;
default = 8080;
description = "Port the HTTP server listens on.";
};
};
config = lib.mkIf cfg.enable {
systemd.services.hello-web = {
description = "hello-web example HTTP server";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
environment = {
HOST = cfg.host;
PORT = toString cfg.port;
};
serviceConfig = {
ExecStart = lib.getExe cfg.package;
Restart = "on-failure";
RestartSec = 2;
DynamicUser = true;
};
};
};
};
# incus-pet contract — see SKILL.md in the incus-pet tree.
nixosModules.incus = { config, lib, pkgs, ... }: {
imports = [ self.nixosModules.default ];
incus.container = {
enable = true;
hostname = lib.mkDefault "hello-web";
};
system.stateVersion = "25.05";
services.hello-web = {
enable = true;
package = lib.mkDefault self.packages.${pkgs.stdenv.hostPlatform.system}.default;
host = lib.mkDefault "0.0.0.0";
port = 8080; # fixed by the incus-pet contract
};
};
formatter = eachSystem (pkgs: pkgs.nixpkgs-fmt);
};
}

View file

@ -0,0 +1,11 @@
# Loosen kernel hardening so LLM agents can debug without sudo.
#
# NixOS defaults `kernel.dmesg_restrict` to 1, so reading the kernel ring
# buffer (`dmesg`) requires CAP_SYSLOG — i.e. sudo. The kernel log is
# read-only, and handing an agent sudo just to tail it is a far bigger
# hammer than the problem. Setting this to 0 lets unprivileged users (and
# the agents running as them) run `dmesg` directly.
{ ... }:
{
boot.kernel.sysctl."kernel.dmesg_restrict" = 0;
}

View file

@ -30,10 +30,11 @@ writers.writeHaskellBin "fuckport"
main :: IO () main :: IO ()
main = do main = do
port <- Prelude.head <$> getArgs ports <- getArgs
forM_ ports $ \port -> do
s <- lsof "-i" (":" <> port) |> jc "--lsof" |> capture s <- lsof "-i" (":" <> port) |> jc "--lsof" |> capture
let v = fromJust $ decode @[LsofRow] s let v = fromJust $ decode @[LsofRow] s
forM_ v $ \r -> do forM_ v $ \r -> do
putStrLn $ "Killing " <> show (pid r) <> " (" <> command r <> ")" putStrLn $ "Killing " <> show (pid r) <> " (" <> command r <> ") on port " <> port
kill ["-KILL", show (pid r)] kill ["-KILL", show (pid r)]
'' ''

20
packages/portfwd.nix Normal file
View file

@ -0,0 +1,20 @@
{ writeShellApplication, openssh, ... }:
writeShellApplication {
name = "portfwd";
meta.description = ''
Forward a local port to the same port on a remote host over SSH.
Usage: portfwd <port> [host] (host defaults to pureintent)
e.g. `portfwd 7721` or `portfwd 7721 sincereintent`
'';
runtimeInputs = [ openssh ];
text = ''
if [ "$#" -lt 1 ] || [ "$#" -gt 2 ]; then
echo "usage: portfwd <port> [host]" >&2
exit 1
fi
port="$1"
host="''${2:-pureintent}"
ssh -L "$port:localhost:$port" "$host"
'';
}

View file

@ -0,0 +1,18 @@
{ writeShellApplication, ffmpeg, ... }:
# Some useful resources for this undocumented shit:
# - https://stackoverflow.com/q/59056863/55246
# - https://gist.github.com/nikhan/26ddd9c4e99bbf209dd7
writeShellApplication {
name = "twitter-convert";
runtimeInputs = [ ffmpeg ];
meta.description = ''
Convert a video for uploading to X / Twitter.
You may need a Basic or Premium tier to upload longer videos.
'';
text = ''
export ARSCRIPT="${./ffmpeg_ar}"
${./ffmpeg_twitter} "$1"
'';
}

View file

@ -0,0 +1,12 @@
#!/bin/bash
eval $(ffprobe -v error -show_entries stream=width,height -of default=noprint_wrappers=1 "$1")
#results in e.g.
#width=300
#height=1200
echo "height=$height"$'\n'"width=$width"
newenv=$(bc <<< "width=$width; height=$height;"$'\nscale=3; aspect=width / height;\nprint "aspect=", aspect;
print "''\n'$'";\n\nif (aspect>3) { r=(height) * (aspect/3); print "height=";}
if (aspect<(1/3)) {r=(width) / (aspect/(1/3)); print "width=";}\nscale=0
if (r) { print r/1 }')
[ ! -z "$newenv" ] && echo "$newenv" && export $newenv
echo "aspect=""$(bc <<< "scale=3; $width/$height")"

View file

@ -0,0 +1,8 @@
#!/usr/bin/env bash
set -xe
export $($ARSCRIPT "$1")
ffmpeg -i "$1" \
-filter_complex 'fps=30,format=yuv420p,pad='"ceil($width/2)*2:ceil($height/2)*2"':(ow-iw)/2:(oh-ih)/2' -c:v h264_videotoolbox -c:a aac -ac 2 -ar 44100 -r 30 \
output.mp4

View file

@ -1,9 +1,9 @@
age-encryption.org/v1 age-encryption.org/v1
-> ssh-ed25519 96IXNQ 9d0R+OUXFfcIyavqAjeTQ0cQdC5ikhQec4k4QlqKmXE -> ssh-ed25519 96IXNQ xARGTD+HwoFJNUlNqJnPgCFgh21NWpfpG3IgVbnY53M
zGcRQZj4iv5xivSrz2IQDIXHEOfajtXuXUFLaSJpOeM th3oAcHh2oFmwrCJCuTk//hfn3ad8RiDfElQ0UYdM4c
-> ssh-ed25519 It7HZQ yI5rfIG/CPOUbXnXyK8voA9iZ2JaXalV9eVPUjaqQwU -> ssh-ed25519 It7HZQ ADxdXtnMvKfV57VgvO9JQx09Uk4yNFgSnMEr8PX9tUM
pfzi9JfBtDocCQW5xEAuH1UT1MzqH7CQbaSgKtt2EUE BzIMSHpZ8vfa1PU7cINGMuPZCgamNuyk6Sl9JPz3/TA
-> ssh-ed25519 0mMrRw Wsd1KEmQFVWa4qcw56ByW3HG2djFGvtmYjlVsqczPRU -> ssh-ed25519 0mMrRw lBGsIYsWnQBvuOGaxpPMN9FxNIy848ciNEuxOGC8fBc
OmZncQdNMzxgHJ8puGwMzrXNrmA6vBweEHHyU0u7kD8 GY5+rAUPB97L0SVbWAmT26sfNCul9WytTonyXqFTB8s
--- 3u/3Uxd+nfjADOvXfht8mJWjcrXJRiyt/UgfJ3YERi8 --- 1dYxiDNUh3B9KRKw6P8vnZFWYR/uKKef+bRlTB6dUuQ
À…#ÌìÂúh'ruî_çz—)üˆŠ¾¸ðd· OâÇÎ0‡ûã•ì;»Caxü¢ÏNlDenõ o±1Á%?çß&„Ö:$ª†£ #bÞ{<7B>­ *ièZšþ—5l·.ØÓ§‹ûi9þõëõj ˜hÚç” ˜Ô

View file

@ -1,11 +0,0 @@
# Hosting webapps on home-server
Host them on `pureintent` (home-server)
Run nginx on `gate` (Hetzner VPS).
Put the two in a Tailscale network. Profit!
WARNING: This is not cleanly designed yet, so don't use it as a reference.
NOTE: This model would be made redundant/simplified after https://github.com/tailscale/tailscale/issues/11563

View file

@ -1,12 +0,0 @@
{ flake, system, ... }:
# By convention, ports 15000 and above are used for webapps
{
/* actualism-app = rec {
port = 15001;
domain = "actualism.app";
package = flake.inputs.actualism-app.packages.${system}.default;
exec = "${package}/bin/actualism-app ${builtins.toString port}";
};
*/
}

View file

@ -1,24 +0,0 @@
# Configuration for the host on which all webapps will run.
{ flake, pkgs, lib, ... }:
let
webapps = import ./. { inherit flake; system = pkgs.stdenv.hostPlatform.system; };
in
{
# Run each web app as a systemd service decided inside a container.
containers = lib.mapAttrs
(name: v: {
autoStart = true;
config = {
systemd.services.${name} = {
description = name;
wantedBy = [ "multi-user.target" ];
serviceConfig = {
ExecStart = v.exec;
Restart = "always";
};
};
};
})
webapps;
}

View file

@ -1,30 +0,0 @@
# Configuration for the VPS running nginx reverse proxy
{ flake, pkgs, lib, webapps, ... }:
let
host = "pureintent"; # See host.nix
webapps = import ./. { inherit flake; system = pkgs.stdenv.hostPlatform.system; };
in
{
services.tailscale.enable = true;
services.nginx = {
enable = true;
recommendedProxySettings = true;
recommendedTlsSettings = true;
virtualHosts = lib.mapAttrs'
(name: v: lib.nameValuePair v.domain {
locations."/".proxyPass = "http://${host}:${builtins.toString v.port}";
enableACME = true;
addSSL = true;
})
webapps;
};
security.acme = {
acceptTerms = true;
defaults.email = "srid@srid.ca";
};
networking.firewall.allowedTCPPorts = [ 80 443 22 ];
}