diff --git a/hermes_cli/container_boot.py b/hermes_cli/container_boot.py index 0a51cdcf76..5266f363f5 100644 --- a/hermes_cli/container_boot.py +++ b/hermes_cli/container_boot.py @@ -62,10 +62,9 @@ class ReconcileAction: def _slot_action( profile: str, profile_dir: Path, prior_state: str | None, start: bool, ) -> ReconcileAction: - return ReconcileAction( - profile=profile, prior_state=prior_state, action="started" if start else "registered", - prior_exit=_read_prior_exit_label(profile_dir), - ) + return ReconcileAction(profile=profile, prior_state=prior_state, + action="started" if start else "registered", + prior_exit=_read_prior_exit_label(profile_dir)) def reconcile_profile_gateways( @@ -92,11 +91,8 @@ def reconcile_profile_gateways( try: multiplex_profiles = load_gateway_config().multiplex_profiles except Exception: - log.warning( - "Unable to load gateway configuration during container boot; " - "using the GATEWAY_MULTIPLEX_PROFILES override if set.", - exc_info=True, - ) + log.warning("Unable to load gateway configuration during container boot; using the " + "GATEWAY_MULTIPLEX_PROFILES override if set.", exc_info=True) multiplex_profiles = is_truthy_value(os.environ.get("GATEWAY_MULTIPLEX_PROFILES")) # Default profile — always register, even if nothing has ever populated the root profile @@ -104,8 +100,7 @@ def reconcile_profile_gateways( # legacy `gateway run` container with no state yet seeds that intent as `running` so the # s6 reconciler preserves the pre-s6 behavior. legacy_default_state = _maybe_migrate_legacy_gateway_run_state( - hermes_home, container_argv=container_argv, dry_run=dry_run, - ) + hermes_home, container_argv=container_argv, dry_run=dry_run) default_prior_state = legacy_default_state or _read_desired_state(hermes_home) default_should_start = default_prior_state in _AUTOSTART_STATES if not dry_run: @@ -124,10 +119,8 @@ def reconcile_profile_gateways( # "default" is reserved for the root profile (above); skip a stray # ``profiles/default/`` rather than collide on the slot. if entry.name == "default": - log.warning( - "profiles/default/ exists — skipping to avoid colliding " - "with the reserved root-profile s6 slot", - ) + log.warning("profiles/default/ exists — skipping to avoid colliding with the " + "reserved root-profile s6 slot") continue prior_state = _read_desired_state(entry) diff --git a/hermes_cli/copilot_auth.py b/hermes_cli/copilot_auth.py index 7c434bf754..e8040871d1 100644 --- a/hermes_cli/copilot_auth.py +++ b/hermes_cli/copilot_auth.py @@ -79,11 +79,9 @@ def resolve_copilot_token() -> tuple[str, str]: # one silently substituted from the gh credential store. Skipping the subprocess also # avoids a slow `gh auth token` call (up to 5s on Windows) on every cold start. if any_env_var_set: - logger.debug( - "Copilot env var(s) set but none held a supported token; " - "skipping `gh auth token` fallback to honor explicit env-var " - "intent (and avoid the subprocess cost on cold start, #60800)." - ) + logger.debug("Copilot env var(s) set but none held a supported token; skipping `gh auth " + "token` fallback to honor explicit env-var intent (and avoid the subprocess " + "cost on cold start, #60800).") return "", "" token = _try_gh_cli_token() @@ -99,9 +97,8 @@ def resolve_copilot_token() -> tuple[str, str]: def _gh_cli_candidates() -> list[str]: """Candidate ``gh`` binary paths, including common Homebrew installs.""" candidates: list[str] = [c for c in (shutil.which("gh"),) if c] - for candidate in ( - "/opt/homebrew/bin/gh", "/usr/local/bin/gh", str(Path.home() / ".local" / "bin" / "gh") - ): + for candidate in ("/opt/homebrew/bin/gh", "/usr/local/bin/gh", + str(Path.home() / ".local" / "bin" / "gh")): if (candidate not in candidates and os.path.isfile(candidate) and os.access(candidate, os.X_OK)): candidates.append(candidate) @@ -154,10 +151,9 @@ def _probe_gh_cli_token() -> Optional[str]: if hostname: cmd += ["--hostname", hostname] try: - result = subprocess.run( - cmd, capture_output=True, text=True, encoding='utf-8', errors='replace', - timeout=5, env=clean_env, stdin=subprocess.DEVNULL, **_popen_kwargs, - ) + result = subprocess.run(cmd, capture_output=True, text=True, encoding='utf-8', + errors='replace', timeout=5, env=clean_env, + stdin=subprocess.DEVNULL, **_popen_kwargs) except (FileNotFoundError, subprocess.TimeoutExpired) as exc: logger.debug("gh CLI token lookup failed (%s): %s", gh_path, exc) continue @@ -168,10 +164,8 @@ def _probe_gh_cli_token() -> Optional[str]: # ─── OAuth Device Code Flow ──────────────────────────────────────────────── -_DEVICE_CODE_TERMINAL_ERRORS = { - "expired_token": " ✗ Device code expired. Please try again.", - "access_denied": " ✗ Authorization was denied.", -} +_DEVICE_CODE_TERMINAL_ERRORS = {"expired_token": " ✗ Device code expired. Please try again.", + "access_denied": " ✗ Authorization was denied."} def _post_form(url: str, fields: dict, timeout: float) -> dict: @@ -197,9 +191,8 @@ def copilot_device_code_login( access_token_url = f"https://{domain}/login/oauth/access_token" try: - device_data = _post_form( - device_code_url, {"client_id": COPILOT_OAUTH_CLIENT_ID, "scope": "read:user"}, 15 - ) + device_data = _post_form(device_code_url, + {"client_id": COPILOT_OAUTH_CLIENT_ID, "scope": "read:user"}, 15) except Exception as exc: logger.error("Failed to initiate device authorization: %s", exc) print(f" ✗ Failed to start device authorization: {exc}") @@ -332,9 +325,8 @@ def _read_jwt_store(path: Path) -> Optional[dict]: return None try: if path.stat().st_size > _JWT_DISK_MAX_BYTES: - logger.debug( - "Persisted Copilot JWT store exceeds %d bytes; ignoring", _JWT_DISK_MAX_BYTES - ) + logger.debug("Persisted Copilot JWT store exceeds %d bytes; ignoring", + _JWT_DISK_MAX_BYTES) return None loaded = json.loads(path.read_text(encoding="utf-8")) return loaded if isinstance(loaded, dict) else None @@ -549,10 +541,8 @@ def _exchange_copilot_token_locked( break if attempt < _EXCHANGE_MAX_ATTEMPTS: sleep_s = _EXCHANGE_BACKOFF_BASE_SECONDS * attempt - logger.debug( - "Copilot token exchange attempt %d/%d failed (%s); retrying in %.1fs", - attempt, _EXCHANGE_MAX_ATTEMPTS, exc, sleep_s, - ) + logger.debug("Copilot token exchange attempt %d/%d failed (%s); retrying in %.1fs", + attempt, _EXCHANGE_MAX_ATTEMPTS, exc, sleep_s) time.sleep(sleep_s) if data is None: ttl = ( @@ -624,13 +614,10 @@ def copilot_request_headers( *, is_agent_turn: bool = True, is_vision: bool = False, ) -> dict[str, str]: """Build the standard headers for Copilot API requests.""" - headers: dict[str, str] = { - "Editor-Version": _EDITOR_VERSION, - "User-Agent": "HermesAgent/1.0", - "Copilot-Integration-Id": "vscode-chat", - "Openai-Intent": "conversation-edits", - "x-initiator": "agent" if is_agent_turn else "user", - } + headers: dict[str, str] = {"Editor-Version": _EDITOR_VERSION, "User-Agent": "HermesAgent/1.0", + "Copilot-Integration-Id": "vscode-chat", + "Openai-Intent": "conversation-edits", + "x-initiator": "agent" if is_agent_turn else "user"} if is_vision: headers["Copilot-Vision-Request"] = "true" diff --git a/hermes_cli/cron.py b/hermes_cli/cron.py index c8a028da6b..65fc418eb5 100644 --- a/hermes_cli/cron.py +++ b/hermes_cli/cron.py @@ -254,10 +254,8 @@ def cron_list(show_all: bool = False): latest_execution = job.get("latest_execution") if latest_execution: - print( - f" Execution: {latest_execution.get('status', '?')} " - f"{latest_execution.get('id', '?')}" - ) + print(f" Execution: {latest_execution.get('status', '?')} " + f"{latest_execution.get('id', '?')}") delivery_err = job.get("last_delivery_error") if delivery_err: @@ -267,17 +265,13 @@ def cron_list(show_all: bool = False): # (Slack/Matrix/Mattermost shape): accepted as delivered, but say so here. unverified = job.get("last_delivery_unverified") if unverified: - print( - f" {color('⚠ Delivery UNVERIFIED:', Colors.YELLOW)} " - f"adapter acked {_unverified_targets(unverified)} without message_id/raw_response" - ) + print(f" {color('⚠ Delivery UNVERIFIED:', Colors.YELLOW)} adapter acked " + f"{_unverified_targets(unverified)} without message_id/raw_response") fire_err = job.get("last_fire_error") if isinstance(fire_err, dict) and fire_err.get("detail"): - print( - f" {color('⚠ Missed scheduled fire:', Colors.RED)} " - f"{fire_err.get('at', '?')} {fire_err['detail']}" - ) + print(f" {color('⚠ Missed scheduled fire:', Colors.RED)} " + f"{fire_err.get('at', '?')} {fire_err['detail']}") print() @@ -293,10 +287,8 @@ def cron_tick(): # Not expected here (a one-shot CLI process has no boot fingerprint, so the yield # gate is inert) — report cleanly instead of a traceback if a future caller records one. print(color(f"✗ {exc}", Colors.YELLOW)) - print( - " A fresher gateway process owns the runtime lock and will fire " - "due jobs; this stale process yielded its tick." - ) + print(" A fresher gateway process owns the runtime lock and will fire due jobs; this " + "stale process yielded its tick.") return 1 except OSError as exc: # tick() propagates real lock-acquisition failures (EMFILE, EACCES on open, ...) @@ -316,11 +308,9 @@ def cron_runs(job_id: Optional[str] = None, limit: int = 20): print("No cron execution attempts recorded.") return for record in records: - print( - f"{record.get('id', '?')} {record.get('status', '?'):<9} " - f"job={record.get('job_id', '?')} source={record.get('source', '?')} " - f"{record.get('claimed_at', '?')}" - ) + print(f"{record.get('id', '?')} {record.get('status', '?'):<9} " + f"job={record.get('job_id', '?')} source={record.get('source', '?')} " + f"{record.get('claimed_at', '?')}") if record.get("error"): print(f" {record['error']}") @@ -379,18 +369,12 @@ def cron_incidents(args) -> int: return 0 -_PERMISSION_HINT = ( - " Hint: jobs.json may be owned by another user " - "(e.g. rewritten by a root `docker exec hermes " - "hermes cron ...`). Fix ownership to match the " - "gateway user, and prefer `docker exec -u :`." -) -_FD_EXHAUSTION_HINT = ( - " Hint: the ticker hit file-descriptor exhaustion " - "(EMFILE). The scheduler now retries with backoff and " - "attempts fd reclamation, but if the leak persists, " - "restart the gateway to recover scheduling." -) +_PERMISSION_HINT = (" Hint: jobs.json may be owned by another user (e.g. rewritten by a root " + "`docker exec hermes hermes cron ...`). Fix ownership to match the gateway " + "user, and prefer `docker exec -u :`.") +_FD_EXHAUSTION_HINT = (" Hint: the ticker hit file-descriptor exhaustion (EMFILE). The scheduler " + "now retries with backoff and attempts fd reclamation, but if the leak " + "persists, restart the gateway to recover scheduling.") def _print_ticker_health(pids: list) -> None: @@ -669,13 +653,11 @@ def cron_doctor() -> int: return 1 -_JOB_ARG_FIELDS = ( - ("name", "name"), ("deliver", "deliver"), ("failure_deliver", "failure_deliver"), - ("repeat", "repeat"), ("script", "script"), ("workdir", "workdir"), ("model", "model"), - ("provider", "model_provider"), ("monitor_script", "monitor_script"), - ("monitor_url", "monitor_url"), ("continuity", "continuity"), - ("reasoning_effort", "reasoning_effort"), -) +_JOB_ARG_FIELDS = (("name", "name"), ("deliver", "deliver"), ("failure_deliver", "failure_deliver"), + ("repeat", "repeat"), ("script", "script"), ("workdir", "workdir"), + ("model", "model"), ("provider", "model_provider"), + ("monitor_script", "monitor_script"), ("monitor_url", "monitor_url"), + ("continuity", "continuity"), ("reasoning_effort", "reasoning_effort")) def _job_api_kwargs(args) -> Dict[str, Any]: @@ -753,11 +735,10 @@ def cron_edit(args): if skill not in final_skills: final_skills.append(skill) - result = _cron_api( - action="update", job_id=args.job_id, schedule=getattr(args, "schedule", None), - prompt=getattr(args, "prompt", None), skills=final_skills, - no_agent=getattr(args, "no_agent", None), **_job_api_kwargs(args), - ) + result = _cron_api(action="update", job_id=args.job_id, + schedule=getattr(args, "schedule", None), + prompt=getattr(args, "prompt", None), skills=final_skills, + no_agent=getattr(args, "no_agent", None), **_job_api_kwargs(args)) if not result.get("success"): print(color(f"Failed to update job: {result.get('error', 'unknown error')}", Colors.RED)) return 1 diff --git a/hermes_cli/dashboard_procs.py b/hermes_cli/dashboard_procs.py index cdd8b217ed..552359d16d 100644 --- a/hermes_cli/dashboard_procs.py +++ b/hermes_cli/dashboard_procs.py @@ -295,12 +295,10 @@ def _kill_pids_windows(pids: list[int], killed: list[int], failed: list[tuple[in if not pid_is_hermes(pid, expected_start_time=expected_start_time): failed.append((pid, "not hermes-owned or process identity changed")) continue - result = subprocess.run( - ["taskkill", "/PID", str(pid), "/F"], - stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.DEVNULL, - text=True, encoding="utf-8", errors="replace", timeout=10, - creationflags=windows_hide_flags(), - ) + result = subprocess.run(["taskkill", "/PID", str(pid), "/F"], stdout=subprocess.PIPE, + stderr=subprocess.PIPE, stdin=subprocess.DEVNULL, text=True, + encoding="utf-8", errors="replace", timeout=10, + creationflags=windows_hide_flags()) if result.returncode == 0: killed.append(pid) else: @@ -365,8 +363,7 @@ def _kill_stale_dashboard_processes( # below drops PIDs it owns) and keep going. _dash_unit = getattr(_m(), "_DASHBOARD_SYSTEMD_UNIT", "hermes-dashboard.service") already_restarted_units = set(already_restarted_units or ()) | { - str(_dash_unit).removesuffix(".service") - } + str(_dash_unit).removesuffix(".service")} exclude = _exclude_pids_from_env() if restart_managed: diff --git a/hermes_cli/dashboard_register.py b/hermes_cli/dashboard_register.py index 7045fa98ce..0dca8de774 100644 --- a/hermes_cli/dashboard_register.py +++ b/hermes_cli/dashboard_register.py @@ -93,10 +93,8 @@ def _register_self_hosted_client( except Exception: pass if exc.code == 401: - raise RuntimeError( - "Nous Portal rejected the access token (401). " - "Try `hermes auth add nous` to re-authenticate." - ) from exc + raise RuntimeError("Nous Portal rejected the access token (401). Try `hermes auth add " + "nous` to re-authenticate.") from exc if exc.code == 403: raise RuntimeError( detail or "Your account is not permitted to register a self-hosted dashboard." diff --git a/hermes_cli/debug.py b/hermes_cli/debug.py index 41627fcbcf..a0d4e16161 100644 --- a/hermes_cli/debug.py +++ b/hermes_cli/debug.py @@ -168,9 +168,8 @@ def delete_paste(url: str) -> bool: if not paste_id: raise ValueError(f"Cannot delete: only paste.rs URLs are supported. Got: {url}") - req = urllib.request.Request( - f"{_PASTE_RS_URL}{paste_id}", method="DELETE", headers={"User-Agent": _USER_AGENT}, - ) + req = urllib.request.Request(f"{_PASTE_RS_URL}{paste_id}", method="DELETE", + headers={"User-Agent": _USER_AGENT}) with urllib.request.urlopen(req, timeout=30) as resp: return 200 <= resp.status < 300 @@ -191,10 +190,8 @@ def _schedule_auto_delete(urls: list[str], delay_seconds: int = _AUTO_DELETE_SEC def _post_paste(service: str, endpoint: str, body: bytes, content_type: str) -> str: """POST *body* to a paste service and return the paste URL it echoes back.""" - req = urllib.request.Request( - endpoint, data=body, method="POST", - headers={"Content-Type": content_type, "User-Agent": _USER_AGENT}, - ) + req = urllib.request.Request(endpoint, data=body, method="POST", + headers={"Content-Type": content_type, "User-Agent": _USER_AGENT}) with urllib.request.urlopen(req, timeout=30) as resp: url = resp.read().decode("utf-8").strip() if not url.startswith("http"): @@ -488,12 +485,9 @@ def build_nous_bundle(bundle: dict[str, str], redact: bool = True) -> bytes: The JSON shape (``format``, ``redacted``, ``created``, ``files``) is what the discord-support viewer parses — keep it stable. """ - envelope = { - "format": _NOUS_BUNDLE_FORMAT, - "redacted": bool(redact), - "created": datetime.datetime.now(datetime.timezone.utc).isoformat(), - "files": bundle, - } + envelope = {"format": _NOUS_BUNDLE_FORMAT, "redacted": bool(redact), + "created": datetime.datetime.now(datetime.timezone.utc).isoformat(), + "files": bundle} return gzip.compress(json.dumps(envelope).encode("utf-8")) @@ -544,10 +538,8 @@ def build_debug_share(*, log_lines: int = 200, expiry: int = 7, redact: bool = T _schedule_auto_delete(list(urls.values())) - return DebugShareResult( - urls=urls, failures=failures, redacted=redact, - auto_delete_seconds=_AUTO_DELETE_SECONDS, report=report, - ) + return DebugShareResult(urls=urls, failures=failures, redacted=redact, + auto_delete_seconds=_AUTO_DELETE_SECONDS, report=report) def _confirm_upload(args) -> bool: