<feed xmlns='http://www.w3.org/2005/Atom'>
<title>archsetup/docs, branch main</title>
<subtitle>Builds a full dev workstation from a bare Arch Linux install.
</subtitle>
<id>https://git.cjennings.net/archsetup/atom?h=main</id>
<link rel='self' href='https://git.cjennings.net/archsetup/atom?h=main'/>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/archsetup/'/>
<updated>2026-08-26T00:54:55+00:00</updated>
<entry>
<title>docs(spec): split the live upgrade and hold the kernel for a dedicated session</title>
<updated>2026-08-26T00:54:55+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-08-26T00:54:55+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/archsetup/commit/?id=e777ddca34f5ea8040d9905c55b853a7089d6682'/>
<id>urn:sha1:e777ddca34f5ea8040d9905c55b853a7089d6682</id>
<content type='text'>
I rewrote the guarded-upgrade spec around a script the maintenance panel
calls: pacman -Syu --ignore on what the live-update guard would block plus
the kernel set, then the AUR remainder and topgrade with the system step
off. The boot oneshot now completes only the deferred GPU set. Kernels wait
for a session I choose, behind a gate on the DKMS build, the initramfs, and
a pre-pacman snapshot, because on velox a failed zfs-dkms rebuild after the
swap can't be aborted and leaves the new kernel unbootable. All seven
decisions are closed.

Also closes the ratio Emacs 31 task (upgraded, verified on Wayland) and
files gcalcli in the installer.
</content>
</entry>
<entry>
<title>docs: draft the guarded-upgrade spec and file the open velox items</title>
<updated>2026-08-25T18:58:26+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-08-25T18:58:26+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/archsetup/commit/?id=77447d02dcc3d68abfbf6e097f8dc0b0f7d443ac'/>
<id>urn:sha1:77447d02dcc3d68abfbf6e097f8dc0b0f7d443ac</id>
<content type='text'>
The topgrade freshness warning never clears because the stamp is written only when topgrade exits 0, and the live-update guard plus any failing ecosystem step makes that exit almost unreachable. The spec (DRAFT) weighs four ways to close that. Its open decisions are state versus recency, an armed boot-time oneshot, an arch-only boot run and a persistent one-shot arm flag.

Four tasks filed:
- the spec's decisions, review and decomposition
- ratio pulling .emacs.d before its Emacs 31.1 upgrade
- the stale lock-screen clock after a real sleep
- the Signal tray icon waybar can't read. Electron 43 registers it under a well-known bus name and answers property reads only there. Downgrading is closed off by the database schema.

The Lua-port task got its date and the sleep task carries the two Aug 23 hibernate failures. Wrap hygiene moved 42 aged resolved subtrees into the task archive.
</content>
</entry>
<entry>
<title>fix(cmail): let Bridge setup finish without cmail-action</title>
<updated>2026-08-21T09:09:35+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-08-21T09:09:35+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/archsetup/commit/?id=7ed8e5701c83c5e26c2416b9a0365c12f801f7ed'/>
<id>urn:sha1:7ed8e5701c83c5e26c2416b9a0365c12f801f7ed</id>
<content type='text'>
I moved the cmail-action install out of cmail-setup-finish.sh and left behind a guard that aborts when the tool is missing. The script mentions cmail-action five times and invokes it zero — every hit is a comment or that check. So a Bridge provisioning script hard-failed over a mail client it never touches, and a fresh machine got Bridge ready, the cert exportable, the service startable, and a script that refused to configure any of it.

It warns with the same remedy and carries on now. That also drops an ordering neither repo needs: Bridge setup no longer requires rulesets cloned and installed first.

The sequence around it hadn't caught up either. Both the checklist and the installer's completion message still opened with "clone claude-templates to ~/projects/claude-templates", the same dead path that broke the script, and neither mentioned rulesets at all. So the documented order was: clone a stale repo to a path that doesn't exist, log into Bridge, then run a script that aborts pointing at a directory nothing created. Both now run Bridge first and name rulesets as what it is — not a prerequisite for those steps, but the thing mbsync wants.

