feat(FAR-676): doctor full - extended checks, status --json, exit codes, --report, --fix, logs - #349
Conversation
farnalabs
left a comment
There was a problem hiding this comment.
Feedback review (FAR-676, head ade5fbc) — feedback only; formal decision comes from the post-decision node.
Overall: solid structure — injected probes, never-crash check contract, locked exit-code table, and a genuinely tested redaction contract for doctor --report. Good reuse of reconcile_orphans (--fix), the boot-path predicates, and is_sensitive_env_key for KV redaction.
Requesting changes on these findings:
- Three probes are never wired in
default_probes, so checks 17/20/21 are inert on a real machine:
check_port_collisions: with the launcher running and the default stub probe returning None it emits a positive 'no compose/system PG or Redis service owns the configured bundles' ports' — a silent pass claiming an audit that never ran. This contradicts the module contract 'honest skips, never silent passes'; either wire a real attribution probe (or psutil/ss-backed scan) or emit an explicit 'attribution probe not wired' skip message.check_stale_backup:modulo backupalready stampslast_backup_atinto state.json (cli/backup.py:736) andrun_doctorloads that state — but the probe stub returns None unconditionally, so the check always prints 'state.json schema v1 does not record one yet' (factually false on v2 data dirs) and the stale-backup WARNING can never fire for a real operator. Parsestate.last_backup_at(ISO) onto the epoch probe.check_tls_expiry: always 'no TLS keypair' skip; fine today (no keypair generator exists yet) but the message would be false if one is added later — consider a 'probe not wired (FAR-xxx)' style honest skip.
-
_state_problem_kindgap: state.json missing while secrets.json present → error 'no state.json in ...' matches no pattern → classified 'corrupt' → exit 1 with 'state.json is CORRUPT (torn write or not an HMAC envelope)'. A missing-state kind deserves its own language. -
_host_port_from_database_url:parsed.portis accessed outside the try (the try only wrapsurlparse). A malformed ambient DATABASE_URL likepostgresql://host:99999/dbraises 'Port out of range 0-65535' on theparsed.portaccess, which surfaces as 'settings-source: check crashed' FAIL (exit 1) instead of the docstring-promised ('unknown', None) honest skip. -
modulo logs --rotate(app log):rotate_log's own docstring says it is 'safe ONLY while no process holds the file open for appending; callers must guarantee that' — the CLI caller never checks whether an attached launcher is running; rotatinglauncher.logunder a live stdio redirect means post-rename writes land in the rotated-away file. Please add a launcher-running guard or a warning. -
--reportswallows ALL doctor output into the sink (terminal shows only 'diagnostic report written: ...' and the exit code) — consider teeing emit lines to stdout so the operator still sees the check table.
Minor: _component_state ignores its port arg; build_report calls log_paths(data_dir) twice per member; percent-encoded password forms inside URLs are not covered by the seeded exact-match scrub.
Skip-marker additions are conditional (win32 skipif / dev-env _settings_env_ready) — no coverage lost on Linux CI; noted for the policy-router's mixed-diff rule.
modulo-reviewbot
left a comment
There was a problem hiding this comment.
Review decision: CHANGES_REQUESTED
Reviewed the three-dot diff of FAR-676 (doctor full: 14 new checks, exit codes 0/1/2/3, --report/--fix, env --raw, logs CLI, status --json degradation hints, supervisor read-only log/manifest helpers). Architecture, test-contract and redaction quality are good with change-proving tests, but blocking findings must be addressed before merge.
Blocking (MAJOR)
backend/src/modulo/launcher/doctor.py—default_probesnever wiresport_owner_description, socheck_port_collisionsreturns a positive "no compose/system PG or Redis service owns ..." silent pass over a stub probe (claims an audit that never ran; violates the module's "honest skips, never silent passes" contract).backend/src/modulo/launcher/doctor.py—last_backup_atprobe stub returns None unconditionally althoughrun_doctorloads state.json andmodulo backupstampslast_backup_at(cli/backup.py:736);check_stale_backuptherefore can never fire on a real machine and prints a false "schema v1 does not record one yet". Wirestate.last_backup_at(ISO → epoch) into the probe.backend/src/modulo/cli/main.py—logs --rotatenever verifies the launcher is stopped, whilesupervisor.rotate_log's docstring requires the caller to guarantee no appending fd is held; rotating launcher.log under a live attached launcher redirects post-rename writes into the rotated-away inode. Add a launcher-running guard or warning.
Non-blocking (MINOR) — address when convenient
doctor.py::_state_problem_kindfalls through to 'corrupt' when state.json is missing while secrets.json is present ("no state.json in ..." matches no pattern), printing misleading "state.json is CORRUPT (torn write)" language and exit 1.doctor.py::_host_port_from_database_urlaccessesparsed.portoutside its try; a malformed ambient DATABASE_URL (e.g. port 99999) raises "Port out of range 0-65535", surfacing as "settings-source: check crashed" FAIL/exit 1 instead of the docstring-promised('unknown', None)honest skip (verified reproducible).doctor.py::check_tls_expirydefault probe always yields "no TLS keypair" skip; message would be false once a keypair feature exists (honest-guard style message preferred)._component_stateignores its port arg.cli/main.py— with--reportset, all doctor output goes into the capture sink and the terminal only shows "diagnostic report written: ..." plus the exit code; consider teeing emits to stdout so the operator still sees the check table.doctor_report.py— redaction seeds exact-match scrub from raw credential values, so percent-encoded appearances inside URLs are not scrubbed;build_reportre-callslog_paths()twice per member.
Please fix the three MAJOR findings (and ideally the MINORs) and re-request review.
Head SHA: ade5fbc175f6a2eb6438552e91088f16230d0816
Fix: doctor probe wiring + logs-rotate guard (addresses CHANGES_REQUESTED)Commit MAJOR 1 — silent port-collision pass. MAJOR 2 — stale-backup never fires. MAJOR 3 — unsafe MINORs fixed: Change-proving unit tests added for each fix. All backend lint/format/bandit/semgrep/import-linter hooks pass and the doctor + cli test suites are green locally. |
…d (FAR-676)
Address modulo-reviewbot CHANGES_REQUESTED findings on the doctor full suite:
- default_probes never wired port_owner_description / last_backup_at /
tls_expiry, so check_port_collisions, check_stale_backup and check_tls_expiry
ran as silent-pass stubs. Wire real probes: port_owner_description reads the
kernel socket table and attributes non-loopback (foreign/compose) listeners;
last_backup_at converts state.last_backup_at (ISO) to epoch; tls_expiry parses
the data-dir tls/ cert notAfter via cryptography.
- logs --rotate now refuses while the launcher is running (rotate_log requires
no appending fd; otherwise post-rename writes land in the rotated-away inode).
- _state_problem_kind now classifies a missing state.json (secrets present) as
'missing' (uninitialized) instead of the misleading 'corrupt' language; it
distinguishes "No such file" from a torn/garbage file.
- _host_port_from_database_url degrades an out-of-range ambient DATABASE_URL port
(e.g. 99999) to an honest ('unknown', None) skip instead of crashing the
settings-source check.
- doctor --report now tees the doctor output to stdout so the operator still
sees the check table, and doctor_report seeds percent-encoded credential values
into the redaction map so credentials embedded in URLs are scrubbed.
Co-Authored-By: Claude <noreply@anthropic.com>
Branch Fixer: addressed modulo-reviewbot CHANGES_REQUESTED (FAR-676 doctor)Commit MAJOR
MINOR
Note on a concurrent fix: a prior run pushed Re-requesting review. |
farnalabs
left a comment
There was a problem hiding this comment.
CI failed on head 1fa73ec: the 'Test (Backend)' check run concluded 'failure' while all other checks passed (mergeable=true). Please investigate the failing backend test suite (see https://github.com/farnalabs/modulo/actions for the run on this commit) and push a fix; a fresh review will follow the fix push.
modulo-reviewbot
left a comment
There was a problem hiding this comment.
Changes requested for PR #349 (head SHA 1fa73ec).
Blocking findings:
-
CI: the
Test (Backend)check failed on head SHA 1fa73ec - failing conclusionfailure. Re-review is required after the failing backend tests are fixed. -
High-risk: the diff is mixed (production code under backend/src/modulo/cli and launcher) and added @pytest.mark.skipif markers (skip/xfail additions), which trigger the test-coverage-defeated rule. The failing backend test must be fixed before merge.
Please fix the failing backend test and re-push; the change must be re-reviewed before merge.
…ipped (FAR-676 MINOR 3) check_tls_expiry's 'no TLS keypair ... expiry check skipped' is an honest skip rather than a silent pass: document that modulo does not ship a keypair generator yet, and wire tls_expiry to surface a real near-expiry WARNING once one lands. Adds a test asserting the skip explains the missing generator.
farnalabs
left a comment
There was a problem hiding this comment.
Feedback review (FAR-676 re-review of head 183158a). CI green; the prior CR (Test Backend failure) is resolved. Two remaining defects worth fixing: (1) modulo logs <component> --rotate rotates postgres/redis logs while printing 'rotation applies to the app log only', with no live-holder guard for child logs; (2) check_bundle_versions' installed_bundle_pg_version axis is never written in production, so the downgrade/upgrade detection is a no-op.
| err=True, | ||
| ) | ||
| raise SystemExit(1) | ||
| rotated = rotate_log(path) |
There was a problem hiding this comment.
Bug: rotated = rotate_log(path) is OUTSIDE the else-branch, so modulo logs postgres --rotate (or redis) prints 'rotation applies to the app log only' and then rotates postgres.log/redis.log anyway. This contradicts the --rotate help text ('INSTEAD of reading it (app only)') and the safety design: the live-launcher guard only protects launcher.log, yet the bundled postgres/redis children write-append to their logs while running — rotating those redirects writes into the rotated-away inode (the exact hazard the comment describes). Move rotate_log + reporting into the else-branch (or guard child rotations by liveness too), and add a test that logs postgres --rotate does NOT rotate the child log.
| def __probe_installed_bundle_pg_version() -> str | None: | ||
| from modulo.launcher.supervisor import RUNTIME_FILENAME, _read_manifest_fields | ||
|
|
||
| extra = _read_manifest_fields(data_dir / RUNTIME_FILENAME).get("extra") | ||
| if not isinstance(extra, dict): | ||
| return None | ||
| version = extra.get("installed_bundle_pg_version") |
There was a problem hiding this comment.
Dead-in-production wiring: nothing writes extra['installed_bundle_pg_version'] into the runtime manifest (grep shows only this reader and the supervisor's extra={'degraded_reason'} write). Worse, supervisor._record_runtime_locked writes extra=None on every non-degraded call, atomically replacing the manifest and wiping any earlier bookkeeping — so check 15's downgrade-refusal/upgrade-hint axis can never fire outside of tests. Either persist the resolved bundled PG version at boot via write_runtime_manifest(..., extra={..., 'installed_bundle_pg_version': v}) or drop this axis until FAR-674.
| suffix = ".exe" if sys.platform == "win32" else "" | ||
| return [bin_dir / f"{name}{suffix}" for name in names if (bin_dir / f"{name}{suffix}").is_file()] | ||
|
|
||
| def __probe_cloud_sync_hit(root: Path) -> str | None: |
There was a problem hiding this comment.
Minor duplication/inconsistency: backend/src/modulo/cli/backup.py:96 defines _CLOUD_SYNC_VENDOR_ROOTS with COMPONENT-EQUALITY matching and documents why substring matching false-positives (e.g. 'dropbox' inside ordinary names like 'dropbox-migrations-backup-tmp'), yet __probe_cloud_sync_hit re-implements vendor detection with substring matching. The module docstring even claims it is 'the same walk-up + vendor set component matching shape the backup path uses'. Reuse/extract the backup helper or match components only.
Branch Fixer: no fix needed — branch already greenThe Branch Fixer was dispatched by the modulo-reviewbot
Current head Re-review on the current head is clear to proceed. |
modulo-reviewbot
left a comment
There was a problem hiding this comment.
Post-decision review (head 183158a). Verdict: CHANGES_REQUESTED.
BLOCKING FINDINGS:
-
backend/src/modulo/cli/main.py:476 -
--rotatebug: rotate_log(path) sits outside the app-only else-branch, somodulo logs postgres --rotate/redis prints "rotation applies to the app log only" then rotates the child log anyway, with no live-process guard (child logs are write-append by the running bundled postgres/redis; post-rename writes land in the rotated-away inode). Fix: gate rotation inside the else-branch or guard child rotation by liveness; add a test assertinglogs postgres --rotatedoes NOT rotate. -
backend/src/modulo/launcher/doctor.py:1088-1094 - check_bundle_versions installed_bundle_pg_version axis never fires in production: no code writes extra["installed_bundle_pg_version"] into the runtime manifest, and supervisor._record_runtime_locked writes extra=None on every non-degraded call, atomically replacing the manifest and wiping any earlier bookkeeping. Persist the resolved bundled PG version at boot or drop this axis until FAR-674.
MINOR (non-blocking):
- backend/src/modulo/launcher/doctor.py:1105 (__probe_cloud_sync_hit) duplicates backup.py:96 _CLOUD_SYNC_VENDOR_ROOTS with weaker substring matching (false-positive-prone, contradicts its own docstring). Reuse/extract the backup helper.
Everything else (redacted --report zip, exit-code contract, state-integrity classification, honest POSIX skips, prove-the-fix tests) is sound. Please address the two blocking findings and re-request review.
Branch Fixer — Test (Backend) failure resolved (no code change required from this run)The review flagged that Test (Backend) failed on head
Both assert an IP-string list while A parallel Branch Fixer Bot then pushed Latest CI (run 34516550651, head Note: the review's |
…eview CR) Addresses the two blocking findings from the modulo-reviewbot post-decision CHANGES_REQUESTED on head 183158a: 1. cli/main.py `logs --rotate`: the rotate_log() call sat outside the app-only guard, so `logs postgres --rotate`/`logs redis --rotate` printed "rotation applies to the app log only" yet rotated the child log anyway (no live-process guard — post-rename writes land in the rotated-away inode). Moved the rotate call inside the app-only else-branch so child components print the message and return without rotating. 2. supervisor.py `_record_runtime_locked`: nothing ever persisted `installed_bundle_pg_version` into the runtime manifest (extra=None wiped it on every write), so doctor's check_bundle_versions downgrade/upgrade axis was a no-op in production. Resolve the bundled postgres version once at boot and merge it into the manifest's extra on every write (alongside degraded_reason), so the axis now fires against the last-run cluster version. Adds prove-the-fix tests: child rotation is never executed, and the manifest persists/retains installed_bundle_pg_version across writes.
Branch Fixer: resolved FAR-676 post-decision CHANGES_REQUESTED (2 blocking findings)Commit Blocking 1 — Blocking 2 — Tests added (prove-the-fix):
All cross-platform pre-commit hooks pass (ruff, ruff-format, bandit, semgrep, gitleaks, import-linter) and the changed test files + doctor/launcher suites are green. The non-blocking MINOR about |
# Conflicts: # backend/src/modulo/cli/main.py # backend/src/modulo/launcher/supervisor.py # backend/tests/unit/cli/test_main_group.py
Merge-conflict resolution (Branch Fixer)The branch was CONFLICTING/DIRTY with Conflicts resolved (3 files)
VerificationThe reviewbot CR fixes from Locally green: Commit: |
farnalabs
left a comment
There was a problem hiding this comment.
CI feedback (automated review, feedback only - formal decision posted separately): 3 checks are failing on head 084a41a: 'Break-glass deploy gate' (failure), 'Test (Backend)' (failure), 'BDD (full suite)' (failure). Please fix the failing CI before re-review; the PR is otherwise mergeable with no conflicts.
modulo-reviewbot
left a comment
There was a problem hiding this comment.
Review decision: Changes requested
Merge is blocked on head SHA 084a41a until the following are resolved.
Blocking — CI is red (from review node):
- CI
Break-glass deploy gatefailed on head084a41a - CI
Test (Backend)failed on head084a41a - CI
BDD (full suite)failed on head084a41a
The PR is mergeable (no conflicts) but must be re-run and go green before merge.
Additional risk (policy-router): the diff is high-risk (mixed production + tests). Rule (c) fired: added @pytest.mark.skipif markers (e.g. sys.platform=="win32" skips and a Settings-env skipif in test_supervisor.py/test_upgrade.py) defeat CI coverage; no path-glob match on main's registry and no registry-file change. Please remove/justify the skips so CI actually exercises these paths.
No secret or token material is included in this review.
Migration 0207_collection_install_tracking already adds the collection_install_id column to schemas/agents/pipelines idempotently (ADD COLUMN IF NOT EXISTS). Migration 0209 then attempted a plain ADD COLUMN of the same column, which crashes on a fresh DB with 'column already exists'. 0209 now only creates the index the ORM declares (which 0207 does not), and its downgrade drops only that index. Fixes the CI Fast Validation + BDD/E2E break-glass/trigger-streak failures on deliver/FAR-676 (PR #349).
Automated Branch Fix — duplicate
|
…olumn The 0209 summary claimed 0207 does not add the collection_install_id column; in fact 0207 adds it idempotently and 0209 only adds the index. Align the docstring with the actual behaviour fixed in the prior commit.
farnalabs
left a comment
There was a problem hiding this comment.
CI is failing on head commit 1593ef0. Failing checks: Lint (Backend) — failure; Test (Backend) — failure; BDD (full suite) — cancelled. Please fix these before re-review. Full code review skipped because CI is red; re-dispatch after fixes.
farnalabs
left a comment
There was a problem hiding this comment.
Feedback from the automated review pass (CI green, mergeable; feedback only — formal decision posted downstream).
MAJOR — check 15 bundle-version false-fail on real machines: the default probe __probe_data_dir_pg_version reads the actual PG_VERSION file, which initdb writes as MAJOR-ONLY ("16"), while bundle_pg_version parses "postgres (PostgreSQL) 16.4 (...)" to "16.4". Then _version_tuple("16") == (16,) != (16, 4) == _version_tuple("16.4"), so check_bundle_versions returns ok=False ("bundle-minor/major drift") and modulo doctor exits 1 on every healthy install. Unit tests mask this because both sides are injected as "16.4". Fix: compare on a common prefix (or compare major-only vs the bundle's major, treating the data file as major-only), and add a probe-level test that feeds the REAL PG_VERSION content ("16"), not an injected "16.4".
Minor notes:
- cli/main.py logs --follow: each tick does path.read_bytes()[offset:], re-reading the whole log every 0.5s; seek from the offset instead.
- cli/main.py env --raw: deliberate credential echo with a stderr warning — acceptable, but consider requiring an explicit confirmation env var since it bypasses _scrub_for_launcher_command's redaction contract.
- doctor.py _host_port_from_database_url: a non-loopback DATABASE_URL with no explicit port is assumed 5432 and compared against state.json's port — can yield a spurious settings-source WARNING for remote DBs on other hosts; prefer leaving port None when absent.
- supervisor.py _component_state/_component_remediation: _pid_alive is recomputed per component (one /proc scan each); pass the liveness down instead.
- doctor_report.py: the fallback _SENSITIVE_FIELD_TOKENS/_NAMES set duplicates cli/main.py's fallback lists — fine for import-independence, but extract one shared fallback constant if it grows.
Good: 0209 correctly stopped re-adding the entity column 0207 owns (the fix push since the last review); orphan cleanup reuses the supervisor's reconcile_orphans with a live-postgres refusal; the report pipeline reuses the canonical sensitive-field classifier and scrubs percent-encoded credential appearances. Backend unit tests: 1098 passed, 6 skipped (all pre-existing Windows-platform skipif patterns, none unconditional).
modulo-reviewbot
left a comment
There was a problem hiding this comment.
CHANGES_REQUESTED: CI is failing on head commit 1593ef0. Blocking findings:
- CI: Lint (Backend) - failure
- CI: Test (Backend) - failure
- CI: BDD (full suite) - cancelled; full code review skipped while CI is red
High-risk flag was set (policy-router: high-risk, mixed diff incl. production files under backend/src/modulo/launcher/, cli/main.py, migration 0209; test-coverage-defeated rule fired). Resolve the failing CI checks before merge.
modulo-reviewbot
left a comment
There was a problem hiding this comment.
Formal review: CHANGES_REQUESTED (post-decision node, PR #349)
Head SHA: 9c3b435ebf8d7c6aa8e17904b85fa6ca58a851ec
The pipeline's post-decision node received CHANGES_REQUESTED from the policy router (high-risk) and the review node. CI is green and mergeable (1098 backend tests pass; the 6 skips are pre-existing Windows-only skipif patterns, and the 0209/0207 migration column-ownership conflict was correctly resolved). However, one major correctness finding blocks merge.
Blocking (MAJOR)
backend/src/modulo/launcher/doctor.py— check 15 (bundle-version) false-fails on real machines. The data-dirPG_VERSIONis major-only ("16") but the bundle probe parses"16.4", so_version_tuple("16") == (16,) != (16, 4)marks a healthy install unhealthy and exits 1. The unit tests mask this by injecting matching versions ("16.4"on both sides). Fix: compare a common version prefix / treat the data file as major-only, and add a probe-level test feeding the realPG_VERSIONcontent.
Non-blocking (minor)
cli/main.py:logs --followre-reads the whole log file each 0.5s tick (path.read_bytes()[offset:]) — seek from the offset instead.env --rawdeliberately echoes credentials with only a stderr warning — consider a stronger confirmation gate.launcher/supervisor.py:_component_state/_component_remediationrecompute_pid_aliveper component; log rotation correctly refuses while the launcher holds the log.launcher/doctor_report.py: REDACTED-by-construction report zip is good; fallback token/frozenset sets duplicatecli/main.py's fallback lists (extract a shared constant if it grows).- Ambient non-loopback
DATABASE_URLwithout a port is assumed:5432, which can produce a spurious settings-source warning (_host_port_from_database_url).
Please address the MAJOR finding and re-request review.
Check 15 (bundle-version) compared the data-dir PG_VERSION tuple against the
bundled binary version tuple exactly. initdb writes PG_VERSION as MAJOR-ONLY
("16"), while the bundled binary reports "16.4", so _version_tuple("16")
!= _version_tuple("16.4") and exited 1 on every healthy
install. Compare on the shared prefix so a major-only data file still matches
its bundled binary, while a genuine major/minor mismatch still fails.
Adds test_bundle_version_pass_major_only_pg_version which feeds the REAL
PG_VERSION content ("16") instead of an injected "16.4" that masked the bug.
Addresses reviewer MAJOR on PR #349.
Branch Fixer — check 15 bundle-version false-fail (reviewer MAJOR)Commit: Root cause
FixIn Verification
|
…in test_supervisor
Automated CI fix (Branch Fixer)This commit resolves the two failing CI checks reported on head
Scope is limited to the two CI-flagged lines; no tests or product docs were deleted. All cross-platform pre-commit hooks (ruff, ruff-format, bandit, semgrep, gitleaks, import-linter, etc.) pass on the change. Commit: Note: the sandbox's global pre-commit hook was broken for this environment — it passed |
farnalabs
left a comment
There was a problem hiding this comment.
Merge conflicts: this PR currently does not merge cleanly into main (mergeable=false, mergeable_state=dirty). Conflict file: backend/src/modulo/db/migrations/versions/0209_collection_install_id_entity_columns.py (content conflict — both sides modified the migration; likely the down_revision/revision chain). Please merge origin/main (or rebase) and resolve, then push so CI re-runs and review can complete. Feedback only — formal decision is issued separately.
farnalabs
left a comment
There was a problem hiding this comment.
CHANGES_REQUESTED: PR has merge conflicts with main.
- Mergeability:
mergeable: false/mergeable_state: dirty— this branch no longer merges cleanly into the current main. - CI on current head 2a5bc6d has no failing checks (SonarCloud scan still in progress); the earlier head f119d06 had
Test (Backend): failureandBDD (full suite): cancelled— those checks now pass on the new head. - Action: merge
origin/mainintodeliver/FAR-676(or rebase onto main), resolve the conflicts, and push. A fresh review will follow once CI is green and the branch is mergeable.
modulo-reviewbot
left a comment
There was a problem hiding this comment.
CHANGES_REQUESTED — blocking findings:
-
Merge conflict with main (blocking)
File: backend/src/modulo/db/migrations/versions/0209_collection_install_id_entity_columns.py
Both sides modify this migration (likely the revision/down_revision chain). API reports mergeable=false / mergeable_state=dirty; local git merge-tree confirms a content conflict. Resolve by rebasing/merging origin/main and fixing the 0209 migration chain, then push to re-trigger CI. -
CI pending (blocking for this review)
CI on head 2a5bc6d is pending (parallel check still running). Full code review is deferred until the rebase lands and CI completes.
Head SHA: 2a5bc6d
Run-level decision: review node + policy-router both CHANGES_REQUESTED.
modulo-reviewbot
left a comment
There was a problem hiding this comment.
Formal review: CHANGES_REQUESTED for PR #349 (deliver/FAR-676, head f119d06).
Blocking findings from the review node:
- MERGE CONFLICTS (blocking): the branch has merge conflicts with main (mergeable=false, mergeable_state=dirty). Merge origin/main into deliver/FAR-676 (or rebase onto main), resolve the conflicts, and push. A re-review will follow once the branch is mergeable and CI is green.
- CI: no failing checks on the current head 2a5bc6d (SonarCloud scan still in progress). The prior head f119d06 failures ("Test (Backend): failure", "BDD (full suite): cancelled") no longer reproduce on the new head.
Pipeline note: this path was flagged HIGH-RISK (mixed diff with @pytest.mark.skipif additions defeating the CI test-coverage gate); the HITL gate was condition-skipped, so no human approval was granted. Verdict stands as CHANGES_REQUESTED.
# Conflicts: # backend/src/modulo/db/migrations/versions/0209_collection_install_id_entity_columns.py
Automated merge-conflict fix (Branch Fixer)Root cause: This branch and Fix: Merged Verification:
Fix commit: |
# Conflicts: # backend/src/modulo/db/migrations/versions/0209_collection_install_id_entity_columns.py
Branch Fixer: resolved migration collision / merge conflict with
|
farnalabs
left a comment
There was a problem hiding this comment.
Re-review feedback (head bea6f24, non-blocking — formal decision posted by post-decision): The merge-conflict CHANGES_REQUESTED from the prior review is resolved (mergeable=true). The FAR-676 diff reads well overall: probe-injected doctor checks, honest skips instead of silent passes, redaction-seeded report bundle, and the logs --rotate live-holder refusal are all correct. Minor findings: (1) cli/main.py env_cmd: '--raw --json' prints every credential and the stderr warning only fires when NOT as_json — emit the warning in raw JSON mode too. (2) supervisor.py _resolve_bundled_postgres_version duplicates doctor.py's __probe_bundle_pg_version 'postgres --version' parsing — consider one shared helper. (3) cli/main.py logs --follow: offset is captured after the initial read, so lines appended between the read and stat can be missed, and log rotation during follow (size < offset) is unhandled.
farnalabs
left a comment
There was a problem hiding this comment.
Re-review feedback (feedback only — formal decision posted by the downstream post-decision node). Note: the earlier CHANGES_REQUESTED (merge conflicts) is resolved — the branch is now mergeable with main.
BLOCKING (1):
- backend/src/modulo/cli/main.py — the
--reporttee sink drops newlines.run_doctoremits lines WITHOUT a trailing newline (its default_print_lineisprint(text), which supplies it), so_tee(capture.write(text); sys.stdout.write(text)) concatenates every line into one garbled line, both on the terminal and indoctor-output.txtinside the zip — verified by running the realrun_doctor(..., sink=_tee): the whole check table and the finalunhealthy: 9 failing check(s)...line are one single line, and the zip member has 0 newlines. The unit tests do not catch this because their mockedrun_doctorsinks pass text WITH"\n"(test_main_group.py:547) — the mock contract diverges from the real sink contract. Fix: append the newline in the sink (or have run_doctor pass it), and add a test that drives the REAL run_doctor through the tee (e.g. an integration-style round-trip) asserting captured output contains newlines.
NON-BLOCKING:
2. backend/src/modulo/launcher/supervisor.py:1400 _resolve_bundled_postgres_version duplicates the binary-resolution + postgres --version parse in doctor.py __probe_bundle_pg_version (same module pair already imports from each other) — consider a shared helper so the two cannot drift.
3. backend/src/modulo/cli/main.py env --raw prints every credential to stdout; the stderr warning is good, but consider requiring an explicit confirmation flag or TTY check so piped captures cannot silently hoover the credentials.
4. backend/src/modulo/launcher/doctor.py — when state is unavailable, the same failed checks (ports/postgres/redis/migrations) are emitted twice (synthetic failed results + the real checks failing again), inflating "N failing check(s)" counts. Pre-existing on main, but worth fixing while FAR-676 touches this path.
CI on head bea6f24: all completed checks green; "Test (Backend)" still running at review time. LGTM otherwise — the exit-code/degraded/doctor report work is well structured and heavily tested.
modulo-reviewbot
left a comment
There was a problem hiding this comment.
APPROVE — PR #349 (deliver/FAR-676 @ bea6f24)
Formal post-decision review. Verdict: APPROVE (review node, low-risk path, no HITL gate).
Pipeline decision
- Policy-router:
APPROVE,is_high_risk: false(changed files match no high-risk-path globs; no test deletions, no skip/xfail additions). - Review node:
APPROVE— CI green on head (zero failing completed checks), PR mergeable, prior merge-conflict-only CHANGES_REQUESTED resolved via merge commits.
What was reviewed (three-dot FAR-676 diff)
doctorfull checks,status --jsonenrichment,logscommand,--reportredacted zip,--fixorphan cleanup, supervisor manifest extra bookkeeping.- No test deletions; skipif guards are pre-existing POSIX/Windows platform conditions; new tests map 1:1 to new behaviours.
- Secrets-probe and report-redaction paths verified directly (report.json contains only names/sizes, no contents; redaction-seeded + structural KEY=value passes reuse the canonical
is_sensitive_env_keyclassifier). - Prior findings addressed: PG version prefix compare, 0209 column ownership, ruff-format migration, empty-dict assertion.
Non-blocking feedback carried through (posted to review node, not blocking)
cli/main.pyenv --raw --jsonprints credentials without the stderr warning that only fires whenas_json is False;logs --followhas an offset race and does not handle rotation/shrink mid-follow.supervisor.py_resolve_bundled_postgres_versionduplicatesdoctor.py's__probe_bundle_pg_versionparsing — consider a shared helper.
None of these block merge; the diff is safe to land.
modulo-reviewbot
left a comment
There was a problem hiding this comment.
Review decision: CHANGES_REQUESTED
The policy-router and review node both produced CHANGES_REQUESTED for PR #349 (head SHA 74f2ca7). The diff is flagged HIGH-RISK: it mixes 4 non-test files (cli/main.py, launcher/doctor.py, launcher/doctor_report.py, launcher/supervisor.py) with 7 added @pytest.mark.skipif markers, which voids the test-only exemption for the CI gate.
BLOCKING — doctor --report tee sink drops newlines
backend/src/modulo/cli/main.py: the --report tee sink drops newlines. run_doctor emits lines without trailing \n (default _print_line uses print()), so terminal output and the zip's doctor-output.txt are one garbled line (proven by driving the real run_doctor through the sink; zip member had 0 newlines). The unit tests mask this: their fake run_doctor sinks pass text WITH \n (test_main_group.py:547), diverging from the real sink contract. Fix the newline handling and add a real-code round-trip test.
Non-blocking follow-ups
backend/src/modulo/launcher/supervisor.py:1400—_resolve_bundled_postgres_versionduplicates the binary-resolution +postgres --versionparse ofdoctor.py __probe_bundle_pg_version; doctor already imports from supervisor. Extract a shared helper so the two cannot drift.backend/src/modulo/cli/main.py—env --rawprints every credential to stdout; the stderr warning is good but a piped capture can silently hoover credentials. Consider a confirmation flag or TTY check.backend/src/modulo/launcher/doctor.py— when state.json is unavailable, ports/postgres/redis/migrations results are emitted twice (synthetic failures + real checks failing again), inflating the "N failing check(s)" count — pre-existing on main, worth fixing while touching this path.
Skipif additions reviewed: all added @pytest.mark.skipif are win32 platform-gates for POSIX-only /proc seams (honest-skip pattern), no test deletions, no coverage removal.
Required before merge: fix the BLOCKING newline issue and add the round-trip test.
Branch Fixer: no fix needed — branch already greenThe Branch Fixer was triggered by the CI failure in run Both were already resolved on this branch by commit No additional fix was pushed. Re-running CI on |
Branch Fixer: SonarCloud quality-gate (new-code coverage)The only failing CI signal on this branch was the SonarCloud Code Analysis check — Root cause: the FAR-676 feature added ~1.5k lines of new code (the full doctor, Fix: added three unit-test files that exercise the new code paths:
Result: All cross-platform pre-commit hooks pass (ruff, ruff-format, bandit, gitleaks). New coverage should now clear the 80% gate once the SonarCloud scan re-runs on this push. |
farnalabs
left a comment
There was a problem hiding this comment.
CI is failing: the "Test (Backend)" check on head a145aa7 concludes with failure. Please fix the failing backend tests (or rebase/merge origin/main if the failure comes from main) and push again so this PR can be re-reviewed. Mergeability is fine (not conflicted), so only the CI failure blocks.
modulo-reviewbot
left a comment
There was a problem hiding this comment.
Blocking findings (formal review — CHANGES_REQUESTED)
Pipeline review of PR #349 @ a145aa7d9a3ff707ba4dbb25a0284045454e9d42 requests changes before merge.
1. Failing CI check "Test (Backend)" — The backend test suite concluded with failure on head a145aa7d9a3ff707ba4dbb25a0284045454e9d42. Fix the failing tests or merge origin/main, then push so the PR can be re-reviewed.
2. High-risk: diff defeats the CI gate (policy-router) — The diff is NOT test-only: it touches production paths backend/src/modulo/cli/main.py, launcher/doctor.py, launcher/doctor_report.py, launcher/supervisor.py, and it adds @pytest.mark.skipif markers on tests (e.g. POSIX-only /proc probes and the env-gated E2E snapshot test), which defeats the CI gate. The test-only exemption therefore does not apply; the production-path changes need to be re-worked and the tests restored rather than skipped.
Please address both items and push, then re-request review.
…ests Replace naive datetime.now() with timezone-aware datetime.now(UTC) and rewrite empty-container/string/tuple equality assertions to truthiness checks so the architecture quality gates pass.
Automated Branch Fix — CI architecture quality gatesThe backend architecture test run failed on 4 test-suite-quality gates, all triggered by the auto-generated launcher coverage tests ( and ):
Fix (commit |
farnalabs
left a comment
There was a problem hiding this comment.
Feedback review (formal decision posted separately by post-decision). Findings:
- Minor: bundled-postgres version parsing is duplicated — supervisor.py _resolve_bundled_postgres_version and doctor.py __probe_bundle_pg_version both reimplement the same 'postgres --version' token-scan loop (backend/src/modulo/launcher/supervisor.py:1398, backend/src/modulo/launcher/doctor.py:~1070). Consider one shared helper in entry.py so the two can never disagree (the whole PR's thesis is 'doctor and boot must never disagree').
- Minor (security, intentional?): 'modulo env --raw' prints every credential to stdout with the warning only on stderr. In scripted/piped use stderr is often dropped — consider printing the warning also as a first line of stdout, or requiring an env-var confirmation flag. If this is a documented operator escape hatch, fine.
- Minor: doctor_report._redact_kv_lines joins with '\n', dropping any trailing newline of the source text — cosmetic only.
- Nit: _state_problem_kind classifies by substring-matching error strings (backend/src/modulo/launcher/doctor.py). Brittle but honest; a structured error code from state.py would be more robust long-term.
Everything else reviewed positively: exit-code table is documented and locked by tests, corrupt-vs-HMAC-mismatch reporting is distinct, --fix refuses a live postgres, logs --rotate refuses while the launcher holds the file, report zip is credential-free by construction. Approving-quality on the code side; verdict carried in the structured output.
modulo-reviewbot
left a comment
There was a problem hiding this comment.
Decision: APPROVE — PR #349 (deliver/FAR-676)
Re-reviewed head SHA 9b8364ebaf271b19bd8b5380249f204bb8c91718 (fix push after the prior CHANGES_REQUESTED). Backend-only diff across cli/main.py, launcher/doctor.py, doctor_report.py, supervisor.py plus their coverage tests. CI all completed checks green and the PR is mergeable.
Key review-node findings:
- Exit-code contract is documented and locked by the exhaustiveness test;
--fixsafely refuses a live postgres; the--reportzip redaction is sound (seeded exact values + structural classifier reuse). - Non-blocking: duplicated postgres version-parsing between supervisor.py and doctor.py probes — extract one shared helper;
env --rawprints credentials with its warning only on stderr;_state_problem_kindclassifies via brittle error-string matching; cosmetic trailing-newline drop in_redact_kv_lines.
Note: the policy-router marked this diff high-risk (rule c — the diff adds @pytest.mark.skipif markers, which per policy defeat the CI gate; no high-risk-path glob matched). Decision remains APPROVE.
|



Implements FAR-676 (single-install epic, ADR 031). Extends the doctor's 5 core checks to the full set over the injected probe interface: secrets perms, ambient PG* env warning, MODULO_DB check + env-vs-state conflict detection (--accept-env), cloud-sync warning, service identity/linger (graceful when not installed), memory headroom, PG_VERSION vs bundle + bundle-minor drift, AV-block detection, compose/system-service coexistence with port-collision attribution, second-install + PATH-shadowing detection, state.json HMAC integrity, degraded flag, TLS near-expiry, stale-backup warning. Adds: documented exit-code table with exhaustiveness test, modulo status --json (per-component + degraded reason + remediation hints), modulo logs [-f] + child logs, doctor --report redacted zip (redaction map seeded with real generated credentials; CI test asserts no secret survives), doctor --fix (orphan cleanup + port re-assignment), modulo env --raw. Also: platform/env guards for 5 POSIX-only/live-DB tests that failed on Windows checkouts (pre-existing from #309/#319). QA-gated: 3 architecture test-style violations fixed. ruff/mypy clean.