Skip to content

CI, Linux scanners, catalog validation, case export, TUI guides, update rollback, and a real README - #5

Draft
lizTheDeveloper wants to merge 19 commits into
mainfrom
claude/scanner-readme-docs-7ql7cs
Draft

CI, Linux scanners, catalog validation, case export, TUI guides, update rollback, and a real README#5
lizTheDeveloper wants to merge 19 commits into
mainfrom
claude/scanner-readme-docs-7ql7cs

Conversation

@lizTheDeveloper

@lizTheDeveloper lizTheDeveloper commented Aug 5, 2026

Copy link
Copy Markdown
Owner

What this changes

Works through the things docs/ROADMAP_STATUS.md records as unimplemented, and gives the repository the CI and documentation it did not have. After this merges: every push runs the suite on three operating systems and three Python versions; Linux has a real module set instead of a nominal one; the shipped catalog validates itself; a rescue produces a redacted report you can hand to someone; the TUI can actually open the recovery walkthroughs; a content update can be backed out; and the README answers "will this download something malicious?" with file paths rather than reassurance.

Type of change

  • Bug fix
  • New module
  • New or changed profile / guide
  • Engine change (anything under rescue/)
  • Documentation
  • Tests or CI only

What's in it

CI, where there was no .github/ at all — the documented consequence was that three files did not parse on the declared minimum Python and 30 call sites used a 3.13-only keyword, both shipped, because the suite only ever ran on one developer's macOS box. Jobs: a 3.11 byte-compile floor, pytest across 3.11/3.12/3.13 × ubuntu/macos/windows, ruff, integrity-manifest freshness, catalog validation, and a clean-venv install that runs rescue from outside the source tree (the packaging bug in P0#1 is invisible any other way).

Nine Linux modules. 17 of 281 modules declared Platform.LINUX, mostly cross-platform checks that happened not to exclude it — a Linux user got a disk-space number. Now: firewall state, SSH exposure, account and sudo audit, disk encryption, boot persistence, package updates and release EOL, systemd health, journal errors that precede hardware failure, and memory pressure. All read-only, all bounded through rescue.command.run, plus a linux_security_checkup profile that runs the lot in one command.

