Initial curated ITWorx_MCP_Hub source from 368fba691ea8d23403b4d56b53b5ec670d7c5020

This commit is contained in:
Public Source Publisher committed 2026-10-01 01:36:21 +02:00
commit cb5be898ea
663 files changed
+82674

No files matched your search

+4
View File
@@ -0,0 +1,4 @@
# Packages
Reserved for shared contracts, domain, policy, connector-kit, and test-kit
packages. Dependency direction is checked by `scripts/check_boundaries.py`.
+1
View File
@@ -0,0 +1 @@
"""Shared Python packages for the ITWorx MCP Hub."""
+105
View File
@@ -0,0 +1,105 @@
"""Shared connector SDK primitives."""
from .contracts import (
ConnectorContext,
HealthSnapshot,
PageRequest,
ResourcePublication,
ResultEnvelope,
ResultMeta,
ToolPublication,
)
from .gitea import (
FileTokenProvider,
GiteaError,
GiteaRepositoryRegistry,
GiteaSettings,
RegisteredRepository,
StaticTokenProvider,
decode_file_payload,
)
from .gitea_resilient import GiteaClient
from .mobilityops import (
DEFAULT_KNOWLEDGE_LOCALE,
KNOWLEDGE_LOCALES,
MobilityOpsClient,
MobilityOpsError,
MobilityOpsSettings,
)
from .mobilityops import FileTokenProvider as MobilityOpsFileTokenProvider
from .mobilityops import StaticTokenProvider as MobilityOpsStaticTokenProvider
from .pagination import Cursor, InvalidCursor, decode_cursor, encode_cursor
from .redaction import (
is_secret_like,
normalize_text,
redact_sensitive_text,
safe_content,
safe_snippet,
)
__all__ = [
"ConnectorContext",
"SourceType",
"SourceStatus",
"SearchPage",
"SearchHit",
"RegisteredSource",
"PathEscapeError",
"ParserRegistry",
"KnowledgeRefreshJob",
"InMemoryKnowledgeIndex",
"IndexStatus",
"IndexReport",
"DocumentRecord",
"DocumentNotFound",
"DeterministicChunker",
"ChunkRecord",
"Cursor",
"HealthSnapshot",
"InvalidCursor",
"PageRequest",
"ResourcePublication",
"ResultEnvelope",
"ResultMeta",
"ToolPublication",
"decode_cursor",
"encode_cursor",
"is_secret_like",
"normalize_text",
"redact_sensitive_text",
"safe_content",
"safe_snippet",
"FileTokenProvider",
"GiteaClient",
"GiteaError",
"GiteaRepositoryRegistry",
"GiteaSettings",
"RegisteredRepository",
"StaticTokenProvider",
"decode_file_payload",
"MobilityOpsClient",
"MobilityOpsError",
"MobilityOpsFileTokenProvider",
"MobilityOpsSettings",
"MobilityOpsStaticTokenProvider",
"KNOWLEDGE_LOCALES",
"DEFAULT_KNOWLEDGE_LOCALE",
]
from .knowledge import (
ChunkRecord,
DeterministicChunker,
DocumentNotFound,
DocumentRecord,
IndexReport,
IndexStatus,
InMemoryKnowledgeIndex,
KnowledgeRefreshJob,
ParserRegistry,
PathEscapeError,
RegisteredSource,
SearchHit,
SearchPage,
SourceStatus,
SourceType,
)
+80
View File
@@ -0,0 +1,80 @@
"""Shared bounded connector contracts used by all first-party connectors."""
from __future__ import annotations
from datetime import UTC, datetime
from typing import Any
from uuid import UUID
from pydantic import BaseModel, ConfigDict, Field
class ConnectorContext(BaseModel):
"""Validated invocation scope supplied by the Hub, never by target content."""
model_config = ConfigDict(extra="forbid", frozen=True)
workspace_id: UUID
project_id: UUID
project_slug: str = Field(pattern=r"^[a-z0-9][a-z0-9-]{0,62}$")
environment: str = Field(pattern=r"^[a-z0-9][a-z0-9-]{0,31}$")
client_id: UUID | None = None
correlation_id: str | None = Field(default=None, pattern=r"^[A-Za-z0-9._:-]{1,128}$")
class PageRequest(BaseModel):
model_config = ConfigDict(extra="forbid", frozen=True)
limit: int = Field(default=20, ge=1, le=100)
cursor: str | None = Field(default=None, max_length=500)
class ResultMeta(BaseModel):
model_config = ConfigDict(extra="forbid", frozen=True)
project: str
environment: str
source: str
truncated: bool = False
next_cursor: str | None = None
generated_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
class ResultEnvelope[T](BaseModel):
model_config = ConfigDict(extra="forbid", frozen=True)
data: T
meta: ResultMeta
warnings: tuple[str, ...] = ()
class HealthSnapshot(BaseModel):
model_config = ConfigDict(extra="forbid", frozen=True)
status: str = Field(pattern=r"^(healthy|ready|degraded|failed)$")
component: str = Field(min_length=1, max_length=100)
details: dict[str, Any] = Field(default_factory=dict)
class ToolPublication(BaseModel):
model_config = ConfigDict(extra="forbid", frozen=True)
canonical_name: str = Field(pattern=r"^[a-z0-9]+(?:\.[a-z0-9-]+){2,}$")
version: str = Field(pattern=r"^\d+\.\d+\.\d+$")
connector_type: str = Field(min_length=1, max_length=64)
read_only: bool = True
risk_class: str = Field(pattern=r"^R[0-9]+$")
input_schema: dict[str, Any]
output_schema: dict[str, Any]
required_scopes: tuple[str, ...] = ()
class ResourcePublication(BaseModel):
model_config = ConfigDict(extra="forbid", frozen=True)
uri_template: str = Field(min_length=1, max_length=2048)
connector_type: str = Field(min_length=1, max_length=64)
risk_class: str = Field(pattern=r"^R[0-9]+$")
mime_types: tuple[str, ...] = ()
max_bytes: int = Field(ge=1, le=4 * 1024 * 1024)
required_scopes: tuple[str, ...] = ()
File diff suppressed because it is too large. Load diff
+114
View File
@@ -0,0 +1,114 @@
"""Compatibility hardening for Gitea validation discovery and request latency.
Some Gitea versions expose pull-request check results as commit statuses while
returning an empty branch-filtered Actions run wrapper. The base client treats
collections strictly; this subclass preserves that strictness while translating
a fully-green exact-head status set into one synthetic completed validation run.
Large repositories can require longer than the historical 15-second client
budget to commit files, apply changesets, or materialize a server-side squash
merge. The resilient client therefore uses the already allowlisted 60-second
maximum from ``GiteaSettings``. All HTTP method allowlisting, redirect handling,
response budgets, access checks and non-forcing merge behavior remain in the
base client and are not duplicated here.
"""
from __future__ import annotations
from typing import Any
import httpx
from .gitea import GiteaClient as BaseGiteaClient
from .gitea import GiteaError, GiteaSettings, RegisteredRepository, TokenProvider
GITEA_REQUEST_TIMEOUT_SECONDS = 60.0
class GiteaClient(BaseGiteaClient):
"""Gitea client with exact-head validation fallback and bounded latency."""
def __init__(
self,
settings: GiteaSettings,
token_provider: TokenProvider,
*,
transport: httpx.BaseTransport | None = None,
) -> None:
resilient_settings = settings.model_copy(
update={"timeout_seconds": GITEA_REQUEST_TIMEOUT_SECONDS}
)
super().__init__(resilient_settings, token_provider, transport=transport)
def action_runs(
self,
repository: RegisteredRepository,
*,
branch: str,
page: int,
limit: int,
) -> list[dict[str, Any]]:
payload = self._request(
self._path("repos", repository.owner, repository.name, "actions", "runs"),
{"branch": branch, "page": page, "limit": limit},
)
if isinstance(payload, dict):
if "workflow_runs" in payload:
payload = payload.get("workflow_runs")
elif "runs" in payload:
payload = payload.get("runs")
else:
payload = None
runs = self._as_list(payload)
if runs:
return runs
# Only an actually empty Actions result can fall back. Malformed
# collections still fail above, preserving strict response validation.
try:
branch_payload = self.branch(repository, branch)
except GiteaError:
return []
commit = branch_payload.get("commit")
if not isinstance(commit, dict):
return []
head_sha = str(commit.get("id") or commit.get("sha") or "")
if len(head_sha) != 40 or any(
character not in "0123456789abcdefABCDEF" for character in head_sha
):
return []
statuses = self.commit_statuses(repository, head_sha, 1, 100)
if not statuses:
return []
latest_by_context: dict[str, dict[str, Any]] = {}
for item in statuses:
status_context = str(item.get("context") or "__default__")
previous = latest_by_context.get(status_context)
if previous is None or str(item.get("created_at") or "") > str(
previous.get("created_at") or ""
):
latest_by_context[status_context] = item
latest_states = {
str(item.get("status") or "").casefold() for item in latest_by_context.values()
}
# Gitea can omit ``head_branch`` for pull_request runs, causing the
# branch-filtered run list above to be empty even though the exact-head
# commit statuses are available. Skipped jobs are neutral evidence;
# require at least one success and reject every non-terminal state.
if "success" not in latest_states or not latest_states <= {"success", "skipped"}:
return []
return [
{
"id": 0,
"name": "Exact-head commit status validation",
"status": "completed",
"conclusion": "success",
"event": "commit_status_fallback",
"head_branch": branch,
"head_sha": head_sha,
"html_url": "",
"validation_evidence": "commit-statuses",
}
]
+580
View File
@@ -0,0 +1,580 @@
"""Registered-root project knowledge index with deterministic lexical search."""
from __future__ import annotations
import hashlib
import mimetypes
import re
from collections.abc import Iterable
from datetime import UTC, datetime
from enum import StrEnum
from pathlib import Path
from typing import Literal
from urllib.parse import unquote, urlsplit
from uuid import UUID
from pydantic import BaseModel, ConfigDict, Field, model_validator
from .pagination import decode_cursor, encode_cursor
from .redaction import safe_content, safe_snippet
class SourceType(StrEnum):
DOCUMENTATION = "documentation"
ADR = "adr"
RUNBOOK = "runbook"
README = "readme"
CURRENT_STATE = "current-state"
RELEASE_NOTE = "release-note"
class PathEscapeError(ValueError):
"""A registered root or symlink would escape its configured containment."""
class DocumentNotFound(LookupError):
"""The requested source-backed document is not indexed for this project."""
class RegisteredSource(BaseModel):
"""A control-plane registered root; MCP callers never provide this path."""
model_config = ConfigDict(extra="forbid", frozen=True, arbitrary_types_allowed=True)
source_id: UUID
project_id: UUID
project_slug: str = Field(pattern=r"^[a-z0-9][a-z0-9-]{0,62}$")
source_ref: str = Field(pattern=r"^[a-z0-9][a-z0-9-]{0,127}$")
source_type: SourceType
root: Path
repository_ref: str | None = Field(default=None, max_length=200)
repository_commit: str | None = Field(default=None, max_length=200)
allowed_extensions: frozenset[str] = frozenset(
{".md", ".markdown", ".txt", ".rst", ".adoc", ".json", ".yaml", ".yml"}
)
excluded_dirs: frozenset[str] = frozenset(
{
".git",
".hg",
".svn",
"node_modules",
"dist",
"build",
"coverage",
"__pycache__",
".venv",
"venv",
"backups",
"secrets",
}
)
max_file_bytes: int = Field(default=1_048_576, ge=1024, le=4 * 1024 * 1024)
enabled: bool = True
revision: str = Field(default="registered", max_length=200)
@model_validator(mode="after")
def validate_root(self) -> RegisteredSource:
if not self.root.is_absolute():
raise ValueError("registered source roots must be absolute")
if any(not extension.startswith(".") for extension in self.allowed_extensions):
raise ValueError("allowed extensions must start with a dot")
return self
@property
def uri_prefix(self) -> str:
return f"itworx-docs://{self.project_slug}/{self.source_ref}"
def canonical_path(self, relative_path: str | Path) -> Path:
relative = Path(relative_path)
if relative.is_absolute() or ".." in relative.parts:
raise PathEscapeError("source path must remain relative to the registered root")
root = self.root.resolve(strict=True)
candidate = (root / relative).resolve(strict=False)
if candidate != root and root not in candidate.parents:
raise PathEscapeError("source path escapes the registered root")
return candidate
def allows(self, relative_path: Path) -> bool:
if not relative_path.parts or any(
part in self.excluded_dirs for part in relative_path.parts
):
return False
lowered = relative_path.name.lower()
if lowered in {
".env",
".env.local",
".env.production",
"id_rsa",
"id_ed25519",
"credentials.json",
"secrets.yaml",
}:
return False
if any(token in lowered for token in ("secret", "password", "token", "private-key")):
return False
return relative_path.suffix.lower() in self.allowed_extensions
class ChunkRecord(BaseModel):
model_config = ConfigDict(extra="forbid", frozen=True)
index: int = Field(ge=0)
text: str = Field(min_length=1, max_length=200_000)
content_hash: str = Field(pattern=r"^[0-9a-f]{64}$")
class DocumentRecord(BaseModel):
model_config = ConfigDict(extra="forbid", frozen=True)
document_id: str = Field(pattern=r"^[0-9a-f]{64}$")
source_id: UUID
project_id: UUID
project_slug: str
source_ref: str
source_type: SourceType
external_ref: str = Field(min_length=1, max_length=1024)
title: str = Field(min_length=1, max_length=500)
uri: str = Field(min_length=1, max_length=2048)
mime_type: str = Field(min_length=1, max_length=100)
content_hash: str = Field(pattern=r"^[0-9a-f]{64}$")
repository_ref: str | None = None
repository_commit: str | None = None
source_revision: str = Field(max_length=200)
modified_at: datetime
content_bytes: int = Field(ge=0, le=4 * 1024 * 1024)
version: int = Field(ge=1)
state: Literal["registered", "deleted"] = "registered"
content: str = ""
chunks: tuple[ChunkRecord, ...] = ()
class SearchHit(BaseModel):
model_config = ConfigDict(extra="forbid", frozen=True)
uri: str
title: str
path: str
source_type: SourceType
source_revision: str
repository_ref: str | None = None
repository_commit: str | None = None
content_hash: str
modified_at: datetime
snippet: str
score: float = Field(ge=0)
truncated: bool = False
class SearchPage(BaseModel):
model_config = ConfigDict(extra="forbid", frozen=True)
items: tuple[SearchHit, ...]
next_cursor: str | None
truncated: bool
class SourceStatus(BaseModel):
model_config = ConfigDict(extra="forbid", frozen=True)
source_ref: str
source_type: SourceType
uri: str
status: Literal["ready", "indexing", "stale", "failed", "empty", "disabled"]
source_revision: str | None
last_indexed_at: datetime | None
document_count: int = Field(ge=0)
class IndexStatus(BaseModel):
model_config = ConfigDict(extra="forbid", frozen=True)
status: Literal["ready", "indexing", "stale", "failed", "empty"]
source_count: int = Field(ge=0)
document_count: int = Field(ge=0)
last_completed_at: datetime | None
last_error: str | None = Field(default=None, max_length=1000)
class IndexReport(BaseModel):
model_config = ConfigDict(extra="forbid", frozen=True)
source_ref: str
indexed: int = Field(ge=0)
skipped: int = Field(ge=0)
tombstoned: int = Field(ge=0)
completed_at: datetime
class ParserRegistry:
def __init__(self) -> None:
self._extensions: dict[str, str] = {}
def register(self, extension: str, parser_name: str) -> None:
normalized = extension.lower()
if not normalized.startswith(".") or not parser_name:
raise ValueError("parser registration is invalid")
self._extensions[normalized] = parser_name
def supports(self, path: Path) -> bool:
return path.suffix.lower() in self._extensions
def title(self, path: Path, content: str) -> str:
for line in content.splitlines()[:20]:
match = re.match(r"^#{1,6}\s+(.+?)\s*#*$", line.strip())
if match:
return match.group(1)[:500]
return path.stem.replace("_", " ").replace("-", " ").strip()[:500] or path.name
@classmethod
def default(cls) -> ParserRegistry:
registry = cls()
for extension in (".md", ".markdown", ".txt", ".rst", ".adoc", ".json", ".yaml", ".yml"):
registry.register(extension, "plain-text")
return registry
class DeterministicChunker:
def __init__(self, *, max_chars: int = 4000, overlap_chars: int = 200) -> None:
if not 256 <= max_chars <= 20_000 or not 0 <= overlap_chars < max_chars // 2:
raise ValueError("chunk bounds are invalid")
self.max_chars = max_chars
self.overlap_chars = overlap_chars
def chunk(self, content: str) -> tuple[ChunkRecord, ...]:
if not content:
return ()
chunks: list[ChunkRecord] = []
start = 0
index = 0
while start < len(content):
end = min(len(content), start + self.max_chars)
if end < len(content):
boundary = content.rfind("\n", start + self.max_chars // 2, end)
if boundary > start:
end = boundary
text = content[start:end].strip()
if text:
chunks.append(
ChunkRecord(
index=index,
text=text,
content_hash=hashlib.sha256(text.encode("utf-8")).hexdigest(),
)
)
index += 1
if end >= len(content):
break
start = max(end - self.overlap_chars, start + 1)
return tuple(chunks)
def _document_key(source: RegisteredSource, relative: str) -> str:
return hashlib.sha256(f"{source.source_id}:{relative}".encode()).hexdigest()
def _content_hash(content: str) -> str:
return hashlib.sha256(content.encode("utf-8")).hexdigest()
def _score_text(query: str, title: str, content: str) -> float:
terms = [term for term in re.findall(r"[\w-]{2,}", query.casefold())]
if not terms:
return 0.0
folded_title = title.casefold()
folded_content = content.casefold()
score = sum(folded_title.count(term) * 4 for term in terms)
score += sum(folded_content.count(term) for term in terms)
if query.casefold() in folded_content:
score += 2
return float(score)
class InMemoryKnowledgeIndex:
"""Deterministic index used by the connector and as a fake target for tests."""
def __init__(
self, *, parser: ParserRegistry | None = None, chunker: DeterministicChunker | None = None
) -> None:
self.parser = parser or ParserRegistry.default()
self.chunker = chunker or DeterministicChunker()
self.sources: dict[UUID, RegisteredSource] = {}
self.documents: dict[str, DocumentRecord] = {}
self._last_completed_at: datetime | None = None
self._last_error: str | None = None
self._indexing = False
def register_source(self, source: RegisteredSource) -> None:
if not source.enabled:
self.sources[source.source_id] = source
return
canonical_root = source.root.resolve(strict=True)
if not canonical_root.is_dir():
raise ValueError("registered source root must be a directory")
self.sources[source.source_id] = source.model_copy(update={"root": canonical_root})
def index_source(self, source_id: UUID) -> IndexReport:
source = self.sources.get(source_id)
if source is None:
raise KeyError("source is not registered")
if not source.enabled:
return IndexReport(
source_ref=source.source_ref,
indexed=0,
skipped=0,
tombstoned=0,
completed_at=datetime.now(UTC),
)
self._indexing = True
self._last_error = None
previous = {
doc.external_ref: doc
for doc in self.documents.values()
if doc.source_id == source.source_id and doc.state == "registered"
}
seen: set[str] = set()
indexed = 0
skipped = 0
try:
for path in self._iter_files(source):
relative = path.relative_to(source.root.resolve(strict=True)).as_posix()
seen.add(relative)
try:
text = path.read_text(encoding="utf-8")
if len(text.encode("utf-8")) > source.max_file_bytes:
skipped += 1
continue
parsed = safe_content(text)
if parsed is None or not self.parser.supports(path):
skipped += 1
continue
key = _document_key(source, relative)
old = previous.get(relative)
content_hash = _content_hash(parsed)
if old is not None and old.content_hash == content_hash:
continue
modified = datetime.fromtimestamp(path.stat().st_mtime, tz=UTC)
document = DocumentRecord(
document_id=key,
source_id=source.source_id,
project_id=source.project_id,
project_slug=source.project_slug,
source_ref=source.source_ref,
source_type=source.source_type,
external_ref=relative,
title=self.parser.title(path, parsed),
uri=f"{source.uri_prefix}/{key}",
mime_type=mimetypes.guess_type(path.name)[0] or "text/plain",
content_hash=content_hash,
repository_ref=source.repository_ref,
repository_commit=source.repository_commit,
source_revision=source.revision,
modified_at=modified,
content_bytes=len(parsed.encode("utf-8")),
version=(old.version + 1 if old is not None else 1),
content=parsed,
chunks=self.chunker.chunk(parsed),
)
self.documents[key] = document
indexed += 1
except (OSError, UnicodeDecodeError):
skipped += 1
tombstoned = 0
for relative, old in previous.items():
if relative not in seen:
self.documents[old.document_id] = old.model_copy(
update={
"state": "deleted",
"content": "",
"chunks": (),
"content_bytes": 0,
"version": old.version + 1,
}
)
tombstoned += 1
self._last_completed_at = datetime.now(UTC)
return IndexReport(
source_ref=source.source_ref,
indexed=indexed,
skipped=skipped,
tombstoned=tombstoned,
completed_at=self._last_completed_at,
)
except Exception as exc:
self._last_error = type(exc).__name__
raise
finally:
self._indexing = False
def _iter_files(self, source: RegisteredSource) -> Iterable[Path]:
root = source.root.resolve(strict=True)
for path in sorted(root.rglob("*"), key=lambda item: item.as_posix().casefold()):
if not path.is_file():
continue
relative = path.relative_to(root)
if not source.allows(relative):
continue
canonical = source.canonical_path(relative)
if canonical != path.resolve(strict=False) and not path.is_symlink():
continue
if not canonical.is_file():
continue
yield canonical
def get_document(self, project_id: UUID, uri: str) -> DocumentRecord:
parts = urlsplit(uri)
if parts.scheme != "itworx-docs" or parts.query or parts.fragment:
raise DocumentNotFound("document URI is invalid")
project = unquote(parts.netloc)
path_parts = [unquote(part) for part in parts.path.split("/") if part]
if len(path_parts) != 2 or any(
part in {".", ".."} or "/" in part or "\\" in part for part in path_parts
):
raise DocumentNotFound("document URI is invalid")
if project != next(
(
source.project_slug
for source in self.sources.values()
if source.project_id == project_id
),
"",
):
raise DocumentNotFound("document is outside the caller project")
document = self.documents.get(path_parts[1])
if (
document is None
or document.project_id != project_id
or document.state != "registered"
or document.source_ref != path_parts[0]
):
raise DocumentNotFound("document is not indexed")
return document
def search(
self,
project_id: UUID,
query: str,
*,
source_types: Iterable[SourceType] = (),
limit: int = 20,
cursor: str | None = None,
) -> SearchPage:
if not query.strip() or len(query) > 1000:
raise ValueError("query must be between 1 and 1000 characters")
if not 1 <= limit <= 20:
raise ValueError("limit must be between 1 and 20")
selected = tuple(sorted(str(value) for value in source_types))
page_cursor = decode_cursor(cursor, str(project_id), query, *selected)
hits: list[SearchHit] = []
for document in self.documents.values():
if (
document.project_id != project_id
or document.state != "registered"
or (selected and str(document.source_type) not in selected)
):
continue
for chunk in document.chunks:
score = _score_text(query, document.title, chunk.text)
if score <= 0:
continue
hits.append(
SearchHit(
uri=document.uri,
title=document.title,
path=document.external_ref,
source_type=document.source_type,
source_revision=document.source_revision,
repository_ref=document.repository_ref,
repository_commit=document.repository_commit,
content_hash=document.content_hash,
modified_at=document.modified_at,
snippet=safe_snippet(chunk.text),
score=score,
truncated=len(chunk.text.encode("utf-8")) > 2000,
)
)
hits.sort(key=lambda hit: (-hit.score, hit.uri, hit.path))
page = hits[page_cursor.offset : page_cursor.offset + limit]
next_offset = page_cursor.offset + len(page)
next_cursor = (
encode_cursor(next_offset, str(project_id), query, *selected)
if next_offset < len(hits)
else None
)
return SearchPage(
items=tuple(page), next_cursor=next_cursor, truncated=next_cursor is not None
)
def list_sources(self, project_id: UUID) -> tuple[SourceStatus, ...]:
return tuple(
SourceStatus(
source_ref=source.source_ref,
source_type=source.source_type,
uri=source.uri_prefix,
status="disabled"
if not source.enabled
else (
"ready"
if any(
doc.project_id == project_id
and doc.source_id == source.source_id
and doc.state == "registered"
for doc in self.documents.values()
)
else "empty"
),
source_revision=source.revision,
last_indexed_at=self._last_completed_at,
document_count=sum(
1
for doc in self.documents.values()
if doc.project_id == project_id
and doc.source_id == source.source_id
and doc.state == "registered"
),
)
for source in sorted(self.sources.values(), key=lambda item: item.source_ref)
if source.project_id == project_id
)
def status(self, project_id: UUID) -> IndexStatus:
sources = self.list_sources(project_id)
count = sum(source.document_count for source in sources)
status: Literal["ready", "indexing", "stale", "failed", "empty"] = (
"indexing"
if self._indexing
else ("failed" if self._last_error else ("empty" if count == 0 else "ready"))
)
return IndexStatus(
status=status,
source_count=len(sources),
document_count=count,
last_completed_at=self._last_completed_at,
last_error=self._last_error,
)
def documents_for_source(
self, project_id: UUID, source_type: SourceType | None = None
) -> tuple[DocumentRecord, ...]:
return tuple(
sorted(
(
doc
for doc in self.documents.values()
if doc.project_id == project_id
and doc.state == "registered"
and (source_type is None or doc.source_type == source_type)
),
key=lambda doc: (doc.external_ref, doc.document_id),
)
)
class KnowledgeRefreshJob:
"""Single bounded refresh operation; scheduling is owned by the control plane."""
def __init__(self, index: InMemoryKnowledgeIndex, source_ids: Iterable[UUID]) -> None:
self.index = index
self.source_ids = tuple(source_ids)
def run_once(self) -> tuple[IndexReport, ...]:
return tuple(self.index.index_source(source_id) for source_id in self.source_ids)
+356
View File
@@ -0,0 +1,356 @@
"""Typed, read-only MobilityOps (Fleet Ops) API client primitives.
MobilityOps has exactly one configured target per environment (unlike Gitea's
per-project repository registry) -- shape follows the simpler `unraid` broker
client pattern, with Gitea's stricter settings/transport validation reused
since this, like Gitea, is an external HTTPS origin authenticated by a
long-lived shared secret rather than an internally-issued short-lived token.
"""
from __future__ import annotations
import ipaddress
import json
import re
from collections.abc import Mapping
from pathlib import Path
from typing import Any, Protocol, cast
from urllib.parse import urljoin, urlparse
import httpx
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
KNOWLEDGE_LOCALES: tuple[str, ...] = ("nl-BE", "en-GB", "fr-BE")
"""Locales MobilityOps (Fleet Ops) accepts on `POST /search-knowledge`."""
DEFAULT_KNOWLEDGE_LOCALE = "en-GB"
"""Compatible default for older clients that send no locale at all."""
CORRELATION_ID = re.compile(r"^[A-Za-z0-9._:-]{1,128}$")
class MobilityOpsError(RuntimeError):
"""Stable connector error that never contains credentials or raw headers."""
def __init__(self, code: str, message: str) -> None:
self.code = code
super().__init__(message)
class MobilityOpsSettings(BaseModel):
model_config = ConfigDict(extra="forbid", frozen=True)
base_url: str = Field(min_length=1, max_length=2048)
token_reference: str = Field(min_length=1, max_length=512)
timeout_seconds: float = Field(default=15.0, gt=0, le=60)
max_response_bytes: int = Field(default=262144, ge=1024, le=4 * 1024 * 1024)
allow_insecure_http_for_tests: bool = False
@field_validator("base_url")
@classmethod
def validate_base_url(cls, value: str) -> str:
parsed = urlparse(value.strip().rstrip("/"))
if parsed.scheme not in {"https", "http"} or not parsed.hostname:
raise ValueError("MobilityOps base_url must be an absolute HTTP(S) URL")
if parsed.username or parsed.password or parsed.query or parsed.fragment:
raise ValueError("MobilityOps base_url must not contain credentials or query data")
hostname = (parsed.hostname or "").casefold()
if hostname in {"localhost", "metadata", "metadata.google.internal"}:
raise ValueError("MobilityOps base_url host is not allowed")
try:
address = ipaddress.ip_address(hostname)
except ValueError:
address = None
if address is not None and (
address.is_private
or address.is_loopback
or address.is_link_local
or address.is_reserved
or address.is_unspecified
or address.is_multicast
):
raise ValueError("MobilityOps base_url host is not allowed")
return value.strip().rstrip("/")
@model_validator(mode="after")
def require_tls(self) -> MobilityOpsSettings:
if urlparse(self.base_url).scheme != "https" and not self.allow_insecure_http_for_tests:
raise ValueError("MobilityOps base_url must use HTTPS")
if not self.token_reference.startswith(("docker-secret://", "/run/secrets/")):
raise ValueError("token_reference must name a mounted secret, not contain a token")
return self
class TokenProvider(Protocol):
def get_token(self, reference: str) -> str: ...
class FileTokenProvider:
"""Read-only provider for Docker secrets mounted beneath /run/secrets."""
def get_token(self, reference: str) -> str:
if reference.startswith("docker-secret://"):
name = reference.removeprefix("docker-secret://")
path = Path("/run/secrets") / name
else:
path = Path(reference)
root = Path("/run/secrets").resolve()
try:
resolved = path.resolve()
resolved.relative_to(root)
except (OSError, ValueError) as exc:
raise MobilityOpsError(
"MOBILITYOPS_SECRET_REFERENCE_INVALID", "secret reference is outside /run/secrets"
) from exc
try:
token = resolved.read_text(encoding="utf-8").strip()
except OSError as exc:
raise MobilityOpsError(
"MOBILITYOPS_SECRET_UNAVAILABLE", "configured MobilityOps secret is unavailable"
) from exc
if not token or len(token) > 4096 or any(ord(char) < 0x20 for char in token):
raise MobilityOpsError(
"MOBILITYOPS_SECRET_INVALID", "configured MobilityOps secret is invalid"
)
return token
class StaticTokenProvider:
"""Deterministic fake-only provider; never serializes the token."""
def __init__(self, token: str) -> None:
self._token = token
def get_token(self, reference: str) -> str:
if not reference.startswith(("docker-secret://", "/run/secrets/")):
raise MobilityOpsError(
"MOBILITYOPS_SECRET_REFERENCE_INVALID", "invalid secret reference"
)
return self._token
class MobilityOpsClient:
"""Small MobilityOps API client with same-origin redirect enforcement.
Auth is a shared-secret `X-Service-Token` header (MobilityOps's own
`require_mcp_service_token` dependency), not a per-repo bearer token --
there is exactly one configured MobilityOps target per environment.
"""
def __init__(
self,
settings: MobilityOpsSettings,
token_provider: TokenProvider,
*,
transport: httpx.BaseTransport | None = None,
) -> None:
self.settings = settings
self._token_provider = token_provider
self._client = httpx.Client(
base_url=f"{settings.base_url}/api/v1/integrations/mcp",
timeout=settings.timeout_seconds,
follow_redirects=False,
transport=transport,
)
def close(self) -> None:
self._client.close()
def _request(
self,
client_id: str,
method: str,
path: str,
*,
params: Mapping[str, str | int] | None = None,
json_body: Mapping[str, Any] | None = None,
correlation_id: str | None = None,
timeout: float | None = None,
) -> Any:
token = self._token_provider.get_token(self.settings.token_reference)
headers = {
"X-Service-Token": token,
"X-Client-Id": client_id,
"Accept": "application/json",
}
if correlation_id is not None:
# The Hub already has a trace id for this invocation; MobilityOps preserves
# whatever valid correlation id it receives, so never mint a second one here.
if not CORRELATION_ID.fullmatch(correlation_id):
raise MobilityOpsError(
"MOBILITYOPS_CORRELATION_ID_INVALID",
"inbound correlation id is not a safe header value",
)
headers["X-Correlation-Id"] = correlation_id
request_timeout = self.settings.timeout_seconds if timeout is None else timeout
try:
response = self._client.request(
method,
path,
params=params,
json=json_body,
headers=headers,
timeout=request_timeout,
)
if 300 <= response.status_code < 400:
location = response.headers.get("location")
if not location:
raise MobilityOpsError(
"MOBILITYOPS_REDIRECT_DENIED",
"MobilityOps redirect did not provide a location",
)
target = urlparse(urljoin(str(response.url), location))
base = urlparse(self.settings.base_url)
if (target.scheme, target.hostname, target.port) != (
base.scheme,
base.hostname,
base.port,
):
raise MobilityOpsError(
"MOBILITYOPS_REDIRECT_DENIED",
"MobilityOps redirect leaves the configured origin",
)
response = self._client.request(
method, target.geturl(), headers=headers, timeout=request_timeout
)
if 300 <= response.status_code < 400:
raise MobilityOpsError(
"MOBILITYOPS_REDIRECT_DENIED", "MobilityOps returned multiple redirects"
)
except MobilityOpsError:
raise
except (httpx.TimeoutException, httpx.NetworkError) as exc:
raise MobilityOpsError(
"MOBILITYOPS_UNAVAILABLE", "MobilityOps target is unavailable"
) from exc
if len(response.content) > self.settings.max_response_bytes:
raise MobilityOpsError(
"MOBILITYOPS_RESPONSE_TOO_LARGE",
"MobilityOps response exceeds the connector budget",
)
if response.status_code == 404:
raise MobilityOpsError("MOBILITYOPS_NOT_FOUND", "MobilityOps resource was not found")
if response.status_code in {401, 403}:
raise MobilityOpsError(
"MOBILITYOPS_ACCESS_DENIED", "MobilityOps denied the read request"
)
if response.status_code == 422:
raise MobilityOpsError(
"MOBILITYOPS_REQUEST_INVALID", "MobilityOps rejected the request shape"
)
if response.status_code >= 400:
raise MobilityOpsError(
"MOBILITYOPS_REQUEST_FAILED", "MobilityOps returned an unsuccessful response"
)
try:
return response.json()
except (ValueError, json.JSONDecodeError) as exc:
raise MobilityOpsError(
"MOBILITYOPS_INVALID_RESPONSE", "MobilityOps returned invalid JSON"
) from exc
def operations_summary(
self, client_id: str, *, correlation_id: str | None = None
) -> dict[str, Any]:
return cast(
dict[str, Any],
self._request(client_id, "GET", "/operations-summary", correlation_id=correlation_id),
)
READINESS_TIMEOUT_SECONDS = 5.0
def readiness_probe(self, client_id: str, *, correlation_id: str | None = None) -> None:
"""Short, bounded, authenticated read used as the connector's readiness signal.
MobilityOps exposes no authenticated readiness route on this origin -- its only
health route (`GET /health`) is unauthenticated and lives outside the
`/api/v1/integrations/mcp` prefix this client is bound to -- so the narrowest
authenticated read, `operations-summary`, is the readiness probe instead.
Raises `MobilityOpsError` with a stable code; never leaks the token or a raw
transport exception.
"""
payload = self._request(
client_id,
"GET",
"/operations-summary",
correlation_id=correlation_id,
timeout=min(self.settings.timeout_seconds, self.READINESS_TIMEOUT_SECONDS),
)
if not isinstance(payload, dict) or not isinstance(payload.get("metrics"), dict):
raise MobilityOpsError(
"MOBILITYOPS_INVALID_RESPONSE",
"MobilityOps readiness response did not carry operational metrics",
)
def attention_vehicles(
self,
client_id: str,
*,
minimum_severity: str,
date: str | None,
limit: int,
correlation_id: str | None = None,
) -> list[dict[str, Any]]:
params: dict[str, str | int] = {"minimum_severity": minimum_severity, "limit": limit}
if date:
params["date"] = date
value = self._request(
client_id,
"GET",
"/attention-vehicles",
params=params,
correlation_id=correlation_id,
)
if not isinstance(value, list) or not all(isinstance(item, dict) for item in value):
raise MobilityOpsError(
"MOBILITYOPS_INVALID_RESPONSE", "MobilityOps returned an unexpected collection"
)
return value
_VEHICLE_REF = re.compile(r"^[A-Za-z0-9][A-Za-z0-9-]{0,63}$")
def vehicle_details(
self, client_id: str, vehicle_ref: str, *, correlation_id: str | None = None
) -> dict[str, Any]:
if not self._VEHICLE_REF.fullmatch(vehicle_ref):
raise MobilityOpsError(
"MOBILITYOPS_VEHICLE_REF_INVALID", "vehicle reference is invalid"
)
return cast(
dict[str, Any],
self._request(
client_id,
"GET",
f"/vehicles/{vehicle_ref}",
correlation_id=correlation_id,
),
)
def search_knowledge(
self,
client_id: str,
question: str,
*,
max_sources: int,
locale: str = DEFAULT_KNOWLEDGE_LOCALE,
correlation_id: str | None = None,
) -> dict[str, Any]:
if locale not in KNOWLEDGE_LOCALES:
raise MobilityOpsError(
"MOBILITYOPS_LOCALE_INVALID",
"locale must be one of nl-BE, en-GB, fr-BE",
)
return cast(
dict[str, Any],
self._request(
client_id,
"POST",
"/search-knowledge",
json_body={
"question": question,
"max_sources": max_sources,
"locale": locale,
},
correlation_id=correlation_id,
),
)
+47
View File
@@ -0,0 +1,47 @@
"""Opaque, scope-bound pagination cursors."""
from __future__ import annotations
import base64
import json
from dataclasses import dataclass
from hashlib import sha256
class InvalidCursor(ValueError):
"""Raised for malformed, cross-query, or out-of-range cursors."""
@dataclass(frozen=True)
class Cursor:
offset: int
fingerprint: str
def _fingerprint(*parts: str) -> str:
return sha256("\x1f".join(parts).encode("utf-8")).hexdigest()[:32]
def encode_cursor(offset: int, *scope: str) -> str:
if offset < 0:
raise ValueError("cursor offset must be non-negative")
payload = {"o": offset, "f": _fingerprint(*scope)}
raw = json.dumps(payload, separators=(",", ":"), sort_keys=True).encode("ascii")
return base64.urlsafe_b64encode(raw).decode("ascii").rstrip("=")
def decode_cursor(value: str | None, *scope: str) -> Cursor:
if value is None:
return Cursor(0, _fingerprint(*scope))
if not 1 <= len(value) <= 500:
raise InvalidCursor("cursor length is invalid")
try:
padded = value + "=" * (-len(value) % 4)
payload = json.loads(base64.urlsafe_b64decode(padded).decode("ascii"))
offset = payload["o"]
fingerprint = payload["f"]
except (ValueError, KeyError, TypeError, json.JSONDecodeError) as exc:
raise InvalidCursor("cursor is invalid") from exc
if type(offset) is not int or offset < 0 or fingerprint != _fingerprint(*scope):
raise InvalidCursor("cursor does not match this query")
return Cursor(offset, fingerprint)
+70
View File
@@ -0,0 +1,70 @@
"""Deterministic secret exclusion and safe text normalization for untrusted content."""
from __future__ import annotations
import re
import unicodedata
_SECRET_PATTERNS = (
re.compile(r"-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----"),
re.compile(
r"(?i)\b(?:authorization|bearer|api[_-]?key|access[_-]?token|refresh[_-]?token)\s*[:=]\s*[^\s,;]{8,}"
),
re.compile(r"(?i)\b(?:password|passwd|secret)\s*[:=]\s*['\"]?[^\s'\",;]{8,}"),
re.compile(r"\bAKIA[0-9A-Z]{16}\b"),
re.compile(r"\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b"),
)
_CONTROL_RE = re.compile(r"[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]")
_INLINE_SECRET_PATTERNS = (
re.compile(
r"(?i)(\b(?:authorization|bearer|api[_-]?key|access[_-]?token|refresh[_-]?token|"
r"password|passwd|secret|credential|private[_-]?key)\s*[:=]\s*['\"]?)"
r"[^\s'\",;]{4,}"
),
re.compile(
r"(?i)(\b[A-Z][A-Z0-9_]*(?:TOKEN|SECRET|PASSWORD|PASSWD|API_KEY|CREDENTIAL)"
r"\s*=\s*['\"]?)[^\s'\",;]{4,}"
),
re.compile(
r"\b(?:gh[pousr]_[A-Za-z0-9_]{20,}|github_pat_[A-Za-z0-9_]{20,}|glpat-[A-Za-z0-9_-]{20,}|gitea_[A-Za-z0-9_-]{20,})\b"
),
re.compile(r"(?i)(https?://[^\s/:@]+:)[^\s/@]+@"),
)
def normalize_text(value: str) -> str:
"""Normalize Unicode and controls without interpreting content."""
normalized = unicodedata.normalize("NFC", value).replace("\r\n", "\n").replace("\r", "\n")
return _CONTROL_RE.sub("", normalized)
def is_secret_like(value: str) -> bool:
return any(pattern.search(value) is not None for pattern in _SECRET_PATTERNS)
def safe_content(value: str) -> str | None:
"""Return normalized content or None when the whole document must be excluded."""
normalized = normalize_text(value)
return None if is_secret_like(normalized) else normalized
def safe_snippet(value: str, *, max_bytes: int = 2000) -> str:
normalized = normalize_text(value)
encoded = normalized.encode("utf-8")
if len(encoded) <= max_bytes:
return normalized
return encoded[: max_bytes - 3].decode("utf-8", errors="ignore") + "..."
def redact_sensitive_text(value: str, *, max_bytes: int = 2000) -> str:
"""Normalize, truncate and redact credential-shaped inline values."""
result = safe_snippet(value, max_bytes=max_bytes)
for pattern in _INLINE_SECRET_PATTERNS:
result = pattern.sub(
lambda match: f"{match.group(1)}<redacted>" if match.lastindex else "<redacted>",
result,
)
return result
+25
View File
@@ -0,0 +1,25 @@
from .bridge import StdioBridge
from .matrix import FakeVirtualServer, MatrixResult, run_matrix
from .models import (
BootstrapHandle,
ClientConfig,
ClientKind,
ClientRecord,
ClientRegistry,
ClientStatus,
generate_config,
)
__all__ = [
"BootstrapHandle",
"ClientConfig",
"ClientKind",
"ClientRecord",
"ClientRegistry",
"ClientStatus",
"FakeVirtualServer",
"MatrixResult",
"StdioBridge",
"generate_config",
"run_matrix",
]
+80
View File
@@ -0,0 +1,80 @@
"""Minimal JSON-lines stdio bridge; tokens are read only for the call and never emitted."""
from __future__ import annotations
import json
import os
from collections.abc import Callable, Iterable, Iterator, Mapping
from typing import Any, Protocol
class BridgeTransport(Protocol):
def send(
self, request: Mapping[str, Any], *, token: str, endpoint: str
) -> Mapping[str, Any] | None: ...
class StdioBridge:
"""Translate bounded JSON-lines messages to remote MCP without shell or token output."""
ALLOWED_METHODS = frozenset(
{
"initialize",
"notifications/initialized",
"ping",
"tools/list",
"tools/call",
"resources/list",
"resources/templates/list",
"resources/read",
"prompts/list",
"prompts/get",
}
)
def __init__(self, *, endpoint: str, token_env: str | None = None) -> None:
if not endpoint.startswith("https://"):
raise ValueError("stdio bridge endpoints must use HTTPS")
self.endpoint = endpoint
self.token_env = token_env or "ITWORX_MCP_ACCESS_TOKEN"
def forward(
self,
lines: Iterable[str],
transport: BridgeTransport,
*,
environ: Mapping[str, str] | None = None,
token_provider: Callable[[], str] | None = None,
) -> Iterator[str]:
values = environ if environ is not None else os.environ
static_token = values.get(self.token_env)
if not static_token and token_provider is None:
yield _error("authentication_required")
return
for raw in lines:
if len(raw.encode("utf-8")) > 4 * 1024 * 1024:
yield _error("request_too_large")
continue
try:
request = json.loads(raw)
except json.JSONDecodeError:
yield _error("invalid_json")
continue
if not isinstance(request, dict) or request.get("method") not in self.ALLOWED_METHODS:
yield _error("unsupported_method")
continue
try:
token = token_provider() if token_provider is not None else static_token
if not token:
yield _error("authentication_required")
continue
result = transport.send(request, token=token, endpoint=self.endpoint)
if result is not None:
yield json.dumps(dict(result), separators=(",", ":"))
except Exception:
yield _error("upstream_unavailable")
del static_token
def _error(code: str) -> str:
return json.dumps({"error": {"code": code}}, separators=(",", ":"))
+69
View File
@@ -0,0 +1,69 @@
"""Deterministic MCP discovery/invocation matrix for supported client profiles."""
from __future__ import annotations
from dataclasses import dataclass
from typing import Any
from .models import ClientKind
@dataclass(frozen=True)
class MatrixResult:
client_kind: ClientKind
protocol_version: str
connector: str
tool: str
discovered: bool
invoked: bool
detail: str
class FakeVirtualServer:
"""Offline project virtual server used for the client matrix and negative cases."""
def __init__(self) -> None:
self.tools = {
"gitea": "gitea.repository.get",
"knowledge": "knowledge.document.search",
"unraid": "unraid.container.inspect",
}
self.revoked: set[str] = set()
def discover(self, client_key: str, protocol_version: str) -> tuple[str, ...]:
if client_key in self.revoked:
raise PermissionError("client revoked")
if protocol_version not in {"2026-07-28", "2025-11-25"}:
raise ValueError("unsupported protocol version")
return tuple(self.tools.values())
def invoke(
self, client_key: str, tool: str, arguments: dict[str, Any] | None = None
) -> dict[str, Any]:
del arguments
if client_key in self.revoked:
raise PermissionError("client revoked")
if tool not in self.tools.values():
raise LookupError("tool not found")
return {"tool": tool, "ok": True, "truncated": False}
def run_matrix(server: FakeVirtualServer) -> tuple[MatrixResult, ...]:
profiles = (
(ClientKind.NATIVE_2026, "2026-07-28"),
(ClientKind.COMPATIBILITY_2025, "2025-11-25"),
(ClientKind.STDIO_BRIDGE, "2026-07-28"),
(ClientKind.INSPECTOR, "2026-07-28"),
)
results: list[MatrixResult] = []
for kind, revision in profiles:
client_key = kind.value
tools = server.discover(client_key, revision)
for connector, tool in server.tools.items():
invoked = bool(server.invoke(client_key, tool)["ok"])
results.append(
MatrixResult(
kind, revision, connector, tool, tool in tools, invoked, "fixture success"
)
)
return tuple(results)
+235
View File
@@ -0,0 +1,235 @@
"""Typed, secret-free machine-client lifecycle primitives for MCP onboarding."""
from __future__ import annotations
from datetime import UTC, datetime, timedelta
from enum import StrEnum
from threading import RLock
from uuid import UUID, uuid4
from pydantic import BaseModel, ConfigDict, Field
class ClientKind(StrEnum):
NATIVE_2026 = "native-2026"
COMPATIBILITY_2025 = "compatibility-2025"
STDIO_BRIDGE = "stdio-bridge"
INSPECTOR = "inspector"
class ClientStatus(StrEnum):
PENDING = "pending"
ENABLED = "enabled"
DISABLED = "disabled"
REVOKED = "revoked"
class ClientRecord(BaseModel):
model_config = ConfigDict(extra="forbid", frozen=True)
id: UUID
name: str = Field(min_length=1, max_length=128)
project: str = Field(min_length=1, max_length=63, pattern=r"^[a-z0-9][a-z0-9-]*$")
environment: str = Field(min_length=1, max_length=32, pattern=r"^[a-z0-9][a-z0-9-]*$")
kind: ClientKind
status: ClientStatus
credential_reference: str = Field(min_length=1, max_length=256)
created_at: datetime
last_seen_at: datetime | None = None
disabled_at: datetime | None = None
revoked_at: datetime | None = None
class ClientConfig(BaseModel):
"""Generated client configuration. It intentionally contains no token or secret."""
model_config = ConfigDict(extra="forbid", frozen=True, populate_by_name=True)
client_id: UUID = Field(serialization_alias="clientId")
kind: ClientKind
protocol_version: str = Field(serialization_alias="protocolVersion")
endpoint: str
transport: str
scopes: tuple[str, ...]
credential_reference: str = Field(serialization_alias="credentialReference")
environment_variable: str | None = Field(
default=None, serialization_alias="environmentVariable"
)
bridge_command: str | None = Field(default=None, serialization_alias="bridgeCommand")
class BootstrapHandle(BaseModel):
"""A one-time Authentik hand-off reference, never a bearer credential."""
model_config = ConfigDict(extra="forbid", frozen=True, populate_by_name=True)
client_id: UUID
reference: str = Field(min_length=1, max_length=256)
delivery: str = "authentik-service-account"
expires_at: datetime = Field(serialization_alias="expiresAt")
consumed: bool = False
class ClientRegistry:
"""In-memory development registry; production persistence is supplied by the control plane."""
def __init__(self, *, revocation_cache_ttl_seconds: int = 60) -> None:
if not 1 <= revocation_cache_ttl_seconds <= 60:
raise ValueError("revocation cache ceiling is 60 seconds")
self.revocation_cache_ttl_seconds = revocation_cache_ttl_seconds
self._clients: dict[UUID, ClientRecord] = {}
self._bootstrap: dict[str, BootstrapHandle] = {}
self._lock = RLock()
def create(
self,
*,
name: str,
project: str,
environment: str,
kind: ClientKind,
now: datetime | None = None,
) -> ClientRecord:
reference = _aware(now)
client_id = uuid4()
record = ClientRecord(
id=client_id,
name=name,
project=project,
environment=environment,
kind=kind,
status=ClientStatus.ENABLED,
credential_reference=f"authentik://mcp-client/{client_id}",
created_at=reference,
)
with self._lock:
self._clients[client_id] = record
return record
def list(self) -> tuple[ClientRecord, ...]:
with self._lock:
return tuple(self._clients.values())
def get(self, client_id: UUID) -> ClientRecord | None:
with self._lock:
return self._clients.get(client_id)
def disable(self, client_id: UUID, *, now: datetime | None = None) -> ClientRecord:
return self._transition(client_id, ClientStatus.DISABLED, _aware(now))
def revoke(self, client_id: UUID, *, now: datetime | None = None) -> ClientRecord:
return self._transition(client_id, ClientStatus.REVOKED, _aware(now))
def _transition(self, client_id: UUID, status: ClientStatus, now: datetime) -> ClientRecord:
with self._lock:
current = self._clients.get(client_id)
if current is None:
raise KeyError(client_id)
updated = current.model_copy(
update={
"status": status,
"disabled_at": now if status is ClientStatus.DISABLED else current.disabled_at,
"revoked_at": now if status is ClientStatus.REVOKED else current.revoked_at,
}
)
self._clients[client_id] = updated
return updated
def touch(self, client_id: UUID, *, now: datetime | None = None) -> ClientRecord:
reference = _aware(now)
with self._lock:
current = self._clients.get(client_id)
if current is None:
raise KeyError(client_id)
if current.status is not ClientStatus.ENABLED:
raise PermissionError("client is not enabled")
updated = current.model_copy(update={"last_seen_at": reference})
self._clients[client_id] = updated
return updated
def authorize(self, client_id: UUID, *, project: str, environment: str) -> ClientRecord:
record = self.get(client_id)
if record is None or record.status is not ClientStatus.ENABLED:
raise PermissionError("client is disabled or revoked")
if record.project != project or record.environment != environment:
raise PermissionError("client binding does not match endpoint scope")
return record
def issue_bootstrap(
self, client_id: UUID, *, now: datetime | None = None, ttl_seconds: int = 300
) -> BootstrapHandle:
if not 1 <= ttl_seconds <= 900:
raise ValueError("bootstrap TTL must be between 1 and 900 seconds")
if self.get(client_id) is None:
raise KeyError(client_id)
reference = _aware(now)
handle = BootstrapHandle(
client_id=client_id,
reference=f"authentik-bootstrap://{uuid4()}",
expires_at=reference + timedelta(seconds=ttl_seconds),
)
with self._lock:
self._bootstrap[handle.reference] = handle
return handle
def consume_bootstrap(self, reference: str, *, now: datetime | None = None) -> BootstrapHandle:
current = _aware(now)
with self._lock:
handle = self._bootstrap.get(reference)
if handle is None or handle.consumed or handle.expires_at <= current:
raise PermissionError("bootstrap reference is invalid or expired")
consumed = handle.model_copy(update={"consumed": True})
self._bootstrap[reference] = consumed
return consumed
def _aware(value: datetime | None) -> datetime:
result = value or datetime.now(UTC)
if result.tzinfo is None or result.utcoffset() is None:
raise ValueError("timestamps must be timezone-aware")
return result.astimezone(UTC)
def generate_config(record: ClientRecord, *, public_host: str = "mcp.example.com") -> ClientConfig:
endpoint = f"https://{public_host}/mcp/{record.project}/{record.environment}"
if record.kind is ClientKind.NATIVE_2026:
return ClientConfig(
client_id=record.id,
kind=record.kind,
protocol_version="2026-07-28",
endpoint=endpoint,
transport="streamable-http",
scopes=("tools:read",),
credential_reference=record.credential_reference,
)
if record.kind is ClientKind.COMPATIBILITY_2025:
return ClientConfig(
client_id=record.id,
kind=record.kind,
protocol_version="2025-11-25",
endpoint=endpoint,
transport="streamable-http",
scopes=("tools:read",),
credential_reference=record.credential_reference,
)
if record.kind is ClientKind.STDIO_BRIDGE:
return ClientConfig(
client_id=record.id,
kind=record.kind,
protocol_version="2026-07-28",
endpoint=endpoint,
transport="stdio",
scopes=("tools:read",),
credential_reference=record.credential_reference,
environment_variable="ITWORX_MCP_ACCESS_TOKEN",
bridge_command="itworx-mcp-bridge",
)
return ClientConfig(
client_id=record.id,
kind=record.kind,
protocol_version="2026-07-28",
endpoint=f"http://127.0.0.1:18442/mcp/{record.project}/{record.environment}",
transport="streamable-http",
scopes=("tools:read",),
credential_reference=record.credential_reference,
)
+1
View File
@@ -0,0 +1 @@
"""Shared Hub contracts used across service boundaries."""
+153
View File
@@ -0,0 +1,153 @@
"""Typed, public-safe contracts for the dataplane router."""
from __future__ import annotations
import ipaddress
import re
from datetime import datetime
from typing import Literal, Protocol
from urllib.parse import urlparse
from uuid import UUID
from pydantic import AnyHttpUrl, BaseModel, ConfigDict, Field, field_validator, model_validator
EnvironmentName = Literal["dev", "test", "staging", "prod"]
SUPPORTED_PROTOCOLS = frozenset({"2026-07-28", "2025-11-25"})
class ClientBinding(BaseModel):
model_config = ConfigDict(extra="forbid", frozen=True)
client_id: UUID
issuer: str = Field(min_length=1, max_length=2048)
subject: str = Field(min_length=1, max_length=512)
enabled: bool = False
class AliasRecord(BaseModel):
model_config = ConfigDict(extra="forbid", frozen=True)
project_slug: str = Field(pattern=r"^[a-z0-9][a-z0-9-]{0,62}$")
workspace_id: UUID | None = None
project_id: UUID | None = None
environment: EnvironmentName
alias: str
upstream_url: AnyHttpUrl
enabled: bool = False
protocol_revisions: tuple[str, ...] = ("2026-07-28", "2025-11-25")
bindings: tuple[ClientBinding, ...] = ()
@field_validator("alias")
@classmethod
def validate_alias(cls, value: str) -> str:
if len(value) > 128 or not re.fullmatch(
r"/mcp/[a-z0-9][a-z0-9-]{0,62}/(?:dev|test|staging|prod)/?", value
):
raise ValueError("alias must be a stable /mcp path")
return value.rstrip("/")
@field_validator("upstream_url")
@classmethod
def reject_upstream_userinfo(cls, value: AnyHttpUrl) -> AnyHttpUrl:
if value.username is not None or value.password is not None:
raise ValueError("upstream URL userinfo is not permitted")
internal_contextforge = (
value.scheme == "http"
and value.host in {"contextforge", "contextforge-control"}
and value.port == 4444
and (
(value.path or "").rstrip("/") == "/mcp"
or bool(
re.fullmatch(
r"/servers/[0-9a-f]{32}/mcp",
(value.path or "").rstrip("/"),
)
)
)
)
if value.scheme != "https" and not internal_contextforge:
raise ValueError("dataplane upstreams must use HTTPS or the fixed internal gateway")
hostname = (urlparse(str(value)).hostname or "").casefold()
if hostname in {"localhost", "metadata", "metadata.google.internal"}:
raise ValueError("dataplane upstream host is not allowed")
try:
address = ipaddress.ip_address(hostname)
except ValueError:
address = None
if address is not None and (
address.is_private
or address.is_loopback
or address.is_link_local
or address.is_reserved
or address.is_unspecified
or address.is_multicast
):
raise ValueError("dataplane upstream host is not allowed")
return value
@field_validator("protocol_revisions")
@classmethod
def validate_protocols(cls, value: tuple[str, ...]) -> tuple[str, ...]:
if not value or not set(value).issubset(SUPPORTED_PROTOCOLS):
raise ValueError("unsupported MCP protocol revision")
return tuple(dict.fromkeys(value))
@model_validator(mode="after")
def validate_identity(self) -> AliasRecord:
if not self.alias.endswith(f"/{self.environment}"):
raise ValueError("alias environment does not match the record environment")
if len({binding.client_id for binding in self.bindings}) != len(self.bindings):
raise ValueError("duplicate client binding")
return self
class InvocationMetadata(BaseModel):
model_config = ConfigDict(extra="forbid", frozen=True)
trace_id: str = Field(pattern=r"^[A-Za-z0-9._:-]{1,128}$")
project_slug: str
environment: EnvironmentName
client_id: UUID
subject_digest: str = Field(pattern=r"^[0-9a-f]{64}$")
method: str = Field(min_length=1, max_length=16)
mcp_method: str | None = Field(default=None, max_length=128)
mcp_name: str | None = Field(default=None, max_length=256)
protocol_revision: str
request_bytes: int = Field(ge=0, le=4 * 1024 * 1024)
response_status: int = Field(ge=100, le=599)
response_bytes: int = Field(ge=0, le=4 * 1024 * 1024)
response_content_type: str | None = Field(default=None, max_length=128)
jsonrpc_response: bool = False
response_id_matches: bool | None = None
response_shape: str = Field(default="unknown", pattern=r"^(result|error|unknown)$")
duration_ms: int = Field(ge=0, le=120_000)
class RouterSettings(BaseModel):
model_config = ConfigDict(extra="forbid", frozen=True)
max_body_bytes: int = Field(default=4 * 1024 * 1024, ge=1024, le=4 * 1024 * 1024)
max_response_bytes: int = Field(default=4 * 1024 * 1024, ge=1024, le=4 * 1024 * 1024)
timeout_seconds: float = Field(default=30.0, gt=0, le=30)
alias_cache_ttl_seconds: int = Field(default=30, ge=1, le=60)
max_upstream_concurrency: int = Field(default=24, ge=1, le=256)
upstream_queue_timeout_seconds: float = Field(default=5.0, ge=0.1, le=30)
class PrincipalLike(Protocol):
"""Minimal validated principal contract consumed by the dataplane."""
@property
def subject(self) -> str: ...
@property
def issuer(self) -> str: ...
@property
def kind(self) -> object: ...
@property
def client_id(self) -> UUID | None: ...
@property
def expires_at(self) -> datetime: ...
+5
View File
@@ -0,0 +1,5 @@
"""Typed control-plane domain primitives."""
from .domain import Scope
__all__ = ["Scope"]
+303
View File
@@ -0,0 +1,303 @@
"""Pydantic domain contracts for the Hub control plane.
These models deliberately contain references to credentials, never credential
values. Persistence rows use the same field names so repositories can validate
their output through ``from_attributes`` without leaking ORM objects upward.
"""
from __future__ import annotations
from datetime import datetime
from typing import Any, ClassVar
from uuid import UUID, uuid4
from pydantic import BaseModel, ConfigDict, Field, field_validator
from .errors import MissingProjectScope
class DomainModel(BaseModel):
model_config = ConfigDict(extra="forbid", from_attributes=True, use_enum_values=True)
_datetime_fields: ClassVar[frozenset[str]] = frozenset()
@field_validator("*", mode="before")
@classmethod
def require_aware_datetime(cls, value: Any) -> Any:
if isinstance(value, datetime) and value.tzinfo is None:
raise ValueError("timestamps must include timezone information")
return value
class Scope(DomainModel):
"""Mandatory authorization scope passed to every repository boundary."""
workspace_id: UUID
project_id: UUID | None = None
@classmethod
def workspace(cls, workspace_id: UUID) -> Scope:
return cls(workspace_id=workspace_id)
@classmethod
def project(cls, workspace_id: UUID, project_id: UUID) -> Scope:
return cls(workspace_id=workspace_id, project_id=project_id)
def require_project(self) -> UUID:
if self.project_id is None:
raise MissingProjectScope("project scope is required")
return self.project_id
class Timestamped(DomainModel):
id: UUID = Field(default_factory=uuid4)
created_at: datetime
updated_at: datetime
revision: int = Field(default=1, ge=1)
class Workspace(Timestamped):
slug: str = Field(pattern=r"^[a-z0-9][a-z0-9-]{0,62}$")
name: str = Field(min_length=1, max_length=160)
status: str = Field(default="active", min_length=1, max_length=32)
class Project(Timestamped):
workspace_id: UUID
slug: str = Field(pattern=r"^[a-z0-9][a-z0-9-]{0,62}$")
name: str = Field(min_length=1, max_length=160)
description: str | None = Field(default=None, max_length=1000)
status: str = Field(default="active", max_length=32)
repository_provider: str | None = Field(default=None, max_length=32)
repository_owner: str | None = Field(default=None, max_length=120)
repository_name: str | None = Field(default=None, max_length=120)
classification: str = Field(default="internal", max_length=32)
owner_metadata: dict[str, Any] = Field(default_factory=dict)
class Environment(Timestamped):
workspace_id: UUID
project_id: UUID
name: str = Field(pattern=r"^(dev|test|staging|prod)$")
classification: str = Field(max_length=32)
base_url: str | None = Field(default=None, max_length=2048)
status: str = Field(default="active", max_length=32)
class Connector(Timestamped):
workspace_id: UUID
project_id: UUID
environment_id: UUID | None = None
type: str = Field(min_length=1, max_length=64)
slug: str = Field(pattern=r"^[a-z0-9][a-z0-9-]{0,62}$")
endpoint: str | None = Field(default=None, max_length=2048)
credential_reference: str | None = Field(default=None, max_length=512)
configuration: dict[str, Any] = Field(default_factory=dict)
enabled: bool = False
health_state: str = Field(default="unconfigured", max_length=32)
last_check_at: datetime | None = None
latency_ms: int | None = Field(default=None, ge=0)
stable_error_code: str | None = Field(default=None, max_length=128)
sanitized_error: str | None = Field(default=None, max_length=1000)
class RiskClass(DomainModel):
code: str = Field(pattern=r"^[a-z0-9][a-z0-9-]{0,62}$")
description: str = Field(min_length=1, max_length=500)
allows_writes: bool = False
max_output_bytes: int = Field(default=65536, ge=1024, le=10485760)
class Capability(Timestamped):
canonical_name: str = Field(pattern=r"^[a-z0-9][a-z0-9.-]{0,119}$")
version: str = Field(min_length=1, max_length=32)
description: str = Field(min_length=1, max_length=1000)
risk_class: str = Field(min_length=1, max_length=64)
enabled: bool = True
class ToolDefinition(Timestamped):
workspace_id: UUID
project_id: UUID
connector_id: UUID
canonical_name: str = Field(pattern=r"^[a-z0-9][a-z0-9.-]{0,119}$")
version: str = Field(min_length=1, max_length=32)
input_schema: dict[str, Any] = Field(default_factory=dict)
output_schema: dict[str, Any] = Field(default_factory=dict)
timeout_ms: int = Field(default=10000, ge=100, le=120000)
output_limit_bytes: int = Field(default=65536, ge=1024, le=10485760)
rate_limit_per_minute: int = Field(default=60, ge=1, le=100000)
redaction_profile: str = Field(default="default", max_length=64)
lifecycle_status: str = Field(default="active", max_length=32)
class VirtualServer(Timestamped):
workspace_id: UUID
project_id: UUID
environment_id: UUID
slug: str = Field(pattern=r"^[a-z0-9][a-z0-9-]{0,62}$")
gateway_external_id: str | None = Field(default=None, max_length=256)
desired_hash: str = Field(min_length=1, max_length=128)
enabled: bool = False
protocol: str = Field(default="streamable-http", max_length=64)
health_state: str = Field(default="unconfigured", max_length=32)
metadata: dict[str, Any] = Field(default_factory=dict)
class VirtualServerCapability(DomainModel):
id: UUID = Field(default_factory=uuid4)
workspace_id: UUID
project_id: UUID
virtual_server_id: UUID
capability_id: UUID
desired_state_source: str = Field(default="catalog", max_length=64)
created_at: datetime
updated_at: datetime
class Client(Timestamped):
workspace_id: UUID
subject: str = Field(min_length=1, max_length=256)
issuer: str = Field(min_length=1, max_length=512)
client_type: str = Field(default="mcp", max_length=32)
status: str = Field(default="pending", max_length=32)
revoked_at: datetime | None = None
last_seen_at: datetime | None = None
metadata: dict[str, Any] = Field(default_factory=dict)
class ClientBinding(Timestamped):
workspace_id: UUID
project_id: UUID
client_id: UUID
virtual_server_id: UUID
capability_pack: str | None = Field(default=None, max_length=128)
constraints: dict[str, Any] = Field(default_factory=dict)
enabled: bool = False
class Policy(Timestamped):
workspace_id: UUID
project_id: UUID | None = None
slug: str = Field(pattern=r"^[a-z0-9][a-z0-9-]{0,62}$")
priority: int = Field(ge=0, le=100000)
description: str = Field(min_length=1, max_length=1000)
match: dict[str, Any] = Field(default_factory=dict)
conditions: dict[str, Any] = Field(default_factory=dict)
decision: str = Field(pattern=r"^(allow|allow_if_conditions|deny|deny_and_alert|enforce)$")
terminal: bool = False
source: str = Field(default="catalog", max_length=64)
enabled: bool = True
class PolicyDecision(DomainModel):
id: UUID = Field(default_factory=uuid4)
workspace_id: UUID
project_id: UUID | None = None
policy_id: UUID | None = None
decision: str = Field(min_length=1, max_length=32)
matched_rule_ids: list[str] = Field(default_factory=list)
decision_trace: list[dict[str, Any]] = Field(default_factory=list)
reason_code: str | None = Field(default=None, max_length=128)
trace_id: str = Field(min_length=1, max_length=128)
retention_until: datetime | None = None
created_at: datetime
class ReconcileRun(Timestamped):
workspace_id: UUID
project_id: UUID
desired_hash: str = Field(min_length=1, max_length=128)
current_hash: str | None = Field(default=None, max_length=128)
plan_hash: str | None = Field(default=None, max_length=128)
status: str = Field(default="created", max_length=32)
planned_changes: list[dict[str, Any]] = Field(default_factory=list)
applied_changes: list[dict[str, Any]] = Field(default_factory=list)
verification: dict[str, Any] = Field(default_factory=dict)
started_at: datetime | None = None
completed_at: datetime | None = None
class Operation(Timestamped):
workspace_id: UUID
project_id: UUID
environment_id: UUID
operation_type: str = Field(min_length=1, max_length=64)
target_ref: str = Field(min_length=1, max_length=512)
plan_hash: str = Field(min_length=1, max_length=128)
state: str = Field(default="requested", max_length=32)
requested_by: str = Field(min_length=1, max_length=256)
expires_at: datetime | None = None
verification: dict[str, Any] = Field(default_factory=dict)
class Approval(Timestamped):
workspace_id: UUID
project_id: UUID
operation_id: UUID
plan_hash: str = Field(min_length=1, max_length=128)
state: str = Field(default="pending", max_length=32)
actor: str = Field(min_length=1, max_length=256)
reason: str | None = Field(default=None, max_length=1000)
expires_at: datetime
approved_at: datetime | None = None
class AuditEvent(DomainModel):
id: UUID = Field(default_factory=uuid4)
occurred_at: datetime
trace_id: str = Field(min_length=1, max_length=128)
workspace_id: UUID
project_id: UUID | None = None
environment_id: UUID | None = None
event_type: str = Field(min_length=1, max_length=128)
principal_issuer: str | None = Field(default=None, max_length=512)
principal_subject: str | None = Field(default=None, max_length=256)
client_id: UUID | None = None
virtual_server_id: UUID | None = None
tool_name: str | None = Field(default=None, max_length=256)
policy_decision: str | None = Field(default=None, max_length=32)
matched_rule_ids: list[str] = Field(default_factory=list)
policy_trace: list[dict[str, Any]] = Field(default_factory=list)
arguments_hash: str | None = Field(default=None, max_length=128)
sanitized_arguments: dict[str, Any] = Field(default_factory=dict)
duration_ms: int | None = Field(default=None, ge=0)
outcome: str = Field(min_length=1, max_length=32)
error_code: str | None = Field(default=None, max_length=128)
retention_until: datetime | None = None
operation_id: UUID | None = None
class KnowledgeSource(Timestamped):
workspace_id: UUID
project_id: UUID
slug: str = Field(pattern=r"^[a-z0-9][a-z0-9-]{0,62}$")
source_type: str = Field(min_length=1, max_length=64)
uri: str = Field(min_length=1, max_length=2048)
visibility: str = Field(default="project", max_length=32)
state: str = Field(default="registered", max_length=32)
configuration: dict[str, Any] = Field(default_factory=dict)
last_indexed_at: datetime | None = None
class KnowledgeDocument(Timestamped):
workspace_id: UUID
project_id: UUID
source_id: UUID
external_ref: str = Field(min_length=1, max_length=1024)
title: str = Field(min_length=1, max_length=500)
content_hash: str = Field(min_length=1, max_length=128)
document_version: int = Field(default=1, ge=1)
state: str = Field(default="registered", max_length=32)
content_bytes: int = Field(default=0, ge=0, le=104857600)
metadata: dict[str, Any] = Field(default_factory=dict)
class KnowledgeChunk(Timestamped):
workspace_id: UUID
project_id: UUID
document_id: UUID
chunk_index: int = Field(ge=0)
text: str = Field(min_length=1, max_length=200000)
content_hash: str = Field(min_length=1, max_length=128)
token_count: int = Field(default=0, ge=0)
metadata: dict[str, Any] = Field(default_factory=dict)
+21
View File
@@ -0,0 +1,21 @@
"""Domain errors shared by persistence and application services."""
class DomainError(ValueError):
"""Base class for expected domain validation failures."""
class InvalidTransition(DomainError):
"""Raised when a state machine transition is not permitted."""
class MissingProjectScope(DomainError):
"""Raised when a project-owned repository is created without a project scope."""
class ConcurrentUpdateError(DomainError):
"""Raised when an optimistic version check detects a stale write."""
class AuditMutationError(DomainError):
"""Raised when ordinary application code attempts to mutate an audit event."""
+75
View File
@@ -0,0 +1,75 @@
"""Explicit state transition tables for control-plane records."""
from collections.abc import Mapping
from .errors import InvalidTransition
TRANSITIONS: Mapping[str, Mapping[str, frozenset[str]]] = {
"connector_health": {
"unconfigured": frozenset({"checking", "disabled"}),
"checking": frozenset({"healthy", "degraded", "unavailable", "misconfigured", "disabled"}),
"healthy": frozenset({"checking", "degraded", "unavailable", "disabled"}),
"degraded": frozenset({"checking", "healthy", "unavailable", "misconfigured", "disabled"}),
"unavailable": frozenset({"checking", "degraded", "misconfigured", "disabled"}),
"misconfigured": frozenset({"checking", "disabled"}),
"disabled": frozenset(),
},
"reconcile": {
"created": frozenset({"planning"}),
"planning": frozenset({"planned", "invalid"}),
"planned": frozenset({"applying", "invalid"}),
"applying": frozenset({"verifying", "failed"}),
"verifying": frozenset({"succeeded", "failed"}),
"succeeded": frozenset(),
"invalid": frozenset(),
"failed": frozenset(),
},
"client": {
"pending": frozenset({"active", "disabled"}),
"active": frozenset({"disabled", "revoked"}),
"disabled": frozenset({"active", "revoked"}),
"revoked": frozenset(),
},
"operation": {
"requested": frozenset({"planned", "rejected"}),
"planned": frozenset({"approval_required", "rejected"}),
"approval_required": frozenset({"approved", "rejected"}),
"approved": frozenset({"executing", "rejected"}),
"executing": frozenset({"verifying", "failed"}),
"verifying": frozenset({"succeeded", "failed"}),
"failed": frozenset({"rollback_pending"}),
"rollback_pending": frozenset({"rolled_back", "rollback_failed"}),
"rolled_back": frozenset(),
"rollback_failed": frozenset(),
"succeeded": frozenset(),
"rejected": frozenset(),
},
"approval": {
"pending": frozenset({"approved", "rejected", "expired"}),
"approved": frozenset(),
"rejected": frozenset(),
"expired": frozenset(),
},
"knowledge": {
"registered": frozenset({"queued"}),
"queued": frozenset({"indexing", "failed"}),
"indexing": frozenset({"current", "failed"}),
"current": frozenset({"stale"}),
"stale": frozenset({"queued"}),
"failed": frozenset({"queued"}),
},
}
def transition(machine: str, current: str, target: str) -> str:
"""Validate and return a target state for a named state machine."""
table = TRANSITIONS.get(machine)
if table is None:
raise InvalidTransition(f"unknown state machine: {machine}")
allowed = table.get(current)
if allowed is None:
raise InvalidTransition(f"unknown {machine} state: {current}")
if target not in allowed:
raise InvalidTransition(f"invalid {machine} transition: {current} -> {target}")
return target
+25
View File
@@ -0,0 +1,25 @@
"""Small, dependency-light observability primitives for every Hub service."""
from .telemetry import (
MetricsRegistry,
StructuredLogger,
Telemetry,
TraceContext,
build_telemetry,
new_trace_id,
sanitize_fields,
trace_from_headers,
valid_trace_id,
)
__all__ = [
"MetricsRegistry",
"StructuredLogger",
"Telemetry",
"TraceContext",
"build_telemetry",
"new_trace_id",
"sanitize_fields",
"trace_from_headers",
"valid_trace_id",
]
+265
View File
@@ -0,0 +1,265 @@
"""Structured telemetry with W3C trace propagation and OTLP/HTTP export.
The module intentionally uses the standard library so the health process and
connectors can share the same contract without adding a runtime dependency.
Export is best-effort and never puts authorization, cookies, request bodies,
or secret values into logs, metrics, or spans.
"""
from __future__ import annotations
import json
import os
import re
import secrets
import sys
import time
import urllib.error
import urllib.request
from collections.abc import Iterator, Mapping
from contextlib import contextmanager
from dataclasses import dataclass, field
from datetime import UTC, datetime
from typing import Any, TextIO
_TRACE_ID = re.compile(r"^[0-9a-f]{32}$")
_SPAN_ID = re.compile(r"^[0-9a-f]{16}$")
_SENSITIVE = re.compile(
r"(?:authorization|cookie|password|passwd|secret|token|credential|private.?key|api.?key)",
re.IGNORECASE,
)
_METRIC = re.compile(r"[^a-zA-Z0-9_:]")
def new_trace_id() -> str:
return secrets.token_hex(16)
def _new_span_id() -> str:
return secrets.token_hex(8)
def valid_trace_id(value: str | None) -> str | None:
if value is None:
return None
candidate = value.strip().lower()
return candidate if _TRACE_ID.fullmatch(candidate) else None
def trace_from_headers(headers: Mapping[str, str]) -> TraceContext:
"""Accept a W3C traceparent trace-id, otherwise create a new root trace."""
traceparent = headers.get("traceparent", headers.get("Traceparent", ""))
pieces = traceparent.split("-")
trace_id = valid_trace_id(pieces[1] if len(pieces) == 4 else None)
parent = pieces[2].lower() if len(pieces) == 4 and _SPAN_ID.fullmatch(pieces[2]) else None
return TraceContext(trace_id=trace_id or new_trace_id(), parent_span_id=parent)
@dataclass(frozen=True)
class TraceContext:
trace_id: str
parent_span_id: str | None = None
def __post_init__(self) -> None:
if not _TRACE_ID.fullmatch(self.trace_id):
raise ValueError("trace_id must be 32 lowercase hexadecimal characters")
def child(self) -> TraceContext:
return TraceContext(self.trace_id, _new_span_id())
def response_headers(self) -> dict[str, str]:
return {"X-Trace-Id": self.trace_id}
def sanitize_fields(value: Any, *, _key: str = "") -> Any:
"""Return JSON-safe fields with sensitive keys replaced and bounded values."""
if _key and _SENSITIVE.search(_key):
return "[REDACTED]"
if isinstance(value, Mapping):
return {str(key): sanitize_fields(item, _key=str(key)) for key, item in value.items()}
if isinstance(value, list | tuple | set):
return [sanitize_fields(item) for item in value][:100]
if isinstance(value, str | int | float | bool) or value is None:
return str(value)[:512] if isinstance(value, str) else value
return str(value)[:512]
class StructuredLogger:
def __init__(self, service: str, *, stream: TextIO | None = None) -> None:
self.service = service[:64]
self.stream = stream or sys.stdout
def emit(
self,
event_code: str,
trace_id: str,
*,
level: str = "info",
fields: Mapping[str, Any] | None = None,
) -> dict[str, Any]:
safe_trace_id = valid_trace_id(trace_id) or new_trace_id()
record = {
"timestamp": datetime.now(UTC).isoformat(),
"service": self.service,
"version": os.environ.get("APP_VERSION", "dev")[:64],
"environment": os.environ.get("APP_ENV", "unknown")[:32],
"traceId": safe_trace_id,
"eventCode": event_code[:96],
"level": level[:16],
"fields": sanitize_fields(fields or {}),
}
self.stream.write(json.dumps(record, sort_keys=True, separators=(",", ":")) + "\n")
self.stream.flush()
return record
@dataclass
class _Metric:
kind: str
value: float = 0.0
observations: list[float] = field(default_factory=list)
class MetricsRegistry:
"""In-process metrics with a stable Prometheus text representation."""
def __init__(self) -> None:
self._metrics: dict[tuple[str, tuple[tuple[str, str], ...]], _Metric] = {}
@staticmethod
def _name(name: str) -> str:
normalized = _METRIC.sub("_", name).strip("_").lower()
if not normalized or normalized[0].isdigit():
raise ValueError("metric name must start with a letter or underscore")
return normalized
@staticmethod
def _labels(labels: Mapping[str, str] | None) -> tuple[tuple[str, str], ...]:
if not labels:
return ()
safe = []
for key, value in sorted(labels.items()):
if _SENSITIVE.search(key):
continue
safe.append((_METRIC.sub("_", key)[:32], str(value)[:64]))
return tuple(safe)
def _get(self, name: str, labels: Mapping[str, str] | None, kind: str) -> _Metric:
key = (self._name(name), self._labels(labels))
metric = self._metrics.setdefault(key, _Metric(kind=kind))
if metric.kind != kind:
raise ValueError(f"metric {name!r} changed type")
return metric
def inc(
self, name: str, amount: float = 1.0, *, labels: Mapping[str, str] | None = None
) -> None:
if amount < 0:
raise ValueError("counter increments cannot be negative")
self._get(name, labels, "counter").value += amount
def set_gauge(
self, name: str, value: float, *, labels: Mapping[str, str] | None = None
) -> None:
self._get(name, labels, "gauge").value = value
def observe(self, name: str, value: float, *, labels: Mapping[str, str] | None = None) -> None:
metric = self._get(name, labels, "histogram")
metric.observations.append(max(0.0, value))
def render_prometheus(self) -> str:
lines: list[str] = []
for (name, labels), metric in sorted(self._metrics.items()):
label_text = ""
if labels:
parts = []
for key, value in labels:
escaped = value.replace(chr(92), chr(92) * 2).replace(
chr(34), chr(92) + chr(34)
)
parts.append(f'{key}="{escaped}"')
label_text = "{" + ",".join(parts) + "}"
if metric.kind == "histogram":
total = sum(metric.observations)
count = len(metric.observations)
lines.extend(
[f"{name}_count{label_text} {count}", f"{name}_sum{label_text} {total:g}"]
)
else:
lines.append(f"{name}{label_text} {metric.value:g}")
return "\n".join(lines) + ("\n" if lines else "")
class OTLPHttpExporter:
def __init__(self, endpoint: str, *, timeout: float = 1.0) -> None:
if not endpoint.startswith(("http://", "https://")):
raise ValueError("OTLP endpoint must use http or https")
self.endpoint = endpoint.rstrip("/") + "/v1/traces"
self.timeout = max(0.1, min(timeout, 5.0))
def export(self, span: Mapping[str, Any]) -> bool:
payload = json.dumps(
{"resourceSpans": [{"scopeSpans": [{"spans": [sanitize_fields(span)]}]}]}
).encode()
request = urllib.request.Request( # noqa: S310
self.endpoint,
data=payload,
headers={"Content-Type": "application/json"},
method="POST",
)
try:
with urllib.request.urlopen(request, timeout=self.timeout) as response: # noqa: S310
return 200 <= int(response.status) < 300
except (OSError, urllib.error.URLError):
return False
class Telemetry:
def __init__(
self,
service: str,
*,
stream: TextIO | None = None,
exporter: OTLPHttpExporter | None = None,
) -> None:
self.logger = StructuredLogger(service, stream=stream)
self.metrics = MetricsRegistry()
self.exporter = exporter
@contextmanager
def span(
self, name: str, context: TraceContext, *, attributes: Mapping[str, Any] | None = None
) -> Iterator[TraceContext]:
child = context.child()
start = time.perf_counter()
status = "ok"
try:
yield child
except Exception:
status = "error"
raise
finally:
duration = time.perf_counter() - start
self.metrics.inc("itworx_requests_total", labels={"operation": name, "status": status})
self.metrics.observe(
"itworx_request_duration_seconds", duration, labels={"operation": name}
)
span = {
"traceId": child.trace_id,
"spanId": child.parent_span_id or _new_span_id(),
"name": name[:128],
"startTimeUnixNano": int((time.time() - duration) * 1_000_000_000),
"endTimeUnixNano": int(time.time() * 1_000_000_000),
"status": {"code": status},
"attributes": attributes or {},
}
if self.exporter:
self.exporter.export(span)
def build_telemetry(service: str, *, stream: TextIO | None = None) -> Telemetry:
endpoint = os.environ.get("OTEL_EXPORTER_OTLP_ENDPOINT", "").strip()
exporter = OTLPHttpExporter(endpoint) if endpoint else None
return Telemetry(service, stream=stream, exporter=exporter)
+20
View File
@@ -0,0 +1,20 @@
"""Recovery and backup primitives used by the operational runbooks."""
from .backup import (
BackupArtifact,
BackupError,
BackupManager,
BackupPolicy,
build_pg_dump_command,
)
from .recovery import RecoveryProbe, RecoveryReport
__all__ = [
"BackupArtifact",
"BackupError",
"BackupManager",
"BackupPolicy",
"RecoveryProbe",
"RecoveryReport",
"build_pg_dump_command",
]
+243
View File
@@ -0,0 +1,243 @@
"""Hash-verified, clean-room backup and restore primitives.
Archives contain only an explicit allowlist of non-secret inputs. Database
dump commands use mounted service configuration and never accept a DSN on the
command line, keeping credentials out of process listings and evidence.
"""
from __future__ import annotations
import hashlib
import json
import os
import shutil
import subprocess
import tarfile
from collections.abc import Iterable, Mapping
from dataclasses import dataclass
from datetime import UTC, datetime, timedelta
from pathlib import Path
from typing import Any
from packages.hub_observability import sanitize_fields
class BackupError(ValueError):
"""Raised for unsafe, corrupt, or incomplete backup operations."""
_SECRET_NAME = {".env", ".env.example", "secrets", "secret", "password", "credentials"}
def _sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as handle:
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()
def _safe_relative(root: Path, candidate: Path) -> Path:
try:
return candidate.resolve().relative_to(root.resolve())
except ValueError as exc:
raise BackupError("backup input must remain below its registered root") from exc
def _is_secret_path(relative: Path) -> bool:
return any(
any(
term in part.lower()
for term in ("secret", "password", "credential", "token", "private_key")
)
or part.lower() in _SECRET_NAME
for part in relative.parts
)
@dataclass(frozen=True)
class BackupPolicy:
retention_days: int = 30
max_artifacts: int = 14
encryption_required_before_export: bool = True
def __post_init__(self) -> None:
if not 1 <= self.retention_days <= 3650:
raise ValueError("retention_days must be between 1 and 3650")
if not 1 <= self.max_artifacts <= 1000:
raise ValueError("max_artifacts must be between 1 and 1000")
@dataclass(frozen=True)
class BackupArtifact:
archive: Path
manifest: Path
sha256: str
bytes: int
entries: tuple[str, ...]
created_at: str
class BackupManager:
def __init__(self, backup_root: Path, *, policy: BackupPolicy | None = None) -> None:
self.backup_root = backup_root.resolve()
self.policy = policy or BackupPolicy()
def create_archive(
self,
source_root: Path,
*,
name: str,
include: Iterable[Path],
metadata: Mapping[str, Any] | None = None,
) -> BackupArtifact:
if not name or Path(name).name != name or name in {".", ".."}:
raise BackupError("backup name must be a single safe path component")
root = source_root.resolve()
self.backup_root.mkdir(parents=True, exist_ok=True)
archive = self.backup_root / f"{name}.tar.gz"
manifest_path = self.backup_root / f"{name}.manifest.json"
entries: list[str] = []
with tarfile.open(archive, "w:gz") as bundle:
for raw in include:
candidate = (root / raw).resolve() if not raw.is_absolute() else raw.resolve()
relative = _safe_relative(root, candidate)
if _is_secret_path(relative):
raise BackupError(
f"secret-bearing path is not eligible for ordinary backup: {relative}"
)
if not candidate.exists():
raise BackupError(f"backup input does not exist: {relative}")
if candidate.is_symlink() or (
candidate.is_dir() and any(item.is_symlink() for item in candidate.rglob("*"))
):
raise BackupError("symlink backup inputs are not allowed")
bundle.add(candidate, arcname=relative.as_posix(), recursive=True)
entries.append(relative.as_posix())
entries = sorted(set(entries))
created_at = datetime.now(UTC).isoformat()
digest = _sha256(archive)
manifest = {
"schemaVersion": "1.0",
"artifact": archive.name,
"sha256": digest,
"bytes": archive.stat().st_size,
"createdAt": created_at,
"entries": entries,
"metadata": sanitize_fields(dict(metadata or {})),
"encryption": "required-before-export"
if self.policy.encryption_required_before_export
else "not-required",
"retention": {
"days": self.policy.retention_days,
"maxArtifacts": self.policy.max_artifacts,
},
}
manifest_path.write_text(
json.dumps(manifest, indent=2, sort_keys=True) + "\n", encoding="utf-8"
)
archive.with_suffix(archive.suffix + ".sha256").write_text(
f"{digest} {archive.name}\n", encoding="utf-8"
)
return BackupArtifact(
archive, manifest_path, digest, archive.stat().st_size, tuple(entries), created_at
)
def verify(self, artifact: BackupArtifact | Path) -> BackupArtifact:
archive = artifact.archive if isinstance(artifact, BackupArtifact) else artifact
manifest_path = archive.with_name(archive.name.removesuffix(".tar.gz") + ".manifest.json")
if not archive.is_file() or not manifest_path.is_file():
raise BackupError("backup archive and manifest are both required")
raw = json.loads(manifest_path.read_text(encoding="utf-8"))
digest = _sha256(archive)
if raw.get("sha256") != digest or raw.get("bytes") != archive.stat().st_size:
raise BackupError("backup integrity metadata does not match archive")
entries = tuple(str(item) for item in raw.get("entries", []))
if any(Path(item).is_absolute() or ".." in Path(item).parts for item in entries):
raise BackupError("backup manifest contains an unsafe entry")
return BackupArtifact(
archive, manifest_path, digest, archive.stat().st_size, entries, str(raw["createdAt"])
)
def restore_clean(self, artifact: BackupArtifact | Path, target_root: Path) -> BackupArtifact:
verified = self.verify(artifact)
target = target_root.resolve()
if target.exists() and any(target.iterdir()):
raise BackupError("clean restore target must not contain existing files")
target.mkdir(parents=True, exist_ok=True)
with tarfile.open(verified.archive, "r:gz") as bundle:
members = bundle.getmembers()
for member in members:
member_path = Path(member.name)
if (
member_path.is_absolute()
or ".." in member_path.parts
or member.issym()
or member.islnk()
):
raise BackupError("archive contains an unsafe member")
if member.isdir():
continue
destination = (target / member_path).resolve()
try:
destination.relative_to(target)
except ValueError as exc:
raise BackupError("archive member escapes restore target") from exc
destination.parent.mkdir(parents=True, exist_ok=True)
source = bundle.extractfile(member)
if source is None:
raise BackupError("archive member could not be read")
with destination.open("wb") as output:
shutil.copyfileobj(source, output)
return verified
def prune(self, *, now: datetime | None = None) -> list[Path]:
current = now or datetime.now(UTC)
manifests = sorted(
self.backup_root.glob("*.manifest.json"),
key=lambda item: item.stat().st_mtime,
reverse=True,
)
removed: list[Path] = []
for index, manifest in enumerate(manifests):
too_old = datetime.fromtimestamp(manifest.stat().st_mtime, UTC) < current - timedelta(
days=self.policy.retention_days
)
if index >= self.policy.max_artifacts or too_old:
archive = manifest.with_name(
manifest.name.removesuffix(".manifest.json") + ".tar.gz"
)
for path in (manifest, archive, archive.with_suffix(archive.suffix + ".sha256")):
if path.exists():
path.unlink()
removed.append(path)
return removed
def build_pg_dump_command(
database_name: str, output_path: Path, *, executable: str = "pg_dump"
) -> list[str]:
"""Build a password-free command; libpq reads credentials from its mounted service file."""
if not database_name or database_name.startswith("-") or "/" in database_name:
raise BackupError("database name is not a safe identifier")
if output_path.name != output_path.name.replace(" ", "") or output_path.suffix != ".dump":
raise BackupError("database dump output must be a simple .dump filename")
return [
executable,
"--format=custom",
"--no-owner",
"--no-privileges",
"--dbname",
database_name,
"--file",
str(output_path),
]
def run_pg_dump(
database_name: str, output_path: Path, *, environment: Mapping[str, str] | None = None
) -> None:
command = build_pg_dump_command(database_name, output_path)
safe_environment = dict(environment or os.environ)
subprocess.run(command, check=True, env=safe_environment, stdout=subprocess.DEVNULL) # noqa: S603
+66
View File
@@ -0,0 +1,66 @@
"""Deterministic recovery assertions used by clean-room restore tests."""
from __future__ import annotations
from collections.abc import Callable, Mapping
from dataclasses import dataclass
from typing import Any
@dataclass(frozen=True)
class RecoveryReport:
checks: Mapping[str, bool]
details: Mapping[str, str]
@property
def passed(self) -> bool:
return all(self.checks.values())
class RecoveryProbe:
"""Prove recovery semantics without requiring a production credential."""
def __init__(
self,
*,
login: Callable[[], bool],
discovery: Callable[[], bool],
tool_call: Callable[[], bool],
negative_auth: Callable[[], bool],
audit_continuity: Callable[[], bool],
) -> None:
self._checks = {
"login": login,
"discovery": discovery,
"toolCall": tool_call,
"negativeAuth": negative_auth,
"auditContinuity": audit_continuity,
}
def run(self) -> RecoveryReport:
results: dict[str, bool] = {}
details: dict[str, str] = {}
for name, check in self._checks.items():
try:
result = bool(check())
except Exception as exc: # noqa: BLE001
result = False
details[name] = type(exc).__name__
results[name] = result
details.setdefault(name, "passed" if result else "failed")
return RecoveryReport(results, details)
def connector_isolation_report(statuses: Mapping[str, str]) -> dict[str, Any]:
"""A failed connector degrades only itself and never grants access elsewhere."""
normalized = {str(name): str(status) for name, status in statuses.items()}
failed = sorted(
name for name, status in normalized.items() if status not in {"healthy", "ready"}
)
return {
"status": "degraded" if failed else "healthy",
"failedConnectors": failed,
"isolated": True,
"redisAuthoritative": False,
}
+34
View File
@@ -0,0 +1,34 @@
"""Deterministic, deny-first policy, audit, and rate-limit primitives."""
from .audit import AuditEvent, AuditLog, AuditPage, InMemoryAuditLog, sanitize_arguments
from .engine import (
Decision,
DecisionTrace,
InvocationContext,
PolicyEngine,
PolicyRule,
PolicySimulator,
PolicyValidationError,
)
from .governance import Governance, PolicyDenied, governed_tool
from .rate_limit import RateLimitExceeded, SlidingWindowRateLimiter
__all__ = [
"AuditEvent",
"AuditLog",
"AuditPage",
"Decision",
"DecisionTrace",
"Governance",
"governed_tool",
"InMemoryAuditLog",
"InvocationContext",
"PolicyDenied",
"PolicyEngine",
"PolicyRule",
"PolicySimulator",
"PolicyValidationError",
"RateLimitExceeded",
"SlidingWindowRateLimiter",
"sanitize_arguments",
]
+165
View File
@@ -0,0 +1,165 @@
"""Sanitized append-only audit ingestion, search, retention, and export metadata."""
from __future__ import annotations
import hashlib
import json
import re
from collections.abc import Mapping
from datetime import UTC, datetime, timedelta
from typing import Any, Protocol
from uuid import UUID, uuid4
from pydantic import BaseModel, ConfigDict, Field
from .engine import DecisionTrace
_SECRET_VALUE = re.compile(r"(?:bearer\s+|eyJ[A-Za-z0-9_-]{20,}|-----BEGIN[^-]+-----)", re.I)
_SENSITIVE = re.compile(
r"(?:password|secret|token|private|authorization|cookie|credential|api[_-]?key)", re.I
)
def sanitize_arguments(value: Any, *, max_bytes: int = 16_384, _depth: int = 0) -> Any:
if _depth > 5:
return "<depth-limited>"
result: Any
if isinstance(value, Mapping):
result = {
str(key)[:80]: (
"<redacted>"
if _SENSITIVE.search(str(key))
else sanitize_arguments(item, max_bytes=max_bytes, _depth=_depth + 1)
)
for key, item in list(value.items())[:64]
}
elif isinstance(value, list | tuple):
result = [
sanitize_arguments(item, max_bytes=max_bytes, _depth=_depth + 1)
for item in list(value)[:64]
]
elif isinstance(value, str | int | float | bool) or value is None:
if isinstance(value, str) and _SECRET_VALUE.search(value):
result = "<redacted>"
else:
result = value if not isinstance(value, str) else value[:2048]
else:
result = f"<{type(value).__name__}>"
if len(json.dumps(result, sort_keys=True, default=str).encode()) > max_bytes:
return "<arguments-truncated>"
return result
class AuditEvent(BaseModel):
model_config = ConfigDict(extra="forbid", frozen=True)
id: UUID = Field(default_factory=uuid4)
occurred_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
trace_id: str = Field(min_length=1, max_length=128)
event_type: str = Field(default="mcp.tool.invoked", max_length=128)
workspace: str = Field(min_length=1, max_length=128)
project: str = Field(min_length=1, max_length=128)
environment: str = Field(min_length=1, max_length=32)
principal: str = Field(min_length=1, max_length=256)
client_id: UUID | None = None
tool: str = Field(min_length=1, max_length=256)
policy_decision: str
matched_rule_ids: tuple[str, ...] = ()
policy_trace: tuple[dict[str, Any], ...] = ()
arguments_hash: str = Field(pattern=r"^[0-9a-f]{64}$")
sanitized_arguments: dict[str, Any] = Field(default_factory=dict)
duration_ms: int = Field(ge=0, le=120_000, default=0)
outcome: str = Field(pattern=r"^(success|denied|error|rate_limited)$")
error_code: str | None = None
retention_until: datetime | None = None
@classmethod
def from_context(
cls,
context: Any,
trace: DecisionTrace,
*,
outcome: str,
started: float,
error_code: str | None = None,
result_summary: dict[str, Any] | None = None,
) -> AuditEvent:
import time
raw = json.dumps(dict(context.arguments), sort_keys=True, default=str).encode()
return cls(
trace_id=trace.trace_id,
workspace=context.workspace,
project=context.project,
environment=context.environment,
principal=context.principal,
client_id=context.client_id,
tool=context.tool,
policy_decision=trace.decision.value,
matched_rule_ids=trace.matched_rule_ids,
policy_trace=tuple(item.model_dump(mode="json") for item in trace.steps),
arguments_hash=hashlib.sha256(raw).hexdigest(),
sanitized_arguments=sanitize_arguments({
**dict(context.arguments),
**({"_result": result_summary} if result_summary else {}),
}),
duration_ms=min(120_000, int((time.monotonic() - started) * 1000)),
outcome=outcome,
error_code=error_code,
retention_until=datetime.now(UTC) + timedelta(days=30),
)
class AuditPage(BaseModel):
model_config = ConfigDict(extra="forbid", frozen=True)
items: tuple[AuditEvent, ...]
next_offset: int | None = None
class AuditLog(Protocol):
def append(self, event: AuditEvent) -> None: ...
def search(
self, *, workspace: str, project: str, limit: int = 100, offset: int = 0
) -> AuditPage: ...
def export_metadata(self, *, workspace: str, project: str) -> dict[str, Any]: ...
class InMemoryAuditLog:
"""Reference sink used by services and tests; it deliberately has no update/delete API."""
def __init__(self) -> None:
self.events: list[AuditEvent] = []
def append(self, event: AuditEvent) -> None:
if any(item.id == event.id for item in self.events):
raise ValueError("audit event IDs are immutable")
self.events.append(event)
def search(
self, *, workspace: str, project: str, limit: int = 100, offset: int = 0
) -> AuditPage:
if not 1 <= limit <= 500 or offset < 0:
raise ValueError("invalid audit pagination")
items = tuple(
item for item in self.events if item.workspace == workspace and item.project == project
)
selected = items[offset : offset + limit]
next_offset = offset + len(selected) if offset + len(selected) < len(items) else None
return AuditPage(items=selected, next_offset=next_offset)
def export_metadata(self, *, workspace: str, project: str) -> dict[str, Any]:
events = self.search(workspace=workspace, project=project, limit=500).items
digest = hashlib.sha256(
"".join(item.model_dump_json() for item in events).encode()
).hexdigest()
return {
"workspace": workspace,
"project": project,
"eventCount": len(events),
"sha256": digest,
"generatedAt": datetime.now(UTC).isoformat(),
"format": "audit-event-v1",
"access": "policy-restricted",
}
+419
View File
@@ -0,0 +1,419 @@
"""Small typed policy evaluator shared by management, router, and connectors."""
from __future__ import annotations
import fnmatch
import re
from collections.abc import Iterable, Mapping
from enum import StrEnum
from typing import Any
from uuid import UUID, uuid4
from pydantic import BaseModel, ConfigDict, Field, field_validator
class PolicyValidationError(ValueError):
"""A policy contains an unsupported or unsafe selector."""
class Decision(StrEnum):
ALLOW = "allow"
ALLOW_WITH_PREVIEW = "allow_with_preview"
ALLOW_WITH_APPROVAL = "allow_with_approval"
ALLOW_IF_CONDITIONS = "allow_if_conditions"
ENFORCE = "enforce"
DENY = "deny"
DENY_AND_ALERT = "deny_and_alert"
_IDENTIFIER = re.compile(r"^[A-Za-z0-9._:/-]{1,256}$")
_MATCH_FIELDS = {
"workspace",
"workspaceId",
"project",
"projectId",
"projects",
"projectSlugs",
"environment",
"environments",
"environmentClassifications",
"client",
"clientId",
"clientIds",
"principal",
"principalType",
"virtualServer",
"virtualServers",
"capability",
"capabilities",
"tool",
"tools",
"toolPatterns",
"resourceType",
"resourceTypes",
"connector",
"connectors",
"riskClass",
"riskClasses",
"origin",
"origins",
}
_CONDITION_FIELDS = {
"requireEnabledClient",
"requireBinding",
"requireScope",
"requireCapabilityPack",
"requireConnectorBinding",
"requireInputValidation",
"requireOutputValidation",
"requireRedaction",
"maxArgumentBytes",
"allEqual",
"requiredClaims",
"maxDefaultBytes",
"maxDefaultItems",
"redactionProfile",
}
class InvocationContext(BaseModel):
"""Validated identity and target context used at every sensitive boundary."""
model_config = ConfigDict(extra="forbid", frozen=True)
workspace: str = Field(min_length=1, max_length=128)
project: str = Field(min_length=1, max_length=128)
environment: str = Field(pattern=r"^[a-z0-9][a-z0-9-]{0,31}$")
client_id: UUID | None = None
principal: str = Field(min_length=1, max_length=256)
principal_type: str = Field(default="machine", max_length=64)
virtual_server: str | None = Field(default=None, max_length=128)
capability: str | None = Field(default=None, max_length=256)
tool: str = Field(min_length=1, max_length=256)
connector: str = Field(min_length=1, max_length=64)
risk_class: str = Field(default="R1", pattern=r"^(?:R[0-9]+|RX)$")
origin: str = Field(default="mcp", max_length=64)
resource_type: str | None = Field(default=None, max_length=64)
environment_classification: str | None = Field(default=None, max_length=64)
binding_present: bool = False
client_enabled: bool = False
arguments: Mapping[str, Any] = Field(default_factory=dict)
trace_id: str = Field(default_factory=lambda: uuid4().hex, pattern=r"^[A-Za-z0-9._:-]{1,128}$")
class PolicyRule(BaseModel):
"""A catalog or persisted rule with only typed selector primitives."""
model_config = ConfigDict(extra="forbid", frozen=True)
slug: str = Field(pattern=r"^[a-z0-9][a-z0-9-]{0,62}$")
workspace: str | None = None
project: str | None = None
priority: int = Field(ge=0, le=100000)
description: str = Field(min_length=1, max_length=1000)
match: dict[str, Any] = Field(default_factory=dict)
conditions: dict[str, Any] = Field(default_factory=dict)
decision: Decision
terminal: bool = False
source: str = Field(default="catalog", max_length=64)
enabled: bool = True
@field_validator("match", "conditions")
@classmethod
def safe_maps(cls, value: dict[str, Any]) -> dict[str, Any]:
if len(value) > 32:
raise PolicyValidationError("policy selector map is too large")
return value
@classmethod
def from_document(cls, document: Mapping[str, Any]) -> PolicyRule:
metadata = document.get("metadata") or {}
spec = document.get("spec") or {}
if not isinstance(metadata, Mapping) or not isinstance(spec, Mapping):
raise PolicyValidationError("policy document metadata and spec must be objects")
match = dict(spec.get("match") or {})
conditions = dict(spec.get("conditions") or {})
unknown_match = set(match) - _MATCH_FIELDS
unknown_conditions = set(conditions) - _CONDITION_FIELDS
if unknown_match or unknown_conditions:
raise PolicyValidationError(
f"unsupported policy fields: {sorted(unknown_match | unknown_conditions)}"
)
project = spec.get("project")
workspace = spec.get("workspace")
return cls(
slug=str(metadata.get("slug", "")),
workspace=str(workspace) if workspace is not None else None,
project=str(project) if project is not None else None,
priority=int(spec.get("priority", 0)),
description=str(spec.get("description", "")),
match=match,
conditions=conditions,
decision=Decision(str(spec.get("decision", "deny"))),
terminal=bool(spec.get("terminal", False)),
source=str(spec.get("source", "catalog")),
enabled=bool(spec.get("enabled", True)),
)
class TraceStep(BaseModel):
model_config = ConfigDict(extra="forbid", frozen=True)
phase: str
rule: str | None = None
matched: bool
reason: str
class DecisionTrace(BaseModel):
model_config = ConfigDict(extra="forbid", frozen=True)
trace_id: str
decision: Decision
reason_code: str
matched_rule_ids: tuple[str, ...] = ()
steps: tuple[TraceStep, ...] = ()
def _values(value: Any) -> tuple[str, ...]:
if isinstance(value, list | tuple | set):
return tuple(str(item) for item in value)
return (str(value),)
def _context_value(context: InvocationContext, key: str) -> tuple[str, ...]:
mapping: dict[str, Any] = {
"workspace": context.workspace,
"workspaceId": context.workspace,
"project": context.project,
"projectId": context.project,
"projectSlugs": context.project,
"projects": context.project,
"environment": context.environment,
"environments": context.environment,
"environmentClassifications": context.environment_classification or context.environment,
"resourceType": context.resource_type or "",
"resourceTypes": context.resource_type or "",
"client": str(context.client_id) if context.client_id else "",
"clientId": str(context.client_id) if context.client_id else "",
"clientIds": str(context.client_id) if context.client_id else "",
"principal": context.principal,
"principalType": context.principal_type,
"virtualServer": context.virtual_server or "",
"capability": context.capability or "",
"capabilities": context.capability or "",
"tool": context.tool,
"tools": context.tool,
"toolPatterns": context.tool,
"connector": context.connector,
"connectors": context.connector,
"riskClass": context.risk_class,
"riskClasses": context.risk_class,
"origin": context.origin,
"origins": context.origin,
}
return _values(mapping.get(key, ""))
def _rule_scope_matches(rule: PolicyRule, context: InvocationContext) -> bool:
return (rule.workspace is None or rule.workspace == context.workspace) and (
rule.project is None or rule.project == context.project
)
def _matches(rule: PolicyRule, context: InvocationContext) -> bool:
if not _rule_scope_matches(rule, context):
return False
for key, expected in rule.match.items():
if key not in _MATCH_FIELDS:
raise PolicyValidationError(f"unsupported policy match field: {key}")
actual = _context_value(context, key)
expected_values = _values(expected)
if key in {"toolPatterns"}:
if not any(
fnmatch.fnmatchcase(value, pattern)
for value in actual
for pattern in expected_values
):
return False
elif not any(value in expected_values for value in actual):
return False
return True
def _conditions_match(rule: PolicyRule, context: InvocationContext) -> bool:
for key, expected in rule.conditions.items():
if key not in _CONDITION_FIELDS:
raise PolicyValidationError(f"unsupported policy condition: {key}")
if key == "requireEnabledClient" and bool(expected) and not context.client_enabled:
return False
if key == "requireBinding" and bool(expected) and not context.binding_present:
return False
if key == "maxArgumentBytes" and len(str(dict(context.arguments)).encode()) > int(expected):
return False
if key in {
"allEqual",
"requiredClaims",
"maxDefaultBytes",
"maxDefaultItems",
"redactionProfile",
}:
return False
return True
class PolicyEngine:
"""Deterministic precedence: validation, global deny, explicit deny, allow, default deny."""
def __init__(self, policies: Iterable[PolicyRule]) -> None:
self.policies = tuple(
sorted(
(item for item in policies if item.enabled),
key=lambda item: (-item.priority, item.slug),
)
)
for policy in self.policies:
unknown = set(policy.match) - _MATCH_FIELDS
if unknown:
raise PolicyValidationError(f"unsupported policy match fields: {sorted(unknown)}")
if (
policy.decision
in {
Decision.ALLOW,
Decision.ALLOW_IF_CONDITIONS,
Decision.ALLOW_WITH_APPROVAL,
Decision.ALLOW_WITH_PREVIEW,
}
and policy.project is None
and policy.slug != "global-deny"
):
# Workspace-wide allows must not expand a project scope implicitly.
continue
def evaluate(self, context: InvocationContext) -> DecisionTrace:
steps: list[TraceStep] = []
if not context.binding_present:
return DecisionTrace(
trace_id=context.trace_id,
decision=Decision.DENY,
reason_code="MISSING_BINDING",
steps=(
TraceStep(
phase="identity", matched=False, reason="client/project binding is required"
),
),
)
for rule in self.policies:
matched = _matches(rule, context)
steps.append(
TraceStep(
phase="global-deny" if rule.project is None else "match",
rule=rule.slug,
matched=matched,
reason="selector matched" if matched else "selector did not match",
)
)
if (
matched
and rule.project is None
and rule.decision in {Decision.DENY, Decision.DENY_AND_ALERT}
and rule.terminal
):
return self._result(context, rule.decision, "GLOBAL_DENY", [rule.slug], steps)
for rule in self.policies:
if (
rule.slug == "global-deny"
or rule.decision not in {Decision.DENY, Decision.DENY_AND_ALERT}
or not _matches(rule, context)
):
continue
return self._result(context, rule.decision, "EXPLICIT_DENY", [rule.slug], steps)
for rule in self.policies:
if rule.decision not in {
Decision.ALLOW,
Decision.ALLOW_IF_CONDITIONS,
Decision.ALLOW_WITH_APPROVAL,
Decision.ALLOW_WITH_PREVIEW,
} or not _matches(rule, context):
continue
conditions = _conditions_match(rule, context)
steps.append(
TraceStep(
phase="conditions",
rule=rule.slug,
matched=conditions,
reason="conditions satisfied" if conditions else "conditions not satisfied",
)
)
if conditions:
return self._result(context, rule.decision, "ALLOW_RULE", [rule.slug], steps)
return self._result(context, Decision.DENY, "DEFAULT_DENY", [], steps)
@staticmethod
def _result(
context: InvocationContext,
decision: Decision,
reason: str,
rules: list[str],
steps: list[TraceStep],
) -> DecisionTrace:
return DecisionTrace(
trace_id=context.trace_id,
decision=decision,
reason_code=reason,
matched_rule_ids=tuple(rules),
steps=tuple(steps),
)
class PolicySimulator:
"""Runs the exact engine against a known-entity inventory without side effects."""
def __init__(
self,
engine: PolicyEngine,
*,
known_projects: Iterable[str] = (),
known_clients: Iterable[UUID] = (),
known_tools: Iterable[str] = (),
) -> None:
self.engine = engine
self.known_projects = frozenset(known_projects)
self.known_clients = frozenset(known_clients)
self.known_tools = frozenset(known_tools)
def simulate(self, context: InvocationContext) -> DecisionTrace:
if self.known_projects and context.project not in self.known_projects:
return DecisionTrace(
trace_id=context.trace_id,
decision=Decision.DENY,
reason_code="UNKNOWN_PROJECT",
steps=(
TraceStep(
phase="simulation", matched=False, reason="project is not a known entity"
),
),
)
if self.known_tools and context.tool not in self.known_tools:
return DecisionTrace(
trace_id=context.trace_id,
decision=Decision.DENY,
reason_code="UNKNOWN_TOOL",
steps=(
TraceStep(
phase="simulation", matched=False, reason="tool is not a known entity"
),
),
)
if context.client_id and self.known_clients and context.client_id not in self.known_clients:
return DecisionTrace(
trace_id=context.trace_id,
decision=Decision.DENY,
reason_code="UNKNOWN_CLIENT",
steps=(
TraceStep(
phase="simulation", matched=False, reason="client is not a known entity"
),
),
)
return self.engine.evaluate(context)
+173
View File
@@ -0,0 +1,173 @@
"""Shared decision, rate-limit, and audit lifecycle used at every invocation boundary."""
from __future__ import annotations
import inspect
import time
from collections.abc import Callable, Mapping
from functools import wraps
from typing import Any, TypeVar, cast
from .audit import AuditEvent, AuditLog
from .engine import Decision, DecisionTrace, InvocationContext, PolicyEngine
from .rate_limit import RateLimitExceeded, SlidingWindowRateLimiter
class PolicyDenied(PermissionError):
def __init__(self, trace: DecisionTrace) -> None:
super().__init__(trace.reason_code)
self.trace = trace
self.code = trace.reason_code
T = TypeVar("T", bound=Callable[..., Any])
def governed_tool(tool: str) -> Callable[[T], T]:
"""Decorate direct connector tool methods with the shared governance lifecycle."""
def decorator(method: T) -> T:
@wraps(method)
def wrapped(self: Any, context: Any, *args: Any, **kwargs: Any) -> Any:
governance = getattr(self, "governance", None)
if governance is None or getattr(self, "_governance_dispatch", False):
return method(self, context, *args, **kwargs)
bound = inspect.signature(method).bind(self, context, *args, **kwargs)
arguments = {
key: value
for key, value in bound.arguments.items()
if key not in {"self", "context"}
}
policy_context = governance.context(
workspace=str(context.workspace_id),
project=context.project_slug,
environment=context.environment,
principal=str(context.client_id or "anonymous"),
client_id=context.client_id,
binding_present=context.client_id is not None,
client_enabled=context.client_id is not None,
tool=tool,
connector=getattr(self, "connector_type", "connector"),
arguments=arguments,
)
self._governance_dispatch = True
try:
return governance.invoke(
policy_context, lambda: method(self, context, *args, **kwargs)
)
finally:
self._governance_dispatch = False
return cast(T, wrapped)
return decorator
class Governance:
def __init__(
self,
engine: PolicyEngine,
audit: AuditLog,
*,
rate_limiter: SlidingWindowRateLimiter | None = None,
) -> None:
self.engine = engine
self.audit = audit
self.rate_limiter = rate_limiter
def begin(self, context: InvocationContext) -> tuple[DecisionTrace, float]:
started = time.monotonic()
trace = self.engine.evaluate(context)
if trace.decision in {Decision.DENY, Decision.DENY_AND_ALERT}:
self.audit.append(
AuditEvent.from_context(
context, trace, outcome="denied", started=started, error_code=trace.reason_code
)
)
raise PolicyDenied(trace)
if self.rate_limiter is not None:
try:
self.rate_limiter.check(
client_id=context.client_id or context.principal,
project=context.project,
tool=context.tool,
)
except RateLimitExceeded:
self.audit.append(
AuditEvent.from_context(
context,
trace,
outcome="rate_limited",
started=started,
error_code="RATE_LIMITED",
)
)
raise
return trace, started
def finish(
self,
context: InvocationContext,
trace: DecisionTrace,
started: float,
*,
outcome: str = "success",
error_code: str | None = None,
result_summary: dict[str, Any] | None = None,
) -> None:
self.audit.append(
AuditEvent.from_context(
context, trace, outcome=outcome, started=started, error_code=error_code,
result_summary=result_summary,
)
)
def invoke(self, context: InvocationContext, operation: Callable[[], Any]) -> Any:
trace, started = self.begin(context)
try:
result = operation()
except Exception as exc:
self.finish(
context,
trace,
started,
outcome="error",
error_code=getattr(exc, "code", type(exc).__name__),
)
raise
summary = None
if context.tool == "gitea.pullrequest.merge" and isinstance(result, dict):
data = result.get("data") or {}
if isinstance(data, dict):
summary = data.get("auditEvidence")
self.finish(context, trace, started, result_summary=summary)
return result
@staticmethod
def context(
*,
workspace: str,
project: str,
environment: str,
principal: str,
tool: str,
connector: str,
arguments: Mapping[str, Any],
client_id: Any = None,
binding_present: bool = True,
client_enabled: bool = True,
**kwargs: Any,
) -> InvocationContext:
return InvocationContext(
workspace=workspace,
project=project,
environment=environment,
principal=principal,
tool=tool,
connector=connector,
arguments=dict(arguments),
client_id=client_id,
binding_present=binding_present,
client_enabled=client_enabled,
**kwargs,
)
+45
View File
@@ -0,0 +1,45 @@
"""In-process bounded sliding-window rate limiter with isolated compound keys."""
from __future__ import annotations
import time
from collections import defaultdict, deque
from threading import Lock
from uuid import UUID
class RateLimitExceeded(RuntimeError):
code = "RATE_LIMITED"
class SlidingWindowRateLimiter:
def __init__(
self, *, limit: int = 60, window_seconds: float = 60.0, clock: object = time.monotonic
) -> None:
if limit < 1 or window_seconds <= 0:
raise ValueError("rate limit must be positive")
self.limit = limit
self.window_seconds = window_seconds
self.clock = clock
self._entries: dict[tuple[str, str, str], deque[float]] = defaultdict(deque)
self._lock = Lock()
def allow(self, *, client_id: UUID | str, project: str, tool: str) -> bool:
key = (str(client_id), project, tool)
now = self.clock() # type: ignore[operator]
with self._lock:
bucket = self._entries[key]
while bucket and bucket[0] <= now - self.window_seconds:
bucket.popleft()
if len(bucket) >= self.limit:
return False
bucket.append(now)
return True
def check(self, *, client_id: UUID | str, project: str, tool: str) -> None:
if not self.allow(client_id=client_id, project=project, tool=tool):
raise RateLimitExceeded("invocation rate limit exceeded")
def clear(self) -> None:
with self._lock:
self._entries.clear()
+17
View File
@@ -0,0 +1,17 @@
"""Production runtime configuration primitives."""
from .secret_files import (
SecretFileError,
read_secret_file,
validate_required_secret_paths,
validate_secret_file,
)
from .service_tokens import issue_broker_service_token
__all__ = [
"SecretFileError",
"issue_broker_service_token",
"read_secret_file",
"validate_required_secret_paths",
"validate_secret_file",
]
+94
View File
@@ -0,0 +1,94 @@
"""Fail-closed secret-file access without value projection or logging."""
from __future__ import annotations
import os
import stat
from pathlib import Path
class SecretFileError(RuntimeError):
"""Stable non-secret error raised for an invalid runtime secret file."""
def validate_secret_file(
path: Path,
*,
root: Path = Path("/run/secrets"),
expected_uid: int | None = None,
expected_gid: int | None = None,
max_bytes: int = 8192,
) -> bytes:
"""Return bounded bytes only after containment, mode, owner, and content checks."""
try:
resolved_root = root.resolve(strict=True)
resolved = path.resolve(strict=True)
resolved.relative_to(resolved_root)
metadata = resolved.stat()
except (OSError, ValueError) as exc:
raise SecretFileError("required secret file is unavailable") from exc
if not stat.S_ISREG(metadata.st_mode):
raise SecretFileError("required secret path is not a regular file")
mode = stat.S_IMODE(metadata.st_mode)
if os.name != "nt" and (mode & 0o077 or mode & 0o111 or not mode & 0o400):
raise SecretFileError("required secret file permissions are unsafe")
if expected_uid is not None and metadata.st_uid != expected_uid:
raise SecretFileError("required secret file owner is invalid")
if expected_gid is not None and metadata.st_gid != expected_gid:
raise SecretFileError("required secret file group is invalid")
try:
payload = resolved.read_bytes()
except OSError as exc:
raise SecretFileError("required secret file is unreadable") from exc
payload = payload.rstrip(b"\r\n")
if (
not payload
or len(payload) > max_bytes
or b"\x00" in payload
or b"\r" in payload.rstrip(b"\r\n")
or b"\n" in payload.rstrip(b"\r\n")
):
raise SecretFileError("required secret file content is invalid")
return payload
def read_secret_file(
path: Path,
*,
root: Path = Path("/run/secrets"),
expected_uid: int | None = None,
expected_gid: int | None = None,
max_bytes: int = 8192,
) -> str:
"""Read a UTF-8 secret at its internal consumption boundary."""
payload = validate_secret_file(
path,
root=root,
expected_uid=expected_uid,
expected_gid=expected_gid,
max_bytes=max_bytes,
)
try:
return payload.decode("utf-8")
except UnicodeDecodeError as exc:
raise SecretFileError("required secret file encoding is invalid") from exc
def validate_required_secret_paths(raw: str) -> None:
"""Validate a comma-separated production startup contract without returning values."""
if not raw.strip():
return
expected_uid = (
int(os.environ["SECRET_EXPECTED_UID"]) if os.getenv("SECRET_EXPECTED_UID") else None
)
expected_gid = (
int(os.environ["SECRET_EXPECTED_GID"]) if os.getenv("SECRET_EXPECTED_GID") else None
)
for value in raw.split(","):
path = Path(value.strip())
if not path.is_absolute():
raise SecretFileError("required secret path must be absolute")
validate_secret_file(path, expected_uid=expected_uid, expected_gid=expected_gid)
+39
View File
@@ -0,0 +1,39 @@
"""Short-lived internal service tokens shared by trusted runtime components."""
from __future__ import annotations
import base64
import hashlib
import hmac
import json
import time
def issue_broker_service_token(
secret: str,
project: str,
environment: str,
*,
ttl_seconds: int = 60,
) -> str:
"""Issue a bounded Docker broker token for one validated project context."""
payload = {
"aud": "docker-read-broker",
"project": project,
"environment": environment,
"exp": int(time.time()) + ttl_seconds,
}
encoded = (
base64.urlsafe_b64encode(json.dumps(payload, separators=(",", ":")).encode())
.decode()
.rstrip("=")
)
signature = (
base64.urlsafe_b64encode(
hmac.new(secret.encode(), encoded.encode(), hashlib.sha256).digest()
)
.decode()
.rstrip("=")
)
return f"{encoded}.{signature}"
+103
View File
@@ -0,0 +1,103 @@
"""Process-local MCP runtime supervision and recovery telemetry."""
from __future__ import annotations
import hashlib
import os
import socket
import time
from datetime import UTC, datetime
from threading import Lock
from typing import Any
from mcp.server.context import CallNext, ServerRequestContext
from mcp.shared.exceptions import MCPError
class RuntimeSupervisor:
"""Track transport health without turning tool failures into process failures."""
def __init__(self) -> None:
self._started_monotonic = time.monotonic()
self._started_at = datetime.now(UTC).isoformat()
material = f"{socket.gethostname()}:{os.getpid()}:{self._started_at}".encode()
self.worker_id = hashlib.sha256(material).hexdigest()[:16]
self.generation = os.environ.get(
"HUB_RUNTIME_GENERATION",
os.environ.get("GITEA_CONNECTOR_BUILD_SHA", "development"),
)[:64]
self.restart_count = max(int(os.environ.get("HUB_RUNTIME_RESTART_COUNT", "0")), 0)
self._lock = Lock()
self._in_flight = 0
self._requests = 0
self._failures = 0
self._last_error_code: str | None = None
self._last_fatal_error_code: str | None = None
self._circuit_state = "CLOSED"
self._recovery_count = 0
def started(self) -> None:
with self._lock:
self._in_flight += 1
self._requests += 1
def completed(self) -> None:
with self._lock:
self._in_flight = max(self._in_flight - 1, 0)
if self._circuit_state == "DEGRADED":
self._circuit_state = "CLOSED"
self._recovery_count += 1
def failed(self, code: str, *, fatal: bool) -> None:
with self._lock:
self._in_flight = max(self._in_flight - 1, 0)
self._failures += 1
self._last_error_code = code[:128]
if fatal:
self._last_fatal_error_code = code[:128]
self._circuit_state = "DEGRADED"
def snapshot(self) -> dict[str, Any]:
with self._lock:
return {
"workerId": self.worker_id,
"sessionId": None,
"sessionMode": "stateless-resumable-http",
"resumable": True,
"startedAt": self._started_at,
"uptimeSeconds": max(int(time.monotonic() - self._started_monotonic), 0),
"restartCount": self.restart_count,
"generation": self.generation,
"circuitState": self._circuit_state,
"recoveryCount": self._recovery_count,
"requests": self._requests,
"inFlight": self._in_flight,
"failures": self._failures,
"lastErrorCode": self._last_error_code,
"lastFatalErrorCode": self._last_fatal_error_code,
}
SUPERVISOR = RuntimeSupervisor()
class SupervisorMiddleware:
"""Keep per-operation errors observable while preserving the next request."""
async def __call__(self, ctx: ServerRequestContext[Any, Any], call_next: CallNext) -> Any:
SUPERVISOR.started()
try:
result = await call_next(ctx)
except MCPError as exc:
data = exc.data if isinstance(exc.data, dict) else {}
SUPERVISOR.failed(str(data.get("code", "MCP_ERROR")), fatal=False)
raise
except Exception as exc:
SUPERVISOR.failed(type(exc).__name__, fatal=True)
raise
SUPERVISOR.completed()
return result
def runtime_snapshot() -> dict[str, Any]:
return SUPERVISOR.snapshot()
+6
View File
@@ -0,0 +1,6 @@
{
"name": "@itworx-mcp-hub/ui",
"version": "0.1.0",
"private": true,
"description": "Reserved shared UI workspace; implementation begins in WP-120."
}