2565 lines
111 KiB
Python
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())
|