18 changed files with 315 additions and 325 deletions
+1 -1
View File
@@ -54,7 +54,7 @@ jobs:
- name: Install Nix - name: Install Nix
if: steps.detect.outputs.run == 'true' if: steps.detect.outputs.run == 'true'
uses: cachix/install-nix-action@a49548c11d9846ad46ecc0115273879b045f001c # v31 uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31
with: with:
extra_nix_config: | extra_nix_config: |
experimental-features = nix-command flakes experimental-features = nix-command flakes
-63
View File
@@ -1,63 +0,0 @@
# Working on this flake
Project notes for changes to this repository. Persona and memory rules live in
the user-global config; this file is about the flake's checks and conventions.
## Before you commit: run the formatter
Formatting and linting are driven by the flake. CI (`.gitea/workflows/ci.yaml`)
runs `nix flake check`, which fails the build if any file is unformatted or trips
a lint. From the repo root:
- `nix fmt` — format the whole tree (writes changes).
- `nix flake check` — run every check read-only (what CI runs).
- `nix develop` — dev shell; its `shellHook` installs the git pre-commit hooks so
the same gates run on `git commit`.
Never commit with `--no-verify`. A bypassed commit ships unformatted content and
turns CI red on the next push to `main` (see "Docs are checked too").
## What gets checked
Defined in `flake.nix` (the `treefmt`, `pre-commit`, and `checks` blocks) and
`statix.toml`:
| Check | Tool | Covers |
| ------------ | --------------------------------- | ------------------------------------------------------- |
| `formatting` | treefmt → `nixfmt` | all `*.nix` |
| `formatting` | treefmt → `shfmt` | shell scripts |
| `formatting` | treefmt → `prettier` | **Markdown, YAML, JSON** (incl. `README.md`, this file) |
| `deadnix` | deadnix | dead Nix bindings (`--no-lambda-pattern-names`) |
| `statix` | statix | Nix antipatterns (config in `statix.toml`) |
| pre-commit | nixfmt-rfc-style, deadnix, statix | the same gates, run on commit |
Excluded from formatting: `*/hardware-configuration.nix` (generated by
`nixos-generate-config`) and `flake.lock`. Editor defaults (indent, EOL, final
newline) are in `.editorconfig`; note Markdown keeps trailing whitespace, which
encodes hard line breaks.
## Docs are checked too — the common trap
prettier formats `*.md`, so **documentation edits must be run through `nix fmt`**
exactly like code. prettier re-aligns Markdown tables in particular; hand-editing
a table almost always leaves it non-conformant and fails the `formatting` check.
Beware a false green: the CI `detect` step skips the heavy checks on a pull
request that touches **no** `.nix`, `flake.lock`, or the workflow file — so a
docs-only PR reports success without ever running prettier. The failure then
surfaces on the push-to-`main` run (which always runs the full check) or on the
next unrelated PR that does touch Nix. Run `nix flake check` locally before
merging a docs change, regardless of what the PR check shows.
## Host evaluation
CI also evaluates every `nixosConfigurations` / `darwinConfigurations` host's
toplevel (eval only, no build) on an x86_64 runner, so eval errors fail cheaply.
Reproduce locally:
```sh
nix eval --raw ".#nixosConfigurations.<host>.config.system.build.toplevel.drvPath"
```
Host lists are discovered from the flake, so adding or removing a host needs no
change to the workflow.
+32 -99
View File
@@ -12,67 +12,16 @@ Defined in the host table in [`flake.nix`](./flake.nix):
| `lyrathorpe-mbp` | `aarch64-linux` | MacBook Pro (Apple Silicon, Asahi) | | `lyrathorpe-mbp` | `aarch64-linux` | MacBook Pro (Apple Silicon, Asahi) |
| `lyrathorpe-t400` | `x86_64-linux` | ThinkPad T400 — [install notes](./hosts/T400/README.md) | | `lyrathorpe-t400` | `x86_64-linux` | ThinkPad T400 — [install notes](./hosts/T400/README.md) |
| `lyrathorpe-macpro31` | `x86_64-linux` | Mac Pro 3,1, desktop — [install notes](./hosts/MacPro31/README.md) | | `lyrathorpe-macpro31` | `x86_64-linux` | Mac Pro 3,1, desktop — [install notes](./hosts/MacPro31/README.md) |
| `emmathorpe-edaas` | `x86_64-linux` | Work WSL box (NixOS-WSL) — [notes](./hosts/EDaaS/README.md) | | `emmathorpe-edaas` | `x86_64-linux` | Work WSL box (NixOS-WSL) |
| `lyrathorpe-rpi5` | `aarch64-linux` | Raspberry Pi 5 headless server: Docker host + nginx reverse proxy — [install notes](./hosts/RPi5/README.md) | | `lyrathorpe-rpi5` | `aarch64-linux` | Raspberry Pi 5 headless server: Docker host + nginx reverse proxy — [install notes](./hosts/RPi5/README.md) |
| `lyrathorpe-mac` | `aarch64-darwin` | macOS (nix-darwin) — [notes](./hosts/Darwin/README.md) | | `lyrathorpe-mac` | `aarch64-darwin` | macOS (nix-darwin) |
Shared layers: `home` (home-manager: shell, git, editor), Shared layers: `home` (home-manager: shell, git, editor),
`modules/common-nixos.nix` (all NixOS hosts: fonts, nix-ld, caches), `modules/common-nixos.nix` (all NixOS hosts: fonts, nix-ld, caches),
`modules/workstation.nix` (physical graphical hosts: audio, thermald, `modules/workstation.nix` (physical graphical hosts: audio, thermald,
earlyoom, fwupd), `modules/laptop.nix` (laptops: Wi-Fi, Bluetooth, power, earlyoom, fwupd), `modules/laptop.nix` (laptops: Wi-Fi, Bluetooth, power,
lid), `modules/desktop.nix` (wired desktops: NetworkManager), and lid), and `modules/ssh.nix` (key-only sshd). The x86 hosts also pull
`modules/ssh.nix` (key-only sshd). The x86 hosts also pull `nixos-hardware` `nixos-hardware` profiles.
profiles. The full module catalogue is below.
## Repository layout
```
flake.nix # inputs, mkHost/mkDarwinHost, the host tables, dev shell + checks
flake.lock # pinned input revisions (Renovate keeps this fresh)
modules/ # reusable NixOS system modules (see "Module catalogue")
home/ # home-manager profile: shell, git, editor, claude, desktop, sway
users/ # identity registry + per-user home extras (see "Users")
hosts/<Name>/ # per-machine config: configuration.nix + hardware-configuration.nix
lib/ # small pure helpers (currently the Catppuccin Mocha palette)
.gitea/workflows/ # CI (nix flake check + per-host eval)
statix.toml # lint config (house-style lints disabled)
.editorconfig # base whitespace style
tf-inspect/ # UNRELATED scratch project (gitignored, its own git repo);
# RouterOS / home-services Terraform, not part of this flake
```
Each `nixosConfiguration` / `darwinConfiguration` is assembled in `flake.nix`
from three layers: the shared `baseModules` (or `darwinBaseModules`), the
per-form-factor and `nixos-hardware` modules listed in the host table, and the
per-machine `hosts/<Name>/configuration.nix`. Home-manager is wired in as a
system module; each user's home is composed from the `homeModules` list in that
host's table entry.
## Module catalogue
Reusable NixOS modules under [`modules/`](./modules). "Imported by" says how a
module reaches a host: **baseModules** (every NixOS host, via `flake.nix`),
**host table** (listed explicitly per host in `flake.nix`), or **transitively**
(pulled in by another module's `imports`).
| Module | Imported by | What it does / when to use it |
| ------------------ | --------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `common-nixos.nix` | baseModules (all NixOS) | Timezone/locale, store hygiene (auto-optimise, big download buffer, **no** auto-GC), the nix-community binary cache, `nix-ld`, base CLI (`git`, `fastfetch`), and the fleet-wide font stack. |
| `users.nix` | baseModules (all NixOS) | Builds `users.users` from the registry for the host's `hostUsers`; enables zsh; enables Firefox + Thunderbird **only** when `features.swayDesktop.enable` is on. Applies per-user `linger`. |
| `features.nix` | baseModules (all NixOS) | Declares feature-flag options (currently `features.swayDesktop.enable`) so any host can read/set them without importing the heavy implementation module. |
| `workstation.nix` | transitively (via laptop/desktop) | Form-factor-agnostic base for physical graphical hosts: turns on `swayDesktop`, Dvorak console, PipeWire, firewall (default-deny), fstrim, earlyoom, fwupd, thermald (x86), redistributable fw. |
| `laptop.nix` | host table (MBP, T400) | `imports` workstation.nix, then adds the portable bits: iwd Wi-Fi, lid suspend/lock, Bluetooth + blueman. |
| `desktop.nix` | host table (Mac Pro) | `imports` workstation.nix, then swaps Wi-Fi for wired NetworkManager. Pair with `portable = false` in the host table. |
| `sway.nix` | host table (graphical hosts) | Implementation of `features.swayDesktop`: the system Sway package, the greetd/ReGreet (cage) greeter forced to Dvorak, xdg-portal, Wayland utility packages. Home-side Sway config is in `home/sway.nix`. |
| `ssh.nix` | host table (T400, Mac Pro, RPi5) | Enables sshd, opens port 22, enforces a key-only policy (no password / keyboard-interactive, no root). Authorized keys come from the registry via `users.nix`. |
| `firmware/` | referenced by MBP host config | Committed Apple peripheral firmware blobs for the Asahi MBP (see "MacBook (Asahi) firmware"). |
Form-factor decision: a **laptop** imports `laptop.nix` (default
`portable = true`); a **wired desktop** imports `desktop.nix` and sets
`portable = false`; a **headless server** imports neither (leaves
`features.swayDesktop.enable` at its default `false`) and adds only what it
serves. `portable` is threaded through to `home/sway.nix`, which drops the
battery block and brightness keys on desktops.
## Users ## Users
@@ -89,16 +38,6 @@ Identity is data, kept separate from the reusable modules:
identity into that user's home config as the `identity` module arg. A host can identity into that user's home config as the `identity` module arg. A host can
therefore declare any number of users. therefore declare any number of users.
Per-user home extras live under `users/<name>/`:
- [`users/lyrathorpe/home.nix`](./users/lyrathorpe/home.nix) — personal extras
(an ssh host shortcut, gammastep coordinates); imported on Lyra's hosts.
- [`users/emmathorpe/work.nix`](./users/emmathorpe/work.nix) — the work
toolchain (kubectl/helm/az/etc.), work-only LSP servers, and the corporate ssh
handling; imports
[`users/emmathorpe/renovate-review.nix`](./users/emmathorpe/renovate-review.nix),
the daily headless Renovate-PR review timer (EDaaS only).
### Portable home (off-NixOS / external consumers) ### Portable home (off-NixOS / external consumers)
The home config is also exposed for use beyond these hosts: The home config is also exposed for use beyond these hosts:
@@ -113,6 +52,33 @@ The home config is also exposed for use beyond these hosts:
(`inputs.<this>.homeModules.default`). Consumers must supply the module args (`inputs.<this>.homeModules.default`). Consumers must supply the module args
these expect: `inputs` always, `identity` for git/desktop, `portable` for sway. these expect: `inputs` always, `identity` for git/desktop, `portable` for sway.
## Directory authentication (SSSD → Authentik LDAP)
Every NixOS host authenticates users against the Authentik LDAP outpost via
SSSD, implemented in [`modules/sssd.nix`](./modules/sssd.nix) and enabled by
default through the `services.authentikLdap.enable` option (added to
`baseModules`). The **EDaaS** WSL box opts out
(`services.authentikLdap.enable = false`) as a work-managed environment; the
macOS host is unaffected (SSSD is Linux-only).
- Connects over LDAPS to `ldap.lyrapup.pet:636`, search base
`dc=ldap,dc=goauthentik,dc=io`, binding as
`cn=sssd-bind,ou=users,dc=ldap,dc=goauthentik,dc=io`.
- The schema mappings match Authentik's non-standard object classes
(`goauthentik.io/ldap/user`, `goauthentik.io/ldap/group`) over the POSIX
attributes (`uid`, `uidNumber`, `gidNumber`, `homeDirectory`).
- Home directories are created on first login (`pam_mkhomedir`).
### Secrets (agenix)
The LDAP bind credential is an [agenix](https://github.com/ryantm/agenix)
secret, decrypted at activation with each host's SSH host key. The decrypted
plaintext is a full `sssd.conf` drop-in delivered to
`/etc/sssd/conf.d/01-ldap-authtok.conf`, so the password never enters the Nix
store. Owner setup (host recipient keys, encrypting the bind password, DNS for
`ldap.lyrapup.pet`, rebuild) is documented in
[`secrets/README.md`](./secrets/README.md).
## Applying ## Applying
```sh ```sh
@@ -122,36 +88,6 @@ sudo nixos-rebuild switch --flake .#<configuration>
darwin-rebuild switch --flake .#lyrathorpe-mac darwin-rebuild switch --flake .#lyrathorpe-mac
``` ```
On a host whose `networking.hostName` matches its flake attribute (the WSL box
and the Pi are set up this way), `nh os switch` resolves the configuration from
the hostname with no `--flake`/`-H` flag.
## Adding a new host
1. **Create `hosts/<Name>/`.** Add `configuration.nix` with the host-specific
bits only: `networking.hostName`, bootloader (firmware-specific — it is
deliberately not set in the shared modules), and any per-machine hardware
quirks. Keep anything reusable in `modules/` instead.
2. **Hardware config.** Generate `hardware-configuration.nix` on the real
machine with `nixos-generate-config` and commit it. If the machine does not
exist yet, commit a clearly-labelled placeholder so the host still evaluates
in CI (see the existing T400 / RPi5 placeholders), and replace it at install.
These files are excluded from the formatter and linters.
3. **Add a host-table entry in `flake.nix`.** Under `hosts` (NixOS) or
`darwinHosts` (macOS), set `system`, the `modules` list (host config + form
factor + any `nixos-hardware` profiles), and the `users` map (each user's
`homeModules`). Choose the form factor per the decision note above; a headless
host imports neither `laptop.nix` nor `desktop.nix`.
4. **Users.** If the host introduces a new person, add them to
`users/registry.nix` first; otherwise reference an existing username.
5. **Verify.** `nix flake check` formats, lints, and evaluates every host —
including the new one — so a broken entry fails locally before CI. Then
`sudo nixos-rebuild switch --flake .#<configuration>` on the machine.
No change to CI is needed: the host-eval step discovers hosts from the flake
(`attrNames` of the configuration sets), so a new entry is picked up
automatically.
## Shell environment & keybindings ## Shell environment & keybindings
- Interactive shell features (zsh, tmux, git, ssh, CLI tools, auto-tmux): - Interactive shell features (zsh, tmux, git, ssh, CLI tools, auto-tmux):
@@ -202,7 +138,4 @@ A dev shell and a formatting/lint gate are wired through the flake:
[`.gitea/workflows/ci.yaml`](./.gitea/workflows/ci.yaml) runs `nix flake check` [`.gitea/workflows/ci.yaml`](./.gitea/workflows/ci.yaml) runs `nix flake check`
(formatting, `deadnix`, `statix`, the pre-commit hooks) and evaluates every (formatting, `deadnix`, `statix`, the pre-commit hooks) and evaluates every
NixOS and Darwin host configuration on push/PR. It always runs (no `paths:` NixOS and Darwin host configuration on push/PR.
filter) so the required check never hangs pending; the heavy Nix steps are
skipped when a PR touches no `.nix`/lockfile/workflow file, and the job still
reports green.
+11
View File
@@ -46,6 +46,15 @@
url = "github:cachix/git-hooks.nix"; url = "github:cachix/git-hooks.nix";
inputs.nixpkgs.follows = "nixpkgs"; inputs.nixpkgs.follows = "nixpkgs";
}; };
# agenix: age-encrypted secrets, decrypted at activation with each host's
# SSH host key. Provides the SSSD LDAP bind credential (secrets/, see
# modules/sssd.nix). The darwin module is intentionally unused (SSSD is
# Linux-only).
agenix = {
url = "github:ryantm/agenix";
inputs.nixpkgs.follows = "nixpkgs";
inputs.home-manager.follows = "home-manager";
};
# Declarative Neovim (the editor; see home/editor.nix). Release # Declarative Neovim (the editor; see home/editor.nix). Release
# branch matched to the pinned nixpkgs (26.05); follows our nixpkgs to keep a # branch matched to the pinned nixpkgs (26.05); follows our nixpkgs to keep a
# single nixpkgs in the closure. editor.nix sets programs.nixvim.nixpkgs.source # single nixpkgs in the closure. editor.nix sets programs.nixvim.nixpkgs.source
@@ -123,7 +132,9 @@
./modules/users.nix ./modules/users.nix
./modules/common-nixos.nix ./modules/common-nixos.nix
./modules/features.nix ./modules/features.nix
./modules/sssd.nix
commonModule commonModule
inputs.agenix.nixosModules.default
home-manager.nixosModules.home-manager home-manager.nixosModules.home-manager
{ {
home-manager.useGlobalPkgs = true; home-manager.useGlobalPkgs = true;
+1 -3
View File
@@ -1,13 +1,11 @@
- [User name](user_name.md) — address the user as Lyra - [User name](user_name.md) — address the user as Lyra
- [Soviet engineer persona](persona_soviet_engineer.md) — terse, dry, pragmatic; no emojis; technical accuracy over voice - [Soviet engineer persona](persona_soviet_engineer.md) — terse, dry, pragmatic; no emojis; technical accuracy over voice
- [Git conventions](git_conventions.md) — never commit to main, always a branch; Conventional Commits branches and messages; inspect repo style first; commit at logical checkpoints - [Git conventions](git_conventions.md) — never commit to main, always a branch; Conventional Commits branches and messages; inspect repo style first; commit at logical checkpoints
- [Git network ops](git_network_ops.md) — GitHub and Gitea (code.emmathe.dev) both pushable in-sandbox (sandbox off, agent key); raise Gitea PRs via tea CLI - [Git network ops](git_network_ops.md) — GitHub pushable in-sandbox (agent key; just sandbox off); Gitea code.emmathe.dev needs hand-off
- [Git commit signing](git_commit_signing.md) — signs in-sandbox via ssh-agent (allowAllUnixSockets + inlined pubkey) - [Git commit signing](git_commit_signing.md) — signs in-sandbox via ssh-agent (allowAllUnixSockets + inlined pubkey)
- [Git check state first](git_check_state.md) — always check branch/status/divergence before git work; Lyra edits repos between sessions - [Git check state first](git_check_state.md) — always check branch/status/divergence before git work; Lyra edits repos between sessions
- [Keep docs updated](docs_keep_updated.md) — update docs in the same pass as code/config changes; stale docs are a defect - [Keep docs updated](docs_keep_updated.md) — update docs in the same pass as code/config changes; stale docs are a defect
- [SIBO Workabout MX project](sibo_workabout_mx_scanner.md) — RE + barcode-inventory project state; scanner is an OO DYL object (oscanner), blocked on on-device ordinal capture; resume via code/inventory/CONTINUATION.md
- [Jira tooling](jira_tooling.md) — comments are Markdown not wiki; transitions may need assignee; link direction; WSP transition IDs - [Jira tooling](jira_tooling.md) — comments are Markdown not wiki; transitions may need assignee; link direction; WSP transition IDs
- [Review and comments workflow](workflow_review_and_comments.md) — show PR body and non-trivial Jira comments before posting; terse IaC code comments; PR body content rules - [Review and comments workflow](workflow_review_and_comments.md) — show PR body and non-trivial Jira comments before posting; terse IaC code comments; PR body content rules
- [Sandbox prompts](feedback_sandbox_prompts.md) — don't prompt for sandbox-disable or routine read-only shell ops; broaden permissions instead - [Sandbox prompts](feedback_sandbox_prompts.md) — don't prompt for sandbox-disable or routine read-only shell ops; broaden permissions instead
- [Dev clusters disposable](dev_clusters_disposable.md) — Lyra's dev clusters are recreatable; mutate/break freely, no confirmation needed - [Dev clusters disposable](dev_clusters_disposable.md) — Lyra's dev clusters are recreatable; mutate/break freely, no confirmation needed
- [Nix shell tooling](nix_shell_tooling.md) — any nixpkgs tool runs ad hoc via `nix run`/`nix shell nixpkgs#<pkg>`; a missing command is never a dead end
+3 -3
View File
@@ -1,6 +1,6 @@
--- ---
name: git-network-ops name: git-network-ops
description: Push/pull is remote-specific — both GitHub and Gitea (code.emmathe.dev) are agent-pushable in-sandbox (sandbox off); raise Gitea PRs with the tea CLI. description: Push/pull is remote-specific — GitHub is agent-pushable in-sandbox; Gitea (code.emmathe.dev) needs hand-off to Lyra.
metadata: metadata:
node_type: memory node_type: memory
type: feedback type: feedback
@@ -11,8 +11,8 @@ Whether a network op can run depends on which key the remote needs:
**GitHub remotes (e.g. csg-citrix-storefront/\*): pushable in-sandbox by the agent.** ssh-agent holds the decrypted `~/.ssh/id_ed25519` (`emma.thorpe@cloud.com`), which is authorized on GitHub. Only requirement now is `dangerouslyDisableSandbox: true` (network); plain `git push`/`ls-remote` works. Probe non-mutatively with `git ls-remote` first. (Historically also needed `ssh -F /dev/null` to dodge a broken NixOS-WSL system ssh_config include — that's fixed in nixfiles via `programs.ssh.systemd-ssh-proxy.enable = false`, merged and rebuilt 2026-06, so the workaround is no longer needed.) **GitHub remotes (e.g. csg-citrix-storefront/\*): pushable in-sandbox by the agent.** ssh-agent holds the decrypted `~/.ssh/id_ed25519` (`emma.thorpe@cloud.com`), which is authorized on GitHub. Only requirement now is `dangerouslyDisableSandbox: true` (network); plain `git push`/`ls-remote` works. Probe non-mutatively with `git ls-remote` first. (Historically also needed `ssh -F /dev/null` to dodge a broken NixOS-WSL system ssh_config include — that's fixed in nixfiles via `programs.ssh.systemd-ssh-proxy.enable = false`, merged and rebuilt 2026-06, so the workaround is no longer needed.)
**Gitea (`code.emmathe.dev`, e.g. nixfiles): pushable in-sandbox by the agent (as of 2026-07-14).** The ssh-agent now holds the `code.emmathe.dev` key (`git@code.emmathe.dev`), so `git push` works with `dangerouslyDisableSandbox: true` — it needs the agent socket plus `~/.ssh/known_hosts`, both reachable with sandbox off. Probe with `git ls-remote` first. Raise PRs with the `tea` CLI, which is installed and logged in to `code.emmathe.dev` (user `lyrathorpe`): `tea pr create --login code.emmathe.dev --repo lyrathorpe/nixfiles --base main --head <branch> --title "..." --description "..."`. Only fall back to hand-off if `ssh-add -l` (sandbox off) does NOT list the `code.emmathe.dev` key — then it dropped from the agent and Lyra must re-add it (`ssh-add ~/.ssh/code.emmathe.dev`, passphrase-protected). **Gitea (`code.emmathe.dev`, e.g. nixfiles): hand off to Lyra.** Needs `~/.ssh/code.emmathe.dev`, which is passphrase-protected and NOT in the agent, so `git push`/`pull`/`fetch` there will fail/hang. Pause, give Lyra the exact command (she runs `ssh-add ~/.ssh/code.emmathe.dev` once, then pushes).
**Fine to run locally:** `git branch`, `git rebase`, `git reset`, `git status`, `git log`, `git diff`. `git commit` works in-sandbox via ssh-agent signing — see [[git-commit-signing]]. **Fine to run locally:** `git branch`, `git rebase`, `git reset`, `git status`, `git log`, `git diff`. `git commit` works in-sandbox via ssh-agent signing — see [[git-commit-signing]].
**How to apply:** Both remotes → do it with sandbox off; probe with `git ls-remote` first, and raise Gitea PRs via `tea`. Hand off only if the Gitea key is missing from the agent. Related: [[git-conventions]]. **How to apply:** Check the remote host before a network op. GitHub → just do it (sandbox off). Gitea → hand off. Related: [[git-conventions]].
-23
View File
@@ -1,23 +0,0 @@
---
name: nix-shell-tooling
description: "Any nixpkgs tool can be run ad hoc via nix run / nix shell — a missing command is never a dead end during development"
metadata:
node_type: memory
type: feedback
originSessionId: dfb56b58-518b-4daf-b531-7119bb4a9534
---
Any tool in nixpkgs can be run without installing it into the environment. If a
command is missing during development, pull it from nixpkgs on the fly instead
of working around its absence or reporting the tool as unavailable.
**Why:** Lyra runs NixOS; the ambient PATH is deliberately minimal, but the full
nixpkgs set is always one command away. "command not found" is not a blocker.
**How to apply:**
- One-off run: `nix run nixpkgs#<pkg> -- <args>` (e.g. `nix run nixpkgs#jq -- .`).
- Tools on PATH for a session: `nix shell nixpkgs#<pkg> [nixpkgs#<pkg2> ...]`,
then run commands normally.
- Legacy form also works: `nix-shell -p <pkg> --run '<cmd>'`.
- Prefer this over hand-rolling a substitute for a tool that exists in nixpkgs.
@@ -1,24 +0,0 @@
---
name: sibo-workabout-mx-scanner
description: State of the Psion Workabout MX reverse-engineering / barcode-inventory project and how to resume it
metadata:
node_type: memory
type: project
originSessionId: 74de014e-9cf4-47f6-92f4-c34197ac1858
---
Long-running project (July 2026) reverse-engineering the **Psion Workabout MX** (SIBO OS, NEC V30MX, TopSpeed C) to build a barcode **inventory demo** (scan UPC → DBF database file; add stock, consume by a quantity unit) and, alongside, **complete device programming documentation**. Repo: Gitea **lyrathorpe/sibo-playground**, working branch **`feat/inventory-phase1-scan`** (unmerged). Gitea needs hand-off / the contents API for pushes — see [[git-network-ops]]; [[git-conventions]] for branch/PR rules.
**Committed on the branch (durable, survive reboot):**
- `docs/reference/00-08` + index — the SIBO/MX programming reference (building apps, system/OS, I/O devices, PLIB core, file system & DBF, UI, hardware, and RE'd boot/OS-call internals).
- `code/inventory/` — app scaffold: `upc.c/.h` (UPC-A check-digit validation, correct), `bcode.c/.h`, `scan.c` (Phase-1 diagnostics), `README.md`, **`SCANNER-API.md`** (all scanner findings), **`CONTINUATION.md`** (the on-device debugging procedure to finish).
- `docs/mx-re/toolchain-and-plan.md` — the RE toolchain.
- The **ROM `w2mx_v7.20f_eng.bin`** and the full **SDK + HDK** (manuals as `docs/*.txt`; headers/libs/`bar*.ldd` under `code/SIBOSDK/`; HDK under `code/HDK/`) are on the branch. `/tmp/claude/sibo/` working files (ROM slices, MAME rom dir, Ghidra/decomp output) are transient and reproducible from the toolchain doc.
**Scanner — key result:** the integral laser is driven as an **OO library object** in `SCANNER.DYL` (category token **`oscanner`**) via `p_getlibh``p_newsend`/`f_newsend``p_send`, over **LIBMANAGER (INT 0x84)** / **MESSMANAGER (INT 0x83)** — NOT raw device I/O. Confirmed on the physical device: `p_open("WL2:D")` + control ops **6** then **7** (`p_iow(chan,6); p_iow(chan,7)`) fire the laser to a good decode (green LED). Default Symbol2 11-byte param block: `04 3f 01 15 06 04 1e 80 0d 0a 06` (decoded output is CR/LF-terminated). Dead ends (do not retry): raw `TTY:D` reads, and the wand `BAR:` / `bar*.ldd` decoders (probe expansion slots → `-41`).
**Blocked on / next step:** the OO **message ordinals + parameter structs** for init / set-params / trigger / read. OLIB assigns ordinals dynamically across the class hierarchy (base classes in `olib`/`hwim`), so they resolve only at runtime — capture them with the **SIBO Debugger on the physical device** (remote debug over serial; it supports breakpoints inside DYLs). MAME cannot inject a barcode, so the last mile must be on hardware. Full step-by-step is in `code/inventory/CONTINUATION.md`.
**RE toolchain (reproducible):** the ROM is MAME machine **`psionwamx`**; run its debugger headless via `xvfb-run -a mame psionwamx -rompath roms -debug -debugscript CMDS -sound none -seconds_to_run N` (MAME lua input injection into the keyboard matrix does NOT work headless — a known limitation). Static: **radare2** (16-bit x86). Decompile: **Ghidra headless** (processor `x86:LE:16:Real Mode`, a Java GhidraScript — Ghidra 12 has no bundled Python). Get MAME/radare2/Ghidra via `nix-shell -p ...`. Details in `docs/mx-re/toolchain-and-plan.md`.
**Fallback to deliver value now:** Phase 2 (the DBF inventory: add stock, consume by quantity) can be built with keyboard UPC entry against `docs/reference/05-filesystem-dbf.md`, dropping the scanner in behind the same interface once retrieval is finished. [[docs-keep-updated]]
-51
View File
@@ -1,51 +0,0 @@
# macOS (nix-darwin) — `lyrathorpe-mac`
Flake host: `lyrathorpe-mac` (`aarch64-darwin`). Apple Silicon Mac managed by
**nix-darwin** from this same flake. Built via `mkDarwinHost` (single-user —
macOS owns the account; identity still comes from the registry). Files:
`configuration.nix`.
## What this host is
A macOS workstation. The interactive user environment (shell, git, editor,
Claude) is the **shared `../../home` bundle** — the same modules the Linux hosts
use — so the terminal experience matches. The Linux-only `desktop.nix`/`sway.nix`
are intentionally left out. This host config covers the macOS-specific layer:
system packages, Homebrew, and macOS UI defaults.
## Package sourcing
- **nixpkgs** (`environment.systemPackages`) for CLI tooling and libraries.
- **Homebrew**, owned declaratively by `nix-homebrew` (Rosetta enabled for
x86_64 formulae). The `brews`/`casks` lists are **authoritative**:
`onActivation.cleanup = "zap"` uninstalls anything not declared. GUI apps are
casks (nixpkgs darwin GUI support is unreliable); a few version-pinned
toolchains and the PWA host stay on brew for continuity.
- **Mac App Store** apps are **not** declarative: nix-darwin 26.05 runs
activation as root, and `mas` cannot reach the App Store session from root.
Install them by hand with `mas install <id>` from a GUI Terminal (the `mas`
CLI is in `environment.systemPackages`).
## macOS integration
- `security.pam.services.sudo_local`**Touch ID for sudo** (and
`darwin-rebuild`'s sudo prompt), kept in `sudo_local` so it survives OS
updates. `reattach` pulls in `pam_reattach` so Touch ID works inside tmux
(which the terminals auto-start).
- `system.defaults` — declarative dock / finder / global / trackpad preferences,
applied on activation and reversible. This is the main reason to run nix-darwin
beyond package management.
- The JetBrainsMono Nerd Font is installed to `/Library/Fonts`; set it in
iTerm2 (Settings → Profiles → Text → Font) so the tmux statusline glyphs
render.
## stateVersion
`system.stateVersion = 5` (the nix-darwin state version, an integer — not a
NixOS release string). Read `darwin-rebuild changelog` before changing it.
## Apply
```sh
darwin-rebuild switch --flake .#lyrathorpe-mac
```
-53
View File
@@ -1,53 +0,0 @@
# Work WSL box — `emmathorpe-edaas`
Flake host: `emmathorpe-edaas` (`x86_64-linux`). NixOS running under
**NixOS-WSL** on the corporate Windows machine. Headless: no Sway desktop
(`features.swayDesktop.enable = false`), plain WSL shell login. Files:
`configuration.nix`.
## What this host is
The day-to-day work environment. It layers the corporate Kubernetes / Helm /
Terraform / cloud toolchain and a couple of work-only editor language servers on
top of the shared home profile. The system config here is thin — it is mostly
WSL plumbing; the user-facing tooling lives in
[`../../users/emmathorpe/work.nix`](../../users/emmathorpe/work.nix).
## WSL specifics
- `wsl.enable`, default user `emmathorpe`, Windows PATH interop and start-menu
launchers on. `/etc/hosts` generation is off (`generateHosts = false`).
- **Docker Desktop integration**, not the native daemon as the primary path:
`wsl.extraBin` shims the coreutils/`groupadd`/`usermod` binaries Docker
Desktop's `wsl-distro-proxy` expects, and `docker-desktop-proxy.script` is
patched to the real proxy path. The native `virtualisation.docker` is also
enabled (with `enableOnBoot` + `autoPrune`).
- `programs.ssh.systemd-ssh-proxy.enable = false` — the NixOS-WSL store is a
read-only VHD owned by `nobody`, and OpenSSH rejects the generated
`ssh-proxy` Include as "Bad owner or permissions", which would break ssh/git
for every command. The vsock proxy it provides is unused under WSL.
- `networking.hostName = "emmathorpe-edaas"` matches the flake attribute so
`nh os switch` resolves without `-H`.
## Renovate review timer
The host-table entry sets `users.emmathorpe.linger = true` so the user's
`systemd --user` instance stays alive without an open login session. That keeps
the daily headless **Renovate PR review** timer firing — defined in
[`../../users/emmathorpe/renovate-review.nix`](../../users/emmathorpe/renovate-review.nix)
(imported only from `work.nix`, so it exists on this machine alone). See that
file's header for the auth (Vertex AI ADC), triage policy, and caveats.
## stateVersion
`system.stateVersion = "24.11"` — the release this box was first installed on.
Leave it; it freezes stateful defaults and is not meant to track the current
nixpkgs.
## Apply
```sh
sudo nixos-rebuild switch --flake .#emmathorpe-edaas
# or, since the hostname matches the attribute:
nh os switch
```
+5
View File
@@ -62,6 +62,11 @@
features.swayDesktop.enable = false; features.swayDesktop.enable = false;
# Opt out of fleet-wide SSSD/Authentik LDAP auth: this is a work-managed WSL
# box, not part of the personal directory. Every other NixOS host inherits the
# default-true from modules/sssd.nix.
services.authentikLdap.enable = false;
# NOTE: this user's systemd --user lingering -- so the home-manager renovate # NOTE: this user's systemd --user lingering -- so the home-manager renovate
# timer fires without an open login session -- is enabled from the host table # timer fires without an open login session -- is enabled from the host table
# in flake.nix (users.emmathorpe.linger = true) and applied by # in flake.nix (users.emmathorpe.linger = true) and applied by
+1 -1
View File
@@ -48,7 +48,7 @@ gigabit ports.
## Login ## Login
Graphical login via a Wayland greeter — `greetd` running ReGreet inside the Graphical login via a Wayland greeter — `greetd` running ReGreet inside the
`cage` kiosk compositor — configured centrally in `../../modules/sway.nix` for `cage` kiosk compositor — configured centrally in `lyrathorpe/swaywm.nix` for
every Sway host (gated on `features.swayDesktop.enable`). The greeter is forced every Sway host (gated on `features.swayDesktop.enable`). The greeter is forced
to the Dvorak layout to match the console and Sway session. Set the user to the Dvorak layout to match the console and Sway session. Set the user
password (`passwd lyrathorpe`) after install, or the greeter cannot password (`passwd lyrathorpe`) after install, or the greeter cannot
+2 -3
View File
@@ -15,7 +15,7 @@ Headless `aarch64-linux` server with two roles:
```sh ```sh
nixos-generate-config --root /mnt nixos-generate-config --root /mnt
# copy /mnt/etc/nixos/hardware-configuration.nix over # copy /mnt/etc/nixos/hardware-configuration.nix over
# hosts/RPi5/hardware-configuration.nix in this repo, then commit # system/machine/RPi5/hardware-configuration.nix in this repo, then commit
``` ```
`hardware-configuration.nix` in this directory is a **placeholder** committed `hardware-configuration.nix` in this directory is a **placeholder** committed
only so the host evaluates in CI. The machine will not boot correctly until it only so the host evaluates in CI. The machine will not boot correctly until it
@@ -28,8 +28,7 @@ Headless `aarch64-linux` server with two roles:
nh os switch nh os switch
``` ```
4. Give the login user a password (`passwd lyrathorpe`) and confirm the key in 4. Give the login user a password (`passwd lyrathorpe`) and confirm the key in
the user registry (`../../users/registry.nix`, applied by `system/modules/ssh.nix` is the one you will connect with.
`../../modules/ssh.nix`) is the one you will connect with.
## Docker socket (security) ## Docker socket (security)
+1 -1
View File
@@ -35,7 +35,7 @@ change and `radeon` stays idle.
## Login ## Login
Graphical login via a Wayland greeter — `greetd` running ReGreet inside the Graphical login via a Wayland greeter — `greetd` running ReGreet inside the
`cage` kiosk compositor — configured centrally in `../../modules/sway.nix` for `cage` kiosk compositor — configured centrally in `lyrathorpe/swaywm.nix` for
every Sway host (gated on `features.swayDesktop.enable`). The greeter is forced every Sway host (gated on `features.swayDesktop.enable`). The greeter is forced
to the Dvorak layout to match the console and Sway session. Set the user to the Dvorak layout to match the console and Sway session. Set the user
password (`passwd lyrathorpe`) after install, or the greeter cannot password (`passwd lyrathorpe`) after install, or the greeter cannot
+130
View File
@@ -0,0 +1,130 @@
# Authentik LDAP authentication for NixOS hosts.
#
# Wires SSSD (System Security Services Daemon) to the Authentik LDAP outpost so
# every Linux host authenticates users against the same directory that backs the
# SSO stack. Enabled by default on every NixOS host via baseModules; the EDaaS
# WSL box opts out (services.authentikLdap.enable = false) because it is a
# work-managed Windows-hosted environment.
#
# The Authentik LDAP provider exposes NON-standard object classes/attributes
# (goauthentik.io/ldap/user, goauthentik.io/ldap/group) alongside the POSIX
# attributes (uid, uidNumber, gidNumber, homeDirectory), so the schema mappings
# below are explicit rather than relying on an RFC2307 default.
#
# The bind password is NOT inlined: services.sssd.config renders to the world-
# readable Nix store, so the credential is delivered out-of-band by agenix as an
# sssd.conf drop-in under /etc/sssd/conf.d/ (SSSD merges conf.d/*.conf after the
# main file). See secrets/README.md.
{
config,
lib,
...
}:
let
cfg = config.services.authentikLdap;
# Directory coordinates for the Authentik LDAP provider.
ldapUri = "ldaps://ldap.lyrapup.pet:636";
searchBase = "dc=ldap,dc=goauthentik,dc=io";
bindDn = "cn=sssd-bind,ou=users,dc=ldap,dc=goauthentik,dc=io";
in
{
options.services.authentikLdap.enable =
lib.mkEnableOption "SSSD authentication against the Authentik LDAP outpost"
// {
default = true;
};
config = lib.mkIf cfg.enable {
services.sssd = {
enable = true;
# Non-secret sssd.conf. The bind password is injected separately via the
# agenix conf.d drop-in (ldap_default_authtok lives there, not here) to
# keep it out of the Nix store.
config = ''
[sssd]
config_file_version = 2
services = nss, pam
domains = default
[nss]
# Do not walk the whole directory for `getent passwd` etc.
filter_users = root
filter_groups = root
[pam]
[domain/default]
# --- Providers --------------------------------------------------------
id_provider = ldap
auth_provider = ldap
chpass_provider = none
access_provider = permit
# --- Connection -------------------------------------------------------
ldap_uri = ${ldapUri}
ldap_search_base = ${searchBase}
ldap_default_bind_dn = ${bindDn}
ldap_default_authtok_type = password
# ldap_default_authtok is supplied by the agenix drop-in in conf.d.
# --- TLS (LDAPS on 636; no StartTLS) ---------------------------------
ldap_id_use_start_tls = false
ldap_tls_reqcert = demand
# --- Schema: Authentik LDAP provider ---------------------------------
# Authentik returns DN-valued group membership (member/memberOf), so
# rfc2307bis (not rfc2307) is the correct base schema.
ldap_schema = rfc2307bis
# Users: goauthentik.io/ldap/user, keyed by uid; POSIX attrs are
# standard names (uidNumber/gidNumber/homeDirectory).
ldap_user_object_class = goauthentik.io/ldap/user
ldap_user_name = uid
ldap_user_uid_number = uidNumber
ldap_user_gid_number = gidNumber
ldap_user_home_directory = homeDirectory
ldap_user_gecos = displayName
ldap_user_shell = loginShell
# Groups: goauthentik.io/ldap/group, keyed by cn.
ldap_group_object_class = goauthentik.io/ldap/group
ldap_group_name = cn
ldap_group_gid_number = gidNumber
ldap_group_member = member
# --- Behaviour --------------------------------------------------------
cache_credentials = true
enumerate = false
'';
};
# agenix delivers the bind password as an sssd.conf drop-in. The decrypted
# plaintext IS a valid conf.d snippet:
#
# [domain/default]
# ldap_default_authtok = <the bind password>
#
# SSSD requires conf.d files to be root-owned and 0600 or it ignores them.
age.secrets.ldap-bind = {
file = ../secrets/ldap-bind.age;
path = "/etc/sssd/conf.d/01-ldap-authtok.conf";
owner = "root";
group = "root";
mode = "0600";
};
# Restart SSSD when the credential drop-in changes. agenix writes secrets in
# a system activation script that runs before systemd (re)starts services on
# a `switch`, so the file is present by the time sssd starts; the trigger
# picks up rotations of the bind password.
systemd.services.sssd.restartTriggers = [ config.age.secrets.ldap-bind.path ];
# Create home directories on first login for LDAP users (they have no
# locally-provisioned home). NixOS wires nss + the SSSD PAM stack when
# services.sssd.enable is true; mkHomeDir adds pam_mkhomedir to it.
security.pam.services.login.makeHomeDir = true;
security.pam.services.sshd.makeHomeDir = true;
};
}
+92
View File
@@ -0,0 +1,92 @@
# Secrets (agenix)
Encrypted secrets for the fleet, managed with [agenix](https://github.com/ryantm/agenix).
Each secret is an age-encrypted file (`*.age`) encrypted to a set of recipient
public keys declared in [`secrets.nix`](./secrets.nix). A host decrypts its
secrets at activation using its SSH **host** key
(`/etc/ssh/ssh_host_ed25519_key`), so every host that must read a secret has to
be listed as a recipient for it.
`secrets.nix` is read only by the `agenix` CLI. It is never imported into the
NixOS evaluation.
## Secrets in this repo
| File | Purpose | Recipients |
| --------------- | ----------------------------------------------------------------------- | ----------------------------------- |
| `ldap-bind.age` | SSSD → Authentik LDAP bind credential, as an `sssd.conf` drop-in snippet | all SSSD-enabled hosts (not EDaaS) |
Consumed by [`modules/sssd.nix`](../modules/sssd.nix) via
`age.secrets.ldap-bind.path`, which places the decrypted snippet at
`/etc/sssd/conf.d/01-ldap-authtok.conf`.
> **`ldap-bind.age` is not committed yet.** Only `ldap-bind.age.PLACEHOLDER`
> ships in this change (real host recipient keys and the real password were not
> available when it was written). Follow the steps below to create the real
> secret, then delete the `.PLACEHOLDER`.
## Owner setup checklist
Run these once (per new host or when the bind password rotates):
### 1. Collect host recipient keys
On each SSSD-enabled host (all Linux hosts **except** EDaaS):
```sh
cat /etc/ssh/ssh_host_ed25519_key.pub
```
Paste each value into the matching placeholder in `secrets.nix`, replacing the
`AAAA_PLACEHOLDER_REPLACE_ME_*` strings. (Optionally uncomment and set `admin`
to an operator user key so the secret can be edited off-host.)
### 2. Encrypt the bind password
The plaintext must be a **full sssd.conf drop-in snippet**, because SSSD cannot
read `ldap_default_authtok` from a separate file — it only merges `conf.d/*.conf`.
The content is exactly:
```ini
[domain/default]
ldap_default_authtok = <the sssd-bind service-account password>
```
Use the password of the `sssd-bind` (Terraform: `sssd-bind`) Authentik LDAP
service account. Then, from the repo root:
```sh
# Requires the agenix CLI: `nix run github:ryantm/agenix -- -e secrets/ldap-bind.age`
cd secrets
agenix -e ldap-bind.age
```
An `$EDITOR` opens; paste the two-line snippet above, save, quit. agenix writes
the encrypted `ldap-bind.age`. Commit it and delete `ldap-bind.age.PLACEHOLDER`.
### 3. Rekey after changing recipients
If you add/remove hosts in `secrets.nix`, re-encrypt every secret to the new
recipient set:
```sh
cd secrets
agenix -r
```
### 4. DNS
`ldap.lyrapup.pet` must resolve to the Authentik LDAP outpost and serve LDAPS on
port 636 with a certificate the hosts trust (`ldap_tls_reqcert = demand`). If the
cert is not from a system-trusted CA, add it to the hosts' trust store
(`security.pki.certificateFiles`) or relax `ldap_tls_reqcert` in
`modules/sssd.nix`.
### 5. Rebuild
```sh
sudo nixos-rebuild switch --flake .#<host>
```
Verify with `getent passwd <ldap-user>` and `id <ldap-user>`.
+21
View File
@@ -0,0 +1,21 @@
THIS IS A PLACEHOLDER, NOT A REAL AGE SECRET.
The real secrets/ldap-bind.age is produced by the repo owner with `agenix -e`
(see secrets/README.md) and is a binary age-encrypted blob. It is intentionally
NOT committed here because:
* the real host age recipients are not available to the author of this change
(they are each host's /etc/ssh/ssh_host_ed25519_key.pub), and
* fabricating an encrypted blob or fake host keys would be misleading.
Committing this file as `ldap-bind.age` would let modules/sssd.nix reference
`../secrets/ldap-bind.age` and evaluate, but SSSD would fail to decrypt it at
runtime. Do ONE of the following before deploying:
1. Preferred: generate the real secret (secrets/README.md), commit it as
secrets/ldap-bind.age, and delete this .PLACEHOLDER file.
The decrypted plaintext must be a valid sssd.conf drop-in (NOT the bare
password):
[domain/default]
ldap_default_authtok = <the sssd-bind service-account password>
+15
View File
@@ -0,0 +1,15 @@
let
lyrathorpe-mbp = "ssh-ed25519 AAAA_PLACEHOLDER_REPLACE_ME_mbp";
lyrathorpe-t400 = "ssh-ed25519 AAAA_PLACEHOLDER_REPLACE_ME_t400";
lyrathorpe-macpro31 = "ssh-ed25519 AAAA_PLACEHOLDER_REPLACE_ME_macpro31";
lyrathorpe-rpi5 = "ssh-ed25519 AAAA_PLACEHOLDER_REPLACE_ME_rpi5";
sssdHosts = [
lyrathorpe-mbp
lyrathorpe-t400
lyrathorpe-macpro31
lyrathorpe-rpi5
];
in
{
"ldap-bind.age".publicKeys = sssdHosts;
}