Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 67 additions & 16 deletions backend/alembic/versions/v1_0_0_f061_enterprise_info_tenant_id.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,33 +18,84 @@

"""

from typing import Sequence, Union
from __future__ import annotations

from collections.abc import Sequence

from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql

from alembic import op

# revision identifiers, used by Alembic.
revision: str = "f061_enterprise_info_tenant_id"
down_revision: Union[str, None] = "f060_tenant_id_backfill"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
down_revision: str | None = "f060_tenant_id_backfill"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None


def upgrade() -> None:
# 1. Add tenant_id column with default uuid generator or nullable first if populated
op.add_column("enterprise_info", sa.Column("tenant_id", postgresql.UUID(as_uuid=True), nullable=True))
op.create_index(op.f("ix_enterprise_info_tenant_id"), "enterprise_info", ["tenant_id"], unique=False)
inspector = sa.inspect(op.get_bind())

# 2. Drop legacy single info_type unique constraint
op.drop_constraint("enterprise_info_info_type_key", "enterprise_info", type_="unique")
# Fresh deployments build the current ORM schema in ``initial_schema``, so
# these objects may already exist before Alembic reaches this revision.
columns = {column["name"] for column in inspector.get_columns("enterprise_info")}
if "tenant_id" not in columns:
op.add_column(
"enterprise_info",
sa.Column("tenant_id", postgresql.UUID(as_uuid=True), nullable=True),
)

# 3. Create new composite unique constraint (tenant_id, info_type)
op.create_unique_constraint("uq_enterprise_info_tenant_type", "enterprise_info", ["tenant_id", "info_type"])
indexes = {index["name"] for index in inspector.get_indexes("enterprise_info")}
if op.f("ix_enterprise_info_tenant_id") not in indexes:
op.create_index(
op.f("ix_enterprise_info_tenant_id"),
"enterprise_info",
["tenant_id"],
unique=False,
)

constraints = {
constraint["name"]
for constraint in inspector.get_unique_constraints("enterprise_info")
}
if "enterprise_info_info_type_key" in constraints:
op.drop_constraint(
"enterprise_info_info_type_key",
"enterprise_info",
type_="unique",
)
if "uq_enterprise_info_tenant_type" not in constraints:
op.create_unique_constraint(
"uq_enterprise_info_tenant_type",
"enterprise_info",
["tenant_id", "info_type"],
)


def downgrade() -> None:
op.drop_constraint("uq_enterprise_info_tenant_type", "enterprise_info", type_="unique")
op.create_unique_constraint("enterprise_info_info_type_key", "enterprise_info", ["info_type"])
op.drop_index(op.f("ix_enterprise_info_tenant_id"), table_name="enterprise_info")
op.drop_column("enterprise_info", "tenant_id")
inspector = sa.inspect(op.get_bind())
constraints = {
constraint["name"]
for constraint in inspector.get_unique_constraints("enterprise_info")
}
if "uq_enterprise_info_tenant_type" in constraints:
op.drop_constraint(
"uq_enterprise_info_tenant_type",
"enterprise_info",
type_="unique",
)
if "enterprise_info_info_type_key" not in constraints:
op.create_unique_constraint(
"enterprise_info_info_type_key",
"enterprise_info",
["info_type"],
)

indexes = {index["name"] for index in inspector.get_indexes("enterprise_info")}
if op.f("ix_enterprise_info_tenant_id") in indexes:
op.drop_index(op.f("ix_enterprise_info_tenant_id"), table_name="enterprise_info")

columns = {column["name"] for column in inspector.get_columns("enterprise_info")}
if "tenant_id" in columns:
op.drop_column("enterprise_info", "tenant_id")
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
"""Require tenant ownership for every chat message.

Background:
ChatMessage is tenant-scoped, but its tenant_id column remained nullable after
the original backfill. Missing ownership therefore produced messages that
were committed successfully and then hidden by automatic tenant filtering.

Scope:
Make chat_messages.tenant_id NOT NULL after all writers have been updated to
provide an explicit tenant.

Idempotent:
Inspector checks the current nullability before changing the column. Existing
NULL rows intentionally stop the migration so operators can reconcile their
ownership out of band instead of assigning data to the wrong tenant.

