refactor(agent/terminal_env_*,transcription_*): compact ABC/registry docs, drop unused logger
This commit is contained in:
@@ -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.
|
||||
"""
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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.
|
||||
"""
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user