| 1 |
|
- |
# Mountaineer — todo
|
| 2 |
|
- |
|
| 3 |
|
- |
## Key Paths
|
| 4 |
|
- |
|
| 5 |
|
- |
- `MANIFESTO.md` — principles, three purposes, "Dull Edge" channel, refusal-to-default carve-out
|
| 6 |
|
- |
- `STACK.md` — every default Mountaineer ships, defended; includes the compatibility contract at the top
|
| 7 |
|
- |
- `PROFILES.md` — what server/laptop/bastion/homelab actually mean
|
| 8 |
|
- |
- `AESTHETICS.md` — committed picks: surfaces-and-`sysop` doctrine, flatwhite palette, Iosevka, ratatui conventions, yambar segments, river defaults, swaylock, TTY login
|
| 9 |
|
- |
- `ROUTES.md` — recipe library; first-boot, mesh, bastion, Caddy+podman, restic drill landed
|
| 10 |
|
- |
- `BUILD-TOOLS.md` — orientation map for the upstream toolchain (apk, mkimage, mkinitfs, apkovl, lbu, ZFS, zfsbootmenu, etc.) plus Mountaineer-authored components and open toolchain decisions
|
| 11 |
|
- |
- `INSTALL.md` — defended spec for install mechanics: two ISOs from one source, disk layout, ZFS pool/datasets, ARC sizing, zfsbootmenu, apkovl shape and source tree, installer flow, v0.1 dogfood gate
|
| 12 |
|
- |
- `references/` — third-party material we want to keep alongside the spec
|
| 13 |
|
- |
- `../build/` — build pipeline (apkovl-src, mkimg profile, installer, QEMU iteration loop)
|
| 14 |
|
- |
- `../sysop/` — Cargo workspace: CLI binary + sysop-tui primitives crate
|
| 15 |
|
- |
- `/Users/max/Code/CONTRIBUTING.md` — cross-cutting rules (no Docker, no PRs, latest deps, file size, todo format)
|
| 16 |
|
- |
- `/Users/max/Code/CLAUDE.md` — repo-wide LLM instructions
|
| 17 |
|
- |
|
| 18 |
|
- |
## Spec
|
| 19 |
|
- |
|
| 20 |
|
- |
- [x] Manifesto: principles, thesis, three purposes, refusal-to-default, what-we-won't-do, channel name
|
| 21 |
|
- |
- [x] STACK foundation: apk, busybox ash, busybox coreutils, OpenSSH
|
| 22 |
|
- |
- [x] STACK time/DNS: chrony, unbound
|
| 23 |
|
- |
- [x] STACK ingress: Caddy
|
| 24 |
|
- |
- [x] STACK secrets/backup: age, sops, restic
|
| 25 |
|
- |
- [x] STACK interactive userland: ripgrep, fd, sd, bat, eza, dust, procs, bottom, zoxide, lnav, starship
|
| 26 |
|
- |
- [x] STACK spicy picks: s6+s6-rc, helix, zellij, nushell, podman, ZFS on root, Vector, nftables, msmtp, ifupdown-ng+iwd, Nebula
|
| 27 |
|
- |
- [x] STACK Sysadmin (graphical): river, foot, yambar, fuzzel+cliphist, fnott, grim+slurp, swaylock/idle/bg, pipewire+wireplumber, seatd, brightnessctl, tlp, browser-refusal
|
| 28 |
|
- |
- [x] Compatibility contract section in STACK.md
|
| 29 |
|
- |
- [x] Cross-check MANIFESTO against STACK.md (filesystem ext4→ZFS, added chrony/msmtp/ifupdown-ng+iwd/OpenSSH/graphical-layer summary/sd/lnav/starship)
|
| 30 |
|
- |
- [x] Review PROFILES.md against the current stack (added `sd`, removed redundant docker-alias claim)
|
| 31 |
|
- |
- [ ] Decide whether any foundation pick needs promotion to spicy (Caddy? nftables?) — defer until ROUTES surfaces operational stress
|
| 32 |
|
- |
|
| 33 |
|
- |
## ROUTES.md
|
| 34 |
|
- |
|
| 35 |
|
- |
- [x] First-boot checklist (keys, hostname, time, firewall, resolver verification)
|
| 36 |
|
- |
- [x] Stand up the mesh (Nebula) — promoted to STACK as default overlay mesh
|
| 37 |
|
- |
- [x] Bastion + ProxyJump (non-mesh floor)
|
| 38 |
|
- |
- [x] Reverse-proxy a podman container with Caddy auto-HTTPS
|
| 39 |
|
- |
- [x] Restic restore drill
|
| 40 |
|
- |
- [ ] sops + age workflow for service secrets in a repo
|
| 41 |
|
- |
- [ ] WireGuard peer setup via `sysop wg` (raw point-to-point, distinct from mesh)
|
| 42 |
|
- |
- [ ] Mail relay upgrade path (msmtp → OpenSMTPD)
|
| 43 |
|
- |
- [ ] ZFS snapshot/rollback walkthrough for a bad upgrade
|
| 44 |
|
- |
- [ ] Nebula cert rotation + CRL distribution
|
| 45 |
|
- |
- [ ] Reconcile `sysop` subcommands assumed by routes (`hostname set`, `net status`, `resolver verify`, `ssh keys list/add`, `ssh hostkeys`, `fw status/apply`, `mesh ca init`, `mesh sign`, `mesh bundle`, `mesh join`, `mesh status`, `mesh reload`, `mesh revoke`) with the `sysop` subsystem when it opens
|
| 46 |
|
- |
- [ ] Resolve overlap between `sysop ssh keys` and per-host age keys used as bundle recipients in mesh route — resolved structurally (both go under `sysop id host`), but only SSH half ships v1
|
| 47 |
|
- |
- [ ] Confirm `nebula` apk repo placement (community expected) — install mechanics work
|
| 48 |
|
- |
|
| 49 |
|
- |
## Install / overlay mechanics — SPEC
|
| 50 |
|
- |
|
| 51 |
|
- |
- [x] apkovl layout (spec'd in INSTALL.md)
|
| 52 |
|
- |
- [x] Install script: ZFS pool creation, dataset layout, zfsbootmenu integration (spec'd in INSTALL.md)
|
| 53 |
|
- |
- [x] `zfs_arc_max` install-time sizing table (spec'd in INSTALL.md)
|
| 54 |
|
- |
- [x] Sysadmin vs Server build artifacts (two ISOs, one source) — strict subset enforced by build structure
|
| 55 |
|
- |
- [x] First-boot flow: hostname, ssh keys, network, browser-pick prompt (spec'd)
|
| 56 |
|
- |
|
| 57 |
|
- |
## Install / overlay mechanics — IMPLEMENTATION
|
| 58 |
|
- |
|
| 59 |
|
- |
Lives in `../build/`. **Full installer runs end-to-end in QEMU as of build 16**; testable ISO at `../build/out/mountaineer-server-v3.23-x86_64.iso`. Next: Framework 13 hardware verification. See `../build/build_notes.md` for traps and gotchas.
|
| 60 |
|
- |
|
| 61 |
|
- |
### Done
|
| 62 |
|
- |
|
| 63 |
|
- |
- [x] apkovl source tree — `apkovl-src/common/` (target/shared) + `apkovl-src/live/` (live-only overlay). manifest.toml profiles: `server` (common+live + extras) and `server-target` (common only).
|
| 64 |
|
- |
- [x] apkovl builder (`tools/build-apkovl.sh`) — reproducible-best-effort, GNU tar required, supports `--extra <dir>` for dynamic payload merge
|
| 65 |
|
- |
- [x] `mkimg.mountaineer_server.sh` — `arch=x86_64`, `profile_base` parent, `profile_abbrev="mountaineer"` (31-char volid), `apkovl="genapkovl-mountaineer-server.sh"`
|
| 66 |
|
- |
- [x] `genapkovl-mountaineer-server.sh` — builds target apkovl, stages installer + target apkovl + docs into extras, emits live apkovl
|
| 67 |
|
- |
- [x] Installer (`installer/install.sh`) — 17-step shell+dialog, `--dry-run` AND `--config <yaml>` modes, auto-detects config at `/media/cdrom/install.yaml` etc.
|
| 68 |
|
- |
- [x] Top-level `build.sh` — explicitly wipes `.work/apkovl_*` and `.work/image-*` before mkimage (the apkovl section caches by name only; see build_notes.md)
|
| 69 |
|
- |
- [x] QEMU iteration loop (`tools/qemu/`):
|
| 70 |
|
- |
- [x] `vm-up.sh` — cloud-image-based builder VM, cloud-init provisioning ~60s, zero-touch `setup-alpine` skipped
|
| 71 |
|
- |
- [x] `vm-build.sh` — rsync repo → build inside VM → copy artifacts back
|
| 72 |
|
- |
- [x] `vm-test-boot.sh` — boot ISO in fresh target VM, no install config, wait for sshd
|
| 73 |
|
- |
- [x] `vm-test-install.sh` — boot ISO with unattended install config attached (FAT image labeled CIDATA via hdiutil)
|
| 74 |
|
- |
- [x] ISO produced — 660 MB at `out/mountaineer-server-v3.23-x86_64.iso`
|
| 75 |
|
- |
- [x] ISO boots in QEMU — Mountaineer apkovl applied, custom `/etc/issue` rendered at login prompt
|
| 76 |
|
- |
- [x] Cloud image: `generic_alpine-3.23.4-x86_64-uefi-cloudinit-r0.qcow2` (192 MB) downloaded and resized to 30 GiB
|
| 77 |
|
- |
- [x] Mountaineer SSH keypair at `.vms/keys/builder_ed25519` (gitignored, ed25519, doesn't collide with operator keys)
|
| 78 |
|
- |
|
| 79 |
|
- |
### Build 16 — testable ISO (May 17, 13:15 local)
|
| 80 |
|
- |
|
| 81 |
|
- |
- [x] Builds 11–16: full apkovl → mkimage → ISO pipeline works
|
| 82 |
|
- |
- [x] ISO boots in UEFI QEMU; sshd reachable on 2223; `mountaineer-install` at `/usr/local/sbin/`
|
| 83 |
|
- |
- [x] Installer runs all 17 steps end-to-end against `/dev/vda` (incl. ZBM v3.1.0 EFI fetch+verify+install)
|
| 84 |
|
- |
- [x] Reboot into installed disk lands in UEFI → BOOTX64.EFI (ZBM) → Mountaineer kernel; sshd listens on guest:22
|
| 85 |
|
- |
- [x] Build 16 has every fix from #11–#22 baked into the ISO (no live-patching needed)
|
| 86 |
|
- |
|
| 87 |
|
- |
### Next steps (resume here)
|
| 88 |
|
- |
|
| 89 |
|
- |
- [ ] **Framework 13 hardware test**: `dd` `out/mountaineer-server-v3.23-x86_64.iso` to USB, boot F13 in UEFI mode, run installer against `/dev/nvme0n1` (triggers the `p1`/`p2` partition-naming branch in `installer/install.sh`). If F13 needs wifi setup before install: `iwctl` in the live env.
|
| 90 |
|
- |
- [ ] Re-verify the install → reboot → ssh-in cycle in QEMU with build 16 (the new ISO carries the kcl-property + pubkey fixes that haven't yet been exercised end-to-end together).
|
| 91 |
|
- |
- [ ] First-boot route walk-through: ROUTES.md "first boot" beyond hostname (network up, operator key works, services up under s6-rc) — verify each.
|
| 92 |
|
- |
- [ ] Reproducible builds: static audit landed in `../build/build_notes.md` (apkovl half looks reproducible by construction; ISO half has 8 candidate non-determinism sources). Next concrete step is the apkovl-only diff (`build-apkovl.sh` twice, compare sha) before spinning the VM. Requires GNU tar on the build host.
|
| 93 |
|
- |
|
| 94 |
|
- |
### Build history — fixes burned through (institutional memory)
|
| 95 |
|
- |
|
| 96 |
|
- |
Originating builds 1–10 (pre-ISO-boots era):
|
| 97 |
|
- |
- [x] **#1** Invented `--apkovl` flag for mkimage → mkimage uses `apkovl=<script.sh>` in profile + `build_apkovl()` runs it
|
| 98 |
|
- |
- [x] **#2** Hyphen in profile name (`mountaineer-server`) → shell function `profile_$NAME` requires underscore
|
| 99 |
|
- |
- [x] **#3** Made up `profile_abuild` parent → real parent is `profile_base` in mkimg.base.sh
|
| 100 |
|
- |
- [x] **#4** Used `profile_arch=` → real var is `arch=` (silent skip if wrong via `list_has $ARCH $arch || return 0`)
|
| 101 |
|
- |
- [x] **#5** Listed `zfs-utils` in world → doesn't exist in Alpine; userland is in `zfs` package
|
| 102 |
|
- |
- [x] **#6** mtools missing from builder → grub-efi uses `mformat` to format ESP image; added to cloud-init packages
|
| 103 |
|
- |
- [x] **#7** Volume label 32-char truncation w/ trailing space broke grub → `profile_abbrev="mountaineer"` keeps volid under 32 chars without trailing space
|
| 104 |
|
- |
- [x] **#8** Genapkovl produced wrong shape → refactored to two profiles (server, server-target) + extras; live apkovl embeds target+installer+docs
|
| 105 |
|
- |
- [x] **#9** Live env had no ssh access → `apkovl-src/live/root/.ssh/authorized_keys`, `sshd_config.d/10-live.conf` (PermitRootLogin prohibit-password), `etc/runlevels/default/sshd` symlink
|
| 106 |
|
- |
- [x] **#10** sshd doesn't start despite runlevel symlink → apkovl applies after default runlevel reached; `/etc/local.d/sshd.start` retroactively starts it
|
| 107 |
|
- |
|
| 108 |
|
- |
Builds 11–16 (ISO-boots-and-installs era — full session details in `../build/build_notes.md`):
|
| 109 |
|
- |
- [x] **#11** `tools/build-apkovl.sh`: `find ... -exec touch -d '@0'` followed symlinks into root-owned `/etc/init.d/*` and silently broke the live apkovl. Fix: `find ! -type l`.
|
| 110 |
|
- |
- [x] **#12** `apkovl-src/common/etc/ssh/sshd_config`: removed `PrintLastLog yes` — unsupported by Alpine's OpenSSH.
|
| 111 |
|
- |
- [x] **#13** `apkovl-src/live/etc/network/interfaces` missing → networking failed in live env. Added lo + eth0 DHCP.
|
| 112 |
|
- |
- [x] **#14** `build.sh` source-tree-hash cache-busting was ineffective — mkimage's apkovl section caches by name only. Replaced with explicit `rm -rf .work/apkovl_* .work/image-*`.
|
| 113 |
|
- |
- [x] **#15** `installer/install.sh` step 7: busybox `mount` can't auto-detect FAT32 on freshly-formatted ESP → explicit `-t vfat`.
|
| 114 |
|
- |
- [x] **#16** `installer/install.sh` step 9: world-file parser kept inline `# comments` as package tokens → `sed -e 's/#.*//' | xargs`.
|
| 115 |
|
- |
- [x] **#17** `installer/install.sh` step 9: apk on new root reports `UNTRUSTED signature` → seed `/mnt/etc/apk/keys/` from live env.
|
| 116 |
|
- |
- [x] **#18** `installer/install.sh` step 13: `chroot /mnt ssh-keygen -A` failed (`/dev/null` missing) → `bind_target_dev` for `/dev /proc /sys` before chroot ops; unbind in finalize.
|
| 117 |
|
- |
- [x] **#19** `apkovl-src/common/etc/apk/world`: `dialog` missing — installer's interactive prompts need it.
|
| 118 |
|
- |
- [x] **#20** `installer/install.sh` step 12: ZBM placeholder → fetch v3.1.0 `zfsbootmenu-release-x86_64-linux6.18.EFI`, sha256-pinned, installed to `/EFI/BOOT/BOOTX64.EFI`.
|
| 119 |
|
- |
- [x] **#21** `tools/qemu/vm-test-install.sh`: `--daemonize` mode for scripted runs; FAT config image step gracefully skipped if host lacks `dosfstools`/`mtools`.
|
| 120 |
|
- |
- [x] **#22** `installer/install.sh` step 12: kernel cmdline silent on serial → set `org.zfsbootmenu:commandline='ro quiet console=tty0 console=ttyS0,115200'` on root dataset (bypass `run` — its `$@` unquoted word-splits).
|
| 121 |
|
- |
|
| 122 |
|
- |
### Iteration tips (current)
|
| 123 |
|
- |
|
| 124 |
|
- |
- Builder VM persists across sessions: `ps -p $(cat .vms/builder.pid)` or `ssh -p 2222 ... true`. Restart: `./tools/qemu/vm-up.sh`.
|
| 125 |
|
- |
- `vm-build.sh` does rsync + build.sh + pull. `build.sh` now auto-nukes `.work/apkovl_*` and `.work/image-*` so apkovl-src edits propagate without manual cache stomping.
|
| 126 |
|
- |
- Full nuke (rare; restores ~30 min of TCG kernel/modloop build): `ssh -p 2222 ... 'rm -rf /home/build/mountaineer-build/.work'`.
|
| 127 |
|
- |
- VM install test cycle: `./tools/qemu/vm-test-install.sh --daemonize` → `ssh-keygen -R [127.0.0.1]:2223 -f .vms/known_hosts` → wait for port → `ssh ... 'cat > /usr/local/sbin/mountaineer-install' < installer/install.sh` (live ISO has no scp/sftp) → `ssh ... mountaineer-install --config /tmp/install.yaml`.
|
| 128 |
|
- |
- After install: standalone boot of installed disk uses same OVMF setup, `-drive file=.vms/disks/target.qcow2,if=virtio,format=qcow2`, no `-cdrom`, port-forward 2224. See `../build/build_notes.md` for the full incantation.
|
| 129 |
|
- |
|
| 130 |
|
- |
### Package availability — v3.23 main+community (verified, don't re-test)
|
| 131 |
|
- |
|
| 132 |
|
- |
- [x] **IN**: nebula 1.9.7, caddy 2.11.2, helix 25.07.1, zellij 0.42.2, nushell 0.104.1, podman 5.7.0, s6 2.13, s6-rc 0.5.6, execline 2.9.7, ifupdown-ng 0.12.1, iwd 3.10, msmtp 1.8.32, restic 0.18.1, age 1.2.1, sops 3.11.0, chrony 4.8, unbound 1.24.2, nftables 1.1.5, all userland (rg, fd, sd, bat, eza, dust, procs, bottom, zoxide, lnav, starship), `zfs` + `zfs-lts`
|
| 133 |
|
- |
- [x] **NOT IN ANY repo** (main, community, edge/testing checked): zfsbootmenu, vector
|
| 134 |
|
- |
- [x] **Action**: vendor zfsbootmenu — installer fetches v3.1.0 release EFI from `github.com/zbm-dev/zfsbootmenu/releases` (sha256-pinned)
|
| 135 |
|
- |
- [ ] **Action**: vendor vector from `vector.dev/releases` (static binary) — Mountaineer overlay candidate
|
| 136 |
|
- |
- [ ] **Future**: ZBM version pump discipline — update `ZBM_VERSION`+`ZBM_EFI_SHA256` in `installer/install.sh` from upstream `sha256.txt`; pin the linux6.x variant matching the running kernel's major.minor
|
| 137 |
|
- |
|
| 138 |
|
- |
### Open install-mechanics decisions
|
| 139 |
|
- |
|
| 140 |
|
- |
- [x] Where the installer's helper binary lives — MVP: shell-only; ratatui v0.2 goal
|
| 141 |
|
- |
- [ ] Kernel/zfs-lts pairing discipline at upgrade time
|
| 142 |
|
- |
- [ ] ESP redundancy on multi-disk hosts (out of v0.1 scope; layout shouldn't block it)
|
| 143 |
|
- |
- [ ] Installer log routing during install (tmpfs only, serial console, post-install capture)
|
| 144 |
|
- |
- [ ] Non-interactive `--config` schema for unattended installs — basic version implemented; need to formalize
|
| 145 |
|
- |
- [ ] Signing keys: ISO, apkovl, and overlay-repository APKs — three keys probably, lifecycle TBD
|
| 146 |
|
- |
- [x] zfsbootmenu install path in installer step 12 — pinned v3.1.0 release EFI, sha256-verified, written to `/EFI/BOOT/BOOTX64.EFI`. Boot entry registration via `efibootmgr` is best-effort (fallback works without it on hardware whose firmware respects the default removable-media path).
|
| 147 |
|
- |
- [ ] mkimg bootloader name — `boot_efi="grub-efi"` works; verify token names hold across aports updates
|
| 148 |
|
- |
- [ ] OpenRC vs s6-rc on the live ISO — installed system is s6-rc; live ISO inherits Alpine's default (OpenRC) for MVP; v0.2 cleanup
|
| 149 |
|
- |
|
| 150 |
|
- |
### v0.1 install dogfood gate (8 criteria in INSTALL.md)
|
| 151 |
|
- |
|
| 152 |
|
- |
- [x] ISO builds (660 MB produced)
|
| 153 |
|
- |
- [x] ISO boots in UEFI QEMU
|
| 154 |
|
- |
- [ ] Reproducible ISO builds (same source + same builder → same bytes)
|
| 155 |
|
- |
- [ ] Boots on physical x86_64 hardware (Framework 13)
|
| 156 |
|
- |
- [x] Installer completes end-to-end on a clean disk in under 15 minutes (QEMU/TCG; faster on real hw)
|
| 157 |
|
- |
- [~] Installed system boots, comes up on network, accepts operator SSH key, passes first-boot route unmodified — *boots and listens on :22; banner exchange + pubkey-login flow still to verify*
|
| 158 |
|
- |
- [ ] Caddy + podman route works on fresh Server install
|
| 159 |
|
- |
- [ ] Nebula mesh route works between Server (lighthouse) and Sysadmin (laptop)
|
| 160 |
|
- |
- [ ] Restic restore drill route works
|
| 161 |
|
- |
- [ ] Reboot leaves the system in the same state (idempotent)
|
| 162 |
|
- |
|
| 163 |
|
- |
## `sysop` CLI
|
| 164 |
|
- |
|
| 165 |
|
- |
- [x] Subcommands committed in spec: `sysop wifi`, `sysop net`, `sysop wg`, `sysop upgrade`, `sysop info <role>`, `sysop notifications mute`, `sysop service enable/disable <name>`, `sysop podman-s6`, `sysop motd on/off` (from AESTHETICS)
|
| 166 |
|
- |
- [x] ratatui-based; tool-agnostic backends per principle 14
|
| 167 |
|
- |
- [x] **`sysop id` decided as first-class identity subsystem** — replaces "`sysop ssh keys` for authorized_keys"; operators have plural identity (personal vs client work, multiple meshes, hardware-backed keys)
|
| 168 |
|
- |
- [x] **Scopes**: named bundles of operator-owned identity material (one for solo operator, many for contractor)
|
| 169 |
|
- |
- [x] **Per-host identity stays fixed slot**: one SSH host key per host (plus later one age recipient); host can be enrolled in multiple scopes
|
| 170 |
|
- |
- [x] **v1 scope: SSH only** — age and Nebula CA bindings deferred
|
| 171 |
|
- |
- [x] **Signing-as-a-service is v1 design requirement** — operations split: *use* (sign/decrypt/derive — works on all backends including hardware) vs *extract* (read bytes — best-effort, fails on hardware-backed scopes). v1 must model this even though it only ships software-backed scopes
|
| 172 |
|
- |
- [x] **SSH-specific shape decided**: `sysop id agent <scope>` returns ssh-agent socket, backends plug behind it (`file://`, `agent://`, `yubikey://` v2). Consumers set `SSH_AUTH_SOCK`. `sysop id sign` for non-ssh-agent cases. `sysop id export` is extract verb (always succeeds on `file://` in v1)
|
| 173 |
|
- |
- [ ] Final subcommand list spec'd (sketch: `sysop id list/show/new/import/rotate/authorize/revoke/agent/sign/export`, `sysop id host show/rotate`)
|
| 174 |
|
- |
- [ ] Reconcile route-assumed subcommands when sysop section opens
|
| 175 |
|
- |
- [ ] STACK.md: dedicated `sysop` section (currently buried in ifupdown-ng paragraph)
|
| 176 |
|
- |
- [ ] First-boot route step 6 re-scoped from "SSH keys" to "Identity"
|
| 177 |
|
- |
|
| 178 |
|
- |
### `sysop id` open design questions (v1, SSH-only)
|
| 179 |
|
- |
|
| 180 |
|
- |
- [ ] Scope storage location: `~/.config/mountaineer/scopes/<name>/`? Backup/portability story?
|
| 181 |
|
- |
- [ ] Active-scope mechanism: shell env var, on-disk state file, per-command flag, or all three?
|
| 182 |
|
- |
- [ ] `~/.ssh/config` interaction: write `IdentityFile`/`Match` snippets, or stay out of operator's config?
|
| 183 |
|
- |
- [ ] SSH CA model: support operator-side SSH CAs (sign user certs, push to `TrustedUserCAKeys`)? Leaning out-of-scope for v1
|
| 184 |
|
- |
- [ ] Hardware key bootstrap: how does `sysop id import --yubikey` work — discover-and-claim, or operator-supplied handle?
|
| 185 |
|
- |
- [ ] Cross-host scope sync (two laptops): operator's problem (rsync/git) but needs defended recommendation in docs
|
| 186 |
|
- |
|
| 187 |
|
- |
### Per-subcommand implementation
|
| 188 |
|
- |
|
| 189 |
|
- |
Status: MVP slice complete plus the follow-ups previously deferred. `sysop info` now ships version display + server/sysadmin profile filter. `sysop net` shows default-route + resolver liveness. `sysop wifi` auto-detects the interface, pre-scans, rescans on `r`, and has a `disconnect` verb. `sysop storage` has a pool detail screen (`zpool status -P` parsing) and a dataset view (`d`). All `--summary` outputs unchanged. Active: TBD — natural next is `sysop id` (large) or live-system verification on a Mountaineer install.
|
| 190 |
|
- |
|
| 191 |
|
- |
**sysop-tui primitives**
|
| 192 |
|
- |
|
| 193 |
|
- |
- [ ] Re-verify on a real Mountaineer system (foot terminal, Iosevka, flatwhite-configured 24-bit color). Host-side macOS eyeball passed; values may still need tuning once foot/helix/zellij/yambar ship the matching theme
|
| 194 |
|
- |
- [ ] `Footer` right-align option (deferred until a subcommand needs a left status segment)
|
| 195 |
|
- |
- [ ] Help overlay primitive (`?` action classifies; no overlay renderer yet)
|
| 196 |
|
- |
|
| 197 |
|
- |
**sysop info**
|
| 198 |
|
- |
|
| 199 |
|
- |
- [x] Version field on the detail screen — lazy per-tool `<bin> --version` lookup with per-session cache; populated for 18 of 30 roles via `version_cmd` in roles.toml
|
| 200 |
|
- |
- [x] Server vs Sysadmin profile filter — `--server` / `--sysadmin` flags filter via the `profile` field on Role (12 graphical roles tagged `sysadmin`)
|
| 201 |
|
- |
- [ ] Runtime override: layer `/etc/mountaineer/roles.toml` on top of the compiled-in defaults (principle 2 — operator can see/edit). Currently only `include_str!` source
|
| 202 |
|
- |
- [ ] Fill `version_cmd` for the remaining roles (busybox, s6, ifupdown-ng, iwd, cliphist, swaybg, swappy, seatd, tlp) — most need a tool-specific incantation
|
| 203 |
|
- |
- [ ] Replace synchronous Command::output with a bounded-timeout variant if any version_cmd is found to hang on a real Mountaineer system
|
| 204 |
|
- |
|
| 205 |
|
- |
**sysop motd**
|
| 206 |
|
- |
|
| 207 |
|
- |
- [x] `/etc/profile.d/sysop-motd.sh` glue script — calls `sysop motd print` on login (silent no-op if sysop missing). Lives in `../build/apkovl-src/common/`
|
| 208 |
|
- |
- [ ] Tip set worth revisiting once more `sysop` verbs exist — currently 6, drawn from AESTHETICS
|
| 209 |
|
- |
- [ ] Decide whether `sysop motd print` should respect a "first-login-of-the-day" suppression to avoid tip spam in tmux-heavy sessions
|
| 210 |
|
- |
|
| 211 |
|
- |
**sysop services**
|
| 212 |
|
- |
|
| 213 |
|
- |
- [ ] Detail view per service: dependencies, log path, time-in-state. Needs `s6-svstat` shell-out
|
| 214 |
|
- |
- [ ] Enable / disable / restart verbs. Requires real s6-rc; can't be meaningfully mocked, gate on live-system testing
|
| 215 |
|
- |
- [ ] Reflect transient state from `s6-rc-update` runs (currently only Up/Down are derived from `s6-rc -a list` + `-d list`)
|
| 216 |
|
- |
- [ ] Backend mismatch indicator — if mock is active on what looks like a Mountaineer system, surface that as a fault
|
| 217 |
|
- |
|
| 218 |
|
- |
**sysop storage**
|
| 219 |
|
- |
|
| 220 |
|
- |
- [x] Detail view: vdev tree, error counters (read/write/cksum), scrub progress — `zpool status -P <name>` parsing with indent-preserving vdev tree, scan/errors lines, per-row red error counts
|
| 221 |
|
- |
- [x] Dataset view: `zfs list -H -o name,used,avail,mountpoint` — `d` key from pool list opens it; `Esc` returns
|
| 222 |
|
- |
- [ ] Snapshot management — read-only first (list), write later
|
| 223 |
|
- |
- [ ] Scrub trigger verb. Same caveat as services enable/disable — gate on live system
|
| 224 |
|
- |
|
| 225 |
|
- |
**sysop wifi**
|
| 226 |
|
- |
|
| 227 |
|
- |
- [x] Pre-scan: `iwctl station <dev> scan` + 1.5s settle before `get-networks`; `r` rescans from the list view with "scanning…" indicator
|
| 228 |
|
- |
- [x] Auto-detect interface name from `iwctl device list` (mode=station), fall back to `wlan0`
|
| 229 |
|
- |
- [x] Disconnect verb — `sysop wifi disconnect`
|
| 230 |
|
- |
- [ ] Enterprise (8021x) flow — out of scope for MVP, currently a no-op on Enter; needs identity + cert/PEAP plumbing
|
| 231 |
|
- |
- [ ] iwctl `get-networks` text parsing is fragile; verify on a real Mountaineer install and adjust. D-Bus interface is the upgrade path if the text format proves too noisy
|
| 232 |
|
- |
- [ ] Promote the password input to a `sysop-tui` primitive once a second consumer exists (probably `sysop id` for passphrase entry)
|
| 233 |
|
- |
|
| 234 |
|
- |
**sysop net**
|
| 235 |
|
- |
|
| 236 |
|
- |
- [x] Default route / gateway display — `ip -json route show default` parsing, rendered as a header row above the interface list
|
| 237 |
|
- |
- [x] Resolver liveness check — 200 ms TCP probe to 127.0.0.1:53 (`reachable` / `unreachable`), rendered alongside default-route
|
| 238 |
|
- |
- [ ] SSID enrichment for wireless: read `/var/lib/iwd/` or `iwctl station <iface> show` to populate `ssid` on the real backend (mock already returns it)
|
| 239 |
|
- |
- [ ] Yambar `net` segment should reflect the active default-route interface, not just wired-first. Currently wired-first heuristic
|
| 240 |
|
- |
- [ ] Detail view: per-interface MAC, MTU, link speed (ethtool for wired)
|
| 241 |
|
- |
|
| 242 |
|
- |
### Deferred subcommands (post-MVP)
|
| 243 |
|
- |
|
| 244 |
|
- |
- [ ] `sysop logs <service>` — lnav front
|
| 245 |
|
- |
- [ ] `sysop upgrade`, `sysop wg`, `sysop notifications mute`, `sysop podman-s6`
|
| 246 |
|
- |
|
| 247 |
|
- |
### sysop tooling deferred (post-MVP)
|
| 248 |
|
- |
|
| 249 |
|
- |
- [ ] Tab completion (bash/zsh/nushell)
|
| 250 |
|
- |
- [ ] Man pages (mdoc; generated from clap?)
|
| 251 |
|
- |
- [ ] CI / reproducible builds story
|
| 252 |
|
- |
- [ ] Release artifact shape (static-linked musl binary in Alpine apk)
|
| 253 |
|
- |
|
| 254 |
|
- |
### `sysop id` deferred (decided but not v1)
|
| 255 |
|
- |
|
| 256 |
|
- |
- [ ] **Age slot in scopes** + per-host age recipient. Mesh route assumes age-encrypted bundles addressed to per-host recipients; v1-compatible variant needed (operator-decrypted-locally or scp over existing ssh path)
|
| 257 |
|
- |
- [ ] **Nebula CA slot in scopes**. v1: mesh CA management is its own thing under `~/.config/mountaineer/nebula-ca/`, age-encrypted independently. Reunified when scope model is mature
|
| 258 |
|
- |
- [ ] **Multi-backend broker design** (URI-shaped refs to ssh-agent, yubikey/PKCS#11, Bitwarden/`bw`, pass/gopass, native `file://`). Constraints agreed: native is default, supported set caps at five backends, proprietary `op` is documented-not-supported, per-backend capability matrix in docs. v1 URI shape reserved in config.toml so v2 doesn't break format
|
| 259 |
|
- |
|
| 260 |
|
- |
### Nebula external-signer gap (record for when Nebula CA slot ships)
|
| 261 |
|
- |
|
| 262 |
|
- |
- [ ] `nebula-cert sign` upstream takes CA key as file; no signer-plugin interface today. When scopes back Nebula CAs: (a) materialize CA key to tmpfs briefly (OK for software, not for hardware), (b) upstream a signer interface, (c) ship Mountaineer-patched `nebula-cert`. Raise with upstream before painting ourselves in
|
| 263 |
|
- |
|
| 264 |
|
- |
## podman-s6 integration
|
| 265 |
|
- |
|
| 266 |
|
- |
- [ ] Service-template library: quadlet analog for s6-rc
|
| 267 |
|
- |
- [ ] Restart policy, dependency declarations, log routing into Vector
|
| 268 |
|
- |
- [ ] One canonical example (Caddy container) checked in
|
| 269 |
|
- |
|
| 270 |
|
- |
## v0.1 release dogfood gate
|
| 271 |
|
- |
|
| 272 |
|
- |
- [ ] Mountaineer project infra runs on s6-rc before v0.1 ships
|
| 273 |
|
- |
- [ ] Decide what infra: build server, docs site, package mirror
|
| 274 |
|
- |
- [ ] Decide where the infra lives, who pays for it
|
| 275 |
|
- |
|
| 276 |
|
- |
## UX / aesthetics
|
| 277 |
|
- |
|
| 278 |
|
- |
- [x] Lives in `AESTHETICS.md` with source material at `references/classic-high-affordance.md`
|
| 279 |
|
- |
- [x] First pass: open questions turned into defended picks (palette, font, ratatui, yambar, river, swaylock, TTY)
|
| 280 |
|
- |
- [x] Doctrine: surfaces (bar, lock screen, motd, TTY) display; `sysop` operates. Every non-ok state visible on a surface has a `sysop <role>` invocation.
|
| 281 |
|
- |
|
| 282 |
|
- |
### Implementation follow-ups
|
| 283 |
|
- |
|
| 284 |
|
- |
- [x] `sysop-tui` crate: frame, footer keybind renderer, flatwhite color constants, selection widget, reserved globals — lives in `../sysop/crates/sysop-tui/`
|
| 285 |
|
- |
- [x] `sysop info <role>` — first subcommand, role→tool registry shipped (33 roles in `../sysop/crates/sysop/data/roles.toml`)
|
| 286 |
|
- |
- [x] `sysop motd on/off/print` — opt-out tip rotation, time-bucketed; config at `/etc/mountaineer/sysop.toml` (`$SYSOP_CONFIG` override for host dev)
|
| 287 |
|
- |
- [x] `sysop services` (read-only) — s6-rc backend with mock fallback for host dev; `--summary` emits `ok`/`N down` for the yambar `svc` segment to consume. Enable/disable verbs deferred to live-system phase
|
| 288 |
|
- |
- [x] `sysop storage` — zfs backend with mock fallback; pool list + detail (vdev tree, error counters, scrub line) + dataset view (`d` from pool list). `--summary` emits `ok`/`degraded` for the yambar `zfs` segment. Scrub trigger and snapshot ops deferred
|
| 289 |
|
- |
- [x] `sysop wifi` — iwctl backend with mock fallback. Auto-iface, pre-scan, rescan, disconnect, selection + masked password + connect. Enterprise (8021x) deferred
|
| 290 |
|
- |
- [x] `sysop net` — iproute2 backend with mock fallback. Default-route + resolver liveness header rows; interface list with state + addresses + SSID; `--summary` for the yambar `net` segment. SSID enrichment, detail view deferred
|
| 291 |
|
- |
- [x] Overlay configs in flatwhite + Iosevka: foot, helix (theme), zellij (theme), fuzzel, fnott, swaylock — `../build/apkovl-src/sysadmin/`
|
| 292 |
|
- |
- [x] yambar config: tag + clock + bat + net + vol + svc + zfs segments; svc/zfs/net feed scripts at `usr/local/libexec/yambar/{svc,zfs,net}.sh` calling `sysop <subcmd> --summary`. Conditional `vpn`/`dnd` still to add
|
| 293 |
|
- |
- [x] `/etc/issue` content set to spec form (`Mountaineer 3.23 (Dull Edge) · \n · \l`); version literal needs a bump when Alpine base moves
|
| 294 |
|
- |
- [x] `/etc/profile.d/sysop-motd.sh` — shim calling `sysop motd print`; silent no-op if sysop missing
|
| 295 |
|
- |
- [ ] river init: minimal active bind set + commented-available blocks from upstream example — apkovl-src path TBD
|
| 296 |
|
- |
- [ ] Activate the `sysadmin` manifest profile (`sources = ["common", "live", "sysadmin"]`, separate `world` file) so the new overlay tree actually ships
|
| 297 |
|
- |
- [ ] `/etc/mountaineer/sysop.toml` seed with `[motd] enabled = true|false` written by installer based on Y/n prompt
|
| 298 |
|
- |
- [ ] Installer prompt: "Show a `sysop` tip on each login? [Y/n]" → writes motd-enable choice into the seed file above
|
| 299 |
|
- |
- [ ] Conditional yambar segments: `vpn wg0` and `dnd` (notifications muted) — need state probes
|
| 300 |
|
- |
- [ ] Version-pump discipline for `/etc/issue` — emit from build.sh into apkovl-src as a generated file, or accept hand-bump per Alpine release
|
| 301 |
|
- |
|
| 302 |
|
- |
## Project / community
|
| 303 |
|
- |
|
| 304 |
|
- |
- [ ] Contribution workflow: mailing list vs GitHub — undecided
|
| 305 |
|
- |
- [ ] License pick for the overlay + scripts
|
| 306 |
|
- |
- [ ] Where the project itself is hosted (sourcehut / Codeberg / self-hosted Forgejo)
|