diff --git a/docs/superpowers/contracts/atomic-install-live-scripts-2026-11-12.json b/docs/superpowers/contracts/atomic-install-live-scripts-2026-11-12.json new file mode 100644 index 00000000..f837ba1a --- /dev/null +++ b/docs/superpowers/contracts/atomic-install-live-scripts-2026-11-12.json @@ -0,0 +1,11 @@ +{ + "title": "Atomic install of live periodic-setup scripts", + "objective": "Installing the periodic system scripts never truncates a file a service is running; the target gets a new inode and a bad template leaves the live file untouched.", + "acceptance_criteria": [ + "Target inode changes and the old inode keeps its bytes", + "A template failing bash -n is not installed and leaves no temp file", + "hosts-file-monitor.service is restarted only if it was running" + ], + "out_of_scope": ["Re-running the installer on the host", "Other installers"], + "verifier": "lib/tests/test_services_atomic_install.sh, lib/tests/run_all.sh, vmbox guest check" +} diff --git a/docs/superpowers/evidence/atomic-install-live-scripts-2026-11-12.json b/docs/superpowers/evidence/atomic-install-live-scripts-2026-11-12.json new file mode 100644 index 00000000..753b86b9 --- /dev/null +++ b/docs/superpowers/evidence/atomic-install-live-scripts-2026-11-12.json @@ -0,0 +1,40 @@ +{ + "intent": "The periodic setup replaces live /usr/local/bin scripts by atomic rename, so a running hosts-file-monitor never executes a half-written file.", + "scope": [ + "linux_configuration/periodic_background/lib/atomic_install.sh", + "linux_configuration/periodic_background/setup_periodic_system.sh", + "linux_configuration/periodic_background/lib/periodic_browser.sh", + "linux_configuration/periodic_background/lib/tests/test_services_atomic_install.sh", + "Non-goal: re-running the installer on the host" + ], + "changes": [ + "install_rendered_script renders to a temp file beside the target, bash -n, chmod/chown, mv -f", + "Maintenance script, hosts monitor, browser wrapper and auto-update script use it", + "restart_if_active restarts hosts-file-monitor.service after install" + ], + "verification": [ + { + "command": "lib/tests/test_services_atomic_install.sh", + "result": "pass", + "evidence": "red against in-place write (inode kept, old fd saw new bytes), 12 passed after" + }, + { + "command": "vm run atomichosts guest_check.sh", + "result": "pass", + "evidence": "real hosts-file-monitor template under systemd: inode 85520 -> 85524, MainPID 745 -> 768, service active" + }, + { + "command": "lib/tests/run_all.sh", + "result": "pass", + "evidence": "all check_and_enable_services test files passed" + } + ], + "risks": [ + "Temp file left in /usr/local/bin if the installer is killed between mktemp and mv", + "Restart briefly interrupts the hosts monitor" + ], + "rollback": [ + "git revert the commit; the old installer overwrites in place again", + "Check systemctl is-active hosts-file-monitor.service" + ] +} diff --git a/docs/superpowers/evidence/i3blocks-gaming-budget-automation-2026-10-05.json b/docs/superpowers/evidence/i3blocks-gaming-budget-automation-2026-10-05.json deleted file mode 100644 index a45deee9..00000000 --- a/docs/superpowers/evidence/i3blocks-gaming-budget-automation-2026-10-05.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "intent": "The i3 gaming-budget block shows the new earned_time 0.3.0 automation earner (anki-guard's Automation-deck quota) as โš™ instead of its text label.", - "scope": [ - "linux_configuration/i3blocks/gaming_budget.sh", - "linux_configuration/tests/test_i3blocks_gaming_budget.sh", - "Non-goal: no change to how the budget is computed or rendered; only the icon map grows" - ], - "changes": [ - "Icon map gains automation: โš™ beside workout ๐Ÿ’ช, leetcode ๐Ÿงฉ, reading ๐Ÿ“–, anki ๐Ÿ—‚", - "The .rules.earners test case lists an automation earner and expects '+โš™30m' in registry order" - ], - "verification": [ - { - "command": "bash linux_configuration/i3blocks/gaming_budget.sh (live enforcer API, after restarting on earned-time 0.3.0)", - "result": "pass", - "evidence": "'๐ŸŽฎ 1h19 left ยท 5h44/7h03 +๐Ÿ“–1h +๐Ÿ—‚30m +โš™30m'; /api/budget lists automation 0/1800" - }, - { - "command": "bash linux_configuration/tests/test_i3blocks_gaming_budget.sh", - "result": "pass", - "evidence": "All gaming_budget checks passed, incl. '+๐Ÿงฉ1h +๐Ÿ“–1h +๐Ÿ—‚30m +โš™30m +Piano1h30'" - } - ], - "risks": [ - "โš™ (U+2699) without a variation selector may render as text-style in some fonts; it is still one glyph wide." - ], - "rollback": [ - "git revert this commit; automation then shows as its label 'Automation30m'" - ] -} diff --git a/docs/superpowers/evidence/wg-firewall-no-docker-restart-dot-853-2026-10-04.json b/docs/superpowers/evidence/wg-firewall-no-docker-restart-dot-853-2026-10-04.json deleted file mode 100644 index 5838cf83..00000000 --- a/docs/superpowers/evidence/wg-firewall-no-docker-restart-dot-853-2026-10-04.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "intent": "A firewall apply (setup_wireguard_ssh.sh allow-*) no longer bounces Docker and its containers, and the DoT rule (tcp/853 on wg0) survives every re-apply instead of vanishing.", - "scope": [ - "linux_configuration/features/lib/wg_firewall.sh: ruleset replaces only 'table inet filter'; restore_docker_rules restarts docker only when its nat DOCKER chain is gone; ALLOW_DOT renders 853 on wg0; allow_dot()", - "linux_configuration/features/setup_wireguard_ssh.sh, lib/wg_sshd.sh: ALLOW_DOT default/save_config, allow-dot subcommand and usage", - "linux_configuration/features/lib/dot_resolver_install.sh: wg-bound branch goes through 'setup_wireguard_ssh.sh allow-dot' instead of a runtime 'nft add rule'", - "Tests: new lib/tests/test_wg_firewall.sh; test_dot_resolver_install.sh wg-branch cases rewritten", - "Non-goal: the publicly bound DoT branch still adds its rate-limited rule at runtime" - ], - "changes": [ - "'flush ruleset' replaced by 'table inet filter' + 'delete table inet filter' before the definition: atomic, leaves Docker's (iptables-nft) and Tailscale's tables alone, and loads on a first boot", - "restore_docker_rules checks 'iptables -t nat -S DOCKER' and only restarts docker when that chain is missing", - "sshd-death rollback deletes only inet filter instead of flushing the whole ruleset", - "ALLOW_DOT persisted in .wireguard_ssh.conf; open_firewall calls allow-dot so the rule is rendered on every apply" - ], - "verification": [ - { - "command": "vmbox guest dotfw (iptables-nft, docker 29 + busybox probe, an extra 'inet sentinel' table): old HEAD allow-homeassistant vs new allow-dot", - "result": "pass", - "evidence": "old: all non-inet-filter tables deleted, docker + probe restarted; new re-apply from healthy state: docker ActiveEnterTimestamp and probe StartedAt unchanged, sentinel and docker tables kept; verify clean; systemctl restart nftables and first-boot load (no table) rc=0" - }, - { - "command": "sudo setup_wireguard_ssh.sh allow-dot (host, iptables legacy)", - "result": "pass", - "evidence": "'docker's own firewall rules are intact; containers untouched'; docker since 2026-09-28 12:41:15 and gitea-caddy StartedAt unchanged; live input chain gains iifname wg0 tcp dport 853 accept; verify: nftables.conf matches; anki/gitea HTTPS 200" - }, - { - "command": "lib/tests/test_wg_firewall.sh (root in vmbox) ; shell_coverage_jail.sh --subject lib/tests/run_all.sh --min 0 --fail-on-case-error", - "result": "pass", - "evidence": "12 passed incl. real nft -c; jailed suite rc=0" - } - ], - "risks": [ - "Tables left over from an older ruleset outside inet filter are no longer flushed by an apply (none exist on the host).", - "If docker's rules live somewhere other than the nat DOCKER chain, a missing-rules state would not trigger the rebuild." - ], - "rollback": [ - "git revert this commit, then sudo setup_wireguard_ssh.sh allow-web to re-render (old code restarts docker once)", - "After rollback: setup_wireguard_ssh.sh verify, and confirm containers reach the network" - ] -} diff --git a/linux_configuration/periodic_background/lib/atomic_install.sh b/linux_configuration/periodic_background/lib/atomic_install.sh new file mode 100755 index 00000000..32570fc1 --- /dev/null +++ b/linux_configuration/periodic_background/lib/atomic_install.sh @@ -0,0 +1,50 @@ +#!/bin/bash +# Atomic install of live /usr/local/bin scripts for the periodic system setup. +# +# Sourced by setup_periodic_system.sh and periodic_browser.sh. +# +# Why not `sed ... > /usr/local/bin/x.sh`: that truncates the file in place. +# bash reads a script lazily, so a service already running it (the hosts +# monitor loops forever) can execute a half-new file. Rendering beside the +# target and renaming over it gives the target a NEW inode; a running instance +# keeps reading the old one until it exits or is restarted. +# Fail-closed: a template that is missing or fails `bash -n` leaves the live +# file untouched and returns non-zero. + +# install_rendered_script