Revision ID: f062_chat_message_tenant_nn
Revises: f061_enterprise_info_tenant_id
Create Date: 2026-08-14 10:00:00.000000
"""

from __future__ import annotations

from collections.abc import Sequence

import sqlalchemy as sa

from alembic import op

revision: str = "f062_chat_message_tenant_nn"
down_revision: str | None = "f061_enterprise_info_tenant_id"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None


def _tenant_column() -> dict:
inspector = sa.inspect(op.get_bind())
return next(
column
for column in inspector.get_columns("chat_messages")
if column["name"] == "tenant_id"
)


def upgrade() -> None:
if _tenant_column()["nullable"]:
op.alter_column(
"chat_messages",
"tenant_id",
existing_type=sa.UUID(as_uuid=True),
nullable=False,
)


def downgrade() -> None:
if not _tenant_column()["nullable"]:
op.alter_column(
"chat_messages",
"tenant_id",
existing_type=sa.UUID(as_uuid=True),
nullable=True,
)
6 changes: 3 additions & 3 deletions backend/app/api/activity.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ async def get_agent_activity(
agent_id: uuid.UUID,
limit: int = Query(50, le=200),
current_user: User = Depends(get_current_user),
db: Any = None,
db: AsyncSession = Depends(get_db),
):
"""Get recent activity logs for an agent."""
await check_agent_access(db, current_user, agent_id)
Expand All @@ -45,7 +45,7 @@ async def get_agent_activity(
async def list_conversations(
agent_id: uuid.UUID,
current_user: User = Depends(get_current_user),
db: Any = None,
db: AsyncSession = Depends(get_db),
):
"""List all conversation partners for this agent (web users + other agents)."""
await check_agent_access(db, current_user, agent_id)
Expand All @@ -59,7 +59,7 @@ async def get_conversation_messages(
conv_id: str,
limit: int = Query(100, le=500),
current_user: User = Depends(get_current_user),
db: Any = None,
db: AsyncSession = Depends(get_db),
):
"""Get messages for a specific conversation."""
await check_agent_access(db, current_user, agent_id)
Expand Down
16 changes: 8 additions & 8 deletions backend/app/api/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ class PlatformSettingsUpdate(BaseModel):
@router.get("/companies", response_model=list[CompanyStats])
async def list_companies(
current_user: User = Depends(require_role("platform_admin")),
db: Any = None,
db: AsyncSession = Depends(get_db),
):
"""List all companies with stats."""
tenants = await query_dao.execute(db, select(Tenant).order_by(Tenant.created_at.desc()))
Expand Down Expand Up @@ -143,7 +143,7 @@ async def list_companies(
async def create_company(
data: CompanyCreateRequest,
current_user: User = Depends(require_role("platform_admin")),
db: Any = None,
db: AsyncSession = Depends(get_db),
):
"""Create a new company and generate an admin invitation code (max_uses=1)."""
import re
Expand Down Expand Up @@ -184,7 +184,7 @@ async def create_company(
async def toggle_company(
company_id: uuid.UUID,
current_user: User = Depends(require_role("platform_admin")),
db: Any = None,
db: AsyncSession = Depends(get_db),
):
"""Enable or disable a company."""
result = await query_dao.execute(db, select(Tenant).where(Tenant.id == company_id))
Expand Down Expand Up @@ -221,7 +221,7 @@ async def get_platform_timeseries(
start_date: datetime,
end_date: datetime,
current_user: User = Depends(require_role("platform_admin")),
db: Any = None,
db: AsyncSession = Depends(get_db),
):
"""Get daily platform metrics within a date range.

