diff --git a/pyproject.toml b/pyproject.toml index b97f706..a96007b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "ratatoskr" -version = "0.17.1" +version = "0.17.2" description = "Worldtree Conversation API debug TUI — multi-pane observability dashboard" readme = "README.md" requires-python = ">=3.12" @@ -45,8 +45,9 @@ dev = [ ] [project.scripts] -ratatoskr = "ratatoskr.cli:main" -ratatoskr-web = "ratatoskr.web.entrypoint:main" +ratatoskr = "ratatoskr.cli:main" +ratatoskr-web = "ratatoskr.web.entrypoint:main" +ratatoskr-provider = "ratatoskr.provider.serve:main" [project.urls] Repository = "https://gitea.phasefinal.com/vh/ratatoskr" diff --git a/src/ratatoskr/provider/serve.py b/src/ratatoskr/provider/serve.py new file mode 100644 index 0000000..e0b1e4b --- /dev/null +++ b/src/ratatoskr/provider/serve.py @@ -0,0 +1,40 @@ +"""Runnable entrypoint: serve the affect provider as an ASGI app. + +For the live negotiation smoke against a Worldtree instance. Config from env: +- RATATOSKR_HEIMDALL_KEY (required): HS256 shared key for the consumer, utf-8. +- RATATOSKR_AFFECT_DB (default "affect.db"): SQLite path; ":memory:" = ephemeral. +- RATATOSKR_CONSUMER_ID (default "ratatoskr"). +- RATATOSKR_PROVIDER_HOST (default "0.0.0.0"), RATATOSKR_PROVIDER_PORT (default 8390). +""" +from __future__ import annotations + +import os +from collections.abc import Mapping + +from ratatoskr.provider.affect_store import build_affect_provider_app, open_affect_store + + +def build_app_from_env(env: Mapping[str, str] | None = None): + """Build the affect ASGI app from environment config (testable seam).""" + env = os.environ if env is None else env + key = env.get("RATATOSKR_HEIMDALL_KEY") + if not key: + raise RuntimeError( + "RATATOSKR_HEIMDALL_KEY is required to serve the affect provider" + ) + store = open_affect_store(env.get("RATATOSKR_AFFECT_DB", "affect.db")) + return build_affect_provider_app( + store, + heimdall_key=key.encode(), + consumer_id=env.get("RATATOSKR_CONSUMER_ID", "ratatoskr"), + ) + + +def main() -> None: + import uvicorn + + uvicorn.run( + build_app_from_env(), + host=os.environ.get("RATATOSKR_PROVIDER_HOST", "0.0.0.0"), + port=int(os.environ.get("RATATOSKR_PROVIDER_PORT", "8390")), + ) diff --git a/tests/test_provider_serve.py b/tests/test_provider_serve.py new file mode 100644 index 0000000..0577b42 --- /dev/null +++ b/tests/test_provider_serve.py @@ -0,0 +1,23 @@ +"""Tests for the affect-provider serve entrypoint (ratatoskr.provider.serve). + +Only the env -> app seam is unit-tested; uvicorn.run is the untestable shell. +""" +from __future__ import annotations + +import pytest + +from ratatoskr.provider.serve import build_app_from_env + + +def test_build_app_from_env_requires_heimdall_key(): + with pytest.raises(RuntimeError): + build_app_from_env({"RATATOSKR_AFFECT_DB": ":memory:"}) + + +def test_build_app_from_env_builds_app_with_routes(): + app = build_app_from_env( + {"RATATOSKR_HEIMDALL_KEY": "shared-secret", "RATATOSKR_AFFECT_DB": ":memory:"} + ) + paths = {getattr(r, "path", None) for r in app.routes} + assert "/bifrost/handshake" in paths + assert "/bifrost/affect-call" in paths diff --git a/uv.lock b/uv.lock index f43cc8b..17715b6 100644 --- a/uv.lock +++ b/uv.lock @@ -1052,7 +1052,7 @@ wheels = [ [[package]] name = "ratatoskr" -version = "0.17.1" +version = "0.17.2" source = { editable = "." } dependencies = [ { name = "httpx" },