Watch
1
0
Fork
You've already forked systems
0
forked from s/systems
nixos stuff
  • Rust 37.6%
  • TypeScript 35%
  • JavaScript 14.2%
  • Python 3.9%
  • HTML 3%
  • Other 6.3%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
openai-codex/gpt-5.6-luna 5571e998a3 config: raise parallel steward limits
Increase task concurrency to 12 and runtime to three hours for concurrent steward cycles.

Change-Id: I9b283b8fbe1d83c82a2772e52fb0153e7728ed74
2026-09-15 14:02:20 +02:00
.agents/skills prose-style: structural takes a string, and the no-X rule covers verbs 2026-09-14 11:58:04 +02:00
.cargo build: cap concurrent Rust work 2026-08-25 15:07:45 +02:00
.forgejo/workflows Take the factory out of the nix, and say what outlives it 2026-09-12 13:23:22 +02:00
.githooks feat(omp): path-aware model rotation, stage ledger, Change-Id trailers 2026-09-13 16:03:56 +02:00
.omp config: raise parallel steward limits 2026-09-15 14:02:20 +02:00
archive samarium: a port index on 443, and retire the snapshot that refused it 2026-09-14 14:01:12 +02:00
doom doom: local-fir — font-lock for fir, and nothing else 2026-08-23 20:35:09 +02:00
hosts merge(kb): integrate the deputy roadmap record onto current canonical 2026-09-15 13:05:10 +02:00
kb roadmap(deputy): define overseer escalations 2026-09-15 13:35:43 +02:00
lib feat(deputy): render port index with Vite 2026-09-14 15:40:23 +02:00
memory disable mpv 2026-04-21 10:09:03 +02:00
modules Take the factory out of the nix, and say what outlives it 2026-09-12 13:23:22 +02:00
projects fix(director): serialize model and round state 2026-09-15 13:46:07 +02:00
scripts fix(director): serialize model and round state 2026-09-15 13:46:07 +02:00
tests more 2026-03-18 13:37:39 +01:00
.envrc kypsele(guest): drop systems auto-clone + pi extensions; per-project devshell dir 2026-05-25 22:39:35 +02:00
.gitignore deputy: one stop-reason table, and pin the notice id across both languages 2026-09-14 12:02:21 +02:00
.ignore format 2025-04-12 20:29:52 +02:00
.npmrc Take the factory out of the nix, and say what outlives it 2026-09-12 13:23:22 +02:00
.sops.yaml malina: add personal key 1 2026-05-24 18:13:42 +02:00
biome.json artifacts: add 3b, a three-body orbit screensaver at /3b/ 2026-09-14 16:40:20 +02:00
checks.json merge: sync local-source master into candidate work/canonical-sync/2026-09-14 2026-09-14 17:08:32 +02:00
CLAUDE.md kb(roadmap): record the deputy's own infrastructure, drop the kyp-agent instruction 2026-09-15 12:31:44 +02:00
flake.lock feat(deputy): provision OMP model director 2026-09-13 14:21:33 +02:00
flake.nix flake: put omp on the workstation devshell PATH 2026-09-13 16:59:42 +02:00
nerd-fonts.txt icons 2026-03-29 16:06:19 +02:00
pnpm-workspace.yaml feat(deputy): render port index with Vite 2026-09-14 15:40:23 +02:00
README.md README: the lightningcss ::highlight warnings, and why they stand 2026-09-08 11:07:12 +02:00
RESUME.md resume: openrouter is at $0.11, and the aimed repair bought nothing 2026-09-14 13:49:47 +02:00
secrets.yaml malina: updatekeys 2 2026-05-24 18:14:24 +02:00
sync-qs.sh clock 2026-03-29 17:09:23 +02:00
TODO.org dev-vm: remove 2026-04-23 21:02:28 +02:00
victoria-todo.md victoriametrics logs browser 2026-03-27 10:42:17 +01:00

systems

NixOS configurations for all hosts.

TODO: server-side clock in?

TODO: in @lib/overlays.nix , the npmdepshash has to be updated, and it causes a build failure when we fail to do it. i feel like a nicer way to do this would be to load it from a file that lives beside the respective package.json, with an xtask to update it (xtask artifacts update-npm-deps-hash etc), and a commit hook that checks it when a package.lock is changed.

Things to know

