refactor(hermes_cli): group D — merge fragmented string literals, hug call layouts (AST-neutral)
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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 <uid>:<gid>`."
|
||||
)
|
||||
_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 <uid>:<gid>`.")
|
||||
_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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user