Initial curated ITWorx_MCP_Hub source from 368fba691ea8d23403b4d56b53b5ec670d7c5020
This commit is contained in:
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)))
|
||||
@@ -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)
|
||||
@@ -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"}
|
||||
@@ -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)
|
||||
Reference in new issue
Block a user