96e952a4f8
- provider_registry.py: ProviderRegistry (global + per-scope maps, lock, generation counters, register/list/get/snapshot/restore/reset) with export() binding the historical module-level names and _providers/ _scoped_providers/_lock test hooks into each *_registry module - is_available_safe / configured_provider_name replace the 4 nested _is_available_safe closures and 2 config-reading blocks - browser/image_gen/video_gen/web_search/terminal_env/tts/transcription registries keep their public API, log strings, error strings, builtin collision policy (warn vs raise) and key normalization (strip vs lower)
110 lines
3.9 KiB
Python
110 lines
3.9 KiB
Python
"""
|
|
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.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from typing import List, Optional
|
|
|
|
from agent.provider_registry import ProviderRegistry, lower_key
|
|
from agent.terminal_env_provider import TerminalEnvironmentProvider
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
#: Names owned by in-tree backends in tools/environments/ — never
|
|
#: registrable by plugins. Includes internal-mode aliases (managed_modal).
|
|
BUILTIN_BACKEND_NAMES = frozenset({
|
|
"local", "docker", "singularity", "modal", "managed_modal",
|
|
"daytona", "vercel_sandbox", "ssh",
|
|
})
|
|
|
|
|
|
def _reject_builtin_collision(name: str) -> None:
|
|
raise ValueError(
|
|
f"Terminal backend name '{name}' is reserved for the built-in "
|
|
f"{name} backend and cannot be registered by a plugin"
|
|
)
|
|
|
|
|
|
_registry: ProviderRegistry[TerminalEnvironmentProvider] = ProviderRegistry(
|
|
label="Terminal environment",
|
|
provider_cls=TerminalEnvironmentProvider,
|
|
logger=logger,
|
|
normalize=lower_key,
|
|
builtin_names=BUILTIN_BACKEND_NAMES,
|
|
on_builtin_collision=_reject_builtin_collision,
|
|
)
|
|
_registry.export(globals())
|
|
|
|
|
|
def plugin_backend_names(*, scope: Optional[str] = None) -> List[str]:
|
|
"""Names of all registered plugin backends (sorted)."""
|
|
return [p.name.strip().lower() for p in _registry.list_providers(scope=scope)]
|
|
|
|
|
|
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.
|
|
"""
|
|
provider = _registry.get_provider(name)
|
|
if provider is None:
|
|
return default
|
|
try:
|
|
return getattr(provider, attr, default)
|
|
except Exception:
|
|
logger.debug(
|
|
"Terminal environment provider '%s' attribute '%s' raised",
|
|
name, attr, exc_info=True,
|
|
)
|
|
return default
|
|
|
|
|
|
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).
|
|
"""
|
|
keys: set = set()
|
|
with _registry._lock:
|
|
all_providers = list(_registry._providers.values())
|
|
for scoped in _registry._scoped_providers.values():
|
|
all_providers.extend(scoped.values())
|
|
for provider in all_providers:
|
|
try:
|
|
keys.update(provider.strip_env_keys)
|
|
except Exception:
|
|
logger.debug(
|
|
"Terminal environment provider strip_env_keys raised",
|
|
exc_info=True,
|
|
)
|
|
return frozenset(keys)
|