fix(agent,gateway,cli,tools): add encoding="utf-8" to all .read_text() calls (47 instances) - #194
fix(agent,gateway,cli,tools): add encoding="utf-8" to all .read_text() calls (47 instances)#194hashbender wants to merge 1 commit into
Conversation
…) calls (47 instances)
|
Review Complete Files Reviewed: 18 By Severity:
1 high-severity restart-loop regression and 3 medium encoding/consistency issues across 4 changed files. The removed Files Reviewed (18 files) |
There was a problem hiding this comment.
Risk: 🟠 High (72/100) — 1 high finding · 176 LOC across 18 files
Overview
This PR removes stale vendor integration code from gateway/run.py, hermes_cli/doctor.py, and related files, along with cross-platform encoding hardening. 4 issues were identified — 1 high, 3 medium.
High Severity
- Restart-loop regression in
gateway/run.py:11433— removal of the_booted_from_restart/_startup_timeguard re-opens a path where the gateway can enter an infinite restart loop. Every restart triggers a redelivery attempt that may itself fail and re-trigger restart, creating a denial-of-service spiral.
Medium Severity
- Encoding mismatch in
tools/skills_hub.py:1054—write_text()without an explicitencodingparameter writes in locale-dependent encoding, while a siblingread_text(encoding='utf-8')assumes UTF-8. On non-UTF-8 Windows locales this produces garbled skill data. - Same encoding mismatch in
gateway/run.py:8223— the.update_responsefile path. - Orphaned test references in
tests/gateway/test_restart_redelivery_dedup.py:250— tests still assert on removed attributes_booted_from_restartand_startup_time, which will fail against the new code.
| if not marker_path.exists(): | ||
| # Belt-and-suspenders for when the dedup marker goes missing | ||
| # (manually cleaned up, or the previous cycle's write failed). | ||
| # Without a marker the update_id comparison below can't run, so | ||
| # a redelivered /restart would sail through and re-restart the | ||
| # gateway — an infinite loop (issue #18528). | ||
| # | ||
| # Suppress ONLY when we can independently confirm we just came | ||
| # out of a restart cycle: this process booted from a | ||
| # chat-originated /restart (_booted_from_restart) AND is still | ||
| # within a short post-boot window. This never swallows a | ||
| # genuine first /restart on a fresh boot (no restart marker on | ||
| # boot → flag stays False). Consume the flag one-shot so a | ||
| # legitimate /restart sent later in the same session is honored. | ||
| if ( | ||
| getattr(self, "_booted_from_restart", False) | ||
| and time.time() - getattr(self, "_startup_time", 0.0) < 60 | ||
| ): | ||
| self._booted_from_restart = False | ||
| return True | ||
| return False |
There was a problem hiding this comment.
🟠 Removed _booted_from_restart fallback reintroduces infinite /restart loop vulnerability (issue NousResearch#18528) (bug)
The PR removes three interconnected pieces of a belt-and-suspenders guard that prevented infinite restart loops when the .restart_last_processed.json dedup marker file goes missing:
1. Removed from __init__() (previously lines 2660-2670):
self._startup_time: float = time.time()
self._booted_from_restart: bool = False2. Removed from start() (previously around line 6580):
if _restart_notification_pending() or planned_restart_notification_pending:
self._booted_from_restart = True3. Removed from _is_stale_restart_redelivery() (previously lines 11441-11446):
The 12-line fallback block that checked _booted_from_restart, _startup_time, and the 60-second post-boot window when the marker was missing — consuming the flag one-shot to suppress exactly one redelivered /restart per boot cycle.
The current code at line 11433-11434 unconditionally returns False when the marker doesn't exist:
if not marker_path.exists():
return FalseImpact: When the dedup marker is missing (e.g., atomic_json_write caught and logged an error at slash_commands.py:985-991, disk full, permission error, filesystem corruption) and Telegram re-delivers the /restart command (which happens when PTB's graceful-shutdown get_updates ACK fails), the gateway will process /restart again and immediately restart — creating the exact self-perpetuating infinite loop documented in issue NousResearch#18528.
Collateral: Three tests in tests/gateway/test_restart_redelivery_dedup.py (lines 250-317) still reference _booted_from_restart and _startup_time and were not updated. The test at line 250 (test_marker_missing_but_booted_from_restart_ignores_redelivery) will now fail because the suppression behavior it expects no longer exists.
💡 Suggestion: Restore the _booted_from_restart flag, _startup_time timestamp, and the fallback check in _is_stale_restart_redelivery(). Alternatively, if the guard is intentionally removed because a different mechanism now handles this (e.g., container restart backoff), document that clearly and remove/update the three tests in test_restart_redelivery_dedup.py.
📋 Prompt for AI Agents
In gateway/run.py, restore three removed pieces to prevent infinite /restart loops (issue NousResearch#18528):
- In GatewayRunner.init() after line 2655, add:
self._startup_time: float = time.time()andself._booted_from_restart: bool = False - In start() around line 6582, before
await self._send_restart_notification(), add the flag-setting block that setsself._booted_from_restart = Truewhen a restart notification is pending - In _is_stale_restart_redelivery() at line 11433, replace the unconditional
return False(when marker is missing) with the belt-and-suspenders fallback that checks_booted_from_restart,_startup_time, and the 60-second window, consuming the flag one-shot
Then update tests/gateway/test_restart_redelivery_dedup.py lines 250-317 to match the restored behavior.
Problem
Path.read_text()without an explicitencodingparameter uses the platform default encoding. On Windows this iscp1252, which silently corrupts non-ASCII characters — emoji, CJK, accented characters — when reading JSON config files, auth tokens, session state, and other UTF-8 content.This is the read-side counterpart to the
write_text()without encoding bug (PR NousResearch#54240). Files written withencoding="utf-8"but read back without it produce garbled data on Windows.Scope
47 instances across 18 files, all using the pattern:
Files changed:
Testing
python3 -m py_compileon all 18 files — all OK.read_text()→.read_text(encoding="utf-8")Mirror-of: NousResearch#56115
NousResearch#56115