rescue validate (P1#3) — unique names, dependency cycles, platform and risk declarations, profiles selecting real modules, guides not advertising automation nothing can perform.

rescue export (P1#2) — a rescue case as redacted JSON and Markdown. Redacted by construction, not by review, since an export is the artifact most likely to be pasted into a chat window.

TUI guides (P1#5) — the guide screen was three disabled checkboxes and "coming in Plan 3", and nothing ever pushed it. Now a working checklist sharing session state with rescue guide on the CLI.

rescue update --rollback / --use-bundled (P0#2) — rollback re-verifies approval rather than trusting the earlier one, and the bundled fallback deliberately avoids the trust machinery so it works when that is the thing that broke.

Documentation. A MkDocs Material site (mkdocs.yml, docs/, deployed by the docs workflow) with a quickstart, a page per built-in scenario, a CLI reference, a module catalog generated from the live registry and checked for drift in CI, a module-authoring guide, and a trust-and-safety page that traces every safety claim to the file implementing it. Plus a rewritten README, SECURITY.md, CONTRIBUTING.md, and issue/PR templates — every command run against the checkout, every number from the live registry, and the limits stated as plainly as the guarantees.

Nine pre-existing defects the new CI found

The matrix earned its place on the first run. Everything here was already in the tree except where noted:

Defect Impact before the fix
os.getuid() at module import scope Collection aborted; all three Windows jobs ran zero tests
win_user_profiles treated a PowerShell SID object as a string Windows-only module raised TypeError on every real Windows machine
No .gitattributes LF→CRLF on checkout made the integrity manifest report the whole package as tampered on Windows
read_text()/open() without an encoding Guides, profiles, signer JSON and generated docs read and written through cp1252 — silent mojibake in recovery text a frightened person reads
getattr(None, "__doc__", "") returns NoneType's docstring on 3.13 Version-dependent validator behaviour (introduced in this PR, caught by the matrix)
Four invalid escape sequences in Windows-path docstrings SyntaxWarnings today, errors in a future Python
BSOD fixture built exactly on the 24-hour boundary Latent coin-flip, resolved by clock granularity — one way on Linux, the other on Windows
dir="/tmp" in a gpg test; seven POSIX-assuming test files Failed for the platform rather than for the code
A TUI test that started a real scan of the module tree Passed, then spent 16 minutes reaping orphaned powershell/manage-bde processes

Also fixed: a date-bomb fixture in test_module_win_windows_update_status that aged into a failure, a pytest.main call in a file that never imported pytest, and test_module_code_consistency matching only code="security.…" — so every non-security category was exempt by not being looked at.

And one that mattered more than a test failure: all three AI providers told users to pip install multiverse-device-rescue[ai] when their SDK was missing. This project is not on PyPI, so that error message instructed people to install whatever an unrelated party had uploaded under that name — the exact supply-chain hazard the trust page says the tool protects them from, shipped inside the tool's own remediation advice.

Test plan

$ .venv/bin/python -m pytest -q
3676 passed in 363.16s (0:06:03)

$ .venv/bin/python -m rescue.cli validate
287 modules, 7 profiles, 110 guide phases checked: 0 error(s), 1 warning(s).
Catalog is consistent.

$ .venv/bin/ruff check .
All checks passed!

$ .venv/bin/mkdocs build --strict
Documentation built in 1.10 seconds

Also run green on 3.12 and 3.13 locally, which is how the None.__doc__ divergence was confirmed fixed.

Clean-install check, matching what the package CI job does:

$ python3 -m venv /tmp/pkgtest && /tmp/pkgtest/bin/pip install .
$ cd /tmp && /tmp/pkgtest/bin/rescue profiles | head -1
ai_worm_response — AI Worm & Spyware Response
$ /tmp/pkgtest/bin/rescue run disk_space --yes
=== disk_space === Found 4 issue(s): ...

Manual verification: Ubuntu 24.04, all nine Linux modules run end to end against the real host, and rescue --auto --profile linux_security_checkup reports findings and changes nothing. rescue update --use-bundled verified to succeed on this repository, whose signer configuration is still placeholders — which is precisely the case it exists for.

Checklist

  • .venv/bin/python -m pytest -q passes
  • .venv/bin/python -m rescue.cli validate exits 0 (0 errors)
  • .venv/bin/ruff check . is clean
  • Integrity manifest: regenerated and committed after every change under rescue/
  • Not applicable — nothing under rescue/ changed

Safety review

  • Every mutation is confirmed by the user, or gated behind --yes
  • Instructional actions use ActionKind.GUIDANCE and are never reported as completed changes
  • Checks that cannot run return supported=False with a reason, or set error — never an empty healthy result
  • No new module sets auto_apply = True
  • External commands go through rescue.command.run; filesystem recursion goes through rescue.fsbounds.bounded_walk
  • Traversal roots and command paths are class attributes so tests can point them at a fixture tree
  • Nothing new reaches the network outside the opt-in AI layer and rescue update
  • Nothing prompts for a password, one-time code, recovery key, or API token
  • emits_codes matches the code= literals in the module

Anything a reviewer should look at closely

The lint gate is narrow on purpose. Ruff's defaults produce 4000 findings here, so ruff.toml selects only defect-class rules. Widening it is worth doing as its own change; a gate that is red on arrival gets ignored.

CI runs rescue validate, not --strict. The one outstanding warning is that 264 of 287 modules have no docstring. Real debt, but failing every PR on it would make the check something people route around. The workflow comment says what has to be true before --strict goes back on.

Seven test files now skip off POSIX. Each tests a macOS-only or Linux-only module and asserts on POSIX paths or file modes, so there is nothing for a Windows job to exercise — but it is worth a second opinion that none of them was masking a real cross-platform gap.

The Linux modules' severity choices. Whether "no firewall front-end installed" deserves WARNING, and whether the linux_persistence_audit heuristics (fetch-and-pipe-to-shell, execution from /tmp, world-writable units) are tight enough to avoid the false-positive problem the roadmap warns about, are judgement calls worth reviewing.

rescue update --rollback refuses a previous version whose signer has since been revoked. Deliberate — revocation has to apply to content already on the machine — but it does mean the only path left in that case is --use-bundled.

claude added 19 commits August 5, 2026 22:58
There was no .github directory at all. Documented consequence, from
ROADMAP_STATUS: three files did not parse on the declared minimum Python and 30
call sites used a 3.13-only keyword, both shipped, because nothing ever ran the
suite anywhere but one developer's macOS box.

The workflow is shaped around the failures this repo has actually had:

- syntax-floor byte-compiles the whole tree on 3.11 before anything else runs,
  because a parse error is the cheapest failure to catch and the most expensive
  to ship.
- the pytest matrix spans 3.11/3.12/3.13 on ubuntu, macos and windows.
- integrity regenerates the manifest and fails on a diff. A stale manifest
  prints a tamper warning on every launch, which trains users to ignore the one
  signal that would tell them their install was modified.
- package installs into a clean venv and runs from outside the source tree.
  Modules, profiles and guides ship as data_files, so a packaging regression
  produces a tool that installs fine and discovers nothing (P0#1).

Also fixes a time bomb the matrix would have caught: the win_windows_update
fixture hardcoded a date that was recent when written. Real time moved past it
and it aged into a "no updates in 31 days" warning, failing three tests on
nothing but the calendar. Computed relative to now, like its neighbours.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0126sAnDC3XiCXD9oS14JSJp
Two roadmap items that were named as remaining and had no implementation.

rescue validate (P1#3) checks what the tool ships against itself: unique module
names, dependencies that resolve without cycles, platforms and risk levels that
are real enum members, every profile selecting modules that exist, and no guide
advertising a step as automatable that no module can perform. Errors fail; a
--strict flag promotes warnings, which is what CI runs. It never executes a
module's check(), and it returns problems rather than raising — a validator that
crashes on a broken catalog tells you nothing about what is broken.

Two rules are worth calling out. auto_apply on a non-SAFE module is an error,
not a style note: that flag is the switch that lets unattended mode change a
system. And a dependency cycle is an error rather than something topological_sort
quietly resolves in arbitrary order, because the arbitrary order runs a module
before the thing it depends on.

rescue export (P1#2) writes a rescue case as JSON for tooling and Markdown for
people. It is redacted by construction rather than by review, because an export
is the artifact most likely to be pasted into a chat window: credential-shaped
strings, private keys, auth headers, email addresses, the account name and the
home path are removed from every string that leaves, including the free-text
descriptions of 280-odd modules this code cannot audit individually. Files are
written 0600.

It also refuses to overstate what happened. Guidance is recorded as guidance
even where a module marked it successful, mirroring FixResult.executed_mutations
(P0#6), and failed or unsupported checks get their own section instead of being
absent — a check that could not run is not a clean bill of health.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0126sAnDC3XiCXD9oS14JSJp
…ly none

Linux has been advertised as supported since the first release. 17 of 281
modules declared Platform.LINUX, and most of those were cross-platform checks
that happened not to exclude it. A Linux user ran the tool and got a disk-space
number. Roadmap Phase 3 item 1 asks for this; nothing had been built.

Nine modules, all read-only, all routed through rescue.command.run with
timeouts (which also makes them the first worked examples of the P0#7 runner
migration rather than 756 more bare subprocess calls):

  security     linux_firewall_check, linux_ssh_hardening,
               linux_persistence_audit, linux_account_audit,
               linux_disk_encryption_check
  integrity    linux_package_updates, linux_service_health,
               linux_journal_errors
  performance  linux_memory_pressure

Three judgements run through all of them.

Unreadable is not the same as absent. Most of what matters here needs root:
/etc/shadow, an nftables ruleset, the system journal. Each module reports
"could not determine" as its own finding rather than folding it into a healthy
result — telling someone their firewall is off when it is merely unreadable
teaches them to distrust the tool, and CheckResult.supported exists precisely
so an unsupported check cannot read as a pass.

The inventory is the detection mechanism. linux_persistence_audit enumerates
seven autostart locations and reports the list as INFO. Only structural
properties escalate: fetch-and-pipe-to-shell, execution out of /tmp, a
world-writable unit file, a non-empty /etc/ld.so.preload. Flagging every
systemd unit as suspicious would rebuild exactly the false-positive machine the
roadmap warns about.

Absence of pending updates is not good news by itself. A release past its
end-of-life reports zero updates forever, because none are being published.
linux_package_updates asks about the release too, and links the distribution's
own support-cycle page rather than shipping a table that silently goes stale.

Traversal roots and config paths are class attributes so tests point at fixture
trees instead of at the machine running the suite — the convention the 64
environment-coupled failures established. 82 tests; all nine verified end to end
on a real Ubuntu host.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0126sAnDC3XiCXD9oS14JSJp
…lder

The TUI's guide screen rendered three disabled checkboxes and the words
"coming in Plan 3 (Profile System & Guide Engine)". It was also unreachable —
nothing in the app ever pushed it. Roadmap P1#5.

Everything it needed already existed: the Guide model, the phased markdown in
guides/<profile>/phase_N.md, and SessionStore. Only the screens were missing, so
the TUI silently had no route to the recovery walkthroughs that are the reason
someone opens this tool after being compromised.

Three screens: guide sets with progress, the phases of one set with the current
phase marked, and the steps of one phase as a live checklist. Detail view on `d`
renders the step's full markdown body.

Ticking writes through to the same SessionStore that `rescue guide <profile>`
uses, so the CLI and TUI are two views of one piece of progress rather than two
records that disagree. Un-ticking works: someone who marked a step done and then
found it had not taken needs to say so, and a checklist that only moves forward
records a recovery that did not happen.

Steps are labelled "tool-assisted" or "you do this" from the guide's own
automatable_steps metadata. Most of what matters in a security reset — changing
a password at the provider, revoking a session, calling a bank — cannot be done
by anything running on the affected device, and blurring the two invites someone
to assume the tool did something it did not.

The `g` binding is app-level and works from the loading screen, so the
walkthrough is not behind a completed scan.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0126sAnDC3XiCXD9oS14JSJp
… found

Turning ruff on with its default rules produces 4000 findings on this tree. A
gate that is red on arrival is a gate everyone learns to ignore, which leaves
the tree no better protected than having no linter at all.

ruff.toml selects the class of defect this repository has actually shipped:
syntax errors (three files once did not parse on the declared minimum Python),
undefined names, redefinitions that silently discard the first definition, and
comparison mistakes that change meaning. Unused imports and unused locals are
left out on purpose — worth cleaning, but as their own change with its own
review, not bolted onto unrelated work as a blocking check.

It found three real ones:

- `not "No such key" in guest_auth` in login_password_policy. It happens to be
  equivalent here, but the reading is the opposite of the intent.
- `pytest.main(...)` under a __main__ guard in a test that never imports pytest,
  so running that file directly raised NameError.
- `assert action.success == True`, now `is True`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0126sAnDC3XiCXD9oS14JSJp
…k (P0#2)

The roadmap recorded rollback as remaining, with recovery available "via git
history". That is not a recovery path for the person this tool is for — someone
whose machine just started behaving differently after an update, possibly
during an incident.

ContentRepo.checkout now records the commit it replaced in a second marker
alongside the applied-head marker. Derived from markers rather than git history
on purpose: history tells you which commit came earlier, not which one this
machine was actually running, and those differ whenever a machine skips
versions — the normal case for a tool opened occasionally. The previous marker
is written before the applied marker, so a crash between the two leaves a
harmless duplicate rather than losing the record of what worked.

`rescue update --rollback` re-verifies maintainer approval instead of trusting
that the commit was approved when it was applied. A signer can be revoked in
between, and the point of revocation is that content that signer approved stops
being trusted — including content already on the machine. Silently restoring it
would make revocation a note about future downloads.

That leaves the case where the previous version is no longer trusted, so
`rescue update --use-bundled` clears the applied marker and falls back to the
content that shipped inside the install. It touches no signatures, no network,
and nothing an update wrote, which is what makes it the last-resort path.
Nothing is deleted; a later `rescue update` reactivates downloaded content.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0126sAnDC3XiCXD9oS14JSJp
…tion

The first cut routed both recovery paths through UpdateEngine, whose
constructor validates the trusted-signer configuration. That made the
last-resort fallback fail with "trusted signer configuration contains
placeholder or missing key material" on exactly the machine that most needs it:
one whose trust config is broken or, as in this repository today, not yet
populated.

--use-bundled now clears the applied marker through ContentRepo directly. It
touches no signatures, no network, and nothing an update wrote, which is the
only reason it can be relied on. --rollback still constructs the engine,
because re-verifying approval is the point of it, and now says that
--use-bundled remains available when trust loading fails.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0126sAnDC3XiCXD9oS14JSJp
_CODE_LITERAL matched code="security.…" only, because security modules were the
only ones declaring emits_codes when the gate was written. Every other category
was silently exempt — passing by not being looked at. The new integrity and
performance modules are the first non-security modules with codes, and they
walked straight through it.

Matching any category turned up real drift in those modules, now fixed:

- linux_journal_errors built its code with an f-string, so no code was
  statically discoverable. _SIGNALS is now a dataclass carrying the literal.
- linux_service_health and linux_disk_encryption_check chose their code with a
  conditional expression inside the Finding(...) call. Split into two
  constructions each, so the literal sits at the call site.
- linux_service_health.no_systemd and linux_package_updates.no_package_manager
  were declared but never emitted: both are reported through
  CheckResult.supported, and "this check does not apply here" is not something
  a remediation walkthrough can act on.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0126sAnDC3XiCXD9oS14JSJp
…try point

Nine modules are only useful if someone can find them. This is the one command
that runs the lot: `rescue --auto --profile linux_security_checkup`.

Module order is the finding order, and it is deliberate. Someone reading a long
result list acts on the first few rows, so exposure comes first — what can
reach this machine and who can log in to it — then what happens if the machine
itself is taken, then what is arranged to run at boot, then the patch level,
then hardware health. Sorting by category would have buried "anyone on this
network can SSH in with a password" under disk-space advice.

Tests assert the properties a profile can quietly get wrong: every named module
exists, every one of them can actually run on Linux (a macOS-only entry pads
the run with rows that could never produce a result), none is non-SAFE, and
none sets auto_apply — the profile is documented as read-only and auto_apply is
the flag that would make that untrue.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0126sAnDC3XiCXD9oS14JSJp
…rors

Two problems, one cause. `rescue validate` emitted a separate warning for every
module without a docstring, which on the shipped catalog is 264 of 287. The
actionable problems were buried under a backlog, and `--strict` — which the CI
content job ran — could never pass, so the check would have been red on its
first run and routed around from then on.

The documentation gap is now a single line carrying the count and a sample. It
stays visible without drowning the output, and it is still a warning because
"actionable support documentation" is a real roadmap requirement (P1#3), not
something to quietly drop.

CI runs plain `rescue validate`: errors gate, warnings do not, yet. The comment
in the workflow says exactly what has to be true before --strict goes back on,
rather than leaving a flag nobody can turn on.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0126sAnDC3XiCXD9oS14JSJp
The README was 40 lines and told a reader almost nothing they could act on.
This one is written for the person the tool is for: someone who thinks something
is wrong with a computer and needs to know, first, whether running this is safe.

Every command shown was run against this checkout and the output is real,
trimmed where marked. Every number — 287 modules, the per-platform split, 7
profiles, 110 guide phases — comes from the live registry rather than from
memory.

The longest section is "Will this download something malicious?", because that
is the question a security tool has to answer before any of its features
matter. Each claim names the file that implements it so a reader can check
rather than believe: auto mode is read-only because auto_apply defaults False
and zero shipped modules set it; guidance cannot inflate the change count
because executed_mutations filters on kind first; the SHA-256 self-check at
launch, with the command to verify it independently; two-signer approval for
content updates; updates restricted to data paths, never Python.

It is equally specific about the limits, because a page that overclaims is
worth less than no page. The integrity check warns and continues rather than
blocking. The trusted-signer file still holds placeholders, so `rescue update`
fails closed and cannot fetch anything at all. Module discovery imports local
Python in-process (P0#10). 758 subprocess calls in modules still bypass the
bounded runner. `rescue validate --strict` fails on a documentation backlog.

Also adds SECURITY.md (including the point that a check reporting a false
healthy result is a security bug in this project, not a cosmetic one),
CONTRIBUTING.md, CODE_OF_CONDUCT.md, four issue templates — one of them
specifically for false positives and false negatives — and a PR template whose
checklist includes regenerating the integrity manifest.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0126sAnDC3XiCXD9oS14JSJp
First CI run, first real catch. `tests/test_module_disk_permissions_repair.py`
calls `os.getuid()` at module scope, and that attribute does not exist on
Windows — so it did not fail one test, it raised during collection and
interrupted the entire run. All three Windows jobs reported one error and zero
tests. Nobody had noticed because the suite had never run on Windows.

Both that file and `test_module_directory_permissions.py` now skip at module
scope when `os.getuid` is absent. Skipping is the honest outcome rather than a
workaround: both modules under test are macOS-only and call `os.getuid()`
themselves, so there is nothing here Windows could meaningfully exercise. Doing
it at module scope is what keeps the other 3600 tests running.

Also fixes the four invalid escape sequences the Windows runs surfaced as
SyntaxWarnings — Windows registry paths and `C:\Windows\System32\...` inside
non-raw docstrings. They are warnings today and errors in a future Python, and
they are exactly the class of latent breakage that shipped last time because
nothing compiled the tree anywhere but one machine.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0126sAnDC3XiCXD9oS14JSJp
MkDocs + Material, built to site_build/ (site/ is the existing landing page)
and deployed to GitHub Pages by the docs workflow. `mkdocs build --strict`
passes with no warnings, so a dead internal link fails the build.

What is there:

- **Quickstart** — install, first scan, and how to read the three outcomes a
  check can have. Install is from a source checkout on every platform, and the
  page says outright that the project is not on PyPI: telling readers to
  `pip install` a name the maintainers do not control is the supply-chain
  hazard the trust page tells them this project protects them from.
- **Scenarios** — one page per built-in profile, including a "which one do I
  need" table, written from the profile YAML and the guide phases rather than
  from memory. Each says what the tool does, what stays human-led, and what it
  will never ask for.
- **Trust and safety** — the long-form version of the README section. Every
  claim names the file that implements it, and the partial guarantees are
  labelled as partial.
- **CLI reference, architecture, module catalog, writing a module, privacy,
  FAQ, troubleshooting, contributing.**

The module catalog is generated from the live registry by
`scripts/generate_module_catalog.py`, and the docs workflow runs it with
--check, so the page cannot drift from what the tool actually ships.

The `docs` extra is upper-bounded on mkdocs and mkdocs-material: material warns
that MkDocs 2.0 removes the plugin system with no migration path, and an
unpinned dependency would break this build in CI on a change that had nothing
to do with the docs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0126sAnDC3XiCXD9oS14JSJp
All three AI providers raised, when their SDK was missing:

    pip install multiverse-device-rescue[ai]

This project is not published on PyPI. That message instructed users to install
whatever an unrelated party has uploaded under that name — the exact
supply-chain hazard the trust page tells them this tool protects them from, in
an error message shipped by the tool itself. It is worse than a broken link
because a frightened user following a security tool's own remediation advice is
the least likely person to stop and check.

Each provider now names its actual SDK (`pip install anthropic` / `openai` /
`httpx`) and the source-checkout extra.

Also corrects three docs claims that went stale when CI moved from
`rescue validate --strict` to the plain form: the content job gates on errors
and passes, and the workflow comment records what has to be true before
--strict goes back on.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0126sAnDC3XiCXD9oS14JSJp
**1. A Python-version-dependent bug in the validator.** `_has_documentation`
did `getattr(containing, "__doc__", "")` where `containing` may be None. That
does not return the default — it returns *NoneType's own* docstring, which is
empty on 3.11 and 3.12 and "The type of the None singleton." on 3.13. So a
module whose containing module was not in sys.modules counted as documented on
one supported Python and not the others. Now checks `is None` explicitly, and
reads the class docstring via `vars()` so a module cannot inherit credit for
its base class's prose. Verified on 3.11, 3.12 and 3.13.

**2. Every rescue/*.py reported as tampered on a Windows checkout.** git
converts LF to CRLF on checkout by default there, so every byte of every file
changes and the SHA-256 manifest matches nothing — the Windows job printed
"modified:" for all 56 entries on a clean tree. `.gitattributes` pins `eol=lf`.

The alternative was to normalise line endings before hashing, and that would
have been the wrong fix: a tamper check that ignores a class of byte difference
is a tamper check with a hole in it. Worse, a user who gets a tamper warning on
every launch learns to ignore the one signal that would tell them their install
had been modified.

That warning also broke `test_auto_without_copilot_never_mentions_ai`, which
asserts the word "copilot" never appears without --copilot: the integrity
warning lists `ai/copilot.py`. It now asserts against stdout rather than
`output`, which has mixed stderr in since click 8.2.

**3. Six test files assumed POSIX and had never run anywhere else.** Five test
macOS-only modules against ~/Library paths, /tmp fixtures, or `os.path.stat`
(which ntpath does not have); one is mine, asserting POSIX file modes on a
Linux-only module — Windows reports 0o666 for ordinary files, so its
world-writable check fired on every fixture. All now skip off POSIX with a
stated reason. The modules under test cannot run on Windows, so there was
nothing there for a Windows job to exercise, and a test that fails for the
platform rather than for the code teaches people to ignore the suite.

Also drops --maxfail from the workflow: a truncated failure list turns getting
a platform green into one round trip per twenty failures.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0126sAnDC3XiCXD9oS14JSJp
…non-UTF-8 locale

Two real user-facing bugs, both found by running the suite on Windows for the
first time. The remaining four failures were test-side platform assumptions.

**win_user_profiles raised TypeError on every real Windows machine.**
`Get-LocalUser | Select-Object SID | ConvertTo-Json` does not serialise a SID
as the "S-1-5-21-…" string everyone pictures — it serialises the
SecurityIdentifier *object*:

    {"SID": {"BinaryLength": 28, "AccountDomainSid": {...}, "Value": "S-1-5-…"}}

That went straight into a set, so `check()` raised "unhashable type: 'dict'"
and the module reported itself unavailable to every Windows user. A
performance module, Windows-only, that had never once been executed on
Windows. The query now projects the string out, and `_sid_string` normalises
anything object-shaped that reaches the parser anyway, because the exact
serialisation differs between PowerShell 5.1 and 7. Profile SIDs go through
the same normaliser: an orphan check comparing a dict against a set of strings
would silently match nothing and quietly report every profile as orphaned.

**The tool read its own content with the locale codec.** Guides, profiles, the
threat map, signer JSON and session state all used `read_text()` / `open()`
with no encoding. On Windows that is cp1252, and the content is UTF-8 full of
em-dashes and curly quotes. Most of it decodes to mojibake silently; some byte
sequences raise. A recovery guide is text a frightened person reads under
stress, and rendering it as "â€"" is not a cosmetic problem. All now explicit
UTF-8.

The four test-side fixes: `test_module_code_consistency` read module sources
with the locale codec, so the emits_codes gate could not run on Windows at all;
`tests/update/test_verify` hardcoded `dir="/tmp"` for a short gpg-agent socket
path, which does not exist on Windows; and one code_signature_audit test
asserts on the macOS `/Applications` prefix, which a PurePath renders with
backslashes on Windows.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0126sAnDC3XiCXD9oS14JSJp
The fixture built an event at `days_ago=1`, i.e. exactly 24 hours before the
fixture's `now`. The module then computes its own `now` a moment later and
compares with `>=`. On Linux the two differ by microseconds and the event falls
outside the 24-hour window, so the "multiple BSODs in 7 days" WARNING is
emitted as the test expects. On Windows, whose clock granularity is around
15ms, both can land on the same tick — the event counts as within 24 hours, the
CRITICAL branch wins instead, and the WARNING never appears.

Not a Windows bug; a test that was always ambiguous and only ever resolved one
way because it only ever ran on one kind of clock. Moved to two days.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0126sAnDC3XiCXD9oS14JSJp
…t runner

Two more from the Windows leg; down to these from twenty.

**`rescue threat-remediation` and `rescue remediation-catalog` wrote their
markdown with the locale codec.** Both documents are full of em-dashes. On
Windows that means writing mojibake into the repository — and the validation
test then compared the regenerated text against the committed file and found
them different, which is the failure that surfaced it. The generated docs are
committed artifacts, so a maintainer regenerating them on Windows would have
produced a diff full of "â€"" and had no idea why. Same fix as the reader side:
explicit UTF-8.

**The TUI end-to-end test assumed one `pilot.pause()` was enough for a screen
push.** It is on a fast Linux runner; on a loaded Windows runner the assertion
ran while the previous screen was still on top. The test already used a bounded
wait loop for the two slow transitions it knew about — that shape is now a
helper used for all of them, so the test waits for the state it is asserting on
instead of hoping.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0126sAnDC3XiCXD9oS14JSJp
…tions

The last Windows failure, and a second problem in the same file that did not
fail but should have.

**test_app_guides_binding started a real scan.** Constructing a RescueApp puts
up the loading screen, which runs the orchestrator against the real module
tree. On Linux only a handful of modules match the platform, so it was cheap
and invisible. On Windows it fires roughly a hundred checks that shell out to
powershell, manage-bde, driverquery and tasklist — the tests passed, and then
the job spent sixteen minutes after the suite finished printing "Module check
timed out" and reaping orphaned processes, with asyncio warning that the
executor could not join its threads. The orchestrator is now stubbed in all
three tests: 24 seconds down to 2, and nothing touches the host. Tests that
reach real system state are the environment coupling this suite has been bitten
by before.

**Two rapid selections raced Textual's Header.** Pushing a screen while the
previous one's Header is still mounting leaves a deferred `set_title` coroutine
querying a HeaderTitle child that has since been torn down, and the framework
raises NoMatches from inside itself. On a fast runner the two never overlap; on
a loaded Windows runner they do. Both guide tests now wait for the screen they
are about to act on, the same bounded-wait shape used elsewhere in the TUI
tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0126sAnDC3XiCXD9oS14JSJp
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants