CI, Linux scanners, catalog validation, case export, TUI guides, update rollback, and a real README - #5
Draft
lizTheDeveloper wants to merge 19 commits into
Draft
CI, Linux scanners, catalog validation, case export, TUI guides, update rollback, and a real README#5lizTheDeveloper wants to merge 19 commits into
lizTheDeveloper wants to merge 19 commits into
Conversation
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this changes
Works through the things
docs/ROADMAP_STATUS.mdrecords 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
rescue/)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 runsrescuefrom 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 throughrescue.command.run, plus alinux_security_checkupprofile 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 guideon 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 thedocsworkflow) 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:
os.getuid()at module import scopewin_user_profilestreated a PowerShell SID object as a stringTypeErroron every real Windows machine.gitattributesread_text()/open()without an encodinggetattr(None, "__doc__", "")returns NoneType's docstring on 3.13dir="/tmp"in a gpg test; seven POSIX-assuming test filespowershell/manage-bdeprocessesAlso fixed: a date-bomb fixture in
test_module_win_windows_update_statusthat aged into a failure, apytest.maincall in a file that never imported pytest, andtest_module_code_consistencymatching onlycode="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
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
packageCI job does:Manual verification: Ubuntu 24.04, all nine Linux modules run end to end against the real host, and
rescue --auto --profile linux_security_checkupreports findings and changes nothing.rescue update --use-bundledverified 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 -qpasses.venv/bin/python -m rescue.cli validateexits 0 (0 errors).venv/bin/ruff check .is cleanrescue/rescue/changedSafety review
--yesActionKind.GUIDANCEand are never reported as completed changessupported=Falsewith a reason, or seterror— never an empty healthy resultauto_apply = Truerescue.command.run; filesystem recursion goes throughrescue.fsbounds.bounded_walkrescue updateemits_codesmatches thecode=literals in the moduleAnything a reviewer should look at closely
The lint gate is narrow on purpose. Ruff's defaults produce 4000 findings here, so
ruff.tomlselects 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--strictgoes 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_auditheuristics (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 --rollbackrefuses 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.