feat: implement skeleton phase
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
.git
|
||||
.gitignore
|
||||
.idea
|
||||
.vscode
|
||||
.venv
|
||||
.python-version
|
||||
.pre-commit-config.yaml
|
||||
docs
|
||||
tests
|
||||
examples
|
||||
**/__pycache__
|
||||
**/*.pyc
|
||||
**/.pytest_cache
|
||||
**/.ruff_cache
|
||||
**/.ty_cache
|
||||
README.md
|
||||
@@ -0,0 +1,18 @@
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.15.13
|
||||
hooks:
|
||||
- id: ruff-check
|
||||
types_or: [python, pyi]
|
||||
args: [--fix]
|
||||
- id: ruff-format
|
||||
types_or: [python, pyi]
|
||||
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: ty
|
||||
name: ty check
|
||||
entry: uvx ty check
|
||||
language: python
|
||||
types_or: [python, pyi]
|
||||
pass_filenames: false
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
# syntax=docker/dockerfile:1.7
|
||||
#
|
||||
# Two stages: a uv-based builder that resolves the venv, and a thin
|
||||
# python:3.13-slim runtime with bun + the `claude` CLI baked in.
|
||||
#
|
||||
# Alpine is intentionally avoided: @anthropic-ai/claude-code ≥ 2.1.113
|
||||
# ships a glibc-only native binary (anthropics/claude-code#50270).
|
||||
#
|
||||
# This image is deliberately un-opinionated about ports and command —
|
||||
# the consumer's compose file decides what to publish and how to invoke.
|
||||
|
||||
# ---- Builder: uv + Python deps ----
|
||||
FROM ghcr.io/astral-sh/uv:python3.13-bookworm-slim AS builder
|
||||
ENV UV_LINK_MODE=copy \
|
||||
UV_COMPILE_BYTECODE=1 \
|
||||
UV_PYTHON_DOWNLOADS=never
|
||||
# `git` is required at build time: the `prod` extra resolves
|
||||
# raycast-api / claude-code-api from git URLs.
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends git \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
WORKDIR /app
|
||||
|
||||
RUN --mount=type=cache,target=/root/.cache/uv \
|
||||
--mount=type=bind,source=uv.lock,target=uv.lock \
|
||||
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
|
||||
uv sync --frozen --no-dev --no-install-project --extra prod
|
||||
|
||||
COPY . /app
|
||||
RUN --mount=type=cache,target=/root/.cache/uv \
|
||||
uv sync --frozen --no-dev --extra prod
|
||||
|
||||
# ---- Runtime ----
|
||||
FROM python:3.13-slim AS runtime
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends ca-certificates git \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Bun native binary (glibc) — fast `npm install` replacement, only used
|
||||
# to drop the claude CLI into the image. Not invoked at runtime.
|
||||
COPY --from=oven/bun:1-slim /usr/local/bin/bun /usr/local/bin/bun
|
||||
RUN ln -s /usr/local/bin/bun /usr/local/bin/bunx
|
||||
|
||||
# `--trust` is required: without it bun skips the postinstall step that
|
||||
# fetches claude's native binary (anthropics/claude-code#50203).
|
||||
ENV BUN_INSTALL=/usr/local/bun-global \
|
||||
PATH=/usr/local/bun-global/bin:/app/.venv/bin:$PATH
|
||||
RUN bun install -g --trust @anthropic-ai/claude-code \
|
||||
&& claude --version
|
||||
|
||||
COPY --from=builder /app/.venv /app/.venv
|
||||
COPY --from=builder /app /app
|
||||
WORKDIR /app
|
||||
|
||||
ENTRYPOINT ["python", "-m"]
|
||||
CMD ["beaver_gateway"]
|
||||
@@ -0,0 +1,18 @@
|
||||
# Stub user config for the Phase 0 DoD.
|
||||
#
|
||||
# Loader (beaver_gateway/config_loader.py) execs this file with
|
||||
# ClaudeAgent, RaycastAgent, McpServer, ExposedMcp, Gateway already
|
||||
# bound. The top-level `gateway = Gateway(...)` is what gets picked up.
|
||||
|
||||
gateway = Gateway( # type: ignore[name-defined] # noqa: F821
|
||||
agents=[
|
||||
ClaudeAgent( # type: ignore[name-defined] # noqa: F821
|
||||
name="stub",
|
||||
model="claude-sonnet-4-5",
|
||||
system_prompt="You are a stub agent used to validate the Phase 0 skeleton.",
|
||||
cwd="/tmp",
|
||||
),
|
||||
],
|
||||
mcps=[],
|
||||
frontends=[],
|
||||
)
|
||||
@@ -0,0 +1,20 @@
|
||||
# Minimal compose for the Phase 0 DoD: build the image, mount a
|
||||
# single-file user config, watch the process print its summary line
|
||||
# and exit cleanly.
|
||||
#
|
||||
# Real deployments add a postgres service, raycast-config.json bind,
|
||||
# ~/.config/claude bind, etc. — Phase 5 lives in a separate example
|
||||
# repo (see PLAN §5.1).
|
||||
|
||||
services:
|
||||
gateway:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: Dockerfile
|
||||
environment:
|
||||
DATABASE_URL: "sqlite:///./gateway.db"
|
||||
ADMIN_USER: "admin"
|
||||
ADMIN_PASS: "change-me"
|
||||
SESSION_SECRET: "dev-secret-change-me"
|
||||
volumes:
|
||||
- ./config.py:/config/config.py:ro
|
||||
@@ -2,7 +2,6 @@
|
||||
name = "beaver-gateway"
|
||||
version = "0.1.0"
|
||||
description = "Agentic registry, gateway and head of operations for Beaver agent"
|
||||
readme = "README.md"
|
||||
authors = [
|
||||
{ name = "h", email = "h@kotikot.com" }
|
||||
]
|
||||
|
||||
@@ -1,2 +1,7 @@
|
||||
def main() -> None:
|
||||
print("Hello from beaver-gateway!")
|
||||
"""Beaver Gateway — personal AI agent gateway."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from beaver_gateway.cli import main
|
||||
|
||||
__all__ = ["main"]
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
"""Allow ``python -m beaver_gateway``."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from beaver_gateway.cli import main
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,9 @@
|
||||
"""Agent type definitions exposed to user config."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from beaver_gateway.agents.base import BaseAgent, ExposedMcp
|
||||
from beaver_gateway.agents.claude import ClaudeAgent
|
||||
from beaver_gateway.agents.raycast import RaycastAgent
|
||||
|
||||
__all__ = ["BaseAgent", "ClaudeAgent", "ExposedMcp", "RaycastAgent"]
|
||||
@@ -0,0 +1,33 @@
|
||||
"""Shared agent surface.
|
||||
|
||||
``BaseAgent`` is the structural contract every backend-specific agent
|
||||
honours. Subclasses add **capabilities as fields** (``ClaudeAgent.cwd``,
|
||||
``RaycastAgent.streaming``) — backends dispatch on type, not on a runtime
|
||||
matrix.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ExposedMcp:
|
||||
"""Reference to an ``McpServer`` (by name) exposed to a single agent."""
|
||||
|
||||
name: str
|
||||
tools: tuple[str, ...] | None = None
|
||||
|
||||
|
||||
class BaseAgent(BaseModel):
|
||||
"""Common fields. Concrete backends subclass and add capabilities."""
|
||||
|
||||
model_config = ConfigDict(frozen=True, arbitrary_types_allowed=True)
|
||||
|
||||
name: str
|
||||
model: str
|
||||
system_prompt: str
|
||||
expose_mcps: tuple[ExposedMcp, ...] = ()
|
||||
accept_client_tools: bool = False
|
||||
@@ -0,0 +1,17 @@
|
||||
"""Claude Code agent definition.
|
||||
|
||||
Notice the absence of a ``streaming`` field — claude-code does not emit
|
||||
token-level deltas, and that fact is encoded in the type, not in a
|
||||
runtime branch.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from beaver_gateway.agents.base import BaseAgent
|
||||
|
||||
|
||||
class ClaudeAgent(BaseAgent):
|
||||
"""Agent backed by ``claude-code-api``."""
|
||||
|
||||
cwd: str
|
||||
available_native_tools: tuple[str, ...] = ()
|
||||
@@ -0,0 +1,15 @@
|
||||
"""Raycast agent definition."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from raycast_api import Source
|
||||
|
||||
from beaver_gateway.agents.base import BaseAgent
|
||||
|
||||
|
||||
class RaycastAgent(BaseAgent):
|
||||
"""Agent backed by ``raycast-api``."""
|
||||
|
||||
streaming: bool = True
|
||||
available_native_tools: tuple[str, ...] = ()
|
||||
source: Source = Source.AI_CHAT
|
||||
@@ -0,0 +1,28 @@
|
||||
"""Process entrypoint.
|
||||
|
||||
Phase 0.3 — load the user's ``/config/config.py`` via
|
||||
``config_loader``, build registries, print the
|
||||
``loaded N agents, M mcps, K frontends`` line from the Phase 0 DoD,
|
||||
exit cleanly. Phase 0.4 will install uvloop and start uvicorn(s) for
|
||||
the frontends and the internal MCP app.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from beaver_gateway import config_loader
|
||||
from beaver_gateway.core.registry import AgentRegistry, McpRegistry
|
||||
from beaver_gateway.settings import Settings
|
||||
|
||||
|
||||
def main() -> None:
|
||||
settings = Settings() # ty: ignore[missing-argument]
|
||||
|
||||
gateway = config_loader.load(settings.config_path)
|
||||
|
||||
agents = AgentRegistry(gateway.agents)
|
||||
mcps = McpRegistry(gateway.mcps)
|
||||
|
||||
print(
|
||||
f"beaver-gateway: loaded {len(agents)} agents, "
|
||||
f"{len(mcps)} mcps, {len(gateway.frontends)} frontends"
|
||||
)
|
||||
@@ -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)
|
||||
@@ -0,0 +1,7 @@
|
||||
"""Cross-cutting machinery: registries, event protocol, auth, sessions."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from beaver_gateway.core.registry import AgentRegistry, Gateway, McpRegistry
|
||||
|
||||
__all__ = ["AgentRegistry", "Gateway", "McpRegistry"]
|
||||
@@ -0,0 +1,83 @@
|
||||
"""Agent / MCP registries + the user-facing ``Gateway`` collector.
|
||||
|
||||
The user's ``/config/config.py`` ends with::
|
||||
|
||||
gateway = Gateway(agents=[...], mcps=[...], frontends=[...])
|
||||
|
||||
``cli.main`` picks that object up and builds the registries.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Iterable, Iterator
|
||||
|
||||
from beaver_gateway.agents.base import BaseAgent
|
||||
from beaver_gateway.frontends.base import Frontend
|
||||
from beaver_gateway.mcp.types import McpServerT
|
||||
|
||||
|
||||
class AgentRegistry:
|
||||
"""Name → agent lookup with duplicate detection."""
|
||||
|
||||
def __init__(self, agents: Iterable[BaseAgent]) -> None:
|
||||
self._agents: dict[str, BaseAgent] = {}
|
||||
for a in agents:
|
||||
if a.name in self._agents:
|
||||
msg = f"duplicate agent name: {a.name!r}"
|
||||
raise ValueError(msg)
|
||||
self._agents[a.name] = a
|
||||
|
||||
def __getitem__(self, name: str) -> BaseAgent:
|
||||
return self._agents[name]
|
||||
|
||||
def get(self, name: str) -> BaseAgent | None:
|
||||
return self._agents.get(name)
|
||||
|
||||
def __iter__(self) -> Iterator[BaseAgent]:
|
||||
return iter(self._agents.values())
|
||||
|
||||
def __len__(self) -> int:
|
||||
return len(self._agents)
|
||||
|
||||
def __contains__(self, name: object) -> bool:
|
||||
return name in self._agents
|
||||
|
||||
|
||||
class McpRegistry:
|
||||
"""Name → MCP server lookup with duplicate detection."""
|
||||
|
||||
def __init__(self, mcps: Iterable[McpServerT]) -> None:
|
||||
self._mcps: dict[str, McpServerT] = {}
|
||||
for m in mcps:
|
||||
if m.name in self._mcps:
|
||||
msg = f"duplicate mcp name: {m.name!r}"
|
||||
raise ValueError(msg)
|
||||
self._mcps[m.name] = m
|
||||
|
||||
def __getitem__(self, name: str) -> McpServerT:
|
||||
return self._mcps[name]
|
||||
|
||||
def get(self, name: str) -> McpServerT | None:
|
||||
return self._mcps.get(name)
|
||||
|
||||
def __iter__(self) -> Iterator[McpServerT]:
|
||||
return iter(self._mcps.values())
|
||||
|
||||
def __len__(self) -> int:
|
||||
return len(self._mcps)
|
||||
|
||||
def __contains__(self, name: object) -> bool:
|
||||
return name in self._mcps
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class Gateway:
|
||||
"""Top-level object the user assembles in ``/config/config.py``."""
|
||||
|
||||
agents: list[BaseAgent] = field(default_factory=list)
|
||||
mcps: list[McpServerT] = field(default_factory=list)
|
||||
frontends: list[Frontend] = field(default_factory=list)
|
||||
@@ -0,0 +1,7 @@
|
||||
"""Frontend protocols and (later) concrete app implementations."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from beaver_gateway.frontends.base import Frontend
|
||||
|
||||
__all__ = ["Frontend"]
|
||||
@@ -0,0 +1,24 @@
|
||||
"""Frontend ABC.
|
||||
|
||||
A frontend is anything that listens on a port and routes inbound traffic
|
||||
into the agent/MCP registries. ``configure`` is called once after the
|
||||
``Gateway`` is built; ``serve`` runs the listening loop.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from beaver_gateway.core.registry import Gateway
|
||||
|
||||
|
||||
class Frontend(ABC):
|
||||
"""Listens on a port, dispatches into the gateway."""
|
||||
|
||||
@abstractmethod
|
||||
def configure(self, gateway: Gateway) -> None: ...
|
||||
|
||||
@abstractmethod
|
||||
async def serve(self) -> None: ...
|
||||
@@ -0,0 +1,13 @@
|
||||
"""MCP server definitions and (later) the internal aggregator app."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from beaver_gateway.mcp.types import (
|
||||
HttpMcp,
|
||||
McpServer,
|
||||
McpServerT,
|
||||
PythonToolMcp,
|
||||
StdioMcp,
|
||||
)
|
||||
|
||||
__all__ = ["HttpMcp", "McpServer", "McpServerT", "PythonToolMcp", "StdioMcp"]
|
||||
@@ -0,0 +1,77 @@
|
||||
"""User-facing MCP server declarations.
|
||||
|
||||
Three flavours, one factory facade (``McpServer``). The factory returns
|
||||
discriminated-union members so downstream code can ``match`` on ``kind``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable # noqa: TC003 — runtime use by pydantic
|
||||
from typing import TYPE_CHECKING, Annotated, Literal
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Iterable
|
||||
|
||||
|
||||
class _BaseMcp(BaseModel):
|
||||
model_config = ConfigDict(frozen=True, arbitrary_types_allowed=True)
|
||||
|
||||
name: str
|
||||
|
||||
|
||||
class StdioMcp(_BaseMcp):
|
||||
"""Subprocess MCP server we spawn and connect to over stdio."""
|
||||
|
||||
kind: Literal["stdio"] = "stdio"
|
||||
command: tuple[str, ...]
|
||||
env: dict[str, str] | None = None
|
||||
cwd: str | None = None
|
||||
|
||||
|
||||
class HttpMcp(_BaseMcp):
|
||||
"""Remote MCP server reached over streamable HTTP."""
|
||||
|
||||
kind: Literal["http"] = "http"
|
||||
url: str
|
||||
auth: str | None = None
|
||||
|
||||
|
||||
class PythonToolMcp(_BaseMcp):
|
||||
"""Bundle of Python callables exposed as one FastMCP namespace."""
|
||||
|
||||
kind: Literal["python_tool"] = "python_tool"
|
||||
tools: tuple[Callable[..., object], ...]
|
||||
|
||||
|
||||
McpServerT = Annotated[
|
||||
StdioMcp | HttpMcp | PythonToolMcp, Field(discriminator="kind")
|
||||
]
|
||||
|
||||
|
||||
class McpServer:
|
||||
"""Factory facade matching the PRD-documented config surface."""
|
||||
|
||||
@classmethod
|
||||
def stdio(
|
||||
cls,
|
||||
*,
|
||||
name: str,
|
||||
command: Iterable[str],
|
||||
env: dict[str, str] | None = None,
|
||||
cwd: str | None = None,
|
||||
) -> StdioMcp:
|
||||
return StdioMcp(name=name, command=tuple(command), env=env, cwd=cwd)
|
||||
|
||||
@classmethod
|
||||
def http(
|
||||
cls, *, name: str, url: str, auth: str | None = None
|
||||
) -> HttpMcp:
|
||||
return HttpMcp(name=name, url=url, auth=auth)
|
||||
|
||||
@classmethod
|
||||
def python_tool(
|
||||
cls, *, name: str, tools: Iterable[Callable[..., object]]
|
||||
) -> PythonToolMcp:
|
||||
return PythonToolMcp(name=name, tools=tuple(tools))
|
||||
@@ -0,0 +1,31 @@
|
||||
"""Process-wide configuration loaded from environment (`.env`).
|
||||
|
||||
Everything that varies between deployments (creds, paths, ports) lives here.
|
||||
User-facing agent/MCP/frontend definitions live in ``/config/config.py``
|
||||
and are loaded by ``config_loader`` (Phase 0.3).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
"""Runtime environment for the gateway process."""
|
||||
|
||||
model_config = SettingsConfigDict(
|
||||
env_file=".env", env_file_encoding="utf-8", extra="ignore"
|
||||
)
|
||||
|
||||
database_url: str
|
||||
admin_user: str
|
||||
admin_pass: str
|
||||
session_secret: str
|
||||
|
||||
internal_mcp_port: int = 8765
|
||||
config_path: Path = Path("/config/config.py")
|
||||
|
||||
raycast_bearer: str | None = None
|
||||
raycast_config_path: Path = Path("/config/raycast.json")
|
||||
Reference in New Issue
Block a user