refactor(agent/terminal_env_*,transcription_*): compact ABC/registry docs, drop unused logger

This commit is contained in:
Teknium
2026-09-02 18:38:29 -07:00
parent 0e1e86bef4
commit c50e14980f
4 changed files with 74 additions and 149 deletions
+37 -62
View File
@@ -1,43 +1,12 @@
"""
Terminal Environment Provider ABC
=================================
"""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.
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 (built-ins stay in ``tools/environments/``;
third-party sandbox vendors do NOT have to live in core). :meth:`create_environment` returns
any ``BaseEnvironment`` duck type (``execute()``, ``cleanup()`` …); the factory stamps
``_hermes_backend_name`` on the result so file-path resolution can identify plugin backends.
"""
from __future__ import annotations
@@ -51,8 +20,24 @@ 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.
:attr:`name` is the ``terminal.backend`` / ``TERMINAL_ENV`` value (``[a-z0-9_]``); the
registry rejects built-in backend names.
Classification contract — each core policy decision that was historically a frozenset of
built-in names is a declarative attribute so a new backend cannot silently miss one:
* ``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
passed through, host-looking cwds sanitized, file tools use container path resolution.
* ``skip_container_guards`` — sandbox disposable enough to skip dangerous-command approval
prompts. Defaults to ``is_container``; backends that can mount host paths 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 destroy each other.
"""
@property
@@ -60,31 +45,26 @@ class TerminalEnvironmentProvider(ProviderBase):
"""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)"``)."""
"""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 -------------------------------------------
@@ -95,9 +75,8 @@ class TerminalEnvironmentProvider(ProviderBase):
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."""
"""Full requirements check for :func:`check_terminal_requirements` with the merged
terminal env config; log actionable errors before returning False."""
return self.is_available()
def probe(self) -> Tuple[str, str]:
@@ -108,18 +87,15 @@ class TerminalEnvironmentProvider(ProviderBase):
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`."""
"""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."""
"""Optional interactive hook run by ``hermes setup`` after selection (prompt for tokens, install SDKs)."""
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:
@@ -142,9 +118,8 @@ class TerminalEnvironmentProvider(ProviderBase):
):
"""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.
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.
"""
+14 -32
View File
@@ -1,26 +1,13 @@
"""
Terminal Environment Registry
=============================
"""Terminal Environment Registry.
Central map of registered pluggable terminal backends. Populated by plugins
at load time via :meth:`PluginContext.register_terminal_environment_provider`;
consumed by :func:`tools.terminal_tool._create_environment` and the
classification helpers spread across the terminal/file/approval/prompt
surfaces.
Unlike the image/video/web/browser registries there is **no active-provider
resolution here**: the active backend is whatever ``TERMINAL_ENV`` /
``terminal.backend`` names, exactly as for built-in backends. The registry's
only job is mapping that name to a provider instance (and answering the
classification questions the core historically answered with frozensets of
built-in names).
Built-in backend names are reserved — :func:`register_provider` rejects a
provider whose ``name`` collides with one, so a plugin can never shadow the
in-tree docker/modal/... implementations.
Mirrors :mod:`agent.browser_registry` scope semantics: providers register
into a per-profile scope (multiplexed gateways) or the global base map.
Central map of registered pluggable terminal backends, populated by plugins via
:meth:`PluginContext.register_terminal_environment_provider` and consumed by
:func:`tools.terminal_tool._create_environment` plus the classification helpers across the
terminal/file/approval/prompt surfaces. Unlike the image/video/web/browser registries there
is **no active-provider resolution**: the active backend is whatever ``TERMINAL_ENV`` /
``terminal.backend`` names. Built-in names are reserved (registration raises) so a plugin can
never shadow the in-tree docker/modal/... implementations. Scope semantics mirror
:mod:`agent.browser_registry` (per-profile scope or the global base map).
"""
from __future__ import annotations
@@ -68,9 +55,8 @@ def plugin_backend_names(*, scope: Optional[str] = None) -> List[str]:
def provider_flag(name: str, attr: str, default=False):
"""Read a classification attribute off the provider for *name*.
Fail-soft: unknown backend or a raising property returns *default* so a
misbehaving plugin degrades to built-in-equivalent behavior instead of
taking the terminal tool down.
Fail-soft: unknown backend or a raising property returns *default* so a misbehaving
plugin degrades to built-in-equivalent behavior instead of taking the terminal tool down.
"""
provider = _registry.get_provider(name)
if provider is None:
@@ -86,13 +72,9 @@ def provider_flag(name: str, attr: str, default=False):
def plugin_strip_env_keys() -> frozenset:
"""Union of every registered provider's ``strip_env_keys``.
Secrets are stripped for ALL registered backends, not just the active
one — a token in the process environment is strippable regardless of
which backend is selected (mirrors how MODAL_*/DAYTONA_API_KEY sit in
the static tier-1 set unconditionally).
"""
"""Union of every registered provider's ``strip_env_keys`` — across ALL scopes, not
just the active backend: a token in the process environment is strippable regardless of
which backend is selected (as MODAL_*/DAYTONA_API_KEY sit in the static tier-1 set)."""
keys: set = set()
with _registry._lock:
all_providers = list(_registry._providers.values())
+12 -29
View File
@@ -1,34 +1,20 @@
"""
Transcription Provider ABC
==========================
"""Transcription Provider ABC — pluggable speech-to-text backends.
Pluggable-backend interface for speech-to-text. Providers register via
:meth:`PluginContext.register_transcription_provider`; the one named by
``stt.provider`` services :func:`tools.transcription_tools.transcribe_audio`
**when that name is not a built-in**. Built-ins (``BUILTIN_STT_PROVIDERS`` in
:mod:`tools.transcription_tools`) always win: the registry rejects colliding
names at registration and the dispatcher re-checks at dispatch time. The
``HERMES_LOCAL_STT_COMMAND`` shell escape hatch stays on the built-in
``local_command`` path.
Response contract for :meth:`TranscriptionProvider.transcribe`::
success bool
transcript str transcribed text (empty when success=False)
provider str provider name (for diagnostics)
error str only when success=False
Providers register via :meth:`PluginContext.register_transcription_provider`; the one named
by ``stt.provider`` services :func:`tools.transcription_tools.transcribe_audio` **when that
name is not a built-in** (built-ins always win; ``HERMES_LOCAL_STT_COMMAND`` stays on the
built-in ``local_command`` path). :meth:`TranscriptionProvider.transcribe` envelope:
``success`` bool, ``transcript`` str (empty on failure), ``provider`` str, ``error`` str
(only when success=False).
"""
from __future__ import annotations
import abc
import logging
from typing import Any, Dict, Optional
from agent.provider_base import CatalogProviderBase
logger = logging.getLogger(__name__)
class TranscriptionProvider(CatalogProviderBase):
"""Abstract base class for a speech-to-text backend.
@@ -46,13 +32,10 @@ class TranscriptionProvider(CatalogProviderBase):
language: Optional[str] = None,
**extra: Any,
) -> Dict[str, Any]:
"""Transcribe the audio file at ``file_path`` into the module-docstring envelope.
"""Transcribe ``file_path`` (existence + size already validated) into the module envelope.
Implementations should NOT raise — convert exceptions to the error
envelope so the gateway/CLI caller always gets a consistent shape. The
dispatcher has already validated existence + size. ``model`` None →
:meth:`default_model`; ``language`` is an optional BCP-47 hint. The
dispatcher forwards ``prompt`` in ``extra`` when ``stt.prompt`` or a
``pre_transcription`` hook sets one — prompt-capable providers may use
it as a vocabulary hint; unknown keys must be ignored.
Must NOT raise — convert exceptions to the error envelope. ``model`` None →
:meth:`default_model`; ``language`` is an optional BCP-47 hint; ``extra`` may carry
``prompt`` (from ``stt.prompt`` or a ``pre_transcription`` hook) as a vocabulary hint;
unknown keys must be ignored.
"""
+11 -26
View File
@@ -1,16 +1,10 @@
"""
Transcription Provider Registry
================================
"""Transcription Provider Registry.
Central map of registered STT providers. Populated by plugins at import-time
via :meth:`PluginContext.register_transcription_provider`; consumed by
:mod:`tools.transcription_tools` to dispatch :func:`transcribe_audio` calls
to the active plugin backend **when** the configured ``stt.provider`` name is
not a built-in.
Built-ins-always-win: a plugin name colliding with a built-in STT provider is
rejected at registration with a warning (re-checked at dispatch time in
:func:`tools.transcription_tools._dispatch_to_plugin_provider`).
Central map of registered STT providers, populated by plugins via
:meth:`PluginContext.register_transcription_provider` and consumed by
:mod:`tools.transcription_tools` to dispatch :func:`transcribe_audio` to the active plugin
backend **when** ``stt.provider`` is not a built-in. Built-ins always win: a colliding name is
rejected at registration with a warning (re-checked at dispatch time).
"""
from __future__ import annotations
@@ -23,19 +17,11 @@ from agent.transcription_provider import TranscriptionProvider
logger = logging.getLogger(__name__)
# Names reserved for native built-in STT handlers. **Kept in sync with
# ``BUILTIN_STT_PROVIDERS`` in :mod:`tools.transcription_tools`** (a regression
# test in ``tests/agent/test_transcription_registry.py::TestBuiltinSync`` fails
# on drift); importing it directly would be a circular import.
# Native built-in STT handlers. **Kept in sync with ``BUILTIN_STT_PROVIDERS`` in
# :mod:`tools.transcription_tools`** (TestBuiltinSync fails on drift); importing it
# directly would be a circular import.
_BUILTIN_NAMES = frozenset({
"local",
"local_command",
"groq",
"openai",
"mistral",
"xai",
"elevenlabs",
"deepinfra",
"local", "local_command", "groq", "openai", "mistral", "xai", "elevenlabs", "deepinfra",
})
@@ -48,8 +34,7 @@ def _warn_builtin_collision(key: str) -> None:
)
# Case-insensitive, whitespace-tolerant keys mirror how
# ``tools.transcription_tools`` normalizes the configured ``stt.provider``.
# Case-insensitive, whitespace-tolerant keys mirror ``tools.transcription_tools``.
_registry: ProviderRegistry[TranscriptionProvider] = ProviderRegistry(
label="Transcription",
provider_cls=TranscriptionProvider,