4b6817381b
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
CI / coverage-report (push) Has been cancelled
CI / test-kubernetes (push) Has been cancelled
CI / should-run-thorough (push) Has been cancelled
CI / test-thorough (cloudwatch-demo) (push) Has been cancelled
CI / test-thorough (flink-ecs) (push) Has been cancelled
CI / test-thorough (upstream-lambda) (push) Has been cancelled
CI / test-thorough (prefect-ecs-fargate) (push) Has been cancelled
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Has been cancelled
Benchmark image — build + push to ECR (any adapter) / build + push (push) Has been cancelled
CI / quality (ubuntu-latest) (push) Has been cancelled
CI / test (tools-runtime) (push) Has been cancelled
CI / test (e2e-general) (push) Has been cancelled
CI / test (cli-runtime) (push) Has been cancelled
CI / test (e2e-provider-and-openclaw) (push) Has been cancelled
CI / test (integrations-and-misc) (push) Has been cancelled
Release / verify (push) Has been cancelled
Release / build-python-dist (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Has been cancelled
Release / publish-release (push) Has been cancelled
Release / publish-main-release (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Has been cancelled
Release / prepare (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Has been cancelled
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Has been cancelled
171 lines
7.0 KiB
Python
171 lines
7.0 KiB
Python
"""Gateway process entrypoint and lifecycle owner.
|
|
|
|
``GatewayManager`` is the composition root for the OpenSRE background agent:
|
|
it assembles the transport-agnostic turn handler from a booted session's tools
|
|
and starts every daemon component — the web health app, the Telegram chat
|
|
worker (when configured), and the scheduled-task runner — then owns the
|
|
process lifecycle (signals, ``stop``/``wait``). Component states are published
|
|
through :func:`gateway.daemon.write_component_status` so the CLI and the
|
|
interactive shell can report status. It holds no Telegram or agent-dispatch
|
|
logic itself — those live in :mod:`gateway.turn_handler` and
|
|
:mod:`gateway.telegram_gateway`.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import os
|
|
import signal
|
|
import threading
|
|
from typing import Any
|
|
|
|
from rich.console import Console
|
|
|
|
from core.agent_harness.harness import AgentHarness, HarnessConfig
|
|
from core.llm.internal.preload import preload_llm_clients
|
|
from gateway.config.configure_gateway_logging import configure_gateway_logging
|
|
from gateway.config.get_gateway_settings import GatewayConfigurationError, GatewaySettings
|
|
from gateway.daemon import (
|
|
GATEWAY_PID_FILE,
|
|
clear_component_status,
|
|
write_component_status,
|
|
)
|
|
from gateway.polling.telegram_gateway_background import TelegramGatewayBackground
|
|
from gateway.telegram_gateway import start_telegram_worker
|
|
from gateway.turn_handler import GatewayTurnHandler
|
|
|
|
|
|
class GatewayManager:
|
|
"""Composition root and lifecycle handle for the running gateway process."""
|
|
|
|
def __init__(self) -> None:
|
|
self.settings: GatewaySettings | None = None
|
|
self.logger: logging.Logger | None = None
|
|
self.telegram_background_worker: TelegramGatewayBackground | None = None
|
|
self.web_server: Any = None
|
|
self.scheduler: Any = None
|
|
self.components: dict[str, str] = {}
|
|
self._stopped = threading.Event()
|
|
|
|
def start_gateway(self, *, wait: bool = True) -> GatewayManager:
|
|
"""Assemble the turn handler, start all components, and own the lifecycle."""
|
|
from integrations.harness_adapters import register_harness_adapters as register_integrations
|
|
from tools.harness_adapters import register_harness_adapters as register_tools
|
|
|
|
harness = AgentHarness(HarnessConfig(open_storage=False))
|
|
harness.resolve_env_variables()
|
|
# Mirror shell boot: register harness adapters here (gateway cannot import
|
|
# surfaces.boundary without a surfaces↔gateway peer import).
|
|
register_integrations()
|
|
register_tools()
|
|
# Env-gated (OPENSRE_NO_TELEMETRY / DO_NOT_TRACK / missing DSN) — free when off.
|
|
from platform.observability.errors.sentry import init_sentry
|
|
|
|
init_sentry(entrypoint="gateway")
|
|
logger = self.logger = configure_gateway_logging()
|
|
|
|
# Load the LLM client graph as one snapshot at boot (avoids a stale
|
|
# mixed-version process after a code change).
|
|
preload_llm_clients()
|
|
|
|
# Compose the transport-agnostic turn handler. Action tools are resolved
|
|
# per turn from each chat's live session inside the handler (not here).
|
|
console = Console(force_terminal=False)
|
|
handler = GatewayTurnHandler(console=console)
|
|
|
|
self._start_web(logger)
|
|
self._start_telegram(logger, handler)
|
|
self._start_scheduler(logger)
|
|
self._publish_status(logger)
|
|
|
|
signal.signal(signal.SIGINT, self._handle_signal)
|
|
signal.signal(signal.SIGTERM, self._handle_signal)
|
|
|
|
if wait:
|
|
self.wait()
|
|
return self
|
|
|
|
def stop(self, *, timeout: float = 8.0) -> bool:
|
|
"""Shut down all components and return whether the chat worker stopped."""
|
|
if self.scheduler is not None:
|
|
self.scheduler.shutdown(wait=False)
|
|
self.scheduler = None
|
|
if self.web_server is not None:
|
|
self.web_server.stop()
|
|
self.web_server = None
|
|
stopped = True
|
|
if self.telegram_background_worker is not None:
|
|
stopped = self.telegram_background_worker.stop(timeout=timeout)
|
|
clear_component_status()
|
|
self._stopped.set()
|
|
return stopped
|
|
|
|
def wait(self, *, timeout: float | None = None) -> bool:
|
|
"""Wait until shutdown is requested and return whether the gateway has stopped."""
|
|
return self._stopped.wait(timeout)
|
|
|
|
def _start_web(self, logger: logging.Logger) -> None:
|
|
"""Serve the shared web app (health probes + alert intake) in a daemon thread."""
|
|
from core.domain.alerts.inbox import AlertInbox, set_current_inbox
|
|
from gateway.web_server import serve_webapp_in_thread
|
|
|
|
set_current_inbox(AlertInbox())
|
|
port = int(os.environ.get("PORT", "8000"))
|
|
try:
|
|
handle = serve_webapp_in_thread(host="0.0.0.0", port=port)
|
|
except RuntimeError as exc:
|
|
logger.warning("web app disabled: %s", exc)
|
|
self.components["web"] = f"failed ({exc})"
|
|
return
|
|
self.web_server = handle
|
|
self.components["web"] = f"serving http://{handle.bound_address} (health, alerts)"
|
|
logger.info("web app serving on http://%s", handle.bound_address)
|
|
|
|
def _start_telegram(self, logger: logging.Logger, handler: Any) -> None:
|
|
"""Start the Telegram chat worker; run without it when not configured."""
|
|
try:
|
|
worker, settings = start_telegram_worker(logger=logger, handler=handler)
|
|
except GatewayConfigurationError as exc:
|
|
logger.warning("Telegram chat disabled: %s", exc)
|
|
self.components["telegram"] = f"not configured ({exc})"
|
|
return
|
|
self.settings = settings
|
|
self.telegram_background_worker = worker
|
|
self.components["telegram"] = "polling for messages"
|
|
|
|
def _start_scheduler(self, _logger: logging.Logger) -> None:
|
|
"""Run cron-scheduled tasks inside the daemon (no separate process needed)."""
|
|
from platform.scheduler.runner import start_background_scheduler
|
|
from tools.investigation.scheduler_bootstrap import install as install_scheduler_runner
|
|
|
|
install_scheduler_runner()
|
|
scheduler, task_count = start_background_scheduler()
|
|
if scheduler is None:
|
|
self.components["scheduler"] = "idle (no scheduled tasks)"
|
|
return
|
|
self.scheduler = scheduler
|
|
self.components["scheduler"] = f"running {task_count} scheduled task(s)"
|
|
|
|
def _publish_status(self, logger: logging.Logger) -> None:
|
|
GATEWAY_PID_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
GATEWAY_PID_FILE.write_text(f"{os.getpid()}\n")
|
|
write_component_status(self.components)
|
|
for name, detail in self.components.items():
|
|
logger.info("component %s: %s", name, detail)
|
|
|
|
def _handle_signal(self, *_args: object) -> None:
|
|
self.stop()
|
|
|
|
|
|
def start_gateway(*, wait: bool = True) -> GatewayManager:
|
|
"""Compatibility wrapper for existing CLI/import callers."""
|
|
return GatewayManager().start_gateway(wait=wait)
|
|
|
|
|
|
def main() -> None:
|
|
GatewayManager().start_gateway()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|