Files
ITWorx_MCP_Hub-Public/services/runtime/control_api.py
T

2565 lines
111 KiB
Python

from __future__ import annotations
import base64
import hashlib
import json
import os
import re
import stat
from collections.abc import AsyncIterator, Iterable
from concurrent.futures import ThreadPoolExecutor
from contextlib import asynccontextmanager
from datetime import UTC, datetime, timedelta
from pathlib import Path
from typing import Any, Protocol, cast
from urllib.parse import urlparse
from uuid import NAMESPACE_URL, UUID, uuid4, uuid5
import httpx
import yaml # type: ignore[import-untyped]
from fastapi import Depends, FastAPI, HTTPException, Query, Request, Response, status
from fastapi.responses import JSONResponse
from jsonschema import Draft202012Validator # type: ignore[import-untyped]
from pydantic import BaseModel, ConfigDict, Field, field_validator
from sqlalchemy import func, select, text
from sqlalchemy.engine import Engine
from packages.hub_clients import ClientKind, ClientRecord, ClientStatus
from packages.hub_policy import Decision, Governance, PolicyDenied, PolicySimulator
from services.control_api.auth import BFFService, OIDCProviderClient, OIDCSettings, PrincipalMapper
from services.control_api.auth.errors import AuthError
from services.control_api.auth.models import AuthenticatedPrincipal, PrincipalKind
from services.control_api.auth.router import build_auth_router
from services.control_api.authz import InsufficientRole, check_role
from services.control_api.catalog import CatalogLoader
from services.control_api.clients import build_client_router
from services.control_api.contextforge import ContextForgeClient, ContextForgeSettings
from services.control_api.persistence import create_engine_from_url, session_factory
from services.control_api.persistence.models import (
AuditEventRow,
ClientBindingRow,
ClientRow,
ConnectorRow,
EnvironmentRow,
ProjectRow,
VirtualServerRow,
WorkspaceRow,
)
from .audit import SqlAlchemyAuditLog
from .client_service import PersistentClientService
from .common import hub_database_url, identity_assertion, load_connector_identity_key, run_asgi
from .dataplane import _policy_engine
from .state_sync import CONNECTOR_GATEWAYS, reconcile_runtime_state
class ClientLister(Protocol):
"""The minimal shape simulate_policy() needs -- lets tests pass a plain
in-memory stand-in instead of a real database-backed ClientService."""
def list(self) -> Iterable[ClientRecord]: ...
RESOURCE_FOLDERS = {
"projects": "projects",
"servers": "projects",
"tools": "tools",
"connectors": "connectors",
"policies": "policies",
}
CONNECTOR_GATEWAY_BY_TYPE = {
slug.removeprefix("itworx-"): slug for slug, _url in CONNECTOR_GATEWAYS
}
CONNECTOR_READINESS_URL_BY_SLUG = {
slug: f"{url.removesuffix('/mcp')}/health/ready" for slug, url in CONNECTOR_GATEWAYS
}
CONNECTOR_URL_BY_TYPE = {slug.removeprefix("itworx-"): url for slug, url in CONNECTOR_GATEWAYS}
class ToolReadinessRequest(BaseModel):
"""Bounded management-plane request; never a general MCP invocation."""
model_config = ConfigDict(extra="forbid")
project: str = Field(pattern=r"^[a-z0-9][a-z0-9-]{0,62}$")
environment: str = Field(pattern=r"^(dev|test|staging|prod)$")
tool: str = Field(min_length=1, max_length=256)
arguments: dict[str, Any] = Field(default_factory=dict)
@field_validator("arguments")
@classmethod
def bound_arguments(cls, value: dict[str, Any]) -> dict[str, Any]:
if len(value) > 64:
raise ValueError("arguments contain too many top-level properties")
serialized = json.dumps(value, separators=(",", ":"), ensure_ascii=False).encode("utf-8")
if len(serialized) > 32768:
raise ValueError("arguments exceed the readiness-check size limit")
return value
class ToolInvokeRequest(ToolReadinessRequest):
"""The management console can invoke only cataloged read-only tools."""
def read_backup_status(path: Path, *, now: datetime | None = None) -> dict[str, Any]:
"""Read a bounded operator-produced status projection, never backup contents."""
unknown = {
"state": "unknown",
"lastSuccessfulAt": None,
"detail": "No verified backup status has been published yet.",
"restoreVerified": False,
"artifactCount": 0,
}
try:
info = path.lstat()
if not stat.S_ISREG(info.st_mode) or path.is_symlink() or info.st_size > 65536:
return unknown
value = json.loads(path.read_text(encoding="utf-8"))
if not isinstance(value, dict) or value.get("schemaVersion") != "1":
return unknown
completed = datetime.fromisoformat(str(value.get("completedAt", "")))
if completed.tzinfo is None or completed.utcoffset() is None:
return unknown
restore_verified = value.get("restoreVerified") is True
artifact_count = int(value.get("artifactCount", 0))
if not restore_verified or not 1 <= artifact_count <= 1000:
return {
**unknown,
"state": "degraded",
"detail": "The latest backup status is incomplete or lacks restore verification.",
"lastSuccessfulAt": completed.astimezone(UTC).isoformat(),
"artifactCount": max(0, artifact_count),
}
age = (now or datetime.now(UTC)) - completed.astimezone(UTC)
stale_after = timedelta(hours=int(os.environ.get("HUB_BACKUP_STALE_AFTER_HOURS", "26")))
stale = age > stale_after or age < timedelta(minutes=-5)
return {
"state": "degraded" if stale else "healthy",
"lastSuccessfulAt": completed.astimezone(UTC).isoformat(),
"detail": (
"Latest backup is restore-verified but older than the freshness target."
if stale
else "Latest backup and isolated restore verification succeeded."
),
"restoreVerified": True,
"artifactCount": artifact_count,
}
except (OSError, ValueError, TypeError, json.JSONDecodeError):
return unknown
def _documents(root: Path, folder: str) -> list[dict[str, Any]]:
values: list[dict[str, Any]] = []
for path in sorted((root / folder).glob("*.yaml")):
document = yaml.safe_load(path.read_text(encoding="utf-8"))
if isinstance(document, dict):
values.append(document)
return values
def _catalog_registration_health(document: dict[str, Any]) -> tuple[str, str]:
"""Whether a catalog document is a well-formed, registered entry.
This aggregate list signal is registration validity, not an on-demand
readiness probe. Operators can run the bounded readiness workflow for
individual tool paths. Shared between resource rows and the dashboard
overview so those surfaces do not disagree.
"""
if document.get("kind"):
return (
"healthy",
"Catalog entry is registered and well-formed; "
"use tool readiness for a live path check.",
)
return "unknown", "Catalog entry is missing or malformed."
def _row(document: dict[str, Any], kind: str) -> dict[str, Any]:
metadata = cast(
dict[str, Any],
document.get("metadata") if isinstance(document.get("metadata"), dict) else {},
)
spec = cast(
dict[str, Any],
document.get("spec") if isinstance(document.get("spec"), dict) else {},
)
slug = str(metadata.get("slug", "unknown"))[:128]
name = str(metadata.get("name", slug))[:256]
summary = str(spec.get("description", document.get("kind", kind)))[:1000]
status, _detail = _catalog_registration_health(document)
return {
"id": slug,
"name": name,
"status": status,
"summary": summary,
"project": str(spec.get("project", "")) or None,
"environment": None,
"risk": str(spec.get("riskClass", "R0")) if kind == "tools" else None,
# The catalog slug (`id` above) and canonicalName are different
# strings (e.g. "deployment-project-status" vs
# "deployment.project.status") -- callers that need to invoke or
# simulate this tool (not just display it) need the latter.
"canonicalName": str(spec.get("canonicalName")) if kind == "tools" else None,
}
def _is_onboarding_metadata(metadata: dict[str, Any]) -> bool:
"""Recognize only Hub-created pending clients and their external activation form."""
if metadata.get("managedBy") == "runtime-onboarding":
return True
return (
metadata.get("managedBy") == "authentik-machine-provisioning"
and metadata.get("identityProvisioned") is True
and metadata.get("kind") in {kind.value for kind in ClientKind}
and isinstance(metadata.get("oauthUsername"), str)
and str(metadata.get("oauthUsername", "")).startswith("svc-mcp-codex-")
)
def _verify_contextforge_dependency(contextforge: ContextForgeClient) -> None:
health = contextforge.health()
if health.get("status") not in {"ready", "healthy", "ok"}:
raise RuntimeError("gateway not healthy")
contextforge.list_gateways(limit=1)
def measure_connector_drift(contextforge: ContextForgeClient) -> dict[str, Any]:
"""Compare ContextForge with the connector state production reconciliation owns.
Project virtual servers are a separate resource type and must not be compared
with connector gateways. Keeping this desired state sourced from
``CONNECTOR_GATEWAYS`` makes the dashboard and ``reconcile_runtime_state``
describe the same operational reality.
"""
desired = {slug: url.rstrip("/") for slug, url in CONNECTOR_GATEWAYS}
actual = {gateway.slug: gateway for gateway in contextforge.list_gateways(limit=100)}
affected = {
slug
for slug, expected_url in desired.items()
if (gateway := actual.get(slug)) is None
or not gateway.enabled
or gateway.url.rstrip("/") != expected_url
}
affected.update(set(actual) - set(desired))
ordered = sorted(affected)
return {
"count": len(ordered),
"projects": ordered,
"known": True,
"detail": (
"No connector gateway drift measured."
if not ordered
else "Measured connector gateway drift requires review before reconciliation."
),
}
_CURSOR_TTL_SECONDS = 3600
_AUDIT_FETCH_CAP = 1000 # v1 scale bound: bounded audit browsing, not unbounded history search
def encode_page_cursor(offset: int) -> str:
"""Opaque cursor: offset + issue time, so an expired cursor can be
rejected outright instead of silently wrapping around or resetting to
page 1 (which would look like success while returning the wrong page)."""
payload = f"{offset}:{int(datetime.now(UTC).timestamp())}"
return base64.urlsafe_b64encode(payload.encode("utf-8")).decode("ascii")
def decode_page_cursor(cursor: str) -> int:
"""Raises HTTPException(400) for a malformed or expired cursor -- fails
safely (visible 400, never a 500, never a silent wraparound to page 1)."""
try:
decoded = base64.urlsafe_b64decode(cursor.encode("ascii")).decode("utf-8")
offset_text, issued_text = decoded.split(":", 1)
offset = int(offset_text)
issued_at = int(issued_text)
except Exception as exc:
raise HTTPException(status_code=400, detail="invalid pagination cursor") from exc
if offset < 0:
raise HTTPException(status_code=400, detail="invalid pagination cursor")
if datetime.now(UTC).timestamp() - issued_at > _CURSOR_TTL_SECONDS:
raise HTTPException(
status_code=400,
detail="pagination cursor has expired; restart from the first page",
)
return offset
_ALLOW_DECISIONS = frozenset(
{
Decision.ALLOW,
Decision.ALLOW_WITH_PREVIEW,
Decision.ALLOW_WITH_APPROVAL,
Decision.ALLOW_IF_CONDITIONS,
}
)
def simulate_policy(
catalog_root: Path,
clients: ClientLister,
payload: dict[str, Any],
principal: AuthenticatedPrincipal,
) -> dict[str, Any]:
"""Real policy simulation: the exact evaluator production traffic uses
(`_policy_engine` from services/runtime/dataplane.py, unmodified),
against catalog-derived context -- not a hand-maintained stub. Never
mutates runtime state: PolicySimulator only ever calls
PolicyEngine.evaluate(), which reads policy rules and returns a
DecisionTrace; nothing here writes to the catalog, the database, or any
external system.
Module-level (not a nested closure inside build_app()) specifically so
it's unit-testable without constructing a full FastAPI app -- see
tests/test_wp230_policy_simulate.py.
"""
project_slug = str(payload.get("project", ""))
environment = str(payload.get("environment", ""))
tool_name = str(payload.get("tool", ""))
projects_by_slug = {
str(doc.get("metadata", {}).get("slug", "")): doc
for doc in _documents(catalog_root, "projects")
}
tools_by_name = {
str(doc.get("spec", {}).get("canonicalName", "")): doc
for doc in _documents(catalog_root, "tools")
}
connector_type_by_slug = {
str(doc.get("metadata", {}).get("slug", "")): str(
doc.get("spec", {}).get("connectorType", "")
)
for doc in _documents(catalog_root, "connectors")
}
project_doc = projects_by_slug.get(project_slug)
project_spec = cast(dict[str, Any], project_doc.get("spec", {})) if project_doc else {}
tool_doc = tools_by_name.get(tool_name)
tool_spec = cast(dict[str, Any], tool_doc.get("spec", {})) if tool_doc else {}
connector_type = str(tool_spec.get("connectorType", "unknown"))
risk_class = str(tool_spec.get("riskClass", "R1"))
required_scopes = list(tool_spec.get("requiredScopes", []))
environments_spec = cast(dict[str, Any], project_spec.get("environments") or {})
environment_spec = cast(dict[str, Any], environments_spec.get(environment) or {})
environment_classification = environment_spec.get("classification")
# Real capability-pack/binding lookup: is there a registered connector
# binding, for this project, of this tool's connector type, covering
# this environment? This is exactly what "capability pack or binding"
# in the WP-230B acceptance criteria means -- not a caller-supplied flag.
binding_present = any(
connector_type_by_slug.get(str(cast(dict[str, Any], binding).get("ref", "")))
== connector_type
and environment in (cast(dict[str, Any], binding).get("environments") or [])
for binding in project_spec.get("connectorBindings", [])
if isinstance(binding, dict)
)
client_id: UUID | None = None
raw_client_id = payload.get("clientId")
if raw_client_id:
try:
client_id = UUID(str(raw_client_id))
except ValueError:
client_id = None # invalid input -> simulated as an absent/unknown client, not a 400
known_clients = {record.id for record in clients.list()}
client_enabled = False
if client_id is not None:
matching = next((record for record in clients.list() if record.id == client_id), None)
client_enabled = bool(matching and matching.status == ClientStatus.ENABLED)
# Caller-supplied overrides are honored (a deliberate "what if" simulation
# capability -- e.g. "what would happen if this client were bound") but
# every value has a real, catalog/database-derived default rather than an
# unconditional True the way the old stub effectively assumed.
context = Governance.context(
workspace="itworx",
project=project_slug,
environment=environment,
principal=principal.subject,
principal_type=principal.kind.value,
tool=tool_name,
connector=connector_type,
arguments={"requestedScopes": payload.get("scopes", required_scopes)},
client_id=client_id,
binding_present=bool(payload.get("bindingPresent", binding_present)),
client_enabled=bool(payload.get("clientEnabled", client_enabled)),
capability=payload.get("capability"),
risk_class=risk_class,
environment_classification=environment_classification,
)
engine = _policy_engine(catalog_root)
simulator = PolicySimulator(
engine,
known_projects=set(projects_by_slug),
known_clients=known_clients,
known_tools=set(tools_by_name),
)
trace = simulator.simulate(context)
return {
"decision": trace.decision.value,
"allowed": trace.decision in _ALLOW_DECISIONS,
"matchedPolicies": list(trace.matched_rule_ids),
"reasons": [trace.reason_code],
"reasonCode": trace.reason_code,
"traceId": trace.trace_id,
"requiredScopes": required_scopes,
"steps": [step.model_dump(mode="json") for step in trace.steps],
}
def _tool_input_errors(schema: dict[str, Any], arguments: dict[str, Any]) -> list[str]:
"""Return bounded, non-value-bearing JSON Schema diagnostics."""
errors = sorted(
Draft202012Validator(schema).iter_errors(arguments),
key=lambda error: tuple(str(part) for part in error.absolute_path),
)
diagnostics: list[str] = []
for error in errors[:10]:
path = ".".join(str(part) for part in error.absolute_path) or "$"
diagnostics.append(f"{path}: {error.validator or 'schema'} validation failed")
return diagnostics
def _direct_connector_readiness(gateway_slug: str) -> dict[str, Any]:
"""Use an allowlisted internal probe when ContextForge has no test route."""
url = CONNECTOR_READINESS_URL_BY_SLUG.get(gateway_slug)
if url is None:
return {
"reachable": False,
"status": "unsupported",
"detail": "No bounded connector readiness route is registered.",
}
try:
response = httpx.get(url, timeout=5.0, follow_redirects=False)
if response.status_code != 200 or len(response.content) > 65536:
raise ValueError("connector readiness response was not acceptable")
payload = response.json()
status_value = (
str(payload.get("status", "unknown")) if isinstance(payload, dict) else "unknown"
)
reachable = status_value in {"ready", "healthy"}
return {
"reachable": reachable,
"status": status_value if reachable else "unavailable",
"detail": (
"Direct connector readiness verified."
if reachable
else "Connector readiness did not report ready."
),
}
except Exception:
return {
"reachable": False,
"status": "unavailable",
"detail": "Connector readiness could not be verified.",
}
def _connector_readiness_snapshot(gateway_slug: str) -> dict[str, Any]:
"""Return a small safe projection of one fixed-route live probe."""
if gateway_slug not in CONNECTOR_READINESS_URL_BY_SLUG:
return {
"reachable": None,
"status": "unknown",
"detail": "No allowlisted live readiness route is registered for this connector.",
"observedAt": None,
"source": "unavailable",
}
result = _direct_connector_readiness(gateway_slug)
status_value = str(result.get("status", "unknown"))
if status_value not in {"ready", "healthy", "unavailable", "unsupported"}:
status_value = "unknown"
return {
"reachable": result.get("reachable") is True,
"status": status_value,
"detail": str(result.get("detail", "Connector readiness could not be verified."))[:256],
"observedAt": datetime.now(UTC).isoformat(),
"source": "direct-probe",
}
def _project_connector_readiness(
project_doc: dict[str, Any],
environment: str,
connector_type_by_slug: dict[str, str],
) -> dict[str, Any]:
"""Measure a bounded set of catalog-bound connector routes for onboarding."""
spec = cast(dict[str, Any], project_doc.get("spec", {}))
connector_slugs = sorted(
{
str(binding.get("ref", ""))
for binding in spec.get("connectorBindings", [])
if isinstance(binding, dict)
and environment in (binding.get("environments") or [])
and str(binding.get("ref", "")) in connector_type_by_slug
}
)
if not connector_slugs:
return {
"state": "unknown",
"detail": "No registered connector binding covers this project environment.",
"observedAt": None,
}
connector_types = {connector_type_by_slug[slug] for slug in connector_slugs}
gateway_slugs = sorted(
{
CONNECTOR_GATEWAY_BY_TYPE[connector_type]
for connector_type in connector_types
if connector_type in CONNECTOR_GATEWAY_BY_TYPE
}
)
unsupported_count = len(connector_types) - len(gateway_slugs)
bounded_gateway_slugs = gateway_slugs[:4]
if not bounded_gateway_slugs:
return {
"state": "unavailable",
"detail": "Bound connectors have no registered allowlisted readiness routes.",
"observedAt": None,
}
with ThreadPoolExecutor(max_workers=min(4, len(bounded_gateway_slugs))) as pool:
results = tuple(pool.map(_connector_readiness_snapshot, bounded_gateway_slugs))
observed_at = datetime.now(UTC).isoformat()
ready_count = sum(result.get("reachable") is True for result in results)
fully_measured = unsupported_count == 0 and len(gateway_slugs) <= 4
if ready_count == len(results) and fully_measured:
state_value = "healthy"
detail = (
"All registered connector routes for this environment passed direct readiness probes."
)
elif ready_count:
state_value = "degraded"
detail = "Some connector routes are ready; others are unavailable or could not be measured."
else:
state_value = "unavailable"
detail = "No measured connector route reported ready for this environment."
return {"state": state_value, "detail": detail, "observedAt": observed_at}
def evaluate_tool_readiness(
catalog_root: Path,
clients: ClientLister,
contextforge: ContextForgeClient,
request: ToolReadinessRequest,
principal: AuthenticatedPrincipal,
) -> dict[str, Any]:
"""Validate a tool path without invoking the tool or returning input values."""
projects_by_slug = {
str(doc.get("metadata", {}).get("slug", "")): doc
for doc in _documents(catalog_root, "projects")
}
tools_by_name = {
str(doc.get("spec", {}).get("canonicalName", "")): doc
for doc in _documents(catalog_root, "tools")
}
connector_type_by_slug = {
str(doc.get("metadata", {}).get("slug", "")): str(
doc.get("spec", {}).get("connectorType", "")
)
for doc in _documents(catalog_root, "connectors")
}
project_doc = projects_by_slug.get(request.project)
if project_doc is None:
raise HTTPException(status_code=404, detail="project is not registered")
tool_doc = tools_by_name.get(request.tool)
if tool_doc is None:
raise HTTPException(status_code=404, detail="tool is not registered")
project_spec = cast(dict[str, Any], project_doc.get("spec", {}))
environments = cast(dict[str, Any], project_spec.get("environments") or {})
if request.environment not in environments:
raise HTTPException(status_code=422, detail="environment is not registered for project")
tool_spec = cast(dict[str, Any], tool_doc.get("spec", {}))
connector_type = str(tool_spec.get("connectorType", ""))
read_only = bool(tool_spec.get("readOnly", False))
binding_present = any(
connector_type_by_slug.get(str(binding.get("ref", ""))) == connector_type
and request.environment in (binding.get("environments") or [])
and binding.get("verification") == "verified"
for binding in project_spec.get("connectorBindings", [])
if isinstance(binding, dict)
)
input_schema = cast(dict[str, Any], tool_spec.get("inputSchema") or {})
input_errors = _tool_input_errors(input_schema, request.arguments)
policy = simulate_policy(
catalog_root,
clients,
{
"project": request.project,
"environment": request.environment,
"tool": request.tool,
"bindingPresent": binding_present,
"clientEnabled": True,
},
principal,
)
gateway_slug = CONNECTOR_GATEWAY_BY_TYPE.get(connector_type)
if gateway_slug is None:
connector_check = {
"reachable": False,
"status": "not-deployed",
"detail": "No production connector gateway is registered for this connector type.",
}
else:
try:
result = contextforge.test_gateway_by_slug(gateway_slug)
connector_check = result.model_dump(mode="json")
if connector_check["status"] == "unsupported":
connector_check = _direct_connector_readiness(gateway_slug)
except Exception:
connector_check = {
"reachable": False,
"status": "unavailable",
"detail": "Connector readiness could not be verified.",
}
ready = bool(
read_only
and not input_errors
and binding_present
and policy["allowed"]
and connector_check["reachable"]
)
return {
"ready": ready,
"tool": request.tool,
"project": request.project,
"environment": request.environment,
"checks": {
"catalog": {"valid": True, "readOnly": read_only, "connectorType": connector_type},
"input": {"valid": not input_errors, "errors": input_errors},
"binding": {
"valid": binding_present,
"detail": (
"Project connector binding covers this environment."
if binding_present
else "Project connector binding is missing for this environment."
),
},
"policy": {
"allowed": policy["allowed"],
"decision": policy["decision"],
"reasonCode": policy["reasonCode"],
"traceId": policy["traceId"],
},
"connector": connector_check,
},
"note": "Readiness only; no MCP tool invocation was performed.",
}
def _schema_projection(value: Any, schema: dict[str, Any]) -> Any:
if schema.get("type") == "object" and isinstance(value, dict):
properties = schema.get("properties")
if not isinstance(properties, dict):
return value
projected = {
name: _schema_projection(value[name], cast(dict[str, Any], child_schema))
for name, child_schema in properties.items()
if name in value and isinstance(child_schema, dict)
}
additional = schema.get("additionalProperties", True)
if additional is not False:
additional_schema = additional if isinstance(additional, dict) else {}
projected.update(
{
name: _schema_projection(item, additional_schema)
for name, item in value.items()
if name not in properties
}
)
return projected
if schema.get("type") == "array" and isinstance(value, list):
item_schema = schema.get("items")
if isinstance(item_schema, dict):
return [_schema_projection(item, item_schema) for item in value]
return value
def _project_connector_output(
structured_content: Any, output_schema: dict[str, Any]
) -> tuple[Any, list[str]]:
"""Project a standard connector envelope onto its registered public contract."""
if not isinstance(structured_content, dict) or "data" not in structured_content:
return structured_content, []
meta = structured_content.get("meta")
warnings = structured_content.get("warnings")
if (
not isinstance(meta, dict)
or not isinstance(warnings, list)
or len(warnings) > 20
or any(not isinstance(item, str) or len(item) > 500 for item in warnings)
):
raise ValueError("connector envelope is invalid")
data = structured_content["data"]
properties = output_schema.get("properties")
if isinstance(data, dict) and isinstance(properties, dict):
data = {
**data,
**{name: meta[name] for name in properties if name not in data and name in meta},
}
return _schema_projection(data, output_schema), warnings
def invoke_read_only_tool(
catalog_root: Path,
clients: ClientLister,
contextforge: ContextForgeClient,
sessions: Any,
request: ToolInvokeRequest,
principal: AuthenticatedPrincipal,
signing_key: bytes,
*,
transport: httpx.BaseTransport | None = None,
) -> dict[str, Any]:
"""Invoke one cataloged read-only connector through a bounded operator path."""
readiness = evaluate_tool_readiness(catalog_root, clients, contextforge, request, principal)
if not readiness["ready"]:
raise HTTPException(status_code=409, detail="tool path is not ready")
tool_doc = next(
doc
for doc in _documents(catalog_root, "tools")
if str(doc.get("spec", {}).get("canonicalName", "")) == request.tool
)
tool_spec = cast(dict[str, Any], tool_doc.get("spec", {}))
if tool_spec.get("readOnly") is not True:
raise HTTPException(status_code=403, detail="management console permits read-only tools")
connector_type = str(tool_spec.get("connectorType", ""))
connector_url = CONNECTOR_URL_BY_TYPE.get(connector_type)
if connector_url is None:
raise HTTPException(status_code=409, detail="connector is not registered")
console_client_id = uuid5(
NAMESPACE_URL, f"https://hub.example.com/management-console/{principal.subject}"
)
governance = Governance(
_policy_engine(catalog_root),
SqlAlchemyAuditLog(sessions, issuer=principal.issuer),
)
context = Governance.context(
workspace="itworx",
project=request.project,
environment=request.environment,
principal=principal.subject,
principal_type=principal.kind.value,
client_id=console_client_id,
binding_present=True,
client_enabled=True,
tool=request.tool,
connector=connector_type,
arguments=request.arguments,
resource_type="tool",
risk_class=str(tool_spec.get("riskClass", "R1")),
environment_classification=(
"production" if request.environment == "prod" else "development"
),
)
try:
trace, started = governance.begin(context)
except PolicyDenied as exc:
raise HTTPException(
status_code=403, detail="policy denied management-console invocation"
) from exc
headers = {
"Accept": "application/json",
"Content-Type": "application/json",
"MCP-Protocol-Version": "2025-11-25",
"x-itworx-project": request.project,
"x-itworx-environment": request.environment,
"x-itworx-client-id": str(console_client_id),
"x-itworx-trace-id": trace.trace_id,
"x-itworx-identity-assertion": identity_assertion(
signing_key,
project=request.project,
environment=request.environment,
client_id=str(console_client_id),
trace_id=trace.trace_id,
),
}
body = {
"jsonrpc": "2.0",
"id": trace.trace_id,
"method": "tools/call",
"params": {"name": request.tool, "arguments": request.arguments},
}
max_output = min(int(tool_spec.get("limits", {}).get("maxOutputBytes", 131072)), 262144)
timeout_seconds = min(
max(float(tool_spec.get("limits", {}).get("timeoutMs", 10000)) / 1000, 1.0),
20.0,
)
try:
with httpx.Client(
transport=transport, timeout=timeout_seconds, follow_redirects=False
) as client:
response = client.post(connector_url, headers=headers, json=body)
if response.status_code != 200 or len(response.content) > max_output:
governance.finish(
context,
trace,
started,
outcome="error",
error_code="CONNECTOR_RESPONSE_REJECTED",
)
raise HTTPException(status_code=502, detail="connector response was not acceptable")
document = response.json()
if not isinstance(document, dict):
raise ValueError("connector response is not an object")
result = document.get("result")
error = document.get("error")
if error is not None or not isinstance(result, dict):
governance.finish(context, trace, started, outcome="error", error_code="MCP_TOOL_ERROR")
return {
"ok": False,
"traceId": trace.trace_id,
"tool": request.tool,
"error": {"code": "MCP_TOOL_ERROR", "message": "The tool returned an error."},
}
is_error = result.get("isError") is True
output_schema = cast(dict[str, Any], tool_spec.get("outputSchema") or {})
structured_content = result.get("structuredContent")
try:
contract_content, connector_warnings = _project_connector_output(
structured_content, output_schema
)
except ValueError as exc:
governance.finish(
context,
trace,
started,
outcome="error",
error_code="OUTPUT_ENVELOPE_INVALID",
)
raise HTTPException(
status_code=502, detail="connector output envelope was not acceptable"
) from exc
output_errors = list(Draft202012Validator(output_schema).iter_errors(contract_content))
if not is_error and output_errors:
governance.finish(
context,
trace,
started,
outcome="error",
error_code="OUTPUT_SCHEMA_INVALID",
)
raise HTTPException(
status_code=502, detail="connector output failed the registered contract"
)
governance.finish(
context,
trace,
started,
outcome="error" if is_error else "success",
error_code="MCP_TOOL_ERROR" if is_error else None,
result_summary=(
(contract_content.get("data") or {}).get("auditEvidence")
if not is_error and request.tool == "gitea.pullrequest.merge"
else None
),
)
return {
"ok": not is_error,
"traceId": trace.trace_id,
"tool": request.tool,
"structuredContent": contract_content,
"warnings": connector_warnings,
}
except HTTPException:
raise
except (httpx.HTTPError, ValueError, json.JSONDecodeError) as exc:
governance.finish(
context, trace, started, outcome="error", error_code="CONNECTOR_UNAVAILABLE"
)
raise HTTPException(status_code=502, detail="connector invocation failed safely") from exc
def operations_snapshot(sessions: Any, backup_path: Path) -> dict[str, Any]:
"""Return aggregated, bounded operational signals without audit payloads."""
now = datetime.now(UTC)
since = now - timedelta(hours=24)
with sessions() as session:
rows = session.execute(
select(AuditEventRow.occurred_at, AuditEventRow.outcome, AuditEventRow.tool_name)
.where(AuditEventRow.occurred_at >= since)
.order_by(AuditEventRow.occurred_at)
.limit(5001)
).all()
truncated = len(rows) > 5000
rows = rows[:5000]
buckets: dict[str, dict[str, int]] = {}
for occurred_at, outcome, _tool in rows:
key = occurred_at.astimezone(UTC).replace(minute=0, second=0, microsecond=0).isoformat()
bucket = buckets.setdefault(key, {"success": 0, "denied": 0, "error": 0, "rate_limited": 0})
if outcome in bucket:
bucket[outcome] += 1
totals = {"success": 0, "denied": 0, "error": 0, "rate_limited": 0}
for bucket in buckets.values():
for outcome in totals:
totals[outcome] += bucket[outcome]
backup = read_backup_status(backup_path, now=now)
alerts: list[dict[str, str]] = []
if backup["state"] != "healthy":
alerts.append(
{"severity": "warning", "code": "BACKUP_ATTENTION", "message": backup["detail"]}
)
if totals["error"]:
alerts.append(
{
"severity": "warning",
"code": "TOOL_ERRORS",
"message": f"{totals['error']} governed call errors occurred in the last 24 hours.",
}
)
if totals["denied"] + totals["rate_limited"] >= 10:
alerts.append(
{
"severity": "info",
"code": "AUTHORIZATION_ACTIVITY",
"message": "Authorization denials or rate limits need review.",
}
)
if truncated:
alerts.append(
{
"severity": "info",
"code": "AUDIT_AGGREGATE_TRUNCATED",
"message": "The 24-hour aggregate reached its 5,000-event display bound.",
}
)
return {
"generatedAt": now.isoformat(),
"backup": backup,
"audit24h": totals,
"auditTruncated": truncated,
"trend": [{"hour": hour, **values} for hour, values in sorted(buckets.items())],
"alerts": alerts,
}
def build_app(
*,
oidc: OIDCSettings | None = None,
provider_transport: httpx.BaseTransport | None = None,
contextforge_settings: ContextForgeSettings | None = None,
contextforge_transport: httpx.BaseTransport | None = None,
engine: Engine | None = None,
catalog_root: Path | None = None,
) -> FastAPI:
"""Build the production ASGI app.
Every keyword argument is an optional injection point for tests: with
none given, behavior is identical to before (settings from environment,
real network transports, a real database engine). Tests that need a
fully real app -- to exercise actual route wiring and RBAC end to end,
not just the underlying matrix in isolation -- pass a fake OIDC/ContextForge
transport (`services/control_api/auth/fake_provider.py`,
`services/control_api/contextforge/fake.py`) and a throwaway SQLite engine.
"""
catalog_root = (
catalog_root or Path(os.environ.get("HUB_CATALOG_ROOT", "/opt/hub/catalog")).resolve()
)
if not catalog_root.is_dir():
raise ValueError("catalog root is unavailable")
oidc = oidc or OIDCSettings.from_env()
provider = OIDCProviderClient(oidc, transport=provider_transport)
mapper = PrincipalMapper(role_group_map=oidc.role_group_map)
bff = BFFService(oidc, provider, principal_mapper=mapper.map_claims)
contextforge = ContextForgeClient(
contextforge_settings or ContextForgeSettings.from_env(), transport=contextforge_transport
)
engine = engine or create_engine_from_url(hub_database_url())
sessions = session_factory(engine)
loader = CatalogLoader(
catalog_root,
public_base_url=os.environ.get("HUB_PUBLIC_MCP_URL", "https://mcp.example.com"),
)
public_mcp_url = os.environ.get("HUB_PUBLIC_MCP_URL", "https://mcp.example.com")
public_host = urlparse(public_mcp_url).hostname
if not public_host:
raise ValueError("HUB_PUBLIC_MCP_URL must contain a hostname")
clients = PersistentClientService(
sessions,
issuer=str(oidc.issuer),
public_host=public_host,
)
backup_status_path = Path(
os.environ.get("HUB_BACKUP_STATUS_FILE", "/var/lib/itworx/backup-status.json")
)
@asynccontextmanager
async def lifespan(_: FastAPI) -> AsyncIterator[None]:
try:
yield
finally:
provider.close()
contextforge.close()
engine.dispose()
app = FastAPI(
title="ITWorx MCP Hub control API",
version=os.environ.get("HUB_RELEASE_VERSION", "1.0.0-rc"),
docs_url=None,
redoc_url=None,
openapi_url=None,
lifespan=lifespan,
)
app.include_router(build_auth_router(bff))
@app.get("/healthz")
@app.get("/health")
def health() -> dict[str, str]:
return {"status": "healthy", "component": "control-api"}
@app.get("/ready")
@app.get("/health/ready")
def ready(response: Response) -> dict[str, str]:
try:
with sessions() as session:
session.execute(text("SELECT 1")).scalar_one()
_verify_contextforge_dependency(contextforge)
except Exception:
response.status_code = status.HTTP_503_SERVICE_UNAVAILABLE
return {"status": "not-ready", "component": "control-api"}
return {"status": "ready", "component": "control-api"}
@app.get("/version")
def version() -> dict[str, str]:
return {"version": os.environ.get("HUB_RELEASE_VERSION", "1.0.0-rc")}
def authenticate(request: Request) -> AuthenticatedPrincipal:
"""Verify the session is valid and carries at least one recognized
management role. This is authentication plus a coarse baseline gate,
not authorization for any specific action -- see `require()` below
for the actual per-endpoint permission check."""
try:
principal = bff.require_session(request.cookies.get(oidc.cookie_name))
except AuthError as exc:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail={"code": exc.code, "message": exc.public_message},
) from exc
if not principal.roles:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="management role is required",
)
return principal
# Bound once, reused as route defaults below: ruff (flake8-bugbear B008)
# flags calling `Depends(...)` directly inside a function signature's
# default value, the same reason the pre-WP-230 code bound
# `authorized = Depends(authorize)` once instead of inlining it per route.
authenticated = Depends(authenticate)
def require(action: str) -> Any:
"""FastAPI dependency: authenticate, then enforce the per-endpoint
permission matrix (services/control_api/authz.py) for `action`."""
def dependency(principal: AuthenticatedPrincipal = authenticated) -> AuthenticatedPrincipal:
if principal.kind is not PrincipalKind.MANAGEMENT_USER:
raise HTTPException(
status_code=403, detail="management user authorization required"
)
try:
check_role(principal, action)
except InsufficientRole as exc:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=f"role does not permit {action}",
) from exc
return principal
return Depends(dependency)
require_dashboard_read = require("dashboard.read")
require_resource_read = require("resource.read")
require_policy_simulate = require("policy.simulate")
require_tool_test = require("tool.test")
require_tool_invoke = require("tool.invoke")
require_operations_check = require("operations.check")
require_catalog_preview = require("catalog.preview")
require_catalog_apply = require("catalog.apply")
def authorize_action(principal: AuthenticatedPrincipal, action: str) -> None:
"""Non-dependency form of the same check, for routers (e.g. the client
router) that need to select an action per-request rather than
per-route."""
if principal.kind is not PrincipalKind.MANAGEMENT_USER:
raise HTTPException(status_code=403, detail="management user authorization required")
try:
check_role(principal, action)
except InsufficientRole as exc:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=f"role does not permit {action}",
) from exc
def client_onboarding_status(
client_id: UUID, *, include_live_readiness: bool = False
) -> dict[str, Any]:
with sessions() as session:
row = session.scalar(
select(ClientRow)
.join(WorkspaceRow, WorkspaceRow.id == ClientRow.workspace_id)
.where(ClientRow.id == client_id, WorkspaceRow.slug == "itworx")
)
metadata = row.record_metadata if row and isinstance(row.record_metadata, dict) else {}
if row is None or not _is_onboarding_metadata(metadata):
raise KeyError(client_id)
project_slug = str(metadata.get("project", ""))
environment_name = str(metadata.get("environment", ""))
subject = str(row.subject or "")
issuer = str(row.issuer or "")
persisted_status = str(row.status)
verification_data = metadata.get("onboardingVerification")
checked_at = (
str(verification_data.get("checkedAt"))
if isinstance(verification_data, dict)
and isinstance(verification_data.get("checkedAt"), str)
else None
)
checked_at_is_fresh = False
if checked_at:
try:
observed_at = datetime.fromisoformat(checked_at.replace("Z", "+00:00"))
if observed_at.tzinfo is None:
observed_at = observed_at.replace(tzinfo=UTC)
observation_age_seconds = (datetime.now(UTC) - observed_at).total_seconds()
checked_at_is_fresh = -60 <= observation_age_seconds <= 60
except ValueError:
pass
checked_by_management = bool(
isinstance(verification_data, dict)
and verification_data.get("checkedByManagement") is True
)
recorded_hub_checks_passed = bool(
isinstance(verification_data, dict)
and verification_data.get("hubChecksPassed") is True
)
project_row = session.scalar(
select(ProjectRow).where(
ProjectRow.workspace_id == row.workspace_id,
ProjectRow.slug == project_slug,
)
)
environment_row = (
session.scalar(
select(EnvironmentRow).where(
EnvironmentRow.workspace_id == row.workspace_id,
EnvironmentRow.project_id == project_row.id,
EnvironmentRow.name == environment_name,
)
)
if project_row is not None
else None
)
matching_bindings: list[tuple[ClientBindingRow, VirtualServerRow]] = []
if project_row is not None and environment_row is not None:
matching_bindings = [
(binding, server)
for binding, server in session.execute(
select(ClientBindingRow, VirtualServerRow)
.join(
VirtualServerRow,
VirtualServerRow.id == ClientBindingRow.virtual_server_id,
)
.where(
ClientBindingRow.workspace_id == row.workspace_id,
ClientBindingRow.client_id == row.id,
ClientBindingRow.project_id == project_row.id,
VirtualServerRow.environment_id == environment_row.id,
)
).all()
]
project_doc = next(
(
item
for item in _documents(catalog_root, "projects")
if str(item.get("metadata", {}).get("slug", "")) == project_slug
),
None,
)
project_spec = cast(dict[str, Any], project_doc.get("spec", {})) if project_doc else {}
environments = cast(dict[str, Any], project_spec.get("environments") or {})
environment_registered = environment_name in environments and isinstance(
environments[environment_name], dict
)
project_servers = [
item
for item in project_spec.get("virtualServers", [])
if isinstance(item, dict) and item.get("environment") == environment_name
]
catalog_ready = (
project_doc is not None and environment_registered and len(project_servers) == 1
)
catalog_check = {
"state": "ready" if catalog_ready else "blocked",
"detail": (
"Project, environment, and one MCP server are registered in the catalog."
if catalog_ready
else (
"The project environment or its unique MCP server is "
"not registered in the catalog."
)
),
}
identity_provisioned = (
metadata.get("managedBy") == "authentik-machine-provisioning"
and metadata.get("identityProvisioned") is True
and bool(subject)
and subject != f"itworx-mcp-client-{client_id}"
and issuer == oidc.issuer
)
external_subject = {
"state": "blocked"
if persisted_status == "revoked"
else "ready"
if identity_provisioned
else "pending",
"detail": (
"An external identity subject is durably bound to this client."
if identity_provisioned
else (
"Provision the named external service account and bind "
"its exact subject before continuing."
)
if persisted_status != "revoked"
else "This client is revoked and cannot be used for onboarding."
),
}
if persisted_status == "revoked":
binding_check = {"state": "blocked", "detail": "This client is revoked."}
elif len(matching_bindings) > 1:
binding_check = {
"state": "blocked",
"detail": "More than one Hub binding targets this project environment.",
}
elif not matching_bindings:
binding_check = {
"state": "pending",
"detail": "No persisted Hub binding targets this project environment yet.",
}
else:
binding, server = matching_bindings[0]
ready_binding = (
persisted_status == "active" and binding.enabled is True and server.enabled is True
)
binding_check = {
"state": "ready" if ready_binding else "pending",
"detail": (
(
"The persisted client binding and MCP server are "
"enabled for this exact project environment."
)
if ready_binding
else (
"The Hub binding exists but remains disabled until an "
"explicit provisioning action enables it."
)
),
}
if catalog_ready and include_live_readiness:
connector_type_by_slug = {
str(doc.get("metadata", {}).get("slug", "")): str(
doc.get("spec", {}).get("connectorType", "")
)
for doc in _documents(catalog_root, "connectors")
}
connector_check = _project_connector_readiness(
cast(dict[str, Any], project_doc), environment_name, connector_type_by_slug
)
elif catalog_ready:
persisted_check_states = (
verification_data.get("checkStates")
if isinstance(verification_data, dict)
else None
)
persisted_connector_state = (
persisted_check_states.get("connector")
if isinstance(persisted_check_states, dict)
else None
)
if checked_at_is_fresh and persisted_connector_state in {
"healthy",
"degraded",
"unavailable",
"unknown",
}:
connector_check = {
"state": persisted_connector_state,
"detail": (
"Last observed during an explicit Hub verification; "
"record checks to refresh it."
),
"observedAt": checked_at,
}
else:
connector_check = {
"state": "unknown",
"detail": "Connector readiness is not probed during onboarding status reads.",
"observedAt": None,
}
else:
connector_check = {
"state": "unknown",
"detail": (
"Connector readiness was not measured because catalog scope is incomplete."
),
"observedAt": None,
}
mapped_client_status = {
"pending": "pending",
"active": "enabled",
"disabled": "disabled",
"revoked": "revoked",
}.get(persisted_status, "disabled")
hub_checks_passed_now = (
external_subject["state"] == "ready"
and binding_check["state"] == "ready"
and catalog_check["state"] == "ready"
and connector_check["state"] == "healthy"
)
verified_by_management = (
checked_by_management and recorded_hub_checks_passed and hub_checks_passed_now
)
return {
"clientId": str(client_id),
"project": project_slug,
"environment": environment_name,
"clientStatus": mapped_client_status,
"provisioning": {
"status": (
"authentik-service-account-provisioned"
if identity_provisioned
else "pending-authentik-service-account"
),
"subject": subject if identity_provisioned else None,
"issuer": str(oidc.issuer),
"oauthUsername": (
str(metadata["oauthUsername"])
if isinstance(metadata.get("oauthUsername"), str)
and re.fullmatch(
r"svc-mcp-codex-[A-Za-z0-9._@-]{1,128}",
str(metadata["oauthUsername"]),
)
else f"svc-mcp-codex-{project_slug}-{client_id.hex[:8]}"
),
"tokenEndpoint": (
f"{urlparse(str(oidc.issuer)).scheme}://"
f"{urlparse(str(oidc.issuer)).netloc}/application/o/token/"
),
"oauthClientId": oidc.client_id,
"steps": [
"Provision the named Authentik service account and app password.",
"Activate this pending Hub client with the exact Authentik user UUID.",
"Install the generated Codex stdio profile using a user-only credential file.",
"Run MCP discovery, one read-only invocation and one cross-project denial.",
],
},
"checks": {
"externalSubject": external_subject,
"externalScope": {
"state": "unavailable",
"detail": (
"Granted OAuth scopes are managed by the identity "
"provider and are not introspected by the Hub."
),
},
"hubBinding": binding_check,
"catalog": catalog_check,
"connector": connector_check,
"mcpProof": {
"state": "unavailable",
"detail": (
"The Hub cannot observe local MCP client discovery or "
"invocation; verify those from the client installation."
),
},
},
"verification": {
"checkedAt": checked_at,
"checkedByManagement": checked_by_management,
"verifiedByManagement": verified_by_management,
"hubChecksPassed": verified_by_management,
"detail": (
(
"All measurable Hub-side checks passed; external OAuth "
"scopes and local MCP proof remain unverified."
)
if verified_by_management
else (
"A management check is recorded, but Hub prerequisites "
"remain incomplete or unavailable."
)
if checked_by_management
else "No management verification has been recorded yet."
),
},
}
def verify_client_onboarding(
client_id: UUID, principal: AuthenticatedPrincipal
) -> dict[str, Any]:
projection = client_onboarding_status(client_id, include_live_readiness=True)
checked_at_datetime = datetime.now(UTC)
checked_at = checked_at_datetime.isoformat()
check_states = {key: value["state"] for key, value in projection["checks"].items()}
hub_checks_passed = (
projection["checks"]["externalSubject"]["state"] == "ready"
and projection["checks"]["hubBinding"]["state"] == "ready"
and projection["checks"]["catalog"]["state"] == "ready"
and projection["checks"]["connector"]["state"] == "healthy"
)
safe_evidence = {
"clientId": str(client_id),
"checkedAt": checked_at,
"checkedByManagement": True,
"hubChecksPassed": hub_checks_passed,
"checkStates": check_states,
}
evidence_hash = hashlib.sha256(
json.dumps(safe_evidence, sort_keys=True, separators=(",", ":")).encode("utf-8")
).hexdigest()
with sessions() as session, session.begin():
row = session.scalar(
select(ClientRow)
.join(WorkspaceRow, WorkspaceRow.id == ClientRow.workspace_id)
.where(ClientRow.id == client_id, WorkspaceRow.slug == "itworx")
)
if row is None:
raise KeyError(client_id)
metadata = dict(row.record_metadata or {})
if not _is_onboarding_metadata(metadata):
raise KeyError(client_id)
project_row = session.scalar(
select(ProjectRow).where(
ProjectRow.workspace_id == row.workspace_id,
ProjectRow.slug == str(metadata.get("project", "")),
)
)
environment_row = (
session.scalar(
select(EnvironmentRow).where(
EnvironmentRow.workspace_id == row.workspace_id,
EnvironmentRow.project_id == project_row.id,
EnvironmentRow.name == str(metadata.get("environment", "")),
)
)
if project_row is not None
else None
)
if project_row is None or environment_row is None:
raise KeyError(client_id)
metadata["onboardingVerification"] = {
"checkedAt": checked_at,
"checkedByManagement": True,
"verifiedByManagement": hub_checks_passed,
"hubChecksPassed": hub_checks_passed,
"checkStates": check_states,
}
row.record_metadata = metadata
session.add(
AuditEventRow(
id=uuid4(),
occurred_at=checked_at_datetime,
trace_id=str(uuid4()),
workspace_id=row.workspace_id,
project_id=project_row.id,
environment_id=environment_row.id,
event_type="client.onboarding.verify",
principal_issuer=principal.issuer[:512],
principal_subject=principal.subject[:256],
client_id=row.id,
tool_name="client.onboarding.verify",
policy_decision="recorded",
matched_rule_ids=["client.verify"],
policy_trace=[
{"check": key, "state": state} for key, state in check_states.items()
],
arguments_hash=evidence_hash,
sanitized_arguments={
"checkedByManagement": True,
"hubChecksPassed": hub_checks_passed,
"checkStates": check_states,
},
duration_ms=0,
outcome="success",
error_code=None,
retention_until=checked_at_datetime + timedelta(days=30),
)
)
projection["verification"] = {
"checkedAt": checked_at,
"checkedByManagement": True,
"verifiedByManagement": hub_checks_passed,
"hubChecksPassed": hub_checks_passed,
"detail": (
"All measurable Hub-side checks passed; external OAuth "
"scopes and local MCP proof remain unverified."
)
if hub_checks_passed
else (
"A management check is recorded, but Hub prerequisites "
"remain incomplete or unavailable."
),
}
return projection
app.include_router(
build_client_router(
clients, # type: ignore[arg-type]
authenticate=authenticate,
authorize=authorize_action,
onboarding_status=client_onboarding_status,
verify_onboarding=verify_client_onboarding,
transition_client=clients.transition,
prefix="/client-management",
token_endpoint=(
f"{urlparse(str(oidc.issuer)).scheme}://"
f"{urlparse(str(oidc.issuer)).netloc}/application/o/token/"
),
oauth_client_id=oidc.client_id,
),
prefix="/api",
)
@app.middleware("http")
async def response_security(request: Request, call_next: Any) -> Any:
if request.url.path.startswith("/api/") and request.method not in {
"GET",
"HEAD",
"OPTIONS",
}:
try:
bff.verify_csrf(
request.cookies.get(oidc.cookie_name), request.headers.get("X-CSRF-Token")
)
except AuthError as exc:
return JSONResponse(
status_code=status.HTTP_403_FORBIDDEN,
content={"detail": {"code": exc.code, "message": exc.public_message}},
)
response = await call_next(request)
response.headers["Cache-Control"] = "no-store"
response.headers["X-Content-Type-Options"] = "nosniff"
response.headers["Referrer-Policy"] = "no-referrer"
response.headers["X-Frame-Options"] = "DENY"
return response
@app.get("/api/dashboard")
def dashboard(_: Any = require_dashboard_read) -> dict[str, Any]:
gateway = "unavailable"
gateway_detail = "ContextForge unavailable"
try:
health = contextforge.health()
gateway = "healthy" if health.get("status") else "degraded"
gateway_detail = "ContextForge health verified"
except Exception:
gateway = "unavailable"
projects = []
for document in _documents(catalog_root, "projects"):
metadata = document.get("metadata", {})
spec = document.get("spec", {})
registration_health, registration_detail = _catalog_registration_health(document)
for environment in spec.get("environments") or {}:
projects.append(
{
"slug": metadata.get("slug", "unknown"),
"name": metadata.get("name", "Unknown"),
"environment": environment,
"health": registration_health,
"detail": registration_detail,
}
)
connectors = []
for item in _documents(catalog_root, "connectors"):
registration_state, _registration_detail = _catalog_registration_health(item)
slug = str(item.get("metadata", {}).get("slug", "unknown"))[:128]
connectors.append(
{
"id": slug,
"name": item.get("metadata", {}).get("name", "Unknown"),
"project": "registered",
"state": "unknown",
"detail": (
"Catalog registration is known; live connector "
"readiness has not been measured on the dashboard."
),
"registrationState": "registered"
if registration_state == "healthy"
else "invalid",
"readiness": {
"state": "unknown",
"detail": "Open connector detail for an allowlisted readiness observation.",
"observedAt": None,
"source": "not-probed",
},
}
)
with sessions() as session:
denial_count = session.scalar(
select(func.count(AuditEventRow.id)).where(
AuditEventRow.outcome.in_(("denied", "rate_limited"))
)
)
drift: dict[str, Any] = {
"count": 0,
"projects": [],
"known": False,
"detail": "Catalog drift could not be measured because the gateway is unavailable.",
}
try:
drift = measure_connector_drift(contextforge)
except Exception:
# A dashboard read must stay available when ContextForge is down;
# report that the result is unmeasured rather than implying zero drift.
drift["detail"] = (
"Catalog drift could not be measured because the gateway is unavailable."
)
return {
"generatedAt": datetime.now(UTC).isoformat(),
"staleAfterSeconds": 60,
"health": {
"hub": "healthy",
"gateway": gateway,
"detail": gateway_detail,
},
"projects": projects,
"connectors": connectors,
"denials": {"count": int(denial_count or 0), "window": "retained audit window"},
"drift": drift,
"activeClients": len([item for item in clients.list() if item.status == "enabled"]),
"backup": read_backup_status(backup_status_path),
"version": {
"application": os.environ.get("HUB_RELEASE_VERSION", "1.0.0-rc"),
"schema": os.environ.get("HUB_SCHEMA_VERSION", "0004"),
},
}
@app.get("/api/projects/{slug}")
def project_detail(slug: str, _: Any = require_resource_read) -> dict[str, Any]:
"""Real, catalog-derived project detail -- this is what the project
detail page's tabs actually render, replacing the old hardcoded
"prod" / "3 servers" placeholders that showed for every project slug
regardless of what was actually registered.
"""
project_doc = next(
(
doc
for doc in _documents(catalog_root, "projects")
if str(doc.get("metadata", {}).get("slug", "")) == slug
),
None,
)
if project_doc is None:
raise HTTPException(status_code=404, detail="project is not registered")
spec = cast(dict[str, Any], project_doc.get("spec", {}))
metadata = cast(dict[str, Any], project_doc.get("metadata", {}))
connector_type_by_slug = {
str(doc.get("metadata", {}).get("slug", "")): str(
doc.get("spec", {}).get("connectorType", "")
)
for doc in _documents(catalog_root, "connectors")
}
environments = cast(dict[str, Any], spec.get("environments") or {})
connector_bindings = [
{
"ref": str(binding.get("ref", ""))[:128],
"connectorType": connector_type_by_slug.get(str(binding.get("ref", ""))),
"environments": [
str(value)[:32]
for value in binding.get("environments", [])[:50]
if isinstance(value, str)
]
if isinstance(binding.get("environments"), list)
else [],
"verification": binding.get("verification")
if binding.get("verification")
in {"verified", "external-input-required", "unverified"}
else "unverified",
}
for binding in spec.get("connectorBindings", [])
if isinstance(binding, dict)
]
project_clients = [item for item in clients.list() if item.project == slug]
virtual_servers = [
server for server in spec.get("virtualServers", []) if isinstance(server, dict)
]
runtime_servers: dict[str, VirtualServerRow] = {}
with sessions() as session:
persisted_servers = session.execute(
select(VirtualServerRow, EnvironmentRow.name)
.join(ProjectRow, ProjectRow.id == VirtualServerRow.project_id)
.join(WorkspaceRow, WorkspaceRow.id == ProjectRow.workspace_id)
.join(EnvironmentRow, EnvironmentRow.id == VirtualServerRow.environment_id)
.where(
WorkspaceRow.slug == "itworx",
ProjectRow.slug == slug,
EnvironmentRow.project_id == ProjectRow.id,
)
).all()
runtime_servers = {row.slug: row for row, _environment in persisted_servers}
verification_values = {"verified", "external-input-required", "unverified"}
mcp_servers = []
for server in virtual_servers:
server_slug = str(server.get("slug", ""))[:128]
environment_name = str(server.get("environment", ""))[:32]
runtime = runtime_servers.get(server_slug)
revisions = server.get("protocolRevisions", [])
mcp_servers.append(
{
"slug": server_slug,
"environment": environment_name,
"endpointAlias": (
str(server.get("endpointAlias"))[:256]
if str(server.get("endpointAlias", "")).startswith("/mcp/")
else f"/mcp/{slug}/{environment_name}"
),
"protocolRevisions": [
str(value)[:32]
for value in revisions[:10]
if isinstance(value, str) and re.fullmatch(r"\d{4}-\d{2}-\d{2}", value)
]
if isinstance(revisions, list)
else [],
"defaultDecision": str(server.get("defaultDecision", "deny"))
if str(server.get("defaultDecision", "deny"))
in {"deny", "allow", "allow_if_conditions", "deny_and_alert"}
else "deny",
"runtimeState": "disabled"
if runtime is not None and not runtime.enabled
else "unknown",
"enabled": bool(runtime.enabled) if runtime else None,
"stateSource": "persisted" if runtime else "catalog-only",
"stateDetail": (
"Persisted server configuration only; no timestamped route-health "
"observation is available. Readiness remains unknown."
if runtime
else "No persisted Hub runtime state exists for this catalog server."
),
}
)
documentation = cast(dict[str, Any], spec.get("documentation") or {})
knowledge_sources: list[dict[str, Any]] = []
roots = documentation.get("roots", [])
if isinstance(roots, list):
for index, root in enumerate(roots[:100]):
if not isinstance(root, dict):
continue
uri = str(root.get("uri", ""))[:256]
parsed_uri = urlparse(uri)
if (
parsed_uri.scheme not in {"discovery", "itworx-docs"}
or not parsed_uri.netloc
or parsed_uri.username is not None
or parsed_uri.password is not None
or parsed_uri.query
or parsed_uri.fragment
):
continue
includes = root.get("include", [])
safe_includes = (
[
str(value)[:128]
for value in includes[:50]
if isinstance(value, str)
and len(value) <= 128
and all(char.isalnum() or char in "_./*?-{}:" for char in value)
]
if isinstance(includes, list)
else []
)
verification = str(root.get("verification", "unverified"))
if verification not in verification_values:
verification = "unverified"
raw_max_bytes = documentation.get("maxDocumentBytes")
max_document_bytes = (
min(1048576, max(0, int(raw_max_bytes)))
if isinstance(raw_max_bytes, int) and not isinstance(raw_max_bytes, bool)
else None
)
knowledge_sources.append(
{
"id": f"{slug}-source-{index + 1}",
"uri": uri,
"include": safe_includes,
"verification": verification,
"maxDocumentBytes": max_document_bytes,
"indexesSecrets": documentation.get("indexSecrets") is True,
"accessBoundary": (
"Registration metadata only; document content remains "
"behind the scoped Knowledge connector and project policy."
),
}
)
return {
"slug": slug,
"name": str(metadata.get("name", slug)),
"description": str(spec.get("description", "")),
"environments": [
{"name": name, "classification": cast(dict[str, Any], env).get("classification")}
for name, env in environments.items()
],
"capabilityPacks": list(spec.get("capabilityPacks", [])),
"connectorBindings": connector_bindings,
"mcpServers": mcp_servers,
"knowledgeSources": knowledge_sources,
"virtualServerCount": len(virtual_servers),
"clientCount": len(project_clients),
"activeClientCount": len(
[item for item in project_clients if item.status == "enabled"]
),
}
@app.get("/api/connectors/{slug}")
def connector_detail(
slug: str,
project: str | None = Query(default=None, max_length=63),
environment: str | None = Query(default=None, max_length=32),
_: Any = require_resource_read,
) -> dict[str, Any]:
"""Expose a secret-free connector workspace from registered metadata and
safe health sources.
"""
if (project is None) != (environment is None):
raise HTTPException(
status_code=422, detail="project and environment must be supplied together"
)
documents = _documents(catalog_root, "connectors")
document = next(
(item for item in documents if str(item.get("metadata", {}).get("slug", "")) == slug),
None,
)
if document is None:
raise HTTPException(status_code=404, detail="connector is not registered")
metadata = cast(dict[str, Any], document.get("metadata", {}))
spec = cast(dict[str, Any], document.get("spec", {}))
connector_type = str(spec.get("connectorType", ""))[:64]
if not connector_type:
raise HTTPException(status_code=404, detail="connector is not registered")
verification_values = {"verified", "external-input-required", "unverified"}
bindings: list[dict[str, str]] = []
scoped_binding_found = False
project_documents = _documents(catalog_root, "projects")
for project_document in project_documents:
project_spec = cast(dict[str, Any], project_document.get("spec", {}))
project_slug = str(project_document.get("metadata", {}).get("slug", ""))
environments = project_spec.get("environments") or {}
if not isinstance(environments, dict):
continue
for binding in project_spec.get("connectorBindings", []):
if not isinstance(binding, dict) or binding.get("ref") != slug:
continue
declared_environments = binding.get("environments", [])
if not isinstance(declared_environments, list):
continue
verification = str(binding.get("verification", "unverified"))
if verification not in verification_values:
verification = "unverified"
for bound_environment in declared_environments[:50]:
environment_name = str(bound_environment)
if environment_name not in environments:
continue
if project is not None and (
project_slug != project or environment_name != environment
):
continue
binding_row = {
"project": project_slug[:63],
"environment": environment_name[:32],
"verification": verification,
}
if len(bindings) < 100:
bindings.append(binding_row)
if project_slug == project and environment_name == environment:
scoped_binding_found = True
if project is not None and not scoped_binding_found:
raise HTTPException(
status_code=404, detail="connector is not bound to this project environment"
)
with sessions() as session:
statement = (
select(ConnectorRow, ProjectRow.slug, EnvironmentRow.name)
.join(ProjectRow, ProjectRow.id == ConnectorRow.project_id)
.join(WorkspaceRow, WorkspaceRow.id == ConnectorRow.workspace_id)
.outerjoin(EnvironmentRow, EnvironmentRow.id == ConnectorRow.environment_id)
.where(WorkspaceRow.slug == "itworx", ConnectorRow.type == connector_type)
)
if project is not None:
statement = statement.where(ProjectRow.slug == project)
statement = statement.where(
(EnvironmentRow.name == environment)
| (ConnectorRow.environment_scope == "__project__")
)
persisted = session.execute(statement).all()
measured = [row for row in persisted if row[0].last_check_at is not None]
if not measured:
readiness: dict[str, Any] = {
"reachable": None,
"status": "unknown",
"detail": (
"No live route or persisted health observation is available for this connector."
),
"observedAt": None,
"source": "unavailable",
}
else:
connector_row = max(measured, key=lambda item: item[0].last_check_at)
state_value = str(connector_row[0].health_state)
if state_value not in {
"healthy",
"degraded",
"unavailable",
"misconfigured",
"disabled",
"checking",
}:
state_value = "unknown"
observed_at = connector_row[0].last_check_at
if observed_at.tzinfo is None:
observed_at = observed_at.replace(tzinfo=UTC)
age_seconds = (datetime.now(UTC) - observed_at).total_seconds()
stale_observation = age_seconds > 60 or age_seconds < -60
readiness = {
"reachable": True
if state_value == "healthy" and not stale_observation
else False
if not stale_observation
and state_value in {"degraded", "unavailable", "misconfigured", "disabled"}
else None,
"status": "unknown" if stale_observation else state_value,
"detail": (
"Stale observation: the last persisted connector health record "
f"reported {state_value}."
if stale_observation
else f"Latest persisted connector health record reports {state_value}."
),
"observedAt": observed_at.isoformat(),
"source": "persisted",
"stale": stale_observation,
}
related_tools = []
for tool_document in _documents(catalog_root, "tools"):
tool_spec = cast(dict[str, Any], tool_document.get("spec", {}))
if str(tool_spec.get("connectorType", "")) != connector_type:
continue
tool_metadata = cast(dict[str, Any], tool_document.get("metadata", {}))
canonical_name = str(tool_spec.get("canonicalName", ""))
if not canonical_name:
continue
related_tools.append(
{
"canonicalName": canonical_name[:120],
"name": str(tool_metadata.get("name", canonical_name))[:256],
"summary": str(tool_spec.get("description", ""))[:500],
"risk": str(tool_spec.get("riskClass", "R1"))[:32],
"readOnly": tool_spec.get("readOnly") is True,
}
)
if len(related_tools) >= 100:
break
return {
"id": slug,
"name": str(metadata.get("name", slug))[:256],
"connectorType": connector_type,
"description": str(spec.get("description", ""))[:1000],
"registration": {
"state": "registered" if document.get("kind") == "ConnectorTemplate" else "invalid",
"detail": "Connector metadata is registered in the catalog."
if document.get("kind") == "ConnectorTemplate"
else "Catalog entry is present but has an unexpected kind.",
},
"readiness": readiness,
"bindings": bindings,
"relatedTools": related_tools,
}
@app.get("/api/tool-definitions/{canonical_name:path}")
def tool_definition(canonical_name: str, _: Any = require_resource_read) -> dict[str, Any]:
document = next(
(
item
for item in _documents(catalog_root, "tools")
if str(item.get("spec", {}).get("canonicalName", "")) == canonical_name
),
None,
)
if document is None:
raise HTTPException(status_code=404, detail="tool is not registered")
spec = cast(dict[str, Any], document.get("spec", {}))
return {
"canonicalName": canonical_name,
"description": str(spec.get("description", ""))[:1000],
"readOnly": spec.get("readOnly") is True,
"riskClass": str(spec.get("riskClass", "R1")),
"inputSchema": cast(dict[str, Any], spec.get("inputSchema") or {}),
"limits": cast(dict[str, Any], spec.get("limits") or {}),
}
@app.get("/api/operations/summary")
def operation_summary(_: Any = require_dashboard_read) -> dict[str, Any]:
return operations_snapshot(sessions, backup_status_path)
@app.post("/api/operations/check")
def operation_check(_: Any = require_operations_check) -> dict[str, Any]:
checks = {
slug: _direct_connector_readiness(slug)
for slug in sorted(CONNECTOR_READINESS_URL_BY_SLUG)
}
registrations = _documents(catalog_root, "connectors")
for gateway_slug, observation in checks.items():
refs = [
{
"id": str(document.get("metadata", {}).get("slug", "")),
"name": str(document.get("metadata", {}).get("name", "")),
}
for document in registrations
if CONNECTOR_GATEWAY_BY_TYPE.get(
str(document.get("spec", {}).get("connectorType", ""))
)
== gateway_slug
]
observation["connectorRefs"] = refs[:100]
observation["connectorRefsTruncated"] = len(refs) > 100
return {
"generatedAt": datetime.now(UTC).isoformat(),
"healthy": all(item["reachable"] for item in checks.values()),
"connectors": checks,
}
@app.get("/api/{kind}")
def resource_page(
kind: str,
q: str = Query(default="", max_length=200),
limit: int = Query(default=25, ge=1, le=100),
cursor: str | None = Query(default=None, max_length=128),
project: str | None = Query(default=None, max_length=63),
environment: str | None = Query(default=None, max_length=32),
principal: AuthenticatedPrincipal = authenticated,
) -> dict[str, Any]:
# A single handler serves several resource kinds with different
# sensitivity -- audit reads require auditor+, everything else here
# is viewer+ -- so the permission check happens here, keyed on
# `kind`, rather than as a single fixed per-route dependency.
authorize_action(principal, "audit.read" if kind == "audit" else "resource.read")
if environment is not None and project is None:
raise HTTPException(status_code=422, detail="environment filtering requires a project")
project_document = None
if project is not None:
project_document = next(
(
item
for item in _documents(catalog_root, "projects")
if str(item.get("metadata", {}).get("slug", "")) == project
),
None,
)
if project_document is None:
raise HTTPException(status_code=404, detail="project is not registered")
project_environments = cast(
dict[str, Any], project_document.get("spec", {}).get("environments") or {}
)
if environment is not None and environment not in project_environments:
raise HTTPException(
status_code=404, detail="environment is not registered for this project"
)
offset = decode_page_cursor(cursor) if cursor else 0
documents: list[dict[str, Any]] = []
rows: list[dict[str, Any]]
if kind == "clients":
onboarding_stages: dict[str, str] = {}
with sessions() as session:
client_rows = session.execute(
select(ClientRow, ClientBindingRow.enabled)
.join(WorkspaceRow, WorkspaceRow.id == ClientRow.workspace_id)
.outerjoin(ClientBindingRow, ClientBindingRow.client_id == ClientRow.id)
.where(WorkspaceRow.slug == "itworx")
).all()
grouped: dict[UUID, list[tuple[ClientRow, bool | None]]] = {}
for client_row, binding_enabled in client_rows:
grouped.setdefault(client_row.id, []).append((client_row, binding_enabled))
for client_id, values in grouped.items():
client_row = values[0][0]
metadata = (
client_row.record_metadata
if isinstance(client_row.record_metadata, dict)
else {}
)
if not _is_onboarding_metadata(metadata):
continue
if client_row.status == "revoked":
stage = "revoked"
elif metadata.get("identityProvisioned") is not True:
stage = "external identity pending"
elif client_row.status == "active" and any(
binding_enabled is True for _client, binding_enabled in values
):
stage = "Hub binding active; client MCP proof unavailable"
else:
stage = "Hub binding pending"
onboarding_stages[str(client_id)] = stage
rows = [
{
"id": str(item.id),
"name": item.name,
"status": (
"healthy"
if item.status == "enabled"
else "unavailable"
if item.status == "revoked"
else "degraded"
),
"summary": (
f"{item.kind.value}; last seen "
f"{item.last_seen_at.isoformat() if item.last_seen_at else 'never'}"
),
"project": item.project,
"environment": item.environment,
"risk": None,
"onboardingStage": onboarding_stages.get(str(item.id)),
}
for item in clients.list()
]
if project is not None:
rows = [item for item in rows if item["project"] == project]
if environment is not None:
rows = [item for item in rows if item["environment"] == environment]
elif kind == "audit":
# Fetched up to a fixed v1 scale bound, then paginated the same
# way as every other kind below -- bounded audit browsing, not
# unbounded history search (see _AUDIT_FETCH_CAP).
with sessions() as session:
audit_statement = (
select(AuditEventRow, ProjectRow.slug, EnvironmentRow.name)
.join(ProjectRow, ProjectRow.id == AuditEventRow.project_id)
.outerjoin(EnvironmentRow, EnvironmentRow.id == AuditEventRow.environment_id)
.order_by(AuditEventRow.occurred_at.desc(), AuditEventRow.id.desc())
)
if project is not None:
audit_statement = audit_statement.where(ProjectRow.slug == project)
if environment is not None:
audit_statement = audit_statement.where(EnvironmentRow.name == environment)
audit_rows = session.execute(audit_statement.limit(_AUDIT_FETCH_CAP)).all()
rows = [
{
"id": str(event.id),
"name": event.event_type,
"status": "healthy" if event.outcome == "success" else "degraded",
"summary": (
f"{event.tool_name or 'mcp.request'}; {event.outcome}; "
f"{event.occurred_at.isoformat()}"
),
"project": project_slug,
"environment": environment_name,
"decision": event.policy_decision,
"traceId": event.trace_id,
}
for event, project_slug, environment_name in audit_rows
]
else:
folder = RESOURCE_FOLDERS.get(kind)
if folder is None:
raise HTTPException(status_code=404, detail="resource kind is unavailable")
documents = _documents(catalog_root, folder)
if kind not in {"clients", "audit"}:
if kind == "tools" and project_document is not None:
from fnmatch import fnmatchcase
project_spec = project_document.get("spec")
if not isinstance(project_spec, dict):
rows = []
else:
capability_packs_by_slug: dict[str, dict[str, Any]] = {}
for pack_document in _documents(catalog_root, "capability-packs"):
pack_metadata = pack_document.get("metadata")
pack_slug = (
pack_metadata.get("slug") if isinstance(pack_metadata, dict) else None
)
if isinstance(pack_slug, str):
capability_packs_by_slug[pack_slug] = pack_document
connector_types_by_slug: dict[str, str] = {}
for connector_document in _documents(catalog_root, "connectors"):
connector_metadata = connector_document.get("metadata")
connector_spec = connector_document.get("spec")
connector_slug = (
connector_metadata.get("slug")
if isinstance(connector_metadata, dict)
else None
)
connector_type = (
connector_spec.get("connectorType")
if isinstance(connector_spec, dict)
else None
)
if isinstance(connector_slug, str) and isinstance(connector_type, str):
connector_types_by_slug[connector_slug] = connector_type
allowed_patterns: list[str] = []
denied_patterns: list[str] = []
allowed_risk_classes: set[str] = set()
capability_pack_slugs = project_spec.get("capabilityPacks")
if isinstance(capability_pack_slugs, list):
for pack_slug in capability_pack_slugs:
if not isinstance(pack_slug, str):
continue
pack = capability_packs_by_slug.get(pack_slug)
pack_spec = pack.get("spec") if isinstance(pack, dict) else None
if not isinstance(pack_spec, dict):
continue
pack_allowed = pack_spec.get("toolAllowPatterns")
pack_denied = pack_spec.get("toolDenyPatterns")
pack_risks = pack_spec.get("allowedRiskClasses")
if isinstance(pack_allowed, list):
allowed_patterns.extend(
value for value in pack_allowed if isinstance(value, str)
)
if isinstance(pack_denied, list):
denied_patterns.extend(
value for value in pack_denied if isinstance(value, str)
)
if isinstance(pack_risks, list):
allowed_risk_classes.update(
value for value in pack_risks if isinstance(value, str)
)
binding_types_by_environment: dict[str, set[str]] = {}
connector_bindings = project_spec.get("connectorBindings")
if isinstance(connector_bindings, list):
for binding in connector_bindings:
if not isinstance(binding, dict):
continue
connector_ref = binding.get("ref")
binding_environments = binding.get("environments")
if not isinstance(connector_ref, str):
continue
connector_type = connector_types_by_slug.get(connector_ref)
if not isinstance(connector_type, str) or not isinstance(
binding_environments, list
):
continue
for binding_environment in binding_environments:
if isinstance(binding_environment, str):
binding_types_by_environment.setdefault(
binding_environment, set()
).add(connector_type)
project_environment_map = project_spec.get("environments")
declared_environments = (
set(project_environment_map)
if isinstance(project_environment_map, dict)
else set()
)
scoped_servers: list[tuple[str, list[str]]] = []
virtual_servers = project_spec.get("virtualServers")
if isinstance(virtual_servers, list):
for server in virtual_servers:
if not isinstance(server, dict):
continue
server_environment = server.get("environment")
server_patterns = server.get("toolAllowPatterns")
if not isinstance(server_environment, str):
continue
if server_environment not in declared_environments:
continue
if environment is not None and server_environment != environment:
continue
if not isinstance(server_patterns, list):
continue
safe_patterns = [
value for value in server_patterns if isinstance(value, str)
]
if safe_patterns:
scoped_servers.append((server_environment, safe_patterns))
rows = []
if allowed_patterns and allowed_risk_classes:
for document in documents:
tool_spec = document.get("spec")
if not isinstance(tool_spec, dict):
continue
canonical_name = tool_spec.get("canonicalName")
connector_type = tool_spec.get("connectorType")
risk_class = tool_spec.get("riskClass")
if not isinstance(canonical_name, str) or not isinstance(
connector_type, str
):
continue
if (
not isinstance(risk_class, str)
or risk_class not in allowed_risk_classes
):
continue
if not any(
fnmatchcase(canonical_name, pattern) for pattern in allowed_patterns
):
continue
if any(
fnmatchcase(canonical_name, pattern) for pattern in denied_patterns
):
continue
applicable_environments = sorted(
{
server_environment
for server_environment, server_patterns in scoped_servers
if (
connector_type == "control"
or connector_type
in binding_types_by_environment.get(
server_environment, set()
)
)
and any(
fnmatchcase(canonical_name, pattern)
for pattern in server_patterns
)
}
)
if not applicable_environments:
continue
row = _row(document, kind)
row["project"] = project
row["environment"] = (
environment
if environment is not None
else applicable_environments[0]
if len(applicable_environments) == 1
else None
)
rows.append(row)
elif kind == "connectors" and project_document is not None:
project_spec = project_document.get("spec")
rows = []
if isinstance(project_spec, dict):
connector_documents_by_slug: dict[str, dict[str, Any]] = {}
for connector_document in documents:
connector_metadata = connector_document.get("metadata")
connector_slug = (
connector_metadata.get("slug")
if isinstance(connector_metadata, dict)
else None
)
if isinstance(connector_slug, str):
connector_documents_by_slug[connector_slug] = connector_document
connector_project_environments = project_spec.get("environments")
declared_environments = (
set(connector_project_environments)
if isinstance(connector_project_environments, dict)
else set()
)
connector_bindings = project_spec.get("connectorBindings")
seen_bindings: set[tuple[str, str]] = set()
if isinstance(connector_bindings, list):
for binding in connector_bindings:
if not isinstance(binding, dict):
continue
connector_ref = binding.get("ref")
binding_environments = binding.get("environments")
if not isinstance(connector_ref, str) or not isinstance(
binding_environments, list
):
continue
bound_connector_document = connector_documents_by_slug.get(
connector_ref
)
if bound_connector_document is None:
continue
for binding_environment in binding_environments:
if (
not isinstance(binding_environment, str)
or binding_environment not in declared_environments
or (
environment is not None
and binding_environment != environment
)
):
continue
binding_key = (connector_ref, binding_environment)
if binding_key in seen_bindings:
continue
seen_bindings.add(binding_key)
row = _row(bound_connector_document, kind)
row["project"] = project
row["environment"] = binding_environment
rows.append(row)
elif kind == "servers":
rows = []
for document in documents:
server_project_metadata = document.get("metadata")
server_project_spec = document.get("spec")
if not isinstance(server_project_metadata, dict) or not isinstance(
server_project_spec, dict
):
continue
project_slug = server_project_metadata.get("slug")
virtual_servers = server_project_spec.get("virtualServers")
if not isinstance(project_slug, str) or not project_slug:
continue
if not isinstance(virtual_servers, list):
continue
registration_status, _registration_detail = _catalog_registration_health(
document
)
summary = (
"Catalog registered; live runtime health has not been observed."
if registration_status == "healthy"
else (
"Catalog registration is invalid; "
"live runtime health has not been observed."
)
)
for server in virtual_servers:
if not isinstance(server, dict):
continue
server_slug = server.get("slug")
server_environment = server.get("environment")
if not isinstance(server_slug, str) or not server_slug:
continue
if not isinstance(server_environment, str) or not server_environment:
continue
rows.append(
{
"id": server_slug[:128],
"name": server_slug[:256],
"status": "unknown",
"summary": summary,
"project": project_slug[:63],
"environment": server_environment[:32],
"risk": None,
"canonicalName": None,
}
)
else:
rows = [_row(document, kind) for document in documents]
if project is not None:
rows = [item for item in rows if item.get("project") == project]
if environment is not None:
rows = [item for item in rows if item.get("environment") == environment]
if q:
needle = q.casefold()
searchable_fields = (
"id",
"name",
"summary",
"project",
"environment",
"canonicalName",
"traceId",
)
rows = [
item
for item in rows
if any(
isinstance(value, str) and needle in value.casefold()
for value in (item.get(field) for field in searchable_fields)
)
]
total = len(rows)
page_rows = rows[offset : offset + limit]
next_cursor = encode_page_cursor(offset + limit) if offset + limit < total else None
return {
"kind": kind,
"generatedAt": datetime.now(UTC).isoformat(),
"rows": page_rows,
"page": {"total": total, "nextCursor": next_cursor},
}
@app.post("/api/policies/simulate")
def policy_simulation(
payload: dict[str, Any], principal: AuthenticatedPrincipal = require_policy_simulate
) -> dict[str, Any]:
return simulate_policy(catalog_root, clients, payload, principal)
@app.post("/api/tools/test")
def tool_test(
payload: ToolReadinessRequest,
principal: AuthenticatedPrincipal = require_tool_test,
) -> dict[str, Any]:
return evaluate_tool_readiness(catalog_root, clients, contextforge, payload, principal)
@app.post("/api/tools/invoke")
def tool_invoke(
payload: ToolInvokeRequest,
principal: AuthenticatedPrincipal = require_tool_invoke,
) -> dict[str, Any]:
return invoke_read_only_tool(
catalog_root,
clients,
contextforge,
sessions,
payload,
principal,
load_connector_identity_key(),
)
@app.post("/api/catalog/preview")
def catalog_preview(
payload: dict[str, Any], __: Any = require_catalog_preview
) -> dict[str, Any]:
project = str(payload.get("project", ""))
projects = {
str(item.get("metadata", {}).get("slug", ""))
for item in _documents(catalog_root, "projects")
}
report = loader.load()
errors = [issue.message for issue in report.issues]
if project not in projects:
errors.append("requested project is not registered")
return {
"valid": report.valid and project in projects,
"changes": [f"reconcile all registered project routes, including {project}"]
if not errors
else [],
"errors": errors,
}
@app.post("/api/catalog/apply")
def catalog_apply(payload: dict[str, Any], __: Any = require_catalog_apply) -> dict[str, Any]:
project = str(payload.get("project", ""))
projects = {
str(item.get("metadata", {}).get("slug", ""))
for item in _documents(catalog_root, "projects")
}
if project not in projects:
raise HTTPException(status_code=404, detail="requested project is not registered")
reconcile_runtime_state(
contextforge,
sessions,
catalog_root,
str(oidc.issuer),
)
return {"accepted": True, "message": "Catalog reconciliation completed and verified."}
return app
def run() -> None:
run_asgi(build_app())