I also rephrased the header claim that the symlink "arrives on its own". True on a machine that runs agent sessions, since session start runs make install, and false on one that doesn't. It states the condition now instead of the consequence.
</content>
</entry>
<entry>
<title>docs: correct the clock/DNS deadlock mechanism to DNSSEC</title>
<updated>2026-08-19T19:32:07+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-08-19T19:32:07+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/archsetup/commit/?id=7b68f77a9b99e5400472986cb80bae5fb92e2320'/>
<id>urn:sha1:7b68f77a9b99e5400472986cb80bae5fb92e2320</id>
<content type='text'>
I reproduced the failure by winding velox's clock back 27 days with chronyd stopped, and the cause is not what I recorded. Resolved logged signature-expired against the root DNSKEY and every DS beneath it. The DoT handshake to 1.1.1.1:853 verified clean at that same clock, and the Cloudflare certificate runs Dec 2025 to Dec 2026, so it was never outside its window. An RRSIG window is days to weeks while a certificate is good for a year, so a skew that breaks DNSSEC normally leaves DoT untouched.

DNSSEC=allow-downgrade does not rescue it either. Resolved downgrades when a server lacks DNSSEC support, and a signature-window failure is a validation failure, so no downgrade fires. Six retries over eighteen seconds plus a reset-server-features, all dead. I briefly believed otherwise off a test whose success was a cache hit.

The fix itself is verified end to end. With the clock wound back and no DNS at all, chronyd reached the IP-addressed source and stepped the clock straight back.

Also settled: the clock landed on 2026-07-23 because that is systemd 261.2's build date to the minute, and systemd advances a garbage RTC to its own build epoch at boot.
</content>
</entry>
<entry>
<title>fix(installer): give NTP an IP source so a wrong clock can't kill DNS</title>
<updated>2026-08-19T19:16:46+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-08-19T19:16:46+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/archsetup/commit/?id=afbf011aa0937b5702b6d8c1bfca0809ed809425'/>
<id>urn:sha1:afbf011aa0937b5702b6d8c1bfca0809ed809425</id>
<content type='text'>
The installer wrote both halves of a deadlock. configure_dns pins DNSOverTLS=yes with DNSSEC=yes, and both validate against the wall clock. The chrony step enables chronyd without writing a config, so the machine runs Arch's stock one, whose only source is a pool hostname.

Boot with a wrong clock and DoT validation fails, so nothing resolves. Chrony then can't resolve its pool, so the clock stays wrong. Neither side moves, and recovery takes a second device. Velox hit this on the road and I diagnosed it from a phone.

An address needs no DNS and no certificate, so two IP-addressed sources in a drop-in break the cycle whatever caused the skew. Stock chrony.conf reads no drop-in directory, so it gets a confdir line pointing at one.

post-rebuild-check grows a sixth check for the same property. It reads sources only from files chrony is told to read. A drop-in beside a chrony.conf that never names its directory is one chrony won't open, so counting it would pass the machine while describing a file nothing reads.

The failure taxonomy gains the mode in its DNS layer and a cluster 5 triage line. Its egress-layer clock entry assumed working DNS and offered set-ntp true, which can't recover this. That entry now says so.
</content>
</entry>
<entry>
<title>feat(scripts): add post-rebuild-check for the gaps a reinstall leaves</title>
<updated>2026-08-17T18:28:55+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-08-17T18:28:55+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/archsetup/commit/?id=4dfecc5f705c9df0d1f8e8245276146f826e16cc'/>
<id>urn:sha1:4dfecc5f705c9df0d1f8e8245276146f826e16cc</id>
<content type='text'>
A rebuilt machine looks finished and isn't. Five gaps surfaced on velox within two days of its reinstall. Three looked fine on inspection: a stowed unit file, an enabled-looking timer, a present git clone. The script runs those five checks and prints a line for each whether or not it finds anything.

Every probe fails closed. A check that cannot run reports a finding rather than a pass, which matters more here than anywhere: a silent no-op in the checker is the exact failure it exists to catch. `systemctl --user` exits 1 with empty output when there is no user bus, so reading that as "no failed units" would call a machine healthy at the moment nothing was checked. Calls are bounded for the same reason. A check that hangs reports nothing at all, and the machine most in need of checking is the one it hangs on.

