restverify 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- restverify-0.1.0/.gitignore +9 -0
- restverify-0.1.0/CHANGELOG.md +49 -0
- restverify-0.1.0/PKG-INFO +83 -0
- restverify-0.1.0/README.md +71 -0
- restverify-0.1.0/docs/PHASE2-PLAN.md +324 -0
- restverify-0.1.0/docs/RELEASE.md +75 -0
- restverify-0.1.0/docs/SECURITY.md +221 -0
- restverify-0.1.0/pyproject.toml +25 -0
- restverify-0.1.0/rv_testrun.txt +0 -0
- restverify-0.1.0/scripts/security_sweep.py +380 -0
- restverify-0.1.0/src/restverify/__init__.py +25 -0
- restverify-0.1.0/src/restverify/__main__.py +11 -0
- restverify-0.1.0/src/restverify/cli.py +1039 -0
- restverify-0.1.0/src/restverify/compare.py +221 -0
- restverify-0.1.0/src/restverify/config.py +176 -0
- restverify-0.1.0/src/restverify/errors.py +193 -0
- restverify-0.1.0/src/restverify/excludes.py +165 -0
- restverify-0.1.0/src/restverify/history.py +292 -0
- restverify-0.1.0/src/restverify/manifest.py +226 -0
- restverify-0.1.0/src/restverify/restic.py +180 -0
- restverify-0.1.0/src/restverify/sampling.py +193 -0
- restverify-0.1.0/src/restverify/tempstore.py +128 -0
- restverify-0.1.0/tests/conftest.py +293 -0
- restverify-0.1.0/tests/test_cli_contract.py +92 -0
- restverify-0.1.0/tests/test_i1_run.py +228 -0
- restverify-0.1.0/tests/test_i1_safety.py +112 -0
- restverify-0.1.0/tests/test_i2_compare.py +285 -0
- restverify-0.1.0/tests/test_i2_manifest.py +255 -0
- restverify-0.1.0/tests/test_i2_sampling.py +175 -0
- restverify-0.1.0/tests/test_i3a_json.py +202 -0
- restverify-0.1.0/tests/test_i3b_taxonomy.py +148 -0
- restverify-0.1.0/tests/test_i3c_contract.py +102 -0
- restverify-0.1.0/tests/test_i4a_history.py +228 -0
- restverify-0.1.0/tests/test_i4b_report.py +232 -0
- restverify-0.1.0/tests/test_i4c_retention.py +151 -0
- restverify-0.1.0/tests/test_i5a_cron.py +206 -0
- restverify-0.1.0/tests/test_i5b_demo.py +94 -0
- restverify-0.1.0/tests/test_i5c_packaging.py +53 -0
- restverify-0.1.0/tests/test_i6_negatives.py +322 -0
- restverify-0.1.0/tests/test_i6_sweep.py +204 -0
- restverify-0.1.0/tests/test_i7b_reconciliation.py +204 -0
- restverify-0.1.0/tests/test_i7c_closeout.py +68 -0
- restverify-0.1.0/testsuite_final.txt +5 -0
- restverify-0.1.0/testsuite_full.txt +1802 -0
- restverify-0.1.0/testsuite_run2.txt +35 -0
- restverify-0.1.0/testsuite_run3.txt +11 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.1.0] — first release — 2026-09-28
|
|
4
|
+
|
|
5
|
+
### Shipped (I1→I7)
|
|
6
|
+
- **Config + contract (I1):** TOML config (`RESTVERIFY_CONFIG` override; U1: a
|
|
7
|
+
missing config is not an error); a plain `password` key is refused with a
|
|
8
|
+
teaching error, never silently ignored (R2/R24). `restic.py` is the only
|
|
9
|
+
module permitted to invoke restic, and only read-only verbs — mutating verbs
|
|
10
|
+
raise at the call site (N1/N2/N6).
|
|
11
|
+
- **Restore + cleanup (I1):** private (0700) temp dirs carrying ownership
|
|
12
|
+
markers, removed on exit; leftovers from a killed process are swept by the
|
|
13
|
+
next run; foreign directories are never touched.
|
|
14
|
+
- **Manifest + comparison (I2):** restored-tree manifest, deterministic sample
|
|
15
|
+
sha256, excludes-aware source comparison, exit code 2 on drift.
|
|
16
|
+
- **JSON + taxonomy (I3):** `"schema": 1` envelope on every path; stdout stays
|
|
17
|
+
pure; exhaustive exit-code taxonomy test.
|
|
18
|
+
- **History + report (I4):** SQLite store at the XDG state dir (failures
|
|
19
|
+
included), `report` trend, 1000-row retention per repository.
|
|
20
|
+
- **Cron (I5):** `cron` / `--systemd` print a line or a unit pair and never
|
|
21
|
+
install anything.
|
|
22
|
+
- **Security posture (I6):** the negative requirements as executable
|
|
23
|
+
assertions, the dependency claim as a test, `docs/SECURITY.md` with a
|
|
24
|
+
staleness test.
|
|
25
|
+
- **Real-repo validation (I7):** fake-restic fidelity (real exit behaviour),
|
|
26
|
+
missing-repo teaching, restic-glob exclude semantics — **B1 closed**, **U7
|
|
27
|
+
measured**, and the report can no longer rot.
|
|
28
|
+
- **Native Windows support:** the full suite and the run pipeline work on
|
|
29
|
+
Windows (real `restic.exe` fixture, symlink-privilege probe, suffix-matched
|
|
30
|
+
restore root, UTF-8 output pin, and the previously missing
|
|
31
|
+
`python -m restverify` entry point).
|
|
32
|
+
|
|
33
|
+
### What changed for users
|
|
34
|
+
- First public version. Install with `pipx install restverify` (B2 ruling);
|
|
35
|
+
requires Python 3.11+ and a `restic` binary on PATH.
|
|
36
|
+
- The exit codes are the integration surface: `0` verified, `1` run could not
|
|
37
|
+
complete, `2` diff mismatch, `64` usage/config — cron/CI act on them; `64`
|
|
38
|
+
never borrows a verification code.
|
|
39
|
+
- Verification only: bring your own orchestration; restores always go to
|
|
40
|
+
private temp dirs and your password is never stored.
|
|
41
|
+
|
|
42
|
+
### Still open (honest)
|
|
43
|
+
- `init --json` is deferred.
|
|
44
|
+
- C1 (PyYAML ruling) remains open.
|
|
45
|
+
- Verify with a repo you can afford to rehearse on.
|
|
46
|
+
|
|
47
|
+
## [0.1.0.dev0] — Phase 2 scaffold
|
|
48
|
+
- Package layout, CLI surface with full help/examples, exit-code constants,
|
|
49
|
+
12 CLI contract tests.
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: restverify
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Rehearse and verify restic restores locally, on a schedule, with a diff-proof report.
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Provides-Extra: dev
|
|
7
|
+
Requires-Dist: build==1.6.1; extra == 'dev'
|
|
8
|
+
Requires-Dist: pytest==8.3.4; extra == 'dev'
|
|
9
|
+
Provides-Extra: keyring
|
|
10
|
+
Requires-Dist: keyring==25.5.0; extra == 'keyring'
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# restverify
|
|
14
|
+
|
|
15
|
+
Rehearse and verify restic restores locally, on a schedule, with a diff-proof report.
|
|
16
|
+
|
|
17
|
+
> **Status: v0.1.0 — first release.** `run`, `report` and `cron` are implemented
|
|
18
|
+
> and verified against real repos; `init --json` is still deferred. Verify with a
|
|
19
|
+
> repo you can afford to rehearse on.
|
|
20
|
+
|
|
21
|
+
## Why
|
|
22
|
+
|
|
23
|
+
`restic check` validates the repository's internal structure. It does not prove the
|
|
24
|
+
repository can be restored. The restic project tracks this as open issue #2332
|
|
25
|
+
("Backups are only real if they're able to be restored"). Today everyone hand-rolls a
|
|
26
|
+
`restore && diff` script that decays silently.
|
|
27
|
+
|
|
28
|
+
restverify is the verification layer: it restores to a temp directory, compares
|
|
29
|
+
against the source, records the run, and exits with a code your cron/CI can act on.
|
|
30
|
+
It does **not** orchestrate backups.
|
|
31
|
+
|
|
32
|
+
## Install
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pipx install restverify
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Requires Python 3.11+ and a `restic` binary on PATH.
|
|
39
|
+
|
|
40
|
+
## First run
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
restverify run -r /srv/backup
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The run line keeps the Phase-2 field order — `✓ restored snapshot <id>: N files / X
|
|
47
|
+
in Ys; N diffs` — rather than the PDF's example order; three increments of
|
|
48
|
+
regression tests pin this order and the rewrite was cosmetic (I5 ruling R1).
|
|
49
|
+
|
|
50
|
+
## Scheduled verification
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
restverify cron -r /srv/backup # one ready-to-paste crontab line
|
|
54
|
+
restverify cron -r /srv/backup --systemd # a .service + .timer pair instead
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`cron` **prints and never installs**: no crontab is modified, `systemctl` is never
|
|
58
|
+
called, and nothing is written into any unit directory. It prints one example
|
|
59
|
+
cadence and leaves the schedule to you. A scheduler runs with a minimal
|
|
60
|
+
environment, so set `RESTIC_PASSWORD_COMMAND` (or `RESTIC_PASSWORD_FILE`) there —
|
|
61
|
+
restverify never stores your password.
|
|
62
|
+
|
|
63
|
+
## History
|
|
64
|
+
|
|
65
|
+
Every verification writes one row to a local SQLite store — failures included,
|
|
66
|
+
because a trend that hides failures is worse than no trend:
|
|
67
|
+
|
|
68
|
+
- store: `~/.local/state/restverify/history.db` (XDG state dir; override with
|
|
69
|
+
`RESTVERIFY_STATE=/some/dir`)
|
|
70
|
+
- retention: the newest 1000 rows per repository, pruned on write; the first
|
|
71
|
+
prune announces itself on stderr
|
|
72
|
+
- read it with `restverify report` (human) or `restverify report --json`
|
|
73
|
+
(a `"schema": 1` envelope)
|
|
74
|
+
- `--dry-run` writes nothing; disabling the store is not supported yet
|
|
75
|
+
|
|
76
|
+
## Honest limits (required by the contract)
|
|
77
|
+
|
|
78
|
+
- **Biggest risk:** operators who already run a full orchestrator (resticprofile,
|
|
79
|
+
restickler) will not switch. Mitigation: restverify is verification-only — bring your
|
|
80
|
+
own orchestration — and it offers an exit-code contract nothing else provides.
|
|
81
|
+
- Verification is as good as the comparison you ask for: without a source path it only
|
|
82
|
+
proves the restore completed, not that the data matches.
|
|
83
|
+
- Not a backup tool. No cloud targets, no prune/forget, no dashboards, no bouncer.
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# restverify
|
|
2
|
+
|
|
3
|
+
Rehearse and verify restic restores locally, on a schedule, with a diff-proof report.
|
|
4
|
+
|
|
5
|
+
> **Status: v0.1.0 — first release.** `run`, `report` and `cron` are implemented
|
|
6
|
+
> and verified against real repos; `init --json` is still deferred. Verify with a
|
|
7
|
+
> repo you can afford to rehearse on.
|
|
8
|
+
|
|
9
|
+
## Why
|
|
10
|
+
|
|
11
|
+
`restic check` validates the repository's internal structure. It does not prove the
|
|
12
|
+
repository can be restored. The restic project tracks this as open issue #2332
|
|
13
|
+
("Backups are only real if they're able to be restored"). Today everyone hand-rolls a
|
|
14
|
+
`restore && diff` script that decays silently.
|
|
15
|
+
|
|
16
|
+
restverify is the verification layer: it restores to a temp directory, compares
|
|
17
|
+
against the source, records the run, and exits with a code your cron/CI can act on.
|
|
18
|
+
It does **not** orchestrate backups.
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pipx install restverify
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Requires Python 3.11+ and a `restic` binary on PATH.
|
|
27
|
+
|
|
28
|
+
## First run
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
restverify run -r /srv/backup
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
The run line keeps the Phase-2 field order — `✓ restored snapshot <id>: N files / X
|
|
35
|
+
in Ys; N diffs` — rather than the PDF's example order; three increments of
|
|
36
|
+
regression tests pin this order and the rewrite was cosmetic (I5 ruling R1).
|
|
37
|
+
|
|
38
|
+
## Scheduled verification
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
restverify cron -r /srv/backup # one ready-to-paste crontab line
|
|
42
|
+
restverify cron -r /srv/backup --systemd # a .service + .timer pair instead
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
`cron` **prints and never installs**: no crontab is modified, `systemctl` is never
|
|
46
|
+
called, and nothing is written into any unit directory. It prints one example
|
|
47
|
+
cadence and leaves the schedule to you. A scheduler runs with a minimal
|
|
48
|
+
environment, so set `RESTIC_PASSWORD_COMMAND` (or `RESTIC_PASSWORD_FILE`) there —
|
|
49
|
+
restverify never stores your password.
|
|
50
|
+
|
|
51
|
+
## History
|
|
52
|
+
|
|
53
|
+
Every verification writes one row to a local SQLite store — failures included,
|
|
54
|
+
because a trend that hides failures is worse than no trend:
|
|
55
|
+
|
|
56
|
+
- store: `~/.local/state/restverify/history.db` (XDG state dir; override with
|
|
57
|
+
`RESTVERIFY_STATE=/some/dir`)
|
|
58
|
+
- retention: the newest 1000 rows per repository, pruned on write; the first
|
|
59
|
+
prune announces itself on stderr
|
|
60
|
+
- read it with `restverify report` (human) or `restverify report --json`
|
|
61
|
+
(a `"schema": 1` envelope)
|
|
62
|
+
- `--dry-run` writes nothing; disabling the store is not supported yet
|
|
63
|
+
|
|
64
|
+
## Honest limits (required by the contract)
|
|
65
|
+
|
|
66
|
+
- **Biggest risk:** operators who already run a full orchestrator (resticprofile,
|
|
67
|
+
restickler) will not switch. Mitigation: restverify is verification-only — bring your
|
|
68
|
+
own orchestration — and it offers an exit-code contract nothing else provides.
|
|
69
|
+
- Verification is as good as the comparison you ask for: without a source path it only
|
|
70
|
+
proves the restore completed, not that the data matches.
|
|
71
|
+
- Not a backup tool. No cloud targets, no prune/forget, no dashboards, no bouncer.
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
# PHASE 2.0 — PLANNING & VERIFICATION (blocking)
|
|
2
|
+
|
|
3
|
+
Contract: **"PRODUCT GAP RESEARCH — 2026-09-14 (final)"** = `GAP-RESEARCH-2026-09.pdf`
|
|
4
|
+
(`/home/deploy/ezmcyber/.firecrawl/GAP-RESEARCH-2026-09.pdf`, 4 pages, text-extracted
|
|
5
|
+
and read in full). Where the PDF, the master prompt, and this repo differ, this
|
|
6
|
+
document says so explicitly (see **Conflicts & deviations**). Nothing here is improvised.
|
|
7
|
+
|
|
8
|
+
Build order is mandated and not parallelised: **restverify → haccheck**.
|
|
9
|
+
Scaffolding + test infrastructure only may precede the GO gate below.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## 2.0.1 Spec extraction — numbered, testable requirements
|
|
14
|
+
|
|
15
|
+
### PRODUCT 1 — restverify (PDF lines 53–90)
|
|
16
|
+
|
|
17
|
+
| # | Requirement (PDF) | Testable assertion |
|
|
18
|
+
|---|---|---|
|
|
19
|
+
| R1 | `init` writes a multi-repo TOML config | config parses with `tomllib`; 2 repos round-trip |
|
|
20
|
+
| R2 | Passwords via keychain / `RESTIC_PASSWORD_COMMAND` **only** | no code path writes a password; absence test over the package |
|
|
21
|
+
| R3 | Config records source paths per repo | source persisted and overridable via `--source` |
|
|
22
|
+
| R4 | Config records excludes | excludes applied to both restore and compare |
|
|
23
|
+
| R5 | Snapshot selector | `latest` default; explicit id/tag selectable |
|
|
24
|
+
| R6 | `run` lists snapshots then picks the newest | `restic snapshots --json` is called once, newest chosen |
|
|
25
|
+
| R7 | Restore to a temp directory | restore target is under the system temp dir |
|
|
26
|
+
| R8 | Produce a manifest + sample sha256 | manifest counts/sizes; N files hashed deterministically |
|
|
27
|
+
| R9 | Optional excludes-aware source compare | `--no-source` skips it; excludes honored when enabled |
|
|
28
|
+
| R10 | `report` reads SQLite run history, PASS/FAIL | rows have verdict, snapshot id, duration, reason |
|
|
29
|
+
| R11 | Exit codes `0=pass 1=restore-fail 2=diff-mismatch` | G2: each code asserted by a test |
|
|
30
|
+
| R12 | `--json` | valid JSON on stdout, complete fields, non-zero exit still emits JSON |
|
|
31
|
+
| R13 | `--dry-run` | restores nothing; prints what would happen; PASS line still printed |
|
|
32
|
+
| R14 | `--no-source` | verification without a source comparison succeeds |
|
|
33
|
+
| R15 | `cron` prints a crontab line | output is a pasteable line; nothing is installed |
|
|
34
|
+
| R16 | Config at `~/.config/restverify` | default path asserted; `--config` overrides |
|
|
35
|
+
| R17 | Temp restore cleaned up on exit | cleanup verified incl. `kill -9` (G3) |
|
|
36
|
+
| R18 | No daemon | absence test: no long-running process/listener created |
|
|
37
|
+
| R19 | Linux/macOS/WSL | path handling avoids platform assumptions |
|
|
38
|
+
| R20 | pipx/pip + Homebrew | entry point installs; Homebrew formula stub shipped (unverifiable on Linux — see C5) |
|
|
39
|
+
| R21 | Install → first verified run < 5 min | V3 timed proof |
|
|
40
|
+
| R22 | 10-second demo shape `✓ restored N files / X GB in Ys; N diffs; snapshot <id>` | output asserted against this shape (U5) |
|
|
41
|
+
| R23 | Read-only on repositories | no write/delete call against a repo |
|
|
42
|
+
| R24 | Passwords never stored by the tool | absence test + no secret in logs/history |
|
|
43
|
+
| R25 | Zero telemetry | V2: no network imports in shipped paths |
|
|
44
|
+
| R26 | Never writes to sources; `--strict` is the explicit compare | source paths opened read-only; `--strict` only tightens comparison |
|
|
45
|
+
| R27 | `restic` binary is a documented prerequisite | missing restic → teaching error (U2), never a traceback |
|
|
46
|
+
| R28 | Pinned, signed releases | release process doc; pinned dev deps |
|
|
47
|
+
| R29 | Human-readable byte totals (`cli.human_bytes()`, binary units, one decimal) | formatting asserted by a parametrised unit test; consumed by the run/demo line (R22) and the I4 `report` |
|
|
48
|
+
|
|
49
|
+
### Negative requirements — PRODUCT 1 (PDF line 63)
|
|
50
|
+
|
|
51
|
+
| # | Must NOT exist | Absence test |
|
|
52
|
+
|---|---|---|
|
|
53
|
+
| N1 | backup creation | no `restic backup` invocation anywhere in shipped paths |
|
|
54
|
+
| N2 | prune/forget | no `restic prune` / `restic forget` invocation |
|
|
55
|
+
| N3 | cloud targets | no provider SDK/credential handling; repo is an opaque restic location |
|
|
56
|
+
| N4 | dashboards/web UI | no web framework import; no listener |
|
|
57
|
+
| N5 | notification integrations | no SMTP/webhook/chat SDK; only exit codes + an optional command hook that the user supplies |
|
|
58
|
+
| N6 | borg/tar backends | no borg/tar code path |
|
|
59
|
+
| N7 | Windows UI | no Windows-specific code; no GUI toolkit |
|
|
60
|
+
|
|
61
|
+
### PRODUCT 2 — haccheck (PDF lines 143–168)
|
|
62
|
+
|
|
63
|
+
| # | Requirement (PDF) | Testable assertion |
|
|
64
|
+
|---|---|---|
|
|
65
|
+
| R30 | `haccheck <config-dir>` needs nothing else | positional arg only; works on a fixture dir |
|
|
66
|
+
| R31 | Semantic YAML parse: dup keys, multi-doc, quoting/bool pitfalls | each pitfall has a fixture and a distinct message |
|
|
67
|
+
| R32 | HA-aware unknown-key rule for a ~40-integration starter catalog | catalog ships ≥40 integrations; unknown key in a known integration is reported with file:line |
|
|
68
|
+
| R33 | Integration-name case rule | `Sensor` vs `sensor` produces a finding naming the correct form |
|
|
69
|
+
| R34 | Duplicate `entity_id` rule | cross-file duplicates reported once each |
|
|
70
|
+
| R35 | Automation trigger/action shape rule | malformed trigger/action reported with file:line |
|
|
71
|
+
| R36 | Broken `!include` / `!secret` | missing target reported; existing target not |
|
|
72
|
+
| R37 | Dangling `packages:` entries | dangling reference reported |
|
|
73
|
+
| R38 | Jinja compile, strict-undefined, stubbed context — **the real error, never the masked one** | the `#169958` fixture must show the underlying `TemplateAssertionError` text, not the generic schema message |
|
|
74
|
+
| R39 | Severity + plain-language hint per finding | every finding carries tier + one-line hint |
|
|
75
|
+
| R40 | Exit `0` / `1` | errors → 1; warnings/info only → 0 (with `--strict` escalating) |
|
|
76
|
+
| R41 | `--json` | complete machine-readable findings incl. suppressed counts |
|
|
77
|
+
| R42 | Bundled catalog, no runtime fetch | no network import; catalog read from package data |
|
|
78
|
+
|
|
79
|
+
### Negative requirements — PRODUCT 2 (PDF line 149)
|
|
80
|
+
|
|
81
|
+
| # | Must NOT exist | Absence test |
|
|
82
|
+
|---|---|---|
|
|
83
|
+
| N8 | full HA schema | no bundled complete schema; catalog is explicitly partial and self-describing |
|
|
84
|
+
| N9 | live-instance communication | no HTTP client to HA; no token/URL handling |
|
|
85
|
+
| N10 | auto-fixing | no write path to the config dir in v1 (read-only; asserted by making the fixture dir read-only) |
|
|
86
|
+
| N11 | YAML rewriting | no YAML dump/write in shipped paths |
|
|
87
|
+
| N12 | Fleet/Edge | absent |
|
|
88
|
+
| N13 | no false-error on unknown integrations | unknown integration → informational tier only (asserted) |
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## 2.0.2 Environment audit (real output, this box)
|
|
93
|
+
|
|
94
|
+
| Probe | Result | Consequence |
|
|
95
|
+
|---|---|---|
|
|
96
|
+
| `python3 --version` | 3.12.3 | OK |
|
|
97
|
+
| python3.11 / 3.12 | 3.11.15 present, 3.12.3 present | OK |
|
|
98
|
+
| python3.10 | **absent** | `tomllib` needs 3.11 → see C1 |
|
|
99
|
+
| `uv --version` | 0.12.0 | usable for venvs/tools |
|
|
100
|
+
| `pipx` | **absent** | V3 wording says "pipx install"; `uv tool install` is the equivalent — see B2 |
|
|
101
|
+
| `restic` | **absent** | BLOCKS I7 real-repo validation — see B1 |
|
|
102
|
+
| `asciinema` | **absent** | V4 demo artifact needs an alternative — see B3 |
|
|
103
|
+
| `brew` | absent (Linux) | Homebrew formula can be authored, not verified here — see C5 |
|
|
104
|
+
| `pdftotext` | present | PDF read in full (232 lines extracted) |
|
|
105
|
+
|
|
106
|
+
**I7 ran elsewhere.** The table above audits the *primary* build box, and on that
|
|
107
|
+
box `restic`, `pipx` and `asciinema` are all still absent, so the rows stand. The
|
|
108
|
+
operator ruled I7 onto the second host `vmi3416386` (the cowrie box), where
|
|
109
|
+
`restic 0.16.4` (package `0.16.4-2ubuntu0.24.04.3`), `pipx 1.4.3` and
|
|
110
|
+
`asciinema 2.4.0` were installed with authorisation, and the checkout stayed here
|
|
111
|
+
as the single source of truth (the wheel was built and committed here, then
|
|
112
|
+
installed on that host). Evidence: `restverify-I7-briefing.md` §1 and §6.
|
|
113
|
+
|
|
114
|
+
### Git state (2.0.2 requires a clean tree + recorded base commit)
|
|
115
|
+
|
|
116
|
+
The two products are **standalone repos** (Gate F: one repo, one job). They are
|
|
117
|
+
created at `/home/deploy/oss/<product>`, so the WraithWall monorepo's dirty tree
|
|
118
|
+
(122 uncommitted entries at audit time, from unrelated in-flight work) does not
|
|
119
|
+
contaminate them.
|
|
120
|
+
|
|
121
|
+
- Parent monorepo `/home/deploy/ezmcyber`: branch `phase1-sandbox-contract`,
|
|
122
|
+
HEAD at audit **`07e8e93`** (note: HEAD moved during this session — it was
|
|
123
|
+
`b344f50` at the start; the intervening P4–P7 commits are breach-monitor work
|
|
124
|
+
and unrelated to these products).
|
|
125
|
+
- Product repo `/home/deploy/oss/restverify`: new repo, branch
|
|
126
|
+
**`product/restverify/phase2`**, first commit = this scaffold.
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## 2.0.3 Dependency lock
|
|
131
|
+
|
|
132
|
+
| Product | Locked set (master prompt 2.0.3) | What we ship | Justification |
|
|
133
|
+
|---|---|---|---|
|
|
134
|
+
| restverify | stdlib + PyYAML (+optional keyring) | **stdlib only**; `keyring==25.5.0` optional extra | Zero runtime deps is strictly *inside* the lock (nothing outside it is introduced). The PDF's "PyYAML/JSON" was a suggestion of capability, and this design does not need YAML at all (see C1). |
|
|
135
|
+
| haccheck | PyYAML + Jinja2, exact pins | `PyYAML==6.0.2`, `Jinja2==3.1.4` (to be pinned at I1) | Exactly the locked set; nothing else. |
|
|
136
|
+
| dev (both) | — | `pytest==8.3.4` | Not shipped to users. |
|
|
137
|
+
|
|
138
|
+
Rationale for the optional keyring: PDF R2 says passwords come "via keychain /
|
|
139
|
+
`RESTIC_PASSWORD_COMMAND` only". `RESTIC_PASSWORD_COMMAND` needs no dependency;
|
|
140
|
+
keyring is an *optional extra* only (`restverify[keyring]`) and is never required.
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## 2.0.4 Increment plan (commit-sized, each with a gate)
|
|
145
|
+
|
|
146
|
+
Every increment = one commit + one green test run + the standing gates
|
|
147
|
+
G1 (tests incl. ≥1 adversarial), G2 (exit codes), G3 (security: no network, no
|
|
148
|
+
write to sources, no secrets, temp cleanup under `kill -9`), G4 (U2/U3/U5 text),
|
|
149
|
+
G5 (diff ↔ requirement numbers), G6 (honest status).
|
|
150
|
+
|
|
151
|
+
### PRODUCT 1 — restverify (PDF D1–D7 → I1–I7)
|
|
152
|
+
|
|
153
|
+
| I | Scope | Extra gate beyond the standing five |
|
|
154
|
+
|---|---|---|
|
|
155
|
+
| I1 | config/contract + restore-to-temp + cleanup-on-exit (R1,R2,R3,R5,R7,R16,R17,R23,R24,R27,N1,N2,N6) | adversarial: missing `restic`, empty repo, permission-denied temp dir; `kill -9` cleanup proof |
|
|
156
|
+
| I2 | manifest + sample sha256 + excludes-aware source compare (R4,R8,R9,R26,R29) | N-diffs == 0 on an identical tree; a single-byte change is detected |
|
|
157
|
+
| I3 | error handling + exit codes + `--json` `--dry-run` `--no-source` (R11,R12,R13,R14) | **G2 fully**: all three codes asserted; JSON valid on both success and failure |
|
|
158
|
+
| I4 | SQLite run history + `report` (R10) | trend renders; last failure reason in plain language; history survives restart |
|
|
159
|
+
| I5 | CLI surface + `cron` printer (R15,R22) | demo line matches the PDF shape within tolerance; `cron` installs nothing |
|
|
160
|
+
| I6 | test hardening + security pass (R25,N3,N4,N5,N7) | V2 sweep run and pasted into the report |
|
|
161
|
+
| I7 | docs, packaging, Homebrew stub, **≥2 real restic repos**, release prep (R20,R21,R28) | V1–V6 executed; V3 timed proof recorded |
|
|
162
|
+
|
|
163
|
+
Usability focus carried through: `init` walks the user interactively with sane
|
|
164
|
+
defaults; the first `run` prints the PASS line even under `--dry-run`; `report`
|
|
165
|
+
shows the trend with the last failure explained in plain language.
|
|
166
|
+
|
|
167
|
+
### Test infrastructure (recorded 2026-09-19, closing the I2 G5 PARTIAL)
|
|
168
|
+
|
|
169
|
+
Test infrastructure is code under `tests/` that never ships. It carries **no
|
|
170
|
+
requirement id by design**; the rule that keeps G5 meaningful is the opposite one
|
|
171
|
+
for shipped code:
|
|
172
|
+
|
|
173
|
+
> Any new *shipped* helper that no requirement id covers must either gain a
|
|
174
|
+
> requirement row in the same commit that introduces it, or not ship.
|
|
175
|
+
> Test-only helpers are exempt, but every non-obvious one must be named in this
|
|
176
|
+
> section and explained in the docstring of the module that holds it.
|
|
177
|
+
|
|
178
|
+
| test infrastructure | why it exists | where it is documented |
|
|
179
|
+
|---|---|---|
|
|
180
|
+
| `tests/conftest.py` fake-restic harness (`FAKE_RESTIC`, `FAKE_FILES`/`FAKE_DIRS`/`FAKE_LINKS`, `plant_fake_tree`, `source_tree`, `tmp_base`, `config_path`) | the entire suite is deterministic and offline because a fake `restic` is put on `PATH` (B1: no real restic on the build box); `plant_fake_tree`/`source_tree` let a test plant a byte-identical source tree on purpose | module docstring + the I2/I1 briefings |
|
|
181
|
+
| `tests/test_i2_compare.py::fingerprint()` | proves the source tree is never written to (size + mtime_ns + sha256 before/after) — evidence for R26/R23 | that module's docstring |
|
|
182
|
+
| `tests/test_i2_manifest.py::test_human_bytes_units` | the R29 formatter's contract test (6 parametrised cases) | test docstring names R29 |
|
|
183
|
+
| `tests/conftest.py` `isolated_state` / `_state_base` | every test gets its own durable-state dir (autouse, `RESTVERIFY_STATE`), so the suite can never write the real `~/.local/state/restverify`; the per-session base prefers tmpfs because the store now does a real fsync per write | that fixture's docstring + the I4 briefing |
|
|
184
|
+
| `tests/test_i6_negatives.py` source index (`SHIPPED`, `_trees`, `_imports`, `_dotted_name`, `_env_read_names`, `_literal_strings`, `_spawn_calls`, `_listener_calls`, `_platform_branches`) | turns N3/N4/N5/N7 from prose into assertions: it parses every shipped module and answers "which module imports/spawns/listens/reads X". Deliberately **not** shared with `scripts/security_sweep.py` (I6b) — one broken walker would otherwise silently disable every absence test at once. Non-obvious parts: dynamic `importlib.import_module("x")` arguments count as imports, and only *named* env reads (`os.environ.get("AWS_…")`) count, not `dict(os.environ)` | module docstring + the I6 briefing |
|
|
185
|
+
|
|
186
|
+
Worked example of the shipped-code rule: `cli.human_bytes()` arrived as an
|
|
187
|
+
unplanned extra during I2 and was **numbered R29** (added to the requirement
|
|
188
|
+
table and to the I2 row) when the G5 PARTIAL was closed — it is user-visible
|
|
189
|
+
output formatting, so it could not stay uncatalogued.
|
|
190
|
+
|
|
191
|
+
### PRODUCT 2 — haccheck (PDF D1–D7 → I1–I7) — starts only after Product 1 ships
|
|
192
|
+
|
|
193
|
+
| I | Scope | Extra gate |
|
|
194
|
+
|---|---|---|
|
|
195
|
+
| I1 | config-dir walker + robust YAML loader (R30,R31) | malformed/dup-key/multi-doc fixtures; unicode paths |
|
|
196
|
+
| I2 | versioned catalog + unknown-key/case rules (R32,R33,R42) | ≥40 integrations; unknown integration is **info**, never error (N13) |
|
|
197
|
+
| I3 | Jinja compile, strict-undefined, stubbed context (R38) | **MANDATORY #169958 fixture.** If the underlying error cannot be surfaced, STOP and report — no degraded ship |
|
|
198
|
+
| I4 | `!include`/`!secret`/`packages:` + duplicate `entity_id` (R34,R35,R36,R37) | dangling vs resolvable cases distinguished |
|
|
199
|
+
| I5 | severity tiers + suppression + hints + exit 0/1 (R39,R40,R41) | suppression never hides silently: suppressed counts always printed |
|
|
200
|
+
| I6 | tests incl. real public HA config corpora + security pass (N8–N12) | read-only proof on a read-only fixture dir |
|
|
201
|
+
| I7 | docs, packaging, pre-commit stub, release prep | V1–V6 |
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
## 2.0.5 haccheck risk paragraph (load-bearing)
|
|
206
|
+
|
|
207
|
+
**Severity tiers.** Three tiers, and the tier assignment is the contract that keeps
|
|
208
|
+
false positives from killing adoption: `error` (exit 1) is reserved for conditions
|
|
209
|
+
Home Assistant itself will reject or that cannot work — invalid YAML, duplicate
|
|
210
|
+
keys, unresolvable `!include`/`!secret`, a Jinja compile failure (the #169958
|
|
211
|
+
class), duplicate `entity_id`, and an unknown key **inside a known integration**.
|
|
212
|
+
`warning` (exit 0; exit 1 only under `--strict`) covers suspicious-but-possible
|
|
213
|
+
shapes such as a deprecated key or an unusual automation trigger shape. `info`
|
|
214
|
+
(always exit 0) covers catalog-coverage gaps: an integration we do not know is
|
|
215
|
+
**never** an error (PDF mandate N13), it is reported as "not in catalog v0.1" with
|
|
216
|
+
the catalog version, so the tool stays honest about its own partiality instead of
|
|
217
|
+
inventing errors.
|
|
218
|
+
|
|
219
|
+
**Versioned catalog layout.** `src/haccheck/catalog/<version>/` containing
|
|
220
|
+
`integrations` (names + known keys), `rules` (rule id, tier, message template,
|
|
221
|
+
hint), and `meta` (`ha_min_version`, `generated_from`, per-rule
|
|
222
|
+
`false_positive_mode`). `--catalog` selects a version; an unknown version errors
|
|
223
|
+
with the list of shipped ones. New HA releases get a **new catalog version**
|
|
224
|
+
instead of silently editing the old one, so a user can pin behaviour and a
|
|
225
|
+
regression can be bisected to a catalog change. Drift mitigation follows the PDF's
|
|
226
|
+
"check what we know" philosophy: unknown keys only ever reach `warning` when the
|
|
227
|
+
catalog's `ha_min_version` predates the `--ha-version` the user declares, and
|
|
228
|
+
`--explain <rule-id>` prints that rule's documented false-positive mode so a user
|
|
229
|
+
can judge a hit rather than trust it.
|
|
230
|
+
|
|
231
|
+
**Suppression mechanism.** Two one-line forms, both requiring no config file:
|
|
232
|
+
`.haccheckignore` with `rule-id path-or-glob [reason]` per line, and an inline
|
|
233
|
+
`# haccheck: ignore=<rule-id>` on the offending line. `haccheck --suppress-help`
|
|
234
|
+
(also shown in the summary of any run that had suppressions) prints a
|
|
235
|
+
ready-to-paste example. Suppressed findings are **counted and printed** in both
|
|
236
|
+
text and `--json` output — suppression is visible, never silent, mirroring the
|
|
237
|
+
`fp_guard` posture already used in this codebase (a suppressed finding is demoted,
|
|
238
|
+
never deleted).
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
242
|
+
## Conflicts & deviations (required: "PDF wins over this prompt; inform the operator")
|
|
243
|
+
|
|
244
|
+
### C1 — Config format vs the dependency lock vs the Python floor (BLOCKING, needs your call)
|
|
245
|
+
The PDF says two things that cannot both hold literally:
|
|
246
|
+
- MVP: "`restverify init` (multi-repo **TOML** config)" and "config at `~/.config/restverify`".
|
|
247
|
+
- Architecture: "Python 3.10+ CLI (stdlib + **PyYAML**/JSON + sqlite3)".
|
|
248
|
+
|
|
249
|
+
The master prompt then locks "stdlib + PyYAML (+optional keyring)". A TOML config
|
|
250
|
+
written by the tool needs a TOML *writer*; `tomllib` (stdlib, read-only) only
|
|
251
|
+
exists on **3.11+**, while the PDF's floor is 3.10.
|
|
252
|
+
|
|
253
|
+
Options:
|
|
254
|
+
1. **TOML config, Python 3.11+** — `tomllib` reads; we write a ~20-line serializer for
|
|
255
|
+
our simple schema. No new dependency, PDF's TOML is honoured, floor raised to 3.11.
|
|
256
|
+
*(Recommended.)*
|
|
257
|
+
2. TOML config, keep 3.10 — requires a hand-rolled minimal TOML reader (more code,
|
|
258
|
+
more adversarial surface, departs further from the lock).
|
|
259
|
+
3. YAML config via PyYAML — honours the lock and 3.10, but contradicts the PDF's
|
|
260
|
+
explicit "TOML" and the prompt's U1 spirit (a config the user must not need to touch).
|
|
261
|
+
|
|
262
|
+
**Recommendation: option 1.** Scaffold already reflects it (`requires-python = ">=3.11"`,
|
|
263
|
+
zero runtime deps). I will not proceed on this without your GO.
|
|
264
|
+
|
|
265
|
+
### C2 — haccheck catalog format: the PDF contradicts itself
|
|
266
|
+
Architecture line says "TOML schema catalog"; the security line says "**bundled JSON**
|
|
267
|
+
catalog". Same document, two formats.
|
|
268
|
+
**Recommendation:** TOML (`tomllib`, read-only, versioned dir under `catalog/`) because
|
|
269
|
+
it is human-diffable in PRs and matches the rest of the tool's config surface; JSON
|
|
270
|
+
only if you prefer the security line to win. Recorded here so the choice is yours, not mine.
|
|
271
|
+
|
|
272
|
+
### C3 — Dependency lock vs "as few as possible"
|
|
273
|
+
The PDF permits PyYAML; this design ships **zero** runtime dependencies. That is stricter
|
|
274
|
+
than the lock, not looser, so nothing outside the permitted set is introduced. Flagged
|
|
275
|
+
because it is a deliberate deviation from the PDF's stated stack, in the safe direction.
|
|
276
|
+
|
|
277
|
+
### C4 — No license specified anywhere in the PDF
|
|
278
|
+
Both products are to be open-source (prompt: "free, open-source CLI tools") and V5
|
|
279
|
+
requires an honest README, but the PDF names no license. **Your call: MIT or Apache-2.0.**
|
|
280
|
+
restverify shells out to the `restic` binary (BSD-2-Clause) and links no restic code, so
|
|
281
|
+
there is no copyleft entanglement either way. This is a release-blocking (V-gate) item,
|
|
282
|
+
not a coding blocker.
|
|
283
|
+
|
|
284
|
+
### C5 — Homebrew stub cannot be verified on this box
|
|
285
|
+
`brew` is absent (Linux). I7 will author the formula and mark it **UNVERIFIED**;
|
|
286
|
+
claiming otherwise would breach G6/V-gates.
|
|
287
|
+
|
|
288
|
+
---
|
|
289
|
+
|
|
290
|
+
## Blockers (must be resolved before the increments that need them)
|
|
291
|
+
|
|
292
|
+
| # | Blocker | Needed by | Proposed resolution |
|
|
293
|
+
|---|---|---|---|
|
|
294
|
+
| B1 | `restic` not installed on this box | **I7** (PDF: "validation against ≥2 real restic repos, local backend acceptable") | install restic (apt/release binary) and build two throwaway local repos; I1–I6 can proceed without it using a fake-restic harness for unit tests |
|
|
295
|
+
| B2 | `pipx` absent (V3 says "pipx install from local artifact") | **V3 / I7** | install pipx, or record `uv tool install` as the equivalent and state the substitution explicitly in the V3 proof |
|
|
296
|
+
| B3 | `asciinema` absent (V4 demo artifact) | **V4 / I7** | install asciinema, or ship a scripted `script(1)` typescript + the exact command transcript |
|
|
297
|
+
| B4 | Biggest product risk (PDF): operators running a full orchestrator will not switch | design, not tooling | mitigation is the PDF's own: verification-only positioning + the exit-code contract; stated verbatim in the README's honest-limits section |
|
|
298
|
+
|
|
299
|
+
---
|
|
300
|
+
|
|
301
|
+
## HALT-gate check before GO
|
|
302
|
+
|
|
303
|
+
- PDF vs repo divergence found? **Yes → reported** (C1, C2, C3, C5 above, none improvised).
|
|
304
|
+
- Dependency outside the locked set forced? **No** (C3 makes the set smaller).
|
|
305
|
+
- A PDF claim failing under test? **Not yet tested** — the first such claim to test is the
|
|
306
|
+
#169958 masking behaviour at haccheck I3; per the halt rule, if it no longer reproduces
|
|
307
|
+
I will report the delta and **not** improvise.
|
|
308
|
+
|
|
309
|
+
---
|
|
310
|
+
|
|
311
|
+
## GO GATE (2.0.4 requires STOP here)
|
|
312
|
+
|
|
313
|
+
**Status: scaffold committed; no product behaviour implemented. Awaiting operator GO.**
|
|
314
|
+
|
|
315
|
+
On `GO`, I proceed in order: **restverify I1 → I2 → … → I7**, then **haccheck I1 → … → I7**,
|
|
316
|
+
reporting the full R1–R9 completion report per product. No parallelisation. No Phase 3.
|
|
317
|
+
|
|
318
|
+
Outstanding decisions requested:
|
|
319
|
+
1. **C1** — TOML + Python 3.11+ (recommended) / TOML + 3.10 custom reader / YAML.
|
|
320
|
+
2. **C2** — haccheck catalog: TOML (recommended) or JSON.
|
|
321
|
+
3. **C4** — license: MIT or Apache-2.0.
|
|
322
|
+
4. **B2/B3** — may I install `pipx` and `asciinema` on this box, or should I record the
|
|
323
|
+
`uv tool install` / scripted-typescript substitutions for V3/V4?
|
|
324
|
+
5. **B1** — may I install `restic` + create two throwaway local repos for I7 validation?
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Release checklist
|
|
2
|
+
|
|
3
|
+
Feeds **R28** ("Pinned, signed releases"). Seven steps, in order. Nothing here is
|
|
4
|
+
automated yet; each step is a command a human runs and reads the output of.
|
|
5
|
+
|
|
6
|
+
## 1. Version bump
|
|
7
|
+
|
|
8
|
+
`pyproject.toml` → `[project] version`. Update it in one commit with the changelog
|
|
9
|
+
entry so the tag and the artefact agree. The dev suffix (`0.1.0.dev0`) is dropped
|
|
10
|
+
for the first real release.
|
|
11
|
+
|
|
12
|
+
## 2. Changelog
|
|
13
|
+
|
|
14
|
+
Add the entry to the release notes with three headings, always in this shape:
|
|
15
|
+
|
|
16
|
+
- what shipped (requirement ids),
|
|
17
|
+
- what changed in behaviour a user will notice,
|
|
18
|
+
- what is still open (B1/C1/U7 style honest limits).
|
|
19
|
+
|
|
20
|
+
## 3. Build
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
cd <checkout>
|
|
24
|
+
python -m build # requires the dev extra: pip install -e '.[dev]'
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Expect `dist/restverify-<version>-py3-none-any.whl` and the sdist. A wheel that
|
|
28
|
+
contains `tests/`, `__pycache__/`, or a `history.db` is a failed build — inspect it:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
python -m zipfile -l dist/restverify-<version>-py3-none-any.whl
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## 4. Install
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pipx install ./dist/restverify-<version>-py3-none-any.whl
|
|
38
|
+
pipx list
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
`pipx` is the supported install path (B2 ruling). One venv per app, no
|
|
42
|
+
site-packages pollution, and the console script lands on PATH.
|
|
43
|
+
|
|
44
|
+
## 5. Smoke test
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
restverify --version
|
|
48
|
+
restverify --help
|
|
49
|
+
restverify cron -r /srv/backup # prints a line; must not install anything
|
|
50
|
+
restverify run -r <repo> --dry-run # must print the PASS line and write nothing
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
If any of those fails, stop: the release is not shippable. Do not "fix forward"
|
|
54
|
+
on a tagged artefact - bump the version and rebuild.
|
|
55
|
+
|
|
56
|
+
## 6. Tag
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
git tag -s v<version> -m "restverify <version>"
|
|
60
|
+
git push --follow-tags
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Signed tags only (`-s`). An unsigned tag fails R28's intent.
|
|
64
|
+
|
|
65
|
+
## 7. Publish
|
|
66
|
+
|
|
67
|
+
Publish the wheel and sdist to the index (or attach both to the signed tag if
|
|
68
|
+
publishing is deliberately deferred). Record the published hash in the release
|
|
69
|
+
notes so a downloader can verify the artefact they got is the one that was built.
|
|
70
|
+
|
|
71
|
+
## After the release
|
|
72
|
+
|
|
73
|
+
Re-open the loop on the honest limits: B1 (no real restic exercised), C1 (PyYAML
|
|
74
|
+
ruling), U7 (install-to-value not measured). A release that hides those is worse
|
|
75
|
+
than a release note that names them.
|