chore: uptick
This commit is contained in:
+12
-5
@@ -19,6 +19,8 @@ import unicodedata
|
||||
from typing import Optional
|
||||
from hermes_cli.config import cfg_get
|
||||
|
||||
from utils import is_truthy_value
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Per-thread/per-task gateway session identity.
|
||||
@@ -400,8 +402,8 @@ def unregister_gateway_notify(session_key: str) -> None:
|
||||
with _lock:
|
||||
_gateway_notify_cbs.pop(session_key, None)
|
||||
entries = _gateway_queues.pop(session_key, [])
|
||||
for entry in entries:
|
||||
entry.event.set()
|
||||
for entry in entries:
|
||||
entry.event.set()
|
||||
|
||||
|
||||
def resolve_gateway_approval(session_key: str, choice: str,
|
||||
@@ -475,7 +477,12 @@ def clear_session(session_key: str) -> None:
|
||||
_session_approved.pop(session_key, None)
|
||||
_session_yolo.discard(session_key)
|
||||
_pending.pop(session_key, None)
|
||||
_gateway_queues.pop(session_key, None)
|
||||
entries = _gateway_queues.pop(session_key, [])
|
||||
for entry in entries:
|
||||
# Session-boundary cleanup should cancel any blocked approval waits
|
||||
# immediately so the old run can unwind instead of idling until timeout.
|
||||
entry.result = "deny"
|
||||
entry.event.set()
|
||||
|
||||
|
||||
def is_session_yolo_enabled(session_key: str) -> bool:
|
||||
@@ -797,7 +804,7 @@ def check_dangerous_command(command: str, env_type: str,
|
||||
|
||||
# --yolo: bypass all approval prompts. Gateway /yolo is session-scoped;
|
||||
# CLI --yolo remains process-scoped via the env var for local use.
|
||||
if os.getenv("HERMES_YOLO_MODE") or is_current_session_yolo_enabled():
|
||||
if is_truthy_value(os.getenv("HERMES_YOLO_MODE")) or is_current_session_yolo_enabled():
|
||||
return {"approved": True, "message": None}
|
||||
|
||||
is_dangerous, pattern_key, description = detect_dangerous_command(command)
|
||||
@@ -922,7 +929,7 @@ def check_all_command_guards(command: str, env_type: str,
|
||||
# --yolo or approvals.mode=off: bypass all approval prompts.
|
||||
# Gateway /yolo is session-scoped; CLI --yolo remains process-scoped.
|
||||
approval_mode = _get_approval_mode()
|
||||
if os.getenv("HERMES_YOLO_MODE") or is_current_session_yolo_enabled() or approval_mode == "off":
|
||||
if is_truthy_value(os.getenv("HERMES_YOLO_MODE")) or is_current_session_yolo_enabled() or approval_mode == "off":
|
||||
return {"approved": True, "message": None}
|
||||
|
||||
is_cli = os.getenv("HERMES_INTERACTIVE")
|
||||
|
||||
@@ -1304,8 +1304,12 @@ class _SupervisorRegistry:
|
||||
existing = self._by_task.get(task_id)
|
||||
if existing is not None:
|
||||
if existing.cdp_url == cdp_url:
|
||||
return existing
|
||||
# URL changed — tear down old, fall through to re-create.
|
||||
thread_ok = existing._thread is not None and existing._thread.is_alive()
|
||||
loop_ok = existing._loop is not None and existing._loop.is_running()
|
||||
if thread_ok and loop_ok:
|
||||
return existing
|
||||
# Unhealthy — tear down and recreate.
|
||||
# URL changed or unhealthy — tear down, fall through to re-create.
|
||||
self._by_task.pop(task_id, None)
|
||||
if existing is not None:
|
||||
existing.stop()
|
||||
|
||||
@@ -2309,7 +2309,7 @@ def _resolve_delegation_credentials(cfg: dict, parent_agent) -> dict:
|
||||
)
|
||||
|
||||
return {
|
||||
"model": configured_model,
|
||||
"model": configured_model or runtime.get("model") or None,
|
||||
"provider": runtime.get("provider"),
|
||||
"base_url": runtime.get("base_url"),
|
||||
"api_key": api_key,
|
||||
|
||||
@@ -132,7 +132,7 @@ def _channel_type_name(type_id: int) -> str:
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Module-level cache so the app/me endpoint is hit at most once per process.
|
||||
_capability_cache: Optional[Dict[str, Any]] = None
|
||||
_capability_cache: Dict[str, Dict[str, Any]] = {}
|
||||
|
||||
|
||||
def _detect_capabilities(token: str, *, force: bool = False) -> Dict[str, Any]:
|
||||
@@ -148,8 +148,8 @@ def _detect_capabilities(token: str, *, force: bool = False) -> Dict[str, Any]:
|
||||
Cached in a module-global. Pass ``force=True`` to re-fetch.
|
||||
"""
|
||||
global _capability_cache
|
||||
if _capability_cache is not None and not force:
|
||||
return _capability_cache
|
||||
if token in _capability_cache and not force:
|
||||
return _capability_cache[token]
|
||||
|
||||
caps: Dict[str, Any] = {
|
||||
"has_members_intent": True,
|
||||
@@ -172,14 +172,14 @@ def _detect_capabilities(token: str, *, force: bool = False) -> Dict[str, Any]:
|
||||
"Discord capability detection failed (%s); exposing all actions.", exc,
|
||||
)
|
||||
|
||||
_capability_cache = caps
|
||||
_capability_cache[token] = caps
|
||||
return caps
|
||||
|
||||
|
||||
def _reset_capability_cache() -> None:
|
||||
"""Test hook: clear the detection cache."""
|
||||
global _capability_cache
|
||||
_capability_cache = None
|
||||
_capability_cache = {}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -266,7 +266,11 @@ _HIDDEN_SESSION_SOURCES = ("tool",)
|
||||
def _list_recent_sessions(db, limit: int, current_session_id: str = None) -> str:
|
||||
"""Return metadata for the most recent sessions (no LLM calls)."""
|
||||
try:
|
||||
sessions = db.list_sessions_rich(limit=limit + 5, exclude_sources=list(_HIDDEN_SESSION_SOURCES)) # fetch extra to skip current
|
||||
sessions = db.list_sessions_rich(
|
||||
limit=limit + 5,
|
||||
exclude_sources=list(_HIDDEN_SESSION_SOURCES),
|
||||
order_by_last_active=True,
|
||||
) # fetch extra to skip current
|
||||
|
||||
# Resolve current session lineage to exclude it
|
||||
current_root = None
|
||||
|
||||
@@ -42,7 +42,7 @@ from pathlib import Path
|
||||
from hermes_constants import get_hermes_home, display_hermes_home
|
||||
from typing import Dict, Any, Optional, Tuple
|
||||
|
||||
from utils import atomic_replace
|
||||
from utils import atomic_replace, is_truthy_value
|
||||
from hermes_cli.config import cfg_get
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -67,7 +67,10 @@ def _guard_agent_created_enabled() -> bool:
|
||||
try:
|
||||
from hermes_cli.config import load_config
|
||||
cfg = load_config()
|
||||
return bool(cfg_get(cfg, "skills", "guard_agent_created", default=False))
|
||||
return is_truthy_value(
|
||||
cfg_get(cfg, "skills", "guard_agent_created"),
|
||||
default=False,
|
||||
)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
@@ -620,6 +620,32 @@ def _rewrite_real_sudo_invocations(command: str) -> tuple[str, bool]:
|
||||
return "".join(out), found
|
||||
|
||||
|
||||
def _sudo_nopasswd_works() -> bool:
|
||||
"""Return True when local sudo currently works without prompting.
|
||||
|
||||
Only probes for the `local` terminal backend; Docker/SSH/Modal/etc. must
|
||||
not inherit the host's sudo state. Re-probes every call (no process-level
|
||||
cache) so an expired sudo timestamp cannot make a later command silently
|
||||
block waiting for a password.
|
||||
"""
|
||||
terminal_env = os.getenv("TERMINAL_ENV", "local").strip().lower() or "local"
|
||||
if terminal_env != "local":
|
||||
return False
|
||||
|
||||
try:
|
||||
probe = subprocess.run(
|
||||
["sudo", "-n", "true"],
|
||||
stdin=subprocess.DEVNULL,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
timeout=3,
|
||||
check=False,
|
||||
)
|
||||
return probe.returncode == 0
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def _rewrite_compound_background(command: str) -> str:
|
||||
"""Wrap `A && B &` (or `A || B &`) to `A && { B & }` at depth 0.
|
||||
|
||||
@@ -833,6 +859,15 @@ def _transform_sudo_command(command: str | None) -> tuple[str | None, str | None
|
||||
else _get_cached_sudo_password()
|
||||
)
|
||||
|
||||
# Local hosts with sudoers NOPASSWD should not be forced through the
|
||||
# interactive Hermes password prompt or the sudo -S password-pipe path.
|
||||
# Scoped to the local terminal backend so Docker/SSH/Modal/etc. can't
|
||||
# inherit host sudo state. Re-probes every call (no process-lifetime
|
||||
# cache) so an expired sudo timestamp doesn't make a later command block
|
||||
# silently without Hermes prompting.
|
||||
if not has_configured_password and not sudo_password and _sudo_nopasswd_works():
|
||||
return command, None
|
||||
|
||||
if not has_configured_password and not sudo_password and os.getenv("HERMES_INTERACTIVE"):
|
||||
sudo_password = _prompt_for_sudo_password(timeout_seconds=45)
|
||||
if sudo_password:
|
||||
|
||||
Reference in New Issue
Block a user