Findings that are true of this tree and not written anywhere a reader would look. Each names the day it was found; strike it when it stops being true.

  • xtask parnassus test fails two SightReadingDrill cases on master, the same two under npm ci and under pnpm, so they are the tree's and not the package manager's. lib/overlays.nix says to run the suite in dev instead of in the build, as if the build were the problem; it is not. (2026-09-07)

  • pnpm's strictDepBuilds is inert here, twice over: ignore-scripts and the empty onlyBuiltDependencies allowlist both pre-empt it. It stays set because it is the right value; the workspace check says on every run that it is not what is doing the work. (2026-09-07)

  • trustPolicy: no-downgrade exists at pnpm 10.34.5 and is not set: it refuses semver@6.3.1 under @babel/core outright, so turning it on means an exclusion list of transitive packages nobody chose. (2026-09-07)

  • gl@8.1.6 declares an install script that has never run here (prebuild-install || node-gyp rebuild, no build/ directory in the tree). Builds are off; nothing depends on it having run. (2026-09-07)

  • litz's build prints two lightningcss warnings about ::highlight(), and the rules survive: dist carries both, verified. It is lightningcss 1.32's parser, which does not know the Custom Highlight API's pseudo-element; 1.33 parses it silently, measured. It is not overridden because @tailwindcss/node pins lightningcss: "1.32.0" EXACTLY -- Tailwind calls that optimizer's API directly and versions its behaviour with it, so contradicting the pin to silence a cosmetic warning would ship CSS through an optimizer Tailwind never tested against. It goes away when Tailwind bumps. (2026-09-08)

  • The litz suite has one pre-existing React act() warning from CodeBlock's lazy grammar load in test/zkit-chat/web/markdown.test.tsx. Harmless; noisy. (2026-09-07)

  • The chat and turn types were not split in the zkit restructure: canon_id, leaves, starred, hidden, derived_from stay on zkit-chat's Chat and Message although they are litz's. Each is one column read by one reader; splitting them is a type parameter through every store function and hook. src/zkit-chat/README.md names it as the debt it is. (2026-09-07)

  • C-n and C-w cannot be prevented in Chrome (new window, close tab), so a textarea that turns the Emacs set on gets them in Firefox only. The set is off by default. (2026-09-07)

Tailscale / Headscale

All hosts connect via Headscale (self-hosted Tailscale control plane) running on samarium at https://hs.finiteleibniz.com.

MagicDNS is enabled: hosts are reachable as <hostname>.ts.internal. Internal services on erbium (prometheus, perses, vlogs) are accessible at <service>.internal.finiteleibniz.com via headscale extra_records + nginx with ACME certs.

Enrolling a new node

  1. Add ../../modules/nixos/tailscale.nix to the host's imports and deploy.

  2. Generate a preauth key:

    ssh samarium headscale preauthkeys create --user s
    
  3. On the new node:

    tailscale up --login-server=https://hs.finiteleibniz.com --authkey=<key>
    
  4. Verify:

    ssh samarium headscale nodes list
    
  5. If the node should be scraped by Prometheus, add it to the peers list in modules/nixos/monitoring.nix and import modules/nixos/node-exporter.nix.

  6. Update tsIPs in modules/nixos/headscale.nix if the assigned IP doesn't match.

Administration

ssh samarium headscale nodes list          # list all nodes
ssh samarium headscale nodes rename <id> <name>  # rename a node
ssh samarium headscale preauthkeys list --user s  # list preauth keys

Secrets

Secrets are managed with sops-nix using age encryption. Encrypted secrets live in-repo under each host (e.g. hosts/erbium/secrets.yaml) and are decrypted on the target machine at activation time.

Editing secrets

sops hosts/erbium/secrets.yaml

This prompts for your age key passphrase, then opens $EDITOR.

Adding a new device

  1. Generate a passphrase-protected age key:

    age-keygen | age -p > ~/.config/sops/age/keys.txt
    chmod 600 ~/.config/sops/age/keys.txt
    

    This prints the public key to stderr. Copy it.

  2. Add the public key to .sops.yaml:

    keys:
      - &mydevice age1...
    

    And add it to the relevant creation_rules key groups.

  3. Re-encrypt all affected secrets so the new key can decrypt them. This requires a device that can already decrypt:

    sops updatekeys hosts/erbium/secrets.yaml
    

Adding a new host

Hosts decrypt secrets using their SSH host key, converted to age. To get a host's age public key:

ssh-keyscan -t ed25519 <host> | ssh-to-age

Add this to .sops.yaml alongside the personal keys.

Org-mode deadline management (erbium)

Org files live in a Forgejo repo. A pair of services on erbium parse deadlines out of them and make them useful outside of Emacs.

How it works now

Forgejo repo (*.org files)
  │
  │  git pull (every 6h + webhook on push)
  ▼
sync_org_deadlines.py ──▶ SQLite (deadlines table)
  │
  │  every 5 min
  ▼
notify_deadlines.py
  ├──▶ main.ics (served at calendar.tensegrist.com/<token>/main.ics)
  └──▶ ntfy push notifications (escalating: 1d → 1h → 5m before deadline)

Components:

  • sync_org_deadlines.py — clones/pulls the org repo, parses DEADLINE and SCHEDULED timestamps with orgparse, upserts into SQLite, prunes removed items
  • notify_deadlines.py — reads SQLite, sends ntfy notifications at three urgency tiers (skips DONE/CANCELLED), generates an ICS feed with VALARM reminders (Europe/Paris timezone), prunes old alert records
  • webhook_sync.py — tiny Flask app that receives Forgejo webhook POSTs, verifies HMAC signature, touches a trigger file that a systemd path unit watches to start a sync
  • hosts/erbium/org-deadlines.nix — systemd services/timers, nginx vhost, sops secrets, user/group setup
  • hosts/erbium/ntfy.nix — self-hosted ntfy server at ntfy.tensegrist.com

