feat(org): model grants, org memory, MCP gateway (v2 phases A1-3..5)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
c93b25741e
commit
dee2f72c46
@@ -24,7 +24,17 @@ from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import DateTime, ForeignKey, Index, String, Text
|
||||
from sqlalchemy import (
|
||||
Boolean,
|
||||
DateTime,
|
||||
Float,
|
||||
ForeignKey,
|
||||
Index,
|
||||
Integer,
|
||||
String,
|
||||
Text,
|
||||
UniqueConstraint,
|
||||
)
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.database import Base
|
||||
@@ -103,4 +113,139 @@ class OrgSetupDraftRow(Base):
|
||||
)
|
||||
|
||||
|
||||
__all__ = ["OrgSetupDraftRow", "PlatformInviteRow"]
|
||||
class DefaultModelGrantRow(Base):
|
||||
"""The platform owner's default grant set, copied into every new organization.
|
||||
|
||||
Kept in a table of its own (never a column on ``ai_models``) so the model
|
||||
catalogue stays a pure, owner-managed list.
|
||||
"""
|
||||
|
||||
__tablename__ = "default_model_grants"
|
||||
__table_args__ = (UniqueConstraint("ai_model_id", name="uq_default_model_grant"),)
|
||||
|
||||
id: Mapped[str] = pk()
|
||||
ai_model_id: Mapped[str] = mapped_column(
|
||||
ForeignKey("ai_models.id", ondelete="CASCADE"), nullable=False
|
||||
)
|
||||
daily_credit_cap: Mapped[float | None] = mapped_column(Float, nullable=True)
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
|
||||
created_by_user_id: Mapped[str | None] = mapped_column(
|
||||
ForeignKey("users.id"), nullable=True
|
||||
)
|
||||
created_at: Mapped[datetime] = created()
|
||||
|
||||
|
||||
class OrgPolicyRow(Base):
|
||||
"""Per-organization policy the owner sets: limits and automation switches."""
|
||||
|
||||
__tablename__ = "org_policies"
|
||||
|
||||
organization_id: Mapped[str] = mapped_column(
|
||||
ForeignKey("organizations.id", ondelete="CASCADE"), primary_key=True
|
||||
)
|
||||
max_employees: Mapped[int] = mapped_column(Integer, default=5, nullable=False)
|
||||
#: ``None`` means "no explicit budget" (an empty/unlimited wallet view).
|
||||
monthly_credit_budget: Mapped[float | None] = mapped_column(Float, nullable=True)
|
||||
automation_enabled: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
|
||||
#: MCP access for this organization (Phase 5); the owner may switch it off.
|
||||
mcp_enabled: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), default=utcnow, onupdate=utcnow, nullable=False
|
||||
)
|
||||
|
||||
|
||||
class OrgMemoryRow(Base):
|
||||
"""One thing the organization remembers about itself.
|
||||
|
||||
A memory is *small* by design (≤ 1,500 characters): longer material is a
|
||||
document and belongs in the Knowledge Base. ``content_hash`` is what makes
|
||||
"teach the same thing twice" a correction instead of a duplicate, and
|
||||
``key`` gives a rule a stable address so the engine can update it.
|
||||
"""
|
||||
|
||||
__tablename__ = "org_memories"
|
||||
__table_args__ = (
|
||||
Index("ix_org_memories_org_kind", "organization_id", "kind"),
|
||||
Index("ix_org_memories_org_key", "organization_id", "key"),
|
||||
)
|
||||
|
||||
id: Mapped[str] = pk()
|
||||
organization_id: Mapped[str] = mapped_column(
|
||||
ForeignKey("organizations.id", ondelete="CASCADE"), nullable=False, index=True
|
||||
)
|
||||
kind: Mapped[str] = mapped_column(String(16), nullable=False)
|
||||
#: Optional stable handle, unique in spirit within one org.
|
||||
key: Mapped[str | None] = mapped_column(String(80), nullable=True)
|
||||
content: Mapped[str] = mapped_column(String(1500), nullable=False)
|
||||
#: A digest of the normalised content — the dedupe key the domain defines.
|
||||
content_hash: Mapped[str] = mapped_column(String(32), nullable=False, default="")
|
||||
source: Mapped[str] = mapped_column(String(16), default="user", nullable=False)
|
||||
evidence_task_id: Mapped[str | None] = mapped_column(
|
||||
ForeignKey("tasks.id", ondelete="SET NULL"), nullable=True
|
||||
)
|
||||
pinned: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
||||
confidence: Mapped[float] = mapped_column(Float, default=1.0, nullable=False)
|
||||
expires_at: Mapped[datetime | None] = mapped_column(
|
||||
DateTime(timezone=True), nullable=True
|
||||
)
|
||||
hits: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
|
||||
last_used_at: Mapped[datetime | None] = mapped_column(
|
||||
DateTime(timezone=True), nullable=True
|
||||
)
|
||||
created_by_user_id: Mapped[str | None] = mapped_column(
|
||||
ForeignKey("users.id"), nullable=True
|
||||
)
|
||||
created_at: Mapped[datetime] = created()
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), default=utcnow, onupdate=utcnow, nullable=False
|
||||
)
|
||||
#: Soft delete: a memory an admin removed stays in the audit story.
|
||||
deleted_at: Mapped[datetime | None] = mapped_column(
|
||||
DateTime(timezone=True), nullable=True
|
||||
)
|
||||
|
||||
|
||||
class McpTokenRow(Base):
|
||||
"""A scoped token an external AI agent uses to call the MCP gateway.
|
||||
|
||||
The token itself is shown exactly once, at creation; only its hash and a
|
||||
six-character prefix (for display) are stored. Every call is attributed to
|
||||
that prefix in the audit trail, so a leaked token can be identified and
|
||||
revoked long before anyone can reuse it.
|
||||
"""
|
||||
|
||||
__tablename__ = "mcp_tokens"
|
||||
|
||||
id: Mapped[str] = pk()
|
||||
organization_id: Mapped[str] = mapped_column(
|
||||
ForeignKey("organizations.id", ondelete="CASCADE"), nullable=False, index=True
|
||||
)
|
||||
name: Mapped[str] = mapped_column(String(120), nullable=False)
|
||||
token_hash: Mapped[str] = mapped_column(String(128), unique=True, nullable=False)
|
||||
#: First six characters, for display only. Never enough to authenticate.
|
||||
prefix: Mapped[str] = mapped_column(String(16), nullable=False)
|
||||
#: Comma-separated scopes; an empty string means "no scopes", never "all".
|
||||
scopes: Mapped[str] = mapped_column(String(255), default="", nullable=False)
|
||||
created_by_user_id: Mapped[str | None] = mapped_column(
|
||||
ForeignKey("users.id"), nullable=True
|
||||
)
|
||||
expires_at: Mapped[datetime | None] = mapped_column(
|
||||
DateTime(timezone=True), nullable=True
|
||||
)
|
||||
last_used_at: Mapped[datetime | None] = mapped_column(
|
||||
DateTime(timezone=True), nullable=True
|
||||
)
|
||||
revoked_at: Mapped[datetime | None] = mapped_column(
|
||||
DateTime(timezone=True), nullable=True
|
||||
)
|
||||
created_at: Mapped[datetime] = created()
|
||||
|
||||
|
||||
__all__ = [
|
||||
"DefaultModelGrantRow",
|
||||
"McpTokenRow",
|
||||
"OrgMemoryRow",
|
||||
"OrgPolicyRow",
|
||||
"OrgSetupDraftRow",
|
||||
"PlatformInviteRow",
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user