mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-18 04:41:56 +00:00
chore: ruff auto-fix PLR6201 — tuple → set in membership tests (#23937)
Replace with for all literal-tuple membership tests. Set lookup is O(1) vs O(n) for tuple — consistent micro-optimization across the codebase. 608 instances fixed via `ruff --fix --unsafe-fixes`, 0 remaining. 133 files, +626/-626 (net zero).
This commit is contained in:
parent
8c11710314
commit
2ec8d2b42f
133 changed files with 626 additions and 626 deletions
|
|
@ -769,8 +769,8 @@ def _build_patch_mode_content(patch_text: str) -> List[Any]:
|
||||||
old_chunks: list[str] = []
|
old_chunks: list[str] = []
|
||||||
new_chunks: list[str] = []
|
new_chunks: list[str] = []
|
||||||
for hunk in op.hunks:
|
for hunk in op.hunks:
|
||||||
old_lines = [line.content for line in hunk.lines if line.prefix in (" ", "-")]
|
old_lines = [line.content for line in hunk.lines if line.prefix in {" ", "-"}]
|
||||||
new_lines = [line.content for line in hunk.lines if line.prefix in (" ", "+")]
|
new_lines = [line.content for line in hunk.lines if line.prefix in {" ", "+"}]
|
||||||
if old_lines or new_lines:
|
if old_lines or new_lines:
|
||||||
old_chunks.append("\n".join(old_lines))
|
old_chunks.append("\n".join(old_lines))
|
||||||
new_chunks.append("\n".join(new_lines))
|
new_chunks.append("\n".join(new_lines))
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ def _title_case_slug(value: Optional[str]) -> Optional[str]:
|
||||||
|
|
||||||
|
|
||||||
def _parse_dt(value: Any) -> Optional[datetime]:
|
def _parse_dt(value: Any) -> Optional[datetime]:
|
||||||
if value in (None, ""):
|
if value in {None, ""}:
|
||||||
return None
|
return None
|
||||||
if isinstance(value, (int, float)):
|
if isinstance(value, (int, float)):
|
||||||
return datetime.fromtimestamp(float(value), tz=timezone.utc)
|
return datetime.fromtimestamp(float(value), tz=timezone.utc)
|
||||||
|
|
|
||||||
|
|
@ -1537,7 +1537,7 @@ def convert_messages_to_anthropic(
|
||||||
# downgraded to a spurious text block on the last assistant message.
|
# downgraded to a spurious text block on the last assistant message.
|
||||||
reasoning_content = m.get("reasoning_content")
|
reasoning_content = m.get("reasoning_content")
|
||||||
_already_has_thinking = any(
|
_already_has_thinking = any(
|
||||||
isinstance(b, dict) and b.get("type") in ("thinking", "redacted_thinking")
|
isinstance(b, dict) and b.get("type") in {"thinking", "redacted_thinking"}
|
||||||
for b in blocks
|
for b in blocks
|
||||||
)
|
)
|
||||||
if isinstance(reasoning_content, str) and not _already_has_thinking:
|
if isinstance(reasoning_content, str) and not _already_has_thinking:
|
||||||
|
|
@ -1688,7 +1688,7 @@ def convert_messages_to_anthropic(
|
||||||
if isinstance(m["content"], list):
|
if isinstance(m["content"], list):
|
||||||
m["content"] = [
|
m["content"] = [
|
||||||
b for b in m["content"]
|
b for b in m["content"]
|
||||||
if not (isinstance(b, dict) and b.get("type") in ("thinking", "redacted_thinking"))
|
if not (isinstance(b, dict) and b.get("type") in {"thinking", "redacted_thinking"})
|
||||||
]
|
]
|
||||||
prev_blocks = fixed[-1]["content"]
|
prev_blocks = fixed[-1]["content"]
|
||||||
curr_blocks = m["content"]
|
curr_blocks = m["content"]
|
||||||
|
|
|
||||||
|
|
@ -175,7 +175,7 @@ def _normalize_aux_provider(provider: Optional[str]) -> str:
|
||||||
# Resolve to the user's actual main provider so named custom providers
|
# Resolve to the user's actual main provider so named custom providers
|
||||||
# and non-aggregator providers (DeepSeek, Alibaba, etc.) work correctly.
|
# and non-aggregator providers (DeepSeek, Alibaba, etc.) work correctly.
|
||||||
main_prov = (_read_main_provider() or "").strip().lower()
|
main_prov = (_read_main_provider() or "").strip().lower()
|
||||||
if main_prov and main_prov not in ("auto", "main", ""):
|
if main_prov and main_prov not in {"auto", "main", ""}:
|
||||||
normalized = main_prov
|
normalized = main_prov
|
||||||
else:
|
else:
|
||||||
return "custom"
|
return "custom"
|
||||||
|
|
@ -578,7 +578,7 @@ def _convert_content_for_responses(content: Any) -> Any:
|
||||||
if detail:
|
if detail:
|
||||||
entry["detail"] = detail
|
entry["detail"] = detail
|
||||||
converted.append(entry)
|
converted.append(entry)
|
||||||
elif ptype in ("input_text", "input_image"):
|
elif ptype in {"input_text", "input_image"}:
|
||||||
# Already in Responses format — pass through
|
# Already in Responses format — pass through
|
||||||
converted.append(part)
|
converted.append(part)
|
||||||
else:
|
else:
|
||||||
|
|
@ -798,7 +798,7 @@ class _CodexCompletionsAdapter:
|
||||||
if item_type == "message":
|
if item_type == "message":
|
||||||
for part in (_item_get(item, "content") or []):
|
for part in (_item_get(item, "content") or []):
|
||||||
ptype = _item_get(part, "type")
|
ptype = _item_get(part, "type")
|
||||||
if ptype in ("output_text", "text"):
|
if ptype in {"output_text", "text"}:
|
||||||
text_parts.append(_item_get(part, "text", ""))
|
text_parts.append(_item_get(part, "text", ""))
|
||||||
elif item_type == "function_call":
|
elif item_type == "function_call":
|
||||||
tool_calls_raw.append(SimpleNamespace(
|
tool_calls_raw.append(SimpleNamespace(
|
||||||
|
|
@ -1960,7 +1960,7 @@ def _is_payment_error(exc: Exception) -> bool:
|
||||||
err_lower = str(exc).lower()
|
err_lower = str(exc).lower()
|
||||||
# OpenRouter and other providers include "credits" or "afford" in 402 bodies,
|
# OpenRouter and other providers include "credits" or "afford" in 402 bodies,
|
||||||
# but sometimes wrap them in 429 or other codes.
|
# but sometimes wrap them in 429 or other codes.
|
||||||
if status in (402, 429, None):
|
if status in {402, 429, None}:
|
||||||
if any(kw in err_lower for kw in ("credits", "insufficient funds",
|
if any(kw in err_lower for kw in ("credits", "insufficient funds",
|
||||||
"can only afford", "billing",
|
"can only afford", "billing",
|
||||||
"payment required")):
|
"payment required")):
|
||||||
|
|
@ -2157,7 +2157,7 @@ def _pool_cache_hint(
|
||||||
if normalized == "auto":
|
if normalized == "auto":
|
||||||
runtime = _normalize_main_runtime(main_runtime)
|
runtime = _normalize_main_runtime(main_runtime)
|
||||||
normalized = _normalize_aux_provider(runtime.get("provider") or _read_main_provider())
|
normalized = _normalize_aux_provider(runtime.get("provider") or _read_main_provider())
|
||||||
if normalized in ("", "auto", "custom"):
|
if normalized in {"", "auto", "custom"}:
|
||||||
return ""
|
return ""
|
||||||
entry = _peek_pool_entry(normalized)
|
entry = _peek_pool_entry(normalized)
|
||||||
if entry is None:
|
if entry is None:
|
||||||
|
|
@ -2179,7 +2179,7 @@ def _pool_error_context(exc: Exception) -> Dict[str, Any]:
|
||||||
def _recoverable_pool_provider(resolved_provider: str, client: Any) -> Optional[str]:
|
def _recoverable_pool_provider(resolved_provider: str, client: Any) -> Optional[str]:
|
||||||
"""Infer which provider pool can recover the current auxiliary client."""
|
"""Infer which provider pool can recover the current auxiliary client."""
|
||||||
normalized = _normalize_aux_provider(resolved_provider)
|
normalized = _normalize_aux_provider(resolved_provider)
|
||||||
if normalized not in ("", "auto", "custom"):
|
if normalized not in {"", "auto", "custom"}:
|
||||||
return normalized
|
return normalized
|
||||||
base = str(getattr(client, "base_url", "") or "")
|
base = str(getattr(client, "base_url", "") or "")
|
||||||
if base_url_host_matches(base, "chatgpt.com"):
|
if base_url_host_matches(base, "chatgpt.com"):
|
||||||
|
|
@ -2496,7 +2496,7 @@ def _resolve_auto(main_runtime: Optional[Dict[str, Any]] = None) -> Tuple[Option
|
||||||
main_provider = runtime_provider or _read_main_provider()
|
main_provider = runtime_provider or _read_main_provider()
|
||||||
main_model = runtime_model or _read_main_model()
|
main_model = runtime_model or _read_main_model()
|
||||||
if (main_provider and main_model
|
if (main_provider and main_model
|
||||||
and main_provider not in ("auto", "")):
|
and main_provider not in {"auto", ""}):
|
||||||
resolved_provider = main_provider
|
resolved_provider = main_provider
|
||||||
explicit_base_url = None
|
explicit_base_url = None
|
||||||
explicit_api_key = None
|
explicit_api_key = None
|
||||||
|
|
@ -3157,7 +3157,7 @@ def resolve_provider_client(
|
||||||
return (_to_async_client(client, final_model, is_vision=is_vision) if async_mode
|
return (_to_async_client(client, final_model, is_vision=is_vision) if async_mode
|
||||||
else (client, final_model))
|
else (client, final_model))
|
||||||
|
|
||||||
elif pconfig.auth_type in ("oauth_device_code", "oauth_external"):
|
elif pconfig.auth_type in {"oauth_device_code", "oauth_external"}:
|
||||||
# OAuth providers — route through their specific try functions
|
# OAuth providers — route through their specific try functions
|
||||||
if provider == "nous":
|
if provider == "nous":
|
||||||
return resolve_provider_client("nous", model, async_mode)
|
return resolve_provider_client("nous", model, async_mode)
|
||||||
|
|
@ -3266,7 +3266,7 @@ def get_available_vision_backends() -> List[str]:
|
||||||
available: List[str] = []
|
available: List[str] = []
|
||||||
# 1. Active provider — if the user configured a provider, try it first.
|
# 1. Active provider — if the user configured a provider, try it first.
|
||||||
main_provider = _read_main_provider()
|
main_provider = _read_main_provider()
|
||||||
if main_provider and main_provider not in ("auto", ""):
|
if main_provider and main_provider not in {"auto", ""}:
|
||||||
if main_provider in _VISION_AUTO_PROVIDER_ORDER:
|
if main_provider in _VISION_AUTO_PROVIDER_ORDER:
|
||||||
if _strict_vision_backend_available(main_provider):
|
if _strict_vision_backend_available(main_provider):
|
||||||
available.append(main_provider)
|
available.append(main_provider)
|
||||||
|
|
@ -3312,7 +3312,7 @@ def resolve_vision_provider_client(
|
||||||
|
|
||||||
if resolved_base_url:
|
if resolved_base_url:
|
||||||
provider_for_base_override = (
|
provider_for_base_override = (
|
||||||
requested if requested and requested not in ("", "auto") else "custom"
|
requested if requested and requested not in {"", "auto"} else "custom"
|
||||||
)
|
)
|
||||||
client, final_model = resolve_provider_client(
|
client, final_model = resolve_provider_client(
|
||||||
provider_for_base_override,
|
provider_for_base_override,
|
||||||
|
|
@ -3340,7 +3340,7 @@ def resolve_vision_provider_client(
|
||||||
# 4. Stop
|
# 4. Stop
|
||||||
main_provider = _read_main_provider()
|
main_provider = _read_main_provider()
|
||||||
main_model = _read_main_model()
|
main_model = _read_main_model()
|
||||||
if main_provider and main_provider not in ("auto", ""):
|
if main_provider and main_provider not in {"auto", ""}:
|
||||||
vision_model = _PROVIDER_VISION_MODELS.get(main_provider, main_model)
|
vision_model = _PROVIDER_VISION_MODELS.get(main_provider, main_model)
|
||||||
if main_provider == "nous":
|
if main_provider == "nous":
|
||||||
sync_client, default_model = _resolve_strict_vision_backend(
|
sync_client, default_model = _resolve_strict_vision_backend(
|
||||||
|
|
@ -4146,7 +4146,7 @@ def call_llm(
|
||||||
# credentials were found, fail fast instead of silently routing
|
# credentials were found, fail fast instead of silently routing
|
||||||
# through OpenRouter (which causes confusing 404s).
|
# through OpenRouter (which causes confusing 404s).
|
||||||
_explicit = (resolved_provider or "").strip().lower()
|
_explicit = (resolved_provider or "").strip().lower()
|
||||||
if _explicit and _explicit not in ("auto", "openrouter", "custom"):
|
if _explicit and _explicit not in {"auto", "openrouter", "custom"}:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
f"Provider '{_explicit}' is set in config.yaml but no API key "
|
f"Provider '{_explicit}' is set in config.yaml but no API key "
|
||||||
f"was found. Set the {_explicit.upper()}_API_KEY environment "
|
f"was found. Set the {_explicit.upper()}_API_KEY environment "
|
||||||
|
|
@ -4276,7 +4276,7 @@ def call_llm(
|
||||||
|
|
||||||
# ── Auth refresh retry ───────────────────────────────────────
|
# ── Auth refresh retry ───────────────────────────────────────
|
||||||
if (_is_auth_error(first_err)
|
if (_is_auth_error(first_err)
|
||||||
and resolved_provider not in ("auto", "", None)
|
and resolved_provider not in {"auto", "", None}
|
||||||
and not client_is_nous):
|
and not client_is_nous):
|
||||||
if _refresh_provider_credentials(resolved_provider):
|
if _refresh_provider_credentials(resolved_provider):
|
||||||
logger.info(
|
logger.info(
|
||||||
|
|
@ -4359,7 +4359,7 @@ def call_llm(
|
||||||
# Only try alternative providers when the user didn't explicitly
|
# Only try alternative providers when the user didn't explicitly
|
||||||
# configure this task's provider. Explicit provider = hard constraint;
|
# configure this task's provider. Explicit provider = hard constraint;
|
||||||
# auto (the default) = best-effort fallback chain. (#7559)
|
# auto (the default) = best-effort fallback chain. (#7559)
|
||||||
is_auto = resolved_provider in ("auto", "", None)
|
is_auto = resolved_provider in {"auto", "", None}
|
||||||
if should_fallback and is_auto:
|
if should_fallback and is_auto:
|
||||||
if _is_payment_error(first_err):
|
if _is_payment_error(first_err):
|
||||||
reason = "payment error"
|
reason = "payment error"
|
||||||
|
|
@ -4515,7 +4515,7 @@ async def async_call_llm(
|
||||||
)
|
)
|
||||||
if client is None:
|
if client is None:
|
||||||
_explicit = (resolved_provider or "").strip().lower()
|
_explicit = (resolved_provider or "").strip().lower()
|
||||||
if _explicit and _explicit not in ("auto", "openrouter", "custom"):
|
if _explicit and _explicit not in {"auto", "openrouter", "custom"}:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
f"Provider '{_explicit}' is set in config.yaml but no API key "
|
f"Provider '{_explicit}' is set in config.yaml but no API key "
|
||||||
f"was found. Set the {_explicit.upper()}_API_KEY environment "
|
f"was found. Set the {_explicit.upper()}_API_KEY environment "
|
||||||
|
|
@ -4626,7 +4626,7 @@ async def async_call_llm(
|
||||||
|
|
||||||
# ── Auth refresh retry (mirrors sync call_llm) ───────────────
|
# ── Auth refresh retry (mirrors sync call_llm) ───────────────
|
||||||
if (_is_auth_error(first_err)
|
if (_is_auth_error(first_err)
|
||||||
and resolved_provider not in ("auto", "", None)
|
and resolved_provider not in {"auto", "", None}
|
||||||
and not client_is_nous):
|
and not client_is_nous):
|
||||||
if _refresh_provider_credentials(resolved_provider):
|
if _refresh_provider_credentials(resolved_provider):
|
||||||
logger.info(
|
logger.info(
|
||||||
|
|
@ -4688,7 +4688,7 @@ async def async_call_llm(
|
||||||
or _is_connection_error(first_err)
|
or _is_connection_error(first_err)
|
||||||
or _is_rate_limit_error(first_err)
|
or _is_rate_limit_error(first_err)
|
||||||
)
|
)
|
||||||
is_auto = resolved_provider in ("auto", "", None)
|
is_auto = resolved_provider in {"auto", "", None}
|
||||||
if should_fallback and is_auto:
|
if should_fallback and is_auto:
|
||||||
if _is_payment_error(first_err):
|
if _is_payment_error(first_err):
|
||||||
reason = "payment error"
|
reason = "payment error"
|
||||||
|
|
|
||||||
|
|
@ -167,7 +167,7 @@ def _strip_image_parts_from_parts(parts: Any) -> Any:
|
||||||
out.append(part)
|
out.append(part)
|
||||||
continue
|
continue
|
||||||
ptype = part.get("type")
|
ptype = part.get("type")
|
||||||
if ptype in ("image", "image_url", "input_image"):
|
if ptype in {"image", "image_url", "input_image"}:
|
||||||
had_image = True
|
had_image = True
|
||||||
out.append({"type": "text", "text": "[screenshot removed to save context]"})
|
out.append({"type": "text", "text": "[screenshot removed to save context]"})
|
||||||
else:
|
else:
|
||||||
|
|
@ -274,8 +274,8 @@ def _summarize_tool_result(tool_name: str, tool_args: str, tool_content: str) ->
|
||||||
mode = args.get("mode", "replace")
|
mode = args.get("mode", "replace")
|
||||||
return f"[patch] {mode} in {path} ({content_len:,} chars result)"
|
return f"[patch] {mode} in {path} ({content_len:,} chars result)"
|
||||||
|
|
||||||
if tool_name in ("browser_navigate", "browser_click", "browser_snapshot",
|
if tool_name in {"browser_navigate", "browser_click", "browser_snapshot",
|
||||||
"browser_type", "browser_scroll", "browser_vision"):
|
"browser_type", "browser_scroll", "browser_vision"}:
|
||||||
url = args.get("url", "")
|
url = args.get("url", "")
|
||||||
ref = args.get("ref", "")
|
ref = args.get("ref", "")
|
||||||
detail = f" {url}" if url else (f" ref={ref}" if ref else "")
|
detail = f" {url}" if url else (f" ref={ref}" if ref else "")
|
||||||
|
|
@ -304,7 +304,7 @@ def _summarize_tool_result(tool_name: str, tool_args: str, tool_content: str) ->
|
||||||
code_preview += "..."
|
code_preview += "..."
|
||||||
return f"[execute_code] `{code_preview}` ({line_count} lines output)"
|
return f"[execute_code] `{code_preview}` ({line_count} lines output)"
|
||||||
|
|
||||||
if tool_name in ("skill_view", "skills_list", "skill_manage"):
|
if tool_name in {"skill_view", "skills_list", "skill_manage"}:
|
||||||
name = args.get("name", "?")
|
name = args.get("name", "?")
|
||||||
return f"[{tool_name}] name={name} ({content_len:,} chars)"
|
return f"[{tool_name}] name={name} ({content_len:,} chars)"
|
||||||
|
|
||||||
|
|
@ -979,13 +979,13 @@ The user has requested that this compaction PRIORITISE preserving all informatio
|
||||||
_status = getattr(e, "status_code", None) or getattr(getattr(e, "response", None), "status_code", None)
|
_status = getattr(e, "status_code", None) or getattr(getattr(e, "response", None), "status_code", None)
|
||||||
_err_str = str(e).lower()
|
_err_str = str(e).lower()
|
||||||
_is_model_not_found = (
|
_is_model_not_found = (
|
||||||
_status in (404, 503)
|
_status in {404, 503}
|
||||||
or "model_not_found" in _err_str
|
or "model_not_found" in _err_str
|
||||||
or "does not exist" in _err_str
|
or "does not exist" in _err_str
|
||||||
or "no available channel" in _err_str
|
or "no available channel" in _err_str
|
||||||
)
|
)
|
||||||
_is_timeout = (
|
_is_timeout = (
|
||||||
_status in (408, 429, 502, 504)
|
_status in {408, 429, 502, 504}
|
||||||
or "timeout" in _err_str
|
or "timeout" in _err_str
|
||||||
)
|
)
|
||||||
# Non-JSON / malformed-body responses from misconfigured providers
|
# Non-JSON / malformed-body responses from misconfigured providers
|
||||||
|
|
@ -1479,7 +1479,7 @@ The user has requested that this compaction PRIORITISE preserving all informatio
|
||||||
first_tail_role = messages[compress_end].get("role", "user") if compress_end < n_messages else "user"
|
first_tail_role = messages[compress_end].get("role", "user") if compress_end < n_messages else "user"
|
||||||
# Pick a role that avoids consecutive same-role with both neighbors.
|
# Pick a role that avoids consecutive same-role with both neighbors.
|
||||||
# Priority: avoid colliding with head (already committed), then tail.
|
# Priority: avoid colliding with head (already committed), then tail.
|
||||||
if last_head_role in ("assistant", "tool"):
|
if last_head_role in {"assistant", "tool"}:
|
||||||
summary_role = "user"
|
summary_role = "user"
|
||||||
else:
|
else:
|
||||||
summary_role = "assistant"
|
summary_role = "assistant"
|
||||||
|
|
|
||||||
|
|
@ -149,7 +149,7 @@ class PooledCredential:
|
||||||
}
|
}
|
||||||
result: Dict[str, Any] = {}
|
result: Dict[str, Any] = {}
|
||||||
for field_def in fields(self):
|
for field_def in fields(self):
|
||||||
if field_def.name in ("provider", "extra"):
|
if field_def.name in {"provider", "extra"}:
|
||||||
continue
|
continue
|
||||||
value = getattr(self, field_def.name)
|
value = getattr(self, field_def.name)
|
||||||
if value is not None or field_def.name in _ALWAYS_EMIT:
|
if value is not None or field_def.name in _ALWAYS_EMIT:
|
||||||
|
|
|
||||||
|
|
@ -83,7 +83,7 @@ class ClassifiedError:
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_auth(self) -> bool:
|
def is_auth(self) -> bool:
|
||||||
return self.reason in (FailoverReason.auth, FailoverReason.auth_permanent)
|
return self.reason in {FailoverReason.auth, FailoverReason.auth_permanent}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -688,10 +688,10 @@ def _classify_by_status(
|
||||||
result_fn=result_fn,
|
result_fn=result_fn,
|
||||||
)
|
)
|
||||||
|
|
||||||
if status_code in (500, 502):
|
if status_code in {500, 502}:
|
||||||
return result_fn(FailoverReason.server_error, retryable=True)
|
return result_fn(FailoverReason.server_error, retryable=True)
|
||||||
|
|
||||||
if status_code in (503, 529):
|
if status_code in {503, 529}:
|
||||||
return result_fn(FailoverReason.overloaded, retryable=True)
|
return result_fn(FailoverReason.overloaded, retryable=True)
|
||||||
|
|
||||||
# Other 4xx — non-retryable
|
# Other 4xx — non-retryable
|
||||||
|
|
@ -810,7 +810,7 @@ def _classify_400(
|
||||||
# Responses API (and some providers) use flat body: {"message": "..."}
|
# Responses API (and some providers) use flat body: {"message": "..."}
|
||||||
if not err_body_msg:
|
if not err_body_msg:
|
||||||
err_body_msg = str(body.get("message") or "").strip().lower()
|
err_body_msg = str(body.get("message") or "").strip().lower()
|
||||||
is_generic = len(err_body_msg) < 30 or err_body_msg in ("error", "")
|
is_generic = len(err_body_msg) < 30 or err_body_msg in {"error", ""}
|
||||||
# Absolute token/message-count thresholds are only a proxy for smaller
|
# Absolute token/message-count thresholds are only a proxy for smaller
|
||||||
# context windows. Large-context sessions can have many messages while
|
# context windows. Large-context sessions can have many messages while
|
||||||
# still being far below their actual token budget.
|
# still being far below their actual token budget.
|
||||||
|
|
@ -841,14 +841,14 @@ def _classify_by_error_code(
|
||||||
"""Classify by structured error codes from the response body."""
|
"""Classify by structured error codes from the response body."""
|
||||||
code_lower = error_code.lower()
|
code_lower = error_code.lower()
|
||||||
|
|
||||||
if code_lower in ("resource_exhausted", "throttled", "rate_limit_exceeded"):
|
if code_lower in {"resource_exhausted", "throttled", "rate_limit_exceeded"}:
|
||||||
return result_fn(
|
return result_fn(
|
||||||
FailoverReason.rate_limit,
|
FailoverReason.rate_limit,
|
||||||
retryable=True,
|
retryable=True,
|
||||||
should_rotate_credential=True,
|
should_rotate_credential=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
if code_lower in ("insufficient_quota", "billing_not_active", "payment_required"):
|
if code_lower in {"insufficient_quota", "billing_not_active", "payment_required"}:
|
||||||
return result_fn(
|
return result_fn(
|
||||||
FailoverReason.billing,
|
FailoverReason.billing,
|
||||||
retryable=False,
|
retryable=False,
|
||||||
|
|
@ -856,14 +856,14 @@ def _classify_by_error_code(
|
||||||
should_fallback=True,
|
should_fallback=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
if code_lower in ("model_not_found", "model_not_available", "invalid_model"):
|
if code_lower in {"model_not_found", "model_not_available", "invalid_model"}:
|
||||||
return result_fn(
|
return result_fn(
|
||||||
FailoverReason.model_not_found,
|
FailoverReason.model_not_found,
|
||||||
retryable=False,
|
retryable=False,
|
||||||
should_fallback=True,
|
should_fallback=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
if code_lower in ("context_length_exceeded", "max_tokens_exceeded"):
|
if code_lower in {"context_length_exceeded", "max_tokens_exceeded"}:
|
||||||
return result_fn(
|
return result_fn(
|
||||||
FailoverReason.context_overflow,
|
FailoverReason.context_overflow,
|
||||||
retryable=True,
|
retryable=True,
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,7 @@ def _coerce_content_to_text(content: Any) -> str:
|
||||||
if p.get("type") == "text" and isinstance(p.get("text"), str):
|
if p.get("type") == "text" and isinstance(p.get("text"), str):
|
||||||
pieces.append(p["text"])
|
pieces.append(p["text"])
|
||||||
# Multimodal (image_url, etc.) — stub for now; log and skip
|
# Multimodal (image_url, etc.) — stub for now; log and skip
|
||||||
elif p.get("type") in ("image_url", "input_audio"):
|
elif p.get("type") in {"image_url", "input_audio"}:
|
||||||
logger.debug("Dropping multimodal part (not yet supported): %s", p.get("type"))
|
logger.debug("Dropping multimodal part (not yet supported): %s", p.get("type"))
|
||||||
return "\n".join(pieces)
|
return "\n".join(pieces)
|
||||||
return str(content)
|
return str(content)
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,7 @@ def _explicit_aux_vision_override(cfg: Optional[Dict[str, Any]]) -> bool:
|
||||||
base_url = str(vision.get("base_url") or "").strip()
|
base_url = str(vision.get("base_url") or "").strip()
|
||||||
|
|
||||||
# "auto" / "" / blank = not explicit
|
# "auto" / "" / blank = not explicit
|
||||||
if provider in ("", "auto") and not model and not base_url:
|
if provider in {"", "auto"} and not model and not base_url:
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
@ -163,7 +163,7 @@ def _sniff_mime_from_bytes(raw: bytes) -> Optional[str]:
|
||||||
if raw.startswith(b"\xff\xd8\xff"):
|
if raw.startswith(b"\xff\xd8\xff"):
|
||||||
return "image/jpeg"
|
return "image/jpeg"
|
||||||
# GIF87a / GIF89a
|
# GIF87a / GIF89a
|
||||||
if raw[:6] in (b"GIF87a", b"GIF89a"):
|
if raw[:6] in {b"GIF87a", b"GIF89a"}:
|
||||||
return "image/gif"
|
return "image/gif"
|
||||||
# WEBP: "RIFF" .... "WEBP"
|
# WEBP: "RIFF" .... "WEBP"
|
||||||
if len(raw) >= 12 and raw[:4] == b"RIFF" and raw[8:12] == b"WEBP":
|
if len(raw) >= 12 and raw[:4] == b"RIFF" and raw[8:12] == b"WEBP":
|
||||||
|
|
@ -172,9 +172,9 @@ def _sniff_mime_from_bytes(raw: bytes) -> Optional[str]:
|
||||||
if raw.startswith(b"BM"):
|
if raw.startswith(b"BM"):
|
||||||
return "image/bmp"
|
return "image/bmp"
|
||||||
# HEIC/HEIF: ftypheic / ftypheix / ftypmif1 / ftypmsf1 etc.
|
# HEIC/HEIF: ftypheic / ftypheix / ftypmif1 / ftypmsf1 etc.
|
||||||
if len(raw) >= 12 and raw[4:8] == b"ftyp" and raw[8:12] in (
|
if len(raw) >= 12 and raw[4:8] == b"ftyp" and raw[8:12] in {
|
||||||
b"heic", b"heix", b"hevc", b"hevx", b"mif1", b"msf1", b"heim", b"heis",
|
b"heic", b"heix", b"hevc", b"hevx", b"mif1", b"msf1", b"heim", b"heis",
|
||||||
):
|
}:
|
||||||
return "image/heic"
|
return "image/heic"
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -470,11 +470,11 @@ class MemoryManager:
|
||||||
|
|
||||||
accepted = [
|
accepted = [
|
||||||
p for p in params
|
p for p in params
|
||||||
if p.kind in (
|
if p.kind in {
|
||||||
inspect.Parameter.POSITIONAL_ONLY,
|
inspect.Parameter.POSITIONAL_ONLY,
|
||||||
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
||||||
inspect.Parameter.KEYWORD_ONLY,
|
inspect.Parameter.KEYWORD_ONLY,
|
||||||
)
|
}
|
||||||
]
|
]
|
||||||
if len(accepted) >= 4:
|
if len(accepted) >= 4:
|
||||||
return "positional"
|
return "positional"
|
||||||
|
|
|
||||||
|
|
@ -571,7 +571,7 @@ def _extract_pricing(payload: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
pricing: Dict[str, Any] = {}
|
pricing: Dict[str, Any] = {}
|
||||||
for target, aliases in alias_map.items():
|
for target, aliases in alias_map.items():
|
||||||
for alias in aliases:
|
for alias in aliases:
|
||||||
if alias in normalized and normalized[alias] not in (None, ""):
|
if alias in normalized and normalized[alias] not in {None, ""}:
|
||||||
pricing[target] = normalized[alias]
|
pricing[target] = normalized[alias]
|
||||||
break
|
break
|
||||||
if pricing:
|
if pricing:
|
||||||
|
|
@ -1423,7 +1423,7 @@ def get_model_context_length(
|
||||||
# (e.g. claude-opus-4.6 is 1M on Anthropic but 128K on GitHub Copilot).
|
# (e.g. claude-opus-4.6 is 1M on Anthropic but 128K on GitHub Copilot).
|
||||||
# If provider is generic (openrouter/custom/empty), try to infer from URL.
|
# If provider is generic (openrouter/custom/empty), try to infer from URL.
|
||||||
effective_provider = provider
|
effective_provider = provider
|
||||||
if not effective_provider or effective_provider in ("openrouter", "custom"):
|
if not effective_provider or effective_provider in {"openrouter", "custom"}:
|
||||||
if base_url:
|
if base_url:
|
||||||
inferred = _infer_provider_from_url(base_url)
|
inferred = _infer_provider_from_url(base_url)
|
||||||
if inferred:
|
if inferred:
|
||||||
|
|
@ -1433,7 +1433,7 @@ def get_model_context_length(
|
||||||
# This catches account-specific models (e.g. claude-opus-4.6-1m) that
|
# This catches account-specific models (e.g. claude-opus-4.6-1m) that
|
||||||
# don't exist in models.dev. For models that ARE in models.dev, this
|
# don't exist in models.dev. For models that ARE in models.dev, this
|
||||||
# returns the provider-enforced limit which is what users can actually use.
|
# returns the provider-enforced limit which is what users can actually use.
|
||||||
if effective_provider in ("copilot", "copilot-acp", "github-copilot"):
|
if effective_provider in {"copilot", "copilot-acp", "github-copilot"}:
|
||||||
try:
|
try:
|
||||||
from hermes_cli.models import get_copilot_model_context
|
from hermes_cli.models import get_copilot_model_context
|
||||||
ctx = get_copilot_model_context(model, api_key=api_key)
|
ctx = get_copilot_model_context(model, api_key=api_key)
|
||||||
|
|
@ -1533,7 +1533,7 @@ def _count_image_tokens(msg: Dict[str, Any], cost_per_image: int) -> int:
|
||||||
if not isinstance(part, dict):
|
if not isinstance(part, dict):
|
||||||
continue
|
continue
|
||||||
ptype = part.get("type")
|
ptype = part.get("type")
|
||||||
if ptype in ("image", "image_url", "input_image"):
|
if ptype in {"image", "image_url", "input_image"}:
|
||||||
count += 1
|
count += 1
|
||||||
stashed = msg.get("_anthropic_content_blocks") if isinstance(msg, dict) else None
|
stashed = msg.get("_anthropic_content_blocks") if isinstance(msg, dict) else None
|
||||||
if isinstance(stashed, list):
|
if isinstance(stashed, list):
|
||||||
|
|
@ -1545,7 +1545,7 @@ def _count_image_tokens(msg: Dict[str, Any], cost_per_image: int) -> int:
|
||||||
inner = content.get("content")
|
inner = content.get("content")
|
||||||
if isinstance(inner, list):
|
if isinstance(inner, list):
|
||||||
for part in inner:
|
for part in inner:
|
||||||
if isinstance(part, dict) and part.get("type") in ("image", "image_url"):
|
if isinstance(part, dict) and part.get("type") in {"image", "image_url"}:
|
||||||
count += 1
|
count += 1
|
||||||
return count * cost_per_image
|
return count * cost_per_image
|
||||||
|
|
||||||
|
|
@ -1567,7 +1567,7 @@ def _estimate_message_chars(msg: Dict[str, Any]) -> int:
|
||||||
cleaned = []
|
cleaned = []
|
||||||
for part in v:
|
for part in v:
|
||||||
if isinstance(part, dict):
|
if isinstance(part, dict):
|
||||||
if part.get("type") in ("image", "image_url", "input_image"):
|
if part.get("type") in {"image", "image_url", "input_image"}:
|
||||||
cleaned.append({"type": part.get("type"), "image": "[stripped]"})
|
cleaned.append({"type": part.get("type"), "image": "[stripped]"})
|
||||||
else:
|
else:
|
||||||
cleaned.append(part)
|
cleaned.append(part)
|
||||||
|
|
|
||||||
|
|
@ -122,7 +122,7 @@ def _repair_schema(node: Any, is_schema: bool = True) -> Any:
|
||||||
# empty, drop it entirely.
|
# empty, drop it entirely.
|
||||||
if "enum" in repaired and isinstance(repaired["enum"], list):
|
if "enum" in repaired and isinstance(repaired["enum"], list):
|
||||||
node_type = repaired.get("type")
|
node_type = repaired.get("type")
|
||||||
if node_type in ("string", "integer", "number", "boolean"):
|
if node_type in {"string", "integer", "number", "boolean"}:
|
||||||
cleaned = [v for v in repaired["enum"]
|
cleaned = [v for v in repaired["enum"]
|
||||||
if v is not None and v != ""]
|
if v is not None and v != ""]
|
||||||
if cleaned:
|
if cleaned:
|
||||||
|
|
@ -135,7 +135,7 @@ def _repair_schema(node: Any, is_schema: bool = True) -> Any:
|
||||||
|
|
||||||
def _fill_missing_type(node: Dict[str, Any]) -> Dict[str, Any]:
|
def _fill_missing_type(node: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""Infer a reasonable ``type`` if this schema node has none."""
|
"""Infer a reasonable ``type`` if this schema node has none."""
|
||||||
if "type" in node and node["type"] not in (None, ""):
|
if "type" in node and node["type"] not in {None, ""}:
|
||||||
return node
|
return node
|
||||||
|
|
||||||
# Heuristic: presence of ``properties`` → object, ``items`` → array, ``enum``
|
# Heuristic: presence of ``properties`` → object, ``items`` → array, ``enum``
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,7 @@ _SENSITIVE_BODY_KEYS = frozenset({
|
||||||
# cli.py) or `HERMES_REDACT_SECRETS=false` in ~/.hermes/.env. An opt-out
|
# cli.py) or `HERMES_REDACT_SECRETS=false` in ~/.hermes/.env. An opt-out
|
||||||
# warning is logged at gateway and CLI startup so operators see the
|
# warning is logged at gateway and CLI startup so operators see the
|
||||||
# downgrade — see `_log_redaction_status()` in gateway/run.py and cli.py.
|
# downgrade — see `_log_redaction_status()` in gateway/run.py and cli.py.
|
||||||
_REDACT_ENABLED = os.getenv("HERMES_REDACT_SECRETS", "true").lower() in ("1", "true", "yes", "on")
|
_REDACT_ENABLED = os.getenv("HERMES_REDACT_SECRETS", "true").lower() in {"1", "true", "yes", "on"}
|
||||||
|
|
||||||
# Known API key prefixes -- match the prefix + contiguous token chars
|
# Known API key prefixes -- match the prefix + contiguous token chars
|
||||||
_PREFIX_PATTERNS = [
|
_PREFIX_PATTERNS = [
|
||||||
|
|
|
||||||
|
|
@ -312,7 +312,7 @@ def _parse_single_entry(
|
||||||
)
|
)
|
||||||
matcher = None
|
matcher = None
|
||||||
|
|
||||||
if matcher is not None and event not in ("pre_tool_call", "post_tool_call"):
|
if matcher is not None and event not in {"pre_tool_call", "post_tool_call"}:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"hooks.%s[%d].matcher=%r will be ignored at runtime — the "
|
"hooks.%s[%d].matcher=%r will be ignored at runtime — the "
|
||||||
"matcher field is only honored for pre_tool_call / "
|
"matcher field is only honored for pre_tool_call / "
|
||||||
|
|
@ -423,7 +423,7 @@ def _make_callback(spec: ShellHookSpec) -> Callable[..., Optional[Dict[str, Any]
|
||||||
|
|
||||||
def _callback(**kwargs: Any) -> Optional[Dict[str, Any]]:
|
def _callback(**kwargs: Any) -> Optional[Dict[str, Any]]:
|
||||||
# Matcher gate — only meaningful for tool-scoped events.
|
# Matcher gate — only meaningful for tool-scoped events.
|
||||||
if spec.event in ("pre_tool_call", "post_tool_call"):
|
if spec.event in {"pre_tool_call", "post_tool_call"}:
|
||||||
if not spec.matches_tool(kwargs.get("tool_name")):
|
if not spec.matches_tool(kwargs.get("tool_name")):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
@ -658,7 +658,7 @@ def _prompt_and_record(
|
||||||
print() # keep the terminal tidy after ^C
|
print() # keep the terminal tidy after ^C
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if answer in ("y", "yes"):
|
if answer in {"y", "yes"}:
|
||||||
_record_approval(event, command)
|
_record_approval(event, command)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
@ -752,13 +752,13 @@ def _resolve_effective_accept(
|
||||||
if accept_hooks_arg:
|
if accept_hooks_arg:
|
||||||
return True
|
return True
|
||||||
env = os.environ.get("HERMES_ACCEPT_HOOKS", "").strip().lower()
|
env = os.environ.get("HERMES_ACCEPT_HOOKS", "").strip().lower()
|
||||||
if env in ("1", "true", "yes", "on"):
|
if env in {"1", "true", "yes", "on"}:
|
||||||
return True
|
return True
|
||||||
cfg_val = cfg.get("hooks_auto_accept", False)
|
cfg_val = cfg.get("hooks_auto_accept", False)
|
||||||
if isinstance(cfg_val, bool):
|
if isinstance(cfg_val, bool):
|
||||||
return cfg_val
|
return cfg_val
|
||||||
if isinstance(cfg_val, str):
|
if isinstance(cfg_val, str):
|
||||||
return cfg_val.strip().lower() in ("1", "true", "yes", "on")
|
return cfg_val.strip().lower() in {"1", "true", "yes", "on"}
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -261,7 +261,7 @@ def scan_skill_commands() -> Dict[str, Dict[str, Any]]:
|
||||||
|
|
||||||
for scan_dir in dirs_to_scan:
|
for scan_dir in dirs_to_scan:
|
||||||
for skill_md in iter_skill_index_files(scan_dir, "SKILL.md"):
|
for skill_md in iter_skill_index_files(scan_dir, "SKILL.md"):
|
||||||
if any(part in ('.git', '.github', '.hub', '.archive') for part in skill_md.parts):
|
if any(part in {'.git', '.github', '.hub', '.archive'} for part in skill_md.parts):
|
||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
content = skill_md.read_text(encoding='utf-8')
|
content = skill_md.read_text(encoding='utf-8')
|
||||||
|
|
|
||||||
|
|
@ -279,7 +279,7 @@ class ChatCompletionsTransport(ProviderTransport):
|
||||||
_kimi_effort = "medium"
|
_kimi_effort = "medium"
|
||||||
if reasoning_config and isinstance(reasoning_config, dict):
|
if reasoning_config and isinstance(reasoning_config, dict):
|
||||||
_e = (reasoning_config.get("effort") or "").strip().lower()
|
_e = (reasoning_config.get("effort") or "").strip().lower()
|
||||||
if _e in ("low", "medium", "high"):
|
if _e in {"low", "medium", "high"}:
|
||||||
_kimi_effort = _e
|
_kimi_effort = _e
|
||||||
api_kwargs["reasoning_effort"] = _kimi_effort
|
api_kwargs["reasoning_effort"] = _kimi_effort
|
||||||
|
|
||||||
|
|
@ -294,7 +294,7 @@ class ChatCompletionsTransport(ProviderTransport):
|
||||||
_tokenhub_effort = "high"
|
_tokenhub_effort = "high"
|
||||||
if reasoning_config and isinstance(reasoning_config, dict):
|
if reasoning_config and isinstance(reasoning_config, dict):
|
||||||
_e = (reasoning_config.get("effort") or "").strip().lower()
|
_e = (reasoning_config.get("effort") or "").strip().lower()
|
||||||
if _e in ("low", "medium", "high"):
|
if _e in {"low", "medium", "high"}:
|
||||||
_tokenhub_effort = _e
|
_tokenhub_effort = _e
|
||||||
api_kwargs["reasoning_effort"] = _tokenhub_effort
|
api_kwargs["reasoning_effort"] = _tokenhub_effort
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -795,7 +795,7 @@ class BatchRunner:
|
||||||
conversations = entry.get("conversations", [])
|
conversations = entry.get("conversations", [])
|
||||||
for msg in conversations:
|
for msg in conversations:
|
||||||
role = msg.get("role") or msg.get("from")
|
role = msg.get("role") or msg.get("from")
|
||||||
if role in ("user", "human"):
|
if role in {"user", "human"}:
|
||||||
prompt_text = (msg.get("content") or msg.get("value", "")).strip()
|
prompt_text = (msg.get("content") or msg.get("value", "")).strip()
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|
|
||||||
36
cli.py
36
cli.py
|
|
@ -1741,7 +1741,7 @@ def _detect_file_drop(user_input: str) -> "dict | None":
|
||||||
or stripped.startswith("./")
|
or stripped.startswith("./")
|
||||||
or stripped.startswith("../")
|
or stripped.startswith("../")
|
||||||
or stripped.startswith("file://")
|
or stripped.startswith("file://")
|
||||||
or (len(stripped) >= 3 and stripped[1] == ":" and stripped[2] in ("\\", "/") and stripped[0].isalpha())
|
or (len(stripped) >= 3 and stripped[1] == ":" and stripped[2] in {"\\", "/"} and stripped[0].isalpha())
|
||||||
or stripped.startswith('"/')
|
or stripped.startswith('"/')
|
||||||
or stripped.startswith('"~')
|
or stripped.startswith('"~')
|
||||||
or stripped.startswith("'/")
|
or stripped.startswith("'/")
|
||||||
|
|
@ -1750,7 +1750,7 @@ def _detect_file_drop(user_input: str) -> "dict | None":
|
||||||
or stripped.startswith('"../')
|
or stripped.startswith('"../')
|
||||||
or stripped.startswith("'./")
|
or stripped.startswith("'./")
|
||||||
or stripped.startswith("'../")
|
or stripped.startswith("'../")
|
||||||
or (len(stripped) >= 4 and stripped[0] in ("'", '"') and stripped[2] == ":" and stripped[3] in ("\\", "/") and stripped[1].isalpha())
|
or (len(stripped) >= 4 and stripped[0] in {"'", '"'} and stripped[2] == ":" and stripped[3] in {"\\", "/"} and stripped[1].isalpha())
|
||||||
)
|
)
|
||||||
if not starts_like_path:
|
if not starts_like_path:
|
||||||
return None
|
return None
|
||||||
|
|
@ -2487,7 +2487,7 @@ class HermesCLI:
|
||||||
_or_cfg = CLI_CONFIG.get("openrouter", {}) or {}
|
_or_cfg = CLI_CONFIG.get("openrouter", {}) or {}
|
||||||
_raw_score = _or_cfg.get("min_coding_score")
|
_raw_score = _or_cfg.get("min_coding_score")
|
||||||
self._openrouter_min_coding_score: Optional[float] = None
|
self._openrouter_min_coding_score: Optional[float] = None
|
||||||
if _raw_score not in (None, ""):
|
if _raw_score not in {None, ""}:
|
||||||
try:
|
try:
|
||||||
_f = float(_raw_score)
|
_f = float(_raw_score)
|
||||||
if 0.0 <= _f <= 1.0:
|
if 0.0 <= _f <= 1.0:
|
||||||
|
|
@ -4663,7 +4663,7 @@ class HermesCLI:
|
||||||
parts = command.split()
|
parts = command.split()
|
||||||
subcmd = parts[1].lower() if len(parts) > 1 else "list"
|
subcmd = parts[1].lower() if len(parts) > 1 else "list"
|
||||||
|
|
||||||
if subcmd in ("list", "ls"):
|
if subcmd in {"list", "ls"}:
|
||||||
snaps = list_quick_snapshots()
|
snaps = list_quick_snapshots()
|
||||||
if not snaps:
|
if not snaps:
|
||||||
print(" No state snapshots yet.")
|
print(" No state snapshots yet.")
|
||||||
|
|
@ -4691,7 +4691,7 @@ class HermesCLI:
|
||||||
else:
|
else:
|
||||||
print(" No state files found to snapshot.")
|
print(" No state files found to snapshot.")
|
||||||
|
|
||||||
elif subcmd in ("restore", "rewind"):
|
elif subcmd in {"restore", "rewind"}:
|
||||||
if len(parts) < 3:
|
if len(parts) < 3:
|
||||||
print(" Usage: /snapshot restore <snapshot-id>")
|
print(" Usage: /snapshot restore <snapshot-id>")
|
||||||
# Show hint with most recent snapshot
|
# Show hint with most recent snapshot
|
||||||
|
|
@ -5230,7 +5230,7 @@ class HermesCLI:
|
||||||
parts = cmd.split()
|
parts = cmd.split()
|
||||||
|
|
||||||
subcommand = parts[1] if len(parts) > 1 else ""
|
subcommand = parts[1] if len(parts) > 1 else ""
|
||||||
if subcommand not in ("list", "disable", "enable"):
|
if subcommand not in {"list", "disable", "enable"}:
|
||||||
self.show_tools()
|
self.show_tools()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
@ -6814,7 +6814,7 @@ class HermesCLI:
|
||||||
# Set personality
|
# Set personality
|
||||||
personality_name = parts[1].strip().lower()
|
personality_name = parts[1].strip().lower()
|
||||||
|
|
||||||
if personality_name in ("none", "default", "neutral"):
|
if personality_name in {"none", "default", "neutral"}:
|
||||||
self.system_prompt = ""
|
self.system_prompt = ""
|
||||||
self.agent = None # Force re-init
|
self.agent = None # Force re-init
|
||||||
if save_config_value("agent.system_prompt", ""):
|
if save_config_value("agent.system_prompt", ""):
|
||||||
|
|
@ -7222,7 +7222,7 @@ class HermesCLI:
|
||||||
_cmd_def = _resolve_cmd(_base_word)
|
_cmd_def = _resolve_cmd(_base_word)
|
||||||
canonical = _cmd_def.name if _cmd_def else _base_word
|
canonical = _cmd_def.name if _cmd_def else _base_word
|
||||||
|
|
||||||
if canonical in ("quit", "exit"):
|
if canonical in {"quit", "exit"}:
|
||||||
return False
|
return False
|
||||||
elif canonical == "help":
|
elif canonical == "help":
|
||||||
self.show_help()
|
self.show_help()
|
||||||
|
|
@ -8096,7 +8096,7 @@ class HermesCLI:
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
if lower in ("clear", "stop", "done"):
|
if lower in {"clear", "stop", "done"}:
|
||||||
had = mgr.has_goal()
|
had = mgr.has_goal()
|
||||||
mgr.clear()
|
mgr.clear()
|
||||||
if had:
|
if had:
|
||||||
|
|
@ -8186,7 +8186,7 @@ class HermesCLI:
|
||||||
parts = [
|
parts = [
|
||||||
p.get("text", "")
|
p.get("text", "")
|
||||||
for p in content
|
for p in content
|
||||||
if isinstance(p, dict) and p.get("type") in ("text", "output_text")
|
if isinstance(p, dict) and p.get("type") in {"text", "output_text"}
|
||||||
]
|
]
|
||||||
last_response = "\n".join(t for t in parts if t)
|
last_response = "\n".join(t for t in parts if t)
|
||||||
else:
|
else:
|
||||||
|
|
@ -8281,7 +8281,7 @@ class HermesCLI:
|
||||||
current = bool(footer_cfg.get("enabled", False))
|
current = bool(footer_cfg.get("enabled", False))
|
||||||
fields = footer_cfg.get("fields") or ["model", "context_pct", "cwd"]
|
fields = footer_cfg.get("fields") or ["model", "context_pct", "cwd"]
|
||||||
|
|
||||||
if arg in ("status", "?"):
|
if arg in {"status", "?"}:
|
||||||
state = "ON" if current else "OFF"
|
state = "ON" if current else "OFF"
|
||||||
_cprint(
|
_cprint(
|
||||||
f" {_Colors.BOLD}Runtime footer:{_Colors.RESET} {state}\n"
|
f" {_Colors.BOLD}Runtime footer:{_Colors.RESET} {state}\n"
|
||||||
|
|
@ -8289,9 +8289,9 @@ class HermesCLI:
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
if arg in ("on", "enable", "true", "1"):
|
if arg in {"on", "enable", "true", "1"}:
|
||||||
new_state = True
|
new_state = True
|
||||||
elif arg in ("off", "disable", "false", "0"):
|
elif arg in {"off", "disable", "false", "0"}:
|
||||||
new_state = False
|
new_state = False
|
||||||
elif arg == "":
|
elif arg == "":
|
||||||
new_state = not current
|
new_state = not current
|
||||||
|
|
@ -8384,7 +8384,7 @@ class HermesCLI:
|
||||||
arg = parts[1].strip().lower()
|
arg = parts[1].strip().lower()
|
||||||
|
|
||||||
# Display toggle
|
# Display toggle
|
||||||
if arg in ("show", "on"):
|
if arg in {"show", "on"}:
|
||||||
self.show_reasoning = True
|
self.show_reasoning = True
|
||||||
if self.agent:
|
if self.agent:
|
||||||
self.agent.reasoning_callback = self._current_reasoning_callback()
|
self.agent.reasoning_callback = self._current_reasoning_callback()
|
||||||
|
|
@ -8392,7 +8392,7 @@ class HermesCLI:
|
||||||
_cprint(f" {_ACCENT}✓ Reasoning display: ON (saved){_RST}")
|
_cprint(f" {_ACCENT}✓ Reasoning display: ON (saved){_RST}")
|
||||||
_cprint(f" {_DIM} Model thinking will be shown during and after each response.{_RST}")
|
_cprint(f" {_DIM} Model thinking will be shown during and after each response.{_RST}")
|
||||||
return
|
return
|
||||||
if arg in ("hide", "off"):
|
if arg in {"hide", "off"}:
|
||||||
self.show_reasoning = False
|
self.show_reasoning = False
|
||||||
if self.agent:
|
if self.agent:
|
||||||
self.agent.reasoning_callback = self._current_reasoning_callback()
|
self.agent.reasoning_callback = self._current_reasoning_callback()
|
||||||
|
|
@ -9154,7 +9154,7 @@ class HermesCLI:
|
||||||
if event_type == "tool.completed":
|
if event_type == "tool.completed":
|
||||||
self._tool_start_time = 0.0
|
self._tool_start_time = 0.0
|
||||||
# Print stacked scrollback line for "all" / "new" modes
|
# Print stacked scrollback line for "all" / "new" modes
|
||||||
if function_name and self.tool_progress_mode in ("all", "new"):
|
if function_name and self.tool_progress_mode in {"all", "new"}:
|
||||||
duration = kwargs.get("duration", 0.0)
|
duration = kwargs.get("duration", 0.0)
|
||||||
is_error = kwargs.get("is_error", False)
|
is_error = kwargs.get("is_error", False)
|
||||||
# Pop stored args from tool.started for this function
|
# Pop stored args from tool.started for this function
|
||||||
|
|
@ -10806,7 +10806,7 @@ class HermesCLI:
|
||||||
try:
|
try:
|
||||||
from hermes_cli.profiles import get_active_profile_name
|
from hermes_cli.profiles import get_active_profile_name
|
||||||
profile = get_active_profile_name()
|
profile = get_active_profile_name()
|
||||||
if profile not in ("default", "custom"):
|
if profile not in {"default", "custom"}:
|
||||||
symbol = f"{profile} {symbol}"
|
symbol = f"{profile} {symbol}"
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
@ -11010,7 +11010,7 @@ class HermesCLI:
|
||||||
# see that they're running without the safety net.
|
# see that they're running without the safety net.
|
||||||
try:
|
try:
|
||||||
_redact_raw = os.getenv("HERMES_REDACT_SECRETS", "true")
|
_redact_raw = os.getenv("HERMES_REDACT_SECRETS", "true")
|
||||||
if _redact_raw.lower() not in ("1", "true", "yes", "on"):
|
if _redact_raw.lower() not in {"1", "true", "yes", "on"}:
|
||||||
self._console_print(
|
self._console_print(
|
||||||
"[bold red]⚠ Secret redaction is DISABLED[/] "
|
"[bold red]⚠ Secret redaction is DISABLED[/] "
|
||||||
f"(HERMES_REDACT_SECRETS={_redact_raw}). "
|
f"(HERMES_REDACT_SECRETS={_redact_raw}). "
|
||||||
|
|
|
||||||
10
cron/jobs.py
10
cron/jobs.py
|
|
@ -664,7 +664,7 @@ def update_job(job_id: str, updates: Dict[str, Any]) -> Optional[Dict[str, Any]]
|
||||||
# None both mean "clear the field" (restore old behaviour).
|
# None both mean "clear the field" (restore old behaviour).
|
||||||
if "workdir" in updates:
|
if "workdir" in updates:
|
||||||
_wd = updates["workdir"]
|
_wd = updates["workdir"]
|
||||||
if _wd in (None, "", False):
|
if _wd in {None, "", False}:
|
||||||
updates["workdir"] = None
|
updates["workdir"] = None
|
||||||
else:
|
else:
|
||||||
updates["workdir"] = _normalize_workdir(_wd)
|
updates["workdir"] = _normalize_workdir(_wd)
|
||||||
|
|
@ -811,7 +811,7 @@ def mark_job_run(job_id: str, success: bool, error: Optional[str] = None,
|
||||||
# schedule quietly goes off. See issue #16265.
|
# schedule quietly goes off. See issue #16265.
|
||||||
if job["next_run_at"] is None:
|
if job["next_run_at"] is None:
|
||||||
kind = job.get("schedule", {}).get("kind")
|
kind = job.get("schedule", {}).get("kind")
|
||||||
if kind in ("cron", "interval"):
|
if kind in {"cron", "interval"}:
|
||||||
job["state"] = "error"
|
job["state"] = "error"
|
||||||
if not job.get("last_error"):
|
if not job.get("last_error"):
|
||||||
job["last_error"] = (
|
job["last_error"] = (
|
||||||
|
|
@ -855,7 +855,7 @@ def advance_next_run(job_id: str) -> bool:
|
||||||
for job in jobs:
|
for job in jobs:
|
||||||
if job["id"] == job_id:
|
if job["id"] == job_id:
|
||||||
kind = job.get("schedule", {}).get("kind")
|
kind = job.get("schedule", {}).get("kind")
|
||||||
if kind not in ("cron", "interval"):
|
if kind not in {"cron", "interval"}:
|
||||||
return False
|
return False
|
||||||
now = _hermes_now().isoformat()
|
now = _hermes_now().isoformat()
|
||||||
new_next = compute_next_run(job["schedule"], now)
|
new_next = compute_next_run(job["schedule"], now)
|
||||||
|
|
@ -909,7 +909,7 @@ def _get_due_jobs_locked() -> List[Dict[str, Any]]:
|
||||||
# next_run_at unset. Without this branch, such jobs are
|
# next_run_at unset. Without this branch, such jobs are
|
||||||
# silently skipped forever; recompute next_run_at from the
|
# silently skipped forever; recompute next_run_at from the
|
||||||
# schedule so they pick up at their next scheduled tick.
|
# schedule so they pick up at their next scheduled tick.
|
||||||
if not recovered_next and kind in ("cron", "interval"):
|
if not recovered_next and kind in {"cron", "interval"}:
|
||||||
recovered_next = compute_next_run(schedule, now.isoformat())
|
recovered_next = compute_next_run(schedule, now.isoformat())
|
||||||
if recovered_next:
|
if recovered_next:
|
||||||
recovery_kind = kind
|
recovery_kind = kind
|
||||||
|
|
@ -940,7 +940,7 @@ def _get_due_jobs_locked() -> List[Dict[str, Any]]:
|
||||||
# (gateway was down and missed the window). Fast-forward to
|
# (gateway was down and missed the window). Fast-forward to
|
||||||
# the next future occurrence instead of firing a stale run.
|
# the next future occurrence instead of firing a stale run.
|
||||||
grace = _compute_grace_seconds(schedule)
|
grace = _compute_grace_seconds(schedule)
|
||||||
if kind in ("cron", "interval") and (now - next_run_dt).total_seconds() > grace:
|
if kind in {"cron", "interval"} and (now - next_run_dt).total_seconds() > grace:
|
||||||
# Job is past its catch-up grace window — this is a stale missed run.
|
# Job is past its catch-up grace window — this is a stale missed run.
|
||||||
# Grace scales with schedule period: daily=2h, hourly=30m, 10min=5m.
|
# Grace scales with schedule period: daily=2h, hourly=30m, 10min=5m.
|
||||||
new_next = compute_next_run(schedule, now.isoformat())
|
new_next = compute_next_run(schedule, now.isoformat())
|
||||||
|
|
|
||||||
|
|
@ -754,7 +754,7 @@ def _run_job_script(script_path: str) -> tuple[bool, str]:
|
||||||
# shebang: the scripts dir is trusted, but keeping the interpreter
|
# shebang: the scripts dir is trusted, but keeping the interpreter
|
||||||
# choice explicit here keeps the allowed surface small and auditable.
|
# choice explicit here keeps the allowed surface small and auditable.
|
||||||
suffix = path.suffix.lower()
|
suffix = path.suffix.lower()
|
||||||
if suffix in (".sh", ".bash"):
|
if suffix in {".sh", ".bash"}:
|
||||||
# Resolve bash dynamically so Windows (Git Bash) and Linux/macOS
|
# Resolve bash dynamically so Windows (Git Bash) and Linux/macOS
|
||||||
# all work. On native Windows without Git for Windows installed
|
# all work. On native Windows without Git for Windows installed
|
||||||
# shutil.which returns None — fall back to a clear error rather
|
# shutil.which returns None — fall back to a clear error rather
|
||||||
|
|
|
||||||
|
|
@ -264,7 +264,7 @@ def _parse_hint_result(text: str) -> tuple[int | None, str]:
|
||||||
"""Parse the judge's boxed decision and hint text."""
|
"""Parse the judge's boxed decision and hint text."""
|
||||||
boxed = _BOXED_RE.findall(text)
|
boxed = _BOXED_RE.findall(text)
|
||||||
score = int(boxed[-1]) if boxed else None
|
score = int(boxed[-1]) if boxed else None
|
||||||
if score not in (1, -1):
|
if score not in {1, -1}:
|
||||||
score = None
|
score = None
|
||||||
hint_matches = _HINT_RE.findall(text)
|
hint_matches = _HINT_RE.findall(text)
|
||||||
hint = hint_matches[-1].strip() if hint_matches else ""
|
hint = hint_matches[-1].strip() if hint_matches else ""
|
||||||
|
|
|
||||||
|
|
@ -162,7 +162,7 @@ def _normalize_tar_member_parts(member_name: str) -> list:
|
||||||
):
|
):
|
||||||
raise ValueError(f"Unsafe archive member path: {member_name}")
|
raise ValueError(f"Unsafe archive member path: {member_name}")
|
||||||
|
|
||||||
parts = [part for part in posix_path.parts if part not in ("", ".")]
|
parts = [part for part in posix_path.parts if part not in {"", "."}]
|
||||||
if not parts or any(part == ".." for part in parts):
|
if not parts or any(part == ".." for part in parts):
|
||||||
raise ValueError(f"Unsafe archive member path: {member_name}")
|
raise ValueError(f"Unsafe archive member path: {member_name}")
|
||||||
return parts
|
return parts
|
||||||
|
|
@ -561,7 +561,7 @@ class TerminalBench2EvalEnv(HermesAgentBaseEnv):
|
||||||
# --- 5. Verify -- run test suite in the agent's sandbox ---
|
# --- 5. Verify -- run test suite in the agent's sandbox ---
|
||||||
# Skip verification if the agent produced no meaningful output
|
# Skip verification if the agent produced no meaningful output
|
||||||
only_system_and_user = all(
|
only_system_and_user = all(
|
||||||
msg.get("role") in ("system", "user") for msg in result.messages
|
msg.get("role") in {"system", "user"} for msg in result.messages
|
||||||
)
|
)
|
||||||
if result.turns_used == 0 or only_system_and_user:
|
if result.turns_used == 0 or only_system_and_user:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
|
|
|
||||||
|
|
@ -571,7 +571,7 @@ class HermesAgentBaseEnv(BaseEnv):
|
||||||
# (e.g., API call failed on turn 1). No point spinning up a Modal sandbox
|
# (e.g., API call failed on turn 1). No point spinning up a Modal sandbox
|
||||||
# just to verify files that were never created.
|
# just to verify files that were never created.
|
||||||
only_system_and_user = all(
|
only_system_and_user = all(
|
||||||
msg.get("role") in ("system", "user") for msg in result.messages
|
msg.get("role") in {"system", "user"} for msg in result.messages
|
||||||
)
|
)
|
||||||
if result.turns_used == 0 or only_system_and_user:
|
if result.turns_used == 0 or only_system_and_user:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
|
|
|
||||||
|
|
@ -179,7 +179,7 @@ class ToolContext:
|
||||||
|
|
||||||
# Ensure parent directory exists in the sandbox
|
# Ensure parent directory exists in the sandbox
|
||||||
parent = str(_Path(remote_path).parent)
|
parent = str(_Path(remote_path).parent)
|
||||||
if parent not in (".", "/"):
|
if parent not in {".", "/"}:
|
||||||
self.terminal(f"mkdir -p {parent}", timeout=10)
|
self.terminal(f"mkdir -p {parent}", timeout=10)
|
||||||
|
|
||||||
# For small files, single command is fine
|
# For small files, single command is fine
|
||||||
|
|
|
||||||
|
|
@ -28,9 +28,9 @@ def _coerce_bool(value: Any, default: bool = True) -> bool:
|
||||||
return default
|
return default
|
||||||
if isinstance(value, str):
|
if isinstance(value, str):
|
||||||
lowered = value.strip().lower()
|
lowered = value.strip().lower()
|
||||||
if lowered in ("true", "1", "yes", "on"):
|
if lowered in {"true", "1", "yes", "on"}:
|
||||||
return True
|
return True
|
||||||
if lowered in ("false", "0", "no", "off"):
|
if lowered in {"false", "0", "no", "off"}:
|
||||||
return False
|
return False
|
||||||
return default
|
return default
|
||||||
return is_truthy_value(value, default=default)
|
return is_truthy_value(value, default=default)
|
||||||
|
|
@ -799,7 +799,7 @@ def load_gateway_config() -> GatewayConfig:
|
||||||
bridged["group_allow_admin_from"] = platform_cfg["group_allow_admin_from"]
|
bridged["group_allow_admin_from"] = platform_cfg["group_allow_admin_from"]
|
||||||
if "group_user_allowed_commands" in platform_cfg:
|
if "group_user_allowed_commands" in platform_cfg:
|
||||||
bridged["group_user_allowed_commands"] = platform_cfg["group_user_allowed_commands"]
|
bridged["group_user_allowed_commands"] = platform_cfg["group_user_allowed_commands"]
|
||||||
if plat in (Platform.DISCORD, Platform.SLACK) and "channel_skill_bindings" in platform_cfg:
|
if plat in {Platform.DISCORD, Platform.SLACK} and "channel_skill_bindings" in platform_cfg:
|
||||||
bridged["channel_skill_bindings"] = platform_cfg["channel_skill_bindings"]
|
bridged["channel_skill_bindings"] = platform_cfg["channel_skill_bindings"]
|
||||||
if "channel_prompts" in platform_cfg:
|
if "channel_prompts" in platform_cfg:
|
||||||
channel_prompts = platform_cfg["channel_prompts"]
|
channel_prompts = platform_cfg["channel_prompts"]
|
||||||
|
|
@ -1179,7 +1179,7 @@ def _apply_env_overrides(config: GatewayConfig) -> None:
|
||||||
|
|
||||||
# Reply threading mode for Telegram (off/first/all)
|
# Reply threading mode for Telegram (off/first/all)
|
||||||
telegram_reply_mode = os.getenv("TELEGRAM_REPLY_TO_MODE", "").lower()
|
telegram_reply_mode = os.getenv("TELEGRAM_REPLY_TO_MODE", "").lower()
|
||||||
if telegram_reply_mode in ("off", "first", "all"):
|
if telegram_reply_mode in {"off", "first", "all"}:
|
||||||
if Platform.TELEGRAM not in config.platforms:
|
if Platform.TELEGRAM not in config.platforms:
|
||||||
config.platforms[Platform.TELEGRAM] = PlatformConfig()
|
config.platforms[Platform.TELEGRAM] = PlatformConfig()
|
||||||
config.platforms[Platform.TELEGRAM].reply_to_mode = telegram_reply_mode
|
config.platforms[Platform.TELEGRAM].reply_to_mode = telegram_reply_mode
|
||||||
|
|
@ -1220,14 +1220,14 @@ def _apply_env_overrides(config: GatewayConfig) -> None:
|
||||||
|
|
||||||
# Reply threading mode for Discord (off/first/all)
|
# Reply threading mode for Discord (off/first/all)
|
||||||
discord_reply_mode = os.getenv("DISCORD_REPLY_TO_MODE", "").lower()
|
discord_reply_mode = os.getenv("DISCORD_REPLY_TO_MODE", "").lower()
|
||||||
if discord_reply_mode in ("off", "first", "all"):
|
if discord_reply_mode in {"off", "first", "all"}:
|
||||||
if Platform.DISCORD not in config.platforms:
|
if Platform.DISCORD not in config.platforms:
|
||||||
config.platforms[Platform.DISCORD] = PlatformConfig()
|
config.platforms[Platform.DISCORD] = PlatformConfig()
|
||||||
config.platforms[Platform.DISCORD].reply_to_mode = discord_reply_mode
|
config.platforms[Platform.DISCORD].reply_to_mode = discord_reply_mode
|
||||||
|
|
||||||
# WhatsApp (typically uses different auth mechanism)
|
# WhatsApp (typically uses different auth mechanism)
|
||||||
whatsapp_enabled = os.getenv("WHATSAPP_ENABLED", "").lower() in ("true", "1", "yes")
|
whatsapp_enabled = os.getenv("WHATSAPP_ENABLED", "").lower() in {"true", "1", "yes"}
|
||||||
whatsapp_disabled_explicitly = os.getenv("WHATSAPP_ENABLED", "").lower() in ("false", "0", "no")
|
whatsapp_disabled_explicitly = os.getenv("WHATSAPP_ENABLED", "").lower() in {"false", "0", "no"}
|
||||||
if Platform.WHATSAPP in config.platforms:
|
if Platform.WHATSAPP in config.platforms:
|
||||||
# YAML config exists — respect explicit disable
|
# YAML config exists — respect explicit disable
|
||||||
wa_cfg = config.platforms[Platform.WHATSAPP]
|
wa_cfg = config.platforms[Platform.WHATSAPP]
|
||||||
|
|
@ -1285,7 +1285,7 @@ def _apply_env_overrides(config: GatewayConfig) -> None:
|
||||||
config.platforms[Platform.SIGNAL].extra.update({
|
config.platforms[Platform.SIGNAL].extra.update({
|
||||||
"http_url": signal_url,
|
"http_url": signal_url,
|
||||||
"account": signal_account,
|
"account": signal_account,
|
||||||
"ignore_stories": os.getenv("SIGNAL_IGNORE_STORIES", "true").lower() in ("true", "1", "yes"),
|
"ignore_stories": os.getenv("SIGNAL_IGNORE_STORIES", "true").lower() in {"true", "1", "yes"},
|
||||||
})
|
})
|
||||||
signal_home = os.getenv("SIGNAL_HOME_CHANNEL")
|
signal_home = os.getenv("SIGNAL_HOME_CHANNEL")
|
||||||
if signal_home and Platform.SIGNAL in config.platforms:
|
if signal_home and Platform.SIGNAL in config.platforms:
|
||||||
|
|
@ -1334,7 +1334,7 @@ def _apply_env_overrides(config: GatewayConfig) -> None:
|
||||||
matrix_password = os.getenv("MATRIX_PASSWORD", "")
|
matrix_password = os.getenv("MATRIX_PASSWORD", "")
|
||||||
if matrix_password:
|
if matrix_password:
|
||||||
config.platforms[Platform.MATRIX].extra["password"] = matrix_password
|
config.platforms[Platform.MATRIX].extra["password"] = matrix_password
|
||||||
matrix_e2ee = os.getenv("MATRIX_ENCRYPTION", "").lower() in ("true", "1", "yes")
|
matrix_e2ee = os.getenv("MATRIX_ENCRYPTION", "").lower() in {"true", "1", "yes"}
|
||||||
config.platforms[Platform.MATRIX].extra["encryption"] = matrix_e2ee
|
config.platforms[Platform.MATRIX].extra["encryption"] = matrix_e2ee
|
||||||
matrix_device_id = os.getenv("MATRIX_DEVICE_ID", "")
|
matrix_device_id = os.getenv("MATRIX_DEVICE_ID", "")
|
||||||
if matrix_device_id:
|
if matrix_device_id:
|
||||||
|
|
@ -1399,7 +1399,7 @@ def _apply_env_overrides(config: GatewayConfig) -> None:
|
||||||
)
|
)
|
||||||
|
|
||||||
# API Server
|
# API Server
|
||||||
api_server_enabled = os.getenv("API_SERVER_ENABLED", "").lower() in ("true", "1", "yes")
|
api_server_enabled = os.getenv("API_SERVER_ENABLED", "").lower() in {"true", "1", "yes"}
|
||||||
api_server_key = os.getenv("API_SERVER_KEY", "")
|
api_server_key = os.getenv("API_SERVER_KEY", "")
|
||||||
api_server_cors_origins = os.getenv("API_SERVER_CORS_ORIGINS", "")
|
api_server_cors_origins = os.getenv("API_SERVER_CORS_ORIGINS", "")
|
||||||
api_server_port = os.getenv("API_SERVER_PORT")
|
api_server_port = os.getenv("API_SERVER_PORT")
|
||||||
|
|
@ -1426,7 +1426,7 @@ def _apply_env_overrides(config: GatewayConfig) -> None:
|
||||||
config.platforms[Platform.API_SERVER].extra["model_name"] = api_server_model_name
|
config.platforms[Platform.API_SERVER].extra["model_name"] = api_server_model_name
|
||||||
|
|
||||||
# Webhook platform
|
# Webhook platform
|
||||||
webhook_enabled = os.getenv("WEBHOOK_ENABLED", "").lower() in ("true", "1", "yes")
|
webhook_enabled = os.getenv("WEBHOOK_ENABLED", "").lower() in {"true", "1", "yes"}
|
||||||
webhook_port = os.getenv("WEBHOOK_PORT")
|
webhook_port = os.getenv("WEBHOOK_PORT")
|
||||||
webhook_secret = os.getenv("WEBHOOK_SECRET", "")
|
webhook_secret = os.getenv("WEBHOOK_SECRET", "")
|
||||||
if webhook_enabled:
|
if webhook_enabled:
|
||||||
|
|
@ -1442,11 +1442,11 @@ def _apply_env_overrides(config: GatewayConfig) -> None:
|
||||||
config.platforms[Platform.WEBHOOK].extra["secret"] = webhook_secret
|
config.platforms[Platform.WEBHOOK].extra["secret"] = webhook_secret
|
||||||
|
|
||||||
# Microsoft Graph webhook platform
|
# Microsoft Graph webhook platform
|
||||||
msgraph_webhook_enabled = os.getenv("MSGRAPH_WEBHOOK_ENABLED", "").lower() in (
|
msgraph_webhook_enabled = os.getenv("MSGRAPH_WEBHOOK_ENABLED", "").lower() in {
|
||||||
"true",
|
"true",
|
||||||
"1",
|
"1",
|
||||||
"yes",
|
"yes",
|
||||||
)
|
}
|
||||||
msgraph_webhook_port = os.getenv("MSGRAPH_WEBHOOK_PORT")
|
msgraph_webhook_port = os.getenv("MSGRAPH_WEBHOOK_PORT")
|
||||||
msgraph_webhook_client_state = os.getenv("MSGRAPH_WEBHOOK_CLIENT_STATE", "")
|
msgraph_webhook_client_state = os.getenv("MSGRAPH_WEBHOOK_CLIENT_STATE", "")
|
||||||
msgraph_webhook_resources = os.getenv("MSGRAPH_WEBHOOK_ACCEPTED_RESOURCES", "")
|
msgraph_webhook_resources = os.getenv("MSGRAPH_WEBHOOK_ACCEPTED_RESOURCES", "")
|
||||||
|
|
@ -1640,7 +1640,7 @@ def _apply_env_overrides(config: GatewayConfig) -> None:
|
||||||
"webhook_host": os.getenv("BLUEBUBBLES_WEBHOOK_HOST", "127.0.0.1"),
|
"webhook_host": os.getenv("BLUEBUBBLES_WEBHOOK_HOST", "127.0.0.1"),
|
||||||
"webhook_port": int(os.getenv("BLUEBUBBLES_WEBHOOK_PORT", "8645")),
|
"webhook_port": int(os.getenv("BLUEBUBBLES_WEBHOOK_PORT", "8645")),
|
||||||
"webhook_path": os.getenv("BLUEBUBBLES_WEBHOOK_PATH", "/bluebubbles-webhook"),
|
"webhook_path": os.getenv("BLUEBUBBLES_WEBHOOK_PATH", "/bluebubbles-webhook"),
|
||||||
"send_read_receipts": os.getenv("BLUEBUBBLES_SEND_READ_RECEIPTS", "true").lower() in ("true", "1", "yes"),
|
"send_read_receipts": os.getenv("BLUEBUBBLES_SEND_READ_RECEIPTS", "true").lower() in {"true", "1", "yes"},
|
||||||
})
|
})
|
||||||
bluebubbles_home = os.getenv("BLUEBUBBLES_HOME_CHANNEL")
|
bluebubbles_home = os.getenv("BLUEBUBBLES_HOME_CHANNEL")
|
||||||
if bluebubbles_home and Platform.BLUEBUBBLES in config.platforms:
|
if bluebubbles_home and Platform.BLUEBUBBLES in config.platforms:
|
||||||
|
|
|
||||||
|
|
@ -190,13 +190,13 @@ def _normalise(setting: str, value: Any) -> Any:
|
||||||
if value is True:
|
if value is True:
|
||||||
return "all"
|
return "all"
|
||||||
return str(value).lower()
|
return str(value).lower()
|
||||||
if setting in ("show_reasoning", "streaming"):
|
if setting in {"show_reasoning", "streaming"}:
|
||||||
if isinstance(value, str):
|
if isinstance(value, str):
|
||||||
return value.lower() in ("true", "1", "yes", "on")
|
return value.lower() in {"true", "1", "yes", "on"}
|
||||||
return bool(value)
|
return bool(value)
|
||||||
if setting == "cleanup_progress":
|
if setting == "cleanup_progress":
|
||||||
if isinstance(value, str):
|
if isinstance(value, str):
|
||||||
return value.lower() in ("true", "1", "yes", "on")
|
return value.lower() in {"true", "1", "yes", "on"}
|
||||||
return bool(value)
|
return bool(value)
|
||||||
if setting == "tool_preview_length":
|
if setting == "tool_preview_length":
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
|
|
@ -449,7 +449,7 @@ if AIOHTTP_AVAILABLE:
|
||||||
@web.middleware
|
@web.middleware
|
||||||
async def body_limit_middleware(request, handler):
|
async def body_limit_middleware(request, handler):
|
||||||
"""Reject overly large request bodies early based on Content-Length."""
|
"""Reject overly large request bodies early based on Content-Length."""
|
||||||
if request.method in ("POST", "PUT", "PATCH"):
|
if request.method in {"POST", "PUT", "PATCH"}:
|
||||||
cl = request.headers.get("Content-Length")
|
cl = request.headers.get("Content-Length")
|
||||||
if cl is not None:
|
if cl is not None:
|
||||||
try:
|
try:
|
||||||
|
|
@ -646,7 +646,7 @@ class APIServerAdapter(BasePlatformAdapter):
|
||||||
try:
|
try:
|
||||||
from hermes_cli.profiles import get_active_profile_name
|
from hermes_cli.profiles import get_active_profile_name
|
||||||
profile = get_active_profile_name()
|
profile = get_active_profile_name()
|
||||||
if profile and profile not in ("default", "custom"):
|
if profile and profile not in {"default", "custom"}:
|
||||||
return profile
|
return profile
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
@ -1003,7 +1003,7 @@ class APIServerAdapter(BasePlatformAdapter):
|
||||||
system_prompt = content
|
system_prompt = content
|
||||||
else:
|
else:
|
||||||
system_prompt = system_prompt + "\n" + content
|
system_prompt = system_prompt + "\n" + content
|
||||||
elif role in ("user", "assistant"):
|
elif role in {"user", "assistant"}:
|
||||||
try:
|
try:
|
||||||
content = _normalize_multimodal_content(raw_content)
|
content = _normalize_multimodal_content(raw_content)
|
||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
|
|
@ -2381,7 +2381,7 @@ class APIServerAdapter(BasePlatformAdapter):
|
||||||
if cron_err:
|
if cron_err:
|
||||||
return cron_err
|
return cron_err
|
||||||
try:
|
try:
|
||||||
include_disabled = request.query.get("include_disabled", "").lower() in ("true", "1")
|
include_disabled = request.query.get("include_disabled", "").lower() in {"true", "1"}
|
||||||
jobs = _cron_list(include_disabled=include_disabled)
|
jobs = _cron_list(include_disabled=include_disabled)
|
||||||
return web.json_response({"jobs": jobs})
|
return web.json_response({"jobs": jobs})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
|
||||||
|
|
@ -560,7 +560,7 @@ def _looks_like_image(data: bytes) -> bool:
|
||||||
return True
|
return True
|
||||||
if data[:3] == b"\xff\xd8\xff":
|
if data[:3] == b"\xff\xd8\xff":
|
||||||
return True
|
return True
|
||||||
if data[:6] in (b"GIF87a", b"GIF89a"):
|
if data[:6] in {b"GIF87a", b"GIF89a"}:
|
||||||
return True
|
return True
|
||||||
if data[:2] == b"BM":
|
if data[:2] == b"BM":
|
||||||
return True
|
return True
|
||||||
|
|
@ -859,7 +859,7 @@ def cache_document_from_bytes(data: bytes, filename: str) -> str:
|
||||||
# Sanitize: strip directory components, null bytes, and control characters
|
# Sanitize: strip directory components, null bytes, and control characters
|
||||||
safe_name = Path(filename).name if filename else "document"
|
safe_name = Path(filename).name if filename else "document"
|
||||||
safe_name = safe_name.replace("\x00", "").strip()
|
safe_name = safe_name.replace("\x00", "").strip()
|
||||||
if not safe_name or safe_name in (".", ".."):
|
if not safe_name or safe_name in {".", ".."}:
|
||||||
safe_name = "document"
|
safe_name = "document"
|
||||||
cached_name = f"doc_{uuid.uuid4().hex[:12]}_{safe_name}"
|
cached_name = f"doc_{uuid.uuid4().hex[:12]}_{safe_name}"
|
||||||
filepath = cache_dir / cached_name
|
filepath = cache_dir / cached_name
|
||||||
|
|
@ -2793,7 +2793,7 @@ class BasePlatformAdapter(ABC):
|
||||||
# and preserve ordering of queued follow-ups. Route those
|
# and preserve ordering of queued follow-ups. Route those
|
||||||
# through the dedicated handoff path that serializes
|
# through the dedicated handoff path that serializes
|
||||||
# cancellation + runner response + pending drain.
|
# cancellation + runner response + pending drain.
|
||||||
if cmd in ("stop", "new", "reset"):
|
if cmd in {"stop", "new", "reset"}:
|
||||||
try:
|
try:
|
||||||
await self._dispatch_active_session_command(event, session_key, cmd)
|
await self._dispatch_active_session_command(event, session_key, cmd)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
|
||||||
|
|
@ -223,7 +223,7 @@ class BlueBubblesAdapter(BasePlatformAdapter):
|
||||||
def _webhook_url(self) -> str:
|
def _webhook_url(self) -> str:
|
||||||
"""Compute the external webhook URL for BlueBubbles registration."""
|
"""Compute the external webhook URL for BlueBubbles registration."""
|
||||||
host = self.webhook_host
|
host = self.webhook_host
|
||||||
if host in ("0.0.0.0", "127.0.0.1", "localhost", "::"):
|
if host in {"0.0.0.0", "127.0.0.1", "localhost", "::"}:
|
||||||
host = "localhost"
|
host = "localhost"
|
||||||
return f"http://{host}:{self.webhook_port}{self.webhook_path}"
|
return f"http://{host}:{self.webhook_port}{self.webhook_path}"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -353,9 +353,9 @@ class DingTalkAdapter(BasePlatformAdapter):
|
||||||
configured = self.config.extra.get("require_mention")
|
configured = self.config.extra.get("require_mention")
|
||||||
if configured is not None:
|
if configured is not None:
|
||||||
if isinstance(configured, str):
|
if isinstance(configured, str):
|
||||||
return configured.lower() in ("true", "1", "yes", "on")
|
return configured.lower() in {"true", "1", "yes", "on"}
|
||||||
return bool(configured)
|
return bool(configured)
|
||||||
return os.getenv("DINGTALK_REQUIRE_MENTION", "false").lower() in ("true", "1", "yes", "on")
|
return os.getenv("DINGTALK_REQUIRE_MENTION", "false").lower() in {"true", "1", "yes", "on"}
|
||||||
|
|
||||||
def _dingtalk_free_response_chats(self) -> Set[str]:
|
def _dingtalk_free_response_chats(self) -> Set[str]:
|
||||||
raw = self.config.extra.get("free_response_chats")
|
raw = self.config.extra.get("free_response_chats")
|
||||||
|
|
|
||||||
|
|
@ -115,7 +115,7 @@ def _build_allowed_mentions():
|
||||||
raw = os.getenv(name, "").strip().lower()
|
raw = os.getenv(name, "").strip().lower()
|
||||||
if not raw:
|
if not raw:
|
||||||
return default
|
return default
|
||||||
return raw in ("true", "1", "yes", "on")
|
return raw in {"true", "1", "yes", "on"}
|
||||||
|
|
||||||
return discord.AllowedMentions(
|
return discord.AllowedMentions(
|
||||||
everyone=_b("DISCORD_ALLOW_MENTION_EVERYONE", False),
|
everyone=_b("DISCORD_ALLOW_MENTION_EVERYONE", False),
|
||||||
|
|
@ -708,7 +708,7 @@ class DiscordAdapter(BasePlatformAdapter):
|
||||||
|
|
||||||
# Ignore Discord system messages (thread renames, pins, member joins, etc.)
|
# Ignore Discord system messages (thread renames, pins, member joins, etc.)
|
||||||
# Allow both default and reply types — replies have a distinct MessageType.
|
# Allow both default and reply types — replies have a distinct MessageType.
|
||||||
if message.type not in (discord.MessageType.default, discord.MessageType.reply):
|
if message.type not in {discord.MessageType.default, discord.MessageType.reply}:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Bot message filtering (DISCORD_ALLOW_BOTS):
|
# Bot message filtering (DISCORD_ALLOW_BOTS):
|
||||||
|
|
@ -769,7 +769,7 @@ class DiscordAdapter(BasePlatformAdapter):
|
||||||
# answer regardless of who is mentioned.
|
# answer regardless of who is mentioned.
|
||||||
_ignore_no_mention = os.getenv(
|
_ignore_no_mention = os.getenv(
|
||||||
"DISCORD_IGNORE_NO_MENTION", "true"
|
"DISCORD_IGNORE_NO_MENTION", "true"
|
||||||
).lower() in ("true", "1", "yes")
|
).lower() in {"true", "1", "yes"}
|
||||||
if _ignore_no_mention and not _self_mentioned and not _other_bots_mentioned:
|
if _ignore_no_mention and not _self_mentioned and not _other_bots_mentioned:
|
||||||
_channel_id = str(message.channel.id)
|
_channel_id = str(message.channel.id)
|
||||||
_parent_id = None
|
_parent_id = None
|
||||||
|
|
@ -1317,7 +1317,7 @@ class DiscordAdapter(BasePlatformAdapter):
|
||||||
|
|
||||||
def _reactions_enabled(self) -> bool:
|
def _reactions_enabled(self) -> bool:
|
||||||
"""Check if message reactions are enabled via config/env."""
|
"""Check if message reactions are enabled via config/env."""
|
||||||
return os.getenv("DISCORD_REACTIONS", "true").lower() not in ("false", "0", "no")
|
return os.getenv("DISCORD_REACTIONS", "true").lower() not in {"false", "0", "no"}
|
||||||
|
|
||||||
async def on_processing_start(self, event: MessageEvent) -> None:
|
async def on_processing_start(self, event: MessageEvent) -> None:
|
||||||
"""Add an in-progress reaction for normal Discord message events."""
|
"""Add an in-progress reaction for normal Discord message events."""
|
||||||
|
|
@ -3137,9 +3137,9 @@ class DiscordAdapter(BasePlatformAdapter):
|
||||||
# UX so users don't see commands they can't invoke. Off by default
|
# UX so users don't see commands they can't invoke. Off by default
|
||||||
# to preserve the slash UX for deployments that intentionally allow
|
# to preserve the slash UX for deployments that intentionally allow
|
||||||
# everyone in the guild.
|
# everyone in the guild.
|
||||||
if os.getenv("DISCORD_HIDE_SLASH_COMMANDS", "false").strip().lower() in (
|
if os.getenv("DISCORD_HIDE_SLASH_COMMANDS", "false").strip().lower() in {
|
||||||
"true", "1", "yes", "on",
|
"true", "1", "yes", "on",
|
||||||
):
|
}:
|
||||||
self._apply_owner_only_visibility(tree)
|
self._apply_owner_only_visibility(tree)
|
||||||
|
|
||||||
def _apply_owner_only_visibility(self, tree) -> None:
|
def _apply_owner_only_visibility(self, tree) -> None:
|
||||||
|
|
@ -3526,9 +3526,9 @@ class DiscordAdapter(BasePlatformAdapter):
|
||||||
configured = self.config.extra.get("require_mention")
|
configured = self.config.extra.get("require_mention")
|
||||||
if configured is not None:
|
if configured is not None:
|
||||||
if isinstance(configured, str):
|
if isinstance(configured, str):
|
||||||
return configured.lower() not in ("false", "0", "no", "off")
|
return configured.lower() not in {"false", "0", "no", "off"}
|
||||||
return bool(configured)
|
return bool(configured)
|
||||||
return os.getenv("DISCORD_REQUIRE_MENTION", "true").lower() not in ("false", "0", "no", "off")
|
return os.getenv("DISCORD_REQUIRE_MENTION", "true").lower() not in {"false", "0", "no", "off"}
|
||||||
|
|
||||||
def _discord_free_response_channels(self) -> set:
|
def _discord_free_response_channels(self) -> set:
|
||||||
"""Return Discord channel IDs where no bot mention is required.
|
"""Return Discord channel IDs where no bot mention is required.
|
||||||
|
|
@ -4200,7 +4200,7 @@ class DiscordAdapter(BasePlatformAdapter):
|
||||||
no_thread_channels_raw = os.getenv("DISCORD_NO_THREAD_CHANNELS", "")
|
no_thread_channels_raw = os.getenv("DISCORD_NO_THREAD_CHANNELS", "")
|
||||||
no_thread_channels = {ch.strip() for ch in no_thread_channels_raw.split(",") if ch.strip()}
|
no_thread_channels = {ch.strip() for ch in no_thread_channels_raw.split(",") if ch.strip()}
|
||||||
skip_thread = bool(channel_ids & no_thread_channels)
|
skip_thread = bool(channel_ids & no_thread_channels)
|
||||||
auto_thread = os.getenv("DISCORD_AUTO_THREAD", "true").lower() in ("true", "1", "yes")
|
auto_thread = os.getenv("DISCORD_AUTO_THREAD", "true").lower() in {"true", "1", "yes"}
|
||||||
is_reply_message = getattr(message, "type", None) == discord.MessageType.reply
|
is_reply_message = getattr(message, "type", None) == discord.MessageType.reply
|
||||||
if auto_thread and not skip_thread and not is_voice_linked_channel and not is_reply_message:
|
if auto_thread and not skip_thread and not is_voice_linked_channel and not is_reply_message:
|
||||||
thread = await self._auto_create_thread(message)
|
thread = await self._auto_create_thread(message)
|
||||||
|
|
@ -4282,7 +4282,7 @@ class DiscordAdapter(BasePlatformAdapter):
|
||||||
try:
|
try:
|
||||||
# Determine extension from content type (image/png -> .png)
|
# Determine extension from content type (image/png -> .png)
|
||||||
ext = "." + content_type.split("/")[-1].split(";")[0]
|
ext = "." + content_type.split("/")[-1].split(";")[0]
|
||||||
if ext not in (".jpg", ".jpeg", ".png", ".gif", ".webp"):
|
if ext not in {".jpg", ".jpeg", ".png", ".gif", ".webp"}:
|
||||||
ext = ".jpg"
|
ext = ".jpg"
|
||||||
cached_path = await self._cache_discord_image(att, ext)
|
cached_path = await self._cache_discord_image(att, ext)
|
||||||
media_urls.append(cached_path)
|
media_urls.append(cached_path)
|
||||||
|
|
@ -4296,7 +4296,7 @@ class DiscordAdapter(BasePlatformAdapter):
|
||||||
elif content_type.startswith("audio/"):
|
elif content_type.startswith("audio/"):
|
||||||
try:
|
try:
|
||||||
ext = "." + content_type.split("/")[-1].split(";")[0]
|
ext = "." + content_type.split("/")[-1].split(";")[0]
|
||||||
if ext not in (".ogg", ".mp3", ".wav", ".webm", ".m4a"):
|
if ext not in {".ogg", ".mp3", ".wav", ".webm", ".m4a"}:
|
||||||
ext = ".ogg"
|
ext = ".ogg"
|
||||||
cached_path = await self._cache_discord_audio(att, ext)
|
cached_path = await self._cache_discord_audio(att, ext)
|
||||||
media_urls.append(cached_path)
|
media_urls.append(cached_path)
|
||||||
|
|
@ -4339,7 +4339,7 @@ class DiscordAdapter(BasePlatformAdapter):
|
||||||
logger.info("[Discord] Cached user document: %s", cached_path)
|
logger.info("[Discord] Cached user document: %s", cached_path)
|
||||||
# Inject text content for plain-text documents (capped at 100 KB)
|
# Inject text content for plain-text documents (capped at 100 KB)
|
||||||
MAX_TEXT_INJECT_BYTES = 100 * 1024
|
MAX_TEXT_INJECT_BYTES = 100 * 1024
|
||||||
if ext in (".md", ".txt", ".log") and len(raw_bytes) <= MAX_TEXT_INJECT_BYTES:
|
if ext in {".md", ".txt", ".log"} and len(raw_bytes) <= MAX_TEXT_INJECT_BYTES:
|
||||||
try:
|
try:
|
||||||
text_content = raw_bytes.decode("utf-8")
|
text_content = raw_bytes.decode("utf-8")
|
||||||
display_name = att.filename or f"document{ext}"
|
display_name = att.filename or f"document{ext}"
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,7 @@ _NOREPLY_PATTERNS = (
|
||||||
# RFC headers that indicate bulk/automated mail
|
# RFC headers that indicate bulk/automated mail
|
||||||
_AUTOMATED_HEADERS = {
|
_AUTOMATED_HEADERS = {
|
||||||
"Auto-Submitted": lambda v: v.lower() != "no",
|
"Auto-Submitted": lambda v: v.lower() != "no",
|
||||||
"Precedence": lambda v: v.lower() in ("bulk", "list", "junk"),
|
"Precedence": lambda v: v.lower() in {"bulk", "list", "junk"},
|
||||||
"X-Auto-Response-Suppress": lambda v: bool(v),
|
"X-Auto-Response-Suppress": lambda v: bool(v),
|
||||||
"List-Unsubscribe": lambda v: bool(v),
|
"List-Unsubscribe": lambda v: bool(v),
|
||||||
}
|
}
|
||||||
|
|
@ -203,7 +203,7 @@ def _extract_attachments(
|
||||||
continue
|
continue
|
||||||
# Skip text/plain and text/html body parts
|
# Skip text/plain and text/html body parts
|
||||||
content_type = part.get_content_type()
|
content_type = part.get_content_type()
|
||||||
if content_type in ("text/plain", "text/html") and "attachment" not in disposition:
|
if content_type in {"text/plain", "text/html"} and "attachment" not in disposition:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
filename = part.get_filename()
|
filename = part.get_filename()
|
||||||
|
|
|
||||||
|
|
@ -428,7 +428,7 @@ RejectReason = Literal[
|
||||||
|
|
||||||
def _is_bot_sender(sender: Any) -> bool:
|
def _is_bot_sender(sender: Any) -> bool:
|
||||||
# receive_v1 docs say {user, bot}; accept "app" defensively.
|
# receive_v1 docs say {user, bot}; accept "app" defensively.
|
||||||
return getattr(sender, "sender_type", "") in ("bot", "app")
|
return getattr(sender, "sender_type", "") in {"bot", "app"}
|
||||||
|
|
||||||
|
|
||||||
def _sender_identity(sender: Any) -> frozenset:
|
def _sender_identity(sender: Any) -> frozenset:
|
||||||
|
|
@ -1443,7 +1443,7 @@ class FeishuAdapter(BasePlatformAdapter):
|
||||||
# Env-only so adapter and gateway auth bypass share one source; yaml
|
# Env-only so adapter and gateway auth bypass share one source; yaml
|
||||||
# feishu.allow_bots is bridged to this env var at config load.
|
# feishu.allow_bots is bridged to this env var at config load.
|
||||||
allow_bots = os.getenv("FEISHU_ALLOW_BOTS", "none").strip().lower()
|
allow_bots = os.getenv("FEISHU_ALLOW_BOTS", "none").strip().lower()
|
||||||
if allow_bots not in ("none", "mentions", "all"):
|
if allow_bots not in {"none", "mentions", "all"}:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"[Feishu] Unknown allow_bots=%r, falling back to 'none'. Valid: none, mentions, all.",
|
"[Feishu] Unknown allow_bots=%r, falling back to 'none'. Valid: none, mentions, all.",
|
||||||
allow_bots,
|
allow_bots,
|
||||||
|
|
@ -2752,7 +2752,7 @@ class FeishuAdapter(BasePlatformAdapter):
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
|
|
||||||
def _reactions_enabled(self) -> bool:
|
def _reactions_enabled(self) -> bool:
|
||||||
return os.getenv("FEISHU_REACTIONS", "true").strip().lower() not in ("false", "0", "no")
|
return os.getenv("FEISHU_REACTIONS", "true").strip().lower() not in {"false", "0", "no"}
|
||||||
|
|
||||||
async def _add_reaction(self, message_id: str, emoji_type: str) -> Optional[str]:
|
async def _add_reaction(self, message_id: str, emoji_type: str) -> Optional[str]:
|
||||||
"""Return the reaction_id on success, else None. The id is needed later for deletion."""
|
"""Return the reaction_id on success, else None. The id is needed later for deletion."""
|
||||||
|
|
@ -3219,7 +3219,7 @@ class FeishuAdapter(BasePlatformAdapter):
|
||||||
self._on_bot_added_to_chat(data)
|
self._on_bot_added_to_chat(data)
|
||||||
elif event_type == "im.chat.member.bot.deleted_v1":
|
elif event_type == "im.chat.member.bot.deleted_v1":
|
||||||
self._on_bot_removed_from_chat(data)
|
self._on_bot_removed_from_chat(data)
|
||||||
elif event_type in ("im.message.reaction.created_v1", "im.message.reaction.deleted_v1"):
|
elif event_type in {"im.message.reaction.created_v1", "im.message.reaction.deleted_v1"}:
|
||||||
self._on_reaction_event(event_type, data)
|
self._on_reaction_event(event_type, data)
|
||||||
elif event_type == "card.action.trigger":
|
elif event_type == "card.action.trigger":
|
||||||
self._on_card_action_trigger(data)
|
self._on_card_action_trigger(data)
|
||||||
|
|
@ -4815,7 +4815,7 @@ def _poll_registration(
|
||||||
|
|
||||||
# Terminal errors
|
# Terminal errors
|
||||||
error = res.get("error", "")
|
error = res.get("error", "")
|
||||||
if error in ("access_denied", "expired_token"):
|
if error in {"access_denied", "expired_token"}:
|
||||||
if poll_count > 0:
|
if poll_count > 0:
|
||||||
print()
|
print()
|
||||||
logger.warning("[Feishu onboard] Registration %s", error)
|
logger.warning("[Feishu onboard] Registration %s", error)
|
||||||
|
|
|
||||||
|
|
@ -690,7 +690,7 @@ def _extract_docs_links(replies: List[Dict[str, Any]]) -> List[Dict[str, str]]:
|
||||||
except (json.JSONDecodeError, TypeError):
|
except (json.JSONDecodeError, TypeError):
|
||||||
continue
|
continue
|
||||||
for elem in content.get("elements", []):
|
for elem in content.get("elements", []):
|
||||||
if elem.get("type") not in ("docs_link", "link"):
|
if elem.get("type") not in {"docs_link", "link"}:
|
||||||
continue
|
continue
|
||||||
link_data = elem.get("docs_link") or elem.get("link") or {}
|
link_data = elem.get("docs_link") or elem.get("link") or {}
|
||||||
url = link_data.get("url", "")
|
url = link_data.get("url", "")
|
||||||
|
|
@ -1031,7 +1031,7 @@ def _save_session_history(key: str, messages: List[Dict[str, Any]]) -> None:
|
||||||
# Only keep user/assistant messages (strip system messages and tool internals)
|
# Only keep user/assistant messages (strip system messages and tool internals)
|
||||||
cleaned = [
|
cleaned = [
|
||||||
m for m in messages
|
m for m in messages
|
||||||
if m.get("role") in ("user", "assistant") and m.get("content")
|
if m.get("role") in {"user", "assistant"} and m.get("content")
|
||||||
]
|
]
|
||||||
# Keep last N
|
# Keep last N
|
||||||
if len(cleaned) > _SESSION_MAX_MESSAGES:
|
if len(cleaned) > _SESSION_MAX_MESSAGES:
|
||||||
|
|
@ -1170,7 +1170,7 @@ async def handle_drive_comment_event(
|
||||||
rule = resolve_rule(comments_cfg, file_type, file_token)
|
rule = resolve_rule(comments_cfg, file_type, file_token)
|
||||||
|
|
||||||
# If no exact match and config has wiki keys, try reverse-lookup
|
# If no exact match and config has wiki keys, try reverse-lookup
|
||||||
if rule.match_source in ("wildcard", "top") and has_wiki_keys(comments_cfg):
|
if rule.match_source in {"wildcard", "top"} and has_wiki_keys(comments_cfg):
|
||||||
wiki_token = await _reverse_lookup_wiki_token(client, file_type, file_token)
|
wiki_token = await _reverse_lookup_wiki_token(client, file_type, file_token)
|
||||||
if wiki_token:
|
if wiki_token:
|
||||||
rule = resolve_rule(comments_cfg, file_type, file_token, wiki_token=wiki_token)
|
rule = resolve_rule(comments_cfg, file_type, file_token, wiki_token=wiki_token)
|
||||||
|
|
|
||||||
|
|
@ -256,7 +256,7 @@ class HomeAssistantAdapter(BasePlatformAdapter):
|
||||||
await self._handle_ha_event(data.get("event", {}))
|
await self._handle_ha_event(data.get("event", {}))
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
logger.debug("Invalid JSON from HA WS: %s", ws_msg.data[:200])
|
logger.debug("Invalid JSON from HA WS: %s", ws_msg.data[:200])
|
||||||
elif ws_msg.type in (aiohttp.WSMsgType.CLOSED, aiohttp.WSMsgType.ERROR):
|
elif ws_msg.type in {aiohttp.WSMsgType.CLOSED, aiohttp.WSMsgType.ERROR}:
|
||||||
break
|
break
|
||||||
|
|
||||||
async def _handle_ha_event(self, event: Dict[str, Any]) -> None:
|
async def _handle_ha_event(self, event: Dict[str, Any]) -> None:
|
||||||
|
|
@ -361,7 +361,7 @@ class HomeAssistantAdapter(BasePlatformAdapter):
|
||||||
f"(was {'triggered' if old_val == 'on' else 'cleared'})"
|
f"(was {'triggered' if old_val == 'on' else 'cleared'})"
|
||||||
)
|
)
|
||||||
|
|
||||||
if domain in ("light", "switch", "fan"):
|
if domain in {"light", "switch", "fan"}:
|
||||||
return (
|
return (
|
||||||
f"[Home Assistant] {friendly_name}: turned "
|
f"[Home Assistant] {friendly_name}: turned "
|
||||||
f"{'on' if new_val == 'on' else 'off'}"
|
f"{'on' if new_val == 'on' else 'off'}"
|
||||||
|
|
|
||||||
|
|
@ -245,11 +245,11 @@ def check_matrix_requirements() -> bool:
|
||||||
|
|
||||||
# If encryption is requested, verify E2EE deps are available at startup
|
# If encryption is requested, verify E2EE deps are available at startup
|
||||||
# rather than silently degrading to plaintext-only at connect time.
|
# rather than silently degrading to plaintext-only at connect time.
|
||||||
encryption_requested = os.getenv("MATRIX_ENCRYPTION", "").lower() in (
|
encryption_requested = os.getenv("MATRIX_ENCRYPTION", "").lower() in {
|
||||||
"true",
|
"true",
|
||||||
"1",
|
"1",
|
||||||
"yes",
|
"yes",
|
||||||
)
|
}
|
||||||
if encryption_requested and not _check_e2ee_deps():
|
if encryption_requested and not _check_e2ee_deps():
|
||||||
logger.error(
|
logger.error(
|
||||||
"Matrix: MATRIX_ENCRYPTION=true but E2EE dependencies are missing. %s. "
|
"Matrix: MATRIX_ENCRYPTION=true but E2EE dependencies are missing. %s. "
|
||||||
|
|
@ -312,7 +312,7 @@ class MatrixAdapter(BasePlatformAdapter):
|
||||||
)
|
)
|
||||||
self._encryption: bool = config.extra.get(
|
self._encryption: bool = config.extra.get(
|
||||||
"encryption",
|
"encryption",
|
||||||
os.getenv("MATRIX_ENCRYPTION", "").lower() in ("true", "1", "yes"),
|
os.getenv("MATRIX_ENCRYPTION", "").lower() in {"true", "1", "yes"},
|
||||||
)
|
)
|
||||||
self._device_id: str = config.extra.get("device_id", "") or os.getenv(
|
self._device_id: str = config.extra.get("device_id", "") or os.getenv(
|
||||||
"MATRIX_DEVICE_ID", ""
|
"MATRIX_DEVICE_ID", ""
|
||||||
|
|
@ -343,7 +343,7 @@ class MatrixAdapter(BasePlatformAdapter):
|
||||||
# Mention/thread gating — parsed once from env vars.
|
# Mention/thread gating — parsed once from env vars.
|
||||||
self._require_mention: bool = os.getenv(
|
self._require_mention: bool = os.getenv(
|
||||||
"MATRIX_REQUIRE_MENTION", "true"
|
"MATRIX_REQUIRE_MENTION", "true"
|
||||||
).lower() not in ("false", "0", "no")
|
).lower() not in {"false", "0", "no"}
|
||||||
free_rooms_raw = config.extra.get("free_response_rooms")
|
free_rooms_raw = config.extra.get("free_response_rooms")
|
||||||
if free_rooms_raw is None:
|
if free_rooms_raw is None:
|
||||||
free_rooms_raw = os.getenv("MATRIX_FREE_RESPONSE_ROOMS", "")
|
free_rooms_raw = os.getenv("MATRIX_FREE_RESPONSE_ROOMS", "")
|
||||||
|
|
@ -367,22 +367,22 @@ class MatrixAdapter(BasePlatformAdapter):
|
||||||
self._allowed_rooms: Set[str] = {
|
self._allowed_rooms: Set[str] = {
|
||||||
r.strip() for r in str(allowed_rooms_raw).split(",") if r.strip()
|
r.strip() for r in str(allowed_rooms_raw).split(",") if r.strip()
|
||||||
}
|
}
|
||||||
self._auto_thread: bool = os.getenv("MATRIX_AUTO_THREAD", "true").lower() in (
|
self._auto_thread: bool = os.getenv("MATRIX_AUTO_THREAD", "true").lower() in {
|
||||||
"true",
|
"true",
|
||||||
"1",
|
"1",
|
||||||
"yes",
|
"yes",
|
||||||
)
|
}
|
||||||
self._dm_auto_thread: bool = os.getenv(
|
self._dm_auto_thread: bool = os.getenv(
|
||||||
"MATRIX_DM_AUTO_THREAD", "false"
|
"MATRIX_DM_AUTO_THREAD", "false"
|
||||||
).lower() in ("true", "1", "yes")
|
).lower() in {"true", "1", "yes"}
|
||||||
self._dm_mention_threads: bool = os.getenv(
|
self._dm_mention_threads: bool = os.getenv(
|
||||||
"MATRIX_DM_MENTION_THREADS", "false"
|
"MATRIX_DM_MENTION_THREADS", "false"
|
||||||
).lower() in ("true", "1", "yes")
|
).lower() in {"true", "1", "yes"}
|
||||||
|
|
||||||
# Reactions: configurable via MATRIX_REACTIONS (default: true).
|
# Reactions: configurable via MATRIX_REACTIONS (default: true).
|
||||||
self._reactions_enabled: bool = os.getenv(
|
self._reactions_enabled: bool = os.getenv(
|
||||||
"MATRIX_REACTIONS", "true"
|
"MATRIX_REACTIONS", "true"
|
||||||
).lower() not in ("false", "0", "no")
|
).lower() not in {"false", "0", "no"}
|
||||||
self._pending_reactions: dict[tuple[str, str], str] = {}
|
self._pending_reactions: dict[tuple[str, str], str] = {}
|
||||||
# Delay before redacting reactions so Matrix homeservers have time to
|
# Delay before redacting reactions so Matrix homeservers have time to
|
||||||
# deliver the final message event without tripping "missing event"
|
# deliver the final message event without tripping "missing event"
|
||||||
|
|
@ -1771,9 +1771,9 @@ class MatrixAdapter(BasePlatformAdapter):
|
||||||
|
|
||||||
# Cache media locally when downstream tools need a real file path.
|
# Cache media locally when downstream tools need a real file path.
|
||||||
cached_path = None
|
cached_path = None
|
||||||
should_cache_locally = msg_type in (
|
should_cache_locally = msg_type in {
|
||||||
MessageType.PHOTO, MessageType.AUDIO, MessageType.VIDEO, MessageType.DOCUMENT,
|
MessageType.PHOTO, MessageType.AUDIO, MessageType.VIDEO, MessageType.DOCUMENT,
|
||||||
) or is_voice_message or is_encrypted_media
|
} or is_voice_message or is_encrypted_media
|
||||||
if should_cache_locally and url:
|
if should_cache_locally and url:
|
||||||
try:
|
try:
|
||||||
file_bytes = await self._client.download_media(ContentURI(url))
|
file_bytes = await self._client.download_media(ContentURI(url))
|
||||||
|
|
@ -1834,7 +1834,7 @@ class MatrixAdapter(BasePlatformAdapter):
|
||||||
ext = ext_map.get(media_type, ".jpg")
|
ext = ext_map.get(media_type, ".jpg")
|
||||||
cached_path = cache_image_from_bytes(file_bytes, ext=ext)
|
cached_path = cache_image_from_bytes(file_bytes, ext=ext)
|
||||||
logger.info("[Matrix] Cached user image at %s", cached_path)
|
logger.info("[Matrix] Cached user image at %s", cached_path)
|
||||||
elif msg_type in (MessageType.AUDIO, MessageType.VOICE):
|
elif msg_type in {MessageType.AUDIO, MessageType.VOICE}:
|
||||||
ext = (
|
ext = (
|
||||||
Path(
|
Path(
|
||||||
body
|
body
|
||||||
|
|
@ -2602,7 +2602,7 @@ class MatrixAdapter(BasePlatformAdapter):
|
||||||
"""Sanitize a URL for use in an href attribute."""
|
"""Sanitize a URL for use in an href attribute."""
|
||||||
stripped = url.strip()
|
stripped = url.strip()
|
||||||
scheme = stripped.split(":", 1)[0].lower().strip() if ":" in stripped else ""
|
scheme = stripped.split(":", 1)[0].lower().strip() if ":" in stripped else ""
|
||||||
if scheme in ("javascript", "data", "vbscript"):
|
if scheme in {"javascript", "data", "vbscript"}:
|
||||||
return ""
|
return ""
|
||||||
return stripped.replace('"', """)
|
return stripped.replace('"', """)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -611,7 +611,7 @@ class MattermostAdapter(BasePlatformAdapter):
|
||||||
# succeed on retry — stop reconnecting instead of looping forever.
|
# succeed on retry — stop reconnecting instead of looping forever.
|
||||||
import aiohttp
|
import aiohttp
|
||||||
err_str = str(exc).lower()
|
err_str = str(exc).lower()
|
||||||
if isinstance(exc, aiohttp.WSServerHandshakeError) and exc.status in (401, 403):
|
if isinstance(exc, aiohttp.WSServerHandshakeError) and exc.status in {401, 403}:
|
||||||
logger.error("Mattermost WS auth failed (HTTP %d) — stopping reconnect", exc.status)
|
logger.error("Mattermost WS auth failed (HTTP %d) — stopping reconnect", exc.status)
|
||||||
return
|
return
|
||||||
if "401" in err_str or "403" in err_str or "unauthorized" in err_str:
|
if "401" in err_str or "403" in err_str or "unauthorized" in err_str:
|
||||||
|
|
@ -649,21 +649,21 @@ class MattermostAdapter(BasePlatformAdapter):
|
||||||
if self._closing:
|
if self._closing:
|
||||||
return
|
return
|
||||||
|
|
||||||
if raw_msg.type in (
|
if raw_msg.type in {
|
||||||
raw_msg.type.TEXT,
|
raw_msg.type.TEXT,
|
||||||
raw_msg.type.BINARY,
|
raw_msg.type.BINARY,
|
||||||
):
|
}:
|
||||||
try:
|
try:
|
||||||
event = json.loads(raw_msg.data)
|
event = json.loads(raw_msg.data)
|
||||||
except (json.JSONDecodeError, TypeError):
|
except (json.JSONDecodeError, TypeError):
|
||||||
continue
|
continue
|
||||||
await self._handle_ws_event(event)
|
await self._handle_ws_event(event)
|
||||||
elif raw_msg.type in (
|
elif raw_msg.type in {
|
||||||
raw_msg.type.ERROR,
|
raw_msg.type.ERROR,
|
||||||
raw_msg.type.CLOSE,
|
raw_msg.type.CLOSE,
|
||||||
raw_msg.type.CLOSING,
|
raw_msg.type.CLOSING,
|
||||||
raw_msg.type.CLOSED,
|
raw_msg.type.CLOSED,
|
||||||
):
|
}:
|
||||||
logger.info("Mattermost: WebSocket closed (%s)", raw_msg.type)
|
logger.info("Mattermost: WebSocket closed (%s)", raw_msg.type)
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|
@ -732,7 +732,7 @@ class MattermostAdapter(BasePlatformAdapter):
|
||||||
|
|
||||||
require_mention = os.getenv(
|
require_mention = os.getenv(
|
||||||
"MATTERMOST_REQUIRE_MENTION", "true"
|
"MATTERMOST_REQUIRE_MENTION", "true"
|
||||||
).lower() not in ("false", "0", "no")
|
).lower() not in {"false", "0", "no"}
|
||||||
|
|
||||||
free_channels_raw = os.getenv("MATTERMOST_FREE_RESPONSE_CHANNELS", "")
|
free_channels_raw = os.getenv("MATTERMOST_FREE_RESPONSE_CHANNELS", "")
|
||||||
free_channels = {ch.strip() for ch in free_channels_raw.split(",") if ch.strip()}
|
free_channels = {ch.strip() for ch in free_channels_raw.split(",") if ch.strip()}
|
||||||
|
|
|
||||||
|
|
@ -513,7 +513,7 @@ class QQAdapter(BasePlatformAdapter):
|
||||||
self._fail_pending("Connection closed")
|
self._fail_pending("Connection closed")
|
||||||
|
|
||||||
# Stop reconnecting for fatal codes
|
# Stop reconnecting for fatal codes
|
||||||
if code in (4914, 4915):
|
if code in {4914, 4915}:
|
||||||
desc = "offline/sandbox-only" if code == 4914 else "banned"
|
desc = "offline/sandbox-only" if code == 4914 else "banned"
|
||||||
logger.error(
|
logger.error(
|
||||||
"[%s] Bot is %s. Check QQ Open Platform.", self._log_tag, desc
|
"[%s] Bot is %s. Check QQ Open Platform.", self._log_tag, desc
|
||||||
|
|
@ -550,7 +550,7 @@ class QQAdapter(BasePlatformAdapter):
|
||||||
self._token_expires_at = 0.0
|
self._token_expires_at = 0.0
|
||||||
|
|
||||||
# Session invalid → clear session, will re-identify on next Hello
|
# Session invalid → clear session, will re-identify on next Hello
|
||||||
if code in (
|
if code in {
|
||||||
4006,
|
4006,
|
||||||
4007,
|
4007,
|
||||||
4009,
|
4009,
|
||||||
|
|
@ -568,7 +568,7 @@ class QQAdapter(BasePlatformAdapter):
|
||||||
4911,
|
4911,
|
||||||
4912,
|
4912,
|
||||||
4913,
|
4913,
|
||||||
):
|
}:
|
||||||
logger.info(
|
logger.info(
|
||||||
"[%s] Session error (%d), clearing session for re-identify",
|
"[%s] Session error (%d), clearing session for re-identify",
|
||||||
self._log_tag,
|
self._log_tag,
|
||||||
|
|
@ -637,12 +637,12 @@ class QQAdapter(BasePlatformAdapter):
|
||||||
payload = self._parse_json(msg.data)
|
payload = self._parse_json(msg.data)
|
||||||
if payload:
|
if payload:
|
||||||
self._dispatch_payload(payload)
|
self._dispatch_payload(payload)
|
||||||
elif msg.type in (aiohttp.WSMsgType.PING,):
|
elif msg.type in {aiohttp.WSMsgType.PING,}:
|
||||||
# aiohttp auto-replies with PONG
|
# aiohttp auto-replies with PONG
|
||||||
pass
|
pass
|
||||||
elif msg.type == aiohttp.WSMsgType.CLOSE:
|
elif msg.type == aiohttp.WSMsgType.CLOSE:
|
||||||
raise QQCloseError(msg.data, msg.extra)
|
raise QQCloseError(msg.data, msg.extra)
|
||||||
elif msg.type in (aiohttp.WSMsgType.CLOSED, aiohttp.WSMsgType.ERROR):
|
elif msg.type in {aiohttp.WSMsgType.CLOSED, aiohttp.WSMsgType.ERROR}:
|
||||||
raise RuntimeError("WebSocket closed")
|
raise RuntimeError("WebSocket closed")
|
||||||
|
|
||||||
async def _heartbeat_loop(self) -> None:
|
async def _heartbeat_loop(self) -> None:
|
||||||
|
|
@ -783,13 +783,13 @@ class QQAdapter(BasePlatformAdapter):
|
||||||
self._handle_ready(d)
|
self._handle_ready(d)
|
||||||
elif t == "RESUMED":
|
elif t == "RESUMED":
|
||||||
logger.info("[%s] Session resumed", self._log_tag)
|
logger.info("[%s] Session resumed", self._log_tag)
|
||||||
elif t in (
|
elif t in {
|
||||||
"C2C_MESSAGE_CREATE",
|
"C2C_MESSAGE_CREATE",
|
||||||
"GROUP_AT_MESSAGE_CREATE",
|
"GROUP_AT_MESSAGE_CREATE",
|
||||||
"DIRECT_MESSAGE_CREATE",
|
"DIRECT_MESSAGE_CREATE",
|
||||||
"GUILD_MESSAGE_CREATE",
|
"GUILD_MESSAGE_CREATE",
|
||||||
"GUILD_AT_MESSAGE_CREATE",
|
"GUILD_AT_MESSAGE_CREATE",
|
||||||
):
|
}:
|
||||||
asyncio.create_task(self._on_message(t, d))
|
asyncio.create_task(self._on_message(t, d))
|
||||||
elif t == "INTERACTION_CREATE":
|
elif t == "INTERACTION_CREATE":
|
||||||
self._create_task(self._on_interaction(d))
|
self._create_task(self._on_interaction(d))
|
||||||
|
|
@ -859,9 +859,9 @@ class QQAdapter(BasePlatformAdapter):
|
||||||
# Route by event type
|
# Route by event type
|
||||||
if event_type == "C2C_MESSAGE_CREATE":
|
if event_type == "C2C_MESSAGE_CREATE":
|
||||||
await self._handle_c2c_message(d, msg_id, content, author, timestamp)
|
await self._handle_c2c_message(d, msg_id, content, author, timestamp)
|
||||||
elif event_type in ("GROUP_AT_MESSAGE_CREATE",):
|
elif event_type in {"GROUP_AT_MESSAGE_CREATE",}:
|
||||||
await self._handle_group_message(d, msg_id, content, author, timestamp)
|
await self._handle_group_message(d, msg_id, content, author, timestamp)
|
||||||
elif event_type in ("GUILD_MESSAGE_CREATE", "GUILD_AT_MESSAGE_CREATE"):
|
elif event_type in {"GUILD_MESSAGE_CREATE", "GUILD_AT_MESSAGE_CREATE"}:
|
||||||
await self._handle_guild_message(d, msg_id, content, author, timestamp)
|
await self._handle_guild_message(d, msg_id, content, author, timestamp)
|
||||||
elif event_type == "DIRECT_MESSAGE_CREATE":
|
elif event_type == "DIRECT_MESSAGE_CREATE":
|
||||||
await self._handle_dm_message(d, msg_id, content, author, timestamp)
|
await self._handle_dm_message(d, msg_id, content, author, timestamp)
|
||||||
|
|
@ -1864,7 +1864,7 @@ class QQAdapter(BasePlatformAdapter):
|
||||||
return ".wav"
|
return ".wav"
|
||||||
if data[:4] == b"fLaC":
|
if data[:4] == b"fLaC":
|
||||||
return ".flac"
|
return ".flac"
|
||||||
if data[:2] in (b"\xff\xfb", b"\xff\xf3", b"\xff\xf2"):
|
if data[:2] in {b"\xff\xfb", b"\xff\xf3", b"\xff\xf2"}:
|
||||||
return ".mp3"
|
return ".mp3"
|
||||||
if data[:4] == b"\x30\x26\xb2\x75" or data[:4] == b"\x4f\x67\x67\x53":
|
if data[:4] == b"\x30\x26\xb2\x75" or data[:4] == b"\x4f\x67\x67\x53":
|
||||||
return ".ogg"
|
return ".ogg"
|
||||||
|
|
@ -2033,7 +2033,7 @@ class QQAdapter(BasePlatformAdapter):
|
||||||
"base_url": base_url,
|
"base_url": base_url,
|
||||||
"api_key": api_key,
|
"api_key": api_key,
|
||||||
"model": model
|
"model": model
|
||||||
or ("glm-asr" if provider in ("zai", "glm") else "whisper-1"),
|
or ("glm-asr" if provider in {"zai", "glm"} else "whisper-1"),
|
||||||
}
|
}
|
||||||
|
|
||||||
# 2. QQ-specific env vars (set by `hermes setup gateway` / `hermes gateway`)
|
# 2. QQ-specific env vars (set by `hermes setup gateway` / `hermes gateway`)
|
||||||
|
|
@ -2115,7 +2115,7 @@ class QQAdapter(BasePlatformAdapter):
|
||||||
if urlparse(source_url).path
|
if urlparse(source_url).path
|
||||||
else ""
|
else ""
|
||||||
)
|
)
|
||||||
if not ext or ext not in (
|
if not ext or ext not in {
|
||||||
".silk",
|
".silk",
|
||||||
".amr",
|
".amr",
|
||||||
".mp3",
|
".mp3",
|
||||||
|
|
@ -2124,7 +2124,7 @@ class QQAdapter(BasePlatformAdapter):
|
||||||
".m4a",
|
".m4a",
|
||||||
".aac",
|
".aac",
|
||||||
".flac",
|
".flac",
|
||||||
):
|
}:
|
||||||
ext = self._guess_ext_from_data(audio_data)
|
ext = self._guess_ext_from_data(audio_data)
|
||||||
|
|
||||||
with tempfile.NamedTemporaryFile(suffix=ext, delete=False) as tmp_src:
|
with tempfile.NamedTemporaryFile(suffix=ext, delete=False) as tmp_src:
|
||||||
|
|
@ -2870,7 +2870,7 @@ class QQAdapter(BasePlatformAdapter):
|
||||||
raise ValueError("Media source is required")
|
raise ValueError("Media source is required")
|
||||||
|
|
||||||
parsed = urlparse(source)
|
parsed = urlparse(source)
|
||||||
if parsed.scheme in ("http", "https"):
|
if parsed.scheme in {"http", "https"}:
|
||||||
# For URLs, pass through directly to the upload API
|
# For URLs, pass through directly to the upload API
|
||||||
content_type = mimetypes.guess_type(source)[0] or "application/octet-stream"
|
content_type = mimetypes.guess_type(source)[0] or "application/octet-stream"
|
||||||
resolved_name = file_name or Path(parsed.path).name or "media"
|
resolved_name = file_name or Path(parsed.path).name or "media"
|
||||||
|
|
@ -2966,7 +2966,7 @@ class QQAdapter(BasePlatformAdapter):
|
||||||
chat_type = self._guess_chat_type(chat_id)
|
chat_type = self._guess_chat_type(chat_id)
|
||||||
return {
|
return {
|
||||||
"name": chat_id,
|
"name": chat_id,
|
||||||
"type": "group" if chat_type in ("group", "guild") else "dm",
|
"type": "group" if chat_type in {"group", "guild"} else "dm",
|
||||||
}
|
}
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
|
|
@ -2975,7 +2975,7 @@ class QQAdapter(BasePlatformAdapter):
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _is_url(source: str) -> bool:
|
def _is_url(source: str) -> bool:
|
||||||
return urlparse(str(source)).scheme in ("http", "https")
|
return urlparse(str(source)).scheme in {"http", "https"}
|
||||||
|
|
||||||
def _guess_chat_type(self, chat_id: str) -> str:
|
def _guess_chat_type(self, chat_id: str) -> str:
|
||||||
"""Determine chat type from stored inbound metadata, fallback to 'c2c'."""
|
"""Determine chat type from stored inbound metadata, fallback to 'c2c'."""
|
||||||
|
|
|
||||||
|
|
@ -239,7 +239,7 @@ class ChunkedUploader:
|
||||||
:raises UploadFileTooLargeError: When the file exceeds the platform limit.
|
:raises UploadFileTooLargeError: When the file exceeds the platform limit.
|
||||||
:raises RuntimeError: On other API or I/O failures.
|
:raises RuntimeError: On other API or I/O failures.
|
||||||
"""
|
"""
|
||||||
if chat_type not in ("c2c", "group"):
|
if chat_type not in {"c2c", "group"}:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"ChunkedUploader: unsupported chat_type {chat_type!r}"
|
f"ChunkedUploader: unsupported chat_type {chat_type!r}"
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -99,11 +99,11 @@ def _guess_extension(data: bytes) -> str:
|
||||||
|
|
||||||
|
|
||||||
def _is_image_ext(ext: str) -> bool:
|
def _is_image_ext(ext: str) -> bool:
|
||||||
return ext.lower() in (".jpg", ".jpeg", ".png", ".gif", ".webp")
|
return ext.lower() in {".jpg", ".jpeg", ".png", ".gif", ".webp"}
|
||||||
|
|
||||||
|
|
||||||
def _is_audio_ext(ext: str) -> bool:
|
def _is_audio_ext(ext: str) -> bool:
|
||||||
return ext.lower() in (".mp3", ".wav", ".ogg", ".m4a", ".aac")
|
return ext.lower() in {".mp3", ".wav", ".ogg", ".m4a", ".aac"}
|
||||||
|
|
||||||
|
|
||||||
_EXT_TO_MIME = {
|
_EXT_TO_MIME = {
|
||||||
|
|
@ -1449,7 +1449,7 @@ class SignalAdapter(BasePlatformAdapter):
|
||||||
contacts from seeing the 👀 reaction (which fires before run.py's
|
contacts from seeing the 👀 reaction (which fires before run.py's
|
||||||
auth gate and would otherwise reveal that a bot is listening).
|
auth gate and would otherwise reveal that a bot is listening).
|
||||||
"""
|
"""
|
||||||
if os.getenv("SIGNAL_REACTIONS", "true").lower() in ("false", "0", "no"):
|
if os.getenv("SIGNAL_REACTIONS", "true").lower() in {"false", "0", "no"}:
|
||||||
return False
|
return False
|
||||||
if event is not None:
|
if event is not None:
|
||||||
sender = getattr(getattr(event, "source", None), "user_id", None)
|
sender = getattr(getattr(event, "source", None), "user_id", None)
|
||||||
|
|
|
||||||
|
|
@ -935,7 +935,7 @@ class SlackAdapter(BasePlatformAdapter):
|
||||||
raw = self.config.extra.get("dm_top_level_threads_as_sessions")
|
raw = self.config.extra.get("dm_top_level_threads_as_sessions")
|
||||||
if raw is None:
|
if raw is None:
|
||||||
return True # default: each DM thread is its own session
|
return True # default: each DM thread is its own session
|
||||||
return str(raw).strip().lower() in ("1", "true", "yes", "on")
|
return str(raw).strip().lower() in {"1", "true", "yes", "on"}
|
||||||
|
|
||||||
def _resolve_thread_ts(
|
def _resolve_thread_ts(
|
||||||
self,
|
self,
|
||||||
|
|
@ -1300,7 +1300,7 @@ class SlackAdapter(BasePlatformAdapter):
|
||||||
|
|
||||||
def _reactions_enabled(self) -> bool:
|
def _reactions_enabled(self) -> bool:
|
||||||
"""Check if message reactions are enabled via config/env."""
|
"""Check if message reactions are enabled via config/env."""
|
||||||
return os.getenv("SLACK_REACTIONS", "true").lower() not in ("false", "0", "no")
|
return os.getenv("SLACK_REACTIONS", "true").lower() not in {"false", "0", "no"}
|
||||||
|
|
||||||
async def on_processing_start(self, event: MessageEvent) -> None:
|
async def on_processing_start(self, event: MessageEvent) -> None:
|
||||||
"""Add an in-progress reaction when message processing begins."""
|
"""Add an in-progress reaction when message processing begins."""
|
||||||
|
|
@ -1773,7 +1773,7 @@ class SlackAdapter(BasePlatformAdapter):
|
||||||
|
|
||||||
# Ignore message edits and deletions
|
# Ignore message edits and deletions
|
||||||
subtype = event.get("subtype")
|
subtype = event.get("subtype")
|
||||||
if subtype in ("message_changed", "message_deleted"):
|
if subtype in {"message_changed", "message_deleted"}:
|
||||||
return
|
return
|
||||||
|
|
||||||
original_text = event.get("text", "")
|
original_text = event.get("text", "")
|
||||||
|
|
@ -1892,7 +1892,7 @@ class SlackAdapter(BasePlatformAdapter):
|
||||||
channel_type = event.get("channel_type", "")
|
channel_type = event.get("channel_type", "")
|
||||||
if not channel_type and channel_id.startswith("D"):
|
if not channel_type and channel_id.startswith("D"):
|
||||||
channel_type = "im"
|
channel_type = "im"
|
||||||
is_dm = channel_type in ("im", "mpim") # Both 1:1 and group DMs
|
is_dm = channel_type in {"im", "mpim"} # Both 1:1 and group DMs
|
||||||
|
|
||||||
# Build thread_ts for session keying.
|
# Build thread_ts for session keying.
|
||||||
# In channels: fall back to ts so each top-level @mention starts a
|
# In channels: fall back to ts so each top-level @mention starts a
|
||||||
|
|
@ -2033,7 +2033,7 @@ class SlackAdapter(BasePlatformAdapter):
|
||||||
if mimetype.startswith("image/") and url:
|
if mimetype.startswith("image/") and url:
|
||||||
try:
|
try:
|
||||||
ext = "." + mimetype.split("/")[-1].split(";")[0]
|
ext = "." + mimetype.split("/")[-1].split(";")[0]
|
||||||
if ext not in (".jpg", ".jpeg", ".png", ".gif", ".webp"):
|
if ext not in {".jpg", ".jpeg", ".png", ".gif", ".webp"}:
|
||||||
ext = ".jpg"
|
ext = ".jpg"
|
||||||
# Slack private URLs require the bot token as auth header
|
# Slack private URLs require the bot token as auth header
|
||||||
cached = await self._download_slack_file(url, ext, team_id=team_id)
|
cached = await self._download_slack_file(url, ext, team_id=team_id)
|
||||||
|
|
@ -2049,7 +2049,7 @@ class SlackAdapter(BasePlatformAdapter):
|
||||||
elif mimetype.startswith("audio/") and url:
|
elif mimetype.startswith("audio/") and url:
|
||||||
try:
|
try:
|
||||||
ext = "." + mimetype.split("/")[-1].split(";")[0]
|
ext = "." + mimetype.split("/")[-1].split(";")[0]
|
||||||
if ext not in (".ogg", ".mp3", ".wav", ".webm", ".m4a"):
|
if ext not in {".ogg", ".mp3", ".wav", ".webm", ".m4a"}:
|
||||||
ext = ".ogg"
|
ext = ".ogg"
|
||||||
cached = await self._download_slack_file(url, ext, audio=True, team_id=team_id)
|
cached = await self._download_slack_file(url, ext, audio=True, team_id=team_id)
|
||||||
media_urls.append(cached)
|
media_urls.append(cached)
|
||||||
|
|
@ -2737,7 +2737,7 @@ class SlackAdapter(BasePlatformAdapter):
|
||||||
if team_id and channel_id:
|
if team_id and channel_id:
|
||||||
self._channel_team[channel_id] = team_id
|
self._channel_team[channel_id] = team_id
|
||||||
|
|
||||||
if slash_name in ("hermes", ""):
|
if slash_name in {"hermes", ""}:
|
||||||
# Legacy /hermes <subcommand> [args] routing + free-form questions.
|
# Legacy /hermes <subcommand> [args] routing + free-form questions.
|
||||||
# Empty slash_name falls into this branch for backward compat
|
# Empty slash_name falls into this branch for backward compat
|
||||||
# with any caller that didn't populate command["command"].
|
# with any caller that didn't populate command["command"].
|
||||||
|
|
@ -2932,9 +2932,9 @@ class SlackAdapter(BasePlatformAdapter):
|
||||||
configured = self.config.extra.get("require_mention")
|
configured = self.config.extra.get("require_mention")
|
||||||
if configured is not None:
|
if configured is not None:
|
||||||
if isinstance(configured, str):
|
if isinstance(configured, str):
|
||||||
return configured.lower() not in ("false", "0", "no", "off")
|
return configured.lower() not in {"false", "0", "no", "off"}
|
||||||
return bool(configured)
|
return bool(configured)
|
||||||
return os.getenv("SLACK_REQUIRE_MENTION", "true").lower() not in ("false", "0", "no", "off")
|
return os.getenv("SLACK_REQUIRE_MENTION", "true").lower() not in {"false", "0", "no", "off"}
|
||||||
|
|
||||||
def _slack_strict_mention(self) -> bool:
|
def _slack_strict_mention(self) -> bool:
|
||||||
"""When true, channel threads require an explicit @-mention on every
|
"""When true, channel threads require an explicit @-mention on every
|
||||||
|
|
@ -2944,9 +2944,9 @@ class SlackAdapter(BasePlatformAdapter):
|
||||||
configured = self.config.extra.get("strict_mention")
|
configured = self.config.extra.get("strict_mention")
|
||||||
if configured is not None:
|
if configured is not None:
|
||||||
if isinstance(configured, str):
|
if isinstance(configured, str):
|
||||||
return configured.lower() in ("true", "1", "yes", "on")
|
return configured.lower() in {"true", "1", "yes", "on"}
|
||||||
return bool(configured)
|
return bool(configured)
|
||||||
return os.getenv("SLACK_STRICT_MENTION", "false").lower() in ("true", "1", "yes", "on")
|
return os.getenv("SLACK_STRICT_MENTION", "false").lower() in {"true", "1", "yes", "on"}
|
||||||
|
|
||||||
def _slack_free_response_channels(self) -> set:
|
def _slack_free_response_channels(self) -> set:
|
||||||
"""Return channel IDs where no @mention is required."""
|
"""Return channel IDs where no @mention is required."""
|
||||||
|
|
|
||||||
|
|
@ -616,7 +616,7 @@ class TelegramAdapter(BasePlatformAdapter):
|
||||||
def _looks_like_network_error(error: Exception) -> bool:
|
def _looks_like_network_error(error: Exception) -> bool:
|
||||||
"""Return True for transient network errors that warrant a reconnect attempt."""
|
"""Return True for transient network errors that warrant a reconnect attempt."""
|
||||||
name = error.__class__.__name__.lower()
|
name = error.__class__.__name__.lower()
|
||||||
if name in ("networkerror", "timedout", "connectionerror"):
|
if name in {"networkerror", "timedout", "connectionerror"}:
|
||||||
return True
|
return True
|
||||||
try:
|
try:
|
||||||
from telegram.error import NetworkError, TimedOut
|
from telegram.error import NetworkError, TimedOut
|
||||||
|
|
@ -632,9 +632,9 @@ class TelegramAdapter(BasePlatformAdapter):
|
||||||
return default
|
return default
|
||||||
if isinstance(value, str):
|
if isinstance(value, str):
|
||||||
lowered = value.strip().lower()
|
lowered = value.strip().lower()
|
||||||
if lowered in ("true", "1", "yes", "on"):
|
if lowered in {"true", "1", "yes", "on"}:
|
||||||
return True
|
return True
|
||||||
if lowered in ("false", "0", "no", "off"):
|
if lowered in {"false", "0", "no", "off"}:
|
||||||
return False
|
return False
|
||||||
return default
|
return default
|
||||||
return bool(value)
|
return bool(value)
|
||||||
|
|
@ -1171,7 +1171,7 @@ class TelegramAdapter(BasePlatformAdapter):
|
||||||
"write_timeout": _env_float("HERMES_TELEGRAM_HTTP_WRITE_TIMEOUT", 20.0),
|
"write_timeout": _env_float("HERMES_TELEGRAM_HTTP_WRITE_TIMEOUT", 20.0),
|
||||||
}
|
}
|
||||||
|
|
||||||
disable_fallback = (os.getenv("HERMES_TELEGRAM_DISABLE_FALLBACK_IPS", "").strip().lower() in ("1", "true", "yes", "on"))
|
disable_fallback = (os.getenv("HERMES_TELEGRAM_DISABLE_FALLBACK_IPS", "").strip().lower() in {"1", "true", "yes", "on"})
|
||||||
fallback_ips = self._fallback_ips()
|
fallback_ips = self._fallback_ips()
|
||||||
if not fallback_ips:
|
if not fallback_ips:
|
||||||
fallback_ips = await discover_fallback_ips()
|
fallback_ips = await discover_fallback_ips()
|
||||||
|
|
@ -1917,7 +1917,7 @@ class TelegramAdapter(BasePlatformAdapter):
|
||||||
"""
|
"""
|
||||||
if not self._bot or not hasattr(self._bot, "send_message_draft"):
|
if not self._bot or not hasattr(self._bot, "send_message_draft"):
|
||||||
return False
|
return False
|
||||||
return (chat_type or "").lower() in ("dm", "private")
|
return (chat_type or "").lower() in {"dm", "private"}
|
||||||
|
|
||||||
async def send_draft(
|
async def send_draft(
|
||||||
self,
|
self,
|
||||||
|
|
@ -2723,7 +2723,7 @@ class TelegramAdapter(BasePlatformAdapter):
|
||||||
with open(audio_path, "rb") as audio_file:
|
with open(audio_path, "rb") as audio_file:
|
||||||
ext = os.path.splitext(audio_path)[1].lower()
|
ext = os.path.splitext(audio_path)[1].lower()
|
||||||
# .ogg / .opus files -> send as voice (round playable bubble)
|
# .ogg / .opus files -> send as voice (round playable bubble)
|
||||||
if ext in (".ogg", ".opus"):
|
if ext in {".ogg", ".opus"}:
|
||||||
_voice_thread = self._metadata_thread_id(metadata)
|
_voice_thread = self._metadata_thread_id(metadata)
|
||||||
reply_to_id = self._reply_to_message_id_for_send(reply_to, metadata)
|
reply_to_id = self._reply_to_message_id_for_send(reply_to, metadata)
|
||||||
voice_thread_kwargs = self._thread_kwargs_for_send(
|
voice_thread_kwargs = self._thread_kwargs_for_send(
|
||||||
|
|
@ -2747,7 +2747,7 @@ class TelegramAdapter(BasePlatformAdapter):
|
||||||
"voice",
|
"voice",
|
||||||
reset_media=lambda: audio_file.seek(0),
|
reset_media=lambda: audio_file.seek(0),
|
||||||
)
|
)
|
||||||
elif ext in (".mp3", ".m4a"):
|
elif ext in {".mp3", ".m4a"}:
|
||||||
# Telegram's Bot API sendAudio only accepts MP3 / M4A.
|
# Telegram's Bot API sendAudio only accepts MP3 / M4A.
|
||||||
_audio_thread = self._metadata_thread_id(metadata)
|
_audio_thread = self._metadata_thread_id(metadata)
|
||||||
reply_to_id = self._reply_to_message_id_for_send(reply_to, metadata)
|
reply_to_id = self._reply_to_message_id_for_send(reply_to, metadata)
|
||||||
|
|
@ -3498,18 +3498,18 @@ class TelegramAdapter(BasePlatformAdapter):
|
||||||
configured = self.config.extra.get("require_mention")
|
configured = self.config.extra.get("require_mention")
|
||||||
if configured is not None:
|
if configured is not None:
|
||||||
if isinstance(configured, str):
|
if isinstance(configured, str):
|
||||||
return configured.lower() in ("true", "1", "yes", "on")
|
return configured.lower() in {"true", "1", "yes", "on"}
|
||||||
return bool(configured)
|
return bool(configured)
|
||||||
return os.getenv("TELEGRAM_REQUIRE_MENTION", "false").lower() in ("true", "1", "yes", "on")
|
return os.getenv("TELEGRAM_REQUIRE_MENTION", "false").lower() in {"true", "1", "yes", "on"}
|
||||||
|
|
||||||
def _telegram_guest_mode(self) -> bool:
|
def _telegram_guest_mode(self) -> bool:
|
||||||
"""Return whether non-allowlisted groups may trigger via direct @mention."""
|
"""Return whether non-allowlisted groups may trigger via direct @mention."""
|
||||||
configured = self.config.extra.get("guest_mode")
|
configured = self.config.extra.get("guest_mode")
|
||||||
if configured is not None:
|
if configured is not None:
|
||||||
if isinstance(configured, str):
|
if isinstance(configured, str):
|
||||||
return configured.lower() in ("true", "1", "yes", "on")
|
return configured.lower() in {"true", "1", "yes", "on"}
|
||||||
return bool(configured)
|
return bool(configured)
|
||||||
return os.getenv("TELEGRAM_GUEST_MODE", "false").lower() in ("true", "1", "yes", "on")
|
return os.getenv("TELEGRAM_GUEST_MODE", "false").lower() in {"true", "1", "yes", "on"}
|
||||||
|
|
||||||
def _telegram_free_response_chats(self) -> set[str]:
|
def _telegram_free_response_chats(self) -> set[str]:
|
||||||
raw = self.config.extra.get("free_response_chats")
|
raw = self.config.extra.get("free_response_chats")
|
||||||
|
|
@ -3598,7 +3598,7 @@ class TelegramAdapter(BasePlatformAdapter):
|
||||||
if not chat:
|
if not chat:
|
||||||
return False
|
return False
|
||||||
chat_type = str(getattr(chat, "type", "")).split(".")[-1].lower()
|
chat_type = str(getattr(chat, "type", "")).split(".")[-1].lower()
|
||||||
return chat_type in ("group", "supergroup")
|
return chat_type in {"group", "supergroup"}
|
||||||
|
|
||||||
def _is_reply_to_bot(self, message: Message) -> bool:
|
def _is_reply_to_bot(self, message: Message) -> bool:
|
||||||
if not self._bot or not getattr(message, "reply_to_message", None):
|
if not self._bot or not getattr(message, "reply_to_message", None):
|
||||||
|
|
@ -4157,7 +4157,7 @@ class TelegramAdapter(BasePlatformAdapter):
|
||||||
|
|
||||||
# For text files, inject content into event.text (capped at 100 KB)
|
# For text files, inject content into event.text (capped at 100 KB)
|
||||||
MAX_TEXT_INJECT_BYTES = 100 * 1024
|
MAX_TEXT_INJECT_BYTES = 100 * 1024
|
||||||
if ext in (".md", ".txt") and len(raw_bytes) <= MAX_TEXT_INJECT_BYTES:
|
if ext in {".md", ".txt"} and len(raw_bytes) <= MAX_TEXT_INJECT_BYTES:
|
||||||
try:
|
try:
|
||||||
text_content = raw_bytes.decode("utf-8")
|
text_content = raw_bytes.decode("utf-8")
|
||||||
display_name = original_filename or f"document{ext}"
|
display_name = original_filename or f"document{ext}"
|
||||||
|
|
@ -4396,7 +4396,7 @@ class TelegramAdapter(BasePlatformAdapter):
|
||||||
|
|
||||||
# Determine chat type
|
# Determine chat type
|
||||||
chat_type = "dm"
|
chat_type = "dm"
|
||||||
if chat.type in (ChatType.GROUP, ChatType.SUPERGROUP):
|
if chat.type in {ChatType.GROUP, ChatType.SUPERGROUP}:
|
||||||
chat_type = "group"
|
chat_type = "group"
|
||||||
elif chat.type == ChatType.CHANNEL:
|
elif chat.type == ChatType.CHANNEL:
|
||||||
chat_type = "channel"
|
chat_type = "channel"
|
||||||
|
|
@ -4512,7 +4512,7 @@ class TelegramAdapter(BasePlatformAdapter):
|
||||||
|
|
||||||
def _reactions_enabled(self) -> bool:
|
def _reactions_enabled(self) -> bool:
|
||||||
"""Check if message reactions are enabled via config/env."""
|
"""Check if message reactions are enabled via config/env."""
|
||||||
return os.getenv("TELEGRAM_REACTIONS", "false").lower() not in ("false", "0", "no")
|
return os.getenv("TELEGRAM_REACTIONS", "false").lower() not in {"false", "0", "no"}
|
||||||
|
|
||||||
async def _set_reaction(self, chat_id: str, message_id: str, emoji: str) -> bool:
|
async def _set_reaction(self, chat_id: str, message_id: str, emoji: str) -> bool:
|
||||||
"""Set a single emoji reaction on a Telegram message."""
|
"""Set a single emoji reaction on a Telegram message."""
|
||||||
|
|
|
||||||
|
|
@ -295,7 +295,7 @@ class WeComAdapter(BasePlatformAdapter):
|
||||||
|
|
||||||
auth_payload = await self._wait_for_handshake(req_id)
|
auth_payload = await self._wait_for_handshake(req_id)
|
||||||
errcode = auth_payload.get("errcode", 0)
|
errcode = auth_payload.get("errcode", 0)
|
||||||
if errcode not in (0, None):
|
if errcode not in {0, None}:
|
||||||
errmsg = auth_payload.get("errmsg", "authentication failed")
|
errmsg = auth_payload.get("errmsg", "authentication failed")
|
||||||
raise RuntimeError(f"{errmsg} (errcode={errcode})")
|
raise RuntimeError(f"{errmsg} (errcode={errcode})")
|
||||||
|
|
||||||
|
|
@ -320,7 +320,7 @@ class WeComAdapter(BasePlatformAdapter):
|
||||||
if self._payload_req_id(payload) == req_id:
|
if self._payload_req_id(payload) == req_id:
|
||||||
return payload
|
return payload
|
||||||
logger.debug("[%s] Ignoring pre-auth payload: %s", self.name, payload.get("cmd"))
|
logger.debug("[%s] Ignoring pre-auth payload: %s", self.name, payload.get("cmd"))
|
||||||
elif msg.type in (aiohttp.WSMsgType.CLOSED, aiohttp.WSMsgType.CLOSE, aiohttp.WSMsgType.ERROR):
|
elif msg.type in {aiohttp.WSMsgType.CLOSED, aiohttp.WSMsgType.CLOSE, aiohttp.WSMsgType.ERROR}:
|
||||||
raise RuntimeError("WeCom websocket closed during authentication")
|
raise RuntimeError("WeCom websocket closed during authentication")
|
||||||
|
|
||||||
async def _listen_loop(self) -> None:
|
async def _listen_loop(self) -> None:
|
||||||
|
|
@ -360,7 +360,7 @@ class WeComAdapter(BasePlatformAdapter):
|
||||||
payload = self._parse_json(msg.data)
|
payload = self._parse_json(msg.data)
|
||||||
if payload:
|
if payload:
|
||||||
await self._dispatch_payload(payload)
|
await self._dispatch_payload(payload)
|
||||||
elif msg.type in (aiohttp.WSMsgType.CLOSE, aiohttp.WSMsgType.CLOSED, aiohttp.WSMsgType.ERROR):
|
elif msg.type in {aiohttp.WSMsgType.CLOSE, aiohttp.WSMsgType.CLOSED, aiohttp.WSMsgType.ERROR}:
|
||||||
raise RuntimeError("WeCom websocket closed")
|
raise RuntimeError("WeCom websocket closed")
|
||||||
|
|
||||||
async def _heartbeat_loop(self) -> None:
|
async def _heartbeat_loop(self) -> None:
|
||||||
|
|
@ -998,7 +998,7 @@ class WeComAdapter(BasePlatformAdapter):
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _response_error(response: Dict[str, Any]) -> Optional[str]:
|
def _response_error(response: Dict[str, Any]) -> Optional[str]:
|
||||||
errcode = response.get("errcode", 0)
|
errcode = response.get("errcode", 0)
|
||||||
if errcode in (0, None):
|
if errcode in {0, None}:
|
||||||
return None
|
return None
|
||||||
errmsg = str(response.get("errmsg") or "unknown error")
|
errmsg = str(response.get("errmsg") or "unknown error")
|
||||||
return f"WeCom errcode {errcode}: {errmsg}"
|
return f"WeCom errcode {errcode}: {errmsg}"
|
||||||
|
|
|
||||||
|
|
@ -605,7 +605,7 @@ def _assert_weixin_cdn_url(url: str) -> None:
|
||||||
except Exception as exc: # noqa: BLE001
|
except Exception as exc: # noqa: BLE001
|
||||||
raise ValueError(f"Unparseable media URL: {url!r}") from exc
|
raise ValueError(f"Unparseable media URL: {url!r}") from exc
|
||||||
|
|
||||||
if scheme not in ("http", "https"):
|
if scheme not in {"http", "https"}:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Media URL has disallowed scheme {scheme!r}; only http/https are permitted."
|
f"Media URL has disallowed scheme {scheme!r}; only http/https are permitted."
|
||||||
)
|
)
|
||||||
|
|
@ -983,7 +983,7 @@ def _extract_text(item_list: List[Dict[str, Any]]) -> str:
|
||||||
ref = item.get("ref_msg") or {}
|
ref = item.get("ref_msg") or {}
|
||||||
ref_item = ref.get("message_item") or {}
|
ref_item = ref.get("message_item") or {}
|
||||||
ref_type = ref_item.get("type")
|
ref_type = ref_item.get("type")
|
||||||
if ref_type in (ITEM_IMAGE, ITEM_VIDEO, ITEM_FILE, ITEM_VOICE):
|
if ref_type in {ITEM_IMAGE, ITEM_VIDEO, ITEM_FILE, ITEM_VOICE}:
|
||||||
title = ref.get("title") or ""
|
title = ref.get("title") or ""
|
||||||
prefix = f"[引用媒体: {title}]\n" if title else "[引用媒体]\n"
|
prefix = f"[引用媒体: {title}]\n" if title else "[引用媒体]\n"
|
||||||
return f"{prefix}{text}".strip()
|
return f"{prefix}{text}".strip()
|
||||||
|
|
@ -1331,7 +1331,7 @@ class WeixinAdapter(BasePlatformAdapter):
|
||||||
|
|
||||||
ret = response.get("ret", 0)
|
ret = response.get("ret", 0)
|
||||||
errcode = response.get("errcode", 0)
|
errcode = response.get("errcode", 0)
|
||||||
if ret not in (0, None) or errcode not in (0, None):
|
if ret not in {0, None} or errcode not in {0, None}:
|
||||||
if (ret == SESSION_EXPIRED_ERRCODE or errcode == SESSION_EXPIRED_ERRCODE
|
if (ret == SESSION_EXPIRED_ERRCODE or errcode == SESSION_EXPIRED_ERRCODE
|
||||||
or _is_stale_session_ret(ret, errcode, response.get("errmsg"))):
|
or _is_stale_session_ret(ret, errcode, response.get("errmsg"))):
|
||||||
logger.error("[%s] Session expired; pausing for 10 minutes", self.name)
|
logger.error("[%s] Session expired; pausing for 10 minutes", self.name)
|
||||||
|
|
@ -1601,7 +1601,7 @@ class WeixinAdapter(BasePlatformAdapter):
|
||||||
if resp and isinstance(resp, dict):
|
if resp and isinstance(resp, dict):
|
||||||
ret = resp.get("ret")
|
ret = resp.get("ret")
|
||||||
errcode = resp.get("errcode")
|
errcode = resp.get("errcode")
|
||||||
if (ret is not None and ret not in (0,)) or (errcode is not None and errcode not in (0,)):
|
if (ret is not None and ret not in {0,}) or (errcode is not None and errcode not in {0,}):
|
||||||
is_session_expired = (
|
is_session_expired = (
|
||||||
ret == SESSION_EXPIRED_ERRCODE
|
ret == SESSION_EXPIRED_ERRCODE
|
||||||
or errcode == SESSION_EXPIRED_ERRCODE
|
or errcode == SESSION_EXPIRED_ERRCODE
|
||||||
|
|
|
||||||
|
|
@ -301,9 +301,9 @@ class WhatsAppAdapter(BasePlatformAdapter):
|
||||||
configured = self.config.extra.get("require_mention")
|
configured = self.config.extra.get("require_mention")
|
||||||
if configured is not None:
|
if configured is not None:
|
||||||
if isinstance(configured, str):
|
if isinstance(configured, str):
|
||||||
return configured.lower() in ("true", "1", "yes", "on")
|
return configured.lower() in {"true", "1", "yes", "on"}
|
||||||
return bool(configured)
|
return bool(configured)
|
||||||
return os.getenv("WHATSAPP_REQUIRE_MENTION", "false").lower() in ("true", "1", "yes", "on")
|
return os.getenv("WHATSAPP_REQUIRE_MENTION", "false").lower() in {"true", "1", "yes", "on"}
|
||||||
|
|
||||||
def _whatsapp_free_response_chats(self) -> set[str]:
|
def _whatsapp_free_response_chats(self) -> set[str]:
|
||||||
raw = self.config.extra.get("free_response_chats")
|
raw = self.config.extra.get("free_response_chats")
|
||||||
|
|
@ -679,7 +679,7 @@ class WhatsAppAdapter(BasePlatformAdapter):
|
||||||
# getattr-with-default keeps tests that construct the adapter via
|
# getattr-with-default keeps tests that construct the adapter via
|
||||||
# ``WhatsAppAdapter.__new__`` (bypassing __init__) working without
|
# ``WhatsAppAdapter.__new__`` (bypassing __init__) working without
|
||||||
# every _make_adapter() helper having to seed the attribute.
|
# every _make_adapter() helper having to seed the attribute.
|
||||||
if getattr(self, "_shutting_down", False) and returncode in (0, -2, -15):
|
if getattr(self, "_shutting_down", False) and returncode in {0, -2, -15}:
|
||||||
logger.info(
|
logger.info(
|
||||||
"[%s] Bridge exited during shutdown (code %d).",
|
"[%s] Bridge exited during shutdown (code %d).",
|
||||||
self.name,
|
self.name,
|
||||||
|
|
@ -1183,7 +1183,7 @@ class WhatsAppAdapter(BasePlatformAdapter):
|
||||||
if msg_type == MessageType.DOCUMENT and cached_urls:
|
if msg_type == MessageType.DOCUMENT and cached_urls:
|
||||||
for doc_path in cached_urls:
|
for doc_path in cached_urls:
|
||||||
ext = Path(doc_path).suffix.lower()
|
ext = Path(doc_path).suffix.lower()
|
||||||
if ext in (".txt", ".md", ".csv", ".json", ".xml", ".yaml", ".yml", ".log", ".py", ".js", ".ts", ".html", ".css"):
|
if ext in {".txt", ".md", ".csv", ".json", ".xml", ".yaml", ".yml", ".log", ".py", ".js", ".ts", ".html", ".css"}:
|
||||||
try:
|
try:
|
||||||
file_size = Path(doc_path).stat().st_size
|
file_size = Path(doc_path).stat().st_size
|
||||||
if file_size > MAX_TEXT_INJECT_BYTES:
|
if file_size > MAX_TEXT_INJECT_BYTES:
|
||||||
|
|
|
||||||
|
|
@ -2228,7 +2228,7 @@ class MediaResolveMiddleware(InboundMiddleware):
|
||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
payload = resp.json()
|
payload = resp.json()
|
||||||
code = payload.get("code")
|
code = payload.get("code")
|
||||||
if code not in (None, 0):
|
if code not in {None, 0}:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
f"resource/v1/download failed: code={code}, msg={payload.get('msg', '')}"
|
f"resource/v1/download failed: code={code}, msg={payload.get('msg', '')}"
|
||||||
)
|
)
|
||||||
|
|
@ -2391,7 +2391,7 @@ class MediaResolveMiddleware(InboundMiddleware):
|
||||||
rid = m.group(2)
|
rid = m.group(2)
|
||||||
kind, _, filename = head.partition(":")
|
kind, _, filename = head.partition(":")
|
||||||
kind = kind.strip()
|
kind = kind.strip()
|
||||||
if kind not in ("image", "file"):
|
if kind not in {"image", "file"}:
|
||||||
continue
|
continue
|
||||||
if rid in seen:
|
if rid in seen:
|
||||||
continue
|
continue
|
||||||
|
|
@ -2993,10 +2993,10 @@ class ConnectionManager:
|
||||||
|
|
||||||
# Fire-and-forget heartbeat ACKs — server always responds but callers don't
|
# Fire-and-forget heartbeat ACKs — server always responds but callers don't
|
||||||
# wait on these; silently discard to avoid "Unmatched Response" noise.
|
# wait on these; silently discard to avoid "Unmatched Response" noise.
|
||||||
if cmd_type == CMD_TYPE["Response"] and cmd in (
|
if cmd_type == CMD_TYPE["Response"] and cmd in {
|
||||||
"send_group_heartbeat",
|
"send_group_heartbeat",
|
||||||
"send_private_heartbeat",
|
"send_private_heartbeat",
|
||||||
):
|
}:
|
||||||
logger.debug("[%s] Heartbeat ACK received: cmd=%s msg_id=%s", adapter.name, cmd, msg_id)
|
logger.debug("[%s] Heartbeat ACK received: cmd=%s msg_id=%s", adapter.name, cmd, msg_id)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
@ -3369,7 +3369,7 @@ class MediaSendHandler(ABC):
|
||||||
# Remove keys already passed explicitly to avoid "multiple values" TypeError
|
# Remove keys already passed explicitly to avoid "multiple values" TypeError
|
||||||
fwd_kwargs = {
|
fwd_kwargs = {
|
||||||
k: v for k, v in kwargs.items()
|
k: v for k, v in kwargs.items()
|
||||||
if k not in ("file_uuid", "filename", "content_type")
|
if k not in {"file_uuid", "filename", "content_type"}
|
||||||
}
|
}
|
||||||
msg_body = self.build_msg_body(
|
msg_body = self.build_msg_body(
|
||||||
upload_result,
|
upload_result,
|
||||||
|
|
|
||||||
|
|
@ -150,7 +150,7 @@ def _parse_jpeg_size(buf: bytes) -> Optional[dict[str, int]]:
|
||||||
i += 1
|
i += 1
|
||||||
continue
|
continue
|
||||||
marker = buf[i + 1]
|
marker = buf[i + 1]
|
||||||
if marker in (0xC0, 0xC2):
|
if marker in {0xC0, 0xC2}:
|
||||||
h = struct.unpack(">H", buf[i + 5: i + 7])[0]
|
h = struct.unpack(">H", buf[i + 5: i + 7])[0]
|
||||||
w = struct.unpack(">H", buf[i + 7: i + 9])[0]
|
w = struct.unpack(">H", buf[i + 7: i + 9])[0]
|
||||||
return {"width": w, "height": h}
|
return {"width": w, "height": h}
|
||||||
|
|
@ -165,7 +165,7 @@ def _parse_gif_size(buf: bytes) -> Optional[dict[str, int]]:
|
||||||
if len(buf) < 10:
|
if len(buf) < 10:
|
||||||
return None
|
return None
|
||||||
sig = buf[:6].decode("ascii", errors="replace")
|
sig = buf[:6].decode("ascii", errors="replace")
|
||||||
if sig not in ("GIF87a", "GIF89a"):
|
if sig not in {"GIF87a", "GIF89a"}:
|
||||||
return None
|
return None
|
||||||
w = struct.unpack("<H", buf[6:8])[0]
|
w = struct.unpack("<H", buf[6:8])[0]
|
||||||
h = struct.unpack("<H", buf[8:10])[0]
|
h = struct.unpack("<H", buf[8:10])[0]
|
||||||
|
|
|
||||||
|
|
@ -702,7 +702,7 @@ def decode_inbound_push(data: bytes) -> Optional[dict]:
|
||||||
"trace_id": trace_id,
|
"trace_id": trace_id,
|
||||||
}
|
}
|
||||||
# 过滤空值(保持 API 整洁)
|
# 过滤空值(保持 API 整洁)
|
||||||
return {k: v for k, v in result.items() if v or k in ("msg_body", "msg_seq")}
|
return {k: v for k, v in result.items() if v or k in {"msg_body", "msg_seq"}}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if DEBUG_MODE:
|
if DEBUG_MODE:
|
||||||
logger.debug("[yuanbao_proto] decode_inbound_push failed: %s", e)
|
logger.debug("[yuanbao_proto] decode_inbound_push failed: %s", e)
|
||||||
|
|
|
||||||
122
gateway/run.py
122
gateway/run.py
|
|
@ -288,7 +288,7 @@ def _last_transcript_timestamp(history: Optional[List[Dict[str, Any]]]) -> Any:
|
||||||
if not isinstance(msg, dict):
|
if not isinstance(msg, dict):
|
||||||
continue
|
continue
|
||||||
role = msg.get("role")
|
role = msg.get("role")
|
||||||
if not role or role in ("session_meta", "system"):
|
if not role or role in {"session_meta", "system"}:
|
||||||
continue
|
continue
|
||||||
ts = msg.get("timestamp")
|
ts = msg.get("timestamp")
|
||||||
if ts is not None:
|
if ts is not None:
|
||||||
|
|
@ -472,7 +472,7 @@ if _config_path.exists():
|
||||||
# gateway resolves these to Path.home() later (line ~255).
|
# gateway resolves these to Path.home() later (line ~255).
|
||||||
# Writing the raw placeholder here would just be noise.
|
# Writing the raw placeholder here would just be noise.
|
||||||
# Only bridge explicit absolute paths from config.yaml.
|
# Only bridge explicit absolute paths from config.yaml.
|
||||||
if _cfg_key == "cwd" and str(_val) in (".", "auto", "cwd"):
|
if _cfg_key == "cwd" and str(_val) in {".", "auto", "cwd"}:
|
||||||
continue
|
continue
|
||||||
# Expand shell tilde in cwd so subprocess.Popen never
|
# Expand shell tilde in cwd so subprocess.Popen never
|
||||||
# receives a literal "~/" which the kernel rejects.
|
# receives a literal "~/" which the kernel rejects.
|
||||||
|
|
@ -616,7 +616,7 @@ os.environ["HERMES_EXEC_ASK"] = "1"
|
||||||
# to home directory. MESSAGING_CWD is accepted as a backward-compat
|
# to home directory. MESSAGING_CWD is accepted as a backward-compat
|
||||||
# fallback (deprecated — the warning above tells users to migrate).
|
# fallback (deprecated — the warning above tells users to migrate).
|
||||||
_configured_cwd = os.environ.get("TERMINAL_CWD", "")
|
_configured_cwd = os.environ.get("TERMINAL_CWD", "")
|
||||||
if not _configured_cwd or _configured_cwd in (".", "auto", "cwd"):
|
if not _configured_cwd or _configured_cwd in {".", "auto", "cwd"}:
|
||||||
_fallback = os.getenv("MESSAGING_CWD") or str(Path.home())
|
_fallback = os.getenv("MESSAGING_CWD") or str(Path.home())
|
||||||
os.environ["TERMINAL_CWD"] = _fallback
|
os.environ["TERMINAL_CWD"] = _fallback
|
||||||
|
|
||||||
|
|
@ -849,7 +849,7 @@ def _skill_slug_from_frontmatter(skill_md: Path) -> tuple[str | None, str | None
|
||||||
if line.startswith("name:"):
|
if line.startswith("name:"):
|
||||||
raw = line.split(":", 1)[1].strip()
|
raw = line.split(":", 1)[1].strip()
|
||||||
# Strip YAML quote wrappers if present
|
# Strip YAML quote wrappers if present
|
||||||
if len(raw) >= 2 and raw[0] == raw[-1] and raw[0] in ('"', "'"):
|
if len(raw) >= 2 and raw[0] == raw[-1] and raw[0] in {'"', "'"}:
|
||||||
raw = raw[1:-1]
|
raw = raw[1:-1]
|
||||||
declared_name = raw.strip()
|
declared_name = raw.strip()
|
||||||
break
|
break
|
||||||
|
|
@ -891,7 +891,7 @@ def _check_unavailable_skill(command_name: str) -> str | None:
|
||||||
if not skills_dir.exists():
|
if not skills_dir.exists():
|
||||||
continue
|
continue
|
||||||
for skill_md in skills_dir.rglob("SKILL.md"):
|
for skill_md in skills_dir.rglob("SKILL.md"):
|
||||||
if any(part in ('.git', '.github', '.hub', '.archive') for part in skill_md.parts):
|
if any(part in {'.git', '.github', '.hub', '.archive'} for part in skill_md.parts):
|
||||||
continue
|
continue
|
||||||
slug, declared_name = _skill_slug_from_frontmatter(skill_md)
|
slug, declared_name = _skill_slug_from_frontmatter(skill_md)
|
||||||
if not slug or not declared_name:
|
if not slug or not declared_name:
|
||||||
|
|
@ -1033,7 +1033,7 @@ def _parse_session_key(session_key: str) -> "dict | None":
|
||||||
"chat_type": parts[3],
|
"chat_type": parts[3],
|
||||||
"chat_id": parts[4],
|
"chat_id": parts[4],
|
||||||
}
|
}
|
||||||
if len(parts) > 5 and parts[3] in ("dm", "thread"):
|
if len(parts) > 5 and parts[3] in {"dm", "thread"}:
|
||||||
result["thread_id"] = parts[5]
|
result["thread_id"] = parts[5]
|
||||||
return result
|
return result
|
||||||
return None
|
return None
|
||||||
|
|
@ -1561,7 +1561,7 @@ class GatewayRunner:
|
||||||
enabled_chats.clear()
|
enabled_chats.clear()
|
||||||
enabled_chats.update(
|
enabled_chats.update(
|
||||||
key[len(prefix):] for key, mode in self._voice_mode.items()
|
key[len(prefix):] for key, mode in self._voice_mode.items()
|
||||||
if mode in ("voice_only", "all") and key.startswith(prefix)
|
if mode in {"voice_only", "all"} and key.startswith(prefix)
|
||||||
)
|
)
|
||||||
|
|
||||||
async def _safe_adapter_disconnect(self, adapter, platform) -> None:
|
async def _safe_adapter_disconnect(self, adapter, platform) -> None:
|
||||||
|
|
@ -1991,7 +1991,7 @@ class GatewayRunner:
|
||||||
# Both "queue" and "steer" modes imply the user doesn't want messages
|
# Both "queue" and "steer" modes imply the user doesn't want messages
|
||||||
# to be lost during restart — queue them for the newly-spawned gateway
|
# to be lost during restart — queue them for the newly-spawned gateway
|
||||||
# process to pick up. "interrupt" mode drops them (current behaviour).
|
# process to pick up. "interrupt" mode drops them (current behaviour).
|
||||||
return self._restart_requested and self._busy_input_mode in ("queue", "steer")
|
return self._restart_requested and self._busy_input_mode in {"queue", "steer"}
|
||||||
|
|
||||||
# -------- /queue FIFO helpers --------------------------------------
|
# -------- /queue FIFO helpers --------------------------------------
|
||||||
# /queue must produce one full agent turn per invocation, in FIFO
|
# /queue must produce one full agent turn per invocation, in FIFO
|
||||||
|
|
@ -2401,7 +2401,7 @@ class GatewayRunner:
|
||||||
raw = cfg_get(cfg, "display", "background_process_notifications")
|
raw = cfg_get(cfg, "display", "background_process_notifications")
|
||||||
if raw is False:
|
if raw is False:
|
||||||
mode = "off"
|
mode = "off"
|
||||||
elif raw not in (None, ""):
|
elif raw not in {None, ""}:
|
||||||
mode = str(raw)
|
mode = str(raw)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
@ -3247,7 +3247,7 @@ class GatewayRunner:
|
||||||
# for this process's lifetime.
|
# for this process's lifetime.
|
||||||
try:
|
try:
|
||||||
_redact_raw = os.getenv("HERMES_REDACT_SECRETS", "true")
|
_redact_raw = os.getenv("HERMES_REDACT_SECRETS", "true")
|
||||||
_redact_on = _redact_raw.lower() in ("1", "true", "yes", "on")
|
_redact_on = _redact_raw.lower() in {"1", "true", "yes", "on"}
|
||||||
if _redact_on:
|
if _redact_on:
|
||||||
logger.info(
|
logger.info(
|
||||||
"Secret redaction: ENABLED (tool output, logs, and chat "
|
"Secret redaction: ENABLED (tool output, logs, and chat "
|
||||||
|
|
@ -3329,8 +3329,8 @@ class GatewayRunner:
|
||||||
_any_allowlist = any(
|
_any_allowlist = any(
|
||||||
os.getenv(v) for v in _builtin_allowed_vars + _plugin_allowed_vars
|
os.getenv(v) for v in _builtin_allowed_vars + _plugin_allowed_vars
|
||||||
)
|
)
|
||||||
_allow_all = os.getenv("GATEWAY_ALLOW_ALL_USERS", "").lower() in ("true", "1", "yes") or any(
|
_allow_all = os.getenv("GATEWAY_ALLOW_ALL_USERS", "").lower() in {"true", "1", "yes"} or any(
|
||||||
os.getenv(v, "").lower() in ("true", "1", "yes")
|
os.getenv(v, "").lower() in {"true", "1", "yes"}
|
||||||
for v in _builtin_allow_all_vars + _plugin_allow_all_vars
|
for v in _builtin_allow_all_vars + _plugin_allow_all_vars
|
||||||
)
|
)
|
||||||
if not _any_allowlist and not _allow_all:
|
if not _any_allowlist and not _allow_all:
|
||||||
|
|
@ -4379,7 +4379,7 @@ class GatewayRunner:
|
||||||
# dispatcher respawns the task and it cycles into the
|
# dispatcher respawns the task and it cycles into the
|
||||||
# same state. See the longer comment on TERMINAL_KINDS
|
# same state. See the longer comment on TERMINAL_KINDS
|
||||||
# above for the failure mode this prevents.
|
# above for the failure mode this prevents.
|
||||||
task_terminal = task and task.status in ("done", "archived")
|
task_terminal = task and task.status in {"done", "archived"}
|
||||||
if task_terminal:
|
if task_terminal:
|
||||||
await asyncio.to_thread(
|
await asyncio.to_thread(
|
||||||
self._kanban_unsub, sub, board_slug,
|
self._kanban_unsub, sub, board_slug,
|
||||||
|
|
@ -4479,7 +4479,7 @@ class GatewayRunner:
|
||||||
logger.warning("kanban dispatcher: config loader unavailable; disabled")
|
logger.warning("kanban dispatcher: config loader unavailable; disabled")
|
||||||
return
|
return
|
||||||
env_override = os.environ.get("HERMES_KANBAN_DISPATCH_IN_GATEWAY", "").strip().lower()
|
env_override = os.environ.get("HERMES_KANBAN_DISPATCH_IN_GATEWAY", "").strip().lower()
|
||||||
if env_override in ("0", "false", "no", "off"):
|
if env_override in {"0", "false", "no", "off"}:
|
||||||
logger.info("kanban dispatcher: disabled via HERMES_KANBAN_DISPATCH_IN_GATEWAY env")
|
logger.info("kanban dispatcher: disabled via HERMES_KANBAN_DISPATCH_IN_GATEWAY env")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
@ -5156,12 +5156,12 @@ class GatewayRunner:
|
||||||
try:
|
try:
|
||||||
_gw_cfg = _load_gateway_config()
|
_gw_cfg = _load_gateway_config()
|
||||||
_raw = cfg_get(_gw_cfg, "display", "platforms", "telegram", "notifications")
|
_raw = cfg_get(_gw_cfg, "display", "platforms", "telegram", "notifications")
|
||||||
if _raw not in (None, ""):
|
if _raw not in {None, ""}:
|
||||||
_notify_mode = str(_raw).strip().lower()
|
_notify_mode = str(_raw).strip().lower()
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
_notify_mode = _notify_mode or "important"
|
_notify_mode = _notify_mode or "important"
|
||||||
if _notify_mode not in ("all", "important"):
|
if _notify_mode not in {"all", "important"}:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"Unknown telegram notifications mode '%s', "
|
"Unknown telegram notifications mode '%s', "
|
||||||
"defaulting to 'important' (valid: all, important)",
|
"defaulting to 'important' (valid: all, important)",
|
||||||
|
|
@ -5338,7 +5338,7 @@ class GatewayRunner:
|
||||||
# connection, so HA events are always authorized.
|
# connection, so HA events are always authorized.
|
||||||
# Webhook events are authenticated via HMAC signature validation in
|
# Webhook events are authenticated via HMAC signature validation in
|
||||||
# the adapter itself — no user allowlist applies.
|
# the adapter itself — no user allowlist applies.
|
||||||
if source.platform in (Platform.HOMEASSISTANT, Platform.WEBHOOK):
|
if source.platform in {Platform.HOMEASSISTANT, Platform.WEBHOOK}:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
user_id = source.user_id
|
user_id = source.user_id
|
||||||
|
|
@ -5411,12 +5411,12 @@ class GatewayRunner:
|
||||||
|
|
||||||
# Per-platform allow-all flag (e.g., DISCORD_ALLOW_ALL_USERS=true)
|
# Per-platform allow-all flag (e.g., DISCORD_ALLOW_ALL_USERS=true)
|
||||||
platform_allow_all_var = platform_allow_all_map.get(source.platform, "")
|
platform_allow_all_var = platform_allow_all_map.get(source.platform, "")
|
||||||
if platform_allow_all_var and os.getenv(platform_allow_all_var, "").lower() in ("true", "1", "yes"):
|
if platform_allow_all_var and os.getenv(platform_allow_all_var, "").lower() in {"true", "1", "yes"}:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
if getattr(source, "is_bot", False):
|
if getattr(source, "is_bot", False):
|
||||||
allow_bots_var = platform_allow_bots_map.get(source.platform)
|
allow_bots_var = platform_allow_bots_map.get(source.platform)
|
||||||
if allow_bots_var and os.getenv(allow_bots_var, "none").lower().strip() in ("mentions", "all"):
|
if allow_bots_var and os.getenv(allow_bots_var, "none").lower().strip() in {"mentions", "all"}:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# Discord role-based access (DISCORD_ALLOWED_ROLES): the adapter's
|
# Discord role-based access (DISCORD_ALLOWED_ROLES): the adapter's
|
||||||
|
|
@ -5447,7 +5447,7 @@ class GatewayRunner:
|
||||||
|
|
||||||
if not platform_allowlist and not group_user_allowlist and not group_chat_allowlist and not global_allowlist:
|
if not platform_allowlist and not group_user_allowlist and not group_chat_allowlist and not global_allowlist:
|
||||||
# No allowlists configured -- check global allow-all flag
|
# No allowlists configured -- check global allow-all flag
|
||||||
return os.getenv("GATEWAY_ALLOW_ALL_USERS", "").lower() in ("true", "1", "yes")
|
return os.getenv("GATEWAY_ALLOW_ALL_USERS", "").lower() in {"true", "1", "yes"}
|
||||||
|
|
||||||
# Telegram can optionally authorize group traffic by chat ID.
|
# Telegram can optionally authorize group traffic by chat ID.
|
||||||
# Keep this separate from TELEGRAM_GROUP_ALLOWED_USERS, which gates
|
# Keep this separate from TELEGRAM_GROUP_ALLOWED_USERS, which gates
|
||||||
|
|
@ -5742,9 +5742,9 @@ class GatewayRunner:
|
||||||
raw = (event.text or "").strip()
|
raw = (event.text or "").strip()
|
||||||
# Accept /approve and /deny as shorthand for yes/no
|
# Accept /approve and /deny as shorthand for yes/no
|
||||||
cmd = event.get_command()
|
cmd = event.get_command()
|
||||||
if cmd in ("approve", "yes"):
|
if cmd in {"approve", "yes"}:
|
||||||
response_text = "y"
|
response_text = "y"
|
||||||
elif cmd in ("deny", "no"):
|
elif cmd in {"deny", "no"}:
|
||||||
response_text = "n"
|
response_text = "n"
|
||||||
else:
|
else:
|
||||||
_recognized_cmd = None
|
_recognized_cmd = None
|
||||||
|
|
@ -5826,17 +5826,17 @@ class GatewayRunner:
|
||||||
_raw_reply = (event.text or "").strip()
|
_raw_reply = (event.text or "").strip()
|
||||||
_cmd_reply = event.get_command()
|
_cmd_reply = event.get_command()
|
||||||
_confirm_choice = None
|
_confirm_choice = None
|
||||||
if _cmd_reply in ("approve", "yes", "ok", "confirm"):
|
if _cmd_reply in {"approve", "yes", "ok", "confirm"}:
|
||||||
_confirm_choice = "once"
|
_confirm_choice = "once"
|
||||||
elif _cmd_reply in ("always", "remember"):
|
elif _cmd_reply in {"always", "remember"}:
|
||||||
_confirm_choice = "always"
|
_confirm_choice = "always"
|
||||||
elif _cmd_reply in ("cancel", "no", "deny", "nevermind"):
|
elif _cmd_reply in {"cancel", "no", "deny", "nevermind"}:
|
||||||
_confirm_choice = "cancel"
|
_confirm_choice = "cancel"
|
||||||
elif _raw_reply.lower() in ("approve", "approve once", "once"):
|
elif _raw_reply.lower() in {"approve", "approve once", "once"}:
|
||||||
_confirm_choice = "once"
|
_confirm_choice = "once"
|
||||||
elif _raw_reply.lower() in ("always", "always approve"):
|
elif _raw_reply.lower() in {"always", "always approve"}:
|
||||||
_confirm_choice = "always"
|
_confirm_choice = "always"
|
||||||
elif _raw_reply.lower() in ("cancel", "nevermind", "no"):
|
elif _raw_reply.lower() in {"cancel", "nevermind", "no"}:
|
||||||
_confirm_choice = "cancel"
|
_confirm_choice = "cancel"
|
||||||
if _confirm_choice is not None:
|
if _confirm_choice is not None:
|
||||||
_resolved = await _slash_confirm_mod.resolve(
|
_resolved = await _slash_confirm_mod.resolve(
|
||||||
|
|
@ -5972,7 +5972,7 @@ class GatewayRunner:
|
||||||
# Semantics: each /queue invocation produces its own full agent
|
# Semantics: each /queue invocation produces its own full agent
|
||||||
# turn, processed in FIFO order after the current run (and any
|
# turn, processed in FIFO order after the current run (and any
|
||||||
# earlier /queue items) finishes. Messages are NOT merged.
|
# earlier /queue items) finishes. Messages are NOT merged.
|
||||||
if event.get_command() in ("queue", "q"):
|
if event.get_command() in {"queue", "q"}:
|
||||||
queued_text = event.get_command_args().strip()
|
queued_text = event.get_command_args().strip()
|
||||||
if not queued_text:
|
if not queued_text:
|
||||||
return "Usage: /queue <prompt>"
|
return "Usage: /queue <prompt>"
|
||||||
|
|
@ -6045,7 +6045,7 @@ class GatewayRunner:
|
||||||
# The agent thread is blocked on a threading.Event inside
|
# The agent thread is blocked on a threading.Event inside
|
||||||
# tools/approval.py — sending an interrupt won't unblock it.
|
# tools/approval.py — sending an interrupt won't unblock it.
|
||||||
# Route directly to the approval handler so the event is signalled.
|
# Route directly to the approval handler so the event is signalled.
|
||||||
if _cmd_def_inner and _cmd_def_inner.name in ("approve", "deny"):
|
if _cmd_def_inner and _cmd_def_inner.name in {"approve", "deny"}:
|
||||||
if _cmd_def_inner.name == "approve":
|
if _cmd_def_inner.name == "approve":
|
||||||
return await self._handle_approve_command(event)
|
return await self._handle_approve_command(event)
|
||||||
return await self._handle_deny_command(event)
|
return await self._handle_deny_command(event)
|
||||||
|
|
@ -6076,7 +6076,7 @@ class GatewayRunner:
|
||||||
# continuation prompt against the current turn.
|
# continuation prompt against the current turn.
|
||||||
if _cmd_def_inner and _cmd_def_inner.name == "goal":
|
if _cmd_def_inner and _cmd_def_inner.name == "goal":
|
||||||
_goal_arg = (event.get_command_args() or "").strip().lower()
|
_goal_arg = (event.get_command_args() or "").strip().lower()
|
||||||
if not _goal_arg or _goal_arg in ("status", "pause", "resume", "clear", "stop", "done"):
|
if not _goal_arg or _goal_arg in {"status", "pause", "resume", "clear", "stop", "done"}:
|
||||||
return await self._handle_goal_command(event)
|
return await self._handle_goal_command(event)
|
||||||
return "Agent is running — use /goal status / pause / clear mid-run, or /stop before setting a new goal."
|
return "Agent is running — use /goal status / pause / clear mid-run, or /stop before setting a new goal."
|
||||||
|
|
||||||
|
|
@ -6088,7 +6088,7 @@ class GatewayRunner:
|
||||||
# /fast and /reasoning are config-only and take effect next
|
# /fast and /reasoning are config-only and take effect next
|
||||||
# message, so they fall through to the catch-all busy response
|
# message, so they fall through to the catch-all busy response
|
||||||
# below — users should wait and set them between turns.
|
# below — users should wait and set them between turns.
|
||||||
if _cmd_def_inner and _cmd_def_inner.name in ("yolo", "verbose"):
|
if _cmd_def_inner and _cmd_def_inner.name in {"yolo", "verbose"}:
|
||||||
if _cmd_def_inner.name == "yolo":
|
if _cmd_def_inner.name == "yolo":
|
||||||
return await self._handle_yolo_command(event)
|
return await self._handle_yolo_command(event)
|
||||||
if _cmd_def_inner.name == "verbose":
|
if _cmd_def_inner.name == "verbose":
|
||||||
|
|
@ -6711,7 +6711,7 @@ class GatewayRunner:
|
||||||
mtype = event.media_types[i] if i < len(event.media_types) else ""
|
mtype = event.media_types[i] if i < len(event.media_types) else ""
|
||||||
if mtype.startswith("image/") or event.message_type == MessageType.PHOTO:
|
if mtype.startswith("image/") or event.message_type == MessageType.PHOTO:
|
||||||
image_paths.append(path)
|
image_paths.append(path)
|
||||||
if mtype.startswith("audio/") or event.message_type in (MessageType.VOICE, MessageType.AUDIO):
|
if mtype.startswith("audio/") or event.message_type in {MessageType.VOICE, MessageType.AUDIO}:
|
||||||
audio_paths.append(path)
|
audio_paths.append(path)
|
||||||
|
|
||||||
if image_paths:
|
if image_paths:
|
||||||
|
|
@ -6780,7 +6780,7 @@ class GatewayRunner:
|
||||||
_TEXT_EXTENSIONS = {".txt", ".md", ".csv", ".log", ".json", ".xml", ".yaml", ".yml", ".toml", ".ini", ".cfg"}
|
_TEXT_EXTENSIONS = {".txt", ".md", ".csv", ".log", ".json", ".xml", ".yaml", ".yml", ".toml", ".ini", ".cfg"}
|
||||||
for i, path in enumerate(event.media_urls):
|
for i, path in enumerate(event.media_urls):
|
||||||
mtype = event.media_types[i] if i < len(event.media_types) else ""
|
mtype = event.media_types[i] if i < len(event.media_types) else ""
|
||||||
if mtype in ("", "application/octet-stream"):
|
if mtype in {"", "application/octet-stream"}:
|
||||||
_ext = os.path.splitext(path)[1].lower()
|
_ext = os.path.splitext(path)[1].lower()
|
||||||
if _ext in _TEXT_EXTENSIONS:
|
if _ext in _TEXT_EXTENSIONS:
|
||||||
mtype = "text/plain"
|
mtype = "text/plain"
|
||||||
|
|
@ -7164,7 +7164,7 @@ class GatewayRunner:
|
||||||
if isinstance(_comp_cfg, dict):
|
if isinstance(_comp_cfg, dict):
|
||||||
_hyg_compression_enabled = str(
|
_hyg_compression_enabled = str(
|
||||||
_comp_cfg.get("enabled", True)
|
_comp_cfg.get("enabled", True)
|
||||||
).lower() in ("true", "1", "yes")
|
).lower() in {"true", "1", "yes"}
|
||||||
_raw_hard_limit = _comp_cfg.get("hygiene_hard_message_limit")
|
_raw_hard_limit = _comp_cfg.get("hygiene_hard_message_limit")
|
||||||
if _raw_hard_limit is not None:
|
if _raw_hard_limit is not None:
|
||||||
try:
|
try:
|
||||||
|
|
@ -7287,7 +7287,7 @@ class GatewayRunner:
|
||||||
_hyg_msgs = [
|
_hyg_msgs = [
|
||||||
{"role": m.get("role"), "content": m.get("content")}
|
{"role": m.get("role"), "content": m.get("content")}
|
||||||
for m in history
|
for m in history
|
||||||
if m.get("role") in ("user", "assistant")
|
if m.get("role") in {"user", "assistant"}
|
||||||
and m.get("content")
|
and m.get("content")
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -7651,7 +7651,7 @@ class GatewayRunner:
|
||||||
while not _pr.completion_queue.empty():
|
while not _pr.completion_queue.empty():
|
||||||
evt = _pr.completion_queue.get_nowait()
|
evt = _pr.completion_queue.get_nowait()
|
||||||
evt_type = evt.get("type", "completion")
|
evt_type = evt.get("type", "completion")
|
||||||
if evt_type in ("watch_match", "watch_disabled"):
|
if evt_type in {"watch_match", "watch_disabled"}:
|
||||||
_watch_events.append(evt)
|
_watch_events.append(evt)
|
||||||
# else: completion events are handled by the watcher task
|
# else: completion events are handled by the watcher task
|
||||||
for evt in _watch_events:
|
for evt in _watch_events:
|
||||||
|
|
@ -7893,7 +7893,7 @@ class GatewayRunner:
|
||||||
status_hint = " You are being rate-limited. Please wait a moment and try again."
|
status_hint = " You are being rate-limited. Please wait a moment and try again."
|
||||||
elif status_code == 529:
|
elif status_code == 529:
|
||||||
status_hint = " The API is temporarily overloaded. Please try again shortly."
|
status_hint = " The API is temporarily overloaded. Please try again shortly."
|
||||||
elif status_code in (400, 500):
|
elif status_code in {400, 500}:
|
||||||
# 400 with a large session is context overflow.
|
# 400 with a large session is context overflow.
|
||||||
# 500 with a large session often means the payload is too large
|
# 500 with a large session often means the payload is too large
|
||||||
# for the API to process — treat it the same way.
|
# for the API to process — treat it the same way.
|
||||||
|
|
@ -8255,7 +8255,7 @@ class GatewayRunner:
|
||||||
policy = _policy_for_source(self.config, source)
|
policy = _policy_for_source(self.config, source)
|
||||||
platform = source.platform.value if source and source.platform else "?"
|
platform = source.platform.value if source and source.platform else "?"
|
||||||
chat_type = (source.chat_type if source else "") or "dm"
|
chat_type = (source.chat_type if source else "") or "dm"
|
||||||
scope = "DM" if chat_type.lower() in ("dm", "direct", "private", "") else "group/channel"
|
scope = "DM" if chat_type.lower() in {"dm", "direct", "private", ""} else "group/channel"
|
||||||
user_id = (source.user_id if source else None) or "?"
|
user_id = (source.user_id if source else None) or "?"
|
||||||
|
|
||||||
if not policy.enabled:
|
if not policy.enabled:
|
||||||
|
|
@ -9193,7 +9193,7 @@ class GatewayRunner:
|
||||||
return "\n".join(p for p in parts if p)
|
return "\n".join(p for p in parts if p)
|
||||||
return str(value)
|
return str(value)
|
||||||
|
|
||||||
if args in ("none", "default", "neutral"):
|
if args in {"none", "default", "neutral"}:
|
||||||
try:
|
try:
|
||||||
if "agent" not in config or not isinstance(config.get("agent"), dict):
|
if "agent" not in config or not isinstance(config.get("agent"), dict):
|
||||||
config["agent"] = {}
|
config["agent"] = {}
|
||||||
|
|
@ -9345,7 +9345,7 @@ class GatewayRunner:
|
||||||
return t("gateway.goal.no_resume")
|
return t("gateway.goal.no_resume")
|
||||||
return t("gateway.goal.resumed", goal=state.goal)
|
return t("gateway.goal.resumed", goal=state.goal)
|
||||||
|
|
||||||
if lower in ("clear", "stop", "done"):
|
if lower in {"clear", "stop", "done"}:
|
||||||
had = mgr.has_goal()
|
had = mgr.has_goal()
|
||||||
mgr.clear()
|
mgr.clear()
|
||||||
try:
|
try:
|
||||||
|
|
@ -9598,13 +9598,13 @@ class GatewayRunner:
|
||||||
|
|
||||||
adapter = self.adapters.get(platform)
|
adapter = self.adapters.get(platform)
|
||||||
|
|
||||||
if args in ("on", "enable"):
|
if args in {"on", "enable"}:
|
||||||
self._voice_mode[voice_key] = "voice_only"
|
self._voice_mode[voice_key] = "voice_only"
|
||||||
self._save_voice_modes()
|
self._save_voice_modes()
|
||||||
if adapter:
|
if adapter:
|
||||||
self._set_adapter_auto_tts_enabled(adapter, chat_id, enabled=True)
|
self._set_adapter_auto_tts_enabled(adapter, chat_id, enabled=True)
|
||||||
return t("gateway.voice.enabled_voice_only")
|
return t("gateway.voice.enabled_voice_only")
|
||||||
elif args in ("off", "disable"):
|
elif args in {"off", "disable"}:
|
||||||
self._voice_mode[voice_key] = "off"
|
self._voice_mode[voice_key] = "off"
|
||||||
self._save_voice_modes()
|
self._save_voice_modes()
|
||||||
if adapter:
|
if adapter:
|
||||||
|
|
@ -9616,7 +9616,7 @@ class GatewayRunner:
|
||||||
if adapter:
|
if adapter:
|
||||||
self._set_adapter_auto_tts_enabled(adapter, chat_id, enabled=True)
|
self._set_adapter_auto_tts_enabled(adapter, chat_id, enabled=True)
|
||||||
return t("gateway.voice.tts_enabled")
|
return t("gateway.voice.tts_enabled")
|
||||||
elif args in ("channel", "join"):
|
elif args in {"channel", "join"}:
|
||||||
return await self._handle_voice_channel_join(event)
|
return await self._handle_voice_channel_join(event)
|
||||||
elif args == "leave":
|
elif args == "leave":
|
||||||
return await self._handle_voice_channel_leave(event)
|
return await self._handle_voice_channel_leave(event)
|
||||||
|
|
@ -10390,12 +10390,12 @@ class GatewayRunner:
|
||||||
|
|
||||||
# Display toggle (per-platform)
|
# Display toggle (per-platform)
|
||||||
platform_key = _platform_config_key(event.source.platform)
|
platform_key = _platform_config_key(event.source.platform)
|
||||||
if args in ("show", "on"):
|
if args in {"show", "on"}:
|
||||||
self._show_reasoning = True
|
self._show_reasoning = True
|
||||||
_save_config_key(f"display.platforms.{platform_key}.show_reasoning", True)
|
_save_config_key(f"display.platforms.{platform_key}.show_reasoning", True)
|
||||||
return t("gateway.reasoning.display_set_on", platform=platform_key)
|
return t("gateway.reasoning.display_set_on", platform=platform_key)
|
||||||
|
|
||||||
if args in ("hide", "off"):
|
if args in {"hide", "off"}:
|
||||||
self._show_reasoning = False
|
self._show_reasoning = False
|
||||||
_save_config_key(f"display.platforms.{platform_key}.show_reasoning", False)
|
_save_config_key(f"display.platforms.{platform_key}.show_reasoning", False)
|
||||||
return t("gateway.reasoning.display_set_off", platform=platform_key)
|
return t("gateway.reasoning.display_set_off", platform=platform_key)
|
||||||
|
|
@ -10411,7 +10411,7 @@ class GatewayRunner:
|
||||||
return t("gateway.reasoning.reset_done")
|
return t("gateway.reasoning.reset_done")
|
||||||
if effort == "none":
|
if effort == "none":
|
||||||
parsed = {"enabled": False}
|
parsed = {"enabled": False}
|
||||||
elif effort in ("minimal", "low", "medium", "high", "xhigh"):
|
elif effort in {"minimal", "low", "medium", "high", "xhigh"}:
|
||||||
parsed = {"enabled": True, "effort": effort}
|
parsed = {"enabled": True, "effort": effort}
|
||||||
else:
|
else:
|
||||||
return t(
|
return t(
|
||||||
|
|
@ -10603,7 +10603,7 @@ class GatewayRunner:
|
||||||
|
|
||||||
effective = resolve_footer_config(user_config, platform_key)
|
effective = resolve_footer_config(user_config, platform_key)
|
||||||
|
|
||||||
if arg in ("status", "?"):
|
if arg in {"status", "?"}:
|
||||||
state = t("gateway.footer.state_on") if effective["enabled"] else t("gateway.footer.state_off")
|
state = t("gateway.footer.state_on") if effective["enabled"] else t("gateway.footer.state_off")
|
||||||
fields = ", ".join(effective.get("fields") or [])
|
fields = ", ".join(effective.get("fields") or [])
|
||||||
return t(
|
return t(
|
||||||
|
|
@ -10613,9 +10613,9 @@ class GatewayRunner:
|
||||||
platform=platform_key,
|
platform=platform_key,
|
||||||
)
|
)
|
||||||
|
|
||||||
if arg in ("on", "enable", "true", "1"):
|
if arg in {"on", "enable", "true", "1"}:
|
||||||
new_state = True
|
new_state = True
|
||||||
elif arg in ("off", "disable", "false", "0"):
|
elif arg in {"off", "disable", "false", "0"}:
|
||||||
new_state = False
|
new_state = False
|
||||||
elif arg == "":
|
elif arg == "":
|
||||||
new_state = not effective["enabled"]
|
new_state = not effective["enabled"]
|
||||||
|
|
@ -10683,7 +10683,7 @@ class GatewayRunner:
|
||||||
msgs = [
|
msgs = [
|
||||||
{"role": m.get("role"), "content": m.get("content")}
|
{"role": m.get("role"), "content": m.get("content")}
|
||||||
for m in history
|
for m in history
|
||||||
if m.get("role") in ("user", "assistant") and m.get("content")
|
if m.get("role") in {"user", "assistant"} and m.get("content")
|
||||||
]
|
]
|
||||||
|
|
||||||
tmp_agent = AIAgent(
|
tmp_agent = AIAgent(
|
||||||
|
|
@ -11597,7 +11597,7 @@ class GatewayRunner:
|
||||||
history = self.session_store.load_transcript(session_entry.session_id)
|
history = self.session_store.load_transcript(session_entry.session_id)
|
||||||
if history:
|
if history:
|
||||||
from agent.model_metadata import estimate_messages_tokens_rough
|
from agent.model_metadata import estimate_messages_tokens_rough
|
||||||
msgs = [m for m in history if m.get("role") in ("user", "assistant") and m.get("content")]
|
msgs = [m for m in history if m.get("role") in {"user", "assistant"} and m.get("content")]
|
||||||
approx = estimate_messages_tokens_rough(msgs)
|
approx = estimate_messages_tokens_rough(msgs)
|
||||||
lines = [
|
lines = [
|
||||||
t("gateway.usage.header_session_info"),
|
t("gateway.usage.header_session_info"),
|
||||||
|
|
@ -12151,9 +12151,9 @@ class GatewayRunner:
|
||||||
resolve_all = "all" in args
|
resolve_all = "all" in args
|
||||||
remaining = [a for a in args if a != "all"]
|
remaining = [a for a in args if a != "all"]
|
||||||
|
|
||||||
if any(a in ("always", "permanent", "permanently") for a in remaining):
|
if any(a in {"always", "permanent", "permanently"} for a in remaining):
|
||||||
choice = "always"
|
choice = "always"
|
||||||
elif any(a in ("session", "ses") for a in remaining):
|
elif any(a in {"session", "ses"} for a in remaining):
|
||||||
choice = "session"
|
choice = "session"
|
||||||
else:
|
else:
|
||||||
choice = "once"
|
choice = "once"
|
||||||
|
|
@ -13270,8 +13270,8 @@ class GatewayRunner:
|
||||||
# --- Normal text-only notification ---
|
# --- Normal text-only notification ---
|
||||||
# Decide whether to notify based on mode
|
# Decide whether to notify based on mode
|
||||||
should_notify = (
|
should_notify = (
|
||||||
notify_mode in ("all", "result")
|
notify_mode in {"all", "result"}
|
||||||
or (notify_mode == "error" and session.exit_code not in (0, None))
|
or (notify_mode == "error" and session.exit_code not in {0, None})
|
||||||
)
|
)
|
||||||
if should_notify:
|
if should_notify:
|
||||||
new_output = session.output_buffer[-1000:] if session.output_buffer else ""
|
new_output = session.output_buffer[-1000:] if session.output_buffer else ""
|
||||||
|
|
@ -13866,7 +13866,7 @@ class GatewayRunner:
|
||||||
for msg in history:
|
for msg in history:
|
||||||
role = msg.get("role")
|
role = msg.get("role")
|
||||||
content = msg.get("content")
|
content = msg.get("content")
|
||||||
if role in ("user", "assistant") and content:
|
if role in {"user", "assistant"} and content:
|
||||||
api_messages.append({"role": role, "content": content})
|
api_messages.append({"role": role, "content": content})
|
||||||
|
|
||||||
api_messages.append({"role": "user", "content": message})
|
api_messages.append({"role": "user", "content": message})
|
||||||
|
|
@ -14257,7 +14257,7 @@ class GatewayRunner:
|
||||||
|
|
||||||
|
|
||||||
# Only act on tool.started events (ignore tool.completed, reasoning.available, etc.)
|
# Only act on tool.started events (ignore tool.completed, reasoning.available, etc.)
|
||||||
if event_type not in ("tool.started",):
|
if event_type not in {"tool.started",}:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Suppress tool-progress bubbles once the user has sent `stop`.
|
# Suppress tool-progress bubbles once the user has sent `stop`.
|
||||||
|
|
@ -14954,7 +14954,7 @@ class GatewayRunner:
|
||||||
|
|
||||||
# Skip metadata entries (tool definitions, session info)
|
# Skip metadata entries (tool definitions, session info)
|
||||||
# -- these are for transcript logging, not for the LLM
|
# -- these are for transcript logging, not for the LLM
|
||||||
if role in ("session_meta",):
|
if role in {"session_meta",}:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Skip system messages -- the agent rebuilds its own system prompt
|
# Skip system messages -- the agent rebuilds its own system prompt
|
||||||
|
|
@ -14991,7 +14991,7 @@ class GatewayRunner:
|
||||||
# even if the message list shrinks, we know which paths are old.
|
# even if the message list shrinks, we know which paths are old.
|
||||||
_history_media_paths: set = set()
|
_history_media_paths: set = set()
|
||||||
for _hm in agent_history:
|
for _hm in agent_history:
|
||||||
if _hm.get("role") in ("tool", "function"):
|
if _hm.get("role") in {"tool", "function"}:
|
||||||
_hc = _hm.get("content", "")
|
_hc = _hm.get("content", "")
|
||||||
if "MEDIA:" in _hc:
|
if "MEDIA:" in _hc:
|
||||||
for _match in re.finditer(r'MEDIA:(\S+)', _hc):
|
for _match in re.finditer(r'MEDIA:(\S+)', _hc):
|
||||||
|
|
@ -15263,7 +15263,7 @@ class GatewayRunner:
|
||||||
media_tags = []
|
media_tags = []
|
||||||
has_voice_directive = False
|
has_voice_directive = False
|
||||||
for msg in result.get("messages", []):
|
for msg in result.get("messages", []):
|
||||||
if msg.get("role") in ("tool", "function"):
|
if msg.get("role") in {"tool", "function"}:
|
||||||
content = msg.get("content", "")
|
content = msg.get("content", "")
|
||||||
if "MEDIA:" in content:
|
if "MEDIA:" in content:
|
||||||
for match in re.finditer(r'MEDIA:(\S+)', content):
|
for match in re.finditer(r'MEDIA:(\S+)', content):
|
||||||
|
|
|
||||||
|
|
@ -764,12 +764,12 @@ class SessionStore:
|
||||||
|
|
||||||
now = _now()
|
now = _now()
|
||||||
|
|
||||||
if policy.mode in ("idle", "both"):
|
if policy.mode in {"idle", "both"}:
|
||||||
idle_deadline = entry.updated_at + timedelta(minutes=policy.idle_minutes)
|
idle_deadline = entry.updated_at + timedelta(minutes=policy.idle_minutes)
|
||||||
if now > idle_deadline:
|
if now > idle_deadline:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
if policy.mode in ("daily", "both"):
|
if policy.mode in {"daily", "both"}:
|
||||||
today_reset = now.replace(
|
today_reset = now.replace(
|
||||||
hour=policy.at_hour,
|
hour=policy.at_hour,
|
||||||
minute=0, second=0, microsecond=0,
|
minute=0, second=0, microsecond=0,
|
||||||
|
|
@ -805,12 +805,12 @@ class SessionStore:
|
||||||
|
|
||||||
now = _now()
|
now = _now()
|
||||||
|
|
||||||
if policy.mode in ("idle", "both"):
|
if policy.mode in {"idle", "both"}:
|
||||||
idle_deadline = entry.updated_at + timedelta(minutes=policy.idle_minutes)
|
idle_deadline = entry.updated_at + timedelta(minutes=policy.idle_minutes)
|
||||||
if now > idle_deadline:
|
if now > idle_deadline:
|
||||||
return "idle"
|
return "idle"
|
||||||
|
|
||||||
if policy.mode in ("daily", "both"):
|
if policy.mode in {"daily", "both"}:
|
||||||
today_reset = now.replace(
|
today_reset = now.replace(
|
||||||
hour=policy.at_hour,
|
hour=policy.at_hour,
|
||||||
minute=0,
|
minute=0,
|
||||||
|
|
|
||||||
|
|
@ -604,7 +604,7 @@ def acquire_scoped_lock(scope: str, identity: str, metadata: Optional[dict[str,
|
||||||
for _line in _proc_status.read_text(encoding="utf-8").splitlines():
|
for _line in _proc_status.read_text(encoding="utf-8").splitlines():
|
||||||
if _line.startswith("State:"):
|
if _line.startswith("State:"):
|
||||||
_state = _line.split()[1]
|
_state = _line.split()[1]
|
||||||
if _state in ("T", "t"): # stopped or tracing stop
|
if _state in {"T", "t"}: # stopped or tracing stop
|
||||||
stale = True
|
stale = True
|
||||||
break
|
break
|
||||||
except (OSError, PermissionError):
|
except (OSError, PermissionError):
|
||||||
|
|
|
||||||
|
|
@ -1450,7 +1450,7 @@ def resolve_provider(
|
||||||
# whose availability isn't implied by LM_API_KEY presence (it may be
|
# whose availability isn't implied by LM_API_KEY presence (it may be
|
||||||
# offline, and the no-auth setup uses a placeholder value), so it
|
# offline, and the no-auth setup uses a placeholder value), so it
|
||||||
# also requires explicit selection.
|
# also requires explicit selection.
|
||||||
if pid in ("copilot", "lmstudio"):
|
if pid in {"copilot", "lmstudio"}:
|
||||||
continue
|
continue
|
||||||
for env_var in pconfig.api_key_env_vars:
|
for env_var in pconfig.api_key_env_vars:
|
||||||
if has_usable_secret(os.getenv(env_var, "")):
|
if has_usable_secret(os.getenv(env_var, "")):
|
||||||
|
|
@ -2541,7 +2541,7 @@ def refresh_codex_oauth_pure(
|
||||||
# A 401/403 from the token endpoint always means the refresh token
|
# A 401/403 from the token endpoint always means the refresh token
|
||||||
# is invalid/expired — force relogin even if the body error code
|
# is invalid/expired — force relogin even if the body error code
|
||||||
# wasn't one of the known strings above.
|
# wasn't one of the known strings above.
|
||||||
if response.status_code in (401, 403) and not relogin_required:
|
if response.status_code in {401, 403} and not relogin_required:
|
||||||
relogin_required = True
|
relogin_required = True
|
||||||
raise AuthError(
|
raise AuthError(
|
||||||
message,
|
message,
|
||||||
|
|
@ -2947,7 +2947,7 @@ def _merge_shared_nous_oauth_state(state: Dict[str, Any]) -> bool:
|
||||||
"expires_at",
|
"expires_at",
|
||||||
):
|
):
|
||||||
value = shared.get(key)
|
value = shared.get(key)
|
||||||
if value not in (None, ""):
|
if value not in {None, ""}:
|
||||||
state[key] = value
|
state[key] = value
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
@ -3986,7 +3986,7 @@ def get_api_key_provider_status(provider_id: str) -> Dict[str, Any]:
|
||||||
if pconfig.base_url_env_var:
|
if pconfig.base_url_env_var:
|
||||||
env_url = os.getenv(pconfig.base_url_env_var, "").strip()
|
env_url = os.getenv(pconfig.base_url_env_var, "").strip()
|
||||||
|
|
||||||
if provider_id in ("kimi-coding", "kimi-coding-cn"):
|
if provider_id in {"kimi-coding", "kimi-coding-cn"}:
|
||||||
base_url = _resolve_kimi_base_url(api_key, pconfig.inference_base_url, env_url)
|
base_url = _resolve_kimi_base_url(api_key, pconfig.inference_base_url, env_url)
|
||||||
elif env_url:
|
elif env_url:
|
||||||
base_url = env_url
|
base_url = env_url
|
||||||
|
|
@ -4090,7 +4090,7 @@ def resolve_api_key_provider_credentials(provider_id: str) -> Dict[str, Any]:
|
||||||
if pconfig.base_url_env_var:
|
if pconfig.base_url_env_var:
|
||||||
env_url = os.getenv(pconfig.base_url_env_var, "").strip()
|
env_url = os.getenv(pconfig.base_url_env_var, "").strip()
|
||||||
|
|
||||||
if provider_id in ("kimi-coding", "kimi-coding-cn"):
|
if provider_id in {"kimi-coding", "kimi-coding-cn"}:
|
||||||
base_url = _resolve_kimi_base_url(api_key, pconfig.inference_base_url, env_url)
|
base_url = _resolve_kimi_base_url(api_key, pconfig.inference_base_url, env_url)
|
||||||
elif provider_id == "zai":
|
elif provider_id == "zai":
|
||||||
base_url = _resolve_zai_base_url(api_key, pconfig.inference_base_url, env_url)
|
base_url = _resolve_zai_base_url(api_key, pconfig.inference_base_url, env_url)
|
||||||
|
|
@ -4510,7 +4510,7 @@ def _login_openai_codex(
|
||||||
reuse = input("Use existing credentials? [Y/n]: ").strip().lower()
|
reuse = input("Use existing credentials? [Y/n]: ").strip().lower()
|
||||||
except (EOFError, KeyboardInterrupt):
|
except (EOFError, KeyboardInterrupt):
|
||||||
reuse = "y"
|
reuse = "y"
|
||||||
if reuse in ("", "y", "yes"):
|
if reuse in {"", "y", "yes"}:
|
||||||
config_path = _update_config_for_provider("openai-codex", existing.get("base_url", DEFAULT_CODEX_BASE_URL))
|
config_path = _update_config_for_provider("openai-codex", existing.get("base_url", DEFAULT_CODEX_BASE_URL))
|
||||||
print()
|
print()
|
||||||
print("Login successful!")
|
print("Login successful!")
|
||||||
|
|
@ -4531,7 +4531,7 @@ def _login_openai_codex(
|
||||||
do_import = input("Import these credentials? (a separate login is recommended) [y/N]: ").strip().lower()
|
do_import = input("Import these credentials? (a separate login is recommended) [y/N]: ").strip().lower()
|
||||||
except (EOFError, KeyboardInterrupt):
|
except (EOFError, KeyboardInterrupt):
|
||||||
do_import = "n"
|
do_import = "n"
|
||||||
if do_import in ("y", "yes"):
|
if do_import in {"y", "yes"}:
|
||||||
_save_codex_tokens(cli_tokens)
|
_save_codex_tokens(cli_tokens)
|
||||||
base_url = os.getenv("HERMES_CODEX_BASE_URL", "").strip().rstrip("/") or DEFAULT_CODEX_BASE_URL
|
base_url = os.getenv("HERMES_CODEX_BASE_URL", "").strip().rstrip("/") or DEFAULT_CODEX_BASE_URL
|
||||||
config_path = _update_config_for_provider("openai-codex", base_url)
|
config_path = _update_config_for_provider("openai-codex", base_url)
|
||||||
|
|
@ -4623,7 +4623,7 @@ def _codex_device_code_login() -> Dict[str, Any]:
|
||||||
if poll_resp.status_code == 200:
|
if poll_resp.status_code == 200:
|
||||||
code_resp = poll_resp.json()
|
code_resp = poll_resp.json()
|
||||||
break
|
break
|
||||||
elif poll_resp.status_code in (403, 404):
|
elif poll_resp.status_code in {403, 404}:
|
||||||
continue # User hasn't completed login yet
|
continue # User hasn't completed login yet
|
||||||
else:
|
else:
|
||||||
raise AuthError(
|
raise AuthError(
|
||||||
|
|
@ -5188,7 +5188,7 @@ def _login_nous(args, pconfig: ProviderConfig) -> None:
|
||||||
do_import = input("Import these credentials? [Y/n]: ").strip().lower()
|
do_import = input("Import these credentials? [Y/n]: ").strip().lower()
|
||||||
except (EOFError, KeyboardInterrupt):
|
except (EOFError, KeyboardInterrupt):
|
||||||
do_import = "y"
|
do_import = "y"
|
||||||
if do_import in ("", "y", "yes"):
|
if do_import in {"", "y", "yes"}:
|
||||||
print("Rehydrating Nous session from shared credentials...")
|
print("Rehydrating Nous session from shared credentials...")
|
||||||
auth_state = _try_import_shared_nous_state(
|
auth_state = _try_import_shared_nous_state(
|
||||||
timeout_seconds=timeout_seconds,
|
timeout_seconds=timeout_seconds,
|
||||||
|
|
|
||||||
|
|
@ -266,7 +266,7 @@ def auth_add_command(args) -> None:
|
||||||
do_import = input("Import these credentials? [Y/n]: ").strip().lower()
|
do_import = input("Import these credentials? [Y/n]: ").strip().lower()
|
||||||
except (EOFError, KeyboardInterrupt):
|
except (EOFError, KeyboardInterrupt):
|
||||||
do_import = "y"
|
do_import = "y"
|
||||||
if do_import in ("", "y", "yes"):
|
if do_import in {"", "y", "yes"}:
|
||||||
print("Rehydrating Nous session from shared credentials...")
|
print("Rehydrating Nous session from shared credentials...")
|
||||||
rehydrated = auth_mod._try_import_shared_nous_state(
|
rehydrated = auth_mod._try_import_shared_nous_state(
|
||||||
timeout_seconds=getattr(args, "timeout", None) or 15.0,
|
timeout_seconds=getattr(args, "timeout", None) or 15.0,
|
||||||
|
|
|
||||||
|
|
@ -298,7 +298,7 @@ def _detect_prefix(zf: zipfile.ZipFile) -> str:
|
||||||
if len(first_parts) == 1:
|
if len(first_parts) == 1:
|
||||||
prefix = first_parts.pop()
|
prefix = first_parts.pop()
|
||||||
# Only strip if it looks like a hermes dir name
|
# Only strip if it looks like a hermes dir name
|
||||||
if prefix in (".hermes", "hermes"):
|
if prefix in {".hermes", "hermes"}:
|
||||||
return prefix + "/"
|
return prefix + "/"
|
||||||
|
|
||||||
return ""
|
return ""
|
||||||
|
|
@ -349,7 +349,7 @@ def run_import(args) -> None:
|
||||||
except (EOFError, KeyboardInterrupt):
|
except (EOFError, KeyboardInterrupt):
|
||||||
print("\nAborted.")
|
print("\nAborted.")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if answer not in ("y", "yes"):
|
if answer not in {"y", "yes"}:
|
||||||
print("Aborted.")
|
print("Aborted.")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -139,7 +139,7 @@ def _confirm(prompt: str) -> bool:
|
||||||
except (EOFError, KeyboardInterrupt):
|
except (EOFError, KeyboardInterrupt):
|
||||||
print()
|
print()
|
||||||
return False
|
return False
|
||||||
return resp in ("y", "yes")
|
return resp in {"y", "yes"}
|
||||||
|
|
||||||
|
|
||||||
def cmd_clear(args: argparse.Namespace) -> int:
|
def cmd_clear(args: argparse.Namespace) -> int:
|
||||||
|
|
|
||||||
|
|
@ -298,7 +298,7 @@ def claw_command(args):
|
||||||
|
|
||||||
if action == "migrate":
|
if action == "migrate":
|
||||||
_cmd_migrate(args)
|
_cmd_migrate(args)
|
||||||
elif action in ("cleanup", "clean"):
|
elif action in {"cleanup", "clean"}:
|
||||||
_cmd_cleanup(args)
|
_cmd_cleanup(args)
|
||||||
else:
|
else:
|
||||||
print("Usage: hermes claw <command> [options]")
|
print("Usage: hermes claw <command> [options]")
|
||||||
|
|
|
||||||
|
|
@ -101,7 +101,7 @@ def _fetch_models_from_api(access_token: str) -> List[str]:
|
||||||
# Some valid Codex CLI models (for example gpt-5.3-codex-spark) are
|
# Some valid Codex CLI models (for example gpt-5.3-codex-spark) are
|
||||||
# marked false here but are still accepted by the Codex route.
|
# marked false here but are still accepted by the Codex route.
|
||||||
visibility = item.get("visibility", "")
|
visibility = item.get("visibility", "")
|
||||||
if isinstance(visibility, str) and visibility.strip().lower() in ("hide", "hidden"):
|
if isinstance(visibility, str) and visibility.strip().lower() in {"hide", "hidden"}:
|
||||||
continue
|
continue
|
||||||
priority = item.get("priority")
|
priority = item.get("priority")
|
||||||
rank = int(priority) if isinstance(priority, (int, float)) else 10_000
|
rank = int(priority) if isinstance(priority, (int, float)) else 10_000
|
||||||
|
|
@ -152,7 +152,7 @@ def _read_cache_models(codex_home: Path) -> List[str]:
|
||||||
# public OpenAI API, while Hermes openai-codex talks to the same
|
# public OpenAI API, while Hermes openai-codex talks to the same
|
||||||
# OAuth-backed Codex backend as Codex CLI.
|
# OAuth-backed Codex backend as Codex CLI.
|
||||||
visibility = item.get("visibility")
|
visibility = item.get("visibility")
|
||||||
if isinstance(visibility, str) and visibility.strip().lower() in ("hide", "hidden"):
|
if isinstance(visibility, str) and visibility.strip().lower() in {"hide", "hidden"}:
|
||||||
continue
|
continue
|
||||||
priority = item.get("priority")
|
priority = item.get("priority")
|
||||||
rank = int(priority) if isinstance(priority, (int, float)) else 10_000
|
rank = int(priority) if isinstance(priority, (int, float)) else 10_000
|
||||||
|
|
|
||||||
|
|
@ -3202,7 +3202,7 @@ def warn_deprecated_cwd_env_vars(config: Optional[Dict[str, Any]] = None) -> Non
|
||||||
terminal_cfg = config.get("terminal", {})
|
terminal_cfg = config.get("terminal", {})
|
||||||
config_cwd = terminal_cfg.get("cwd", ".") if isinstance(terminal_cfg, dict) else "."
|
config_cwd = terminal_cfg.get("cwd", ".") if isinstance(terminal_cfg, dict) else "."
|
||||||
# Only warn if config.yaml doesn't have an explicit path
|
# Only warn if config.yaml doesn't have an explicit path
|
||||||
config_has_explicit_cwd = config_cwd not in (".", "auto", "cwd", "")
|
config_has_explicit_cwd = config_cwd not in {".", "auto", "cwd", ""}
|
||||||
|
|
||||||
lines: list[str] = []
|
lines: list[str] = []
|
||||||
if messaging_cwd:
|
if messaging_cwd:
|
||||||
|
|
@ -3262,10 +3262,10 @@ def migrate_config(interactive: bool = True, quiet: bool = False) -> Dict[str, A
|
||||||
if "tool_progress" not in display:
|
if "tool_progress" not in display:
|
||||||
old_enabled = get_env_value("HERMES_TOOL_PROGRESS")
|
old_enabled = get_env_value("HERMES_TOOL_PROGRESS")
|
||||||
old_mode = get_env_value("HERMES_TOOL_PROGRESS_MODE")
|
old_mode = get_env_value("HERMES_TOOL_PROGRESS_MODE")
|
||||||
if old_enabled and old_enabled.lower() in ("false", "0", "no"):
|
if old_enabled and old_enabled.lower() in {"false", "0", "no"}:
|
||||||
display["tool_progress"] = "off"
|
display["tool_progress"] = "off"
|
||||||
results["config_added"].append("display.tool_progress=off (from HERMES_TOOL_PROGRESS=false)")
|
results["config_added"].append("display.tool_progress=off (from HERMES_TOOL_PROGRESS=false)")
|
||||||
elif old_mode and old_mode.lower() in ("new", "all"):
|
elif old_mode and old_mode.lower() in {"new", "all"}:
|
||||||
display["tool_progress"] = old_mode.lower()
|
display["tool_progress"] = old_mode.lower()
|
||||||
results["config_added"].append(f"display.tool_progress={old_mode.lower()} (from HERMES_TOOL_PROGRESS_MODE)")
|
results["config_added"].append(f"display.tool_progress={old_mode.lower()} (from HERMES_TOOL_PROGRESS_MODE)")
|
||||||
else:
|
else:
|
||||||
|
|
@ -3344,7 +3344,7 @@ def migrate_config(interactive: bool = True, quiet: bool = False) -> Dict[str, A
|
||||||
new_entry = {"api": old_url}
|
new_entry = {"api": old_url}
|
||||||
if old_name:
|
if old_name:
|
||||||
new_entry["name"] = old_name
|
new_entry["name"] = old_name
|
||||||
if old_key and old_key not in ("no-key", "no-key-required", ""):
|
if old_key and old_key not in {"no-key", "no-key-required", ""}:
|
||||||
new_entry["api_key"] = old_key
|
new_entry["api_key"] = old_key
|
||||||
|
|
||||||
# Carry over model and api_mode if present
|
# Carry over model and api_mode if present
|
||||||
|
|
@ -3402,7 +3402,7 @@ def migrate_config(interactive: bool = True, quiet: bool = False) -> Dict[str, A
|
||||||
stt.pop("model", None)
|
stt.pop("model", None)
|
||||||
# Place it in the appropriate provider section only if the
|
# Place it in the appropriate provider section only if the
|
||||||
# user didn't already set a model there
|
# user didn't already set a model there
|
||||||
if provider in ("local", "local_command"):
|
if provider in {"local", "local_command"}:
|
||||||
# Don't migrate an OpenAI model name into the local section
|
# Don't migrate an OpenAI model name into the local section
|
||||||
_local_models = {
|
_local_models = {
|
||||||
"tiny.en", "tiny", "base.en", "base", "small.en", "small",
|
"tiny.en", "tiny", "base.en", "base", "small.en", "small",
|
||||||
|
|
@ -3486,7 +3486,7 @@ def migrate_config(interactive: bool = True, quiet: bool = False) -> Dict[str, A
|
||||||
if not aux_comp.get("model"):
|
if not aux_comp.get("model"):
|
||||||
aux_comp["model"] = str(s_model).strip()
|
aux_comp["model"] = str(s_model).strip()
|
||||||
migrated_keys.append(f"model={s_model}")
|
migrated_keys.append(f"model={s_model}")
|
||||||
if s_provider and str(s_provider).strip() not in ("", "auto"):
|
if s_provider and str(s_provider).strip() not in {"", "auto"}:
|
||||||
aux = config.setdefault("auxiliary", {})
|
aux = config.setdefault("auxiliary", {})
|
||||||
aux_comp = aux.setdefault("compression", {})
|
aux_comp = aux.setdefault("compression", {})
|
||||||
if not aux_comp.get("provider") or aux_comp.get("provider") == "auto":
|
if not aux_comp.get("provider") or aux_comp.get("provider") == "auto":
|
||||||
|
|
@ -3717,7 +3717,7 @@ def migrate_config(interactive: bool = True, quiet: bool = False) -> Dict[str, A
|
||||||
except (EOFError, KeyboardInterrupt):
|
except (EOFError, KeyboardInterrupt):
|
||||||
answer = "n"
|
answer = "n"
|
||||||
|
|
||||||
if answer in ("y", "yes"):
|
if answer in {"y", "yes"}:
|
||||||
print()
|
print()
|
||||||
for name, info in new_and_unset:
|
for name, info in new_and_unset:
|
||||||
if info.get("url"):
|
if info.get("url"):
|
||||||
|
|
@ -3778,7 +3778,7 @@ def migrate_config(interactive: bool = True, quiet: bool = False) -> Dict[str, A
|
||||||
except (EOFError, KeyboardInterrupt):
|
except (EOFError, KeyboardInterrupt):
|
||||||
answer = "n"
|
answer = "n"
|
||||||
|
|
||||||
if answer in ("y", "yes"):
|
if answer in {"y", "yes"}:
|
||||||
print()
|
print()
|
||||||
config = load_config()
|
config = load_config()
|
||||||
try:
|
try:
|
||||||
|
|
@ -4860,9 +4860,9 @@ def set_config_value(key: str, value: str):
|
||||||
# inline navigation here silently overwrote lists with dicts.
|
# inline navigation here silently overwrote lists with dicts.
|
||||||
|
|
||||||
# Convert value to appropriate type
|
# Convert value to appropriate type
|
||||||
if value.lower() in ('true', 'yes', 'on'):
|
if value.lower() in {'true', 'yes', 'on'}:
|
||||||
value = True
|
value = True
|
||||||
elif value.lower() in ('false', 'no', 'off'):
|
elif value.lower() in {'false', 'no', 'off'}:
|
||||||
value = False
|
value = False
|
||||||
elif value.isdigit():
|
elif value.isdigit():
|
||||||
value = int(value)
|
value = int(value)
|
||||||
|
|
@ -5067,7 +5067,7 @@ def _inject_profile_env_vars() -> None:
|
||||||
try:
|
try:
|
||||||
from providers import list_providers
|
from providers import list_providers
|
||||||
for _pp in list_providers():
|
for _pp in list_providers():
|
||||||
if _pp.auth_type not in ("api_key",):
|
if _pp.auth_type not in {"api_key",}:
|
||||||
continue
|
continue
|
||||||
for _var in _pp.env_vars:
|
for _var in _pp.env_vars:
|
||||||
if _var in OPTIONAL_ENV_VARS:
|
if _var in OPTIONAL_ENV_VARS:
|
||||||
|
|
|
||||||
|
|
@ -128,7 +128,7 @@ def _try_gh_cli_token() -> Optional[str]:
|
||||||
|
|
||||||
# Build a clean env so gh doesn't short-circuit on GITHUB_TOKEN / GH_TOKEN
|
# Build a clean env so gh doesn't short-circuit on GITHUB_TOKEN / GH_TOKEN
|
||||||
clean_env = {k: v for k, v in os.environ.items()
|
clean_env = {k: v for k, v in os.environ.items()
|
||||||
if k not in ("GITHUB_TOKEN", "GH_TOKEN")}
|
if k not in {"GITHUB_TOKEN", "GH_TOKEN"}}
|
||||||
|
|
||||||
for gh_path in _gh_cli_candidates():
|
for gh_path in _gh_cli_candidates():
|
||||||
cmd = [gh_path, "auth", "token"]
|
cmd = [gh_path, "auth", "token"]
|
||||||
|
|
|
||||||
|
|
@ -347,7 +347,7 @@ def _cmd_prune(args) -> int:
|
||||||
except (EOFError, KeyboardInterrupt):
|
except (EOFError, KeyboardInterrupt):
|
||||||
print("\ncurator: aborted")
|
print("\ncurator: aborted")
|
||||||
return 1
|
return 1
|
||||||
if reply not in ("y", "yes"):
|
if reply not in {"y", "yes"}:
|
||||||
print("curator: aborted")
|
print("curator: aborted")
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
|
|
@ -449,7 +449,7 @@ def _cmd_rollback(args) -> int:
|
||||||
except (EOFError, KeyboardInterrupt):
|
except (EOFError, KeyboardInterrupt):
|
||||||
print("\ncancelled")
|
print("\ncancelled")
|
||||||
return 1
|
return 1
|
||||||
if ans not in ("y", "yes"):
|
if ans not in {"y", "yes"}:
|
||||||
print("cancelled")
|
print("cancelled")
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -139,16 +139,16 @@ def curses_checklist(
|
||||||
stdscr.refresh()
|
stdscr.refresh()
|
||||||
key = stdscr.getch()
|
key = stdscr.getch()
|
||||||
|
|
||||||
if key in (curses.KEY_UP, ord("k")):
|
if key in {curses.KEY_UP, ord("k")}:
|
||||||
cursor = (cursor - 1) % len(items)
|
cursor = (cursor - 1) % len(items)
|
||||||
elif key in (curses.KEY_DOWN, ord("j")):
|
elif key in {curses.KEY_DOWN, ord("j")}:
|
||||||
cursor = (cursor + 1) % len(items)
|
cursor = (cursor + 1) % len(items)
|
||||||
elif key == ord(" "):
|
elif key == ord(" "):
|
||||||
chosen.symmetric_difference_update({cursor})
|
chosen.symmetric_difference_update({cursor})
|
||||||
elif key in (curses.KEY_ENTER, 10, 13):
|
elif key in {curses.KEY_ENTER, 10, 13}:
|
||||||
result_holder[0] = set(chosen)
|
result_holder[0] = set(chosen)
|
||||||
return
|
return
|
||||||
elif key in (27, ord("q")):
|
elif key in {27, ord("q")}:
|
||||||
result_holder[0] = cancel_returns
|
result_holder[0] = cancel_returns
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
@ -265,14 +265,14 @@ def curses_radiolist(
|
||||||
stdscr.refresh()
|
stdscr.refresh()
|
||||||
key = stdscr.getch()
|
key = stdscr.getch()
|
||||||
|
|
||||||
if key in (curses.KEY_UP, ord("k")):
|
if key in {curses.KEY_UP, ord("k")}:
|
||||||
cursor = (cursor - 1) % len(items)
|
cursor = (cursor - 1) % len(items)
|
||||||
elif key in (curses.KEY_DOWN, ord("j")):
|
elif key in {curses.KEY_DOWN, ord("j")}:
|
||||||
cursor = (cursor + 1) % len(items)
|
cursor = (cursor + 1) % len(items)
|
||||||
elif key in (ord(" "), curses.KEY_ENTER, 10, 13):
|
elif key in {ord(" "), curses.KEY_ENTER, 10, 13}:
|
||||||
result_holder[0] = cursor
|
result_holder[0] = cursor
|
||||||
return
|
return
|
||||||
elif key in (27, ord("q")):
|
elif key in {27, ord("q")}:
|
||||||
result_holder[0] = cancel_returns
|
result_holder[0] = cancel_returns
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
@ -388,14 +388,14 @@ def curses_single_select(
|
||||||
stdscr.refresh()
|
stdscr.refresh()
|
||||||
key = stdscr.getch()
|
key = stdscr.getch()
|
||||||
|
|
||||||
if key in (curses.KEY_UP, ord("k")):
|
if key in {curses.KEY_UP, ord("k")}:
|
||||||
cursor = (cursor - 1) % len(all_items)
|
cursor = (cursor - 1) % len(all_items)
|
||||||
elif key in (curses.KEY_DOWN, ord("j")):
|
elif key in {curses.KEY_DOWN, ord("j")}:
|
||||||
cursor = (cursor + 1) % len(all_items)
|
cursor = (cursor + 1) % len(all_items)
|
||||||
elif key in (curses.KEY_ENTER, 10, 13):
|
elif key in {curses.KEY_ENTER, 10, 13}:
|
||||||
result_holder[0] = cursor
|
result_holder[0] = cursor
|
||||||
return
|
return
|
||||||
elif key in (27, ord("q")):
|
elif key in {27, ord("q")}:
|
||||||
result_holder[0] = None
|
result_holder[0] = None
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -93,7 +93,7 @@ def poll_registration(device_code: str) -> dict:
|
||||||
"""
|
"""
|
||||||
data = _api_post("/app/registration/poll", {"device_code": device_code})
|
data = _api_post("/app/registration/poll", {"device_code": device_code})
|
||||||
status_raw = str(data.get("status", "")).strip().upper()
|
status_raw = str(data.get("status", "")).strip().upper()
|
||||||
if status_raw not in ("WAITING", "SUCCESS", "FAIL", "EXPIRED"):
|
if status_raw not in {"WAITING", "SUCCESS", "FAIL", "EXPIRED"}:
|
||||||
status_raw = "UNKNOWN"
|
status_raw = "UNKNOWN"
|
||||||
return {
|
return {
|
||||||
"status": status_raw,
|
"status": status_raw,
|
||||||
|
|
|
||||||
|
|
@ -473,7 +473,7 @@ def run_doctor(args):
|
||||||
if (
|
if (
|
||||||
provider
|
provider
|
||||||
and _resolve_auth_provider is not None
|
and _resolve_auth_provider is not None
|
||||||
and provider not in ("auto", "custom")
|
and provider not in {"auto", "custom"}
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
runtime_provider = _resolve_auth_provider(provider)
|
runtime_provider = _resolve_auth_provider(provider)
|
||||||
|
|
@ -485,7 +485,7 @@ def run_doctor(args):
|
||||||
if (
|
if (
|
||||||
provider
|
provider
|
||||||
and _resolve_provider_full is not None
|
and _resolve_provider_full is not None
|
||||||
and provider not in ("auto", "custom")
|
and provider not in {"auto", "custom"}
|
||||||
):
|
):
|
||||||
provider_def = _resolve_provider_full(provider, user_providers, custom_providers)
|
provider_def = _resolve_provider_full(provider, user_providers, custom_providers)
|
||||||
catalog_provider = provider_def.id if provider_def is not None else None
|
catalog_provider = provider_def.id if provider_def is not None else None
|
||||||
|
|
@ -542,7 +542,7 @@ def run_doctor(args):
|
||||||
# own env-var checks elsewhere in doctor, and get_auth_status()
|
# own env-var checks elsewhere in doctor, and get_auth_status()
|
||||||
# returns a bare {logged_in: False} for anything it doesn't
|
# returns a bare {logged_in: False} for anything it doesn't
|
||||||
# explicitly dispatch, which would produce false positives.
|
# explicitly dispatch, which would produce false positives.
|
||||||
if runtime_provider and runtime_provider not in ("auto", "custom", "openrouter"):
|
if runtime_provider and runtime_provider not in {"auto", "custom", "openrouter"}:
|
||||||
try:
|
try:
|
||||||
from hermes_cli.auth import PROVIDER_REGISTRY, get_auth_status
|
from hermes_cli.auth import PROVIDER_REGISTRY, get_auth_status
|
||||||
pconfig = PROVIDER_REGISTRY.get(runtime_provider)
|
pconfig = PROVIDER_REGISTRY.get(runtime_provider)
|
||||||
|
|
@ -1010,7 +1010,7 @@ def run_doctor(args):
|
||||||
issues.append(f"Set TERMINAL_VERCEL_RUNTIME to one of: {supported}")
|
issues.append(f"Set TERMINAL_VERCEL_RUNTIME to one of: {supported}")
|
||||||
|
|
||||||
disk = os.getenv("TERMINAL_CONTAINER_DISK", "51200").strip()
|
disk = os.getenv("TERMINAL_CONTAINER_DISK", "51200").strip()
|
||||||
if disk in ("", "0", "51200"):
|
if disk in {"", "0", "51200"}:
|
||||||
check_ok("Vercel disk setting", "(uses platform default)")
|
check_ok("Vercel disk setting", "(uses platform default)")
|
||||||
else:
|
else:
|
||||||
check_fail("Vercel custom disk unsupported", "(reset terminal.container_disk to 51200)")
|
check_fail("Vercel custom disk unsupported", "(reset terminal.container_disk to 51200)")
|
||||||
|
|
@ -1036,7 +1036,7 @@ def run_doctor(args):
|
||||||
for line in auth_status.detail_lines:
|
for line in auth_status.detail_lines:
|
||||||
check_info(f"Vercel auth {line}")
|
check_info(f"Vercel auth {line}")
|
||||||
|
|
||||||
persistent = os.getenv("TERMINAL_CONTAINER_PERSISTENT", "true").lower() in ("1", "true", "yes", "on")
|
persistent = os.getenv("TERMINAL_CONTAINER_PERSISTENT", "true").lower() in {"1", "true", "yes", "on"}
|
||||||
if persistent:
|
if persistent:
|
||||||
check_info("Vercel persistence: snapshot filesystem only; live processes do not survive sandbox recreation")
|
check_info("Vercel persistence: snapshot filesystem only; live processes do not survive sandbox recreation")
|
||||||
else:
|
else:
|
||||||
|
|
|
||||||
|
|
@ -307,7 +307,7 @@ def cmd_fallback_clear(args) -> None: # noqa: ARG001
|
||||||
print()
|
print()
|
||||||
print(" Cancelled.")
|
print(" Cancelled.")
|
||||||
return
|
return
|
||||||
if resp not in ("y", "yes"):
|
if resp not in {"y", "yes"}:
|
||||||
print(" Cancelled — no change.")
|
print(" Cancelled — no change.")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
@ -347,11 +347,11 @@ def _numbered_pick(question: str, choices: List[str]) -> Optional[int]:
|
||||||
def cmd_fallback(args) -> None:
|
def cmd_fallback(args) -> None:
|
||||||
"""Top-level dispatcher for ``hermes fallback [subcommand]``."""
|
"""Top-level dispatcher for ``hermes fallback [subcommand]``."""
|
||||||
sub = getattr(args, "fallback_command", None)
|
sub = getattr(args, "fallback_command", None)
|
||||||
if sub in (None, "", "list", "ls"):
|
if sub in {None, "", "list", "ls"}:
|
||||||
cmd_fallback_list(args)
|
cmd_fallback_list(args)
|
||||||
elif sub == "add":
|
elif sub == "add":
|
||||||
cmd_fallback_add(args)
|
cmd_fallback_add(args)
|
||||||
elif sub in ("remove", "rm"):
|
elif sub in {"remove", "rm"}:
|
||||||
cmd_fallback_remove(args)
|
cmd_fallback_remove(args)
|
||||||
elif sub == "clear":
|
elif sub == "clear":
|
||||||
cmd_fallback_clear(args)
|
cmd_fallback_clear(args)
|
||||||
|
|
|
||||||
|
|
@ -1194,7 +1194,7 @@ def _systemd_operational(system: bool = False) -> bool:
|
||||||
)
|
)
|
||||||
# "running", "degraded", "starting" all mean systemd is PID 1
|
# "running", "degraded", "starting" all mean systemd is PID 1
|
||||||
status = result.stdout.strip().lower()
|
status = result.stdout.strip().lower()
|
||||||
return status in ("running", "degraded", "starting", "initializing")
|
return status in {"running", "degraded", "starting", "initializing"}
|
||||||
except (RuntimeError, subprocess.TimeoutExpired, OSError):
|
except (RuntimeError, subprocess.TimeoutExpired, OSError):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
@ -2915,7 +2915,7 @@ def launchd_start():
|
||||||
try:
|
try:
|
||||||
subprocess.run(["launchctl", "kickstart", f"{_launchd_domain()}/{label}"], check=True, timeout=30)
|
subprocess.run(["launchctl", "kickstart", f"{_launchd_domain()}/{label}"], check=True, timeout=30)
|
||||||
except subprocess.CalledProcessError as e:
|
except subprocess.CalledProcessError as e:
|
||||||
if e.returncode not in (3, 113):
|
if e.returncode not in {3, 113}:
|
||||||
raise
|
raise
|
||||||
print("↻ launchd job was unloaded; reloading service definition")
|
print("↻ launchd job was unloaded; reloading service definition")
|
||||||
subprocess.run(["launchctl", "bootstrap", _launchd_domain(), str(plist_path)], check=True, timeout=30)
|
subprocess.run(["launchctl", "bootstrap", _launchd_domain(), str(plist_path)], check=True, timeout=30)
|
||||||
|
|
@ -2939,7 +2939,7 @@ def launchd_stop():
|
||||||
try:
|
try:
|
||||||
subprocess.run(["launchctl", "bootout", target], check=True, timeout=90)
|
subprocess.run(["launchctl", "bootout", target], check=True, timeout=90)
|
||||||
except subprocess.CalledProcessError as e:
|
except subprocess.CalledProcessError as e:
|
||||||
if e.returncode in (3, 113):
|
if e.returncode in {3, 113}:
|
||||||
pass # Already unloaded — nothing to stop.
|
pass # Already unloaded — nothing to stop.
|
||||||
else:
|
else:
|
||||||
raise
|
raise
|
||||||
|
|
@ -3011,7 +3011,7 @@ def launchd_restart():
|
||||||
subprocess.run(["launchctl", "kickstart", "-k", target], check=True, timeout=90)
|
subprocess.run(["launchctl", "kickstart", "-k", target], check=True, timeout=90)
|
||||||
print("✓ Service restarted")
|
print("✓ Service restarted")
|
||||||
except subprocess.CalledProcessError as e:
|
except subprocess.CalledProcessError as e:
|
||||||
if e.returncode not in (3, 113):
|
if e.returncode not in {3, 113}:
|
||||||
raise
|
raise
|
||||||
# Job not loaded — bootstrap and start fresh
|
# Job not loaded — bootstrap and start fresh
|
||||||
print("↻ launchd job was unloaded; reloading")
|
print("↻ launchd job was unloaded; reloading")
|
||||||
|
|
@ -3749,7 +3749,7 @@ def _platform_status(platform: dict) -> str:
|
||||||
password = get_env_value("MATRIX_PASSWORD")
|
password = get_env_value("MATRIX_PASSWORD")
|
||||||
if (val or password) and homeserver:
|
if (val or password) and homeserver:
|
||||||
e2ee = get_env_value("MATRIX_ENCRYPTION")
|
e2ee = get_env_value("MATRIX_ENCRYPTION")
|
||||||
suffix = " + E2EE" if e2ee and e2ee.lower() in ("true", "1", "yes") else ""
|
suffix = " + E2EE" if e2ee and e2ee.lower() in {"true", "1", "yes"} else ""
|
||||||
return f"configured{suffix}"
|
return f"configured{suffix}"
|
||||||
if val or password or homeserver:
|
if val or password or homeserver:
|
||||||
return "partially configured"
|
return "partially configured"
|
||||||
|
|
|
||||||
|
|
@ -270,7 +270,7 @@ def _parse_judge_response(raw: str) -> Tuple[bool, str, bool]:
|
||||||
|
|
||||||
done_val = data.get("done")
|
done_val = data.get("done")
|
||||||
if isinstance(done_val, str):
|
if isinstance(done_val, str):
|
||||||
done = done_val.strip().lower() in ("true", "yes", "1", "done")
|
done = done_val.strip().lower() in {"true", "yes", "1", "done"}
|
||||||
else:
|
else:
|
||||||
done = bool(done_val)
|
done = bool(done_val)
|
||||||
reason = str(data.get("reason") or "").strip()
|
reason = str(data.get("reason") or "").strip()
|
||||||
|
|
@ -389,11 +389,11 @@ class GoalManager:
|
||||||
return self._state is not None and self._state.status == "active"
|
return self._state is not None and self._state.status == "active"
|
||||||
|
|
||||||
def has_goal(self) -> bool:
|
def has_goal(self) -> bool:
|
||||||
return self._state is not None and self._state.status in ("active", "paused")
|
return self._state is not None and self._state.status in {"active", "paused"}
|
||||||
|
|
||||||
def status_line(self) -> str:
|
def status_line(self) -> str:
|
||||||
s = self._state
|
s = self._state
|
||||||
if s is None or s.status in ("cleared",):
|
if s is None or s.status in {"cleared",}:
|
||||||
return "No active goal. Set one with /goal <text>."
|
return "No active goal. Set one with /goal <text>."
|
||||||
turns = f"{s.turns_used}/{s.max_turns} turns"
|
turns = f"{s.turns_used}/{s.max_turns} turns"
|
||||||
if s.status == "active":
|
if s.status == "active":
|
||||||
|
|
|
||||||
|
|
@ -32,11 +32,11 @@ def hooks_command(args) -> None:
|
||||||
print("Run 'hermes hooks --help' for details.")
|
print("Run 'hermes hooks --help' for details.")
|
||||||
return
|
return
|
||||||
|
|
||||||
if sub in ("list", "ls"):
|
if sub in {"list", "ls"}:
|
||||||
_cmd_list(args)
|
_cmd_list(args)
|
||||||
elif sub == "test":
|
elif sub == "test":
|
||||||
_cmd_test(args)
|
_cmd_test(args)
|
||||||
elif sub in ("revoke", "remove", "rm"):
|
elif sub in {"revoke", "remove", "rm"}:
|
||||||
_cmd_revoke(args)
|
_cmd_revoke(args)
|
||||||
elif sub == "doctor":
|
elif sub == "doctor":
|
||||||
_cmd_doctor(args)
|
_cmd_doctor(args)
|
||||||
|
|
@ -220,7 +220,7 @@ def _cmd_test(args) -> None:
|
||||||
if getattr(args, "for_tool", None):
|
if getattr(args, "for_tool", None):
|
||||||
specs = [
|
specs = [
|
||||||
s for s in specs
|
s for s in specs
|
||||||
if s.event not in ("pre_tool_call", "post_tool_call")
|
if s.event not in {"pre_tool_call", "post_tool_call"}
|
||||||
or s.matches_tool(args.for_tool)
|
or s.matches_tool(args.for_tool)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -82,7 +82,7 @@ def _parse_workspace_flag(value: str) -> tuple[str, Optional[str]]:
|
||||||
if not value:
|
if not value:
|
||||||
return ("scratch", None)
|
return ("scratch", None)
|
||||||
v = value.strip()
|
v = value.strip()
|
||||||
if v in ("scratch", "worktree"):
|
if v in {"scratch", "worktree"}:
|
||||||
return (v, None)
|
return (v, None)
|
||||||
if v.startswith("dir:"):
|
if v.startswith("dir:"):
|
||||||
path = v[len("dir:"):].strip()
|
path = v[len("dir:"):].strip()
|
||||||
|
|
@ -788,15 +788,15 @@ def _dispatch_boards(args: argparse.Namespace) -> int:
|
||||||
can still run ``boards create`` / ``boards list``.
|
can still run ``boards create`` / ``boards list``.
|
||||||
"""
|
"""
|
||||||
sub = getattr(args, "boards_action", None) or "list"
|
sub = getattr(args, "boards_action", None) or "list"
|
||||||
if sub in ("list", "ls"):
|
if sub in {"list", "ls"}:
|
||||||
return _cmd_boards_list(args)
|
return _cmd_boards_list(args)
|
||||||
if sub in ("create", "new"):
|
if sub in {"create", "new"}:
|
||||||
return _cmd_boards_create(args)
|
return _cmd_boards_create(args)
|
||||||
if sub in ("rm", "remove", "delete"):
|
if sub in {"rm", "remove", "delete"}:
|
||||||
return _cmd_boards_rm(args)
|
return _cmd_boards_rm(args)
|
||||||
if sub in ("switch", "use"):
|
if sub in {"switch", "use"}:
|
||||||
return _cmd_boards_switch(args)
|
return _cmd_boards_switch(args)
|
||||||
if sub in ("show", "current"):
|
if sub in {"show", "current"}:
|
||||||
return _cmd_boards_show(args)
|
return _cmd_boards_show(args)
|
||||||
if sub == "rename":
|
if sub == "rename":
|
||||||
return _cmd_boards_rename(args)
|
return _cmd_boards_rename(args)
|
||||||
|
|
@ -1301,7 +1301,7 @@ def _cmd_show(args: argparse.Namespace) -> int:
|
||||||
|
|
||||||
|
|
||||||
def _cmd_assign(args: argparse.Namespace) -> int:
|
def _cmd_assign(args: argparse.Namespace) -> int:
|
||||||
profile = None if args.profile.lower() in ("none", "-", "null") else args.profile
|
profile = None if args.profile.lower() in {"none", "-", "null"} else args.profile
|
||||||
with kb.connect() as conn:
|
with kb.connect() as conn:
|
||||||
ok = kb.assign_task(conn, args.task_id, profile)
|
ok = kb.assign_task(conn, args.task_id, profile)
|
||||||
if not ok:
|
if not ok:
|
||||||
|
|
@ -1328,7 +1328,7 @@ def _cmd_reclaim(args: argparse.Namespace) -> int:
|
||||||
|
|
||||||
|
|
||||||
def _cmd_reassign(args: argparse.Namespace) -> int:
|
def _cmd_reassign(args: argparse.Namespace) -> int:
|
||||||
profile = None if args.profile.lower() in ("none", "-", "null") else args.profile
|
profile = None if args.profile.lower() in {"none", "-", "null"} else args.profile
|
||||||
with kb.connect() as conn:
|
with kb.connect() as conn:
|
||||||
ok = kb.reassign_task(
|
ok = kb.reassign_task(
|
||||||
conn, args.task_id, profile,
|
conn, args.task_id, profile,
|
||||||
|
|
@ -2230,7 +2230,7 @@ def run_slash(rest: str) -> str:
|
||||||
out = buf_out.getvalue().rstrip()
|
out = buf_out.getvalue().rstrip()
|
||||||
err = buf_err.getvalue().rstrip()
|
err = buf_err.getvalue().rstrip()
|
||||||
# Help dump (exit 0) → return the captured help text directly.
|
# Help dump (exit 0) → return the captured help text directly.
|
||||||
if exc.code in (0, None) and out:
|
if exc.code in {0, None} and out:
|
||||||
return out
|
return out
|
||||||
body = err or out
|
body = err or out
|
||||||
return f"⚠ /kanban usage error\n{body}" if body else "⚠ /kanban usage error"
|
return f"⚠ /kanban usage error\n{body}" if body else "⚠ /kanban usage error"
|
||||||
|
|
|
||||||
|
|
@ -1844,7 +1844,7 @@ def recompute_ready(conn: sqlite3.Connection) -> int:
|
||||||
"WHERE l.child_id = ?",
|
"WHERE l.child_id = ?",
|
||||||
(task_id,),
|
(task_id,),
|
||||||
).fetchall()
|
).fetchall()
|
||||||
if all(p["status"] in ("done", "archived") for p in parents):
|
if all(p["status"] in {"done", "archived"} for p in parents):
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"UPDATE tasks SET status = 'ready' WHERE id = ? AND status = 'todo'",
|
"UPDATE tasks SET status = 'ready' WHERE id = ? AND status = 'todo'",
|
||||||
(task_id,),
|
(task_id,),
|
||||||
|
|
|
||||||
|
|
@ -177,7 +177,7 @@ def _active_hallucination_events(
|
||||||
active: list[Any] = []
|
active: list[Any] = []
|
||||||
for ev in events:
|
for ev in events:
|
||||||
k = _event_kind(ev)
|
k = _event_kind(ev)
|
||||||
if k in ("completed", "edited"):
|
if k in {"completed", "edited"}:
|
||||||
active.clear()
|
active.clear()
|
||||||
elif k == kind:
|
elif k == kind:
|
||||||
active.append(ev)
|
active.append(ev)
|
||||||
|
|
@ -193,7 +193,7 @@ def _latest_clean_event_ts(events: Iterable[Any]) -> int:
|
||||||
"""
|
"""
|
||||||
latest = 0
|
latest = 0
|
||||||
for ev in events:
|
for ev in events:
|
||||||
if _event_kind(ev) in ("completed", "edited"):
|
if _event_kind(ev) in {"completed", "edited"}:
|
||||||
t = _event_ts(ev)
|
t = _event_ts(ev)
|
||||||
latest = max(latest, t)
|
latest = max(latest, t)
|
||||||
return latest
|
return latest
|
||||||
|
|
@ -355,7 +355,7 @@ def _rule_repeated_failures(task, events, runs, now, cfg) -> list[Diagnostic]:
|
||||||
most_recent_outcome = None
|
most_recent_outcome = None
|
||||||
for r in reversed(ordered_runs):
|
for r in reversed(ordered_runs):
|
||||||
oc = _task_field(r, "outcome")
|
oc = _task_field(r, "outcome")
|
||||||
if oc in ("spawn_failed", "timed_out", "crashed"):
|
if oc in {"spawn_failed", "timed_out", "crashed"}:
|
||||||
most_recent_outcome = oc
|
most_recent_outcome = oc
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|
@ -373,7 +373,7 @@ def _rule_repeated_failures(task, events, runs, now, cfg) -> list[Diagnostic]:
|
||||||
label=f"Fix profile auth: hermes -p {assignee} auth",
|
label=f"Fix profile auth: hermes -p {assignee} auth",
|
||||||
payload={"command": f"hermes -p {assignee} auth"},
|
payload={"command": f"hermes -p {assignee} auth"},
|
||||||
))
|
))
|
||||||
elif most_recent_outcome in ("timed_out", "crashed"):
|
elif most_recent_outcome in {"timed_out", "crashed"}:
|
||||||
# Worker got off the ground but died. Logs are the right place
|
# Worker got off the ground but died. Logs are the right place
|
||||||
# to diagnose; reclaim/reassign are the recovery levers.
|
# to diagnose; reclaim/reassign are the recovery levers.
|
||||||
task_id = _task_field(task, "id")
|
task_id = _task_field(task, "id")
|
||||||
|
|
@ -466,7 +466,7 @@ def _rule_repeated_crashes(task, events, runs, now, cfg) -> list[Diagnostic]:
|
||||||
consecutive += 1
|
consecutive += 1
|
||||||
if last_err is None:
|
if last_err is None:
|
||||||
last_err = _task_field(r, "error")
|
last_err = _task_field(r, "error")
|
||||||
elif outcome in ("completed", "reclaimed"):
|
elif outcome in {"completed", "reclaimed"}:
|
||||||
# A success (or manual reclaim) breaks the streak.
|
# A success (or manual reclaim) breaks the streak.
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
|
|
@ -541,7 +541,7 @@ def _rule_stuck_in_blocked(task, events, runs, now, cfg) -> list[Diagnostic]:
|
||||||
return []
|
return []
|
||||||
# Any comment / unblock after the block breaks the "stale" signal.
|
# Any comment / unblock after the block breaks the "stale" signal.
|
||||||
for ev in events:
|
for ev in events:
|
||||||
if _event_kind(ev) in ("commented", "unblocked") and _event_ts(ev) > last_blocked_ts:
|
if _event_kind(ev) in {"commented", "unblocked"} and _event_ts(ev) > last_blocked_ts:
|
||||||
return []
|
return []
|
||||||
actions: list[DiagnosticAction] = [
|
actions: list[DiagnosticAction] = [
|
||||||
DiagnosticAction(
|
DiagnosticAction(
|
||||||
|
|
|
||||||
|
|
@ -124,7 +124,7 @@ def _apply_profile_override() -> None:
|
||||||
|
|
||||||
# 1. Check for explicit -p / --profile flag
|
# 1. Check for explicit -p / --profile flag
|
||||||
for i, arg in enumerate(argv):
|
for i, arg in enumerate(argv):
|
||||||
if arg in ("--profile", "-p") and i + 1 < len(argv):
|
if arg in {"--profile", "-p"} and i + 1 < len(argv):
|
||||||
profile_name = argv[i + 1]
|
profile_name = argv[i + 1]
|
||||||
consume = 2
|
consume = 2
|
||||||
break
|
break
|
||||||
|
|
@ -192,7 +192,7 @@ def _apply_profile_override() -> None:
|
||||||
# Strip the flag from argv so argparse doesn't choke
|
# Strip the flag from argv so argparse doesn't choke
|
||||||
if consume > 0:
|
if consume > 0:
|
||||||
for i, arg in enumerate(argv):
|
for i, arg in enumerate(argv):
|
||||||
if arg in ("--profile", "-p"):
|
if arg in {"--profile", "-p"}:
|
||||||
start = i + 1 # +1 because argv is sys.argv[1:]
|
start = i + 1 # +1 because argv is sys.argv[1:]
|
||||||
sys.argv = sys.argv[:start] + sys.argv[start + consume :]
|
sys.argv = sys.argv[:start] + sys.argv[start + consume :]
|
||||||
break
|
break
|
||||||
|
|
@ -567,13 +567,13 @@ def _session_browse_picker(sessions: list) -> Optional[str]:
|
||||||
stdscr.refresh()
|
stdscr.refresh()
|
||||||
key = stdscr.getch()
|
key = stdscr.getch()
|
||||||
|
|
||||||
if key in (curses.KEY_UP,):
|
if key in {curses.KEY_UP,}:
|
||||||
if filtered:
|
if filtered:
|
||||||
cursor = (cursor - 1) % len(filtered)
|
cursor = (cursor - 1) % len(filtered)
|
||||||
elif key in (curses.KEY_DOWN,):
|
elif key in {curses.KEY_DOWN,}:
|
||||||
if filtered:
|
if filtered:
|
||||||
cursor = (cursor + 1) % len(filtered)
|
cursor = (cursor + 1) % len(filtered)
|
||||||
elif key in (curses.KEY_ENTER, 10, 13):
|
elif key in {curses.KEY_ENTER, 10, 13}:
|
||||||
if filtered:
|
if filtered:
|
||||||
result_holder[0] = filtered[cursor]["id"]
|
result_holder[0] = filtered[cursor]["id"]
|
||||||
return
|
return
|
||||||
|
|
@ -587,7 +587,7 @@ def _session_browse_picker(sessions: list) -> Optional[str]:
|
||||||
else:
|
else:
|
||||||
# Second Esc exits
|
# Second Esc exits
|
||||||
return
|
return
|
||||||
elif key in (curses.KEY_BACKSPACE, 127, 8):
|
elif key in {curses.KEY_BACKSPACE, 127, 8}:
|
||||||
if search_text:
|
if search_text:
|
||||||
search_text = search_text[:-1]
|
search_text = search_text[:-1]
|
||||||
if search_text:
|
if search_text:
|
||||||
|
|
@ -626,7 +626,7 @@ def _session_browse_picker(sessions: list) -> Optional[str]:
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
val = input(f"\n Select [1-{len(sessions)}]: ").strip()
|
val = input(f"\n Select [1-{len(sessions)}]: ").strip()
|
||||||
if not val or val.lower() in ("q", "quit", "exit"):
|
if not val or val.lower() in {"q", "quit", "exit"}:
|
||||||
return None
|
return None
|
||||||
idx = int(val) - 1
|
idx = int(val) - 1
|
||||||
if 0 <= idx < len(sessions):
|
if 0 <= idx < len(sessions):
|
||||||
|
|
@ -1303,7 +1303,7 @@ def _launch_tui(
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
code = 130
|
code = 130
|
||||||
|
|
||||||
if code in (0, 130):
|
if code in {0, 130}:
|
||||||
_print_tui_exit_summary(resume_session_id, active_session_file)
|
_print_tui_exit_summary(resume_session_id, active_session_file)
|
||||||
finally:
|
finally:
|
||||||
try:
|
try:
|
||||||
|
|
@ -1403,7 +1403,7 @@ def cmd_chat(args):
|
||||||
reply = input("Run setup now? [Y/n] ").strip().lower()
|
reply = input("Run setup now? [Y/n] ").strip().lower()
|
||||||
except (EOFError, KeyboardInterrupt):
|
except (EOFError, KeyboardInterrupt):
|
||||||
reply = "n"
|
reply = "n"
|
||||||
if reply in ("", "y", "yes"):
|
if reply in {"", "y", "yes"}:
|
||||||
cmd_setup(args)
|
cmd_setup(args)
|
||||||
return
|
return
|
||||||
print()
|
print()
|
||||||
|
|
@ -1583,7 +1583,7 @@ def cmd_whatsapp(args):
|
||||||
response = input("\n Update allowed users? [y/N] ").strip()
|
response = input("\n Update allowed users? [y/N] ").strip()
|
||||||
except (EOFError, KeyboardInterrupt):
|
except (EOFError, KeyboardInterrupt):
|
||||||
response = "n"
|
response = "n"
|
||||||
if response.lower() in ("y", "yes"):
|
if response.lower() in {"y", "yes"}:
|
||||||
if wa_mode == "bot":
|
if wa_mode == "bot":
|
||||||
phone = input(
|
phone = input(
|
||||||
" Phone numbers that can message the bot (comma-separated): "
|
" Phone numbers that can message the bot (comma-separated): "
|
||||||
|
|
@ -1658,7 +1658,7 @@ def cmd_whatsapp(args):
|
||||||
).strip()
|
).strip()
|
||||||
except (EOFError, KeyboardInterrupt):
|
except (EOFError, KeyboardInterrupt):
|
||||||
response = "n"
|
response = "n"
|
||||||
if response.lower() in ("y", "yes"):
|
if response.lower() in {"y", "yes"}:
|
||||||
shutil.rmtree(session_dir, ignore_errors=True)
|
shutil.rmtree(session_dir, ignore_errors=True)
|
||||||
session_dir.mkdir(parents=True, exist_ok=True)
|
session_dir.mkdir(parents=True, exist_ok=True)
|
||||||
print(" ✓ Session cleared")
|
print(" ✓ Session cleared")
|
||||||
|
|
@ -2012,7 +2012,7 @@ def select_provider_and_model(args=None):
|
||||||
_model_flow_bedrock(config, current_model)
|
_model_flow_bedrock(config, current_model)
|
||||||
elif selected_provider == "azure-foundry":
|
elif selected_provider == "azure-foundry":
|
||||||
_model_flow_azure_foundry(config, current_model)
|
_model_flow_azure_foundry(config, current_model)
|
||||||
elif selected_provider in (
|
elif selected_provider in {
|
||||||
"gemini",
|
"gemini",
|
||||||
"deepseek",
|
"deepseek",
|
||||||
"xai",
|
"xai",
|
||||||
|
|
@ -2032,18 +2032,18 @@ def select_provider_and_model(args=None):
|
||||||
"ollama-cloud",
|
"ollama-cloud",
|
||||||
"tencent-tokenhub",
|
"tencent-tokenhub",
|
||||||
"lmstudio",
|
"lmstudio",
|
||||||
) or _is_profile_api_key_provider(selected_provider):
|
} or _is_profile_api_key_provider(selected_provider):
|
||||||
_model_flow_api_key_provider(config, selected_provider, current_model)
|
_model_flow_api_key_provider(config, selected_provider, current_model)
|
||||||
|
|
||||||
# ── Post-switch cleanup: clear stale OPENAI_BASE_URL ──────────────
|
# ── Post-switch cleanup: clear stale OPENAI_BASE_URL ──────────────
|
||||||
# When the user switches to a named provider (anything except "custom"),
|
# When the user switches to a named provider (anything except "custom"),
|
||||||
# a leftover OPENAI_BASE_URL in ~/.hermes/.env can poison auxiliary
|
# a leftover OPENAI_BASE_URL in ~/.hermes/.env can poison auxiliary
|
||||||
# clients that use provider:auto. Clear it proactively. (#5161)
|
# clients that use provider:auto. Clear it proactively. (#5161)
|
||||||
if selected_provider not in (
|
if selected_provider not in {
|
||||||
"custom",
|
"custom",
|
||||||
"cancel",
|
"cancel",
|
||||||
"remove-custom",
|
"remove-custom",
|
||||||
) and not selected_provider.startswith("custom:"):
|
} and not selected_provider.startswith("custom:"):
|
||||||
_clear_stale_openai_base_url()
|
_clear_stale_openai_base_url()
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -2169,7 +2169,7 @@ def _reset_aux_to_auto() -> int:
|
||||||
entry = {}
|
entry = {}
|
||||||
aux[task] = entry
|
aux[task] = entry
|
||||||
changed = False
|
changed = False
|
||||||
if entry.get("provider") not in (None, "", "auto"):
|
if entry.get("provider") not in {None, "", "auto"}:
|
||||||
entry["provider"] = "auto"
|
entry["provider"] = "auto"
|
||||||
changed = True
|
changed = True
|
||||||
for field in ("model", "base_url", "api_key"):
|
for field in ("model", "base_url", "api_key"):
|
||||||
|
|
@ -3080,7 +3080,7 @@ def _model_flow_custom(config):
|
||||||
_add_v1 = input(" Add /v1? [Y/n]: ").strip().lower()
|
_add_v1 = input(" Add /v1? [Y/n]: ").strip().lower()
|
||||||
except (KeyboardInterrupt, EOFError):
|
except (KeyboardInterrupt, EOFError):
|
||||||
_add_v1 = "n"
|
_add_v1 = "n"
|
||||||
if _add_v1 in ("", "y", "yes"):
|
if _add_v1 in {"", "y", "yes"}:
|
||||||
effective_url = effective_url.rstrip("/") + "/v1"
|
effective_url = effective_url.rstrip("/") + "/v1"
|
||||||
if base_url:
|
if base_url:
|
||||||
base_url = effective_url
|
base_url = effective_url
|
||||||
|
|
@ -3124,7 +3124,7 @@ def _model_flow_custom(config):
|
||||||
if len(detected_models) == 1:
|
if len(detected_models) == 1:
|
||||||
print(f" Detected model: {detected_models[0]}")
|
print(f" Detected model: {detected_models[0]}")
|
||||||
confirm = input(" Use this model? [Y/n]: ").strip().lower()
|
confirm = input(" Use this model? [Y/n]: ").strip().lower()
|
||||||
if confirm in ("", "y", "yes"):
|
if confirm in {"", "y", "yes"}:
|
||||||
model_name = detected_models[0]
|
model_name = detected_models[0]
|
||||||
else:
|
else:
|
||||||
model_name = input("Model name (e.g. gpt-4, llama-3-70b): ").strip()
|
model_name = input("Model name (e.g. gpt-4, llama-3-70b): ").strip()
|
||||||
|
|
@ -3957,7 +3957,7 @@ def _model_flow_copilot(config, current_model=""):
|
||||||
api_key = creds.get("api_key", "")
|
api_key = creds.get("api_key", "")
|
||||||
source = creds.get("source", "")
|
source = creds.get("source", "")
|
||||||
else:
|
else:
|
||||||
if source in ("GITHUB_TOKEN", "GH_TOKEN"):
|
if source in {"GITHUB_TOKEN", "GH_TOKEN"}:
|
||||||
print(f" GitHub token: {api_key[:8]}... ✓ ({source})")
|
print(f" GitHub token: {api_key[:8]}... ✓ ({source})")
|
||||||
elif source == "gh auth token":
|
elif source == "gh auth token":
|
||||||
print(" GitHub token: ✓ (from `gh auth token`)")
|
print(" GitHub token: ✓ (from `gh auth token`)")
|
||||||
|
|
@ -5277,7 +5277,7 @@ def cmd_slack(args):
|
||||||
command registered as a first-class slash.
|
command registered as a first-class slash.
|
||||||
"""
|
"""
|
||||||
sub = getattr(args, "slack_command", None)
|
sub = getattr(args, "slack_command", None)
|
||||||
if sub in (None, ""):
|
if sub in {None, ""}:
|
||||||
# No subcommand — print usage hint.
|
# No subcommand — print usage hint.
|
||||||
print(
|
print(
|
||||||
"usage: hermes slack <subcommand>\n"
|
"usage: hermes slack <subcommand>\n"
|
||||||
|
|
@ -5424,7 +5424,7 @@ def _clear_bytecode_cache(root: Path) -> int:
|
||||||
dirnames[:] = [
|
dirnames[:] = [
|
||||||
d
|
d
|
||||||
for d in dirnames
|
for d in dirnames
|
||||||
if d not in ("venv", ".venv", "node_modules", ".git", ".worktrees")
|
if d not in {"venv", ".venv", "node_modules", ".git", ".worktrees"}
|
||||||
]
|
]
|
||||||
if os.path.basename(dirpath) == "__pycache__":
|
if os.path.basename(dirpath) == "__pycache__":
|
||||||
try:
|
try:
|
||||||
|
|
@ -6219,7 +6219,7 @@ def _restore_stashed_changes(
|
||||||
response = input_fn("Restore local changes now? [Y/n]", "y")
|
response = input_fn("Restore local changes now? [Y/n]", "y")
|
||||||
else:
|
else:
|
||||||
response = input().strip().lower()
|
response = input().strip().lower()
|
||||||
if response not in ("", "y", "yes"):
|
if response not in {"", "y", "yes"}:
|
||||||
print("Skipped restoring local changes.")
|
print("Skipped restoring local changes.")
|
||||||
print("Your changes are still preserved in git stash.")
|
print("Your changes are still preserved in git stash.")
|
||||||
print(f"Restore manually with: git stash apply {stash_ref}")
|
print(f"Restore manually with: git stash apply {stash_ref}")
|
||||||
|
|
@ -6462,7 +6462,7 @@ def _sync_with_upstream_if_needed(git_cmd: list[str], cwd: Path) -> None:
|
||||||
print()
|
print()
|
||||||
response = "n"
|
response = "n"
|
||||||
|
|
||||||
if response in ("", "y", "yes"):
|
if response in {"", "y", "yes"}:
|
||||||
print("→ Adding upstream remote...")
|
print("→ Adding upstream remote...")
|
||||||
if _add_upstream_remote(git_cmd, cwd):
|
if _add_upstream_remote(git_cmd, cwd):
|
||||||
print(
|
print(
|
||||||
|
|
@ -7521,7 +7521,7 @@ def _cmd_update_impl(args, gateway_mode: bool):
|
||||||
prompt_user=prompt_for_restore,
|
prompt_user=prompt_for_restore,
|
||||||
input_fn=gw_input_fn,
|
input_fn=gw_input_fn,
|
||||||
)
|
)
|
||||||
if current_branch not in ("main", "HEAD"):
|
if current_branch not in {"main", "HEAD"}:
|
||||||
subprocess.run(
|
subprocess.run(
|
||||||
git_cmd + ["checkout", current_branch],
|
git_cmd + ["checkout", current_branch],
|
||||||
cwd=PROJECT_ROOT,
|
cwd=PROJECT_ROOT,
|
||||||
|
|
@ -7805,7 +7805,7 @@ def _cmd_update_impl(args, gateway_mode: bool):
|
||||||
except EOFError:
|
except EOFError:
|
||||||
response = "n"
|
response = "n"
|
||||||
|
|
||||||
if response in ("", "y", "yes", "auto"):
|
if response in {"", "y", "yes", "auto"}:
|
||||||
print()
|
print()
|
||||||
# Gateway mode, --yes, and non-interactive update contexts
|
# Gateway mode, --yes, and non-interactive update contexts
|
||||||
# (dashboard / web server actions) cannot prompt for API keys.
|
# (dashboard / web server actions) cannot prompt for API keys.
|
||||||
|
|
@ -8866,7 +8866,7 @@ def cmd_profile(args):
|
||||||
answer = input("\nProceed with install? [y/N] ").strip().lower()
|
answer = input("\nProceed with install? [y/N] ").strip().lower()
|
||||||
except (EOFError, KeyboardInterrupt):
|
except (EOFError, KeyboardInterrupt):
|
||||||
answer = ""
|
answer = ""
|
||||||
if answer not in ("y", "yes"):
|
if answer not in {"y", "yes"}:
|
||||||
print("Install cancelled.")
|
print("Install cancelled.")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
@ -8925,7 +8925,7 @@ def cmd_profile(args):
|
||||||
answer = input("\nProceed? [y/N] ").strip().lower()
|
answer = input("\nProceed? [y/N] ").strip().lower()
|
||||||
except (EOFError, KeyboardInterrupt):
|
except (EOFError, KeyboardInterrupt):
|
||||||
answer = ""
|
answer = ""
|
||||||
if answer not in ("y", "yes"):
|
if answer not in {"y", "yes"}:
|
||||||
print("Update cancelled.")
|
print("Update cancelled.")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
@ -10713,9 +10713,9 @@ Examples:
|
||||||
mem_dir = get_hermes_home() / "memories"
|
mem_dir = get_hermes_home() / "memories"
|
||||||
target = getattr(args, "target", "all")
|
target = getattr(args, "target", "all")
|
||||||
files_to_reset = []
|
files_to_reset = []
|
||||||
if target in ("all", "memory"):
|
if target in {"all", "memory"}:
|
||||||
files_to_reset.append(("MEMORY.md", "agent notes"))
|
files_to_reset.append(("MEMORY.md", "agent notes"))
|
||||||
if target in ("all", "user"):
|
if target in {"all", "user"}:
|
||||||
files_to_reset.append(("USER.md", "user profile"))
|
files_to_reset.append(("USER.md", "user profile"))
|
||||||
|
|
||||||
# Check what exists
|
# Check what exists
|
||||||
|
|
@ -10826,7 +10826,7 @@ Examples:
|
||||||
|
|
||||||
def cmd_tools(args):
|
def cmd_tools(args):
|
||||||
action = getattr(args, "tools_action", None)
|
action = getattr(args, "tools_action", None)
|
||||||
if action in ("list", "disable", "enable"):
|
if action in {"list", "disable", "enable"}:
|
||||||
from hermes_cli.tools_config import tools_disable_enable_command
|
from hermes_cli.tools_config import tools_disable_enable_command
|
||||||
|
|
||||||
tools_disable_enable_command(args)
|
tools_disable_enable_command(args)
|
||||||
|
|
@ -11035,7 +11035,7 @@ Examples:
|
||||||
def _confirm_prompt(prompt: str) -> bool:
|
def _confirm_prompt(prompt: str) -> bool:
|
||||||
"""Prompt for y/N confirmation, safe against non-TTY environments."""
|
"""Prompt for y/N confirmation, safe against non-TTY environments."""
|
||||||
try:
|
try:
|
||||||
return input(prompt).strip().lower() in ("y", "yes")
|
return input(prompt).strip().lower() in {"y", "yes"}
|
||||||
except (EOFError, KeyboardInterrupt):
|
except (EOFError, KeyboardInterrupt):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,7 @@ def _confirm(question: str, default: bool = True) -> bool:
|
||||||
return default
|
return default
|
||||||
if not val:
|
if not val:
|
||||||
return default
|
return default
|
||||||
return val in ("y", "yes")
|
return val in {"y", "yes"}
|
||||||
|
|
||||||
|
|
||||||
def _prompt(question: str, *, password: bool = False, default: str = "") -> str:
|
def _prompt(question: str, *, password: bool = False, default: str = "") -> str:
|
||||||
|
|
@ -375,11 +375,11 @@ def cmd_mcp_add(args):
|
||||||
_info("Cancelled.")
|
_info("Cancelled.")
|
||||||
return
|
return
|
||||||
|
|
||||||
if choice in ("n", "no"):
|
if choice in {"n", "no"}:
|
||||||
_info("Cancelled — server not saved.")
|
_info("Cancelled — server not saved.")
|
||||||
return
|
return
|
||||||
|
|
||||||
if choice in ("s", "select"):
|
if choice in {"s", "select"}:
|
||||||
# Interactive tool selection
|
# Interactive tool selection
|
||||||
from hermes_cli.curses_ui import curses_checklist
|
from hermes_cli.curses_ui import curses_checklist
|
||||||
|
|
||||||
|
|
@ -509,7 +509,7 @@ def cmd_mcp_list(args=None):
|
||||||
# Enabled status
|
# Enabled status
|
||||||
enabled = cfg.get("enabled", True)
|
enabled = cfg.get("enabled", True)
|
||||||
if isinstance(enabled, str):
|
if isinstance(enabled, str):
|
||||||
enabled = enabled.lower() in ("true", "1", "yes")
|
enabled = enabled.lower() in {"true", "1", "yes"}
|
||||||
status = color("✓ enabled", Colors.GREEN) if enabled else color("✗ disabled", Colors.DIM)
|
status = color("✓ enabled", Colors.GREEN) if enabled else color("✗ disabled", Colors.DIM)
|
||||||
|
|
||||||
print(f" {name:<16} {transport:<30} {tools_str:<12} {status}")
|
print(f" {name:<16} {transport:<30} {tools_str:<12} {status}")
|
||||||
|
|
|
||||||
|
|
@ -825,7 +825,7 @@ def switch_model(
|
||||||
|
|
||||||
# --- Step e: detect_provider_for_model() as last resort ---
|
# --- Step e: detect_provider_for_model() as last resort ---
|
||||||
_base = current_base_url or ""
|
_base = current_base_url or ""
|
||||||
is_custom = current_provider in ("custom", "local") or (
|
is_custom = current_provider in {"custom", "local"} or (
|
||||||
"localhost" in _base or "127.0.0.1" in _base
|
"localhost" in _base or "127.0.0.1" in _base
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -1525,7 +1525,7 @@ def list_authenticated_providers(
|
||||||
api_key = os.environ.get(key_env, "").strip() if key_env else ""
|
api_key = os.environ.get(key_env, "").strip() if key_env else ""
|
||||||
discover = ep_cfg.get("discover_models", True)
|
discover = ep_cfg.get("discover_models", True)
|
||||||
if isinstance(discover, str):
|
if isinstance(discover, str):
|
||||||
discover = discover.lower() not in ("false", "no", "0")
|
discover = discover.lower() not in {"false", "no", "0"}
|
||||||
if api_url and api_key and discover:
|
if api_url and api_key and discover:
|
||||||
try:
|
try:
|
||||||
from hermes_cli.models import fetch_api_models
|
from hermes_cli.models import fetch_api_models
|
||||||
|
|
|
||||||
|
|
@ -818,7 +818,7 @@ try:
|
||||||
for _pp in _list_providers_for_canonical():
|
for _pp in _list_providers_for_canonical():
|
||||||
if _pp.name in _canonical_slugs:
|
if _pp.name in _canonical_slugs:
|
||||||
continue
|
continue
|
||||||
if _pp.auth_type in ("oauth_device_code", "oauth_external", "external_process", "aws_sdk", "copilot"):
|
if _pp.auth_type in {"oauth_device_code", "oauth_external", "external_process", "aws_sdk", "copilot"}:
|
||||||
continue # non-api-key flows need bespoke picker UX; skip auto-inject
|
continue # non-api-key flows need bespoke picker UX; skip auto-inject
|
||||||
_label = _pp.display_name or _pp.name
|
_label = _pp.display_name or _pp.name
|
||||||
_desc = _pp.description or f"{_label} (direct API)"
|
_desc = _pp.description or f"{_label} (direct API)"
|
||||||
|
|
@ -2335,7 +2335,7 @@ def _lmstudio_fetch_raw_models(
|
||||||
with urllib.request.urlopen(request, timeout=timeout) as resp:
|
with urllib.request.urlopen(request, timeout=timeout) as resp:
|
||||||
payload = json.loads(resp.read().decode())
|
payload = json.loads(resp.read().decode())
|
||||||
except urllib.error.HTTPError as exc:
|
except urllib.error.HTTPError as exc:
|
||||||
if exc.code in (401, 403):
|
if exc.code in {401, 403}:
|
||||||
from hermes_cli.auth import AuthError
|
from hermes_cli.auth import AuthError
|
||||||
raise AuthError(
|
raise AuthError(
|
||||||
f"LM Studio rejected the request with HTTP {exc.code}.",
|
f"LM Studio rejected the request with HTTP {exc.code}.",
|
||||||
|
|
@ -3270,7 +3270,7 @@ def validate_requested_model(
|
||||||
|
|
||||||
# MiniMax providers don't expose a /models endpoint — validate against
|
# MiniMax providers don't expose a /models endpoint — validate against
|
||||||
# the static catalog instead, similar to openai-codex.
|
# the static catalog instead, similar to openai-codex.
|
||||||
if normalized in ("minimax", "minimax-cn"):
|
if normalized in {"minimax", "minimax-cn"}:
|
||||||
try:
|
try:
|
||||||
catalog_models = provider_model_ids(normalized)
|
catalog_models = provider_model_ids(normalized)
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|
|
||||||
|
|
@ -86,9 +86,9 @@ logger = logging.getLogger(__name__)
|
||||||
# The env var is read once at import time; tests that need to flip it
|
# The env var is read once at import time; tests that need to flip it
|
||||||
# mid-process can call ``_install_plugin_debug_handler(force=True)``.
|
# mid-process can call ``_install_plugin_debug_handler(force=True)``.
|
||||||
|
|
||||||
_PLUGINS_DEBUG = os.getenv("HERMES_PLUGINS_DEBUG", "").strip().lower() in (
|
_PLUGINS_DEBUG = os.getenv("HERMES_PLUGINS_DEBUG", "").strip().lower() in {
|
||||||
"1", "true", "yes", "on",
|
"1", "true", "yes", "on",
|
||||||
)
|
}
|
||||||
_DEBUG_HANDLER_INSTALLED = False
|
_DEBUG_HANDLER_INSTALLED = False
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -100,9 +100,9 @@ def _install_plugin_debug_handler(force: bool = False) -> None:
|
||||||
"""
|
"""
|
||||||
global _DEBUG_HANDLER_INSTALLED, _PLUGINS_DEBUG
|
global _DEBUG_HANDLER_INSTALLED, _PLUGINS_DEBUG
|
||||||
if force:
|
if force:
|
||||||
_PLUGINS_DEBUG = os.getenv("HERMES_PLUGINS_DEBUG", "").strip().lower() in (
|
_PLUGINS_DEBUG = os.getenv("HERMES_PLUGINS_DEBUG", "").strip().lower() in {
|
||||||
"1", "true", "yes", "on",
|
"1", "true", "yes", "on",
|
||||||
)
|
}
|
||||||
if not _PLUGINS_DEBUG or _DEBUG_HANDLER_INSTALLED:
|
if not _PLUGINS_DEBUG or _DEBUG_HANDLER_INSTALLED:
|
||||||
return
|
return
|
||||||
handler = logging.StreamHandler(sys.stderr)
|
handler = logging.StreamHandler(sys.stderr)
|
||||||
|
|
@ -824,7 +824,7 @@ class PluginManager:
|
||||||
# Bundled platform plugins (gateway adapters like IRC) auto-load
|
# Bundled platform plugins (gateway adapters like IRC) auto-load
|
||||||
# for the same reason: every platform Hermes ships must be
|
# for the same reason: every platform Hermes ships must be
|
||||||
# available out of the box without the user having to opt in.
|
# available out of the box without the user having to opt in.
|
||||||
if manifest.source == "bundled" and manifest.kind in ("backend", "platform"):
|
if manifest.source == "bundled" and manifest.kind in {"backend", "platform"}:
|
||||||
self._load_plugin(manifest)
|
self._load_plugin(manifest)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|
@ -1075,7 +1075,7 @@ class PluginManager:
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if manifest.source in ("user", "project", "bundled"):
|
if manifest.source in {"user", "project", "bundled"}:
|
||||||
module = self._load_directory_module(manifest)
|
module = self._load_directory_module(manifest)
|
||||||
else:
|
else:
|
||||||
module = self._load_entrypoint_module(manifest)
|
module = self._load_entrypoint_module(manifest)
|
||||||
|
|
|
||||||
|
|
@ -85,7 +85,7 @@ def _sanitize_plugin_name(name: str, plugins_dir: Path) -> Path:
|
||||||
if not name:
|
if not name:
|
||||||
raise ValueError("Plugin name must not be empty.")
|
raise ValueError("Plugin name must not be empty.")
|
||||||
|
|
||||||
if name in (".", ".."):
|
if name in {".", ".."}:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Invalid plugin name '{name}': must not reference the plugins directory itself."
|
f"Invalid plugin name '{name}': must not reference the plugins directory itself."
|
||||||
)
|
)
|
||||||
|
|
@ -491,7 +491,7 @@ def cmd_install(
|
||||||
answer = input(
|
answer = input(
|
||||||
f" Enable '{installed_name}' now? [y/N]: ",
|
f" Enable '{installed_name}' now? [y/N]: ",
|
||||||
).strip().lower()
|
).strip().lower()
|
||||||
should_enable = answer in ("y", "yes")
|
should_enable = answer in {"y", "yes"}
|
||||||
except (EOFError, KeyboardInterrupt):
|
except (EOFError, KeyboardInterrupt):
|
||||||
should_enable = False
|
should_enable = False
|
||||||
else:
|
else:
|
||||||
|
|
@ -731,7 +731,7 @@ def _discover_all_plugins() -> list:
|
||||||
for d in sorted(base.iterdir()):
|
for d in sorted(base.iterdir()):
|
||||||
if not d.is_dir():
|
if not d.is_dir():
|
||||||
continue
|
continue
|
||||||
if source == "bundled" and d.name in ("memory", "context_engine"):
|
if source == "bundled" and d.name in {"memory", "context_engine"}:
|
||||||
continue
|
continue
|
||||||
manifest_file = d / "plugin.yaml"
|
manifest_file = d / "plugin.yaml"
|
||||||
if not manifest_file.exists():
|
if not manifest_file.exists():
|
||||||
|
|
@ -1129,10 +1129,10 @@ def _run_composite_ui(curses, plugin_names, plugin_labels, plugin_selected,
|
||||||
stdscr.refresh()
|
stdscr.refresh()
|
||||||
key = stdscr.getch()
|
key = stdscr.getch()
|
||||||
|
|
||||||
if key in (curses.KEY_UP, ord("k")):
|
if key in {curses.KEY_UP, ord("k")}:
|
||||||
if total_items > 0:
|
if total_items > 0:
|
||||||
cursor = (cursor - 1) % total_items
|
cursor = (cursor - 1) % total_items
|
||||||
elif key in (curses.KEY_DOWN, ord("j")):
|
elif key in {curses.KEY_DOWN, ord("j")}:
|
||||||
if total_items > 0:
|
if total_items > 0:
|
||||||
cursor = (cursor + 1) % total_items
|
cursor = (cursor + 1) % total_items
|
||||||
elif key == ord(" "):
|
elif key == ord(" "):
|
||||||
|
|
@ -1168,7 +1168,7 @@ def _run_composite_ui(curses, plugin_names, plugin_labels, plugin_selected,
|
||||||
curses.init_pair(3, curses.COLOR_CYAN, -1)
|
curses.init_pair(3, curses.COLOR_CYAN, -1)
|
||||||
curses.init_pair(4, 8, -1)
|
curses.init_pair(4, 8, -1)
|
||||||
curses.curs_set(0)
|
curses.curs_set(0)
|
||||||
elif key in (curses.KEY_ENTER, 10, 13):
|
elif key in {curses.KEY_ENTER, 10, 13}:
|
||||||
if cursor < n_plugins:
|
if cursor < n_plugins:
|
||||||
# ENTER on a plugin checkbox — confirm and exit
|
# ENTER on a plugin checkbox — confirm and exit
|
||||||
result_holder["plugins_changed"] = True
|
result_holder["plugins_changed"] = True
|
||||||
|
|
@ -1200,7 +1200,7 @@ def _run_composite_ui(curses, plugin_names, plugin_labels, plugin_selected,
|
||||||
curses.init_pair(3, curses.COLOR_CYAN, -1)
|
curses.init_pair(3, curses.COLOR_CYAN, -1)
|
||||||
curses.init_pair(4, 8, -1)
|
curses.init_pair(4, 8, -1)
|
||||||
curses.curs_set(0)
|
curses.curs_set(0)
|
||||||
elif key in (27, ord("q")):
|
elif key in {27, ord("q")}:
|
||||||
# Save plugin changes on exit
|
# Save plugin changes on exit
|
||||||
result_holder["plugins_changed"] = True
|
result_holder["plugins_changed"] = True
|
||||||
return
|
return
|
||||||
|
|
@ -1569,13 +1569,13 @@ def plugins_command(args) -> None:
|
||||||
)
|
)
|
||||||
elif action == "update":
|
elif action == "update":
|
||||||
cmd_update(args.name)
|
cmd_update(args.name)
|
||||||
elif action in ("remove", "rm", "uninstall"):
|
elif action in {"remove", "rm", "uninstall"}:
|
||||||
cmd_remove(args.name)
|
cmd_remove(args.name)
|
||||||
elif action == "enable":
|
elif action == "enable":
|
||||||
cmd_enable(args.name)
|
cmd_enable(args.name)
|
||||||
elif action == "disable":
|
elif action == "disable":
|
||||||
cmd_disable(args.name)
|
cmd_disable(args.name)
|
||||||
elif action in ("list", "ls"):
|
elif action in {"list", "ls"}:
|
||||||
cmd_list()
|
cmd_list()
|
||||||
elif action is None:
|
elif action is None:
|
||||||
cmd_toggle()
|
cmd_toggle()
|
||||||
|
|
|
||||||
|
|
@ -989,7 +989,7 @@ def _default_export_ignore(root_dir: Path):
|
||||||
if entry == "__pycache__" or entry.endswith((".sock", ".tmp")):
|
if entry == "__pycache__" or entry.endswith((".sock", ".tmp")):
|
||||||
ignored.add(entry)
|
ignored.add(entry)
|
||||||
# npm lockfiles can appear at root
|
# npm lockfiles can appear at root
|
||||||
elif entry in ("package.json", "package-lock.json"):
|
elif entry in {"package.json", "package-lock.json"}:
|
||||||
ignored.add(entry)
|
ignored.add(entry)
|
||||||
# Root-level exclusions
|
# Root-level exclusions
|
||||||
if Path(directory) == root_dir:
|
if Path(directory) == root_dir:
|
||||||
|
|
@ -1057,7 +1057,7 @@ def _normalize_profile_archive_parts(member_name: str) -> List[str]:
|
||||||
):
|
):
|
||||||
raise ValueError(f"Unsafe archive member path: {member_name}")
|
raise ValueError(f"Unsafe archive member path: {member_name}")
|
||||||
|
|
||||||
parts = [part for part in posix_path.parts if part not in ("", ".")]
|
parts = [part for part in posix_path.parts if part not in {"", "."}]
|
||||||
if not parts or any(part == ".." for part in parts):
|
if not parts or any(part == ".." for part in parts):
|
||||||
raise ValueError(f"Unsafe archive member path: {member_name}")
|
raise ValueError(f"Unsafe archive member path: {member_name}")
|
||||||
return parts
|
return parts
|
||||||
|
|
|
||||||
|
|
@ -164,7 +164,7 @@ class PtyBridge:
|
||||||
data = os.read(self._fd, 65536)
|
data = os.read(self._fd, 65536)
|
||||||
except OSError as exc:
|
except OSError as exc:
|
||||||
# EIO on Linux = slave side closed. EBADF = already closed.
|
# EIO on Linux = slave side closed. EBADF = already closed.
|
||||||
if exc.errno in (errno.EIO, errno.EBADF):
|
if exc.errno in {errno.EIO, errno.EBADF}:
|
||||||
return None
|
return None
|
||||||
raise
|
raise
|
||||||
if not data:
|
if not data:
|
||||||
|
|
@ -181,7 +181,7 @@ class PtyBridge:
|
||||||
try:
|
try:
|
||||||
n = os.write(self._fd, view)
|
n = os.write(self._fd, view)
|
||||||
except OSError as exc:
|
except OSError as exc:
|
||||||
if exc.errno in (errno.EIO, errno.EBADF, errno.EPIPE):
|
if exc.errno in {errno.EIO, errno.EBADF, errno.EPIPE}:
|
||||||
return
|
return
|
||||||
raise
|
raise
|
||||||
if n <= 0:
|
if n <= 0:
|
||||||
|
|
|
||||||
|
|
@ -260,7 +260,7 @@ def _resolve_runtime_from_pool_entry(
|
||||||
if cfg_base_url:
|
if cfg_base_url:
|
||||||
base_url = cfg_base_url
|
base_url = cfg_base_url
|
||||||
configured_mode = _parse_api_mode(model_cfg.get("api_mode"))
|
configured_mode = _parse_api_mode(model_cfg.get("api_mode"))
|
||||||
if provider in ("opencode-zen", "opencode-go"):
|
if provider in {"opencode-zen", "opencode-go"}:
|
||||||
# Re-derive api_mode from the effective model rather than the
|
# Re-derive api_mode from the effective model rather than the
|
||||||
# persisted api_mode: the opencode providers serve both
|
# persisted api_mode: the opencode providers serve both
|
||||||
# anthropic_messages and chat_completions models, so the previous
|
# anthropic_messages and chat_completions models, so the previous
|
||||||
|
|
@ -282,7 +282,7 @@ def _resolve_runtime_from_pool_entry(
|
||||||
# Anthropic SDK prepends its own /v1/messages to the base_url. Strip the
|
# Anthropic SDK prepends its own /v1/messages to the base_url. Strip the
|
||||||
# trailing /v1 so the SDK constructs the correct path (e.g.
|
# trailing /v1 so the SDK constructs the correct path (e.g.
|
||||||
# https://opencode.ai/zen/go/v1/messages instead of .../v1/v1/messages).
|
# https://opencode.ai/zen/go/v1/messages instead of .../v1/v1/messages).
|
||||||
if api_mode == "anthropic_messages" and provider in ("opencode-zen", "opencode-go"):
|
if api_mode == "anthropic_messages" and provider in {"opencode-zen", "opencode-go"}:
|
||||||
base_url = re.sub(r"/v1/?$", "", base_url)
|
base_url = re.sub(r"/v1/?$", "", base_url)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -859,7 +859,7 @@ def _resolve_explicit_runtime(
|
||||||
|
|
||||||
base_url = explicit_base_url
|
base_url = explicit_base_url
|
||||||
if not base_url:
|
if not base_url:
|
||||||
if provider in ("kimi-coding", "kimi-coding-cn"):
|
if provider in {"kimi-coding", "kimi-coding-cn"}:
|
||||||
creds = resolve_api_key_provider_credentials(provider)
|
creds = resolve_api_key_provider_credentials(provider)
|
||||||
base_url = creds.get("base_url", "").rstrip("/")
|
base_url = creds.get("base_url", "").rstrip("/")
|
||||||
else:
|
else:
|
||||||
|
|
@ -1223,7 +1223,7 @@ def resolve_runtime_provider(
|
||||||
# trust boto3's credential chain — it handles IMDS, ECS task roles,
|
# trust boto3's credential chain — it handles IMDS, ECS task roles,
|
||||||
# Lambda execution roles, SSO, and other implicit sources that our
|
# Lambda execution roles, SSO, and other implicit sources that our
|
||||||
# env-var check can't detect.
|
# env-var check can't detect.
|
||||||
is_explicit = requested_provider in ("bedrock", "aws", "aws-bedrock", "amazon-bedrock", "amazon")
|
is_explicit = requested_provider in {"bedrock", "aws", "aws-bedrock", "amazon-bedrock", "amazon"}
|
||||||
if not is_explicit and not has_aws_credentials():
|
if not is_explicit and not has_aws_credentials():
|
||||||
raise AuthError(
|
raise AuthError(
|
||||||
"No AWS credentials found for Bedrock. Configure one of:\n"
|
"No AWS credentials found for Bedrock. Configure one of:\n"
|
||||||
|
|
@ -1303,7 +1303,7 @@ def resolve_runtime_provider(
|
||||||
configured_provider = str(model_cfg.get("provider") or "").strip().lower()
|
configured_provider = str(model_cfg.get("provider") or "").strip().lower()
|
||||||
# Only honor persisted api_mode when it belongs to the same provider family.
|
# Only honor persisted api_mode when it belongs to the same provider family.
|
||||||
configured_mode = _parse_api_mode(model_cfg.get("api_mode"))
|
configured_mode = _parse_api_mode(model_cfg.get("api_mode"))
|
||||||
if provider in ("opencode-zen", "opencode-go"):
|
if provider in {"opencode-zen", "opencode-go"}:
|
||||||
# opencode-zen/go must always re-derive api_mode from the
|
# opencode-zen/go must always re-derive api_mode from the
|
||||||
# target model (not the stale persisted api_mode), because
|
# target model (not the stale persisted api_mode), because
|
||||||
# the same provider serves both anthropic_messages
|
# the same provider serves both anthropic_messages
|
||||||
|
|
@ -1325,7 +1325,7 @@ def resolve_runtime_provider(
|
||||||
if detected:
|
if detected:
|
||||||
api_mode = detected
|
api_mode = detected
|
||||||
# Strip trailing /v1 for OpenCode Anthropic models (see comment above).
|
# Strip trailing /v1 for OpenCode Anthropic models (see comment above).
|
||||||
if api_mode == "anthropic_messages" and provider in ("opencode-zen", "opencode-go"):
|
if api_mode == "anthropic_messages" and provider in {"opencode-zen", "opencode-go"}:
|
||||||
base_url = re.sub(r"/v1/?$", "", base_url)
|
base_url = re.sub(r"/v1/?$", "", base_url)
|
||||||
return {
|
return {
|
||||||
"provider": provider,
|
"provider": provider,
|
||||||
|
|
|
||||||
|
|
@ -292,9 +292,9 @@ def prompt_yes_no(question: str, default: bool = True) -> bool:
|
||||||
|
|
||||||
if not value:
|
if not value:
|
||||||
return default
|
return default
|
||||||
if value in ("y", "yes"):
|
if value in {"y", "yes"}:
|
||||||
return True
|
return True
|
||||||
if value in ("n", "no"):
|
if value in {"n", "no"}:
|
||||||
return False
|
return False
|
||||||
print_error("Please enter 'y' or 'n'")
|
print_error("Please enter 'y' or 'n'")
|
||||||
|
|
||||||
|
|
@ -641,7 +641,7 @@ def _prompt_container_resources(config: dict):
|
||||||
persist_str = prompt(
|
persist_str = prompt(
|
||||||
" Persist filesystem across sessions? (yes/no)", persist_label
|
" Persist filesystem across sessions? (yes/no)", persist_label
|
||||||
)
|
)
|
||||||
terminal["container_persistent"] = persist_str.lower() in ("yes", "true", "y", "1")
|
terminal["container_persistent"] = persist_str.lower() in {"yes", "true", "y", "1"}
|
||||||
|
|
||||||
# CPU
|
# CPU
|
||||||
current_cpu = terminal.get("container_cpu", 1)
|
current_cpu = terminal.get("container_cpu", 1)
|
||||||
|
|
@ -692,7 +692,7 @@ def _prompt_vercel_sandbox_settings(config: dict):
|
||||||
persist_label = "yes" if current_persist else "no"
|
persist_label = "yes" if current_persist else "no"
|
||||||
terminal["container_persistent"] = prompt(
|
terminal["container_persistent"] = prompt(
|
||||||
" Persist filesystem with snapshots? (yes/no)", persist_label
|
" Persist filesystem with snapshots? (yes/no)", persist_label
|
||||||
).lower() in ("yes", "true", "y", "1")
|
).lower() in {"yes", "true", "y", "1"}
|
||||||
|
|
||||||
current_cpu = terminal.get("container_cpu", 1)
|
current_cpu = terminal.get("container_cpu", 1)
|
||||||
cpu_str = prompt(" CPU cores", str(current_cpu))
|
cpu_str = prompt(" CPU cores", str(current_cpu))
|
||||||
|
|
@ -708,7 +708,7 @@ def _prompt_vercel_sandbox_settings(config: dict):
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if terminal.get("container_disk", 51200) not in (0, 51200):
|
if terminal.get("container_disk", 51200) not in {0, 51200}:
|
||||||
print_warning("Vercel Sandbox does not support custom disk sizing; resetting container_disk to 51200.")
|
print_warning("Vercel Sandbox does not support custom disk sizing; resetting container_disk to 51200.")
|
||||||
terminal["container_disk"] = 51200
|
terminal["container_disk"] = 51200
|
||||||
|
|
||||||
|
|
@ -1729,7 +1729,7 @@ def setup_agent_settings(config: dict):
|
||||||
|
|
||||||
current_mode = cfg_get(config, "display", "tool_progress", default="all")
|
current_mode = cfg_get(config, "display", "tool_progress", default="all")
|
||||||
mode = prompt("Tool progress mode", current_mode)
|
mode = prompt("Tool progress mode", current_mode)
|
||||||
if mode.lower() in ("off", "new", "all", "verbose"):
|
if mode.lower() in {"off", "new", "all", "verbose"}:
|
||||||
if "display" not in config:
|
if "display" not in config:
|
||||||
config["display"] = {}
|
config["display"] = {}
|
||||||
config["display"]["tool_progress"] = mode.lower()
|
config["display"]["tool_progress"] = mode.lower()
|
||||||
|
|
|
||||||
|
|
@ -593,7 +593,7 @@ def do_install(identifier: str, category: str = "", force: bool = False,
|
||||||
answer = input("Confirm [y/N]: ").strip().lower()
|
answer = input("Confirm [y/N]: ").strip().lower()
|
||||||
except (EOFError, KeyboardInterrupt):
|
except (EOFError, KeyboardInterrupt):
|
||||||
answer = "n"
|
answer = "n"
|
||||||
if answer not in ("y", "yes"):
|
if answer not in {"y", "yes"}:
|
||||||
c.print("[dim]Installation cancelled.[/]\n")
|
c.print("[dim]Installation cancelled.[/]\n")
|
||||||
shutil.rmtree(q_path, ignore_errors=True)
|
shutil.rmtree(q_path, ignore_errors=True)
|
||||||
return
|
return
|
||||||
|
|
@ -948,7 +948,7 @@ def do_uninstall(name: str, console: Optional[Console] = None,
|
||||||
answer = input("Confirm [y/N]: ").strip().lower()
|
answer = input("Confirm [y/N]: ").strip().lower()
|
||||||
except (EOFError, KeyboardInterrupt):
|
except (EOFError, KeyboardInterrupt):
|
||||||
answer = "n"
|
answer = "n"
|
||||||
if answer not in ("y", "yes"):
|
if answer not in {"y", "yes"}:
|
||||||
c.print("[dim]Cancelled.[/]\n")
|
c.print("[dim]Cancelled.[/]\n")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
@ -984,7 +984,7 @@ def do_reset(name: str, restore: bool = False,
|
||||||
answer = input("Confirm [y/N]: ").strip().lower()
|
answer = input("Confirm [y/N]: ").strip().lower()
|
||||||
except (EOFError, KeyboardInterrupt):
|
except (EOFError, KeyboardInterrupt):
|
||||||
answer = "n"
|
answer = "n"
|
||||||
if answer not in ("y", "yes"):
|
if answer not in {"y", "yes"}:
|
||||||
c.print("[dim]Cancelled.[/]\n")
|
c.print("[dim]Cancelled.[/]\n")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
@ -1138,7 +1138,7 @@ def _github_publish(skill_path: Path, skill_name: str, target_repo: str,
|
||||||
f"https://api.github.com/repos/{target_repo}/forks",
|
f"https://api.github.com/repos/{target_repo}/forks",
|
||||||
headers=headers, timeout=30,
|
headers=headers, timeout=30,
|
||||||
)
|
)
|
||||||
if resp.status_code in (200, 202):
|
if resp.status_code in {200, 202}:
|
||||||
fork = resp.json()
|
fork = resp.json()
|
||||||
fork_repo = fork["full_name"]
|
fork_repo = fork["full_name"]
|
||||||
elif resp.status_code == 403:
|
elif resp.status_code == 403:
|
||||||
|
|
@ -1564,7 +1564,7 @@ def handle_skills_slash(cmd: str, console: Optional[Console] = None) -> None:
|
||||||
repo = args[1] if len(args) > 1 else ""
|
repo = args[1] if len(args) > 1 else ""
|
||||||
do_tap(tap_action, repo=repo, console=c)
|
do_tap(tap_action, repo=repo, console=c)
|
||||||
|
|
||||||
elif action in ("help", "--help", "-h"):
|
elif action in {"help", "--help", "-h"}:
|
||||||
_print_skills_help(c)
|
_print_skills_help(c)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
|
|
||||||
|
|
@ -367,7 +367,7 @@ def show_status(args):
|
||||||
if persist is None:
|
if persist is None:
|
||||||
persist_enabled = bool(terminal_cfg.get("container_persistent", True))
|
persist_enabled = bool(terminal_cfg.get("container_persistent", True))
|
||||||
else:
|
else:
|
||||||
persist_enabled = persist.lower() in ("1", "true", "yes", "on")
|
persist_enabled = persist.lower() in {"1", "true", "yes", "on"}
|
||||||
auth_status = describe_vercel_auth()
|
auth_status = describe_vercel_auth()
|
||||||
sdk_ok = importlib.util.find_spec("vercel") is not None
|
sdk_ok = importlib.util.find_spec("vercel") is not None
|
||||||
sdk_label = "installed" if sdk_ok else "missing (install: pip install 'hermes-agent[vercel]')"
|
sdk_label = "installed" if sdk_ok else "missing (install: pip install 'hermes-agent[vercel]')"
|
||||||
|
|
|
||||||
|
|
@ -105,7 +105,7 @@ def configure_windows_stdio() -> bool:
|
||||||
_CONFIGURED = True
|
_CONFIGURED = True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if os.environ.get("HERMES_DISABLE_WINDOWS_UTF8") in ("1", "true", "True", "yes"):
|
if os.environ.get("HERMES_DISABLE_WINDOWS_UTF8") in {"1", "true", "True", "yes"}:
|
||||||
_CONFIGURED = True
|
_CONFIGURED = True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -594,7 +594,7 @@ def _pip_install(
|
||||||
def _run_post_setup(post_setup_key: str):
|
def _run_post_setup(post_setup_key: str):
|
||||||
"""Run post-setup hooks for tools that need extra installation steps."""
|
"""Run post-setup hooks for tools that need extra installation steps."""
|
||||||
import shutil
|
import shutil
|
||||||
if post_setup_key in ("agent_browser", "browserbase"):
|
if post_setup_key in {"agent_browser", "browserbase"}:
|
||||||
node_modules = PROJECT_ROOT / "node_modules" / "agent-browser"
|
node_modules = PROJECT_ROOT / "node_modules" / "agent-browser"
|
||||||
npm_bin = shutil.which("npm")
|
npm_bin = shutil.which("npm")
|
||||||
npx_bin = shutil.which("npx")
|
npx_bin = shutil.which("npx")
|
||||||
|
|
@ -1631,7 +1631,7 @@ def _is_provider_active(provider: dict, config: dict) -> bool:
|
||||||
image_cfg = config.get("image_gen", {})
|
image_cfg = config.get("image_gen", {})
|
||||||
if isinstance(image_cfg, dict):
|
if isinstance(image_cfg, dict):
|
||||||
configured_provider = image_cfg.get("provider")
|
configured_provider = image_cfg.get("provider")
|
||||||
if configured_provider not in (None, "", "fal"):
|
if configured_provider not in {None, "", "fal"}:
|
||||||
return False
|
return False
|
||||||
if image_cfg.get("use_gateway") is not None and not is_truthy_value(image_cfg.get("use_gateway"), default=False):
|
if image_cfg.get("use_gateway") is not None and not is_truthy_value(image_cfg.get("use_gateway"), default=False):
|
||||||
return False
|
return False
|
||||||
|
|
@ -1664,7 +1664,7 @@ def _is_provider_active(provider: dict, config: dict) -> bool:
|
||||||
configured_provider = image_cfg.get("provider")
|
configured_provider = image_cfg.get("provider")
|
||||||
return (
|
return (
|
||||||
provider["imagegen_backend"] == "fal"
|
provider["imagegen_backend"] == "fal"
|
||||||
and configured_provider in (None, "", "fal")
|
and configured_provider in {None, "", "fal"}
|
||||||
and not is_truthy_value(image_cfg.get("use_gateway"), default=False)
|
and not is_truthy_value(image_cfg.get("use_gateway"), default=False)
|
||||||
)
|
)
|
||||||
return False
|
return False
|
||||||
|
|
@ -1914,7 +1914,7 @@ def _configure_provider(provider: dict, config: dict):
|
||||||
|
|
||||||
# For tools without a specific config key (e.g. image_gen), still
|
# For tools without a specific config key (e.g. image_gen), still
|
||||||
# track use_gateway so the runtime knows the user's intent.
|
# track use_gateway so the runtime knows the user's intent.
|
||||||
if managed_feature and managed_feature not in ("web", "tts", "browser"):
|
if managed_feature and managed_feature not in {"web", "tts", "browser"}:
|
||||||
config.setdefault(managed_feature, {})["use_gateway"] = True
|
config.setdefault(managed_feature, {})["use_gateway"] = True
|
||||||
elif not managed_feature:
|
elif not managed_feature:
|
||||||
# User picked a non-gateway provider — find which category this
|
# User picked a non-gateway provider — find which category this
|
||||||
|
|
@ -1946,7 +1946,7 @@ def _configure_provider(provider: dict, config: dict):
|
||||||
# image_gen.provider clear so the dispatch shim falls through
|
# image_gen.provider clear so the dispatch shim falls through
|
||||||
# to the legacy FAL path.
|
# to the legacy FAL path.
|
||||||
img_cfg = config.setdefault("image_gen", {})
|
img_cfg = config.setdefault("image_gen", {})
|
||||||
if isinstance(img_cfg, dict) and img_cfg.get("provider") not in (None, "", "fal"):
|
if isinstance(img_cfg, dict) and img_cfg.get("provider") not in {None, "", "fal"}:
|
||||||
img_cfg["provider"] = "fal"
|
img_cfg["provider"] = "fal"
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
@ -1991,7 +1991,7 @@ def _configure_provider(provider: dict, config: dict):
|
||||||
if backend:
|
if backend:
|
||||||
_configure_imagegen_model(backend, config)
|
_configure_imagegen_model(backend, config)
|
||||||
img_cfg = config.setdefault("image_gen", {})
|
img_cfg = config.setdefault("image_gen", {})
|
||||||
if isinstance(img_cfg, dict) and img_cfg.get("provider") not in (None, "", "fal"):
|
if isinstance(img_cfg, dict) and img_cfg.get("provider") not in {None, "", "fal"}:
|
||||||
img_cfg["provider"] = "fal"
|
img_cfg["provider"] = "fal"
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -2186,7 +2186,7 @@ def _reconfigure_provider(provider: dict, config: dict):
|
||||||
web_cfg["use_gateway"] = bool(managed_feature)
|
web_cfg["use_gateway"] = bool(managed_feature)
|
||||||
_print_success(f" Web backend set to: {provider['web_backend']}")
|
_print_success(f" Web backend set to: {provider['web_backend']}")
|
||||||
|
|
||||||
if managed_feature and managed_feature not in ("web", "tts", "browser"):
|
if managed_feature and managed_feature not in {"web", "tts", "browser"}:
|
||||||
section = config.setdefault(managed_feature, {})
|
section = config.setdefault(managed_feature, {})
|
||||||
if not isinstance(section, dict):
|
if not isinstance(section, dict):
|
||||||
section = {}
|
section = {}
|
||||||
|
|
@ -2535,7 +2535,7 @@ def _configure_mcp_tools_interactive(config: dict):
|
||||||
# Count enabled servers
|
# Count enabled servers
|
||||||
enabled_names = [
|
enabled_names = [
|
||||||
k for k, v in mcp_servers.items()
|
k for k, v in mcp_servers.items()
|
||||||
if v.get("enabled", True) not in (False, "false", "0", "no", "off")
|
if v.get("enabled", True) not in {False, "false", "0", "no", "off"}
|
||||||
]
|
]
|
||||||
if not enabled_names:
|
if not enabled_names:
|
||||||
_print_info("All MCP servers are disabled.")
|
_print_info("All MCP servers are disabled.")
|
||||||
|
|
|
||||||
|
|
@ -490,7 +490,7 @@ def run_uninstall(args):
|
||||||
print("Cancelled.")
|
print("Cancelled.")
|
||||||
return
|
return
|
||||||
|
|
||||||
if choice == "3" or choice.lower() in ("c", "cancel", "q", "quit", "n", "no"):
|
if choice == "3" or choice.lower() in {"c", "cancel", "q", "quit", "n", "no"}:
|
||||||
print()
|
print()
|
||||||
print("Uninstall cancelled.")
|
print("Uninstall cancelled.")
|
||||||
return
|
return
|
||||||
|
|
@ -517,7 +517,7 @@ def run_uninstall(args):
|
||||||
print()
|
print()
|
||||||
print("Cancelled.")
|
print("Cancelled.")
|
||||||
return
|
return
|
||||||
remove_profiles = resp in ("y", "yes")
|
remove_profiles = resp in {"y", "yes"}
|
||||||
|
|
||||||
# Final confirmation
|
# Final confirmation
|
||||||
print()
|
print()
|
||||||
|
|
|
||||||
|
|
@ -179,7 +179,7 @@ def _is_accepted_host(host_header: str, bound_host: str) -> bool:
|
||||||
# 0.0.0.0 bind means operator explicitly opted into all-interfaces
|
# 0.0.0.0 bind means operator explicitly opted into all-interfaces
|
||||||
# (requires --insecure per web_server.start_server). No Host-layer
|
# (requires --insecure per web_server.start_server). No Host-layer
|
||||||
# defence can protect that mode; rely on operator network controls.
|
# defence can protect that mode; rely on operator network controls.
|
||||||
if bound_host in ("0.0.0.0", "::"):
|
if bound_host in {"0.0.0.0", "::"}:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# Loopback bind: accept the loopback names
|
# Loopback bind: accept the loopback names
|
||||||
|
|
@ -385,7 +385,7 @@ def _build_schema_from_config(
|
||||||
full_key = f"{prefix}.{key}" if prefix else key
|
full_key = f"{prefix}.{key}" if prefix else key
|
||||||
|
|
||||||
# Skip internal / version keys
|
# Skip internal / version keys
|
||||||
if full_key in ("_config_version",):
|
if full_key in {"_config_version",}:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Category is the first path component for nested keys, or "general"
|
# Category is the first path component for nested keys, or "general"
|
||||||
|
|
@ -576,13 +576,13 @@ async def get_status():
|
||||||
gateway_exit_reason = runtime.get("exit_reason")
|
gateway_exit_reason = runtime.get("exit_reason")
|
||||||
gateway_updated_at = runtime.get("updated_at")
|
gateway_updated_at = runtime.get("updated_at")
|
||||||
if not gateway_running:
|
if not gateway_running:
|
||||||
gateway_state = gateway_state if gateway_state in ("stopped", "startup_failed") else "stopped"
|
gateway_state = gateway_state if gateway_state in {"stopped", "startup_failed"} else "stopped"
|
||||||
gateway_platforms = {}
|
gateway_platforms = {}
|
||||||
elif gateway_running and remote_health_body is not None:
|
elif gateway_running and remote_health_body is not None:
|
||||||
# The health probe confirmed the gateway is alive, but the local
|
# The health probe confirmed the gateway is alive, but the local
|
||||||
# runtime status file may be stale (cross-container). Override
|
# runtime status file may be stale (cross-container). Override
|
||||||
# stopped/None state so the dashboard shows the correct badge.
|
# stopped/None state so the dashboard shows the correct badge.
|
||||||
if gateway_state in (None, "stopped"):
|
if gateway_state in {None, "stopped"}:
|
||||||
gateway_state = "running"
|
gateway_state = "running"
|
||||||
|
|
||||||
# If there was no runtime info at all but the health probe confirmed alive,
|
# If there was no runtime info at all but the health probe confirmed alive,
|
||||||
|
|
@ -1075,7 +1075,7 @@ async def set_model_assignment(body: ModelAssignment):
|
||||||
model = (body.model or "").strip()
|
model = (body.model or "").strip()
|
||||||
task = (body.task or "").strip().lower()
|
task = (body.task or "").strip().lower()
|
||||||
|
|
||||||
if scope not in ("main", "auxiliary"):
|
if scope not in {"main", "auxiliary"}:
|
||||||
raise HTTPException(status_code=400, detail="scope must be 'main' or 'auxiliary'")
|
raise HTTPException(status_code=400, detail="scope must be 'main' or 'auxiliary'")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
@ -1568,7 +1568,7 @@ async def disconnect_oauth_provider(provider_id: str, request: Request):
|
||||||
# AND forget the Claude Code import. We don't touch ~/.claude/* directly
|
# AND forget the Claude Code import. We don't touch ~/.claude/* directly
|
||||||
# — that's owned by the Claude Code CLI; users can re-auth there if they
|
# — that's owned by the Claude Code CLI; users can re-auth there if they
|
||||||
# want to undo a disconnect.
|
# want to undo a disconnect.
|
||||||
if provider_id in ("anthropic", "claude-code"):
|
if provider_id in {"anthropic", "claude-code"}:
|
||||||
try:
|
try:
|
||||||
from agent.anthropic_adapter import _HERMES_OAUTH_FILE
|
from agent.anthropic_adapter import _HERMES_OAUTH_FILE
|
||||||
if _HERMES_OAUTH_FILE.exists():
|
if _HERMES_OAUTH_FILE.exists():
|
||||||
|
|
@ -2024,7 +2024,7 @@ def _codex_full_login_worker(session_id: str) -> None:
|
||||||
if poll.status_code == 200:
|
if poll.status_code == 200:
|
||||||
code_resp = poll.json()
|
code_resp = poll.json()
|
||||||
break
|
break
|
||||||
if poll.status_code in (403, 404):
|
if poll.status_code in {403, 404}:
|
||||||
continue # user hasn't authorized yet
|
continue # user hasn't authorized yet
|
||||||
raise RuntimeError(f"deviceauth/token poll returned {poll.status_code}")
|
raise RuntimeError(f"deviceauth/token poll returned {poll.status_code}")
|
||||||
|
|
||||||
|
|
@ -3003,7 +3003,7 @@ _LOOPBACK_HOSTS = frozenset({"127.0.0.1", "::1", "localhost", "testclient"})
|
||||||
|
|
||||||
def _is_public_bind() -> bool:
|
def _is_public_bind() -> bool:
|
||||||
"""True when bound to all-interfaces (operator used --insecure)."""
|
"""True when bound to all-interfaces (operator used --insecure)."""
|
||||||
return getattr(app.state, "bound_host", "") in ("0.0.0.0", "::")
|
return getattr(app.state, "bound_host", "") in {"0.0.0.0", "::"}
|
||||||
|
|
||||||
|
|
||||||
def _ws_client_is_allowed(ws: "WebSocket") -> bool:
|
def _ws_client_is_allowed(ws: "WebSocket") -> bool:
|
||||||
|
|
@ -3585,7 +3585,7 @@ def _normalise_theme_definition(data: Dict[str, Any]) -> Optional[Dict[str, Any]
|
||||||
if isinstance(radius, str) and radius.strip():
|
if isinstance(radius, str) and radius.strip():
|
||||||
layout["radius"] = radius
|
layout["radius"] = radius
|
||||||
density = layout_src.get("density")
|
density = layout_src.get("density")
|
||||||
if isinstance(density, str) and density in ("compact", "comfortable", "spacious"):
|
if isinstance(density, str) and density in {"compact", "comfortable", "spacious"}:
|
||||||
layout["density"] = density
|
layout["density"] = density
|
||||||
|
|
||||||
# Color overrides — keep only valid keys with string values.
|
# Color overrides — keep only valid keys with string values.
|
||||||
|
|
@ -3918,7 +3918,7 @@ def _merged_plugins_hub() -> Dict[str, Any]:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
can_remove_update = (
|
can_remove_update = (
|
||||||
source in ("user", "git") and under_user_tree and Path(dir_str).is_dir()
|
source in {"user", "git"} and under_user_tree and Path(dir_str).is_dir()
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check if this plugin provides tools that require auth
|
# Check if this plugin provides tools that require auth
|
||||||
|
|
|
||||||
|
|
@ -124,11 +124,11 @@ def webhook_command(args):
|
||||||
if not _require_webhook_enabled():
|
if not _require_webhook_enabled():
|
||||||
return
|
return
|
||||||
|
|
||||||
if sub in ("subscribe", "add"):
|
if sub in {"subscribe", "add"}:
|
||||||
_cmd_subscribe(args)
|
_cmd_subscribe(args)
|
||||||
elif sub in ("list", "ls"):
|
elif sub in {"list", "ls"}:
|
||||||
_cmd_list(args)
|
_cmd_list(args)
|
||||||
elif sub in ("remove", "rm"):
|
elif sub in {"remove", "rm"}:
|
||||||
_cmd_remove(args)
|
_cmd_remove(args)
|
||||||
elif sub == "test":
|
elif sub == "test":
|
||||||
_cmd_test(args)
|
_cmd_test(args)
|
||||||
|
|
|
||||||
|
|
@ -1967,7 +1967,7 @@ class SessionDB:
|
||||||
# Route to LIKE when any non-operator CJK token is <3 CJK chars.
|
# Route to LIKE when any non-operator CJK token is <3 CJK chars.
|
||||||
_tokens_for_check = [
|
_tokens_for_check = [
|
||||||
t for t in raw_query.split()
|
t for t in raw_query.split()
|
||||||
if t.upper() not in ("AND", "OR", "NOT") and self._contains_cjk(t)
|
if t.upper() not in {"AND", "OR", "NOT"} and self._contains_cjk(t)
|
||||||
]
|
]
|
||||||
_any_short_cjk = any(
|
_any_short_cjk = any(
|
||||||
self._count_cjk(t) < 3 for t in _tokens_for_check
|
self._count_cjk(t) < 3 for t in _tokens_for_check
|
||||||
|
|
@ -1980,7 +1980,7 @@ class SessionDB:
|
||||||
tokens = raw_query.split()
|
tokens = raw_query.split()
|
||||||
parts = []
|
parts = []
|
||||||
for tok in tokens:
|
for tok in tokens:
|
||||||
if tok.upper() in ("AND", "OR", "NOT"):
|
if tok.upper() in {"AND", "OR", "NOT"}:
|
||||||
parts.append(tok)
|
parts.append(tok)
|
||||||
else:
|
else:
|
||||||
parts.append('"' + tok.replace('"', '""') + '"')
|
parts.append('"' + tok.replace('"', '""') + '"')
|
||||||
|
|
@ -2031,7 +2031,7 @@ class SessionDB:
|
||||||
# is matched independently (#20494).
|
# is matched independently (#20494).
|
||||||
non_op_tokens = [
|
non_op_tokens = [
|
||||||
t for t in raw_query.split()
|
t for t in raw_query.split()
|
||||||
if t.upper() not in ("AND", "OR", "NOT")
|
if t.upper() not in {"AND", "OR", "NOT"}
|
||||||
] or [raw_query]
|
] or [raw_query]
|
||||||
token_clauses = []
|
token_clauses = []
|
||||||
like_params: list = []
|
like_params: list = []
|
||||||
|
|
|
||||||
|
|
@ -169,7 +169,7 @@ def _extract_attachments(msg: dict) -> List[dict]:
|
||||||
url = part.get("url", part.get("source", {}).get("url", ""))
|
url = part.get("url", part.get("source", {}).get("url", ""))
|
||||||
if url:
|
if url:
|
||||||
attachments.append({"type": "image", "url": url})
|
attachments.append({"type": "image", "url": url})
|
||||||
elif ptype not in ("text",):
|
elif ptype not in {"text",}:
|
||||||
# Unknown non-text content type
|
# Unknown non-text content type
|
||||||
attachments.append({"type": ptype, "data": part})
|
attachments.append({"type": ptype, "data": part})
|
||||||
|
|
||||||
|
|
@ -414,7 +414,7 @@ class EventBridge:
|
||||||
for msg in messages:
|
for msg in messages:
|
||||||
ts = _ts_float(msg.get("timestamp", 0))
|
ts = _ts_float(msg.get("timestamp", 0))
|
||||||
role = msg.get("role", "")
|
role = msg.get("role", "")
|
||||||
if role not in ("user", "assistant"):
|
if role not in {"user", "assistant"}:
|
||||||
continue
|
continue
|
||||||
if ts > last_seen:
|
if ts > last_seen:
|
||||||
new_messages.append(msg)
|
new_messages.append(msg)
|
||||||
|
|
@ -594,7 +594,7 @@ def create_mcp_server(event_bridge: Optional[EventBridge] = None) -> "FastMCP":
|
||||||
filtered = []
|
filtered = []
|
||||||
for msg in all_messages:
|
for msg in all_messages:
|
||||||
role = msg.get("role", "")
|
role = msg.get("role", "")
|
||||||
if role in ("user", "assistant"):
|
if role in {"user", "assistant"}:
|
||||||
content = _extract_message_content(msg)
|
content = _extract_message_content(msg)
|
||||||
if content:
|
if content:
|
||||||
filtered.append({
|
filtered.append({
|
||||||
|
|
@ -847,7 +847,7 @@ def create_mcp_server(event_bridge: Optional[EventBridge] = None) -> "FastMCP":
|
||||||
id: The approval ID from permissions_list_open
|
id: The approval ID from permissions_list_open
|
||||||
decision: One of "allow-once", "allow-always", or "deny"
|
decision: One of "allow-once", "allow-always", or "deny"
|
||||||
"""
|
"""
|
||||||
if decision not in ("allow-once", "allow-always", "deny"):
|
if decision not in {"allow-once", "allow-always", "deny"}:
|
||||||
return json.dumps({
|
return json.dumps({
|
||||||
"error": f"Invalid decision: {decision}. "
|
"error": f"Invalid decision: {decision}. "
|
||||||
f"Must be allow-once, allow-always, or deny"
|
f"Must be allow-once, allow-always, or deny"
|
||||||
|
|
|
||||||
|
|
@ -598,7 +598,7 @@ def _coerce_value(value: str, expected_type, schema: dict | None = None):
|
||||||
return result
|
return result
|
||||||
return value
|
return value
|
||||||
|
|
||||||
if expected_type in ("integer", "number"):
|
if expected_type in {"integer", "number"}:
|
||||||
return _coerce_number(value, integer_only=(expected_type == "integer"))
|
return _coerce_number(value, integer_only=(expected_type == "integer"))
|
||||||
if expected_type == "boolean":
|
if expected_type == "boolean":
|
||||||
return _coerce_boolean(value)
|
return _coerce_boolean(value)
|
||||||
|
|
|
||||||
|
|
@ -392,7 +392,7 @@ def main(
|
||||||
if not user_input:
|
if not user_input:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if user_input.lower() in ('quit', 'exit', 'q'):
|
if user_input.lower() in {'quit', 'exit', 'q'}:
|
||||||
print("\n👋 Goodbye!")
|
print("\n👋 Goodbye!")
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|
|
||||||
64
run_agent.py
64
run_agent.py
|
|
@ -539,7 +539,7 @@ def _trajectory_normalize_msg(msg: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
if isinstance(content, list):
|
if isinstance(content, list):
|
||||||
cleaned = []
|
cleaned = []
|
||||||
for p in content:
|
for p in content:
|
||||||
if isinstance(p, dict) and p.get("type") in ("image", "image_url", "input_image"):
|
if isinstance(p, dict) and p.get("type") in {"image", "image_url", "input_image"}:
|
||||||
cleaned.append({"type": "text", "text": "[screenshot]"})
|
cleaned.append({"type": "text", "text": "[screenshot]"})
|
||||||
else:
|
else:
|
||||||
cleaned.append(p)
|
cleaned.append(p)
|
||||||
|
|
@ -903,7 +903,7 @@ def _strip_images_from_messages(messages: list) -> bool:
|
||||||
continue
|
continue
|
||||||
new_parts = []
|
new_parts = []
|
||||||
for part in content:
|
for part in content:
|
||||||
if isinstance(part, dict) and part.get("type") in ("image_url", "image", "input_image"):
|
if isinstance(part, dict) and part.get("type") in {"image_url", "image", "input_image"}:
|
||||||
found = True
|
found = True
|
||||||
else:
|
else:
|
||||||
new_parts.append(part)
|
new_parts.append(part)
|
||||||
|
|
@ -1393,7 +1393,7 @@ class AIAgent:
|
||||||
|
|
||||||
_pc_cfg = _load_pc_cfg().get("prompt_caching", {}) or {}
|
_pc_cfg = _load_pc_cfg().get("prompt_caching", {}) or {}
|
||||||
_ttl = _pc_cfg.get("cache_ttl", "5m")
|
_ttl = _pc_cfg.get("cache_ttl", "5m")
|
||||||
if _ttl in ("5m", "1h"):
|
if _ttl in {"5m", "1h"}:
|
||||||
self._cache_ttl = _ttl
|
self._cache_ttl = _ttl
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
@ -1640,7 +1640,7 @@ class AIAgent:
|
||||||
# but no credentials were found, fail fast with a clear
|
# but no credentials were found, fail fast with a clear
|
||||||
# message instead of silently routing through OpenRouter.
|
# message instead of silently routing through OpenRouter.
|
||||||
_explicit = (self.provider or "").strip().lower()
|
_explicit = (self.provider or "").strip().lower()
|
||||||
if _explicit and _explicit not in ("auto", "openrouter", "custom"):
|
if _explicit and _explicit not in {"auto", "openrouter", "custom"}:
|
||||||
# Look up the actual env var name from the provider
|
# Look up the actual env var name from the provider
|
||||||
# config — some providers use non-standard names
|
# config — some providers use non-standard names
|
||||||
# (e.g. alibaba → DASHSCOPE_API_KEY, not ALIBABA_API_KEY).
|
# (e.g. alibaba → DASHSCOPE_API_KEY, not ALIBABA_API_KEY).
|
||||||
|
|
@ -2029,7 +2029,7 @@ class AIAgent:
|
||||||
compression_threshold = _model_cthresh
|
compression_threshold = _model_cthresh
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
compression_enabled = str(_compression_cfg.get("enabled", True)).lower() in ("true", "1", "yes")
|
compression_enabled = str(_compression_cfg.get("enabled", True)).lower() in {"true", "1", "yes"}
|
||||||
compression_target_ratio = float(_compression_cfg.get("target_ratio", 0.20))
|
compression_target_ratio = float(_compression_cfg.get("target_ratio", 0.20))
|
||||||
compression_protect_last = int(_compression_cfg.get("protect_last_n", 20))
|
compression_protect_last = int(_compression_cfg.get("protect_last_n", 20))
|
||||||
|
|
||||||
|
|
@ -2543,7 +2543,7 @@ class AIAgent:
|
||||||
# tests) can't reintroduce the double-/v1 404 bug.
|
# tests) can't reintroduce the double-/v1 404 bug.
|
||||||
if (
|
if (
|
||||||
api_mode == "anthropic_messages"
|
api_mode == "anthropic_messages"
|
||||||
and new_provider in ("opencode-zen", "opencode-go")
|
and new_provider in {"opencode-zen", "opencode-go"}
|
||||||
and isinstance(base_url, str)
|
and isinstance(base_url, str)
|
||||||
and base_url
|
and base_url
|
||||||
):
|
):
|
||||||
|
|
@ -4280,7 +4280,7 @@ class AIAgent:
|
||||||
metadata["task_id"] = task_id
|
metadata["task_id"] = task_id
|
||||||
if tool_call_id:
|
if tool_call_id:
|
||||||
metadata["tool_call_id"] = tool_call_id
|
metadata["tool_call_id"] = tool_call_id
|
||||||
return {k: v for k, v in metadata.items() if v not in (None, "")}
|
return {k: v for k, v in metadata.items() if v not in {None, ""}}
|
||||||
|
|
||||||
def _apply_persist_user_message_override(self, messages: List[Dict]) -> None:
|
def _apply_persist_user_message_override(self, messages: List[Dict]) -> None:
|
||||||
"""Rewrite the current-turn user message before persistence/return.
|
"""Rewrite the current-turn user message before persistence/return.
|
||||||
|
|
@ -4494,7 +4494,7 @@ class AIAgent:
|
||||||
for p in content:
|
for p in content:
|
||||||
if isinstance(p, dict) and p.get("type") == "text":
|
if isinstance(p, dict) and p.get("type") == "text":
|
||||||
_txt.append(str(p.get("text", "")))
|
_txt.append(str(p.get("text", "")))
|
||||||
elif isinstance(p, dict) and p.get("type") in ("image", "image_url", "input_image"):
|
elif isinstance(p, dict) and p.get("type") in {"image", "image_url", "input_image"}:
|
||||||
_txt.append("[screenshot]")
|
_txt.append("[screenshot]")
|
||||||
content = "\n".join(_txt) if _txt else None
|
content = "\n".join(_txt) if _txt else None
|
||||||
tool_calls_data = None
|
tool_calls_data = None
|
||||||
|
|
@ -4853,11 +4853,11 @@ class AIAgent:
|
||||||
context["message"] = message.strip()
|
context["message"] = message.strip()
|
||||||
for key in ("resets_at", "reset_at"):
|
for key in ("resets_at", "reset_at"):
|
||||||
value = payload.get(key)
|
value = payload.get(key)
|
||||||
if value not in (None, ""):
|
if value not in {None, ""}:
|
||||||
context["reset_at"] = value
|
context["reset_at"] = value
|
||||||
break
|
break
|
||||||
retry_after = payload.get("retry_after")
|
retry_after = payload.get("retry_after")
|
||||||
if retry_after not in (None, "") and "reset_at" not in context:
|
if retry_after not in {None, ""} and "reset_at" not in context:
|
||||||
try:
|
try:
|
||||||
context["reset_at"] = time.time() + float(retry_after)
|
context["reset_at"] = time.time() + float(retry_after)
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
|
|
@ -5678,9 +5678,9 @@ class AIAgent:
|
||||||
if self.valid_tool_names:
|
if self.valid_tool_names:
|
||||||
_enforce = self._tool_use_enforcement
|
_enforce = self._tool_use_enforcement
|
||||||
_inject = False
|
_inject = False
|
||||||
if _enforce is True or (isinstance(_enforce, str) and _enforce.lower() in ("true", "always", "yes", "on")):
|
if _enforce is True or (isinstance(_enforce, str) and _enforce.lower() in {"true", "always", "yes", "on"}):
|
||||||
_inject = True
|
_inject = True
|
||||||
elif _enforce is False or (isinstance(_enforce, str) and _enforce.lower() in ("false", "never", "no", "off")):
|
elif _enforce is False or (isinstance(_enforce, str) and _enforce.lower() in {"false", "never", "no", "off"}):
|
||||||
_inject = False
|
_inject = False
|
||||||
elif isinstance(_enforce, list):
|
elif isinstance(_enforce, list):
|
||||||
model_lower = (self.model or "").lower()
|
model_lower = (self.model or "").lower()
|
||||||
|
|
@ -5935,7 +5935,7 @@ class AIAgent:
|
||||||
return False
|
return False
|
||||||
continue
|
continue
|
||||||
btype = block.get("type")
|
btype = block.get("type")
|
||||||
if btype in ("thinking", "redacted_thinking"):
|
if btype in {"thinking", "redacted_thinking"}:
|
||||||
continue
|
continue
|
||||||
if btype == "text":
|
if btype == "text":
|
||||||
text = block.get("text", "")
|
text = block.get("text", "")
|
||||||
|
|
@ -6665,7 +6665,7 @@ class AIAgent:
|
||||||
if done_item is not None:
|
if done_item is not None:
|
||||||
collected_output_items.append(done_item)
|
collected_output_items.append(done_item)
|
||||||
# Log non-completed terminal events for diagnostics
|
# Log non-completed terminal events for diagnostics
|
||||||
elif event_type in ("response.incomplete", "response.failed"):
|
elif event_type in {"response.incomplete", "response.failed"}:
|
||||||
resp_obj = getattr(event, "response", None)
|
resp_obj = getattr(event, "response", None)
|
||||||
status = getattr(resp_obj, "status", None) if resp_obj else None
|
status = getattr(resp_obj, "status", None) if resp_obj else None
|
||||||
incomplete_details = getattr(resp_obj, "incomplete_details", None) if resp_obj else None
|
incomplete_details = getattr(resp_obj, "incomplete_details", None) if resp_obj else None
|
||||||
|
|
@ -6767,7 +6767,7 @@ class AIAgent:
|
||||||
done_item = event.get("item")
|
done_item = event.get("item")
|
||||||
if done_item is not None:
|
if done_item is not None:
|
||||||
collected_output_items.append(done_item)
|
collected_output_items.append(done_item)
|
||||||
elif event_type in ("response.output_text.delta",):
|
elif event_type in {"response.output_text.delta",}:
|
||||||
delta = getattr(event, "delta", "")
|
delta = getattr(event, "delta", "")
|
||||||
if not delta and isinstance(event, dict):
|
if not delta and isinstance(event, dict):
|
||||||
delta = event.get("delta", "")
|
delta = event.get("delta", "")
|
||||||
|
|
@ -7063,7 +7063,7 @@ class AIAgent:
|
||||||
effective_reason = FailoverReason.billing
|
effective_reason = FailoverReason.billing
|
||||||
elif status_code == 429:
|
elif status_code == 429:
|
||||||
effective_reason = FailoverReason.rate_limit
|
effective_reason = FailoverReason.rate_limit
|
||||||
elif status_code in (401, 403):
|
elif status_code in {401, 403}:
|
||||||
effective_reason = FailoverReason.auth
|
effective_reason = FailoverReason.auth
|
||||||
|
|
||||||
if effective_reason == FailoverReason.billing:
|
if effective_reason == FailoverReason.billing:
|
||||||
|
|
@ -8384,7 +8384,7 @@ class AIAgent:
|
||||||
auth resolution and client construction — no duplicated provider→key
|
auth resolution and client construction — no duplicated provider→key
|
||||||
mappings.
|
mappings.
|
||||||
"""
|
"""
|
||||||
if reason in (FailoverReason.rate_limit, FailoverReason.billing):
|
if reason in {FailoverReason.rate_limit, FailoverReason.billing}:
|
||||||
# Only start cooldown when leaving the primary provider. If we're
|
# Only start cooldown when leaving the primary provider. If we're
|
||||||
# already on a fallback and chain-switching, the primary wasn't the
|
# already on a fallback and chain-switching, the primary wasn't the
|
||||||
# source of the 429 so the cooldown should not be reset/extended.
|
# source of the 429 so the cooldown should not be reset/extended.
|
||||||
|
|
@ -8710,7 +8710,7 @@ class AIAgent:
|
||||||
if self._is_openrouter_url():
|
if self._is_openrouter_url():
|
||||||
return False
|
return False
|
||||||
provider_lower = (self.provider or "").strip().lower()
|
provider_lower = (self.provider or "").strip().lower()
|
||||||
if provider_lower in ("nous", "nous-research"):
|
if provider_lower in {"nous", "nous-research"}:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
@ -10304,7 +10304,7 @@ class AIAgent:
|
||||||
store=self._memory_store,
|
store=self._memory_store,
|
||||||
)
|
)
|
||||||
# Bridge: notify external memory provider of built-in memory writes
|
# Bridge: notify external memory provider of built-in memory writes
|
||||||
if self._memory_manager and function_args.get("action") in ("add", "replace"):
|
if self._memory_manager and function_args.get("action") in {"add", "replace"}:
|
||||||
try:
|
try:
|
||||||
self._memory_manager.on_memory_write(
|
self._memory_manager.on_memory_write(
|
||||||
function_args.get("action", ""),
|
function_args.get("action", ""),
|
||||||
|
|
@ -10403,7 +10403,7 @@ class AIAgent:
|
||||||
function_args = {}
|
function_args = {}
|
||||||
|
|
||||||
# Checkpoint for file-mutating tools
|
# Checkpoint for file-mutating tools
|
||||||
if function_name in ("write_file", "patch") and self._checkpoint_mgr.enabled:
|
if function_name in {"write_file", "patch"} and self._checkpoint_mgr.enabled:
|
||||||
try:
|
try:
|
||||||
file_path = function_args.get("path", "")
|
file_path = function_args.get("path", "")
|
||||||
if file_path:
|
if file_path:
|
||||||
|
|
@ -10860,7 +10860,7 @@ class AIAgent:
|
||||||
logging.debug(f"Tool start callback error: {cb_err}")
|
logging.debug(f"Tool start callback error: {cb_err}")
|
||||||
|
|
||||||
# Checkpoint: snapshot working dir before file-mutating tools
|
# Checkpoint: snapshot working dir before file-mutating tools
|
||||||
if not _execution_blocked and function_name in ("write_file", "patch") and self._checkpoint_mgr.enabled:
|
if not _execution_blocked and function_name in {"write_file", "patch"} and self._checkpoint_mgr.enabled:
|
||||||
try:
|
try:
|
||||||
file_path = function_args.get("path", "")
|
file_path = function_args.get("path", "")
|
||||||
if file_path:
|
if file_path:
|
||||||
|
|
@ -10932,7 +10932,7 @@ class AIAgent:
|
||||||
store=self._memory_store,
|
store=self._memory_store,
|
||||||
)
|
)
|
||||||
# Bridge: notify external memory provider of built-in memory writes
|
# Bridge: notify external memory provider of built-in memory writes
|
||||||
if self._memory_manager and function_args.get("action") in ("add", "replace"):
|
if self._memory_manager and function_args.get("action") in {"add", "replace"}:
|
||||||
try:
|
try:
|
||||||
self._memory_manager.on_memory_write(
|
self._memory_manager.on_memory_write(
|
||||||
function_args.get("action", ""),
|
function_args.get("action", ""),
|
||||||
|
|
@ -12462,9 +12462,9 @@ class AIAgent:
|
||||||
_failure_hint = f"upstream gateway timeout (504, {api_duration:.0f}s)"
|
_failure_hint = f"upstream gateway timeout (504, {api_duration:.0f}s)"
|
||||||
elif _resp_error_code == 429:
|
elif _resp_error_code == 429:
|
||||||
_failure_hint = f"rate limited by upstream provider (429)"
|
_failure_hint = f"rate limited by upstream provider (429)"
|
||||||
elif _resp_error_code in (500, 502):
|
elif _resp_error_code in {500, 502}:
|
||||||
_failure_hint = f"upstream server error ({_resp_error_code}, {api_duration:.0f}s)"
|
_failure_hint = f"upstream server error ({_resp_error_code}, {api_duration:.0f}s)"
|
||||||
elif _resp_error_code in (503, 529):
|
elif _resp_error_code in {503, 529}:
|
||||||
_failure_hint = f"upstream provider overloaded ({_resp_error_code})"
|
_failure_hint = f"upstream provider overloaded ({_resp_error_code})"
|
||||||
elif _resp_error_code is not None:
|
elif _resp_error_code is not None:
|
||||||
_failure_hint = f"upstream error (code {_resp_error_code}, {api_duration:.0f}s)"
|
_failure_hint = f"upstream error (code {_resp_error_code}, {api_duration:.0f}s)"
|
||||||
|
|
@ -12652,7 +12652,7 @@ class AIAgent:
|
||||||
"error": _exhaust_error,
|
"error": _exhaust_error,
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.api_mode in ("chat_completions", "bedrock_converse", "anthropic_messages"):
|
if self.api_mode in {"chat_completions", "bedrock_converse", "anthropic_messages"}:
|
||||||
assistant_message = _trunc_msg
|
assistant_message = _trunc_msg
|
||||||
if assistant_message is not None and not _trunc_has_tool_calls:
|
if assistant_message is not None and not _trunc_has_tool_calls:
|
||||||
length_continue_retries += 1
|
length_continue_retries += 1
|
||||||
|
|
@ -12692,7 +12692,7 @@ class AIAgent:
|
||||||
"error": "Response remained truncated after 3 continuation attempts",
|
"error": "Response remained truncated after 3 continuation attempts",
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.api_mode in ("chat_completions", "bedrock_converse", "anthropic_messages"):
|
if self.api_mode in {"chat_completions", "bedrock_converse", "anthropic_messages"}:
|
||||||
assistant_message = _trunc_msg
|
assistant_message = _trunc_msg
|
||||||
if assistant_message is not None and _trunc_has_tool_calls:
|
if assistant_message is not None and _trunc_has_tool_calls:
|
||||||
if truncated_tool_call_retries < 1:
|
if truncated_tool_call_retries < 1:
|
||||||
|
|
@ -13524,10 +13524,10 @@ class AIAgent:
|
||||||
# When a fallback model is configured, switch immediately instead
|
# When a fallback model is configured, switch immediately instead
|
||||||
# of burning through retries with exponential backoff -- the
|
# of burning through retries with exponential backoff -- the
|
||||||
# primary provider won't recover within the retry window.
|
# primary provider won't recover within the retry window.
|
||||||
is_rate_limited = classified.reason in (
|
is_rate_limited = classified.reason in {
|
||||||
FailoverReason.rate_limit,
|
FailoverReason.rate_limit,
|
||||||
FailoverReason.billing,
|
FailoverReason.billing,
|
||||||
)
|
}
|
||||||
if is_rate_limited and self._fallback_index < len(self._fallback_chain):
|
if is_rate_limited and self._fallback_index < len(self._fallback_chain):
|
||||||
# Don't eagerly fallback if credential pool rotation may
|
# Don't eagerly fallback if credential pool rotation may
|
||||||
# still recover. See _pool_may_recover_from_rate_limit
|
# still recover. See _pool_may_recover_from_rate_limit
|
||||||
|
|
@ -13852,7 +13852,7 @@ class AIAgent:
|
||||||
or (
|
or (
|
||||||
not classified.retryable
|
not classified.retryable
|
||||||
and not classified.should_compress
|
and not classified.should_compress
|
||||||
and classified.reason not in (
|
and classified.reason not in {
|
||||||
FailoverReason.rate_limit,
|
FailoverReason.rate_limit,
|
||||||
FailoverReason.billing,
|
FailoverReason.billing,
|
||||||
FailoverReason.overloaded,
|
FailoverReason.overloaded,
|
||||||
|
|
@ -13860,7 +13860,7 @@ class AIAgent:
|
||||||
FailoverReason.payload_too_large,
|
FailoverReason.payload_too_large,
|
||||||
FailoverReason.long_context_tier,
|
FailoverReason.long_context_tier,
|
||||||
FailoverReason.thinking_signature,
|
FailoverReason.thinking_signature,
|
||||||
)
|
}
|
||||||
)
|
)
|
||||||
) and not is_context_length_error
|
) and not is_context_length_error
|
||||||
|
|
||||||
|
|
@ -15307,9 +15307,9 @@ def main(
|
||||||
info = get_toolset_info(name)
|
info = get_toolset_info(name)
|
||||||
if info:
|
if info:
|
||||||
entry = (name, info)
|
entry = (name, info)
|
||||||
if name in ["web", "terminal", "vision", "creative", "reasoning"]:
|
if name in {"web", "terminal", "vision", "creative", "reasoning"}:
|
||||||
basic_toolsets.append(entry)
|
basic_toolsets.append(entry)
|
||||||
elif name in ["research", "development", "analysis", "content_creation", "full_stack"]:
|
elif name in {"research", "development", "analysis", "content_creation", "full_stack"}:
|
||||||
composite_toolsets.append(entry)
|
composite_toolsets.append(entry)
|
||||||
else:
|
else:
|
||||||
scenario_toolsets.append(entry)
|
scenario_toolsets.append(entry)
|
||||||
|
|
|
||||||
|
|
@ -147,7 +147,7 @@ def batch_resolve_paths(skills: list, auth: GitHubAuth) -> list:
|
||||||
4. Match skills to their resolved paths
|
4. Match skills to their resolved paths
|
||||||
"""
|
"""
|
||||||
# Filter to skills.sh entries that need resolution
|
# Filter to skills.sh entries that need resolution
|
||||||
skills_sh = [s for s in skills if s["source"] in ("skills.sh", "skills-sh")]
|
skills_sh = [s for s in skills if s["source"] in {"skills.sh", "skills-sh"}]
|
||||||
if not skills_sh:
|
if not skills_sh:
|
||||||
return skills
|
return skills
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -360,7 +360,7 @@ def format_diff(before: dict[str, float], after: dict[str, float]) -> str:
|
||||||
b = before.get(k, 0.0)
|
b = before.get(k, 0.0)
|
||||||
a = after.get(k, 0.0)
|
a = after.get(k, 0.0)
|
||||||
d = a - b
|
d = a - b
|
||||||
pct_change = ((a / b) - 1) * 100 if b not in (0, 0.0) else float("inf") if a else 0
|
pct_change = ((a / b) - 1) * 100 if b not in {0, 0.0} else float("inf") if a else 0
|
||||||
|
|
||||||
# Flag improvements vs regressions. For _p99 / _max / _total / gaps_over /
|
# Flag improvements vs regressions. For _p99 / _max / _total / gaps_over /
|
||||||
# patches / writeBytes / backpressure, LOWER is better. For fps / gaps_under,
|
# patches / writeBytes / backpressure, LOWER is better. For fps / gaps_under,
|
||||||
|
|
|
||||||
|
|
@ -759,13 +759,13 @@ def prompt_dangerous_approval(command: str, description: str,
|
||||||
return "deny"
|
return "deny"
|
||||||
|
|
||||||
choice = result["choice"]
|
choice = result["choice"]
|
||||||
if choice in ('o', 'once'):
|
if choice in {'o', 'once'}:
|
||||||
print(t("approval.allowed_once"))
|
print(t("approval.allowed_once"))
|
||||||
return "once"
|
return "once"
|
||||||
elif choice in ('s', 'session'):
|
elif choice in {'s', 'session'}:
|
||||||
print(t("approval.allowed_session"))
|
print(t("approval.allowed_session"))
|
||||||
return "session"
|
return "session"
|
||||||
elif choice in ('a', 'always'):
|
elif choice in {'a', 'always'}:
|
||||||
if not allow_permanent:
|
if not allow_permanent:
|
||||||
print(t("approval.allowed_session"))
|
print(t("approval.allowed_session"))
|
||||||
return "session"
|
return "session"
|
||||||
|
|
@ -831,7 +831,7 @@ def _get_cron_approval_mode() -> str:
|
||||||
from hermes_cli.config import load_config
|
from hermes_cli.config import load_config
|
||||||
config = load_config()
|
config = load_config()
|
||||||
mode = str(cfg_get(config, "approvals", "cron_mode", default="deny")).lower().strip()
|
mode = str(cfg_get(config, "approvals", "cron_mode", default="deny")).lower().strip()
|
||||||
if mode in ("approve", "off", "allow", "yes"):
|
if mode in {"approve", "off", "allow", "yes"}:
|
||||||
return "approve"
|
return "approve"
|
||||||
return "deny"
|
return "deny"
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|
@ -900,7 +900,7 @@ def check_dangerous_command(command: str, env_type: str,
|
||||||
Returns:
|
Returns:
|
||||||
{"approved": True/False, "message": str or None, ...}
|
{"approved": True/False, "message": str or None, ...}
|
||||||
"""
|
"""
|
||||||
if env_type in ("docker", "singularity", "modal", "daytona", "vercel_sandbox"):
|
if env_type in {"docker", "singularity", "modal", "daytona", "vercel_sandbox"}:
|
||||||
return {"approved": True, "message": None}
|
return {"approved": True, "message": None}
|
||||||
|
|
||||||
# Hardline floor: commands with no recovery path (rm -rf /, mkfs, dd
|
# Hardline floor: commands with no recovery path (rm -rf /, mkfs, dd
|
||||||
|
|
@ -1025,7 +1025,7 @@ def check_all_command_guards(command: str, env_type: str,
|
||||||
other was shown to the user.
|
other was shown to the user.
|
||||||
"""
|
"""
|
||||||
# Skip containers for both checks
|
# Skip containers for both checks
|
||||||
if env_type in ("docker", "singularity", "modal", "daytona", "vercel_sandbox"):
|
if env_type in {"docker", "singularity", "modal", "daytona", "vercel_sandbox"}:
|
||||||
return {"approved": True, "message": None}
|
return {"approved": True, "message": None}
|
||||||
|
|
||||||
# Hardline floor: unconditional block for catastrophic commands
|
# Hardline floor: unconditional block for catastrophic commands
|
||||||
|
|
@ -1104,7 +1104,7 @@ def check_all_command_guards(command: str, env_type: str,
|
||||||
# Previously, tirith "block" was a hard block with no approval prompt.
|
# Previously, tirith "block" was a hard block with no approval prompt.
|
||||||
# Now both block and warn go through the approval flow so users can
|
# Now both block and warn go through the approval flow so users can
|
||||||
# inspect the explanation and approve if they understand the risk.
|
# inspect the explanation and approve if they understand the risk.
|
||||||
if tirith_result["action"] in ("block", "warn"):
|
if tirith_result["action"] in {"block", "warn"}:
|
||||||
findings = tirith_result.get("findings") or []
|
findings = tirith_result.get("findings") or []
|
||||||
rule_id = findings[0].get("rule_id", "unknown") if findings else "unknown"
|
rule_id = findings[0].get("rule_id", "unknown") if findings else "unknown"
|
||||||
tirith_key = f"tirith:{rule_id}"
|
tirith_key = f"tirith:{rule_id}"
|
||||||
|
|
|
||||||
|
|
@ -184,7 +184,7 @@ class BrowserUseProvider(CloudBrowserProvider):
|
||||||
json={"action": "stop"},
|
json={"action": "stop"},
|
||||||
timeout=10,
|
timeout=10,
|
||||||
)
|
)
|
||||||
if response.status_code in (200, 201, 204):
|
if response.status_code in {200, 201, 204}:
|
||||||
logger.debug("Successfully closed Browser Use session %s", session_id)
|
logger.debug("Successfully closed Browser Use session %s", session_id)
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
|
|
|
||||||
|
|
@ -180,7 +180,7 @@ class BrowserbaseProvider(CloudBrowserProvider):
|
||||||
},
|
},
|
||||||
timeout=10,
|
timeout=10,
|
||||||
)
|
)
|
||||||
if response.status_code in (200, 201, 204):
|
if response.status_code in {200, 201, 204}:
|
||||||
logger.debug("Successfully closed Browserbase session %s", session_id)
|
logger.debug("Successfully closed Browserbase session %s", session_id)
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
|
|
|
||||||
|
|
@ -79,7 +79,7 @@ class FirecrawlProvider(CloudBrowserProvider):
|
||||||
headers=self._headers(),
|
headers=self._headers(),
|
||||||
timeout=10,
|
timeout=10,
|
||||||
)
|
)
|
||||||
if response.status_code in (200, 201, 204):
|
if response.status_code in {200, 201, 204}:
|
||||||
logger.debug("Successfully closed Firecrawl session %s", session_id)
|
logger.debug("Successfully closed Firecrawl session %s", session_id)
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
|
|
|
||||||
|
|
@ -412,7 +412,7 @@ class CDPSupervisor:
|
||||||
``{"ok": False, "error": "..."}`` on a recoverable error (no dialog,
|
``{"ok": False, "error": "..."}`` on a recoverable error (no dialog,
|
||||||
ambiguous dialog_id, supervisor inactive).
|
ambiguous dialog_id, supervisor inactive).
|
||||||
"""
|
"""
|
||||||
if action not in ("accept", "dismiss"):
|
if action not in {"accept", "dismiss"}:
|
||||||
return {"ok": False, "error": f"action must be 'accept' or 'dismiss', got {action!r}"}
|
return {"ok": False, "error": f"action must be 'accept' or 'dismiss', got {action!r}"}
|
||||||
|
|
||||||
with self._state_lock:
|
with self._state_lock:
|
||||||
|
|
@ -1206,7 +1206,7 @@ class CDPSupervisor:
|
||||||
info = params.get("targetInfo") or {}
|
info = params.get("targetInfo") or {}
|
||||||
sid = params.get("sessionId")
|
sid = params.get("sessionId")
|
||||||
target_type = info.get("type")
|
target_type = info.get("type")
|
||||||
if not sid or target_type not in ("iframe", "worker"):
|
if not sid or target_type not in {"iframe", "worker"}:
|
||||||
return
|
return
|
||||||
self._child_sessions[sid] = {"info": info, "type": target_type}
|
self._child_sessions[sid] = {"info": info, "type": target_type}
|
||||||
|
|
||||||
|
|
@ -1290,7 +1290,7 @@ class CDPSupervisor:
|
||||||
event = ConsoleEvent(ts=time.time(), level="exception", text=text, url=url)
|
event = ConsoleEvent(ts=time.time(), level="exception", text=text, url=url)
|
||||||
else:
|
else:
|
||||||
raw_level = str(params.get("type") or "log")
|
raw_level = str(params.get("type") or "log")
|
||||||
level = "error" if raw_level in ("error", "assert") else (
|
level = "error" if raw_level in {"error", "assert"} else (
|
||||||
"warning" if raw_level == "warning" else "log"
|
"warning" if raw_level == "warning" else "log"
|
||||||
)
|
)
|
||||||
args = params.get("args") or []
|
args = params.get("args") or []
|
||||||
|
|
|
||||||
|
|
@ -918,7 +918,7 @@ def _url_is_private(url: str) -> bool:
|
||||||
# Hostname — must resolve to confirm it's private (bare "localhost"
|
# Hostname — must resolve to confirm it's private (bare "localhost"
|
||||||
# resolves to 127.0.0.1 via /etc/hosts). Short-circuit on obvious
|
# resolves to 127.0.0.1 via /etc/hosts). Short-circuit on obvious
|
||||||
# names to avoid a DNS hop.
|
# names to avoid a DNS hop.
|
||||||
if hostname in ("localhost",) or hostname.endswith(".localhost"):
|
if hostname in {"localhost",} or hostname.endswith(".localhost"):
|
||||||
return True
|
return True
|
||||||
if hostname.endswith(".local") or hostname.endswith(".lan") or hostname.endswith(".internal"):
|
if hostname.endswith(".local") or hostname.endswith(".lan") or hostname.endswith(".internal"):
|
||||||
return True
|
return True
|
||||||
|
|
@ -2499,7 +2499,7 @@ def browser_scroll(direction: str, task_id: Optional[str] = None) -> str:
|
||||||
JSON string with scroll result
|
JSON string with scroll result
|
||||||
"""
|
"""
|
||||||
# Validate direction
|
# Validate direction
|
||||||
if direction not in ["up", "down"]:
|
if direction not in {"up", "down"}:
|
||||||
return json.dumps({
|
return json.dumps({
|
||||||
"success": False,
|
"success": False,
|
||||||
"error": f"Invalid direction '{direction}'. Use 'up' or 'down'."
|
"error": f"Invalid direction '{direction}'. Use 'up' or 'down'."
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue