Merge pull request #145 from bunny-lab-io:codex/review-code-migration-tracker-instructions

Stage 3 - Introduce API blueprints and service adapters Implemented
This commit is contained in:
2025-10-26 01:21:30 -06:00
committed by GitHub
4 changed files with 226 additions and 16 deletions

View File

@@ -18,10 +18,10 @@ Lastly, everytime that you complete a stage, you will create a pull request name
- [x] Verify context parity between Engine and legacy startup. - [x] Verify context parity between Engine and legacy startup.
- [x] Initialize logging to Logs/Server/server.log when Engine mode is active. - [x] Initialize logging to Logs/Server/server.log when Engine mode is active.
- [x] Document Engine launch paths and configuration requirements in module docstrings. - [x] Document Engine launch paths and configuration requirements in module docstrings.
- [ ] **Stage 3 — Introduce API blueprints and service adapters** - [x] **Stage 3 — Introduce API blueprints and service adapters**
- [ ] Create domain-focused API blueprints and register_api entry point. - [x] Create domain-focused API blueprints and register_api entry point.
- [ ] Mirror route behaviour from the legacy server via service adapters. - [x] Mirror route behaviour from the legacy server via service adapters.
- [ ] Add configuration toggles for enabling API groups incrementally. - [x] Add configuration toggles for enabling API groups incrementally.
- [ ] **Stage 4 — Build unit and smoke tests for Engine APIs** - [ ] **Stage 4 — Build unit and smoke tests for Engine APIs**
- [ ] Add pytest modules under Data/Engine/Unit_Tests exercising API blueprints. - [ ] Add pytest modules under Data/Engine/Unit_Tests exercising API blueprints.
- [ ] Provide fixtures that mirror the legacy SQLite schema and seed data. - [ ] Provide fixtures that mirror the legacy SQLite schema and seed data.
@@ -43,5 +43,5 @@ Lastly, everytime that you complete a stage, you will create a pull request name
- [ ] Update legacy server to consume Engine WebSocket registration. - [ ] Update legacy server to consume Engine WebSocket registration.
## Current Status ## Current Status
- **Stage:** Stage 2Port configuration and dependency loading into the Engine factory (completed) - **Stage:** Stage 3Introduce API blueprints and service adapters (completed)
- **Active Task:** Awaiting next stage instructions. - **Active Task:** Awaiting next stage instructions.

View File

@@ -37,7 +37,7 @@ import os
from dataclasses import asdict, dataclass, field from dataclasses import asdict, dataclass, field
from logging.handlers import TimedRotatingFileHandler from logging.handlers import TimedRotatingFileHandler
from pathlib import Path from pathlib import Path
from typing import Any, List, Mapping, MutableMapping, Optional, Sequence from typing import Any, Iterable, List, Mapping, MutableMapping, Optional, Sequence, Tuple
try: # pragma: no-cover - optional dependency during early migration stages. try: # pragma: no-cover - optional dependency during early migration stages.
from Modules.crypto import certificates # type: ignore from Modules.crypto import certificates # type: ignore
@@ -139,6 +139,7 @@ class EngineSettings:
tls_key_path: Optional[str] tls_key_path: Optional[str]
tls_bundle_path: Optional[str] tls_bundle_path: Optional[str]
log_file: str log_file: str
api_groups: Tuple[str, ...]
raw: MutableMapping[str, Any] = field(default_factory=dict) raw: MutableMapping[str, Any] = field(default_factory=dict)
def to_flask_config(self) -> MutableMapping[str, Any]: def to_flask_config(self) -> MutableMapping[str, Any]:
@@ -158,6 +159,19 @@ class EngineSettings:
return data return data
def _parse_api_groups(raw: Optional[Any]) -> Tuple[str, ...]:
if raw is None:
return tuple()
if isinstance(raw, str):
parts: Iterable[str] = (part.strip() for part in raw.split(","))
elif isinstance(raw, Sequence):
parts = (str(part).strip() for part in raw)
else:
return tuple()
cleaned = [part.lower() for part in parts if part]
return tuple(dict.fromkeys(cleaned))
def load_runtime_config(overrides: Optional[Mapping[str, Any]] = None) -> EngineSettings: def load_runtime_config(overrides: Optional[Mapping[str, Any]] = None) -> EngineSettings:
"""Resolve Engine configuration values. """Resolve Engine configuration values.
@@ -205,6 +219,12 @@ def load_runtime_config(overrides: Optional[Mapping[str, Any]] = None) -> Engine
log_file = str(runtime_config.get("LOG_FILE") or LOG_FILE_PATH) log_file = str(runtime_config.get("LOG_FILE") or LOG_FILE_PATH)
_ensure_parent(Path(log_file)) _ensure_parent(Path(log_file))
api_groups = _parse_api_groups(
runtime_config.get("API_GROUPS") or os.environ.get("BOREALIS_API_GROUPS")
)
if not api_groups:
api_groups = ("tokens", "enrollment")
settings = EngineSettings( settings = EngineSettings(
database_path=database_path, database_path=database_path,
static_folder=static_folder, static_folder=static_folder,
@@ -217,6 +237,7 @@ def load_runtime_config(overrides: Optional[Mapping[str, Any]] = None) -> Engine
tls_key_path=tls_key_path if tls_key_path else None, tls_key_path=tls_key_path if tls_key_path else None,
tls_bundle_path=tls_bundle_path if tls_bundle_path else None, tls_bundle_path=tls_bundle_path if tls_bundle_path else None,
log_file=str(log_file), log_file=str(log_file),
api_groups=api_groups,
raw=runtime_config, raw=runtime_config,
) )
return settings return settings

