Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci-quality.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ jobs:
- name: Install linters
run: |
python -m pip install --upgrade pip
pip install ruff mypy
pip install "ruff==0.15.5" mypy

- name: Ruff (lint)
run: ruff check . --output-format=github
Expand Down
5 changes: 5 additions & 0 deletions chassis/action_registry.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
"""Action-to-node resolution for the L9 Constellation Runtime."""

from constellation.types import SNAKE, ConstellationError

ACTION_MAP: dict[str, str] = {}
_HANDLERS: dict[str, callable] = {}


def register_action(action_name: str, node_name_or_handler=None):
"""Register an action. Usable as decorator or direct call."""
if not SNAKE.match(action_name):
Expand All @@ -23,11 +25,14 @@ def _register(handler, node_name: str):
ACTION_MAP[action_name] = node_name_or_handler
return None
else:

def decorator(fn):
_register(fn, getattr(fn, "_node_name", fn.__qualname__.split(".")[0]))
return fn

return decorator


def get_action_handler(action_name: str):
if action_name not in _HANDLERS:
raise ConstellationError(f"Unknown action: {action_name}", status="rejected")
Expand Down
14 changes: 9 additions & 5 deletions chassis/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@
LifecycleHook via register_handler() / register_handlers().
"""

from __future__ import annotations

import logging
import time
from datetime import UTC, datetime
from typing import Any, Callable, Awaitable

Check failure on line 27 in chassis/actions.py

View workflow job for this annotation

GitHub Actions / Lint & Format (Ruff + MyPy)

ruff (UP035)

chassis/actions.py:27:1: UP035 Import from `collections.abc` instead: `Callable`, `Awaitable` help: Import from `collections.abc`

Check failure on line 27 in chassis/actions.py

View workflow job for this annotation

GitHub Actions / Lint & Format (Ruff + MyPy)

ruff (I001)

chassis/actions.py:22:1: I001 Import block is un-sorted or un-formatted help: Organize imports

logger = logging.getLogger(__name__)

Expand All @@ -44,6 +44,7 @@

# ── Registration API (called by engine's LifecycleHook.startup) ──────────


def register_handler(action: str, handler: ActionHandler) -> None:
"""Register a single action handler."""
_handlers[action] = handler
Expand Down Expand Up @@ -73,8 +74,12 @@
_deflate_fn = deflate
ENGINE_VERSION = engine_version
NODE_NAME = node_name
logger.info("Packet bridge wired: inflate=%s, deflate=%s, node=%s",
inflate.__name__, deflate.__name__, node_name)
logger.info(
"Packet bridge wired: inflate=%s, deflate=%s, node=%s",
inflate.__name__,
deflate.__name__,
node_name,
)


def clear_handlers() -> None:
Expand All @@ -89,6 +94,7 @@

# ── Execution ────────────────────────────────────────────────────────────


async def execute_action(
action: str,
payload: dict[str, Any],
Expand Down Expand Up @@ -121,9 +127,7 @@
handler = _handlers.get(action)
if not handler:
available = ", ".join(sorted(_handlers)) or "(none)"
raise ValueError(
f"Unknown action: {action!r}. Available: {available}"
)
raise ValueError(f"Unknown action: {action!r}. Available: {available}")

# ── Execute ──
try:
Expand Down
7 changes: 6 additions & 1 deletion chassis/audit.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,9 @@
logger = logging.getLogger(__name__)


class AuditAction(str, Enum):

Check failure on line 42 in chassis/audit.py

View workflow job for this annotation

GitHub Actions / Lint & Format (Ruff + MyPy)

ruff (UP042)

chassis/audit.py:42:7: UP042 Class AuditAction inherits from both `str` and `enum.Enum` help: Inherit from `enum.StrEnum`
"""Auditable action categories — extensible per engine."""

ACCESS = "access"
MUTATION = "mutation"
QUERY = "query"
Expand All @@ -53,7 +54,7 @@
HEALTH = "health"


class AuditSeverity(str, Enum):

Check failure on line 57 in chassis/audit.py

View workflow job for this annotation

GitHub Actions / Lint & Format (Ruff + MyPy)

ruff (UP042)

chassis/audit.py:57:7: UP042 Class AuditSeverity inherits from both `str` and `enum.Enum` help: Inherit from `enum.StrEnum`
INFO = "info"
WARNING = "warning"
CRITICAL = "critical"
Expand All @@ -61,6 +62,7 @@

class AuditEntry(BaseModel):
"""Immutable audit log entry."""

model_config = {"frozen": True}

audit_id: str = Field(default_factory=lambda: str(uuid.uuid4()))
Expand Down Expand Up @@ -91,13 +93,16 @@
DEFAULT_RETENTION: dict[str, RetentionPolicy] = {
"SOC2": RetentionPolicy(tag="SOC2", retention_days=2555, require_immutable_storage=True),
"GDPR": RetentionPolicy(tag="GDPR", retention_days=1825, require_encryption=True),
"HIPAA": RetentionPolicy(tag="HIPAA", retention_days=2190, require_encryption=True, require_immutable_storage=True),
"HIPAA": RetentionPolicy(
tag="HIPAA", retention_days=2190, require_encryption=True, require_immutable_storage=True
),
"ECOA": RetentionPolicy(tag="ECOA", retention_days=730),
}


# ── Pluggable sink protocol ──────────────────────────────────────────


class AuditSink:
"""
Abstract audit sink. Engines provide concrete implementations.
Expand Down
20 changes: 12 additions & 8 deletions chassis/auth/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,15 @@