Setup: see comments at the top of hosts/erbium/org-deadlines.nix.

Systemd units:

  • sync-org-deadlines.service / .timer — pulls org repo + updates SQLite (every 6h)
  • notify-org-deadlines.service / .timer — sends notifications + generates ICS (every 5min)
  • webhook-org-deadlines.service — Flask webhook listener on localhost:8783
  • sync-org-deadlines-trigger.path — watches trigger file, starts sync on webhook
  • org-deadlines-serve-setup.service — creates serve directory + symlink from sops token on boot

Check all statuses:

systemctl status sync-org-deadlines.{service,timer} notify-org-deadlines.{service,timer} webhook-org-deadlines.service org-deadlines-serve-setup.service sync-org-deadlines-trigger.path

Permissions model:

  • All services run as the org-deadlines system user
  • nginx is added to the org-deadlines group to read ICS files from the serve directory
  • The data directory (/var/lib/org-deadlines) is 0750 — owner full access, group traverse+read
  • ICS files are explicitly chmod 0640 so only owner and group (nginx) can read them
  • Secrets are decrypted to /run/secrets/ by sops-nix, each scoped to its service (sync gets repo URL, notify gets ntfy credentials, webhook gets HMAC secret, ntfy gets its user password)
  • The ICS feed URL contains a random token as a path component for unguessable access

ntfy push notifications (erbium)

A self-hosted ntfy instance at ntfy.tensegrist.com, used by the deadline notification system.

  • Config: hosts/erbium/ntfy.nix
  • Topic: org-main — receives deadline alerts from notify_deadlines.py
  • Auth: anonymous access is denied (auth-default-access: deny-all). A ntfy admin user is provisioned declaratively via NTFY_AUTH_USERS — the bcrypt hash is generated from the sops password at boot by ntfy-sh-auth.service.
  • Subscribe: in the ntfy app, add https://ntfy.tensegrist.com/org-main with username ntfy and the password from ntfy-password in secrets.

Systemd units:

  • ntfy-sh.service — the ntfy server (localhost:2586, behind nginx)
  • ntfy-sh-auth.service — hashes the sops password into an env file for ntfy-sh (runs before ntfy-sh)

Monitoring & alerting (erbium)

Prometheus, Alertmanager, and Perses run on erbium behind the tailscale interface.

  • Config: modules/nixos/monitoring.nix
  • Alertmanager sends notifications to Telegram. The bot token and chat ID are stored as sops secrets (alertmanager-telegram-token, alertmanager-telegram-chat-id in hosts/erbium/secrets.yaml), decrypted at activation to /run/secrets/ and owned by the alertmanager user.
  • Alerts are grouped by alertname. Timing uses alertmanager defaults: 30s group wait, 5m group interval, 4h repeat.
  • Perses dashboard at perses.internal.finiteleibniz.com (tailscale only).

Testing alertmanager:

curl -XPOST http://127.0.0.1:9093/api/v2/alerts \
  -H 'Content-Type: application/json' \
  -d '[{"labels":{"alertname":"TestAlert","severity":"info"},"annotations":{"summary":"test"}}]'

Should deliver to Telegram within 30s.

Testing

nix build .#checks.x86_64-linux.ksd --print-build-logs

Future: bidirectional calendar sync

Two possible directions for letting calendar clients create events that flow back into org files.

Option A: Radicale CalDAV (separate calendar for capture)

Keep the ICS feed for deadlines (read-only). Add a Radicale CalDAV server for a second, writable calendar used to capture new events.

  • Radicale runs on erbium, behind nginx at e.g. calendar.tensegrist.com/dav/
  • Calendar clients subscribe to both: the ICS feed (read-only deadlines) and the CalDAV calendar (read-write inbox)
  • A cron job polls Radicale's file store for new events and appends them as * TODO headings with DEADLINE: or SCHEDULED: to refile.org in the Forgejo repo, then commits and pushes
  • No conflict resolution needed — each direction is one-way. Org files are authoritative for deadlines; Radicale is a write-only inbox that gets drained into org.

Pros: simple, no conflicts, calendar clients get full CalDAV support for the inbox calendar. Cons: two calendars in the client, new events don't appear as org items until the drain cron runs + the next sync.

Option B: Full CalDAV with org as backend

Replace the ICS feed entirely with a CalDAV server that reads/writes org files directly.

  • A CalDAV server (Radicale or custom) uses org files as its storage backend
  • Reads: deadlines/scheduled items from org → CalDAV events (like the current ICS generation, but served via CalDAV protocol)
  • Writes: new/modified CalDAV events → appended or patched in org files, committed and pushed to Forgejo
  • Deletes: mark the org heading as CANCELLED (or actually remove it, configurable)

Pros: single calendar, true bidirectional sync, feels native. Cons: complex conflict resolution (what if an org heading is edited and the CalDAV event is modified simultaneously?), CalDAV clients may set properties that don't map to org-mode, significant implementation effort. Likely needs a custom CalDAV backend rather than stock Radicale.

Although org-caldav is fine, and i'm fine with the incoming sync overwriting anything in the inbox.org lol the inbox is strictly client write-only