View File

@@ -14,7 +14,7 @@ import ssl
import sys import sys
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from typing import Any, Mapping, Optional, Tuple from typing import Any, Mapping, Optional, Sequence, Tuple
import eventlet import eventlet
from flask import Flask from flask import Flask
@@ -103,6 +103,7 @@ class EngineContext:
tls_key_path: Optional[str] tls_key_path: Optional[str]
tls_bundle_path: Optional[str] tls_bundle_path: Optional[str]
config: Mapping[str, Any] config: Mapping[str, Any]
api_groups: Sequence[str]
__all__ = ["EngineContext", "create_app"] __all__ = ["EngineContext", "create_app"]
@@ -154,6 +155,7 @@ def create_app(config: Optional[Mapping[str, Any]] = None) -> Tuple[Flask, Socke
tls_key_path=tls_key_path, tls_key_path=tls_key_path,
tls_bundle_path=tls_bundle_path, tls_bundle_path=tls_bundle_path,
config=settings.as_dict(), config=settings.as_dict(),
api_groups=settings.api_groups,
) )
from .services import API, WebSocket, WebUI # Local import to avoid circular deps during bootstrap from .services import API, WebSocket, WebUI # Local import to avoid circular deps during bootstrap

View File

@@ -1,21 +1,208 @@
"""API service stubs for the Borealis Engine runtime. """API service adapters for the Borealis Engine runtime.
Stage 1 only establishes the package layout. Future stages will populate this Stage 3 of the migration introduces blueprint registration that mirrors the
module with blueprint factories that wrap the legacy API helpers. behaviour of :mod:`Data.Server.server` by delegating to the existing domain
modules under ``Data/Server/Modules``. Each adapter wires the Engine context
into the legacy registration helpers so routes continue to function while
configuration toggles control which API groups are exposed.
""" """
from __future__ import annotations from __future__ import annotations
import datetime as _dt
import logging
import re
import sqlite3
import time
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Callable, Iterable, Mapping, Optional, Sequence
from flask import Flask from flask import Flask
from Modules.auth import jwt_service as jwt_service_module
from Modules.auth.dpop import DPoPValidator
from Modules.auth.rate_limit import SlidingWindowRateLimiter
from Modules.crypto import signing
from Modules.enrollment import routes as enrollment_routes
from Modules.enrollment.nonce_store import NonceCache
from Modules.tokens import routes as token_routes
from ...server import EngineContext from ...server import EngineContext
DEFAULT_API_GROUPS: Sequence[str] = ("tokens", "enrollment")
_SERVER_SCOPE_PATTERN = re.compile(r"\b(?:scope|context|agent_context)=([A-Za-z0-9_-]+)", re.IGNORECASE)
_SERVER_AGENT_ID_PATTERN = re.compile(r"\bagent_id=([^\s,]+)", re.IGNORECASE)
def _canonical_server_scope(raw: Optional[str]) -> Optional[str]:
if not raw:
return None
value = "".join(ch for ch in str(raw) if ch.isalnum() or ch in ("_", "-"))
if not value:
return None
return value.upper()
def _scope_from_agent_id(agent_id: Optional[str]) -> Optional[str]:
candidate = _canonical_server_scope(agent_id)
if not candidate:
return None
if candidate.endswith("_SYSTEM"):
return "SYSTEM"
if candidate.endswith("_CURRENTUSER"):
return "CURRENTUSER"
return candidate
def _infer_server_scope(message: str, explicit: Optional[str]) -> Optional[str]:
scope = _canonical_server_scope(explicit)
if scope:
return scope
match = _SERVER_SCOPE_PATTERN.search(message or "")
if match:
scope = _canonical_server_scope(match.group(1))
if scope:
return scope
agent_match = _SERVER_AGENT_ID_PATTERN.search(message or "")
if agent_match:
scope = _scope_from_agent_id(agent_match.group(1))
if scope:
return scope
return None
def _rotate_daily(path: Path) -> None:
try:
if not path.is_file():
return
stat = path.stat()
modified = _dt.datetime.fromtimestamp(stat.st_mtime)
if modified.date() == _dt.datetime.now().date():
return
suffix = modified.strftime("%Y-%m-%d")
rotated = path.with_name(f"{path.name}.{suffix}")
if rotated.exists():
return
path.rename(rotated)
except Exception:
pass
def _make_service_logger(base: Path, logger: logging.Logger) -> Callable[[str, str, Optional[str]], None]:
def _log(service: str, msg: str, scope: Optional[str] = None, *, level: str = "INFO") -> None:
level_upper = level.upper()
try:
base.mkdir(parents=True, exist_ok=True)
path = base / f"{service}.log"
_rotate_daily(path)
timestamp = time.strftime("%Y-%m-%d %H:%M:%S")
resolved_scope = _infer_server_scope(msg, scope)
prefix_parts = [f"[{level_upper}]"]
if resolved_scope:
prefix_parts.append(f"[CONTEXT-{resolved_scope}]")
prefix = "".join(prefix_parts)
with path.open("a", encoding="utf-8") as handle:
handle.write(f"[{timestamp}] {prefix} {msg}\n")
except Exception:
logger.debug("Failed to write service log entry", exc_info=True)
numeric_level = getattr(logging, level_upper, logging.INFO)
logger.log(numeric_level, "[service:%s] %s", service, msg)
return _log
def _make_db_conn_factory(database_path: str) -> Callable[[], sqlite3.Connection]:
def _factory() -> sqlite3.Connection:
conn = sqlite3.connect(database_path, timeout=15)
try:
cur = conn.cursor()
cur.execute("PRAGMA journal_mode=WAL")
cur.execute("PRAGMA busy_timeout=5000")
cur.execute("PRAGMA synchronous=NORMAL")
conn.commit()
except Exception:
pass
return conn
return _factory
@dataclass
class LegacyServiceAdapters:
context: EngineContext
db_conn_factory: Callable[[], sqlite3.Connection] = field(init=False)
jwt_service: Any = field(init=False)
dpop_validator: DPoPValidator = field(init=False)
ip_rate_limiter: SlidingWindowRateLimiter = field(init=False)
fp_rate_limiter: SlidingWindowRateLimiter = field(init=False)
nonce_cache: NonceCache = field(init=False)
script_signer: Any = field(init=False)
service_log: Callable[[str, str, Optional[str]], None] = field(init=False)
def __post_init__(self) -> None:
self.db_conn_factory = _make_db_conn_factory(self.context.database_path)
self.jwt_service = jwt_service_module.load_service()
self.dpop_validator = DPoPValidator()
self.ip_rate_limiter = SlidingWindowRateLimiter()
self.fp_rate_limiter = SlidingWindowRateLimiter()
self.nonce_cache = NonceCache()
try:
self.script_signer = signing.load_signer()
except Exception:
self.script_signer = None
log_file = str(self.context.config.get("log_file") or self.context.config.get("LOG_FILE") or "")
if log_file:
base = Path(log_file).resolve().parent
else:
base = Path(self.context.database_path).resolve().parent
self.service_log = _make_service_logger(base, self.context.logger)
def _register_tokens(app: Flask, adapters: LegacyServiceAdapters) -> None:
token_routes.register(
app,
db_conn_factory=adapters.db_conn_factory,
jwt_service=adapters.jwt_service,
dpop_validator=adapters.dpop_validator,
)
def _register_enrollment(app: Flask, adapters: LegacyServiceAdapters) -> None:
tls_bundle = adapters.context.tls_bundle_path or ""
enrollment_routes.register(
app,
db_conn_factory=adapters.db_conn_factory,
log=adapters.service_log,
jwt_service=adapters.jwt_service,
tls_bundle_path=tls_bundle,
ip_rate_limiter=adapters.ip_rate_limiter,
fp_rate_limiter=adapters.fp_rate_limiter,
nonce_cache=adapters.nonce_cache,
script_signer=adapters.script_signer,
)
_GROUP_REGISTRARS: Mapping[str, Callable[[Flask, LegacyServiceAdapters], None]] = {
"tokens": _register_tokens,
"enrollment": _register_enrollment,
}
def register_api(app: Flask, context: EngineContext) -> None: def register_api(app: Flask, context: EngineContext) -> None:
"""Placeholder hook for API blueprint registration. """Register Engine API blueprints based on the enabled groups."""
Later migration stages will import domain-specific blueprint modules and enabled_groups: Iterable[str] = context.api_groups or DEFAULT_API_GROUPS
attach them to ``app`` using the shared :class:`EngineContext`. For now we normalized = [group.strip().lower() for group in enabled_groups if group]
simply log the intent so tooling can verify the hook is wired. adapters = LegacyServiceAdapters(context)
"""
for group in normalized:
registrar = _GROUP_REGISTRARS.get(group)
if registrar is None:
context.logger.info("Engine API group '%s' is not implemented; skipping.", group)
continue
registrar(app, adapters)
context.logger.info("Engine registered API group '%s'.", group)
context.logger.debug("Engine API services are not yet implemented.")