# Paths that never require authentication.
# Health must stay public for Cloudflare, Coolify, and external uptime monitors.
PUBLIC_PATHS: frozenset[str] = frozenset({
"/v1/health",
"/health",
"/docs",
"/openapi.json",
"/redoc",
})
PUBLIC_PATHS: frozenset[str] = frozenset(
{
"/v1/health",
"/health",
"/docs",
"/openapi.json",
"/redoc",
}
)


class BearerAuthMiddleware(BaseHTTPMiddleware):
Expand Down Expand Up @@ -109,7 +111,9 @@ async def dispatch(
# Validate Bearer scheme
parts = auth_header.split(" ", 1)
if len(parts) != 2 or parts[0].lower() != "bearer":
logger.warning("Malformed Authorization header: scheme=%r", parts[0] if parts else "empty")
logger.warning(
"Malformed Authorization header: scheme=%r", parts[0] if parts else "empty"
)
return JSONResponse(
status_code=401,
content={
Expand Down
11 changes: 6 additions & 5 deletions chassis/chassis_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
# CHASSIS-OWNED CONFIGURATION (engine never touches this)
# ═══════════════════════════════════════════════════════════════════════════


class ChassisSettings(BaseSettings):
"""
Minimal config the chassis itself needs.
Expand Down Expand Up @@ -84,6 +85,7 @@ class ChassisSettings(BaseSettings):
# CHASSIS-OWNED ENVELOPE MODELS
# ═══════════════════════════════════════════════════════════════════════════


class ExecuteRequest(BaseModel):
"""Universal execute request envelope — chassis contract."""

Expand All @@ -107,6 +109,7 @@ class ExecuteResponse(BaseModel):
# LIFECYCLE HOOK — the engine's ONLY coupling surface to the chassis
# ═══════════════════════════════════════════════════════════════════════════


class LifecycleHook(ABC):
"""
Abstract contract that every L9 engine implements ONCE.
Expand Down Expand Up @@ -183,6 +186,7 @@ async def execute(
# HOOK RESOLUTION (env var → importlib → instance)
# ═══════════════════════════════════════════════════════════════════════════


def _resolve_hook(hook: LifecycleHook | None) -> LifecycleHook:
"""
Priority:
Expand Down Expand Up @@ -213,6 +217,7 @@ def _resolve_hook(hook: LifecycleHook | None) -> LifecycleHook:
# APPLICATION FACTORY
# ═══════════════════════════════════════════════════════════════════════════


def create_app(
*,
lifecycle_hook: LifecycleHook | None = None,
Expand Down Expand Up @@ -317,11 +322,7 @@ async def health(request: Request) -> JSONResponse:
try:
result = await hook.health(tenant=tenant, trace_id=trace_id)

status_code = (
200
if result.get("data", {}).get("status") == "healthy"
else 503
)
status_code = 200 if result.get("data", {}).get("status") == "healthy" else 503
return JSONResponse(content=result, status_code=status_code)

except Exception as exc:
Expand Down
5 changes: 5 additions & 0 deletions chassis/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,21 +71,25 @@ def to_dict(self) -> dict[str, Any]:

class ValidationError(ChassisError):
"""Payload or schema validation failure → HTTP 422."""

status_code: int = 422


class NotFoundError(ChassisError):
"""Resource not found (domain, entity, etc.) → HTTP 404."""

status_code: int = 404


class AuthorizationError(ChassisError):
"""Tenant not authorized for this action → HTTP 403."""

status_code: int = 403


class RateLimitError(ChassisError):
"""Rate limit exceeded → HTTP 429."""

status_code: int = 429

def __init__(self, message: str = "Rate limit exceeded", *, retry_after: int = 60, **kwargs):
Expand All @@ -95,4 +99,5 @@ def __init__(self, message: str = "Rate limit exceeded", *, retry_after: int = 6

class ExecutionError(ChassisError):
"""Runtime execution failure (DB down, timeout, etc.) → HTTP 500."""

status_code: int = 500
17 changes: 13 additions & 4 deletions chassis/health.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,10 @@ async def check_all(self) -> dict[str, Any]:
)
total_ms = (time.perf_counter() - start) * 1000

checks = {r.name: {"healthy": r.healthy, "latency_ms": r.latency_ms, "detail": r.detail} for r in results}
checks = {
r.name: {"healthy": r.healthy, "latency_ms": r.latency_ms, "detail": r.detail}
for r in results
}
all_healthy = all(r.healthy for r in results)
any_healthy = any(r.healthy for r in results)

Expand All @@ -114,7 +117,9 @@ async def check_one(self, name: str) -> ProbeResult:
"""Run a single named probe."""
fn = self._probes.get(name)
if fn is None:
return ProbeResult(name=name, healthy=False, latency_ms=0, detail="probe not registered")
return ProbeResult(
name=name, healthy=False, latency_ms=0, detail="probe not registered"
)
return await self._run_probe(name, fn)

async def _run_probe(self, name: str, fn: ProbeFunc) -> ProbeResult:
Expand All @@ -126,11 +131,15 @@ async def _run_probe(self, name: str, fn: ProbeFunc) -> ProbeResult:
except asyncio.TimeoutError:
latency = (time.perf_counter() - start) * 1000
logger.warning("Health probe %s timed out after %.0fms", name, latency)
return ProbeResult(name=name, healthy=False, latency_ms=round(latency, 2), detail="timeout")
return ProbeResult(
name=name, healthy=False, latency_ms=round(latency, 2), detail="timeout"
)
except Exception as exc:
latency = (time.perf_counter() - start) * 1000
logger.warning("Health probe %s failed: %s", name, exc)
return ProbeResult(name=name, healthy=False, latency_ms=round(latency, 2), detail=str(exc))
return ProbeResult(
name=name, healthy=False, latency_ms=round(latency, 2), detail=str(exc)
)

@property
def probe_names(self) -> list[str]:
Expand Down
5 changes: 5 additions & 0 deletions chassis/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@

# ── Request ID / Trace Propagation ────────────────────────────────────


class RequestIDMiddleware(BaseHTTPMiddleware):
"""
Injects X-Request-ID and X-Trace-ID headers.
Expand Down Expand Up @@ -64,6 +65,7 @@ async def dispatch(self, request: Request, call_next: Callable) -> Response:

# ── Request Timing ────────────────────────────────────────────────────


class TimingMiddleware(BaseHTTPMiddleware):
"""
Measures request duration and sets X-Process-Time-Ms header.
Expand Down Expand Up @@ -95,6 +97,7 @@ async def dispatch(self, request: Request, call_next: Callable) -> Response:

# ── Security Headers ──────────────────────────────────────────────────


class SecurityHeadersMiddleware(BaseHTTPMiddleware):
"""
Adds standard security headers to every response.
Expand All @@ -113,6 +116,7 @@ async def dispatch(self, request: Request, call_next: Callable) -> Response:

# ── Structured Request Logger ─────────────────────────────────────────


class StructuredLogMiddleware(BaseHTTPMiddleware):
"""
Emits one structured JSON log line per request.
Expand Down Expand Up @@ -144,6 +148,7 @@ async def dispatch(self, request: Request, call_next: Callable) -> Response:

# ── Convenience: Apply All ────────────────────────────────────────────


def apply_chassis_middleware(
app,
*,
Expand Down
19 changes: 16 additions & 3 deletions chassis/orchestrator.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
"""Public entrypoint runtime for the L9 Constellation Runtime v1.0.0."""

import time
from constellation.types import (
PacketEnvelope, normalize_packet, TerminalResult, ConstellationError, _uid
PacketEnvelope,
normalize_packet,
TerminalResult,
ConstellationError,
_uid,
)
from constellation.action_registry import ACTION_MAP, get_action_handler
from constellation.node_registry import list_nodes, get_node
Expand All @@ -17,9 +22,11 @@
"cost_total": 0.0,
}


def register_domain(domain: str):
_DOMAINS.add(domain)


def execute(request: dict) -> dict:
start = time.time()
_METRICS["request_count"] += 1
Expand Down Expand Up @@ -64,10 +71,14 @@ def execute(request: dict) -> dict:
"action": request.get("action", "unknown"),
"domain": request.get("domain", "unknown"),
"data": {"error": str(exc)},
"meta": {"trace_id": request.get("trace_id", _uid()),
"execution_ms": round(elapsed, 2), "node_hops": []},
"meta": {
"trace_id": request.get("trace_id", _uid()),
"execution_ms": round(elapsed, 2),
"node_hops": [],
},
}


def health() -> dict:
nodes = list_nodes()
return {
Expand All @@ -78,9 +89,11 @@ def health() -> dict:
"domains_registered": len(_DOMAINS),
}


def metrics() -> dict:
return dict(_METRICS)


def validate_startup():
errors = []
if not ACTION_MAP:
Expand Down
Loading
Loading