From 75e0d52034656a3f1584649fa0c2a12fbbbeb284 Mon Sep 17 00:00:00 2001 From: teknium1 <127238744+teknium1@users.noreply.github.com> Date: Fri, 24 Jul 2026 16:14:22 -0700 Subject: [PATCH] fix(windows): sweep remaining bare read_text/write_text sites + linter rule AST-driven pass over every Path.read_text()/write_text() without an explicit encoding= across non-test code: 71 sites in 34 files (skills_hub, hermes_cli/main+profiles+service_manager+container_boot, mem0/hindsight/honcho plugins, achievements dashboard, release/CI scripts, productivity+comfyui skill helpers, agent/*). Verified zero positional-encoding collisions before insertion; per-file compile() check after. Adds a check-windows-footguns rule flagging bare single-line read_text/write_text (multi-line forms stay covered by the AST guard test from #38985). Together with the salvaged contributor commits this retires the ~169-site bare file-I/O class (#37423's long tail). --- agent/credential_sources.py | 2 +- hermes_cli/gateway.py | 8 +++--- hermes_cli/main.py | 4 +-- hermes_cli/plugins.py | 2 +- hermes_constants.py | 2 +- .../scripts/bootstrap_pipeline.py | 10 +++---- .../scripts/build_findings.py | 4 +-- .../scripts/timing_analysis.py | 2 +- plugins/cron_providers/__init__.py | 2 +- plugins/disk-cleanup/disk_cleanup.py | 6 ++-- plugins/google_meet/realtime/openai_client.py | 6 ++-- .../dashboard/plugin_api.py | 12 ++++---- plugins/memory/__init__.py | 2 +- plugins/memory/mem0/_setup.py | 6 ++-- scripts/check-windows-footguns.py | 28 +++++++++++++++++++ scripts/check_subprocess_stdin.py | 4 +-- scripts/ci/assemble_review_comment.py | 2 +- scripts/lint_diff.py | 4 +-- scripts/profile-tui.py | 2 +- scripts/release.py | 10 +++---- scripts/run_tests_parallel.py | 4 +-- .../creative/comfyui/scripts/auto_fix_deps.py | 2 +- .../comfyui/scripts/extract_schema.py | 2 +- .../creative/comfyui/scripts/run_workflow.py | 2 +- skills/creative/comfyui/tests/conftest.py | 6 ++-- skills/creative/comfyui/tests/test_common.py | 2 +- .../comfyui/tests/test_run_workflow.py | 6 ++-- .../docx/scripts/accept_changes.py | 4 +-- .../docx/scripts/office/soffice.py | 2 +- .../google-workspace/scripts/google_api.py | 4 +-- .../google-workspace/scripts/gws_bridge.py | 4 +-- .../google-workspace/scripts/setup.py | 14 +++++----- .../powerpoint/scripts/office/soffice.py | 2 +- .../xlsx/scripts/office/soffice.py | 2 +- skills/productivity/xlsx/scripts/recalc.py | 2 +- tools/managed_tool_gateway.py | 2 +- 36 files changed, 103 insertions(+), 75 deletions(-) diff --git a/agent/credential_sources.py b/agent/credential_sources.py index 18f0823ba842..32cd5e01a80d 100644 --- a/agent/credential_sources.py +++ b/agent/credential_sources.py @@ -164,7 +164,7 @@ def _remove_env_source(provider: str, removed) -> RemovalResult: if env_path.exists(): env_in_dotenv = any( line.strip().startswith(f"{env_var}=") - for line in env_path.read_text(errors="replace").splitlines() + for line in env_path.read_text(errors="replace", encoding="utf-8").splitlines() ) except OSError: pass diff --git a/hermes_cli/gateway.py b/hermes_cli/gateway.py index ec2aae5b47f1..6e298ea1bd40 100644 --- a/hermes_cli/gateway.py +++ b/hermes_cli/gateway.py @@ -3066,7 +3066,7 @@ def refresh_systemd_unit_if_needed(system: bool = False) -> bool: if _refuse_temp_home_service_write(new_unit, "systemd unit"): return False - unit_path.write_text(new_unit, encoding="utf-8", encoding="utf-8") + unit_path.write_text(new_unit, encoding="utf-8") _run_systemctl(["daemon-reload"], system=system, check=True, timeout=30) print( f"↻ Updated gateway {_service_scope_label(system)} service definition to match the current Hermes install" @@ -3248,7 +3248,7 @@ def systemd_install( if _refuse_temp_home_service_write(new_unit, "systemd unit"): return print(f"Installing {_service_scope_label(system)} systemd service to: {unit_path}") - unit_path.write_text(new_unit, encoding="utf-8", encoding="utf-8") + unit_path.write_text(new_unit, encoding="utf-8") _run_systemctl(["daemon-reload"], system=system, check=True, timeout=30) if enable_on_startup: @@ -4082,7 +4082,7 @@ def refresh_launchd_plist_if_needed() -> bool: if _refuse_temp_home_service_write(new_plist, "launchd plist"): return False - plist_path.write_text(new_plist, encoding="utf-8", encoding="utf-8") + plist_path.write_text(new_plist, encoding="utf-8") label = get_launchd_label() domain = _launchd_domain() target = f"{domain}/{label}" @@ -4301,7 +4301,7 @@ def launchd_start(): sys.exit(1) print("↻ launchd plist missing; regenerating service definition") plist_path.parent.mkdir(parents=True, exist_ok=True) - plist_path.write_text(new_plist, encoding="utf-8", encoding="utf-8") + plist_path.write_text(new_plist, encoding="utf-8") try: _launchctl_bootstrap(_launchd_domain(), plist_path, label, timeout=30) subprocess.run( diff --git a/hermes_cli/main.py b/hermes_cli/main.py index 619311ed18b1..700d5e23623e 100644 --- a/hermes_cli/main.py +++ b/hermes_cli/main.py @@ -9959,7 +9959,7 @@ def _ensure_fhs_path_guard() -> None: if not cfg.is_file(): continue try: - existing = cfg.read_text(errors="replace") + existing = cfg.read_text(errors="replace", encoding="utf-8") except OSError: continue # Idempotency: skip if any uncommented PATH= line already references @@ -12535,7 +12535,7 @@ def _cmd_update_impl(args, gateway_mode: bool): if gateway_mode: _exit_code_path = get_hermes_home() / ".update_exit_code" try: - _exit_code_path.write_text("1") + _exit_code_path.write_text("1", encoding="utf-8") except OSError: pass _warn_incomplete_gateway_fleet_restart(failed_or_stale_units) diff --git a/hermes_cli/plugins.py b/hermes_cli/plugins.py index 6ca393fca53c..80142c15db1d 100644 --- a/hermes_cli/plugins.py +++ b/hermes_cli/plugins.py @@ -1601,7 +1601,7 @@ class PluginManager: init_file = plugin_dir / "__init__.py" if init_file.exists(): try: - source_text = init_file.read_text(errors="replace")[:8192] + source_text = init_file.read_text(errors="replace", encoding="utf-8")[:8192] if ( "register_memory_provider" in source_text or "MemoryProvider" in source_text diff --git a/hermes_constants.py b/hermes_constants.py index 7cb52e16ca2a..bbf45f4d268f 100644 --- a/hermes_constants.py +++ b/hermes_constants.py @@ -78,7 +78,7 @@ def _warn_profile_fallback_once() -> None: try: fallback_home = _get_platform_default_hermes_home() active_path = fallback_home / "active_profile" - active = active_path.read_text().strip() if active_path.exists() else "" + active = active_path.read_text(encoding="utf-8").strip() if active_path.exists() else "" except (UnicodeDecodeError, OSError): active = "" if active and active != "default": diff --git a/optional-skills/creative/kanban-video-orchestrator/scripts/bootstrap_pipeline.py b/optional-skills/creative/kanban-video-orchestrator/scripts/bootstrap_pipeline.py index 7ea146adc137..6fe764f7e6bb 100755 --- a/optional-skills/creative/kanban-video-orchestrator/scripts/bootstrap_pipeline.py +++ b/optional-skills/creative/kanban-video-orchestrator/scripts/bootstrap_pipeline.py @@ -68,7 +68,7 @@ ASSETS_DIR = Path(__file__).resolve().parent.parent / "assets" def load_template(name: str) -> str: - return (ASSETS_DIR / name).read_text() + return (ASSETS_DIR / name).read_text(encoding="utf-8") PROFILE_NAME_RE = re.compile(r"^[a-z0-9][a-z0-9_-]{0,63}$") @@ -471,7 +471,7 @@ def main(): help="Write TEAM.md alongside (default: skipped)") args = ap.parse_args() - plan = json.loads(Path(args.plan_json).read_text()) + plan = json.loads(Path(args.plan_json).read_text(encoding="utf-8")) errors = validate_plan(plan) if errors: print("Plan validation failed:", file=sys.stderr) @@ -483,15 +483,15 @@ def main(): team = render_team_md(plan) setup = render_setup_sh(plan, brief, team) - Path(args.out).write_text(setup) + Path(args.out).write_text(setup, encoding="utf-8") os.chmod(args.out, 0o755) print(f"Wrote {args.out}") if args.brief_out: - Path(args.brief_out).write_text(brief) + Path(args.brief_out).write_text(brief, encoding="utf-8") print(f"Wrote {args.brief_out}") if args.team_out: - Path(args.team_out).write_text(team) + Path(args.team_out).write_text(team, encoding="utf-8") print(f"Wrote {args.team_out}") diff --git a/optional-skills/research/osint-investigation/scripts/build_findings.py b/optional-skills/research/osint-investigation/scripts/build_findings.py index 15021eb08780..0d8c89760884 100644 --- a/optional-skills/research/osint-investigation/scripts/build_findings.py +++ b/optional-skills/research/osint-investigation/scripts/build_findings.py @@ -141,7 +141,7 @@ def build_findings( # 3. Timing-based findings. if timing_path and Path(timing_path).exists(): - timing = json.loads(Path(timing_path).read_text()) + timing = json.loads(Path(timing_path).read_text(encoding="utf-8")) for r in timing.get("results", []): if not r.get("significant"): continue @@ -190,7 +190,7 @@ def build_findings( }, "findings": findings, } - Path(out_path).write_text(json.dumps(payload, indent=2)) + Path(out_path).write_text(json.dumps(payload, indent=2), encoding="utf-8") return payload diff --git a/optional-skills/research/osint-investigation/scripts/timing_analysis.py b/optional-skills/research/osint-investigation/scripts/timing_analysis.py index 9407264158d1..06ef2ded1cbe 100644 --- a/optional-skills/research/osint-investigation/scripts/timing_analysis.py +++ b/optional-skills/research/osint-investigation/scripts/timing_analysis.py @@ -198,7 +198,7 @@ def analyze( "results": results, } - Path(out_path).write_text(json.dumps(payload, indent=2)) + Path(out_path).write_text(json.dumps(payload, indent=2), encoding="utf-8") return payload diff --git a/plugins/cron_providers/__init__.py b/plugins/cron_providers/__init__.py index 456c81b41e31..79dd1a2256e8 100644 --- a/plugins/cron_providers/__init__.py +++ b/plugins/cron_providers/__init__.py @@ -86,7 +86,7 @@ def _is_cron_provider_dir(path: Path) -> bool: if not init_file.exists(): return False try: - source = init_file.read_text(errors="replace")[:8192] + source = init_file.read_text(errors="replace", encoding="utf-8")[:8192] return "register_cron_scheduler" in source or "CronScheduler" in source except Exception: return False diff --git a/plugins/disk-cleanup/disk_cleanup.py b/plugins/disk-cleanup/disk_cleanup.py index 1e1d453f80a4..31f5779b1ff1 100755 --- a/plugins/disk-cleanup/disk_cleanup.py +++ b/plugins/disk-cleanup/disk_cleanup.py @@ -110,12 +110,12 @@ def load_tracked() -> List[Dict[str, Any]]: return [] try: - return json.loads(tf.read_text()) + return json.loads(tf.read_text(encoding="utf-8")) except (json.JSONDecodeError, ValueError): bak = tf.with_suffix(".json.bak") if bak.exists(): try: - data = json.loads(bak.read_text()) + data = json.loads(bak.read_text(encoding="utf-8")) _log("WARN: tracked.json corrupted — restored from .bak") return data except Exception: @@ -129,7 +129,7 @@ def save_tracked(tracked: List[Dict[str, Any]]) -> None: tf = get_tracked_file() tf.parent.mkdir(parents=True, exist_ok=True) tmp = tf.with_suffix(".json.tmp") - tmp.write_text(json.dumps(tracked, indent=2)) + tmp.write_text(json.dumps(tracked, indent=2), encoding="utf-8") if tf.exists(): shutil.copy2(tf, tf.with_suffix(".json.bak")) tmp.replace(tf) diff --git a/plugins/google_meet/realtime/openai_client.py b/plugins/google_meet/realtime/openai_client.py index 24527603e524..2e84c89125c5 100644 --- a/plugins/google_meet/realtime/openai_client.py +++ b/plugins/google_meet/realtime/openai_client.py @@ -262,7 +262,7 @@ class RealtimeSpeaker: if not self.queue_path.exists(): return [] out: list[dict] = [] - for line in self.queue_path.read_text().splitlines(): + for line in self.queue_path.read_text(encoding="utf-8").splitlines(): line = line.strip() if not line: continue @@ -281,10 +281,10 @@ class RealtimeSpeaker: if not remaining: # Keep the file but empty — consumers may be watching for # new writes via mtime, and delete-then-recreate is a race. - self.queue_path.write_text("") + self.queue_path.write_text("", encoding="utf-8") return self.queue_path.write_text( - "\n".join(json.dumps(e) for e in remaining) + "\n" + "\n".join(json.dumps(e) for e in remaining) + "\n", encoding="utf-8" ) def _append_processed(self, entry: dict, result: dict) -> None: diff --git a/plugins/hermes-achievements/dashboard/plugin_api.py b/plugins/hermes-achievements/dashboard/plugin_api.py index b419efc6c27f..c2f69a22999b 100644 --- a/plugins/hermes-achievements/dashboard/plugin_api.py +++ b/plugins/hermes-achievements/dashboard/plugin_api.py @@ -159,7 +159,7 @@ def load_state() -> Dict[str, Any]: if not path.exists(): return {"unlocks": {}} try: - return json.loads(path.read_text()) + return json.loads(path.read_text(encoding="utf-8")) except Exception: return {"unlocks": {}} @@ -167,7 +167,7 @@ def load_state() -> Dict[str, Any]: def save_state(state: Dict[str, Any]) -> None: path = state_path() path.parent.mkdir(parents=True, exist_ok=True) - path.write_text(json.dumps(state, indent=2, sort_keys=True)) + path.write_text(json.dumps(state, indent=2, sort_keys=True), encoding="utf-8") def _json_safe(value: Any) -> Any: @@ -185,7 +185,7 @@ def load_snapshot() -> Optional[Dict[str, Any]]: if not path.exists(): return None try: - data = json.loads(path.read_text()) + data = json.loads(path.read_text(encoding="utf-8")) if isinstance(data, dict): return data except Exception: @@ -196,7 +196,7 @@ def load_snapshot() -> Optional[Dict[str, Any]]: def save_snapshot(data: Dict[str, Any]) -> None: path = snapshot_path() path.parent.mkdir(parents=True, exist_ok=True) - path.write_text(json.dumps(_json_safe(data), indent=2, sort_keys=True)) + path.write_text(json.dumps(_json_safe(data), indent=2, sort_keys=True), encoding="utf-8") def load_checkpoint() -> Dict[str, Any]: @@ -204,7 +204,7 @@ def load_checkpoint() -> Dict[str, Any]: if not path.exists(): return {"schema_version": 1, "generated_at": 0, "sessions": {}} try: - data = json.loads(path.read_text()) + data = json.loads(path.read_text(encoding="utf-8")) if isinstance(data, dict): data.setdefault("schema_version", 1) data.setdefault("generated_at", 0) @@ -219,7 +219,7 @@ def load_checkpoint() -> Dict[str, Any]: def save_checkpoint(data: Dict[str, Any]) -> None: path = checkpoint_path() path.parent.mkdir(parents=True, exist_ok=True) - path.write_text(json.dumps(_json_safe(data), indent=2, sort_keys=True)) + path.write_text(json.dumps(_json_safe(data), indent=2, sort_keys=True), encoding="utf-8") def session_fingerprint(meta: Dict[str, Any]) -> Dict[str, Any]: diff --git a/plugins/memory/__init__.py b/plugins/memory/__init__.py index cccda75ce849..53eeb998a0e5 100644 --- a/plugins/memory/__init__.py +++ b/plugins/memory/__init__.py @@ -81,7 +81,7 @@ def _is_memory_provider_dir(path: Path) -> bool: if not init_file.exists(): return False try: - source = init_file.read_text(errors="replace")[:8192] + source = init_file.read_text(errors="replace", encoding="utf-8")[:8192] return "register_memory_provider" in source or "MemoryProvider" in source except Exception: return False diff --git a/plugins/memory/mem0/_setup.py b/plugins/memory/mem0/_setup.py index 4fdf6f7f0511..5eaa3dc4c8ee 100644 --- a/plugins/memory/mem0/_setup.py +++ b/plugins/memory/mem0/_setup.py @@ -228,7 +228,7 @@ def _save_mem0_json(hermes_home: str, data: dict) -> None: except Exception: pass existing.update(data) - config_path.write_text(json.dumps(existing, indent=2) + "\n") + config_path.write_text(json.dumps(existing, indent=2) + "\n", encoding="utf-8") def _setup_platform(hermes_home: str, config: dict, flags: dict[str, str]) -> None: @@ -248,7 +248,7 @@ def _setup_platform(hermes_home: str, config: dict, flags: dict[str, str]) -> No config_path = Path(hermes_home) / "mem0.json" if config_path.exists(): try: - existing_config = json.loads(config_path.read_text()) + existing_config = json.loads(config_path.read_text(encoding="utf-8")) except Exception: pass @@ -369,7 +369,7 @@ def _setup_selfhosted(hermes_home: str, config: dict, flags: dict[str, str]) -> config_path = Path(hermes_home) / "mem0.json" if config_path.exists(): try: - existing_config = json.loads(config_path.read_text()) + existing_config = json.loads(config_path.read_text(encoding="utf-8")) except Exception: pass diff --git a/scripts/check-windows-footguns.py b/scripts/check-windows-footguns.py index 8c1bd0365752..bb45d85a1e0c 100644 --- a/scripts/check-windows-footguns.py +++ b/scripts/check-windows-footguns.py @@ -373,6 +373,34 @@ FOOTGUNS: list[Footgun] = [ and _is_likely_subprocess_call(line) ), ), + Footgun( + name="bare Path.read_text()/write_text() without encoding=", + # Match ``.read_text(`` / ``.write_text(`` when the same line does + # not pass ``encoding=``. Multi-line calls where encoding= sits on + # a later line are handled by the post_filter's lookahead-free + # heuristic accepting a small false-negative rate — the AST guard + # test in tests/gateway/test_gateway_utf8_encoding.py catches the + # gateway/adapters exactly, and this rule catches the common + # single-line form everywhere else. + pattern=re.compile(r"\.(read_text|write_text)\s*\("), + message=( + "Path.read_text()/write_text() without encoding= uses " + "locale.getpreferredencoding() — cp936/cp1252 on Windows — " + "so UTF-8 content (config JSON, session state, skills) " + "crashes with UnicodeDecodeError or writes mojibake. " + "See issue #37423 and the #71014 / read_text campaign." + ), + fix='path.read_text(encoding="utf-8") / path.write_text(data, encoding="utf-8")', + post_filter=lambda m, line: ( + "encoding=" not in line + and "encoding =" not in line + and not _looks_like_string_literal(line, m) + # Skip calls that continue onto the next line — the closing + # paren isn't on this line, so encoding= may follow. AST-level + # enforcement for those lives in the gateway guard test. + and line.rstrip().endswith(")") + ), + ), ] diff --git a/scripts/check_subprocess_stdin.py b/scripts/check_subprocess_stdin.py index eca28b814ee5..884923e2e04c 100644 --- a/scripts/check_subprocess_stdin.py +++ b/scripts/check_subprocess_stdin.py @@ -181,7 +181,7 @@ def main() -> int: if any(skip.rstrip("/") in parts for skip in SKIP_DIRS): continue - content = py_file.read_text() + content = py_file.read_text(encoding="utf-8") violations = find_subprocess_calls(content, rel) all_violations.extend(violations) @@ -205,7 +205,7 @@ def main() -> int: continue try: - content = py_file.read_text() + content = py_file.read_text(encoding="utf-8") except Exception: continue violations = find_subprocess_calls(content, rel) diff --git a/scripts/ci/assemble_review_comment.py b/scripts/ci/assemble_review_comment.py index 76f3488012ad..312f3c058767 100644 --- a/scripts/ci/assemble_review_comment.py +++ b/scripts/ci/assemble_review_comment.py @@ -419,7 +419,7 @@ def main() -> int: pending_jobs=pending, ) - args.output.write_text(body) + args.output.write_text(body, encoding="utf-8") print(f"Wrote {len(body)} chars to {args.output}") return 0 diff --git a/scripts/lint_diff.py b/scripts/lint_diff.py index a84156fc8e2d..c8d88a256275 100755 --- a/scripts/lint_diff.py +++ b/scripts/lint_diff.py @@ -30,7 +30,7 @@ def _load_json(path: Path | None) -> list[dict]: if path is None or not path.exists() or path.stat().st_size == 0: return [] try: - data = json.loads(path.read_text()) + data = json.loads(path.read_text(encoding="utf-8")) except json.JSONDecodeError as exc: print(f"warning: could not parse {path}: {exc}", file=sys.stderr) return [] @@ -197,7 +197,7 @@ def main() -> int: summary = "\n".join(buf) if args.output: - args.output.write_text(summary) + args.output.write_text(summary, encoding="utf-8") else: print(summary) return 0 diff --git a/scripts/profile-tui.py b/scripts/profile-tui.py index a85573a6f762..4d86aa057bfa 100755 --- a/scripts/profile-tui.py +++ b/scripts/profile-tui.py @@ -516,7 +516,7 @@ def main() -> int: if not path.exists(): print(f"\n⚠ no baseline at {path} — run with --save {args.compare} first") else: - before = json.loads(path.read_text()) + before = json.loads(path.read_text(encoding="utf-8")) print(f"\n═══ A/B diff vs /tmp/perf-{args.compare}.json ═══") print(format_diff(before, metrics)) diff --git a/scripts/release.py b/scripts/release.py index 58734dec76c8..0df1a1b70d57 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -2138,7 +2138,7 @@ def next_available_tag(base_tag: str) -> tuple[str, str]: def get_current_version(): """Read current semver from __init__.py.""" - content = VERSION_FILE.read_text() + content = VERSION_FILE.read_text(encoding="utf-8") match = re.search(r'__version__\s*=\s*"([^"]+)"', content) return match.group(1) if match else "0.0.0" @@ -2168,7 +2168,7 @@ def bump_version(current: str, part: str) -> str: def update_version_files(semver: str, calver_date: str): """Update version strings in source files.""" # Update __init__.py - content = VERSION_FILE.read_text() + content = VERSION_FILE.read_text(encoding="utf-8") content = re.sub( r'__version__\s*=\s*"[^"]+"', f'__version__ = "{semver}"', @@ -2179,17 +2179,17 @@ def update_version_files(semver: str, calver_date: str): f'__release_date__ = "{calver_date}"', content, ) - VERSION_FILE.write_text(content) + VERSION_FILE.write_text(content, encoding="utf-8") # Update pyproject.toml - pyproject = PYPROJECT_FILE.read_text() + pyproject = PYPROJECT_FILE.read_text(encoding="utf-8") pyproject = re.sub( r'^version\s*=\s*"[^"]+"', f'version = "{semver}"', pyproject, flags=re.MULTILINE, ) - PYPROJECT_FILE.write_text(pyproject) + PYPROJECT_FILE.write_text(pyproject, encoding="utf-8") # Keep the desktop Electron app's package.json version in lockstep with the # Python package version. The desktop About panel reads the live Hermes diff --git a/scripts/run_tests_parallel.py b/scripts/run_tests_parallel.py index 91cf0373f60b..797f78b2bda8 100755 --- a/scripts/run_tests_parallel.py +++ b/scripts/run_tests_parallel.py @@ -533,7 +533,7 @@ def _load_durations(repo_root: Path) -> dict[str, float]: if not path.is_file(): return {} try: - return json.loads(path.read_text()) + return json.loads(path.read_text(encoding="utf-8")) except (json.JSONDecodeError, OSError) as e: print("[ERROR] Failed to load json durations file! {e}") return {} @@ -555,7 +555,7 @@ def _save_durations( key = _format_file(f, repo_root) data[key] = round(t, 3) path = repo_root / _DURATIONS_FILE - path.write_text(json.dumps(data, indent=2, sort_keys=True) + "\n") + path.write_text(json.dumps(data, indent=2, sort_keys=True) + "\n", encoding="utf-8") def _compute_lpt_slices( diff --git a/skills/creative/comfyui/scripts/auto_fix_deps.py b/skills/creative/comfyui/scripts/auto_fix_deps.py index 79689972b06a..58cce00c72e6 100755 --- a/skills/creative/comfyui/scripts/auto_fix_deps.py +++ b/skills/creative/comfyui/scripts/auto_fix_deps.py @@ -158,7 +158,7 @@ def main(argv: list[str] | None = None) -> int: sources: dict[str, str] = {} if args.models_from_file: try: - sources = json.loads(Path(args.models_from_file).read_text()) + sources = json.loads(Path(args.models_from_file).read_text(encoding="utf-8")) except (OSError, json.JSONDecodeError) as e: log(f"Could not read --models-from-file: {e}") diff --git a/skills/creative/comfyui/scripts/extract_schema.py b/skills/creative/comfyui/scripts/extract_schema.py index 0eab65b20fdb..82f6eac70464 100755 --- a/skills/creative/comfyui/scripts/extract_schema.py +++ b/skills/creative/comfyui/scripts/extract_schema.py @@ -303,7 +303,7 @@ def main(argv: list[str] | None = None) -> int: out = json.dumps(schema, indent=2, default=str) if args.output: - Path(args.output).write_text(out) + Path(args.output).write_text(out, encoding="utf-8") print(f"Schema written to {args.output}", file=sys.stderr) else: print(out) diff --git a/skills/creative/comfyui/scripts/run_workflow.py b/skills/creative/comfyui/scripts/run_workflow.py index 05afb1e319f5..9b08ec0e5709 100755 --- a/skills/creative/comfyui/scripts/run_workflow.py +++ b/skills/creative/comfyui/scripts/run_workflow.py @@ -620,7 +620,7 @@ def main(argv: list[str] | None = None) -> int: args_str = args.args if args_str.startswith("@"): try: - args_str = Path(args_str[1:]).read_text() + args_str = Path(args_str[1:]).read_text(encoding="utf-8") except OSError as e: emit_json({"error": f"Cannot read args file: {e}"}) return 1 diff --git a/skills/creative/comfyui/tests/conftest.py b/skills/creative/comfyui/tests/conftest.py index a800fa79f1b2..7014edd7d554 100644 --- a/skills/creative/comfyui/tests/conftest.py +++ b/skills/creative/comfyui/tests/conftest.py @@ -22,17 +22,17 @@ sys.path.insert(0, str(SCRIPTS)) @pytest.fixture def sd15_workflow() -> dict: - return json.loads((WORKFLOWS / "sd15_txt2img.json").read_text()) + return json.loads((WORKFLOWS / "sd15_txt2img.json").read_text(encoding="utf-8")) @pytest.fixture def flux_workflow() -> dict: - return json.loads((WORKFLOWS / "flux_dev_txt2img.json").read_text()) + return json.loads((WORKFLOWS / "flux_dev_txt2img.json").read_text(encoding="utf-8")) @pytest.fixture def video_workflow() -> dict: - return json.loads((WORKFLOWS / "wan_video_t2v.json").read_text()) + return json.loads((WORKFLOWS / "wan_video_t2v.json").read_text(encoding="utf-8")) @pytest.fixture diff --git a/skills/creative/comfyui/tests/test_common.py b/skills/creative/comfyui/tests/test_common.py index a5ce6a32714d..2c6afdbfc96f 100644 --- a/skills/creative/comfyui/tests/test_common.py +++ b/skills/creative/comfyui/tests/test_common.py @@ -436,7 +436,7 @@ class TestVideoWorkflow: def test_animatediff_workflow(self, workflows_dir): import json - wf = json.loads((workflows_dir / "animatediff_video.json").read_text()) + wf = json.loads((workflows_dir / "animatediff_video.json").read_text(encoding="utf-8")) assert looks_like_video_workflow(wf) is True def test_wan_workflow(self, video_workflow): diff --git a/skills/creative/comfyui/tests/test_run_workflow.py b/skills/creative/comfyui/tests/test_run_workflow.py index 69957dd23559..ec4091015c25 100644 --- a/skills/creative/comfyui/tests/test_run_workflow.py +++ b/skills/creative/comfyui/tests/test_run_workflow.py @@ -16,20 +16,20 @@ from run_workflow import ( class TestParseInputImageArg: def test_with_name(self, tmp_path): f = tmp_path / "x.png" - f.write_text("x") + f.write_text("x", encoding="utf-8") n, p = parse_input_image_arg(f"image={f}") assert n == "image" assert p == f def test_without_name_defaults(self, tmp_path): f = tmp_path / "x.png" - f.write_text("x") + f.write_text("x", encoding="utf-8") n, p = parse_input_image_arg(str(f)) assert n == "image" def test_custom_name(self, tmp_path): f = tmp_path / "x.png" - f.write_text("x") + f.write_text("x", encoding="utf-8") n, p = parse_input_image_arg(f"mask_image={f}") assert n == "mask_image" diff --git a/skills/productivity/docx/scripts/accept_changes.py b/skills/productivity/docx/scripts/accept_changes.py index a3cc665a19d5..146f09c14f6f 100755 --- a/skills/productivity/docx/scripts/accept_changes.py +++ b/skills/productivity/docx/scripts/accept_changes.py @@ -92,7 +92,7 @@ def _setup_libreoffice_macro() -> bool: macro_dir = Path(MACRO_DIR) macro_file = macro_dir / "Module1.xba" - if macro_file.exists() and "AcceptAllTrackedChanges" in macro_file.read_text(): + if macro_file.exists() and "AcceptAllTrackedChanges" in macro_file.read_text(encoding="utf-8"): return True if not macro_dir.exists(): @@ -111,7 +111,7 @@ def _setup_libreoffice_macro() -> bool: macro_dir.mkdir(parents=True, exist_ok=True) try: - macro_file.write_text(ACCEPT_CHANGES_MACRO) + macro_file.write_text(ACCEPT_CHANGES_MACRO, encoding="utf-8") return True except Exception as e: logger.warning(f"Failed to setup LibreOffice macro: {e}") diff --git a/skills/productivity/docx/scripts/office/soffice.py b/skills/productivity/docx/scripts/office/soffice.py index 0b4c99deca54..eec16879cd4c 100644 --- a/skills/productivity/docx/scripts/office/soffice.py +++ b/skills/productivity/docx/scripts/office/soffice.py @@ -64,7 +64,7 @@ def _ensure_shim() -> Path: return _SHIM_SO src = Path(tempfile.gettempdir()) / "lo_socket_shim.c" - src.write_text(_SHIM_SOURCE) + src.write_text(_SHIM_SOURCE, encoding="utf-8") subprocess.run( ["gcc", "-shared", "-fPIC", "-o", str(_SHIM_SO), str(src), "-ldl"], check=True, diff --git a/skills/productivity/google-workspace/scripts/google_api.py b/skills/productivity/google-workspace/scripts/google_api.py index a4b0451b6ae4..5c2e82323d74 100644 --- a/skills/productivity/google-workspace/scripts/google_api.py +++ b/skills/productivity/google-workspace/scripts/google_api.py @@ -70,7 +70,7 @@ def _ensure_authenticated(): def _stored_token_scopes() -> list[str]: try: - data = json.loads(TOKEN_PATH.read_text()) + data = json.loads(TOKEN_PATH.read_text(encoding="utf-8")) except Exception: return list(SCOPES) scopes = data.get("scopes") @@ -192,7 +192,7 @@ def get_credentials(): json.dumps( _normalize_authorized_user_payload(json.loads(creds.to_json())), indent=2, - ) + ), encoding="utf-8" ) if not creds.valid: print("Token is invalid. Re-run setup.", file=sys.stderr) diff --git a/skills/productivity/google-workspace/scripts/gws_bridge.py b/skills/productivity/google-workspace/scripts/gws_bridge.py index 7d10ba257416..5fa7a9daca52 100755 --- a/skills/productivity/google-workspace/scripts/gws_bridge.py +++ b/skills/productivity/google-workspace/scripts/gws_bridge.py @@ -69,7 +69,7 @@ def refresh_token(token_data: dict) -> dict: ).isoformat() get_token_path().write_text( - json.dumps(_normalize_authorized_user_payload(token_data), indent=2) + json.dumps(_normalize_authorized_user_payload(token_data), indent=2), encoding="utf-8" ) return token_data @@ -81,7 +81,7 @@ def get_valid_token() -> str: print("ERROR: No Google token found. Run setup.py --auth-url first.", file=sys.stderr) sys.exit(1) - token_data = json.loads(token_path.read_text()) + token_data = json.loads(token_path.read_text(encoding="utf-8")) expiry = token_data.get("expiry", "") if expiry: diff --git a/skills/productivity/google-workspace/scripts/setup.py b/skills/productivity/google-workspace/scripts/setup.py index 26f50c52c704..c2393f92db11 100644 --- a/skills/productivity/google-workspace/scripts/setup.py +++ b/skills/productivity/google-workspace/scripts/setup.py @@ -71,7 +71,7 @@ def _normalize_authorized_user_payload(payload: dict) -> dict: def _load_token_payload(path: Path = TOKEN_PATH) -> dict: try: - return json.loads(path.read_text()) + return json.loads(path.read_text(encoding="utf-8")) except Exception: return {} @@ -220,7 +220,7 @@ def check_auth(quiet: bool = False): json.dumps( _normalize_authorized_user_payload(json.loads(creds.to_json())), indent=2, - ) + ), encoding="utf-8" ) missing_scopes = _missing_scopes_from_payload(_load_token_payload(TOKEN_PATH)) if missing_scopes: @@ -260,7 +260,7 @@ def store_client_secret(path: str): sys.exit(1) try: - data = json.loads(src.read_text()) + data = json.loads(src.read_text(encoding="utf-8")) except json.JSONDecodeError: print("ERROR: File is not valid JSON.") sys.exit(1) @@ -270,7 +270,7 @@ def store_client_secret(path: str): print("Download the correct file from: https://console.cloud.google.com/apis/credentials") sys.exit(1) - CLIENT_SECRET_PATH.write_text(json.dumps(data, indent=2)) + CLIENT_SECRET_PATH.write_text(json.dumps(data, indent=2), encoding="utf-8") print(f"OK: Client secret saved to {CLIENT_SECRET_PATH}") @@ -284,7 +284,7 @@ def _save_pending_auth(*, state: str, code_verifier: str): "redirect_uri": REDIRECT_URI, }, indent=2, - ) + ), encoding="utf-8" ) @@ -295,7 +295,7 @@ def _load_pending_auth() -> dict: sys.exit(1) try: - data = json.loads(PENDING_AUTH_PATH.read_text()) + data = json.loads(PENDING_AUTH_PATH.read_text(encoding="utf-8")) except Exception as e: print(f"ERROR: Could not read pending OAuth session: {e}") print("Run --auth-url again to start a fresh OAuth session.") @@ -410,7 +410,7 @@ def exchange_auth_code(code: str): print(f"WARNING: Token missing some Google Workspace scopes: {', '.join(missing_scopes)}") print("Some services may not be available.") - TOKEN_PATH.write_text(json.dumps(token_payload, indent=2)) + TOKEN_PATH.write_text(json.dumps(token_payload, indent=2), encoding="utf-8") PENDING_AUTH_PATH.unlink(missing_ok=True) print(f"OK: Authenticated. Token saved to {TOKEN_PATH}") print(f"Profile-scoped token location: {display_hermes_home()}/google_token.json") diff --git a/skills/productivity/powerpoint/scripts/office/soffice.py b/skills/productivity/powerpoint/scripts/office/soffice.py index 0b4c99deca54..eec16879cd4c 100644 --- a/skills/productivity/powerpoint/scripts/office/soffice.py +++ b/skills/productivity/powerpoint/scripts/office/soffice.py @@ -64,7 +64,7 @@ def _ensure_shim() -> Path: return _SHIM_SO src = Path(tempfile.gettempdir()) / "lo_socket_shim.c" - src.write_text(_SHIM_SOURCE) + src.write_text(_SHIM_SOURCE, encoding="utf-8") subprocess.run( ["gcc", "-shared", "-fPIC", "-o", str(_SHIM_SO), str(src), "-ldl"], check=True, diff --git a/skills/productivity/xlsx/scripts/office/soffice.py b/skills/productivity/xlsx/scripts/office/soffice.py index 0b4c99deca54..eec16879cd4c 100644 --- a/skills/productivity/xlsx/scripts/office/soffice.py +++ b/skills/productivity/xlsx/scripts/office/soffice.py @@ -64,7 +64,7 @@ def _ensure_shim() -> Path: return _SHIM_SO src = Path(tempfile.gettempdir()) / "lo_socket_shim.c" - src.write_text(_SHIM_SOURCE) + src.write_text(_SHIM_SOURCE, encoding="utf-8") subprocess.run( ["gcc", "-shared", "-fPIC", "-o", str(_SHIM_SO), str(src), "-ldl"], check=True, diff --git a/skills/productivity/xlsx/scripts/recalc.py b/skills/productivity/xlsx/scripts/recalc.py index b9f1527b5f30..2317ce627b37 100755 --- a/skills/productivity/xlsx/scripts/recalc.py +++ b/skills/productivity/xlsx/scripts/recalc.py @@ -71,7 +71,7 @@ def setup_libreoffice_macro(profile_dir: Path, timeout=30): return None, "LibreOffice did not create a usable profile; formulas were NOT recalculated" try: - (macro_dir / MACRO_FILENAME).write_text(RECALCULATE_MACRO) + (macro_dir / MACRO_FILENAME).write_text(RECALCULATE_MACRO, encoding="utf-8") except OSError as e: return None, f"Could not install the recalculation macro: {e}" diff --git a/tools/managed_tool_gateway.py b/tools/managed_tool_gateway.py index d894dcb4b29b..2bbc9dcff16c 100644 --- a/tools/managed_tool_gateway.py +++ b/tools/managed_tool_gateway.py @@ -37,7 +37,7 @@ def _read_nous_provider_state() -> Optional[dict]: path = auth_json_path() if not path.is_file(): return None - data = json.loads(path.read_text()) + data = json.loads(path.read_text(encoding="utf-8")) providers = data.get("providers", {}) if not isinstance(providers, dict): return None