I suppressed three classes of finding, each because the live run produced them and reality disagreed. A timer-activated service is supposed to sit linked and not enabled. One expected tooling file is seed-only, so most projects legitimately never have one. Vendored package trees ship their own example files. Left in, those were 19 of the first run's 27 findings, and a check nobody reads is a check that isn't run.

The post-install checklist points at it, and 58 tests cover it.
</content>
</entry>
<entry>
<title>feat(installer): grant brightness control to the video group</title>
<updated>2026-08-14T03:34:42+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-08-14T03:34:42+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/archsetup/commit/?id=a42e06d6ef4fc6296a17c69580aa9911667a6c88'/>
<id>urn:sha1:a42e06d6ef4fc6296a17c69580aa9911667a6c88</id>
<content type='text'>
Arch's brightnessctl ships no udev rules and leans on logind, which grants brightness writes only to the active seat session. Anything outside that session (a script, an ssh shell, a panel launched into a different one) meets root-owned sysfs and gets EPERM. On velox both panel sliders were dead after a fresh install.

configure_backlight_access writes a udev rule making the backlight and keyboard-LED brightness attributes group-writable by video, which create_user already adds the user to. I granted the keyboard LED to video rather than input: anyone in input can read every input device, which is too much authority for dimming a keyboard.

essential_services is already marked complete on machines installed before this step, so a re-run skips them. The post-install checklist carries the manual route.
</content>
</entry>
<entry>
<title>feat: enable the podman API socket and ship the camera udev grant</title>
<updated>2026-08-09T16:47:36+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-08-09T16:20:43+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/archsetup/commit/?id=169dab71a063f4fab6c46f95e11d6e85b140cb8f'/>
<id>urn:sha1:169dab71a063f4fab6c46f95e11d6e85b140cb8f</id>
<content type='text'>
Two one-time machine setups from the winvm handoffs now happen at install time.

- The devops podman block enables the rootless podman API socket. Socket-activated, so it costs nothing idle, and API clients like Pods fail with an empty window without it. enable_user_service grew an optional wants-target argument because a socket unit's [Install] is WantedBy=sockets.target. The old default.target link would never socket-activate.
- install_camera_passthrough_rules ships 72-usb-passthrough-cameras.rules: GROUP="video", MODE="0660" plus the uaccess tag on the OBSBOT and BRIO USB IDs, so usbredirect can claim them for VM passthrough. The filename is load-bearing: logind's ACL is applied by 73-seat-late.rules, so the tag only works from a file sorting below 73. A test pins that property.

Both are live on ratio (the old 99- rules file is retired there). Whether uaccess alone would suffice from the corrected position is untested and stays documented as a hypothesis.
</content>
</entry>
<entry>
<title>docs: add the post-install checklist and podman/camera-udev notes</title>
<updated>2026-08-09T16:47:36+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-08-08T09:55:52+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/archsetup/commit/?id=f88dfa7620a289f1aff63e446ebdd31853ac3d51'/>
<id>urn:sha1:f88dfa7620a289f1aff63e446ebdd31853ac3d51</id>
<content type='text'>
The checklist is the standing home for manual post-first-boot steps: bluetooth pairing and the Proton Bridge login start it. The podman-socket and camera-udev notes carry the evidence for the filed install-time task, including why uaccess alone can't grant a raw USB node.
</content>
</entry>
<entry>
<title>docs: keep the timeline face's design history, not its engine</title>
<updated>2026-08-01T03:45:08+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-08-01T03:45:08+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/archsetup/commit/?id=1e81319c7a41623a8e361ef3cc3898b9a023530b'/>
<id>urn:sha1:1e81319c7a41623a8e361ef3cc3898b9a023530b</id>
<content type='text'>
Six prototypes from the world-face work, kept as the record of how the design got to where it is. The first five are self-contained; prototype 6 and the option-A pages load the engine, so they read here but do not run here.

The engine stays out on purpose. It carries the list of places I travel to, with coordinates, and this repo is published — a design record is not worth publishing that for. The README says how to copy the modules in locally to run prototype 6, and gitignore keeps such a copy from being committed by accident.

Dropping the prototype test target with it: the engine and its tests now live in one place, so the suite runs once rather than twice over identical files.
</content>
</entry>
</feed>
