Skip to content
Open
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
78 changes: 43 additions & 35 deletions backend/app/api/agents.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,56 +138,66 @@ async def list_agents(
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""List all agents the current user has access to."""
# platform_admin & org_admin see all agents (optionally filtered by tenant)
if current_user.role in ("platform_admin", "org_admin"):
stmt = select(Agent)
if tenant_id:
stmt = stmt.where(Agent.tenant_id == tenant_id)
result = await db.execute(stmt.order_by(Agent.created_at.desc()))
agents = result.scalars().all()
# Lazy reset token counters
needs_flush = False
for a in agents:
if await _lazy_reset_token_counters(a, db):
needs_flush = True
if needs_flush:
await db.commit()
return [AgentOut.model_validate(a) for a in agents]

# agent_admin sees their own created agents + permitted
# member sees only permitted
# All scoped to user's tenant
"""List all agents the current user has access to.

All users (including admins) only see agents they have permission to access.
Agents with scope_type='user' and scope_id=creator are only visible to the creator.
"""
user_tenant = current_user.tenant_id

# Get agents user created (within their tenant)
created = select(Agent).where(Agent.creator_id == current_user.id, Agent.tenant_id == user_tenant)
# Build the permission filter:
# 1. Agents the user created
# 2. Agents with scope_type='company' (visible to all in tenant)
# 3. Agents with scope_type='user' where user is in scope_ids
created = select(Agent).where(
Agent.creator_id == current_user.id,
Agent.tenant_id == user_tenant
)

# Get agents user has permission to (within their tenant)
permitted_ids = (
# Get agents with company-wide visibility
company_ids = (
select(AgentPermission.agent_id)
.where(
(AgentPermission.scope_type == "company")
| ((AgentPermission.scope_type == "user") & (AgentPermission.scope_id == current_user.id))
AgentPermission.scope_type == "company",
AgentPermission.agent_id.in_(
select(Agent.id).where(Agent.tenant_id == user_tenant)
)
)
)
permitted = select(Agent).where(Agent.id.in_(permitted_ids), Agent.tenant_id == user_tenant)

# Union
from sqlalchemy import union_all

combined = union_all(created, permitted).subquery()
result = await db.execute(
select(Agent).where(Agent.id.in_(select(combined.c.id))).order_by(Agent.created_at.desc())
# Get agents with user-specific visibility where user is in scope
user_permitted_ids = (
select(AgentPermission.agent_id)
.where(
(AgentPermission.scope_type == "user") & (AgentPermission.scope_id == current_user.id)
)
)

# Combine: created OR company-wide OR user-permitted
from sqlalchemy import or_, and_

query = select(Agent).where(
and_(
Agent.tenant_id == user_tenant,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve tenant_id override for platform admins

The new list_agents query now hard-codes tenant scoping to current_user.tenant_id and never reads the tenant_id query parameter, which regresses admin behavior from the previous implementation. In practice, a platform admin can no longer list another tenant’s agents via /agents?tenant_id=..., and a platform admin operating in global mode (no tenant bound in token, which other endpoints explicitly support) will be filtered to tenant_id IS NULL and miss normal tenant agents entirely.

Useful? React with 👍 / 👎.

or_(
Agent.creator_id == current_user.id,
Agent.id.in_(company_ids),
Agent.id.in_(user_permitted_ids)
)
)
).order_by(Agent.created_at.desc())

result = await db.execute(query)
agents = result.scalars().all()

# Lazy reset token counters
needs_flush = False
for a in agents:
if await _lazy_reset_token_counters(a, db):
needs_flush = True
if needs_flush:
await db.commit()

return [AgentOut.model_validate(a) for a in agents]


Expand Down Expand Up @@ -694,7 +704,6 @@ async def stop_agent(

# ─── Agent-Level Approvals ──────────────────────────────


@router.get("/{agent_id}/approvals")
async def list_agent_approvals(
agent_id: uuid.UUID,
Expand Down Expand Up @@ -758,7 +767,6 @@ async def resolve_agent_approval(

# ─── OpenClaw API Key Management ────────────────────────


@router.post("/{agent_id}/api-key")
async def generate_or_reset_api_key(
agent_id: uuid.UUID,
Expand Down