Expand Down Expand Up @@ -386,7 +386,7 @@ async def get_platform_timeseries(
@router.get("/metrics/leaderboards")
async def get_platform_leaderboards(
current_user: User = Depends(require_role("platform_admin")),
db: Any = None,
db: AsyncSession = Depends(get_db),
):
"""Get Top 20 token consuming companies and agents."""
# Top 20 Companies by total tokens
Expand Down Expand Up @@ -438,7 +438,7 @@ async def get_platform_leaderboards(
@router.get("/metrics/enhanced")
async def get_enhanced_metrics(
current_user: User = Depends(require_role("platform_admin")),
db: Any = None,
db: AsyncSession = Depends(get_db),
):
"""Enhanced platform metrics: retention, avg tokens/session,
channel distribution, tool categories, and churn warnings.
Expand Down Expand Up @@ -589,7 +589,7 @@ async def get_enhanced_metrics(
@router.get("/platform-settings", response_model=PlatformSettingsOut)
async def get_platform_settings(
current_user: User = Depends(require_role("platform_admin")),
db: Any = None,
db: AsyncSession = Depends(get_db),
):
"""Get platform-level settings."""
settings: dict[str, bool] = {}
Expand All @@ -610,7 +610,7 @@ async def get_platform_settings(
async def update_platform_settings(
data: PlatformSettingsUpdate,
current_user: User = Depends(require_role("platform_admin")),
db: Any = None,
db: AsyncSession = Depends(get_db),
):
"""Update platform-level settings."""
updates = data.model_dump(exclude_unset=True)
Expand Down
10 changes: 5 additions & 5 deletions backend/app/api/advanced.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ class InterAgentMessage(BaseModel):
async def list_collaborators(
agent_id: uuid.UUID,
current_user: User = Depends(get_current_user),
db: Any = None,
db: AsyncSession = Depends(get_db),
):
"""List agents that can collaborate with this agent."""
await check_agent_access(db, current_user, agent_id)
Expand All @@ -49,7 +49,7 @@ async def delegate_task(
agent_id: uuid.UUID,
data: DelegateRequest,
current_user: User = Depends(get_current_user),
db: Any = None,
db: AsyncSession = Depends(get_db),
):
"""Delegate a task from one agent to another."""
await check_agent_access(db, current_user, agent_id)
Expand All @@ -67,7 +67,7 @@ async def send_inter_agent_message(
agent_id: uuid.UUID,
data: InterAgentMessage,
current_user: User = Depends(get_current_user),
db: Any = None,
db: AsyncSession = Depends(get_db),
):
"""Send a message between agents."""
await check_agent_access(db, current_user, agent_id)
Expand Down Expand Up @@ -164,7 +164,7 @@ async def handover_agent(
agent_id: uuid.UUID,
data: HandoverRequest,
current_user: User = Depends(get_current_user),
db: Any = None,
db: AsyncSession = Depends(get_db),
):
"""Transfer ownership of a digital employee to another user."""
from app.models.audit import AuditLog
Expand Down Expand Up @@ -206,7 +206,7 @@ async def handover_agent(
async def get_agent_metrics(
agent_id: uuid.UUID,
current_user: User = Depends(get_current_user),
db: Any = None,
db: AsyncSession = Depends(get_db),
):
"""Get observability metrics for an agent."""
agent, _access = await check_agent_access(db, current_user, agent_id)
Expand Down
8 changes: 4 additions & 4 deletions backend/app/api/agent_credentials.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ def _to_response(cred: AgentCredential) -> dict:
async def list_credentials(
agent_id: uuid.UUID,
current_user: User = Depends(get_current_user),
db: Any = None,
db: AsyncSession = Depends(get_db),
):
"""List all credentials for an agent (sensitive data excluded)."""
# Verify the user has manage-level access to this agent
Expand All @@ -73,7 +73,7 @@ async def create_credential(
agent_id: uuid.UUID,
data: AgentCredentialCreate,
current_user: User = Depends(get_current_user),
db: Any = None,
db: AsyncSession = Depends(get_db),
):
"""Create a new credential for an agent.

Expand Down Expand Up @@ -122,7 +122,7 @@ async def update_credential(
credential_id: uuid.UUID,
data: AgentCredentialUpdate,
current_user: User = Depends(get_current_user),
db: Any = None,
db: AsyncSession = Depends(get_db),
):
"""Update an existing credential.

Expand Down Expand Up @@ -178,7 +178,7 @@ async def delete_credential(
agent_id: uuid.UUID,
credential_id: uuid.UUID,
current_user: User = Depends(get_current_user),
db: Any = None,
db: AsyncSession = Depends(get_db),
):
"""Delete a credential."""
_agent, access_level = await check_agent_access(db, current_user, agent_id)
Expand Down
Loading