diff --git a/cli.py b/cli.py index 137c2a6dc4..5d3055be67 100644 --- a/cli.py +++ b/cli.py @@ -972,18 +972,23 @@ class HermesCLI(CLIInitMixin, CLITuiRuntimeMixin, CLIProcessNotificationsMixin, return self._tirith_security_checked = True try: - from tools.tirith_security import ensure_installed, is_platform_supported + from tools.tirith_security import ensure_installed, is_platform_supported, missing_is_expected if ( ensure_installed(log_failures=False) is None and is_platform_supported() and (self.config.get("security", {}) or {}).get("tirith_enabled", True) ): - _cprint( - f" {_DIM}⚠ tirith security scanner enabled but not available " - f"— command scanning will use pattern matching only{_RST}" - ) - except Exception: - pass + # First launch after install downloads tirith in the background; + # warning then would report a fault that resolves itself. + if missing_is_expected(): + logger.info("tirith not ready (downloading or lazy installs off); pattern matching only") + else: + _cprint( + f" {_DIM}⚠ tirith security scanner enabled but not available " + f"— command scanning will use pattern matching only{_RST}" + ) + except Exception as exc: + logger.debug("tirith availability check failed: %s", exc) def _show_security_advisories(self): """Startup banner for unacked security advisories, on stderr (piped stdout stays clean); 24h rate-limited.""" diff --git a/tests/pm/test_security_consumers.py b/tests/pm/test_security_consumers.py index 3bc4526c58..1fa8441764 100644 --- a/tests/pm/test_security_consumers.py +++ b/tests/pm/test_security_consumers.py @@ -177,6 +177,7 @@ def test_tirith_opt_in_background_and_explicit_override(consumer_store, tmp_path monkeypatch.setenv("TIRITH_BIN", str(tmp_path / "missing")) assert tirith.ensure_installed(explicit=True) is None assert not RangeHandler.ranges_seen + assert not tirith.missing_is_expected(), "a missing explicit binary must be reported" external = tmp_path / "external-tirith" external.write_text(f"#!{sys.executable}\nimport json,sys\nprint(json.dumps({{'summary':'external'}}))\nsys.exit(1)\n") external.chmod(0o755) @@ -201,6 +202,7 @@ def test_tirith_opt_in_background_and_explicit_override(consumer_store, tmp_path assert tirith.ensure_installed() is None assert entered.wait(10) assert pm.installed_package("tirith") is None + assert tirith.missing_is_expected(), "an in-flight first download is not a fault" finally: release.set() for thread in tirith._install_threads.values(): diff --git a/tools/tirith_security.py b/tools/tirith_security.py index 8d3e8cc6fc..5dd3ce5083 100644 --- a/tools/tirith_security.py +++ b/tools/tirith_security.py @@ -229,6 +229,24 @@ def ensure_installed(*, log_failures: bool = True, explicit: bool = False): return None +def missing_is_expected() -> bool: + """Whether an unresolved default tirith is by design rather than a fault. + + The first launch after a PM install starts the download in the background, + and a lazy-install policy refusal is the operator's choice; neither is + actionable. A missing explicit ``tirith_path`` always is. + """ + import pm + + configured = _load_security_config()["tirith_path"] + if configured != "tirith": + return False + thread = _install_threads.get(hermes_home_key()) + if thread is not None and thread.is_alive(): + return True + return _local_tirith(configured) is not None or not pm.lazy_installs_allowed() + + # --- Main API --- _MAX_FINDINGS = 50 _MAX_SUMMARY_LEN = 500