- 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
151 lines
6.3 KiB
Python
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.
|
|
"""
|