Files
hermes-agent/agent/terminal_env_provider.py
Teknium 8dcb2b6ada refactor(agent/providers): shared ProviderBase/CatalogProviderBase and provider_media; compact contract docs
- provider_base.py: ProviderBase (name/display_name/get_setup_schema) and
  CatalogProviderBase (default_model/list_models/is_available) replace the
  identical default-method bodies duplicated across 7 provider ABCs
- provider_media.py: one save_b64/save_bytes/save_url/cache_dir implementation
  behind image_gen_provider and video_gen_provider
- memory_manager.py: _each_provider fan-out helper replaces per-hook
  try/except loops; _signature_params/_has_var_kwargs unify signature probes
- image_routing.py: _resolve_inference_value shared by base_url/api_key
  resolution; _dict_or_empty/_clean_str/_custom_provider_entries helpers
- MemoryProvider/ContextEngine/TTS/browser/web/terminal-env ABC docstrings
  compacted to their invariants; method names and signatures unchanged
2026-09-02 13:53:28 -07:00

151 lines
6.3 KiB
Python

"""
Terminal Environment Provider ABC
=================================
Pluggable-backend interface for terminal execution environments (cloud
sandboxes, remote runners). Providers register via
:meth:`PluginContext.register_terminal_environment_provider`;
:func:`tools.terminal_tool._create_environment` consults the registry for any
``TERMINAL_ENV`` / ``terminal.backend`` value that is not a built-in backend.
Built-ins stay in-tree under ``tools/environments/``; this extension point
exists so third-party sandbox vendors do NOT have to live in core.
Classification contract
-----------------------
A backend participates in core policy decisions that were historically
frozensets of built-in names. Each is a declarative attribute so a new backend
cannot silently miss a classification site:
* ``is_remote`` — commands run off-host: suppresses host OS/home/cwd hints in
the system prompt, the host Python probe, and remote-aware skill env handling.
* ``is_container`` — own filesystem rooted away from the host: container
resource config is passed through, host-looking cwds are sanitized, file
tools use container path resolution.
* ``skip_container_guards`` — sandbox is disposable enough to skip
dangerous-command approval prompts. Defaults to ``is_container``; backends
that can mount host paths should override to ``False``.
* ``cache_path_base`` — where auto-synced ``~/.hermes/cache`` files land inside
the backend (``"~/.hermes"``, ``"/root/.hermes"``), or ``None`` when host
paths remain correct.
* ``strip_env_keys`` — vendor credential env vars, stripped from every
subprocess the agent spawns so a model-authored command can never read them.
* ``session_isolated_when_nonpersistent`` — non-persistent mode gives each
session its own sandbox identity; opt in when a shared name would let two
ephemeral runs attach to and destroy each other's sandbox.
:meth:`create_environment` returns any object satisfying the
:class:`tools.environments.base.BaseEnvironment` duck-typed interface
(``execute()``, ``cleanup()`` …); the registry does not isinstance-check it.
The factory stamps ``_hermes_backend_name`` on the result so file-path
resolution can identify plugin backends without class-name sniffing.
"""
from __future__ import annotations
import abc
from typing import Any, Dict, List, Optional, Tuple
from agent.provider_base import ProviderBase
class TerminalEnvironmentProvider(ProviderBase):
"""Abstract base class for a pluggable terminal execution backend.
:attr:`name` is the ``terminal.backend`` / ``TERMINAL_ENV`` value
(``[a-z0-9_]``); the registry rejects built-in backend names.
"""
@property
def description(self) -> str:
"""One-line description shown in backend pickers."""
return f"Run commands in a {self.display_name} environment."
# -- Classification flags (see module docstring) -----------------------
is_remote: bool = True
is_container: bool = True
session_isolated_when_nonpersistent: bool = False
@property
def skip_container_guards(self) -> bool:
"""Whether dangerous-command approval prompts are skipped."""
return self.is_container
@property
def cache_path_base(self) -> Optional[str]:
"""Base dir for synced Hermes cache files inside the backend."""
return None
@property
def strip_env_keys(self) -> frozenset:
"""Backend-owned credential env var names to strip from subprocesses."""
return frozenset()
@property
def env_description(self) -> str:
"""Prompt-builder fallback for where commands run when the live backend
probe fails at system-prompt build time (e.g. ``"a Daytona workspace (Linux)"``)."""
return f"a {self.display_name} environment (likely Linux)"
# -- Availability / setup UX -------------------------------------------
@abc.abstractmethod
def is_available(self) -> bool:
"""True when this backend can service commands. Cheap check only — must
NOT make network calls; runs during requirement checks and UI paints."""
def check_requirements(self, config: Dict[str, Any]) -> bool:
"""Full requirements check for :func:`check_terminal_requirements` with the
merged terminal env config. Default defers to :meth:`is_available`; log
actionable errors before returning False."""
return self.is_available()
def probe(self) -> Tuple[str, str]:
"""Dashboard picker health probe ``(status, detail)`` with status in
``ready`` / ``needs_setup`` / ``unavailable``. Must never raise; stay fast (<~2s)."""
if self.is_available():
return ("ready", "")
return ("needs_setup", f"{self.display_name} is not configured.")
def setup_instructions(self) -> List[str]:
"""Lines printed by ``hermes setup`` after this backend is selected. The
wizard persists ``terminal.backend`` itself; interactive flows go in
:meth:`post_setup`."""
return []
def post_setup(self) -> None:
"""Optional interactive hook run by ``hermes setup`` after selection
(prompt for tokens, install SDKs). Default no-op."""
def doctor_checks(self) -> List[Tuple[bool, str, str]]:
"""``hermes doctor`` rows ``(ok, label, detail)``; default reflects :meth:`is_available`."""
ok = False
try:
ok = bool(self.is_available())
except Exception:
ok = False
detail = "(configured)" if ok else "(not configured — see setup instructions)"
return [(ok, f"{self.display_name} backend", detail)]
# -- The factory -------------------------------------------------------
@abc.abstractmethod
def create_environment(
self,
*,
cwd: str,
timeout: int,
task_id: str = "default",
image: Optional[str] = None,
container_config: Optional[Dict[str, Any]] = None,
**kwargs: Any,
):
"""Create and return an execution environment (``BaseEnvironment`` duck type).
MUST accept ``**kwargs`` and ignore unknown keys so the factory signature
can evolve without breaking older plugins. ``task_id`` keys environment
reuse/persistence; ``container_config`` carries ``container_cpu`` /
``container_memory`` / ``container_disk`` / ``container_persistent`` when
:attr:`is_container` is True.
"""