fix(security): don't warn while tirith's first download runs

The first CLI launch after a PM install starts tirith's download in the
background, so ensure_installed() returns None and the CLI printed
"tirith security scanner enabled but not available". The scanner was on
its way, not missing; the same warning also fired when lazy installs are
disabled by the operator's own policy.

missing_is_expected() tells those by-design states apart. The CLI logs
them and keeps the visible warning for a missing explicit tirith_path or
a finished download that failed.
This commit is contained in:
ethernet
2026-09-24 14:16:58 -04:00
parent cfeffe851d
commit df1b647b42
3 changed files with 32 additions and 7 deletions

19
cli.py
View File

@@ -972,18 +972,23 @@ class HermesCLI(CLIInitMixin, CLITuiRuntimeMixin, CLIProcessNotificationsMixin,
return return
self._tirith_security_checked = True self._tirith_security_checked = True
try: 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 ( if (
ensure_installed(log_failures=False) is None and is_platform_supported() ensure_installed(log_failures=False) is None and is_platform_supported()
and (self.config.get("security", {}) or {}).get("tirith_enabled", True) and (self.config.get("security", {}) or {}).get("tirith_enabled", True)
): ):
_cprint( # First launch after install downloads tirith in the background;
f" {_DIM}⚠ tirith security scanner enabled but not available " # warning then would report a fault that resolves itself.
f"— command scanning will use pattern matching only{_RST}" if missing_is_expected():
) logger.info("tirith not ready (downloading or lazy installs off); pattern matching only")
except Exception: else:
pass _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): def _show_security_advisories(self):
"""Startup banner for unacked security advisories, on stderr (piped stdout stays clean); 24h rate-limited.""" """Startup banner for unacked security advisories, on stderr (piped stdout stays clean); 24h rate-limited."""

View File

@@ -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")) monkeypatch.setenv("TIRITH_BIN", str(tmp_path / "missing"))
assert tirith.ensure_installed(explicit=True) is None assert tirith.ensure_installed(explicit=True) is None
assert not RangeHandler.ranges_seen assert not RangeHandler.ranges_seen
assert not tirith.missing_is_expected(), "a missing explicit binary must be reported"
external = tmp_path / "external-tirith" external = tmp_path / "external-tirith"
external.write_text(f"#!{sys.executable}\nimport json,sys\nprint(json.dumps({{'summary':'external'}}))\nsys.exit(1)\n") external.write_text(f"#!{sys.executable}\nimport json,sys\nprint(json.dumps({{'summary':'external'}}))\nsys.exit(1)\n")
external.chmod(0o755) 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 tirith.ensure_installed() is None
assert entered.wait(10) assert entered.wait(10)
assert pm.installed_package("tirith") is None assert pm.installed_package("tirith") is None
assert tirith.missing_is_expected(), "an in-flight first download is not a fault"
finally: finally:
release.set() release.set()
for thread in tirith._install_threads.values(): for thread in tirith._install_threads.values():

View File

@@ -229,6 +229,24 @@ def ensure_installed(*, log_failures: bool = True, explicit: bool = False):
return None 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 --- # --- Main API ---
_MAX_FINDINGS = 50 _MAX_FINDINGS = 50
_MAX_SUMMARY_LEN = 500 _MAX_SUMMARY_LEN = 500