feat: implement skeleton phase
This commit is contained in:
1 parent
75a23d231e
commit
221e660c5c
21 files changed
+586
-3
No files matched your search
@@ -0,0 +1,99 @@
|
||||
"""Load the user's ``/config/config.py``.
|
||||
|
||||
The config file is regular Python. We ``exec`` it in a namespace seeded
|
||||
with the public surface the user is expected to use (``ClaudeAgent``,
|
||||
``RaycastAgent``, ``McpServer``, ``ExposedMcp``, ``Gateway``). The file
|
||||
must assign a top-level ``gateway = Gateway(...)``.
|
||||
|
||||
Element-level validation already happens at construction time — agents
|
||||
and ``McpServer`` factories are pydantic models that reject garbage.
|
||||
What we *can't* validate at construction is the ``Gateway`` container
|
||||
itself (deliberately a plain dataclass per PRD), so we type-check its
|
||||
contents here before handing it back.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from beaver_gateway.agents.base import BaseAgent, ExposedMcp
|
||||
from beaver_gateway.agents.claude import ClaudeAgent
|
||||
from beaver_gateway.agents.raycast import RaycastAgent
|
||||
from beaver_gateway.core.registry import Gateway
|
||||
from beaver_gateway.frontends.base import Frontend
|
||||
from beaver_gateway.mcp.types import HttpMcp, McpServer, PythonToolMcp, StdioMcp
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class ConfigError(Exception):
|
||||
"""User config file is missing, unreadable, or structurally wrong."""
|
||||
|
||||
|
||||
_PUBLIC_NAMES: dict[str, Any] = {
|
||||
"ClaudeAgent": ClaudeAgent,
|
||||
"RaycastAgent": RaycastAgent,
|
||||
"McpServer": McpServer,
|
||||
"ExposedMcp": ExposedMcp,
|
||||
"Gateway": Gateway,
|
||||
}
|
||||
|
||||
_McpInstance = StdioMcp | HttpMcp | PythonToolMcp
|
||||
|
||||
|
||||
def load(path: Path) -> Gateway:
|
||||
"""Execute ``path`` and return its top-level ``gateway`` object."""
|
||||
try:
|
||||
source = path.read_text(encoding="utf-8")
|
||||
except FileNotFoundError as exc:
|
||||
msg = f"config file not found: {path}"
|
||||
raise ConfigError(msg) from exc
|
||||
except OSError as exc:
|
||||
msg = f"could not read config file {path}: {exc}"
|
||||
raise ConfigError(msg) from exc
|
||||
|
||||
code = compile(source, str(path), "exec")
|
||||
namespace: dict[str, Any] = {"__file__": str(path), **_PUBLIC_NAMES}
|
||||
exec(code, namespace) # noqa: S102 - exec'ing user config is the feature
|
||||
|
||||
try:
|
||||
gw = namespace["gateway"]
|
||||
except KeyError as exc:
|
||||
msg = f"{path}: no top-level `gateway = Gateway(...)` found"
|
||||
raise ConfigError(msg) from exc
|
||||
|
||||
if not isinstance(gw, Gateway):
|
||||
msg = (
|
||||
f"{path}: top-level `gateway` must be a Gateway instance, "
|
||||
f"got {type(gw).__name__}"
|
||||
)
|
||||
raise ConfigError(msg)
|
||||
|
||||
_validate(gw, path)
|
||||
return gw
|
||||
|
||||
|
||||
def _validate(gw: Gateway, path: Path) -> None:
|
||||
for i, a in enumerate(gw.agents):
|
||||
if not isinstance(a, BaseAgent):
|
||||
msg = (
|
||||
f"{path}: gateway.agents[{i}] must be a ClaudeAgent / "
|
||||
f"RaycastAgent instance, got {type(a).__name__}"
|
||||
)
|
||||
raise ConfigError(msg)
|
||||
for i, m in enumerate(gw.mcps):
|
||||
if not isinstance(m, _McpInstance):
|
||||
msg = (
|
||||
f"{path}: gateway.mcps[{i}] must be built via "
|
||||
f"McpServer.stdio/.http/.python_tool, "
|
||||
f"got {type(m).__name__}"
|
||||
)
|
||||
raise ConfigError(msg)
|
||||
for i, f in enumerate(gw.frontends):
|
||||
if not isinstance(f, Frontend):
|
||||
msg = (
|
||||
f"{path}: gateway.frontends[{i}] must be a Frontend, "
|
||||
f"got {type(f).__name__}"
|
||||
)
|
||||
raise ConfigError(msg)
|
||||
Reference in new issue
Block a user