Fix/tirith missing binary cache#23855
Closed
xxxigm wants to merge 4 commits into
Closed
Conversation
…y warnings Issue NousResearch#23845 reports that on Windows installs without the tirith binary on PATH, every single terminal command emits an identical "tirith spawn failed" WARNING. A session with 20+ terminal calls leaves 20 copies of the same line in the log, drowning out anything actionable. Introduce two thread-safe helpers plus a small reset hook: - `_log_unavailable_once(path_key, message)`: emits the warning the first time a given path key is seen, silent on every later call. - `_clear_unavailable_log(path_key)`: drops a key from the cache so a later regression (e.g. binary uninstalled mid-session) still surfaces one fresh warning instead of being silenced forever. - `_reset_unavailable_log()`: maintenance/test helper. Pure-additive commit — no existing call site updated yet (next commit).
…ousResearch#23845) When `tirith` is missing from PATH and no install path is configured, `subprocess.run` raises OSError on every terminal command and `check_command_security` logs an identical WARNING each time. On a Windows install without the Rust toolchain (or any host that removed the binary), a chat session with 20 terminal calls produces 20 copies of: WARNING tools.tirith_security: tirith spawn failed: [WinError 2] The system cannot find the file specified. The same anti-pattern applies to the `tirith path resolved to None` warning emitted when `_resolve_tirith_path` returns None (install attempt cached as failed). Route both warnings through `_log_unavailable_once` so each distinct path key logs once per process. On the success path, call `_clear_unavailable_log(tirith_path)` so a later regression (binary uninstalled mid-session) still surfaces one fresh warning rather than being silenced by an earlier suppression. No behavior change to verdicts: - fail_open still allows on spawn failure - fail_closed still blocks on spawn failure - timeouts / unknown exit codes still log every time (they're potentially transient, not the "binary missing" repeating pattern this issue is about) Refs NousResearch#23845.
…3845) Adds `TestUnavailableLogHelpers` with seven cases exercising the `_log_unavailable_once` / `_clear_unavailable_log` / `_reset_unavailable_log` contract introduced in the previous commit: - first call logs exactly one WARNING with the suppression-hint suffix - second/third calls with the same key are silent - distinct keys each log once - `_clear_unavailable_log` re-arms a key so it logs again on the next miss - `_clear_unavailable_log` of an unknown key is a no-op (does not raise) - `_reset_unavailable_log` wipes every key - thread-safety: 20 concurrent threads racing on the same key emit exactly one WARNING Setup/teardown calls `_reset_unavailable_log` so cases are order- independent regardless of how the existing 63-test suite runs first.
…spam Adds `TestSpawnFailureLogSpamSuppression` with seven cases that drive the original reproducer through `check_command_security` instead of the internal helpers: - 20 repeated FileNotFoundError fail-open scans → exactly ONE WARNING - 10 repeated PermissionError fail-open scans → exactly ONE WARNING - 5 repeated FileNotFoundError fail-closed scans → exactly ONE WARNING (verdict still `block` on every call) - success between two failure bursts → exactly TWO WARNINGs (the intermediate success re-arms the cache so a true regression still surfaces) - 15 repeated `_resolve_tirith_path` → None calls → exactly ONE "resolved to None" WARNING - two distinct missing-binary paths in the same session → ONE WARNING per path (two total) - timeouts are NOT deduplicated (potentially transient — not the persistent binary-missing pattern this issue is about) Every test still asserts the verdict (`allow` under fail_open, `block` under fail_closed) so we catch any regression that trades log spam for incorrect security behaviour. I verified the suite reproduces the bug by reverting the production commits on this branch: all 14 new cases ERROR out because the helpers don't exist on `upstream/main`, and the dedup invariants fail once the helpers are reintroduced without wiring them into `check_command_security`. Fixes NousResearch#23845.
This was referenced May 11, 2026
Merged
Contributor
|
Automated hermes-sweeper review: this tirith missing-binary log-spam fix is already implemented on current main. Evidence:
Thanks for the detailed repro and tests in the original PR; the bug class it targeted is now covered on main. |
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 does this PR do?
Stops
tirithfrom spamming an identical WARNING line on every single terminal command when the binary is missing. On a Windows install without the Rust toolchain (or any host that removed the binary),subprocess.run([tirith_path, ...])raisesOSErroron every scan, andcheck_command_securitycallslogger.warning("tirith spawn failed: ...")each time. A chat session with 20 terminal calls leaves 20 copies of:drowning the log and hiding real errors. The same pattern applies to the
"tirith path resolved to None"warning when_resolve_tirith_pathcaches a failed auto-install.The fix routes both warnings through a small
_log_unavailable_once(path_key, message)helper that dedupes by path. The first miss for a given path logs WARNING; subsequent misses are silent. Whensubprocess.runlater succeeds for that path, the cache entry is cleared via_clear_unavailable_logso a true regression (e.g. binary uninstalled mid-session) still surfaces one fresh warning instead of being silenced forever.No verdict semantics change —
fail_openstill allows,fail_closedstill blocks, every spawn is still attempted (we only deduplicate the log message). Timeouts and unexpected exit codes are intentionally NOT deduped: those are potentially transient and operators want to see them every time.Related Issue
Fixes #23845
Type of Change
Changes Made
Four logically separated commits:
fix(tools/tirith): add one-shot logging helpers for unavailable-binary warnings— adds_log_unavailable_once,_clear_unavailable_log,_reset_unavailable_logplus the_unavailable_paths_loggedset and dedicated_unavailable_log_lock. Pure-additive; no call sites changed.fix(tools/tirith): dedup spawn-failure warnings via one-shot helper (#23845)— rewires the two repeating WARNING sites incheck_command_security:OSErrorfromsubprocess.run(line ~648) now calls_log_unavailable_once(tirith_path, ...).tirith_path is None(line ~635) now calls_log_unavailable_once(f"<none>:{cfg['tirith_path']}", ...)._clear_unavailable_log(tirith_path)so a later regression re-arms one fresh warning.test(tools/tirith): cover the unavailable-log helpers (#23845)— 7 unit tests for the helpers: first-call logs, repeat is silent, distinct keys each log once, clear re-arms, clear of unknown key is a no-op, full reset wipes all keys, thread-safety under 20 racing threads → exactly one WARNING.test(tools/tirith): end-to-end regression for #23845 log spam— 7 integration tests drivingcheck_command_security:FileNotFoundErrorfail-open scans → exactly one WARNING (the original repro).PermissionErrorfail-open scans → exactly one WARNING.FileNotFoundErrorfail-closed scans → one WARNING (verdict stillblockon every call)._resolve_tirith_path → Nonecalls → one "resolved to None" WARNING.Files touched:
tools/tirith_security.py(+77, −3)tests/tools/test_tirith_security.py(+380, −0)How to Test
Check out the branch and ensure
.venvis set up:Run the new tests on their own:
Expected: 14 passed.
Run the full tirith suite to confirm no regression in the existing 63 tests:
Expected: 77 passed (63 pre-existing + 14 new).
Reproduce the bug on
mainto confirm the regression test catches it:Expected: 14 errors (
AttributeErroron missing helpers — proves the tests would catch the spam regression).Then restore the fix:
Checklist
Code
fix(tools/tirith): …,test(tools/tirith): …)tools/tirith_security.pyfor this issuescripts/run_tests.sh tests/tools/test_tirith_security.pyand all tests pass (77 passed)Documentation & Housekeeping
cli-config.yaml.exampleif I added/changed config keys — N/ACONTRIBUTING.mdorAGENTS.mdif I changed architecture or workflows — N/AScreenshots / Logs
Bug reproduction proof — with the production code reverted to
upstream/main, the new regression tests fail: