diff --git a/docs/DOCS-lan-blocker-contingency.md b/docs/DOCS-lan-blocker-contingency.md index 8275d3f7..24a5f498 100644 --- a/docs/DOCS-lan-blocker-contingency.md +++ b/docs/DOCS-lan-blocker-contingency.md @@ -120,8 +120,8 @@ with no internet. So the laptop runs `warp-home` (user timer, every 30 s): at home with no window open it turns WARP off, and plain internet with the blocklist comes back. On any other network (office) it never touches WARP. -- Mon/Thu/Fri 08:00-16:00 at home: `warp-home on` asks the PC, then connects. - The PC opens WARP for the LAN until 16:00 sharp, then closes by itself. +- Mon/Thu/Fri 07:00-17:00 at home: `warp-home on` asks the PC, then connects. + The PC opens WARP for the LAN until 17:00 sharp, then closes by itself. Outside that window the PC refuses. `warp-home off` / `warp-home status`. - Window closed early (the PC firewall was re-applied): `warp-home on` again. - PC side by hand: `sudo warp-window open|close|status`. @@ -139,4 +139,4 @@ blocklist comes back. On any other network (office) it never touches WARP. real IPv6 address); Firefox DoH canary + DoH hostnames blocked; DHCP lease 12h -> 2h; gateway mode added (nextcloud-pi and the P110 plug exempt). - 2026-10-25: WARP tunnel dropped at the gateway (work laptop bypassed the - DNS block through it); Mon/Thu/Fri 08:00-16:00 window via `warp-home on`. + DNS block through it); Mon/Thu/Fri 07:00-17:00 window via `warp-home on`. diff --git a/docs/superpowers/contracts/warp-window-0700-1700-2026-11-09.json b/docs/superpowers/contracts/warp-window-0700-1700-2026-11-09.json new file mode 100644 index 00000000..801f4ae7 --- /dev/null +++ b/docs/superpowers/contracts/warp-window-0700-1700-2026-11-09.json @@ -0,0 +1,14 @@ +{ + "title": "WARP home window: widen to 07:00-17:00", + "objective": "kuhy can run `warp-home on` on the work laptop at home Mon/Thu/Fri 07:00-17:00 (was 08:00-16:00); the PC's clock still decides, WARP stays blocked at home outside that window, and on any other network WARP is always available.", + "acceptance_criteria": [ + "warp_window.sh open succeeds only Mon/Thu/Fri 07:00:00-16:59:59 and fills warp_open with a timeout ending at 17:00; Tue/Wed/Sat/Sun stay closed (test_warp_window.sh)", + "warp_home.sh messages name Mon/Thu/Fri 07:00-17:00 and the 17:00 cut-off; away from home tick never touches WARP and on connects without the PC (test_warp_home.sh)", + "Live: Monday 07:56 the installed PC script reports the window open until 17:00 and `warp-home on` connects WARP" + ], + "out_of_scope": [ + "Window days, install path, sudoers entry and authorized_keys forced command (unchanged)", + "Jail-only suites in features/lib/tests (dwm, nc_php, rpi_nc, dot_resolver_install, website_admin)" + ], + "verifier": "linux_configuration/features/lib/tests/test_warp_window.sh; linux_configuration/features/lib/tests/test_warp_home.sh; on the laptop: warp-home on && warp-home status" +} diff --git a/docs/superpowers/evidence/warp-window-0700-1700-2026-11-09.json b/docs/superpowers/evidence/warp-window-0700-1700-2026-11-09.json new file mode 100644 index 00000000..a04147a9 --- /dev/null +++ b/docs/superpowers/evidence/warp-window-0700-1700-2026-11-09.json @@ -0,0 +1,37 @@ +{ + "intent": "The home WARP window for the work laptop opens Mon/Thu/Fri 07:00-17:00 instead of 08:00-16:00. Away from home WARP stays always available, any day or time.", + "scope": [ + "linux_configuration/features/warp_window.sh (PC, enforces the window)", + "linux_configuration/features/warp_home.sh (laptop, messages only)", + "DOCS-lan-blocker-contingency.md, test_warp_window.sh, test_warp_home.sh", + "No change to days, install, sudoers, the ssh forced command or the away-from-home path" + ], + "changes": [ + "OPEN_SEC 08:00 -> 07:00; CLOSE_SEC 16:00 -> 17:00; refusal/open/warning messages updated", + "test_warp_window.sh asserts the 06:59:59/07:00/16:59:59/17:00 edges and a 25200 s timeout at Mon 10:00" + ], + "verification": [ + { + "command": "linux_configuration/features/lib/tests/test_warp_window.sh (before the change)", + "result": "fail", + "evidence": "red first: 27 passed, 8 failed (07:00 closed, 14400 instead of 18000 at Friday noon)" + }, + { + "command": "linux_configuration/features/lib/tests/test_warp_window.sh; test_warp_home.sh", + "result": "pass", + "evidence": "35 passed, 0 failed; 35 passed, 0 failed (incl. away-from-home tick never touches WARP, on away connects without the PC)" + }, + { + "command": "sudo install -m 0755 warp_window.sh /usr/local/sbin/warp-window; scp warp_home.sh to laptop ~/.local/bin/warp-home; . /usr/local/sbin/warp-window; window_seconds_left", + "result": "pass", + "evidence": "Mon 2026-11-09 07:56:42 -> 32598 s left (open until 17:00; closed under the old window); kuhy ran warp-home on and confirmed it works" + } + ], + "risks": [ + "Laptop copy is installed by hand; a later warp_home.sh install overwrites it with the repo version (same content now)" + ], + "rollback": [ + "git revert this commit, then sudo install -m 0755 warp_window.sh /usr/local/sbin/warp-window and copy warp_home.sh to the laptop again", + "Then: window_seconds_left is closed at Mon 07:30 and 16:30" + ] +} diff --git a/docs/superpowers/evidence/wsg-grabber-cloud-keep-trash-cap-2026-09-18.json b/docs/superpowers/evidence/wsg-grabber-cloud-keep-trash-cap-2026-09-18.json deleted file mode 100644 index edfd5a1a..00000000 --- a/docs/superpowers/evidence/wsg-grabber-cloud-keep-trash-cap-2026-09-18.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "intent": "Kept /wsg/ videos go to the dufs cloud folder (or ~/Downloads as fallback) instead of a buried data dir, and trash/ is capped at the newest 200 passes with everything older deleted.", - "scope": [ - "python_pkg/wsg_grabber/paths.py: cloud_root, downloads_dir, cloud_keep_dir, keep_dir (cloud or Downloads), legacy_keep_dir, kept_candidates", - "python_pkg/wsg_grabber/_prune.py, _migrate_keep.py, housekeeping.py (new); files.remove; FileState.PURGED; store_verdicts recent_passed_names / purge_older_passes / rename_kept", - "app.py wires on_startup / after_verdict / locate_reviewed; cli.py and README drop the 'nothing is ever deleted' promise", - "Non-goal: capping kept files, touching dufs-cloud or the media sync timer" - ], - "changes": [ - "keep_dir() resolves per call to ~/data/cloud/Media//wsg when the cloud folder exists, else ~/Downloads", - "prune_trash deletes every trash/ file outside the 200 newest reviewed_at passes (orphans included) and marks the rows purged; runs at startup and after each pass", - "migrate_keep moves the legacy keep/ into the current keep dir once (dedupe by name+size, suffix + index rename on clash) and removes the empty dir", - "undo locates a kept file through kept_candidates so year rollover, cloud absence and the media sync sweep do not lose the verdict" - ], - "verification": [ - { - "command": "~/.claude/scripts/capped.sh python -m pytest python_pkg/wsg_grabber/tests -q", - "result": "pass", - "evidence": "303 passed; every python_pkg/wsg_grabber module at 100% line+branch coverage" - }, - { - "command": "HOME=/tmp/wsgfake/home XDG_DATA_HOME=$HOME/.local/share python -m python_pkg.wsg_grabber scrape --seconds 1 (copy of the real index, 23218 stand-in trash files, 50 stand-in keep files, fake data/cloud)", - "result": "pass", - "evidence": "keep.migrated moved=50; trash.pruned removed=23018 purged_rows=23018 retained=200; all 200 newest reviewed_at names present; keep/ removed; stats shows keep dir under data/cloud, and ~/Downloads after rm -rf data/cloud" - }, - { - "command": "python -m python_pkg.wsg_grabber scrape --seconds 1 (real home)", - "result": "pass", - "evidence": "keep/ 342 -> ~/data/cloud/Media/2026/wsg 647 (was 305), keep/ gone; trash 23218 files / 77G -> 200 files / 823M; stats: kept 648, passed 200, purged 23018" - }, - { - "command": "pre-commit run --files <18 changed files>", - "result": "pass", - "evidence": "no hook output beyond Passed/Skipped" - } - ], - "risks": [ - "Purging is irreversible; the first run deleted 77 GB of passed videos, as agreed (Q3).", - "A keep made while the cloud folder is absent lands in ~/Downloads and is swept by media-cloud-sync into Media//; undo follows it via kept_candidates but a same-name suffix applied by that sync would break the link.", - "Kept videos in Media//wsg now appear in the dufs gallery and get thumbnails." - ], - "rollback": [ - "git revert the commit; kept files stay in the cloud folder (undo will still find them there through the old keep_dir only if moved back by hand)", - "Validate with `python -m python_pkg.wsg_grabber stats` and the test suite" - ] -} diff --git a/linux_configuration/features/lib/tests/test_warp_home.sh b/linux_configuration/features/lib/tests/test_warp_home.sh index 9ad2d8ac..8847026f 100755 --- a/linux_configuration/features/lib/tests/test_warp_home.sh +++ b/linux_configuration/features/lib/tests/test_warp_home.sh @@ -120,7 +120,7 @@ _t_not "the early-closed window is forgotten" test -e "$OPEN_UNTIL" SSH_OUT="seconds_left=600" printf '\n-- on: a refusal connects nothing --\n' -SSH_OUT="WARP window is Mon/Thu/Fri 08:00-16:00 only" +SSH_OUT="WARP window is Mon/Thu/Fri 07:00-17:00 only" SSH_RC=1 reset_calls on_refused() { cmd_on; } diff --git a/linux_configuration/features/lib/tests/test_warp_window.sh b/linux_configuration/features/lib/tests/test_warp_window.sh index 122bad78..a79379be 100755 --- a/linux_configuration/features/lib/tests/test_warp_window.sh +++ b/linux_configuration/features/lib/tests/test_warp_window.sh @@ -2,7 +2,7 @@ # Tests for warp_window.sh -- the PC side of the work-from-home WARP window. # # Gateway mode drops the Cloudflare WARP tunnel because it carries everything -# past the DNS block. kuhy may open it only Mon/Thu/Fri 08:00-16:00; the laptop +# past the DNS block. kuhy may open it only Mon/Thu/Fri 07:00-17:00; the laptop # asks over an SSH key whose forced command can run nothing but open/status. # The window edges and the forced-command filter are the security boundary, # so both are asserted exactly. The clock and nft are stubbed. @@ -28,26 +28,26 @@ left_at() { window_seconds_left || printf 'closed' } -printf '\n-- the window is Mon/Thu/Fri 08:00-16:00, nothing else --\n' -_t_eq "closed" "$(left_at '1 07 59 59')" "Monday 07:59:59 is closed" -_t_eq "28800" "$(left_at '1 08 00 00')" "Monday 08:00 opens until 16:00" -_t_eq "1" "$(left_at '1 15 59 59')" "Monday 15:59:59 has one second left" -_t_eq "closed" "$(left_at '1 16 00 00')" "Monday 16:00 is closed" -_t_eq "14400" "$(left_at '5 12 00 00')" "Friday noon is open" -_t_eq "28800" "$(left_at '4 08 00 00')" "Thursday 08:00 opens until 16:00" -_t_eq "closed" "$(left_at '4 16 00 00')" "Thursday 16:00 is closed" +printf '\n-- the window is Mon/Thu/Fri 07:00-17:00, nothing else --\n' +_t_eq "closed" "$(left_at '1 06 59 59')" "Monday 06:59:59 is closed" +_t_eq "36000" "$(left_at '1 07 00 00')" "Monday 07:00 opens until 17:00" +_t_eq "1" "$(left_at '1 16 59 59')" "Monday 16:59:59 has one second left" +_t_eq "closed" "$(left_at '1 17 00 00')" "Monday 17:00 is closed" +_t_eq "18000" "$(left_at '5 12 00 00')" "Friday noon is open" +_t_eq "36000" "$(left_at '4 07 00 00')" "Thursday 07:00 opens until 17:00" +_t_eq "closed" "$(left_at '4 17 00 00')" "Thursday 17:00 is closed" for day in 2 3 6 7; do _t_eq "closed" "$(left_at "$day 12 00 00")" "weekday $day noon is closed" done -_t_eq "28260" "$(left_at '5 08 09 00')" "a leading-zero hour is decimal, not octal" +_t_eq "31860" "$(left_at '5 08 09 00')" "a leading-zero hour is decimal, not octal" -printf '\n-- open fills the set with a timeout ending at 16:00 --\n' +printf '\n-- open fills the set with a timeout ending at 17:00 --\n' FAKE_DATE="1 10 00 00" out="$(cmd_open)" -_t_called 'nft add element inet filter warp_open { 0.0.0.0/0 timeout 21600s }' \ +_t_called 'nft add element inet filter warp_open { 0.0.0.0/0 timeout 25200s }' \ "open adds the catch-all element with the time left" -_t_has "$out" "seconds_left=21600" "open reports the seconds left for the laptop" -_t_has "$out" "until 16:00" "open names the 16:00 cut-off" +_t_has "$out" "seconds_left=25200" "open reports the seconds left for the laptop" +_t_has "$out" "until 17:00" "open names the 17:00 cut-off" printf '\n-- outside the window open refuses and touches nothing --\n' : >"$TEST_TMPDIR/calls.log" diff --git a/linux_configuration/features/warp_home.sh b/linux_configuration/features/warp_home.sh index a008bfec..cc5ef0ef 100755 --- a/linux_configuration/features/warp_home.sh +++ b/linux_configuration/features/warp_home.sh @@ -9,7 +9,7 @@ # plain internet (with the DNS blocklist) comes back. On any other network, # e.g. the office, it never touches WARP. # -# warp-home on Mon/Thu/Fri 08:00-16:00 at home: ask the PC, then connect. +# warp-home on Mon/Thu/Fri 07:00-17:00 at home: ask the PC, then connect. # Anywhere else: just connect. # warp-home off disconnect and forget the window # warp-home status network, window, WARP and the PC's view @@ -23,7 +23,7 @@ set -euo pipefail readonly PC_IP="192.168.1.43" PC_MAC="9c:6b:00:5c:c5:6d" PC_USER="kuhy" -readonly WARN_SEC=300 # warn five minutes before the 16:00 cut-off +readonly WARN_SEC=300 # warn five minutes before the 17:00 cut-off KEY="$HOME/.ssh/warp_window_ed25519" STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/warp-home" OPEN_UNTIL="$STATE_DIR/open-until" @@ -70,17 +70,17 @@ warp_off_because() { cmd_tick() { at_home || return 0 window_open || { - warp_off_because "WARP turned off at home. 'warp-home on' works Mon/Thu/Fri 08:00-16:00." + warp_off_because "WARP turned off at home. 'warp-home on' works Mon/Thu/Fri 07:00-17:00." return 0 } - # Any re-apply of the PC firewall empties its set before 16:00. Only an + # Any re-apply of the PC firewall empties its set before 17:00. Only an # explicit "closed" counts: an unreachable PC proves nothing. if [[ $(pc status 2>/dev/null) == closed ]]; then warp_off_because "The PC closed the WARP window early; run 'warp-home on' again." return 0 fi if (($(<"$OPEN_UNTIL") - $(date +%s) <= WARN_SEC)) && [[ ! -e $WARNED ]]; then - notify "WARP closes at 16:00, in five minutes." + notify "WARP closes at 17:00, in five minutes." touch "$WARNED" fi } @@ -96,7 +96,7 @@ cmd_on() { printf '%s\n' "$(($(date +%s) + left))" >"$OPEN_UNTIL" fi warp connect >/dev/null - printf 'WARP connecting.%s\n' "$(at_home && printf ' Home window ends at 16:00.')" + printf 'WARP connecting.%s\n' "$(at_home && printf ' Home window ends at 17:00.')" } cmd_off() { diff --git a/linux_configuration/features/warp_window.sh b/linux_configuration/features/warp_window.sh index 39a00c35..336cb189 100755 --- a/linux_configuration/features/warp_window.sh +++ b/linux_configuration/features/warp_window.sh @@ -4,8 +4,8 @@ # # Gateway mode (setup_dns_blocker.sh gateway) drops the WARP tunnel, because # WARP carries all traffic past the DNS block. This opens it for every LAN -# device, but ONLY Mon/Thu/Fri 08:00-16:00 by this PC's clock: the open adds one -# element to the nft set warp_open whose timeout ends at 16:00, so the kernel +# device, but ONLY Mon/Thu/Fri 07:00-17:00 by this PC's clock: the open adds one +# element to the nft set warp_open whose timeout ends at 17:00, so the kernel # closes it again with nothing left to fail. The office days (Tue/Wed) need # nothing here: the home gateway never touches the work network. # @@ -23,7 +23,7 @@ set -euo pipefail readonly WINDOW_DAYS=" 1 4 5 " # date +%u: Monday, Thursday, Friday -readonly OPEN_SEC=$((8 * 3600)) CLOSE_SEC=$((16 * 3600)) +readonly OPEN_SEC=$((7 * 3600)) CLOSE_SEC=$((17 * 3600)) readonly NFT_SET="inet filter warp_open" readonly KEY_FROM="192.168.1.0/24" # the key works only from the LAN INSTALL_PATH="/usr/local/sbin/warp-window" @@ -36,8 +36,8 @@ die() { exit 1 } -# Seconds until 16:00 while the window is open; fails when it is closed. -# Plain arithmetic, not `date -d 16:00`: DST never changes on a window day. +# Seconds until 17:00 while the window is open; fails when it is closed. +# Plain arithmetic, not `date -d 17:00`: DST never changes on a window day. window_seconds_left() { local day h m s now read -r day h m s <<<"$(date '+%u %H %M %S')" @@ -49,13 +49,13 @@ window_seconds_left() { cmd_open() { local left - left="$(window_seconds_left)" || die "WARP window is Mon/Thu/Fri 08:00-16:00 only." + left="$(window_seconds_left)" || die "WARP window is Mon/Thu/Fri 07:00-17:00 only." # Re-opening must refresh the timeout, and add keeps an old one: flush first. # shellcheck disable=SC2086 # NFT_SET is three words on purpose nft flush set $NFT_SET 2>/dev/null || die "no warp_open set: is gateway mode on?" # shellcheck disable=SC2086 nft add element $NFT_SET "{ 0.0.0.0/0 timeout ${left}s }" - printf 'WARP open for the LAN until 16:00.\nseconds_left=%s\n' "$left" + printf 'WARP open for the LAN until 17:00.\nseconds_left=%s\n' "$left" } cmd_close() {