diff --git a/tools/spill_safety.py b/tools/spill_safety.py index 69b68ec172..0f2d12ff85 100644 --- a/tools/spill_safety.py +++ b/tools/spill_safety.py @@ -1,17 +1,11 @@ -"""Symlink-safe creation helpers for spill/cache files. - -Spill files live in predictable directories under ``~/.hermes``; a plain -``open(path, "w")`` there would follow a pre-planted symlink and let a local -process redirect the write onto ``~/.bashrc``, ``authorized_keys``, etc. Every -helper refuses symlinks by construction: new files use ``O_CREAT | O_EXCL`` -(fails on ANY existing path, including a dangling link); overwrites ``lstat`` + -``unlink`` the existing path first (removes the link, never its target), then -create exclusively, so the pair can't be raced. ``private=True`` (default) -forces ``0o700`` dirs / ``0o600`` files for spills that may hold pre-redaction -secrets; ``private=False`` keeps umask perms for cache dirs bind-mounted into -remote backends (``credential_files._CACHE_DIRS``) where a non-root container -UID must read them. Disk failures are the caller's concern: helpers raise ``OSError``. -""" +"""Symlink-safe creation helpers for spill/cache files under ``~/.hermes``, where a +plain ``open(path, "w")`` would follow a pre-planted symlink onto ``~/.bashrc`` etc. +New files use ``O_CREAT | O_EXCL`` (fails on ANY existing path, even a dangling +link); overwrites ``lstat`` + ``unlink`` first (removes the link, never its target) +then create exclusively, so the pair can't be raced. ``private=True`` (default) = +``0o700`` dirs / ``0o600`` files for spills that may hold pre-redaction secrets; +``private=False`` keeps umask perms for cache dirs bind-mounted into remote backends +(``credential_files._CACHE_DIRS``). Disk failures raise ``OSError`` to the caller.""" from __future__ import annotations diff --git a/tools/tip_tool.py b/tools/tip_tool.py index c5dd4eb76b..c9edcf40c1 100644 --- a/tools/tip_tool.py +++ b/tools/tip_tool.py @@ -1,13 +1,8 @@ #!/usr/bin/env python3 -"""Point at something in the Hermes desktop GUI and say one line about it. - -The quiet sibling of ``tour``: same ``data-tour`` handles and discovery call, -but an arrow bubble with no scrim/spotlight/paging. Fire-and-forget — a tip is -not a question, so blocking the turn on a round-trip would stall the reply. - -Lives in ``desktop_ui`` (GUI sessions only) and withdraws itself when the user -turns tips off, so the model is never offered a tool whose call would fail. -""" +"""Point at something in the Hermes desktop GUI and say one line about it — the quiet +sibling of ``tour`` (same ``data-tour`` handles) with no scrim/spotlight/paging. +Fire-and-forget: a tip is not a question, so blocking on a round-trip would stall the +reply. Lives in ``desktop_ui`` and withdraws itself when the user turns tips off.""" import json diff --git a/tools/tool_backend_helpers.py b/tools/tool_backend_helpers.py index 5a8aba8fc1..e7bbc0036c 100644 --- a/tools/tool_backend_helpers.py +++ b/tools/tool_backend_helpers.py @@ -18,18 +18,14 @@ _VALID_MODAL_MODES = {"auto", "direct", "managed"} def managed_nous_tools_enabled(*, force_fresh: bool = False) -> bool: - """True when the user is entitled to the Nous Tool Gateway (coarse gate: - paid Portal service access OR a live free tool pool). Fails closed on - unknown/error entitlement — never blocks startup. Per-category coverage is - narrowed by callers via ``tool_gateway_entitled_for``. ``force_fresh=True`` - is for interactive flows that must see a just-purchased grant.""" + """Coarse gate: entitled to the Nous Tool Gateway (paid Portal access OR a live free + pool). Fails closed on unknown/error — never blocks startup. Callers narrow per category + via ``tool_gateway_entitled_for``; ``force_fresh`` is for flows needing a just-bought grant.""" try: from hermes_cli.nous_account import get_nous_portal_account_info - if force_fresh: - account_info = get_nous_portal_account_info(force_fresh=True) - else: - account_info = get_nous_portal_account_info() + account_info = (get_nous_portal_account_info(force_fresh=True) if force_fresh + else get_nous_portal_account_info()) return bool(account_info.logged_in) and account_info.tool_gateway_entitled except Exception: return False diff --git a/tools/tool_output_limits.py b/tools/tool_output_limits.py index 8b670aa289..a2fc8500ca 100644 --- a/tools/tool_output_limits.py +++ b/tools/tool_output_limits.py @@ -1,15 +1,8 @@ -"""Configurable tool-output truncation limits (``tool_output`` in config.yaml). - -Centralises the caps once hardcoded in ``terminal_tool`` (``max_bytes``) and -``file_operations`` (``max_lines`` / ``max_line_length``). Defaults equal the -old constants and the reader never raises, so behaviour is unchanged when the -section is absent or malformed:: - - tool_output: - max_bytes: 100000 # terminal output cap (chars) - max_lines: 5000 # read_file pagination + truncation cap - max_line_length: 2000 # per-line cap before '... [truncated]' -""" +"""Configurable tool-output truncation limits (``tool_output`` in config.yaml): +``max_bytes`` (terminal output cap), ``max_lines`` (read_file pagination cap), +``max_line_length`` (per-line cap before '... [truncated]'). Defaults equal the +constants once hardcoded in terminal_tool / file_operations and the reader never +raises, so behaviour is unchanged when the section is absent or malformed.""" from __future__ import annotations diff --git a/tools/tool_result_storage.py b/tools/tool_result_storage.py index 775fcc4ca2..91bfef712d 100644 --- a/tools/tool_result_storage.py +++ b/tools/tool_result_storage.py @@ -1,15 +1,11 @@ """Tool result persistence -- preserves large outputs instead of truncating. -Three layers against context-window overflow: (1) per-tool output caps inside -each tool; (2) ``maybe_persist_tool_result`` — output over the tool's threshold -is persisted and replaced in-context by a preview + path. The canonical home is -ALWAYS host-side ``$HERMES_HOME/cache/spillover/{tool_use_id}.txt`` (works for -MCP-only/cron/gateway sessions that never ran a terminal); remote backends see -the translated in-sandbox path (``cache/spillover`` is in the auto-mounted cache -list), probed for readability, else a copy written into the sandbox temp dir. -(3) ``enforce_turn_budget`` — spills the largest results of a turn until the -aggregate fits. -""" +Layers against context overflow: (1) per-tool caps inside each tool; (2) +``maybe_persist_tool_result`` — output over the tool's threshold is persisted and +replaced by a preview + path. Canonical home is ALWAYS host-side +``$HERMES_HOME/cache/spillover/{id}.txt`` (works for sessions that never ran a +terminal); remote backends get the translated in-sandbox path (probed for +readability) else a copy in the sandbox temp dir. (3) ``enforce_turn_budget``.""" import hashlib import logging diff --git a/tools/tool_search.py b/tools/tool_search.py index 333683a070..3695be2f9e 100644 --- a/tools/tool_search.py +++ b/tools/tool_search.py @@ -1,15 +1,11 @@ """Progressive tool disclosure ("tool search"): MCP/plugin tools and a curated set of event-triggered core tools are replaced in the model-visible array by three -bridge tools — tool_search / tool_describe / tool_call. - -Invariants: working-set core tools (``toolsets._HERMES_CORE_TOOLS``) and -session-gated GUI toolsets never defer unless named in ``defer``; ANY deferrable -tool activates the bridge (the listing scales with budget, not activation); the -catalog is stateless — rebuilt from the live tool-defs on every assembly (a -session-keyed catalog drifts from the registry and silently drops tools); bridge -calls route through ``model_tools.handle_function_call`` so guardrails, hooks, -approvals and truncation fire identically. -""" +bridge tools — tool_search / tool_describe / tool_call. Invariants: core tools +(``toolsets._HERMES_CORE_TOOLS``) and session-gated GUI toolsets never defer unless +named in ``defer``; ANY deferrable tool activates the bridge (the listing scales +with budget, not activation); the catalog is stateless — rebuilt from the live +tool-defs every assembly (a session-keyed one drifts and silently drops tools); +bridge calls route through ``model_tools.handle_function_call`` (same guardrails).""" from __future__ import annotations @@ -111,6 +107,7 @@ def _safe_float(value: Any, fallback: float) -> float: def _config_from_loader(loader_name: str) -> ToolSearchConfig: + """Tool-search config via ``hermes_cli.config.`` (defaults on any failure).""" try: import hermes_cli.config as _cfg_mod cfg = getattr(_cfg_mod, loader_name)() or {} @@ -122,12 +119,11 @@ def _config_from_loader(loader_name: str) -> ToolSearchConfig: def load_config() -> ToolSearchConfig: - """Load tool-search config from the user config file.""" return _config_from_loader("load_config") def load_config_readonly() -> ToolSearchConfig: - """Load tool-search config without copying the cached full config.""" + """Same as ``load_config`` without copying the cached full config.""" return _config_from_loader("load_config_readonly") @@ -221,11 +217,9 @@ def should_activate( config: ToolSearchConfig, deferrable_tokens: int, context_length: Optional[int]) -> bool: - """``"off"`` never activates; ``"on"``/``"auto"`` activate whenever any - deferrable tool exists. ``"auto"`` is an alias of ``"on"`` reserved for a - future budget-gated mode — do not distinguish them without that design. - ``context_length`` stays in the signature for caller compatibility; the - threshold governs the listing budget, not activation.""" + """``"off"`` never activates; ``"on"``/``"auto"`` activate whenever any deferrable + tool exists ("auto" is reserved for a future budget-gated mode — do not distinguish + them without that design). ``context_length`` is kept for caller compatibility.""" return config.enabled != "off" and deferrable_tokens > 0 @@ -363,9 +357,8 @@ def assemble_tool_defs( *, context_length: Optional[int] = None, config: Optional[ToolSearchConfig] = None) -> AssemblyResult: - """Return the tool-defs list the model should actually see: passthrough - when inactive, otherwise deferrable tools replaced by the three bridge - tools. Idempotent — bridge tools already present are stripped first.""" + """Tool-defs the model should see: passthrough when inactive, else deferrable tools + replaced by the three bridge tools. Idempotent — existing bridge tools are stripped first.""" if config is None: config = load_config() incoming = [td for td, name in zip(tool_defs, _tool_def_names(tool_defs)) @@ -404,9 +397,8 @@ def is_bridge_tool(name: str) -> bool: def _shared_tool_record(entry: CatalogEntry) -> Dict[str, Any]: - """One record for the response's shared ``tools`` map (held once per tool; - per-query groups carry names only). ``required`` lets the model attempt a - trivial call without a ``tool_describe`` round-trip.""" + """One record for the shared ``tools`` map (per-query groups carry names only); + ``required`` lets the model attempt a trivial call without a ``tool_describe`` round-trip.""" schema = entry.schema if isinstance(entry.schema, dict) else {} fn = schema.get("function") params = fn.get("parameters") if isinstance(fn, dict) else None @@ -430,9 +422,8 @@ def _available_source_summary(catalog: List[CatalogEntry]) -> List[Dict[str, Any def _string_list_arg( args: Dict[str, Any], key: str, *, dedupe: bool, max_items: int, retry_hint: str, ) -> Tuple[Optional[List[str]], Optional[str]]: - """Read a list-of-strings bridge argument -> ``(items, error_json)``. A bare - string (a common model slip) counts as a one-item list. Rejects non-list - input, an empty list (after stripping blanks), and more than ``max_items``.""" + """Read a list-of-strings bridge argument -> ``(items, error_json)``. A bare string (a + common model slip) is a one-item list; rejects non-lists, all-blank lists, > ``max_items``.""" raw = args.get(key) if isinstance(raw, str): raw = [raw] @@ -552,10 +543,9 @@ def dispatch_tool_describe(args: Dict[str, Any], def scoped_deferrable_names(tool_defs: List[Dict[str, Any]]) -> frozenset[str]: - """Deferrable tool names in the *pre-assembly* ``tool_defs`` of the session's - toolset scope — the universe ``tool_call`` may reach. Gates both bridge - dispatch and the executor unwrap so a restricted session cannot invoke an - out-of-scope tool via the bridge.""" + """Deferrable names in the *pre-assembly* ``tool_defs`` of the session scope — the + universe ``tool_call`` may reach. Gates bridge dispatch AND the executor unwrap so a + restricted session cannot invoke an out-of-scope tool via the bridge.""" defer_tools = load_config_readonly().effective_defer_tools return frozenset( name for name in _tool_def_names(tool_defs) @@ -563,9 +553,8 @@ def scoped_deferrable_names(tool_defs: List[Dict[str, Any]]) -> frozenset[str]: def resolve_underlying_call(args: Dict[str, Any]) -> Tuple[Optional[str], Dict[str, Any], Optional[str]]: - """Parse a ``tool_call`` invocation into (underlying_name, args, error_msg); - ``(None, {}, msg)`` on parse error. Shared by dispatch, display, and the - trajectory recorder so all three agree on the underlying tool.""" + """Parse a ``tool_call`` invocation -> (underlying_name, args, error_msg); ``(None, {}, msg)`` + on error. Shared by dispatch, display and the trajectory recorder so all three agree.""" name = str(args.get("name") or "").strip() if not name: return None, {}, "tool_call requires a 'name' argument" diff --git a/tools/tour_tool.py b/tools/tour_tool.py index 0422443d8e..b1d4022f06 100644 --- a/tools/tour_tool.py +++ b/tools/tour_tool.py @@ -1,14 +1,11 @@ #!/usr/bin/env python3 -"""Guided tour (highlight + narrate UI elements) in the Hermes desktop GUI. - -Generic, no baked-in tours: the agent discovers targets (``action="targets"``), -then highlights by CSS selector one step at a time (``show``) or hands over a -step list the user pages with Next/Prev (``start``). Round-trips through the -gateway blocking-prompt bridge (``tour.request``/``tour.respond``) so the agent -learns whether the selector matched. Lives in ``desktop_ui`` and withdraws itself -when the user turns tours off: a tour takes the whole screen, so "off" must mean -the model is never told the tool exists rather than offered a call that fails. -""" +"""Guided tour (highlight + narrate UI elements) in the Hermes desktop GUI. Generic: +the agent discovers targets (``action="targets"``), then highlights one step at a +time (``show``) or hands over a step list the user pages (``start``). Round-trips +through the gateway blocking-prompt bridge (``tour.request``/``tour.respond``) so the +agent learns whether the selector matched. Lives in ``desktop_ui`` and withdraws +itself when tours are off: a tour takes the whole screen, so "off" must mean the +model is never told the tool exists rather than offered a call that fails.""" import json from typing import Callable, Optional