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

@@ -0,0 +1,6 @@
"""Hub-owned persistence boundary."""
from .dataplane_source import SqlAlchemyAliasSource
from .db import Base, create_engine_from_url, session_factory
__all__ = ["Base", "SqlAlchemyAliasSource", "create_engine_from_url", "session_factory"]
@@ -0,0 +1,133 @@
"""Read-only alias projection from Hub-owned persistence."""
from __future__ import annotations
import re
from collections import defaultdict
from typing import Any, cast
from urllib.parse import urlsplit
from pydantic import AnyHttpUrl, TypeAdapter
from sqlalchemy import select
from sqlalchemy.orm import Session
from packages.hub_contracts.dataplane import (
SUPPORTED_PROTOCOLS,
AliasRecord,
ClientBinding,
EnvironmentName,
)
from .models import (
ClientBindingRow,
ClientRow,
EnvironmentRow,
ProjectRow,
VirtualServerRow,
)
class SqlAlchemyAliasSource:
"""Projects only enabled Hub virtual servers and active explicit bindings."""
def __init__(self, session: Session, *, workspace_id: Any, gateway_base_url: str) -> None:
self.session = session
self.workspace_id = workspace_id
base = TypeAdapter(AnyHttpUrl).validate_python(gateway_base_url)
internal_contextforge = (
base.scheme == "http"
and base.host in {"contextforge", "contextforge-control"}
and base.port == 4444
and (base.path or "").rstrip("/") == "/mcp"
)
if (
base.username is not None
or base.password is not None
or (base.scheme != "https" and not internal_contextforge)
):
raise ValueError("gateway URL must be HTTPS or the fixed internal gateway")
self.gateway_base_url = str(base).rstrip("/")
self.internal_contextforge = internal_contextforge
def aliases(self) -> tuple[AliasRecord, ...]:
projects = {
row.id: row
for row in self.session.scalars(
select(ProjectRow).where(ProjectRow.workspace_id == self.workspace_id)
).all()
}
environments = {
row.id: row
for row in self.session.scalars(
select(EnvironmentRow).where(EnvironmentRow.workspace_id == self.workspace_id)
).all()
}
clients = {
row.id: row
for row in self.session.scalars(
select(ClientRow).where(ClientRow.workspace_id == self.workspace_id)
).all()
}
bindings_by_server: dict[Any, list[ClientBinding]] = defaultdict(list)
for row in self.session.scalars(
select(ClientBindingRow).where(
ClientBindingRow.workspace_id == self.workspace_id,
ClientBindingRow.enabled.is_(True),
)
).all():
client = clients.get(row.client_id)
if client is None or client.status != "active":
continue
bindings_by_server[row.virtual_server_id].append(
ClientBinding(
client_id=client.id,
issuer=client.issuer,
subject=client.subject,
enabled=True,
)
)
values: list[AliasRecord] = []
for server in self.session.scalars(
select(VirtualServerRow).where(
VirtualServerRow.workspace_id == self.workspace_id,
VirtualServerRow.enabled.is_(True),
VirtualServerRow.health_state.in_(("healthy", "degraded")),
)
).all():
if self.internal_contextforge and not server.gateway_external_id:
continue
project = projects.get(server.project_id)
environment = environments.get(server.environment_id)
if project is None or environment is None:
continue
if environment.name not in {"dev", "test", "staging", "prod"}:
continue
metadata = server.record_metadata
endpoint_alias = metadata.get("endpointAlias") if isinstance(metadata, dict) else None
if not isinstance(endpoint_alias, str):
endpoint_alias = f"/mcp/{project.slug}/{environment.name}"
try:
if self.internal_contextforge:
external_id = server.gateway_external_id or ""
if not re.fullmatch(r"[0-9a-f]{32}", external_id):
raise ValueError("invalid ContextForge server route identifier")
parsed = urlsplit(self.gateway_base_url)
upstream = f"{parsed.scheme}://{parsed.netloc}/servers/{external_id}/mcp"
else:
upstream = f"{self.gateway_base_url}{endpoint_alias}"
values.append(
AliasRecord(
project_slug=project.slug,
workspace_id=self.workspace_id,
project_id=project.id,
environment=cast(EnvironmentName, environment.name),
alias=endpoint_alias,
upstream_url=TypeAdapter(AnyHttpUrl).validate_python(upstream),
enabled=True,
protocol_revisions=tuple(SUPPORTED_PROTOCOLS),
bindings=tuple(bindings_by_server.get(server.id, ())),
)
)
except ValueError:
continue
return tuple(sorted(values, key=lambda item: (item.project_slug, item.environment)))
+40
View File
@@ -0,0 +1,40 @@
"""SQLAlchemy engine and session construction for the Hub database only."""
from __future__ import annotations
from collections.abc import Callable
from datetime import UTC, datetime
from typing import Any
from sqlalchemy import Engine, create_engine, event
from sqlalchemy.orm import DeclarativeBase, Session, sessionmaker
class Base(DeclarativeBase):
"""Declarative root for Hub-owned tables; ContextForge is never imported."""
def utc_now() -> datetime:
return datetime.now(UTC)
def create_engine_from_url(database_url: str, **kwargs: Any) -> Engine:
"""Create an engine with safe SQLite test defaults and explicit pool settings."""
options: dict[str, Any] = {"future": True, "pool_pre_ping": True, **kwargs}
if database_url.startswith("sqlite"):
options.setdefault("connect_args", {"check_same_thread": False})
engine = create_engine(database_url, **options)
if database_url.startswith("sqlite"):
def enable_sqlite_foreign_keys(dbapi_connection: Any, _: Any) -> None:
dbapi_connection.execute("PRAGMA foreign_keys=ON")
event.listen(engine, "connect", enable_sqlite_foreign_keys)
return engine
def session_factory(engine: Engine) -> Callable[[], Session]:
"""Return an explicit session factory; no hidden global session is used."""
return sessionmaker(bind=engine, autoflush=False, expire_on_commit=False, class_=Session)
@@ -0,0 +1,118 @@
"""One-time, idempotent provisioning of the Fleet Ops demo client identity.
Docs: docs/superpowers/specs/2026-08-05-mobilityops-demo-token-design.md
"""
from __future__ import annotations
from dataclasses import dataclass
from uuid import NAMESPACE_URL, UUID, uuid5
from sqlalchemy import select
from sqlalchemy.orm import Session
from .models import (
ClientBindingRow,
ClientRow,
EnvironmentRow,
ProjectRow,
VirtualServerRow,
WorkspaceRow,
)
DEMO_CLIENT_ISSUER = "itworx-mcp-hub:demo-token"
DEMO_CLIENT_SUBJECT = "mobilityops-demo"
DEMO_CLIENT_ID: UUID = uuid5(NAMESPACE_URL, "https://hub.example.com/demo-client/mobilityops")
@dataclass(frozen=True)
class DemoClientResult:
client_id: UUID
binding_id: UUID
def provision_demo_client(
session: Session,
*,
workspace_slug: str = "itworx",
project_slug: str = "mobilityops",
environment_name: str = "prod",
) -> DemoClientResult:
workspace = session.scalar(select(WorkspaceRow).where(WorkspaceRow.slug == workspace_slug))
if workspace is None:
raise ValueError("demo client workspace is not registered")
project = session.scalar(
select(ProjectRow).where(
ProjectRow.workspace_id == workspace.id,
ProjectRow.slug == project_slug,
ProjectRow.status == "active",
)
)
if project is None:
raise ValueError("demo client project is not registered")
environment = session.scalar(
select(EnvironmentRow).where(
EnvironmentRow.workspace_id == workspace.id,
EnvironmentRow.project_id == project.id,
EnvironmentRow.name == environment_name,
EnvironmentRow.status == "active",
)
)
if environment is None:
raise ValueError("demo client environment is not registered")
# The demo client must bind to exactly one, unambiguous, externally-routed
# virtual server. Taking "the first match" would silently bind the demo
# identity to an arbitrary server if the project/environment ever grows a
# second one.
servers = session.scalars(
select(VirtualServerRow).where(
VirtualServerRow.workspace_id == workspace.id,
VirtualServerRow.project_id == project.id,
VirtualServerRow.environment_id == environment.id,
VirtualServerRow.enabled.is_(True),
VirtualServerRow.gateway_external_id.is_not(None),
)
).all()
if len(servers) != 1:
raise ValueError(
f"demo client virtual server is not uniquely determined "
f"({len(servers)} enabled, externally-routed servers found for "
f"{project_slug}/{environment_name}; expected exactly 1)"
)
server = servers[0]
client = session.get(ClientRow, DEMO_CLIENT_ID)
if client is None:
client = ClientRow(
id=DEMO_CLIENT_ID,
workspace_id=workspace.id,
subject=DEMO_CLIENT_SUBJECT,
issuer=DEMO_CLIENT_ISSUER,
client_type="mcp",
status="active",
record_metadata={"managedBy": "demo-token-provisioning"},
)
session.add(client)
session.flush()
binding = session.scalar(
select(ClientBindingRow).where(
ClientBindingRow.client_id == DEMO_CLIENT_ID,
ClientBindingRow.virtual_server_id == server.id,
)
)
if binding is None:
binding = ClientBindingRow(
workspace_id=workspace.id,
project_id=project.id,
client_id=DEMO_CLIENT_ID,
virtual_server_id=server.id,
enabled=True,
)
session.add(binding)
session.flush()
elif not binding.enabled:
binding.enabled = True
session.flush()
return DemoClientResult(client_id=client.id, binding_id=binding.id)
@@ -0,0 +1,169 @@
"""Idempotent provisioning for real Authentik-backed MCP machine clients."""
from __future__ import annotations
from dataclasses import dataclass
from uuid import NAMESPACE_URL, UUID, uuid5
from sqlalchemy import select
from sqlalchemy.orm import Session
from .models import (
ClientBindingRow,
ClientRow,
EnvironmentRow,
ProjectRow,
VirtualServerRow,
WorkspaceRow,
)
@dataclass(frozen=True)
class MachineClientResult:
client_id: UUID
binding_id: UUID
endpoint: str
def provision_machine_client(
session: Session,
*,
issuer: str,
subject: str,
oauth_username: str,
name: str,
project_slug: str,
environment_name: str = "prod",
workspace_slug: str = "itworx",
public_host: str = "mcp.example.com",
client_id: UUID | None = None,
) -> MachineClientResult:
"""Bind one validated Authentik subject to exactly one external virtual server.
The caller obtains ``subject`` from a validated Authentik token or the exact
Authentik user UUID. Credentials are deliberately not accepted here.
"""
issuer = issuer.strip()
subject = subject.strip()
oauth_username = oauth_username.strip()
name = name.strip()
if not issuer.startswith("https://") or not subject or not oauth_username or not name:
raise ValueError("machine client identity is incomplete")
workspace = session.scalar(select(WorkspaceRow).where(WorkspaceRow.slug == workspace_slug))
if workspace is None:
raise ValueError("machine client workspace is not registered")
project = session.scalar(
select(ProjectRow).where(
ProjectRow.workspace_id == workspace.id,
ProjectRow.slug == project_slug,
ProjectRow.status == "active",
)
)
if project is None:
raise ValueError("machine client project is not registered")
environment = session.scalar(
select(EnvironmentRow).where(
EnvironmentRow.workspace_id == workspace.id,
EnvironmentRow.project_id == project.id,
EnvironmentRow.name == environment_name,
EnvironmentRow.status == "active",
)
)
if environment is None:
raise ValueError("machine client environment is not registered")
servers = session.scalars(
select(VirtualServerRow).where(
VirtualServerRow.workspace_id == workspace.id,
VirtualServerRow.project_id == project.id,
VirtualServerRow.environment_id == environment.id,
VirtualServerRow.enabled.is_(True),
VirtualServerRow.gateway_external_id.is_not(None),
)
).all()
if len(servers) != 1:
raise ValueError(
"machine client virtual server is not uniquely determined "
f"({len(servers)} externally-routed servers found)"
)
server = servers[0]
deterministic_id = uuid5(NAMESPACE_URL, f"{issuer}\n{subject}")
principal = session.scalar(
select(ClientRow).where(
ClientRow.workspace_id == workspace.id,
ClientRow.issuer == issuer,
ClientRow.subject == subject,
)
)
selected_id = client_id or deterministic_id
by_id = session.get(ClientRow, selected_id)
if principal is not None and principal.id != selected_id:
raise ValueError("machine client principal is already managed by another identity")
if (
client_id is None
and by_id is not None
and (by_id.issuer != issuer or by_id.subject != subject)
):
raise ValueError("machine client deterministic identity collision")
if client_id is not None and by_id is not None:
existing_metadata = by_id.record_metadata if isinstance(by_id.record_metadata, dict) else {}
if (
existing_metadata.get("managedBy") != "runtime-onboarding"
or existing_metadata.get("project") != project_slug
or existing_metadata.get("environment") != environment_name
or by_id.status != "pending"
):
raise ValueError("existing Hub client is not an activatable pending identity")
client = principal or by_id
metadata = {
"managedBy": "authentik-machine-provisioning",
"identityProvisioned": True,
"name": name,
"project": project_slug,
"environment": environment_name,
"kind": "stdio-bridge",
"oauthUsername": oauth_username,
"credentialReference": f"authentik://service-account/{oauth_username}",
"operatorDisabled": False,
}
if client is None:
client = ClientRow(
id=selected_id,
workspace_id=workspace.id,
subject=subject,
issuer=issuer,
client_type="mcp",
status="active",
record_metadata=metadata,
)
session.add(client)
session.flush()
else:
if client.status == "revoked":
raise ValueError("revoked machine client cannot be reprovisioned")
client.issuer = issuer
client.subject = subject
client.status = "active"
client.record_metadata = metadata
session.flush()
bindings = session.scalars(
select(ClientBindingRow).where(ClientBindingRow.client_id == client.id)
).all()
foreign = [item for item in bindings if item.virtual_server_id != server.id]
if foreign:
raise ValueError("machine client already has a different project binding")
binding = next(iter(bindings), None)
if binding is None:
binding = ClientBindingRow(
workspace_id=workspace.id,
project_id=project.id,
client_id=client.id,
virtual_server_id=server.id,
enabled=True,
)
session.add(binding)
else:
binding.enabled = True
session.flush()
endpoint = f"https://{public_host}/mcp/{project_slug}/{environment_name}"
return MachineClientResult(client_id=client.id, binding_id=binding.id, endpoint=endpoint)
+550
View File
@@ -0,0 +1,550 @@
"""Hub-owned SQLAlchemy rows and constraints.
The model intentionally does not contain gateway tables or foreign keys to the
ContextForge database. Cross-system identity is represented only by opaque
external IDs and is reconciled by a later adapter.
"""
from __future__ import annotations
from datetime import datetime
from typing import Any
from uuid import UUID, uuid4
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
from sqlalchemy.orm import Mapped, mapped_column
from .db import Base, utc_now
JSON_TYPE = sa.JSON().with_variant(postgresql.JSONB(), "postgresql")
UUID_TYPE = sa.Uuid(as_uuid=True)
class TimestampMixin:
created_at: Mapped[datetime] = mapped_column(
sa.DateTime(timezone=True), default=utc_now, nullable=False
)
updated_at: Mapped[datetime] = mapped_column(
sa.DateTime(timezone=True), default=utc_now, onupdate=utc_now, nullable=False
)
revision: Mapped[int] = mapped_column(sa.Integer, default=1, nullable=False)
class WorkspaceRow(TimestampMixin, Base):
__tablename__ = "workspaces"
__table_args__ = (sa.UniqueConstraint("slug", name="uq_workspaces_slug"),)
id: Mapped[UUID] = mapped_column(UUID_TYPE, primary_key=True, default=uuid4)
slug: Mapped[str] = mapped_column(sa.String(63), nullable=False)
name: Mapped[str] = mapped_column(sa.String(160), nullable=False)
status: Mapped[str] = mapped_column(sa.String(32), nullable=False, default="active")
class ProjectRow(TimestampMixin, Base):
__tablename__ = "projects"
__table_args__ = (
sa.UniqueConstraint("workspace_id", "slug", name="uq_projects_workspace_slug"),
sa.UniqueConstraint("workspace_id", "id", name="uq_projects_workspace_id"),
)
id: Mapped[UUID] = mapped_column(UUID_TYPE, primary_key=True, default=uuid4)
workspace_id: Mapped[UUID] = mapped_column(sa.ForeignKey("workspaces.id"), nullable=False)
slug: Mapped[str] = mapped_column(sa.String(63), nullable=False)
name: Mapped[str] = mapped_column(sa.String(160), nullable=False)
description: Mapped[str | None] = mapped_column(sa.String(1000))
status: Mapped[str] = mapped_column(sa.String(32), nullable=False, default="active")
repository_provider: Mapped[str | None] = mapped_column(sa.String(32))
repository_owner: Mapped[str | None] = mapped_column(sa.String(120))
repository_name: Mapped[str | None] = mapped_column(sa.String(120))
classification: Mapped[str] = mapped_column(sa.String(32), nullable=False, default="internal")
owner_metadata: Mapped[dict[str, Any]] = mapped_column(JSON_TYPE, default=dict, nullable=False)
class EnvironmentRow(TimestampMixin, Base):
__tablename__ = "environments"
__table_args__ = (
sa.ForeignKeyConstraint(
["workspace_id", "project_id"],
["projects.workspace_id", "projects.id"],
name="fk_environments_project_scope",
),
sa.UniqueConstraint("workspace_id", "project_id", "id", name="uq_environments_scope_id"),
sa.UniqueConstraint("project_id", "name", name="uq_environments_project_name"),
)
id: Mapped[UUID] = mapped_column(UUID_TYPE, primary_key=True, default=uuid4)
workspace_id: Mapped[UUID] = mapped_column(nullable=False)
project_id: Mapped[UUID] = mapped_column(nullable=False)
name: Mapped[str] = mapped_column(sa.String(32), nullable=False)
classification: Mapped[str] = mapped_column(sa.String(32), nullable=False)
base_url: Mapped[str | None] = mapped_column(sa.String(2048))
status: Mapped[str] = mapped_column(sa.String(32), nullable=False, default="active")
class ConnectorRow(TimestampMixin, Base):
__tablename__ = "connectors"
__table_args__ = (
sa.ForeignKeyConstraint(
["workspace_id", "project_id"],
["projects.workspace_id", "projects.id"],
name="fk_connectors_project_scope",
),
sa.ForeignKeyConstraint(
["workspace_id", "project_id", "environment_id"],
["environments.workspace_id", "environments.project_id", "environments.id"],
name="fk_connectors_environment_scope",
),
sa.UniqueConstraint(
"workspace_id",
"project_id",
"environment_scope",
"slug",
name="uq_connectors_scope_slug",
),
sa.CheckConstraint(
"health_state in ("
"'unconfigured','checking','healthy','degraded','unavailable','misconfigured','disabled')",
name="ck_connectors_health_state",
),
)
id: Mapped[UUID] = mapped_column(UUID_TYPE, primary_key=True, default=uuid4)
workspace_id: Mapped[UUID] = mapped_column(nullable=False)
project_id: Mapped[UUID] = mapped_column(nullable=False)
environment_id: Mapped[UUID | None] = mapped_column()
environment_scope: Mapped[str] = mapped_column(
sa.String(36), nullable=False, default="__project__"
)
type: Mapped[str] = mapped_column(sa.String(64), nullable=False)
slug: Mapped[str] = mapped_column(sa.String(63), nullable=False)
endpoint: Mapped[str | None] = mapped_column(sa.String(2048))
credential_reference: Mapped[str | None] = mapped_column(sa.String(512))
configuration: Mapped[dict[str, Any]] = mapped_column(JSON_TYPE, default=dict, nullable=False)
enabled: Mapped[bool] = mapped_column(sa.Boolean, nullable=False, default=False)
health_state: Mapped[str] = mapped_column(sa.String(32), nullable=False, default="unconfigured")
last_check_at: Mapped[datetime | None] = mapped_column(sa.DateTime(timezone=True))
latency_ms: Mapped[int | None] = mapped_column(sa.Integer)
stable_error_code: Mapped[str | None] = mapped_column(sa.String(128))
sanitized_error: Mapped[str | None] = mapped_column(sa.String(1000))
class RiskClassRow(Base):
__tablename__ = "risk_classes"
code: Mapped[str] = mapped_column(sa.String(64), primary_key=True)
description: Mapped[str] = mapped_column(sa.String(500), nullable=False)
allows_writes: Mapped[bool] = mapped_column(sa.Boolean, nullable=False, default=False)
max_output_bytes: Mapped[int] = mapped_column(sa.Integer, nullable=False, default=65536)
class CapabilityRow(TimestampMixin, Base):
__tablename__ = "capabilities"
__table_args__ = (
sa.ForeignKeyConstraint(
["risk_class"], ["risk_classes.code"], name="fk_capabilities_risk_class"
),
sa.UniqueConstraint("canonical_name", "version", name="uq_capabilities_name_version"),
)
id: Mapped[UUID] = mapped_column(UUID_TYPE, primary_key=True, default=uuid4)
canonical_name: Mapped[str] = mapped_column(sa.String(120), nullable=False)
version: Mapped[str] = mapped_column(sa.String(32), nullable=False)
description: Mapped[str] = mapped_column(sa.String(1000), nullable=False)
risk_class: Mapped[str] = mapped_column(sa.String(64), nullable=False)
enabled: Mapped[bool] = mapped_column(sa.Boolean, nullable=False, default=True)
class ToolDefinitionRow(TimestampMixin, Base):
__tablename__ = "tool_definitions"
__table_args__ = (
sa.ForeignKeyConstraint(
["workspace_id", "project_id"],
["projects.workspace_id", "projects.id"],
name="fk_tools_project_scope",
),
sa.ForeignKeyConstraint(["connector_id"], ["connectors.id"], name="fk_tools_connector"),
sa.UniqueConstraint("canonical_name", "version", name="uq_tools_name_version"),
)
id: Mapped[UUID] = mapped_column(UUID_TYPE, primary_key=True, default=uuid4)
workspace_id: Mapped[UUID] = mapped_column(nullable=False)
project_id: Mapped[UUID] = mapped_column(nullable=False)
connector_id: Mapped[UUID] = mapped_column(nullable=False)
canonical_name: Mapped[str] = mapped_column(sa.String(120), nullable=False)
version: Mapped[str] = mapped_column(sa.String(32), nullable=False)
input_schema: Mapped[dict[str, Any]] = mapped_column(JSON_TYPE, default=dict, nullable=False)
output_schema: Mapped[dict[str, Any]] = mapped_column(JSON_TYPE, default=dict, nullable=False)
timeout_ms: Mapped[int] = mapped_column(sa.Integer, nullable=False, default=10000)
output_limit_bytes: Mapped[int] = mapped_column(sa.Integer, nullable=False, default=65536)
rate_limit_per_minute: Mapped[int] = mapped_column(sa.Integer, nullable=False, default=60)
redaction_profile: Mapped[str] = mapped_column(sa.String(64), nullable=False, default="default")
lifecycle_status: Mapped[str] = mapped_column(sa.String(32), nullable=False, default="active")
class VirtualServerRow(TimestampMixin, Base):
__tablename__ = "virtual_servers"
__table_args__ = (
sa.ForeignKeyConstraint(
["workspace_id", "project_id"],
["projects.workspace_id", "projects.id"],
name="fk_virtual_servers_project_scope",
),
sa.ForeignKeyConstraint(
["workspace_id", "project_id", "environment_id"],
["environments.workspace_id", "environments.project_id", "environments.id"],
name="fk_virtual_servers_environment_scope",
),
sa.UniqueConstraint("workspace_id", "project_id", "id", name="uq_virtual_servers_scope_id"),
sa.UniqueConstraint("slug", name="uq_virtual_servers_slug"),
)
id: Mapped[UUID] = mapped_column(UUID_TYPE, primary_key=True, default=uuid4)
workspace_id: Mapped[UUID] = mapped_column(nullable=False)
project_id: Mapped[UUID] = mapped_column(nullable=False)
environment_id: Mapped[UUID] = mapped_column(nullable=False)
slug: Mapped[str] = mapped_column(sa.String(63), nullable=False)
gateway_external_id: Mapped[str | None] = mapped_column(sa.String(256))
desired_hash: Mapped[str] = mapped_column(sa.String(128), nullable=False)
enabled: Mapped[bool] = mapped_column(sa.Boolean, nullable=False, default=False)
protocol: Mapped[str] = mapped_column(sa.String(64), nullable=False, default="streamable-http")
health_state: Mapped[str] = mapped_column(sa.String(32), nullable=False, default="unconfigured")
record_metadata: Mapped[dict[str, Any]] = mapped_column(JSON_TYPE, default=dict, nullable=False)
class VirtualServerCapabilityRow(Base):
__tablename__ = "virtual_server_capabilities"
__table_args__ = (
sa.ForeignKeyConstraint(
["workspace_id", "project_id", "virtual_server_id"],
["virtual_servers.workspace_id", "virtual_servers.project_id", "virtual_servers.id"],
name="fk_vs_capabilities_server_scope",
),
sa.ForeignKeyConstraint(
["capability_id"], ["capabilities.id"], name="fk_vs_capabilities_capability"
),
sa.UniqueConstraint(
"virtual_server_id", "capability_id", name="uq_vs_capabilities_binding"
),
)
id: Mapped[UUID] = mapped_column(UUID_TYPE, primary_key=True, default=uuid4)
workspace_id: Mapped[UUID] = mapped_column(nullable=False)
project_id: Mapped[UUID] = mapped_column(nullable=False)
virtual_server_id: Mapped[UUID] = mapped_column(nullable=False)
capability_id: Mapped[UUID] = mapped_column(nullable=False)
desired_state_source: Mapped[str] = mapped_column(
sa.String(64), nullable=False, default="catalog"
)
created_at: Mapped[datetime] = mapped_column(
sa.DateTime(timezone=True), default=utc_now, nullable=False
)
updated_at: Mapped[datetime] = mapped_column(
sa.DateTime(timezone=True), default=utc_now, nullable=False
)
class ClientRow(TimestampMixin, Base):
__tablename__ = "clients"
__table_args__ = (
sa.ForeignKeyConstraint(["workspace_id"], ["workspaces.id"], name="fk_clients_workspace"),
sa.UniqueConstraint("workspace_id", "id", name="uq_clients_workspace_id"),
sa.UniqueConstraint("workspace_id", "issuer", "subject", name="uq_clients_principal"),
sa.CheckConstraint(
"status in ('pending','active','disabled','revoked')", name="ck_clients_status"
),
)
id: Mapped[UUID] = mapped_column(UUID_TYPE, primary_key=True, default=uuid4)
workspace_id: Mapped[UUID] = mapped_column(nullable=False)
subject: Mapped[str] = mapped_column(sa.String(256), nullable=False)
issuer: Mapped[str] = mapped_column(sa.String(512), nullable=False)
client_type: Mapped[str] = mapped_column(sa.String(32), nullable=False, default="mcp")
status: Mapped[str] = mapped_column(sa.String(32), nullable=False, default="pending")
revoked_at: Mapped[datetime | None] = mapped_column(sa.DateTime(timezone=True))
last_seen_at: Mapped[datetime | None] = mapped_column(sa.DateTime(timezone=True))
record_metadata: Mapped[dict[str, Any]] = mapped_column(JSON_TYPE, default=dict, nullable=False)
class ClientBindingRow(TimestampMixin, Base):
__tablename__ = "client_bindings"
__table_args__ = (
sa.ForeignKeyConstraint(
["workspace_id", "project_id"],
["projects.workspace_id", "projects.id"],
name="fk_client_bindings_project_scope",
),
sa.ForeignKeyConstraint(
["workspace_id", "client_id"],
["clients.workspace_id", "clients.id"],
name="fk_client_bindings_client_scope",
),
sa.ForeignKeyConstraint(
["workspace_id", "project_id", "virtual_server_id"],
["virtual_servers.workspace_id", "virtual_servers.project_id", "virtual_servers.id"],
name="fk_client_bindings_server_scope",
),
sa.UniqueConstraint(
"client_id", "virtual_server_id", "capability_pack", name="uq_client_bindings"
),
)
id: Mapped[UUID] = mapped_column(UUID_TYPE, primary_key=True, default=uuid4)
workspace_id: Mapped[UUID] = mapped_column(nullable=False)
project_id: Mapped[UUID] = mapped_column(nullable=False)
client_id: Mapped[UUID] = mapped_column(nullable=False)
virtual_server_id: Mapped[UUID] = mapped_column(nullable=False)
capability_pack: Mapped[str | None] = mapped_column(sa.String(128))
constraints: Mapped[dict[str, Any]] = mapped_column(JSON_TYPE, default=dict, nullable=False)
enabled: Mapped[bool] = mapped_column(sa.Boolean, nullable=False, default=False)
class PolicyRow(TimestampMixin, Base):
__tablename__ = "policies"
__table_args__ = (
sa.ForeignKeyConstraint(["workspace_id"], ["workspaces.id"], name="fk_policies_workspace"),
sa.ForeignKeyConstraint(
["workspace_id", "project_id"],
["projects.workspace_id", "projects.id"],
name="fk_policies_project_scope",
),
sa.UniqueConstraint("workspace_id", "slug", name="uq_policies_workspace_slug"),
sa.CheckConstraint(
"decision in ('allow','allow_if_conditions','deny','deny_and_alert','enforce')",
name="ck_policies_decision",
),
)
id: Mapped[UUID] = mapped_column(UUID_TYPE, primary_key=True, default=uuid4)
workspace_id: Mapped[UUID] = mapped_column(nullable=False)
project_id: Mapped[UUID | None] = mapped_column()
slug: Mapped[str] = mapped_column(sa.String(63), nullable=False)
priority: Mapped[int] = mapped_column(sa.Integer, nullable=False)
description: Mapped[str] = mapped_column(sa.String(1000), nullable=False)
match_json: Mapped[dict[str, Any]] = mapped_column(JSON_TYPE, default=dict, nullable=False)
conditions_json: Mapped[dict[str, Any]] = mapped_column(JSON_TYPE, default=dict, nullable=False)
decision: Mapped[str] = mapped_column(sa.String(32), nullable=False)
terminal: Mapped[bool] = mapped_column(sa.Boolean, nullable=False, default=False)
source: Mapped[str] = mapped_column(sa.String(64), nullable=False, default="catalog")
enabled: Mapped[bool] = mapped_column(sa.Boolean, nullable=False, default=True)
class PolicyDecisionRow(Base):
__tablename__ = "policy_decisions"
id: Mapped[UUID] = mapped_column(UUID_TYPE, primary_key=True, default=uuid4)
workspace_id: Mapped[UUID] = mapped_column(sa.ForeignKey("workspaces.id"), nullable=False)
project_id: Mapped[UUID | None] = mapped_column()
policy_id: Mapped[UUID | None] = mapped_column(sa.ForeignKey("policies.id"))
decision: Mapped[str] = mapped_column(sa.String(32), nullable=False)
matched_rule_ids: Mapped[list[str]] = mapped_column(JSON_TYPE, default=list, nullable=False)
decision_trace: Mapped[list[dict[str, Any]]] = mapped_column(
JSON_TYPE, default=list, nullable=False
)
reason_code: Mapped[str | None] = mapped_column(sa.String(128))
trace_id: Mapped[str] = mapped_column(sa.String(128), nullable=False)
retention_until: Mapped[datetime | None] = mapped_column(sa.DateTime(timezone=True))
created_at: Mapped[datetime] = mapped_column(
sa.DateTime(timezone=True), default=utc_now, nullable=False
)
class ReconcileRunRow(TimestampMixin, Base):
__tablename__ = "reconcile_runs"
__table_args__ = (
sa.ForeignKeyConstraint(
["workspace_id", "project_id"],
["projects.workspace_id", "projects.id"],
name="fk_reconcile_runs_project_scope",
),
sa.CheckConstraint(
"status in ("
"'created','planning','planned','applying','verifying','succeeded','invalid','failed')",
name="ck_reconcile_runs_status",
),
)
id: Mapped[UUID] = mapped_column(UUID_TYPE, primary_key=True, default=uuid4)
workspace_id: Mapped[UUID] = mapped_column(nullable=False)
project_id: Mapped[UUID] = mapped_column(nullable=False)
desired_hash: Mapped[str] = mapped_column(sa.String(128), nullable=False)
current_hash: Mapped[str | None] = mapped_column(sa.String(128))
plan_hash: Mapped[str | None] = mapped_column(sa.String(128))
status: Mapped[str] = mapped_column(sa.String(32), nullable=False, default="created")
planned_changes: Mapped[list[dict[str, Any]]] = mapped_column(
JSON_TYPE, default=list, nullable=False
)
applied_changes: Mapped[list[dict[str, Any]]] = mapped_column(
JSON_TYPE, default=list, nullable=False
)
verification: Mapped[dict[str, Any]] = mapped_column(JSON_TYPE, default=dict, nullable=False)
started_at: Mapped[datetime | None] = mapped_column(sa.DateTime(timezone=True))
completed_at: Mapped[datetime | None] = mapped_column(sa.DateTime(timezone=True))
class OperationRow(TimestampMixin, Base):
__tablename__ = "operations"
__table_args__ = (
sa.ForeignKeyConstraint(
["workspace_id", "project_id"],
["projects.workspace_id", "projects.id"],
name="fk_operations_project_scope",
),
sa.ForeignKeyConstraint(
["workspace_id", "project_id", "environment_id"],
["environments.workspace_id", "environments.project_id", "environments.id"],
name="fk_operations_environment_scope",
),
sa.UniqueConstraint("workspace_id", "project_id", "id", name="uq_operations_project_id"),
sa.CheckConstraint(
"state in ("
"'requested','planned','approval_required','approved','executing','verifying','succeeded','rejected','failed','rollback_pending','rolled_back','rollback_failed')",
name="ck_operations_state",
),
)
id: Mapped[UUID] = mapped_column(UUID_TYPE, primary_key=True, default=uuid4)
workspace_id: Mapped[UUID] = mapped_column(nullable=False)
project_id: Mapped[UUID] = mapped_column(nullable=False)
environment_id: Mapped[UUID] = mapped_column(nullable=False)
operation_type: Mapped[str] = mapped_column(sa.String(64), nullable=False)
target_ref: Mapped[str] = mapped_column(sa.String(512), nullable=False)
plan_hash: Mapped[str] = mapped_column(sa.String(128), nullable=False)
state: Mapped[str] = mapped_column(sa.String(32), nullable=False, default="requested")
requested_by: Mapped[str] = mapped_column(sa.String(256), nullable=False)
expires_at: Mapped[datetime | None] = mapped_column(sa.DateTime(timezone=True))
verification: Mapped[dict[str, Any]] = mapped_column(JSON_TYPE, default=dict, nullable=False)
class ApprovalRow(TimestampMixin, Base):
__tablename__ = "approvals"
__table_args__ = (
sa.ForeignKeyConstraint(
["workspace_id", "project_id", "operation_id"],
["operations.workspace_id", "operations.project_id", "operations.id"],
name="fk_approvals_operation_scope",
),
sa.CheckConstraint(
"state in ('pending','approved','rejected','expired')", name="ck_approvals_state"
),
)
id: Mapped[UUID] = mapped_column(UUID_TYPE, primary_key=True, default=uuid4)
workspace_id: Mapped[UUID] = mapped_column(nullable=False)
project_id: Mapped[UUID] = mapped_column(nullable=False)
operation_id: Mapped[UUID] = mapped_column(nullable=False)
plan_hash: Mapped[str] = mapped_column(sa.String(128), nullable=False)
state: Mapped[str] = mapped_column(sa.String(32), nullable=False, default="pending")
actor: Mapped[str] = mapped_column(sa.String(256), nullable=False)
reason: Mapped[str | None] = mapped_column(sa.String(1000))
expires_at: Mapped[datetime] = mapped_column(sa.DateTime(timezone=True), nullable=False)
approved_at: Mapped[datetime | None] = mapped_column(sa.DateTime(timezone=True))
class AuditEventRow(Base):
__tablename__ = "audit_events"
__table_args__ = (
sa.ForeignKeyConstraint(
["workspace_id"], ["workspaces.id"], name="fk_audit_events_workspace"
),
sa.Index("ix_audit_events_occurred_at_id", "occurred_at", "id"),
)
id: Mapped[UUID] = mapped_column(UUID_TYPE, primary_key=True, default=uuid4)
occurred_at: Mapped[datetime] = mapped_column(
sa.DateTime(timezone=True), default=utc_now, nullable=False
)
trace_id: Mapped[str] = mapped_column(sa.String(128), nullable=False)
workspace_id: Mapped[UUID] = mapped_column(nullable=False)
project_id: Mapped[UUID | None] = mapped_column()
environment_id: Mapped[UUID | None] = mapped_column()
event_type: Mapped[str] = mapped_column(sa.String(128), nullable=False)
principal_issuer: Mapped[str | None] = mapped_column(sa.String(512))
principal_subject: Mapped[str | None] = mapped_column(sa.String(256))
client_id: Mapped[UUID | None] = mapped_column()
virtual_server_id: Mapped[UUID | None] = mapped_column()
tool_name: Mapped[str | None] = mapped_column(sa.String(256))
policy_decision: Mapped[str | None] = mapped_column(sa.String(32))
matched_rule_ids: Mapped[list[str]] = mapped_column(JSON_TYPE, default=list, nullable=False)
policy_trace: Mapped[list[dict[str, Any]]] = mapped_column(
JSON_TYPE, default=list, nullable=False
)
arguments_hash: Mapped[str | None] = mapped_column(sa.String(128))
sanitized_arguments: Mapped[dict[str, Any]] = mapped_column(
JSON_TYPE, default=dict, nullable=False
)
duration_ms: Mapped[int | None] = mapped_column(sa.Integer)
outcome: Mapped[str] = mapped_column(sa.String(32), nullable=False)
error_code: Mapped[str | None] = mapped_column(sa.String(128))
retention_until: Mapped[datetime | None] = mapped_column(sa.DateTime(timezone=True))
operation_id: Mapped[UUID | None] = mapped_column()
class KnowledgeSourceRow(TimestampMixin, Base):
__tablename__ = "knowledge_sources"
__table_args__ = (
sa.ForeignKeyConstraint(
["workspace_id", "project_id"],
["projects.workspace_id", "projects.id"],
name="fk_knowledge_sources_project_scope",
),
sa.UniqueConstraint("project_id", "slug", name="uq_knowledge_sources_project_slug"),
)
id: Mapped[UUID] = mapped_column(UUID_TYPE, primary_key=True, default=uuid4)
workspace_id: Mapped[UUID] = mapped_column(nullable=False)
project_id: Mapped[UUID] = mapped_column(nullable=False)
slug: Mapped[str] = mapped_column(sa.String(63), nullable=False)
source_type: Mapped[str] = mapped_column(sa.String(64), nullable=False)
uri: Mapped[str] = mapped_column(sa.String(2048), nullable=False)
visibility: Mapped[str] = mapped_column(sa.String(32), nullable=False, default="project")
state: Mapped[str] = mapped_column(sa.String(32), nullable=False, default="registered")
configuration: Mapped[dict[str, Any]] = mapped_column(JSON_TYPE, default=dict, nullable=False)
last_indexed_at: Mapped[datetime | None] = mapped_column(sa.DateTime(timezone=True))
class KnowledgeDocumentRow(TimestampMixin, Base):
__tablename__ = "knowledge_documents"
__table_args__ = (
sa.ForeignKeyConstraint(
["workspace_id", "project_id"],
["projects.workspace_id", "projects.id"],
name="fk_knowledge_documents_project_scope",
),
sa.ForeignKeyConstraint(
["source_id"], ["knowledge_sources.id"], name="fk_knowledge_documents_source"
),
sa.UniqueConstraint("source_id", "external_ref", name="uq_knowledge_documents_source_ref"),
)
id: Mapped[UUID] = mapped_column(UUID_TYPE, primary_key=True, default=uuid4)
workspace_id: Mapped[UUID] = mapped_column(nullable=False)
project_id: Mapped[UUID] = mapped_column(nullable=False)
source_id: Mapped[UUID] = mapped_column(nullable=False)
external_ref: Mapped[str] = mapped_column(sa.String(1024), nullable=False)
title: Mapped[str] = mapped_column(sa.String(500), nullable=False)
content_hash: Mapped[str] = mapped_column(sa.String(128), nullable=False)
document_version: Mapped[int] = mapped_column(sa.Integer, nullable=False, default=1)
state: Mapped[str] = mapped_column(sa.String(32), nullable=False, default="registered")
content_bytes: Mapped[int] = mapped_column(sa.Integer, nullable=False, default=0)
record_metadata: Mapped[dict[str, Any]] = mapped_column(JSON_TYPE, default=dict, nullable=False)
class KnowledgeChunkRow(TimestampMixin, Base):
__tablename__ = "knowledge_chunks"
__table_args__ = (
sa.ForeignKeyConstraint(
["document_id"], ["knowledge_documents.id"], name="fk_knowledge_chunks_document"
),
sa.UniqueConstraint("document_id", "chunk_index", name="uq_knowledge_chunks_index"),
)
id: Mapped[UUID] = mapped_column(UUID_TYPE, primary_key=True, default=uuid4)
workspace_id: Mapped[UUID] = mapped_column(nullable=False)
project_id: Mapped[UUID] = mapped_column(nullable=False)
document_id: Mapped[UUID] = mapped_column(nullable=False)
chunk_index: Mapped[int] = mapped_column(sa.Integer, nullable=False)
text: Mapped[str] = mapped_column(sa.Text, nullable=False)
content_hash: Mapped[str] = mapped_column(sa.String(128), nullable=False)
token_count: Mapped[int] = mapped_column(sa.Integer, nullable=False, default=0)
record_metadata: Mapped[dict[str, Any]] = mapped_column(JSON_TYPE, default=dict, nullable=False)
@@ -0,0 +1,599 @@
"""Workspace/project-scoped repositories.
Every repository constructor requires a ``Scope``. Project-owned repositories
reject workspace-only scopes, and every query applies the scope predicate before
loading or mutating a row. There is intentionally no generic unrestricted
query or audit update method.
"""
from __future__ import annotations
import re
from datetime import UTC, datetime
from typing import Any, TypeVar, cast
from uuid import UUID
from pydantic import BaseModel
from sqlalchemy import Select, select, update
from sqlalchemy.orm import Session
from packages.hub_core.domain import (
Approval,
AuditEvent,
Capability,
Client,
ClientBinding,
Connector,
Environment,
KnowledgeChunk,
KnowledgeDocument,
KnowledgeSource,
Operation,
Policy,
PolicyDecision,
Project,
ReconcileRun,
RiskClass,
Scope,
ToolDefinition,
VirtualServer,
VirtualServerCapability,
Workspace,
)
from packages.hub_core.errors import (
AuditMutationError,
ConcurrentUpdateError,
DomainError,
InvalidTransition,
MissingProjectScope,
)
from packages.hub_core.state_machines import transition
from .models import (
ApprovalRow,
AuditEventRow,
CapabilityRow,
ClientBindingRow,
ClientRow,
ConnectorRow,
EnvironmentRow,
KnowledgeChunkRow,
KnowledgeDocumentRow,
KnowledgeSourceRow,
OperationRow,
PolicyDecisionRow,
PolicyRow,
ProjectRow,
ReconcileRunRow,
RiskClassRow,
ToolDefinitionRow,
VirtualServerCapabilityRow,
VirtualServerRow,
WorkspaceRow,
)
RowT = TypeVar("RowT")
DomainT = TypeVar("DomainT", bound=BaseModel)
def _now() -> datetime:
return datetime.now(UTC)
def _normalized_row_data(row: Any, rename_fields: dict[str, str] | None = None) -> dict[str, Any]:
reverse = {target: source for source, target in (rename_fields or {}).items()}
values: dict[str, Any] = {}
for column in row.__table__.columns:
value = getattr(row, column.name)
if isinstance(value, datetime) and value.tzinfo is None:
value = value.replace(tzinfo=UTC)
values[reverse.get(column.name, column.name)] = value
return values
def _atomic_transition(
session: Session,
row_type: type[Any],
scope: Scope,
record_id: UUID,
current: str,
target: str,
expected_version: int,
machine: str,
state_field: str,
*,
project_required: bool,
) -> None:
next_state = transition(machine, current, target)
statement = (
update(row_type)
.where(row_type.id == record_id)
.where(row_type.workspace_id == scope.workspace_id)
.where(row_type.revision == expected_version)
)
if project_required:
statement = statement.where(row_type.project_id == scope.project_id)
result = session.execute(
statement.values(
**{
state_field: next_state,
"revision": expected_version + 1,
"updated_at": _now(),
}
)
)
if result.rowcount != 1:
raise ConcurrentUpdateError(f"stale version for {row_type.__name__} {record_id}")
class ScopedRepository[RowT, DomainT]:
"""Small typed CRUD boundary with mandatory workspace/project filtering."""
row_type: Any
domain_type: Any
project_required = True
project_filter_if_present = True
rename_fields: dict[str, str] = {}
def __init__(self, session: Session, scope: Scope) -> None:
if not isinstance(scope, Scope):
raise TypeError("repository scope must be a Scope")
self.session = session
self.scope = scope
if self.project_required and scope.project_id is None:
raise MissingProjectScope(f"{type(self).__name__} requires project scope")
def _statement(self) -> Select[tuple[RowT]]:
row = self.row_type
statement = select(row).where(row.workspace_id == self.scope.workspace_id)
if self.project_required or (
self.project_filter_if_present and self.scope.project_id is not None
):
statement = statement.where(row.project_id == self.scope.project_id)
return statement.order_by(row.id)
def _row_to_domain(self, row: RowT) -> DomainT:
data = _normalized_row_data(row, self.rename_fields)
data = {key: value for key, value in data.items() if key in self.domain_type.model_fields}
return cast(DomainT, self.domain_type.model_validate(data))
def _model_data(self, model: DomainT) -> dict[str, Any]:
values = cast(Any, model).model_dump()
columns = set(self.row_type.__table__.columns.keys())
data: dict[str, Any] = {}
for field, value in values.items():
target = self.rename_fields.get(field, field)
if target in columns:
data[target] = value
return data
def list(self, *, limit: int = 100, offset: int = 0) -> list[DomainT]:
if not 1 <= limit <= 500:
raise ValueError("limit must be between 1 and 500")
if offset < 0:
raise ValueError("offset must be non-negative")
rows = self.session.scalars(self._statement().limit(limit).offset(offset)).all()
return [self._row_to_domain(row) for row in rows]
def get(self, record_id: UUID) -> DomainT | None:
row = self.session.scalars(self._statement().where(self.row_type.id == record_id)).first()
return self._row_to_domain(row) if row is not None else None
def create(self, model: DomainT) -> DomainT:
data = self._model_data(model)
if data.get("workspace_id") != self.scope.workspace_id:
raise ValueError("model workspace does not match repository scope")
if self.project_required and data.get("project_id") != self.scope.project_id:
raise ValueError("model project does not match repository scope")
row = self.row_type(**data)
self.session.add(row)
self.session.flush()
return self._row_to_domain(row)
def update(self, model: DomainT, *, expected_version: int) -> DomainT:
record_id = cast(Any, model).id
data = self._model_data(model)
data.pop("id", None)
data.pop("created_at", None)
data.pop("revision", None)
data["updated_at"] = _now()
data["revision"] = expected_version + 1
statement = (
update(self.row_type)
.where(self.row_type.id == record_id)
.where(self.row_type.workspace_id == self.scope.workspace_id)
.where(self.row_type.revision == expected_version)
)
if self.project_required or (
self.project_filter_if_present and self.scope.project_id is not None
):
statement = statement.where(self.row_type.project_id == self.scope.project_id)
result = self.session.execute(statement.values(**data))
if result.rowcount != 1:
raise ConcurrentUpdateError(f"stale version for {type(self).__name__} {record_id}")
self.session.flush()
return self.get(record_id) # type: ignore[return-value]
class WorkspaceRepository(ScopedRepository[WorkspaceRow, Workspace]):
row_type = WorkspaceRow
domain_type = Workspace
project_required = False
project_filter_if_present = False
def create(self, model: Workspace) -> Workspace:
if model.id != self.scope.workspace_id:
raise ValueError("workspace model does not match repository scope")
row = WorkspaceRow(**model.model_dump())
self.session.add(row)
self.session.flush()
return self._row_to_domain(row)
def _statement(self) -> Select[tuple[WorkspaceRow]]:
return (
select(self.row_type)
.where(self.row_type.id == self.scope.workspace_id)
.order_by(self.row_type.id)
)
class ProjectRepository(ScopedRepository[ProjectRow, Project]):
row_type = ProjectRow
domain_type = Project
project_required = False
project_filter_if_present = False
def _statement(self) -> Select[tuple[ProjectRow]]:
statement = select(self.row_type).where(
self.row_type.workspace_id == self.scope.workspace_id
)
if self.scope.project_id is not None:
statement = statement.where(self.row_type.id == self.scope.project_id)
return statement.order_by(self.row_type.id)
def create(self, model: Project) -> Project:
if self.scope.project_id is not None and model.id != self.scope.project_id:
raise ValueError("project model does not match repository scope")
return super().create(model)
class RiskClassRepository:
"""Read-only catalog boundary for safe global risk classes."""
def __init__(self, session: Session, scope: Scope) -> None:
if not isinstance(scope, Scope):
raise TypeError("repository scope must be a Scope")
self.session = session
self.scope = scope
def list(self) -> list[RiskClass]:
rows = self.session.scalars(select(RiskClassRow).order_by(RiskClassRow.code)).all()
return [RiskClass.model_validate(row) for row in rows]
class CapabilityRepository:
"""Read/create boundary for global capability definitions, always scoped by caller."""
def __init__(self, session: Session, scope: Scope) -> None:
if not isinstance(scope, Scope):
raise TypeError("repository scope must be a Scope")
self.session = session
self.scope = scope
def list(self, *, limit: int = 100, offset: int = 0) -> list[Capability]:
rows = self.session.scalars(
select(CapabilityRow)
.order_by(CapabilityRow.canonical_name, CapabilityRow.version)
.limit(limit)
.offset(offset)
).all()
return [Capability.model_validate(_normalized_row_data(row)) for row in rows]
def create(self, model: Capability) -> Capability:
row = CapabilityRow(**model.model_dump())
self.session.add(row)
self.session.flush()
return Capability.model_validate(_normalized_row_data(row))
class ToolDefinitionRepository(ScopedRepository[ToolDefinitionRow, ToolDefinition]):
row_type = ToolDefinitionRow
domain_type = ToolDefinition
class VirtualServerCapabilityRepository(
ScopedRepository[VirtualServerCapabilityRow, VirtualServerCapability]
):
row_type = VirtualServerCapabilityRow
domain_type = VirtualServerCapability
class ClientBindingRepository(ScopedRepository[ClientBindingRow, ClientBinding]):
row_type = ClientBindingRow
domain_type = ClientBinding
class EnvironmentRepository(ScopedRepository[EnvironmentRow, Environment]):
row_type = EnvironmentRow
domain_type = Environment
class ConnectorRepository(ScopedRepository[ConnectorRow, Connector]):
row_type = ConnectorRow
domain_type = Connector
def _model_data(self, model: Connector) -> dict[str, Any]:
data = super()._model_data(model)
data["environment_scope"] = (
str(model.environment_id) if model.environment_id else "__project__"
)
return data
def transition_health(
self, connector_id: UUID, target: str, *, expected_version: int
) -> Connector:
row = self.session.scalars(
self._statement().where(self.row_type.id == connector_id)
).first()
if row is None:
raise LookupError(f"connector not found in scope: {connector_id}")
_atomic_transition(
self.session,
self.row_type,
self.scope,
connector_id,
row.health_state,
target,
expected_version,
"connector_health",
"health_state",
project_required=True,
)
self.session.flush()
return self.get(connector_id) # type: ignore[return-value]
class VirtualServerRepository(ScopedRepository[VirtualServerRow, VirtualServer]):
row_type = VirtualServerRow
domain_type = VirtualServer
rename_fields = {"metadata": "record_metadata"}
class ClientRepository(ScopedRepository[ClientRow, Client]):
row_type = ClientRow
domain_type = Client
project_required = False
project_filter_if_present = False
rename_fields = {"metadata": "record_metadata"}
def transition_status(self, client_id: UUID, target: str, *, expected_version: int) -> Client:
row = self.session.scalars(self._statement().where(self.row_type.id == client_id)).first()
if row is None:
raise LookupError(f"client not found in workspace: {client_id}")
_atomic_transition(
self.session,
self.row_type,
self.scope,
client_id,
row.status,
target,
expected_version,
"client",
"status",
project_required=False,
)
if target == "revoked":
row.revoked_at = _now()
self.session.flush()
return self.get(client_id) # type: ignore[return-value]
class PolicyRepository(ScopedRepository[PolicyRow, Policy]):
row_type = PolicyRow
domain_type = Policy
project_required = False
rename_fields = {"match": "match_json", "conditions": "conditions_json"}
class PolicyDecisionRepository:
"""Short-retention decision record boundary with mandatory workspace scope."""
def __init__(self, session: Session, scope: Scope) -> None:
if not isinstance(scope, Scope):
raise TypeError("repository scope must be a Scope")
self.session = session
self.scope = scope
def create(self, model: PolicyDecision) -> PolicyDecision:
if model.workspace_id != self.scope.workspace_id:
raise ValueError("policy decision workspace does not match scope")
if self.scope.project_id is not None and model.project_id != self.scope.project_id:
raise ValueError("policy decision project does not match scope")
row = PolicyDecisionRow(**model.model_dump())
self.session.add(row)
self.session.flush()
return PolicyDecision.model_validate(_normalized_row_data(row))
def list(self, *, limit: int = 100, offset: int = 0) -> list[PolicyDecision]:
statement = select(PolicyDecisionRow).where(
PolicyDecisionRow.workspace_id == self.scope.workspace_id
)
if self.scope.project_id is not None:
statement = statement.where(PolicyDecisionRow.project_id == self.scope.project_id)
statement = (
statement.order_by(PolicyDecisionRow.created_at, PolicyDecisionRow.id)
.limit(limit)
.offset(offset)
)
return [
PolicyDecision.model_validate(_normalized_row_data(row))
for row in self.session.scalars(statement).all()
]
class ReconcileRunRepository(ScopedRepository[ReconcileRunRow, ReconcileRun]):
row_type = ReconcileRunRow
domain_type = ReconcileRun
def transition_status(
self, run_id: UUID, target: str, *, expected_version: int
) -> ReconcileRun:
row = self.session.scalars(self._statement().where(self.row_type.id == run_id)).first()
if row is None:
raise LookupError(f"reconcile run not found in scope: {run_id}")
_atomic_transition(
self.session,
self.row_type,
self.scope,
run_id,
row.status,
target,
expected_version,
"reconcile",
"status",
project_required=True,
)
if target in {"succeeded", "failed", "invalid"}:
row.completed_at = _now()
self.session.flush()
return self.get(run_id) # type: ignore[return-value]
class OperationRepository(ScopedRepository[OperationRow, Operation]):
row_type = OperationRow
domain_type = Operation
def transition_state(
self, operation_id: UUID, target: str, *, expected_version: int
) -> Operation:
row = self.session.scalars(
self._statement().where(self.row_type.id == operation_id)
).first()
if row is None:
raise LookupError(f"operation not found in scope: {operation_id}")
_atomic_transition(
self.session,
self.row_type,
self.scope,
operation_id,
row.state,
target,
expected_version,
"operation",
"state",
project_required=True,
)
self.session.flush()
return self.get(operation_id) # type: ignore[return-value]
class ApprovalRepository(ScopedRepository[ApprovalRow, Approval]):
"""Approval boundary with plan-hash and state-machine enforcement."""
row_type = ApprovalRow
domain_type = Approval
def transition_state(
self, approval_id: UUID, target: str, *, expected_version: int
) -> Approval:
row = self.session.scalars(self._statement().where(self.row_type.id == approval_id)).first()
if row is None:
raise LookupError(f"approval not found in scope: {approval_id}")
if target == "approved":
operation = self.session.scalars(
select(OperationRow)
.where(OperationRow.id == row.operation_id)
.where(OperationRow.workspace_id == self.scope.workspace_id)
.where(OperationRow.project_id == self.scope.project_id)
).first()
if operation is None or operation.plan_hash != row.plan_hash:
raise DomainError("approval plan hash does not match operation")
if operation.state != "approval_required":
raise InvalidTransition("operation is not waiting for approval")
_atomic_transition(
self.session,
self.row_type,
self.scope,
approval_id,
row.state,
target,
expected_version,
"approval",
"state",
project_required=True,
)
if target == "approved":
row.approved_at = _now()
self.session.flush()
return self.get(approval_id) # type: ignore[return-value]
class AuditEventRepository:
"""Append/query-only audit boundary; update and delete are not exposed."""
_sensitive_key = re.compile(
r"(?:password|secret|token|private|authorization|cookie|credential|connection)", re.I
)
def __init__(self, session: Session, scope: Scope) -> None:
if not isinstance(scope, Scope):
raise TypeError("repository scope must be a Scope")
self.session = session
self.scope = scope
@classmethod
def _assert_sanitized(cls, value: Any) -> None:
if isinstance(value, dict):
for key, item in value.items():
if cls._sensitive_key.search(str(key)):
raise AuditMutationError(f"sensitive audit field is prohibited: {key}")
cls._assert_sanitized(item)
elif isinstance(value, list):
for item in value:
cls._assert_sanitized(item)
def create(self, event: AuditEvent) -> AuditEvent:
if event.workspace_id != self.scope.workspace_id:
raise ValueError("audit workspace does not match repository scope")
if self.scope.project_id is not None and event.project_id != self.scope.project_id:
raise ValueError("audit project does not match repository scope")
self._assert_sanitized(event.sanitized_arguments)
row = AuditEventRow(**event.model_dump())
self.session.add(row)
self.session.flush()
return AuditEvent.model_validate(row)
def list(self, *, limit: int = 100, offset: int = 0) -> list[AuditEvent]:
if not 1 <= limit <= 500 or offset < 0:
raise ValueError("invalid pagination")
statement = select(AuditEventRow).where(
AuditEventRow.workspace_id == self.scope.workspace_id
)
if self.scope.project_id is not None:
statement = statement.where(AuditEventRow.project_id == self.scope.project_id)
statement = (
statement.order_by(AuditEventRow.occurred_at, AuditEventRow.id)
.limit(limit)
.offset(offset)
)
return [
AuditEvent.model_validate(_normalized_row_data(row))
for row in self.session.scalars(statement).all()
]
class KnowledgeSourceRepository(ScopedRepository[KnowledgeSourceRow, KnowledgeSource]):
row_type = KnowledgeSourceRow
domain_type = KnowledgeSource
class KnowledgeDocumentRepository(ScopedRepository[KnowledgeDocumentRow, KnowledgeDocument]):
row_type = KnowledgeDocumentRow
domain_type = KnowledgeDocument
rename_fields = {"metadata": "record_metadata"}
class KnowledgeChunkRepository(ScopedRepository[KnowledgeChunkRow, KnowledgeChunk]):
row_type = KnowledgeChunkRow
domain_type = KnowledgeChunk
rename_fields = {"metadata": "record_metadata"}
+58
View File
@@ -0,0 +1,58 @@
"""Deterministic, credential-free Hub seed data."""
from __future__ import annotations
from dataclasses import dataclass
from uuid import NAMESPACE_URL, UUID, uuid5
from sqlalchemy import select
from sqlalchemy.orm import Session
from .models import RiskClassRow, WorkspaceRow
SEED_NAMESPACE = "https://hub.example.com/seed/v1"
@dataclass(frozen=True)
class SeedResult:
workspace_id: UUID
risk_class_codes: tuple[str, ...]
def _stable_id(kind: str, name: str) -> UUID:
return uuid5(NAMESPACE_URL, f"{SEED_NAMESPACE}:{kind}:{name}")
def seed_database(session: Session) -> SeedResult:
"""Insert the ITWorx workspace and safe read-only risk classes idempotently."""
workspace_id = _stable_id("workspace", "itworx")
workspace = session.get(WorkspaceRow, workspace_id)
if workspace is None:
workspace = WorkspaceRow(id=workspace_id, slug="itworx", name="ITWorx", status="active")
session.add(workspace)
risk_classes = (
("health-read", "Read-only health and liveness metadata", 65536),
("metadata-read", "Read-only catalog and metadata inspection", 65536),
("read-only", "Read-only target-system capability", 131072),
)
for code, description, max_output_bytes in risk_classes:
existing = session.get(RiskClassRow, code)
if existing is None:
session.add(
RiskClassRow(
code=code,
description=description,
allows_writes=False,
max_output_bytes=max_output_bytes,
)
)
session.flush()
persisted = session.scalar(select(WorkspaceRow.id).where(WorkspaceRow.slug == "itworx"))
if persisted is None:
raise RuntimeError("deterministic workspace seed was not persisted")
return SeedResult(
workspace_id=persisted, risk_class_codes=tuple(code for code, _, _ in risk_classes)
)
@@ -0,0 +1,56 @@
"""Application service façade for scoped persistence operations."""
from __future__ import annotations
from uuid import UUID
from sqlalchemy.orm import Session
from packages.hub_core.domain import Approval, AuditEvent, Connector, Operation, ReconcileRun, Scope
from .repositories import (
ApprovalRepository,
AuditEventRepository,
ConnectorRepository,
OperationRepository,
ReconcileRunRepository,
)
class ControlPlanePersistenceService:
"""Explicit service boundary used by future API/worker adapters."""
def __init__(self, session: Session, scope: Scope) -> None:
self.session = session
self.scope = scope
def transition_connector_health(
self, connector_id: UUID, target: str, *, expected_version: int
) -> Connector:
return ConnectorRepository(self.session, self.scope).transition_health(
connector_id, target, expected_version=expected_version
)
def transition_reconcile_run(
self, run_id: UUID, target: str, *, expected_version: int
) -> ReconcileRun:
return ReconcileRunRepository(self.session, self.scope).transition_status(
run_id, target, expected_version=expected_version
)
def transition_operation(
self, operation_id: UUID, target: str, *, expected_version: int
) -> Operation:
return OperationRepository(self.session, self.scope).transition_state(
operation_id, target, expected_version=expected_version
)
def transition_approval(
self, approval_id: UUID, target: str, *, expected_version: int
) -> Approval:
return ApprovalRepository(self.session, self.scope).transition_state(
approval_id, target, expected_version=expected_version
)
def record_audit(self, event: AuditEvent) -> AuditEvent:
return AuditEventRepository(self.session, self.scope).create(event)